diff --git a/.gitignore b/.gitignore index fcf8682c7..345f311b5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,9 @@ O.*/ .cproject external/ -ci/coverage.info +ci/coverage*.info ci/html/ +.iocsh_history # Files produced by testing suite invalid_points.xml diff --git a/ADApp/Db/NDPluginBase.template b/ADApp/Db/NDPluginBase.template index 70bace4e1..ab6e7a216 100644 --- a/ADApp/Db/NDPluginBase.template +++ b/ADApp/Db/NDPluginBase.template @@ -18,6 +18,27 @@ record(stringin, "$(P)$(R)PluginType_RBV") field(SCAN, "I/O Intr") } +################################################################### +# This plugin's EPICS record prefix $(P)$(R), pushed into the # +# driver at init so it can be advertised in the plugin inventory. # +################################################################### +record(stringout, "$(P)$(R)PvPrefix") +{ + field(PINI, "YES") + field(DTYP, "asynOctetWrite") + field(OUT, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))PLUGIN_PV_PREFIX") + field(VAL, "$(P)$(R)") + # Written once at init via PINI; reject external puts thereafter + field(DISP, "1") +} + +record(stringin, "$(P)$(R)PvPrefix_RBV") +{ + field(DTYP, "asynOctetRead") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))PLUGIN_PV_PREFIX") + field(SCAN, "I/O Intr") +} + ################################################################### # These records control the connection of the server to # # an NDArray driver port and address # diff --git a/ADApp/Db/NDPluginInventory.template b/ADApp/Db/NDPluginInventory.template new file mode 100644 index 000000000..a3a4a27ce --- /dev/null +++ b/ADApp/Db/NDPluginInventory.template @@ -0,0 +1,16 @@ +#=================================================================# +# Template file: NDPluginInventory.template +# Configures the IOC-wide PVA channel that advertises all plugins. +# Load once per IOC against the NDPluginInventoryConfigure() port. +#=================================================================# + +# Name of the IOC-wide PVA channel that publishes the plugin inventory +record(waveform, "$(P)PluginInventoryName_RBV") +{ + field(DTYP, "asynOctetRead") + field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=1))PV_NAME") + field(FTVL, "CHAR") + field(NELM, "256") + field(SCAN, "I/O Intr") + info(Q:form, "String") +} diff --git a/ADApp/pluginSrc/Makefile b/ADApp/pluginSrc/Makefile index 8687f5d67..5318b496b 100644 --- a/ADApp/pluginSrc/Makefile +++ b/ADApp/pluginSrc/Makefile @@ -25,6 +25,13 @@ USR_CPPFLAGS += -DBUILDING_NDPlugin_API INC += NDPluginAPI.h INC += NDPluginDriver.h LIB_SRCS += NDPluginDriver.cpp +ifeq ($(WITH_PLUGIN_INVENTORY), YES) + INC += NDPluginInventory.h + LIB_SRCS += NDPluginInventory.cpp + NDPluginInventory_CXXFLAGS_Linux += -std=c++11 + NDPluginSupport_DBD += NDPluginInventory.dbd + USR_CXXFLAGS += -DWITH_PLUGIN_INVENTORY +endif LIB_SRCS += throttler.cpp NDPluginSupport_DBD += NDPluginAttribute.dbd @@ -188,6 +195,7 @@ ifeq ($(WITH_PVA), YES) DBD += NDPluginPva.dbd INC += NDPluginPva.h LIB_SRCS += NDPluginPva.cpp + USR_CXXFLAGS += -DWITH_PVA endif ifeq ($(WITH_PVXS), YES) @@ -195,6 +203,7 @@ ifeq ($(WITH_PVXS), YES) INC += NDPluginPvxs.h LIB_SRCS += NDPluginPvxs.cpp NDPluginPvxs_CXXFLAGS_Linux += -std=c++11 + USR_CXXFLAGS += -DWITH_PVXS endif ifeq ($(WITH_BLOSC), YES) diff --git a/ADApp/pluginSrc/NDFileHDF5.cpp b/ADApp/pluginSrc/NDFileHDF5.cpp index 2b4d89b88..41f755a24 100644 --- a/ADApp/pluginSrc/NDFileHDF5.cpp +++ b/ADApp/pluginSrc/NDFileHDF5.cpp @@ -2384,12 +2384,13 @@ NDFileHDF5::NDFileHDF5(const char *portName, int queueSize, int blockingCallback } /* Set the plugin type string */ + setStringParam(NDPluginDriverPluginType, "NDFileHDF5"); + /* Report the HDF5 library version through the SDK version parameter */ unsigned majnum=0, minnum=0, relnum=0; H5get_libversion( &majnum, &minnum, &relnum ); - char* plugin_type = (char*)calloc(40, sizeof(char)); - epicsSnprintf(plugin_type, 40, "NDFileHDF5 ver%d.%d.%d", majnum, minnum, relnum); - //printf("plugin type and version: %s\n", plugin_type ); - setStringParam(NDPluginDriverPluginType, plugin_type); + char sdk_version[40] = {0}; + epicsSnprintf(sdk_version, sizeof(sdk_version), "%d.%d.%d", majnum, minnum, relnum); + setStringParam(ADSDKVersion, sdk_version); this->supportsMultipleArrays = 1; this->pAttributeId = NULL; this->pFileAttributes = new NDAttributeList; diff --git a/ADApp/pluginSrc/NDPluginDriver.cpp b/ADApp/pluginSrc/NDPluginDriver.cpp index 211315c84..9a10ee5b4 100644 --- a/ADApp/pluginSrc/NDPluginDriver.cpp +++ b/ADApp/pluginSrc/NDPluginDriver.cpp @@ -17,6 +17,9 @@ #include #include "NDPluginDriver.h" +#ifdef WITH_PLUGIN_INVENTORY +#include "NDPluginInventory.h" +#endif #include "throttler.h" #include @@ -142,6 +145,12 @@ NDPluginDriver::NDPluginDriver(const char *portName, int queueSize, int blocking createParam(NDPluginDriverExecutionTimeString, asynParamFloat64, &NDPluginDriverExecutionTime); createParam(NDPluginDriverMinCallbackTimeString, asynParamFloat64, &NDPluginDriverMinCallbackTime); createParam(NDPluginDriverMaxByteRateString, asynParamFloat64, &NDPluginDriverMaxByteRate); + createParam(NDPluginDriverPvPrefixString, asynParamOctet, &NDPluginDriverPvPrefix); + +#ifdef WITH_PLUGIN_INVENTORY + // If enabled, register this plugin in the process-wide inventory published over PVAccess. + NDPluginInventory::registerPlugin(portName); +#endif /* Here we set the values of read-only parameters and of read/write parameters that cannot * or should not get their values from the database. Note that values set here will override @@ -195,6 +204,11 @@ NDPluginDriver::~NDPluginDriver() if (pToThreadMsgQ_) shutdownPortDriver(); +#ifdef WITH_PLUGIN_INVENTORY + // Remove the plugin from the inventory on destruction. + NDPluginInventory::unregisterPlugin(portName); +#endif + delete throttler_; } @@ -835,6 +849,11 @@ asynStatus NDPluginDriver::writeOctet(asynUser *pasynUser, const char *value, this->unlock(); connectToArrayPort(); this->lock(); + } else if (function == NDPluginDriverPvPrefix) { +#ifdef WITH_PLUGIN_INVENTORY + // If we are using the plugin inventory then set the PV prefix in it as well + NDPluginInventory::setPvPrefix(portName, value); +#endif } else { /* If this parameter belongs to a base class call its method */ if (function < FIRST_NDPLUGIN_PARAM) @@ -902,6 +921,16 @@ asynStatus NDPluginDriver::start(void) assert(!this->pluginStarted_); //static const char *functionName = "start"; +#ifdef WITH_PLUGIN_INVENTORY + // The derived class is now fully constructed, so its plugin type is set. + // Register the plugin type with the plugin inventory. + char pluginType[256] = {0}; + this->lock(); + getStringParam(NDPluginDriverPluginType, sizeof(pluginType), pluginType); + this->unlock(); + NDPluginInventory::setPluginType(portName, pluginType); +#endif + this->pluginStarted_ = true; // If the plugin was started with BlockingCallbacks=Yes then pThreads_.size() will be 0 if (pThreads_.size() == 0) return asynSuccess; diff --git a/ADApp/pluginSrc/NDPluginDriver.h b/ADApp/pluginSrc/NDPluginDriver.h index 935f4d61a..ac4933b83 100644 --- a/ADApp/pluginSrc/NDPluginDriver.h +++ b/ADApp/pluginSrc/NDPluginDriver.h @@ -53,6 +53,7 @@ class sortedListElement { #define NDPluginDriverMinCallbackTimeString "MIN_CALLBACK_TIME" /**< (asynFloat64, r/w) Minimum time between calling processCallbacks *to execute plugin code */ #define NDPluginDriverMaxByteRateString "MAX_BYTE_RATE" /**< (asynFloat64, r/w) Limit on byte rate output of plugin */ +#define NDPluginDriverPvPrefixString "PLUGIN_PV_PREFIX" /**< (asynOctet, r/w) This plugin's EPICS record prefix $(P)$(R) */ /** Class from which actual plugin drivers are derived; derived from asynNDArrayDriver */ class NDPLUGIN_API NDPluginDriver : public asynNDArrayDriver, public epicsThreadRunable { public: @@ -107,6 +108,7 @@ class NDPLUGIN_API NDPluginDriver : public asynNDArrayDriver, public epicsThread int NDPluginDriverExecutionTime; int NDPluginDriverMinCallbackTime; int NDPluginDriverMaxByteRate; + int NDPluginDriverPvPrefix; NDArray *pPrevInputArray_; bool throttled(NDArray *pArray); diff --git a/ADApp/pluginSrc/NDPluginInventory.cpp b/ADApp/pluginSrc/NDPluginInventory.cpp new file mode 100644 index 000000000..a19249754 --- /dev/null +++ b/ADApp/pluginSrc/NDPluginInventory.cpp @@ -0,0 +1,366 @@ +/* + * NDPluginInventory.cpp + * + * Process-wide registry of NDPlugin instances, published as a single PVAccess + * structure keyed by plugin type. See NDPluginInventory.h for details. + * + * The PVA backend is selected at compile time: PVXS is preferred, falling back + * to pvDatabase/pvData. If neither WITH_PVXS nor WITH_PVA is defined the + * registry still works but publish() is a no-op. + */ + +#include + +#include +#include +#include +#include +#include + +#include "NDPluginInventory.h" + +#include + +#if defined(WITH_PVXS) + #include + #include + #include + #include +#elif defined(WITH_PVA) + #include + #include + #include +#endif + +namespace { + +using NDPluginInventory::RegisteredPlugin; + +// Mutex to protect updates to the plugin registry list. +epicsMutex& mutex() { + static epicsMutex m; + return m; +} + +// Vector storing all registered plugins. Access to this vector must be protected by the mutex. +// Plugins are initially added with just port names at construction, and then their +// type and PV prefix are populated later via setter functions. At iocInit() this +// vector is restructured into a map keyed by plugin type and published as a single PVAccess structure. +std::vector& registry() { + static std::vector r; + return r; +} + + +/** + * Search for a plugin in the registry by its port name. + * + * \param[in] portName The asyn port name of the plugin to find. + * \return An iterator to the plugin if found, or registry().end() if not found + */ +std::vector::iterator findPlugin(const std::string& portName) +{ + std::vector& regs = registry(); + std::vector::iterator it = regs.begin(); + for (; it != regs.end(); ++it) { + if (it->portName == portName) break; + } + return it; +} + +/** + * Convert a plugin type string into a valid structure field name. + * + * \param[in] type The plugin type string to convert. + * \return A valid structure field name derived from the plugin type string. + */ +std::string toFieldName(const std::string& type) +{ + std::string name = type; + for (size_t i = 0; i < name.size(); ++i) { + char c = name[i]; + bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_'; + if (!ok) name[i] = '_'; + } + if (name.empty() || (name[0] >= '0' && name[0] <= '9')) name = "_" + name; + return name; +} + +} // anonymous namespace + +namespace NDPluginInventory { + +/** + * Register a plugin in the process-wide inventory. + * + * \param[in] portName The asyn port name of the plugin to register. + */ +void registerPlugin(const std::string& portName) +{ + epicsGuard guard(mutex()); + if (findPlugin(portName) == registry().end()) { + RegisteredPlugin plugin; + plugin.portName = portName; + registry().push_back(plugin); + } +} + +/** + * Unregister a plugin from the process-wide inventory. + * + * \param[in] portName The asyn port name of the plugin to unregister. + */ +void unregisterPlugin(const std::string& portName) +{ + epicsGuard guard(mutex()); + std::vector::iterator it = findPlugin(portName); + if (it != registry().end()) { + registry().erase(it); + } +} + + +/** + * Set the plugin type for a registered plugin. + * + * \param[in] portName The asyn port name of the plugin to update. + * \param[in] pluginType The type of the plugin to set. + */ +void setPluginType(const std::string& portName, const std::string& pluginType) +{ + epicsGuard guard(mutex()); + std::vector::iterator it = findPlugin(portName); + if (it != registry().end()) { + it->pluginType = pluginType; + } +} + +/** + * Set the PV prefix for a registered plugin. + * + * \param[in] portName The asyn port name of the plugin to update. + * \param[in] pvPrefix The PV prefix to set for the plugin. + */ +void setPvPrefix(const std::string& portName, const std::string& pvPrefix) +{ + epicsGuard guard(mutex()); + std::vector::iterator it = findPlugin(portName); + if (it != registry().end()) { + it->pvPrefix = pvPrefix; + } +} + +/** + * Given the list of registered plugins, construct a mapping of plugin types to their corresponding plugins. + * + * \return An inventory mapping plugin types to lists of registered plugins. + */ +Inventory getInventory() +{ + epicsGuard guard(mutex()); + Inventory inventory; + std::vector& regs = registry(); + for (std::vector::const_iterator it = regs.begin(); it != regs.end(); ++it) { + RegisteredPlugin plugin = *it; + if (plugin.pluginType.empty()) plugin.pluginType = "Unknown"; + inventory[plugin.pluginType].push_back(plugin); + } + return inventory; +} + +} // namespace NDPluginInventory + +namespace { + +#if defined(WITH_PVA) && !defined(WITH_PVXS) + +class InventoryRecord; +typedef std::tr1::shared_ptr InventoryRecordPtr; + +// Minimal PVRecord wrapper around a pre-built PVStructure. +class InventoryRecord : public epics::pvDatabase::PVRecord { +public: + static InventoryRecordPtr create(const std::string& name, + epics::pvData::PVStructurePtr const & pvStructure) + { + InventoryRecordPtr rec(new InventoryRecord(name, pvStructure)); + if (!rec->init()) rec.reset(); + return rec; + } + virtual bool init() { initPVRecord(); return true; } + virtual void process() {} +private: + InventoryRecord(const std::string& name, epics::pvData::PVStructurePtr const & pvStructure) + : epics::pvDatabase::PVRecord(name, pvStructure) {} +}; + +#endif + +class PluginInventory : public asynPortDriver { +public: + PluginInventory(const char *portName, const char *pvName) + : asynPortDriver(portName, 1, + asynOctetMask | asynDrvUserMask, asynOctetMask, + 0, 1, 0, 0), + channelName_(pvName ? pvName : "") + { + createParam(NDPluginInventoryPvNameString, asynParamOctet, &pvName_); + setStringParam(pvName_, channelName_.c_str()); +#if defined(WITH_PVXS) + pv_ = pvxs::server::SharedPV::buildReadonly(); + added_ = false; +#elif defined(WITH_PVA) + // Register the pvDatabase "local" provider now, at config time, so the PVA + // server includes it when it reads EPICS_PVAS_PROVIDER_NAMES during iocInit. + // Without this the provider is first created at publish() (after iocInit) and + // the server never serves the inventory channel. + epics::pvDatabase::getChannelProviderLocal(); +#endif + theInstance_ = this; + initHookRegister(&PluginInventory::initHook); + } + + // Builds the inventory structure and (re)serves it on the configured channel. + void publish() + { + if (channelName_.empty()) return; + NDPluginInventory::Inventory inventory = NDPluginInventory::getInventory(); +#if defined(WITH_PVXS) || defined(WITH_PVA) + serve(inventory); +#else + (void)inventory; +#endif + } + +private: + static void initHook(initHookState state) + { + if (state == initHookAfterIocRunning && theInstance_) { + theInstance_->publish(); + } + } + +#if defined(WITH_PVXS) + void serve(const NDPluginInventory::Inventory& inventory) + { + using NDPluginInventory::RegisteredPlugin; + + // One member per plugin type, each a struct array of {portName, pvPrefix}. + std::vector children; + for (NDPluginInventory::Inventory::const_iterator it = inventory.begin(); + it != inventory.end(); ++it) { + std::vector row; + row.push_back(pvxs::members::String("portName")); + row.push_back(pvxs::members::String("pvPrefix")); + children.push_back(pvxs::members::StructA(toFieldName(it->first), row)); + } + + pvxs::TypeDef def(pvxs::TypeCode::Struct, "epics:nt/NTTable:1.0", children); + pvxs::Value root = def.create(); + + for (NDPluginInventory::Inventory::const_iterator it = inventory.begin(); + it != inventory.end(); ++it) { + const std::string field = toFieldName(it->first); + const std::vector& plugins = it->second; + pvxs::shared_array arr(plugins.size()); + for (size_t i = 0; i < plugins.size(); ++i) { + pvxs::Value elem = root[field].allocMember(); + elem["portName"] = plugins[i].portName; + elem["pvPrefix"] = plugins[i].pvPrefix; + arr[i] = elem; + } + root[field] = arr.freeze(); + } + + if (!added_) { + pvxs::ioc::server().addPV(channelName_, pv_); + added_ = true; + } + if (pv_.isOpen()) { + pv_.close(); + } + pv_.open(root); + } +#elif defined(WITH_PVA) + void serve(const NDPluginInventory::Inventory& inventory) + { + using namespace epics::pvData; + using NDPluginInventory::RegisteredPlugin; + + FieldBuilderPtr fb = getFieldCreate()->createFieldBuilder(); + for (NDPluginInventory::Inventory::const_iterator it = inventory.begin(); + it != inventory.end(); ++it) { + fb->addNestedStructureArray(toFieldName(it->first)) + ->add("portName", pvString) + ->add("pvPrefix", pvString) + ->endNested(); + } + StructureConstPtr structure = fb->createStructure(); + PVStructurePtr pvStructure = getPVDataCreate()->createPVStructure(structure); + + for (NDPluginInventory::Inventory::const_iterator it = inventory.begin(); + it != inventory.end(); ++it) { + const std::vector& plugins = it->second; + PVStructureArrayPtr array = pvStructure->getSubField(toFieldName(it->first)); + StructureConstPtr elemType = array->getStructureArray()->getStructure(); + PVStructureArray::svector vec(plugins.size()); + for (size_t i = 0; i < plugins.size(); ++i) { + PVStructurePtr elem = getPVDataCreate()->createPVStructure(elemType); + elem->getSubField("portName")->put(plugins[i].portName); + elem->getSubField("pvPrefix")->put(plugins[i].pvPrefix); + vec[i] = elem; + } + array->replace(freeze(vec)); + } + + epics::pvDatabase::PVDatabasePtr master = epics::pvDatabase::PVDatabase::getMaster(); + epics::pvDatabase::getChannelProviderLocal(); + if (master->findRecord(channelName_)) { + master->removeRecord(master->findRecord(channelName_)); + } + InventoryRecordPtr record = InventoryRecord::create(channelName_, pvStructure); + if (record) { + master->addRecord(record); + } + } +#endif + + int pvName_; + std::string channelName_; +#if defined(WITH_PVXS) + pvxs::server::SharedPV pv_; + bool added_; +#endif + static PluginInventory* theInstance_; +}; + +PluginInventory* PluginInventory::theInstance_ = NULL; + +} // anonymous namespace + + +// IOC shell registration for the plugin inventory. +extern "C" int NDPluginInventoryConfigure(const char *portName, const char *pvName) +{ + new PluginInventory(portName, pvName); + return asynSuccess; +} + +static const iocshArg initArg0 = { "portName", iocshArgString }; +static const iocshArg initArg1 = { "pvName", iocshArgString }; +static const iocshArg * const initArgs[] = { &initArg0, &initArg1 }; +static const iocshFuncDef initFuncDef = { "NDPluginInventoryConfigure", 2, initArgs }; +static void initCallFunc(const iocshArgBuf *args) +{ + NDPluginInventoryConfigure(args[0].sval, args[1].sval); +} + +extern "C" void NDPluginInventoryRegister(void) +{ + iocshRegister(&initFuncDef, initCallFunc); +} + +extern "C" { +epicsExportRegistrar(NDPluginInventoryRegister); +} diff --git a/ADApp/pluginSrc/NDPluginInventory.dbd b/ADApp/pluginSrc/NDPluginInventory.dbd new file mode 100644 index 000000000..a21bab769 --- /dev/null +++ b/ADApp/pluginSrc/NDPluginInventory.dbd @@ -0,0 +1 @@ +registrar(NDPluginInventoryRegister) diff --git a/ADApp/pluginSrc/NDPluginInventory.h b/ADApp/pluginSrc/NDPluginInventory.h new file mode 100644 index 000000000..b6880aaad --- /dev/null +++ b/ADApp/pluginSrc/NDPluginInventory.h @@ -0,0 +1,45 @@ +#ifndef NDPluginInventory_H +#define NDPluginInventory_H + +#include +#include +#include + +#include + +/** Process-wide registry of NDPlugin instances. + * + * Every NDPluginDriver registers itself here at construction; its plugin type + * and EPICS PV prefix are filled in via setters as they become known. The + * collected information (asyn port name, EPICS PV prefix and plugin type) is + * published once, when the IOC reaches the running state, as a single PVAccess + * structure keyed by plugin type. This lets clients discover which plugins exist + * and with what EPICS PV prefix they are associated, + * without any knowledge of the (site specific) IOC startup configuration. + */ +namespace NDPluginInventory { + +/** asyn parameter string for the inventory port's PVA channel name. */ +#define NDPluginInventoryPvNameString "PV_NAME" + +/** Information about a single registered plugin instance. */ +struct RegisteredPlugin { + std::string portName; /**< asyn port name, e.g. "ROI1" */ + std::string pluginType; /**< plugin class name, e.g. "NDPluginROI" */ + std::string pvPrefix; /**< EPICS record prefix $(P)$(R), e.g. "13SIM1:ROI1:" */ +}; + +/** map of plugin type strings to lists of registered plugins of that type */ +typedef std::map> Inventory; + +NDPLUGIN_API void registerPlugin(const std::string& portName); +NDPLUGIN_API void unregisterPlugin(const std::string& portName); +NDPLUGIN_API void setPluginType(const std::string& portName, const std::string& pluginType); +NDPLUGIN_API void setPvPrefix(const std::string& portName, const std::string& pvPrefix); + +/** Returns the current inventory grouped by plugin type. */ +NDPLUGIN_API Inventory getInventory(); + +} // namespace NDPluginInventory + +#endif diff --git a/ADApp/pluginTests/Makefile b/ADApp/pluginTests/Makefile index b3c33b085..0fab3e5d5 100644 --- a/ADApp/pluginTests/Makefile +++ b/ADApp/pluginTests/Makefile @@ -48,6 +48,11 @@ ifeq ($(WITH_BOOST),YES) PROD_IOC_WIN32 += plugin-test plugin-test_SRCS += plugin-test.cpp plugin-test_SRCS += test_NDPluginCircularBuff.cpp + ifeq ($(WITH_PLUGIN_INVENTORY),YES) + ifeq ($(WITH_HDF5),YES) + plugin-test_SRCS += test_NDPluginInventory.cpp + endif + endif ifeq ($(WITH_HDF5),YES) plugin-test_SRCS += test_NDFileHDF5.cpp plugin-test_SRCS += test_NDFileHDF5AttributeDataset.cpp diff --git a/ADApp/pluginTests/test_NDPluginInventory.cpp b/ADApp/pluginTests/test_NDPluginInventory.cpp new file mode 100644 index 000000000..1386095b5 --- /dev/null +++ b/ADApp/pluginTests/test_NDPluginInventory.cpp @@ -0,0 +1,142 @@ +/* + * test_NDPluginInventory.cpp + * + * Created on: 15 Sep 2026 + * Author: Jakub Wlodek + */ +#include +#include + +#include "boost/test/unit_test.hpp" + +#include +#include +#include + +#include "testingutilities.h" +#include "CodecPluginWrapper.h" +#include "HDF5PluginWrapper.h" +#include "ROIPluginWrapper.h" + +using namespace std; + +struct PluginInventoryTestFixture +{ + asynNDArrayDriver *dummy_driver; + vector codecs; + HDF5PluginWrapper *hdf; + vector rois; + vector codecPorts; + string hdfPort; + vector roiPorts; + + /** Create a dummy port driver with two codec, one HDF5 and, four ROI plugins. */ + PluginInventoryTestFixture() + { + string dummy_port("simInvPort"); + uniqueAsynPortName(dummy_port); + + dummy_driver = new asynNDArrayDriver(dummy_port.c_str(), 1, 0, 0, + asynGenericPointerMask, asynGenericPointerMask, 0, 0, 0, 0); + + // Two codec plugins. + for (int i = 0; i < 2; i++) { + string port("invCodec"); + uniqueAsynPortName(port); + codecPorts.push_back(port); + CodecPluginWrapper *codec = new CodecPluginWrapper(port, dummy_port); + // Plugin type is set in start() + codec->start(); + // PV prefixes are set in writeOctet() when the PvPrefix record PINI is processed. + codec->write(NDPluginDriverPvPrefixString, "TEST:CODEC" + to_string(i + 1) + ":"); + codecs.push_back(codec); + } + + // One HDF5 plugin. + hdfPort = "invHDF"; + uniqueAsynPortName(hdfPort); + hdf = new HDF5PluginWrapper(hdfPort, dummy_port); + hdf->start(); + hdf->write(NDPluginDriverPvPrefixString, "TEST:HDF:"); + + // Four ROI plugins. + for (int i = 0; i < 4; i++) { + string port("invROI"); + uniqueAsynPortName(port); + roiPorts.push_back(port); + ROIPluginWrapper *roi = new ROIPluginWrapper(port, dummy_port); + roi->start(); + roi->write(NDPluginDriverPvPrefixString, "TEST:ROI" + to_string(i + 1) + ":"); + rois.push_back(roi); + } + } + + ~PluginInventoryTestFixture() + { + for (size_t i = 0; i < codecs.size(); i++) delete codecs[i]; + delete hdf; + for (size_t i = 0; i < rois.size(); i++) delete rois[i]; + delete dummy_driver; + } + + // Finds the plugin for a given port within a type's plugin list. + const NDPluginInventory::RegisteredPlugin *findPlugin( + const vector &plugins, + const string &portName) + { + for (size_t i = 0; i < plugins.size(); i++) { + if (plugins[i].portName == portName) return &plugins[i]; + } + return NULL; + } +}; + +BOOST_FIXTURE_TEST_SUITE(PluginInventoryTests, PluginInventoryTestFixture) + +// Plugins group under their plugin type, carrying port name and PV prefix. +BOOST_AUTO_TEST_CASE(test_InventoryKeyedByType) +{ + NDPluginInventory::Inventory inv = NDPluginInventory::getInventory(); + + NDPluginInventory::Inventory::const_iterator codecIt = inv.find("NDPluginCodec"); + BOOST_REQUIRE(codecIt != inv.end()); + BOOST_CHECK_EQUAL(codecIt->second.size(), 2u); + for (size_t i = 0; i < codecPorts.size(); i++) { + const NDPluginInventory::RegisteredPlugin *e = findPlugin(codecIt->second, codecPorts[i]); + BOOST_REQUIRE(e != NULL); + BOOST_CHECK_EQUAL(e->pvPrefix, "TEST:CODEC" + to_string(i + 1) + ":"); + } + + NDPluginInventory::Inventory::const_iterator hdfIt = inv.find("NDFileHDF5"); + BOOST_REQUIRE(hdfIt != inv.end()); + BOOST_CHECK_EQUAL(hdfIt->second.size(), 1u); + const NDPluginInventory::RegisteredPlugin *hdfEntry = findPlugin(hdfIt->second, hdfPort); + BOOST_REQUIRE(hdfEntry != NULL); + BOOST_CHECK_EQUAL(hdfEntry->pvPrefix, "TEST:HDF:"); + + NDPluginInventory::Inventory::const_iterator roiIt = inv.find("NDPluginROI"); + BOOST_REQUIRE(roiIt != inv.end()); + BOOST_CHECK_EQUAL(roiIt->second.size(), 4u); + for (size_t i = 0; i < roiPorts.size(); i++) { + const NDPluginInventory::RegisteredPlugin *e = findPlugin(roiIt->second, roiPorts[i]); + BOOST_REQUIRE(e != NULL); + BOOST_CHECK_EQUAL(e->pvPrefix, "TEST:ROI" + to_string(i + 1) + ":"); + } +} + +// A plugin is removed from the inventory when it is destroyed. +BOOST_AUTO_TEST_CASE(test_UnregisterOnDelete) +{ + delete rois[3]; + rois[3] = NULL; + + NDPluginInventory::Inventory inv = NDPluginInventory::getInventory(); + NDPluginInventory::Inventory::const_iterator it = inv.find("NDPluginROI"); + BOOST_REQUIRE(it != inv.end()); + + BOOST_CHECK_EQUAL(it->second.size(), 3u); + BOOST_CHECK(findPlugin(it->second, roiPorts[3]) == NULL); + BOOST_CHECK(findPlugin(it->second, roiPorts[0]) != NULL); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/iocBoot/EXAMPLE_commonPlugins.cmd b/iocBoot/EXAMPLE_commonPlugins.cmd index 56383cba1..a045b7565 100644 --- a/iocBoot/EXAMPLE_commonPlugins.cmd +++ b/iocBoot/EXAMPLE_commonPlugins.cmd @@ -173,6 +173,11 @@ save_restoreSet_status_prefix("$(PREFIX)") dbLoadRecords("$(AUTOSAVE)/db/save_restoreStatus.db", "P=$(PREFIX)") dbLoadRecords("$(AUTOSAVE)/db/configMenu.db", "P=$(PREFIX), CONFIG=ADAutoSave") +# Create the process-wide plugin inventory, advertised as a single PVA channel +# ($(PREFIX)Plugins_RBV) listing every plugin keyed by plugin type. +NDPluginInventoryConfigure("PINV1", "$(PREFIX)Plugins_RBV") +dbLoadRecords("NDPluginInventory.template", "P=$(PREFIX),PORT=PINV1,ADDR=0,TIMEOUT=1") + # Load NDPluginPva plugin NDPvaConfigure("PVA1", $(QSIZE), 0, "$(PORT)", 0, $(PREFIX)Pva1:Image, 0, 0, 0) dbLoadRecords("NDPva.template", "P=$(PREFIX),R=Pva1:, PORT=PVA1,ADDR=0,TIMEOUT=1,NDARRAY_PORT=$(PORT)")