Newer
Older
Martin Christoph Hierholzer
committed
/*
* DeviceModule.h
*
* Created on: Jun 27, 2016
* Author: Martin Hierholzer
*/
#ifndef CHIMERATK_DEVICE_MODULE_H
#define CHIMERATK_DEVICE_MODULE_H
Martin Christoph Hierholzer
committed
#include "Module.h"
#include "ScalarAccessor.h"
#include "VariableGroup.h"
#include "VariableNetworkNode.h"
#include "VirtualModule.h"
Martin Killenberg
committed
#include "RecoveryHelper.h"
#include <ChimeraTK/ForwardDeclarations.h>
#include <ChimeraTK/RegisterPath.h>
#include <ChimeraTK/Device.h>
Martin Christoph Hierholzer
committed
#include "ModuleGroup.h"
Martin Christoph Hierholzer
committed
#include "StatusAccessor.h"
#include <boost/thread/latch.hpp>
Martin Christoph Hierholzer
committed
namespace ChimeraTK {
class DeviceModule;
struct ServerHistory;
}
/*********************************************************************************************************************/
namespace detail {
struct DeviceModuleProxy : Module {
DeviceModuleProxy(const DeviceModule& owner, const std::string& registerNamePrefix);
DeviceModuleProxy(DeviceModuleProxy&& other);
DeviceModuleProxy() {}
VariableNetworkNode operator()(const std::string& registerName, UpdateMode mode,
const std::type_info& valueType = typeid(AnyType), size_t nElements = 0) const;
VariableNetworkNode operator()(const std::string& registerName, const std::type_info& valueType,
size_t nElements = 0, UpdateMode mode = UpdateMode::poll) const;
VariableNetworkNode operator()(const std::string& variableName) const override;
Module& operator[](const std::string& moduleName) const override;
const Module& virtualise() const override;
void connectTo(const Module& target, VariableNetworkNode trigger = {}) const override;
ModuleType getModuleType() const override { return ModuleType::Device; }
DeviceModuleProxy& operator=(DeviceModuleProxy&& other);
private:
friend class ChimeraTK::DeviceModule;
const DeviceModule* _myowner;
std::string _registerNamePrefix;
};
} // namespace detail
/*********************************************************************************************************************/
/** Implementes access to a ChimeraTK::Device.
*/
class DeviceModule : public Module {
public:
Martin Christoph Hierholzer
committed
/**
* Create (non-connecting) DeviceModule
*
* The device represented by this DeviceModule is identified by either the device alias found in the DMAP file or
* directly a CDD.
*
* A callback function to initialise the device can be registered as an optional argument (see
* addInitialisationHandler() for more information).
*
* Connecting the device to other modules is up to the user, hence using this class directly is discouraged in
* new applications. Instead use the ConnectingDeviceModule.
*/
DeviceModule(Application* application, const std::string& deviceAliasOrCDD,
std::function<void(DeviceModule*)> initialisationHandler = nullptr);
/** Destructor */
virtual ~DeviceModule();
/** Move operation with the move constructor */
DeviceModule(DeviceModule&& other) {
operator=(std::move(other));
}
/** Move assignment */
DeviceModule& operator=(DeviceModule&& other) {
assert(!moduleThread.joinable());
assert(other.isHoldingInitialValueLatch);
if(owner) owner->unregisterDeviceModule(this);
device = std::move(other.device);
deviceAliasOrURI = std::move(other.deviceAliasOrURI);
registerNamePrefix = std::move(other.registerNamePrefix);
deviceError = std::move(other.deviceError);
owner = other.owner;
proxies = std::move(other.proxies);
Martin Christoph Hierholzer
committed
deviceHasError = other.deviceHasError;
for(auto& proxy : proxies) proxy.second._myowner = this;
owner->registerDeviceModule(this);
return *this;
}
/** The subscript operator returns a VariableNetworkNode which can be used in
* the Application::initialise()
* function to connect the register with another variable. */
VariableNetworkNode operator()(const std::string& registerName, UpdateMode mode,
const std::type_info& valueType = typeid(AnyType), size_t nElements = 0) const;
VariableNetworkNode operator()(const std::string& registerName, const std::type_info& valueType,
size_t nElements = 0, UpdateMode mode = UpdateMode::poll) const {
return operator()(registerName, mode, valueType, nElements);
}
VariableNetworkNode operator()(const std::string& variableName) const override {
return operator()(variableName, UpdateMode::poll);
}
Module& operator[](const std::string& moduleName) const override;
const Module& virtualise() const override;
void connectTo(const Module& target, VariableNetworkNode trigger = {}) const override;
ModuleType getModuleType() const override { return ModuleType::Device; }
/** Use this function to report an exception. It should be called whenever a
* ChimeraTK::runtime_error has been caught when trying to interact with this
* device. It is primarily used by the ExceptionHandlingDecorator, but also user modules
* can report exception and trigger the recovery mechanism like this. */
void reportException(std::string errMsg);
void run() override;
void terminate() override;
VersionNumber getCurrentVersionNumber() const override { return currentVersionNumber; }
void setCurrentVersionNumber(VersionNumber versionNumber) override {
if(versionNumber > currentVersionNumber) currentVersionNumber = versionNumber;
}
Martin Christoph Hierholzer
committed
VersionNumber currentVersionNumber{nullptr};
/** This function connects DeviceError VariableGroup to ContolSystem*/
void defineConnections() override;
Martin Christoph Hierholzer
committed
DataValidity getDataValidity() const override { return DataValidity::ok; }
void incrementDataFaultCounter() override {
Martin Christoph Hierholzer
committed
throw ChimeraTK::logic_error("incrementDataFaultCounter() called on a DeviceModule. This is probably "
"caused by incorrect ownership of variables/accessors or VariableGroups.");
}
void decrementDataFaultCounter() override {
Martin Christoph Hierholzer
committed
throw ChimeraTK::logic_error("decrementDataFaultCounter() called on a DeviceModule. This is probably "
"caused by incorrect ownership of variables/accessors or VariableGroups.");
}
/** Add initialisation handlers to the device.
*
* Initialisation handlers are called after the device has been opened, or after the device is recovering
* from an error (i.e. an accessor has thrown an exception and Device::isFunctional() returns true afterwards).
*
* You can add mupltiple handlers. They are executed in the sequence in which they are registered. If a handler
* has been registered in the constructor, it is called first.
*
* The handler function is called from the DeviceModule thread (not from the thread with the accessor that threw the exception).
* It is handed a pointer to the instance of the DeviceModule
* where the handler was registered. The handler function may throw a ChimeraTK::runtime_error, so you don't have to
* catch errors thrown when accessing the Device inside the handler. After a handler has thrown an exception, the
* following handlers are not called. The DeviceModule will wait until the Device reports isFunctional() again and retry.
* The exception is reported to other modules and the control system.
*
* Notice: Especially in network based devices which do not hold a permanent connection, it is not always possible
* to predict whether the next read()/write() will succeed. In this case the Device will always report isFunctional()
* and one just has to retry. In this case the DeviceModule will start the initialisation sequence every 500 ms.
*/
void addInitialisationHandler(std::function<void(DeviceModule*)> initialisationHandler);
/** A trigger that indicated that the device just became available again an error (in contrast to the
* error status which is also send when the device goes away).
* The output is public so your module can connect to it and trigger re-sending of variables that
* have to be send to the device again. e.g. after this has re-booted.
* Attention: It is not send the first time the device is being opened. In this case the normal startup
* mechanism takes care that the data is send.
* Like the deviceError, it is automatically published to the control systen to ensure that there is at least one
* consumer connected.
*/
ScalarOutput<int> deviceBecameFunctional{
this, "deviceBecameFunctional", "", ""}; // should be changed to data type void
/** Add a TransferElement to the list DeviceModule::writeRecoveryOpen. This list will be written during a recovery,
* after the constant accessors DeviceModule::writeAfterOpen are written. This is locked by a unique_lock.
* You can get a shared_lock with getRecoverySharedLock(). */
Martin Killenberg
committed
void addRecoveryAccessor(boost::shared_ptr<RecoveryHelper> recoveryAccessor);
Martin Killenberg
committed
/** Each call to this function gives a unique number. It is atomically increased with each call.
* The smalled valid write order is 1.
*/
uint64_t writeOrder();
Martin Killenberg
committed
/** Returns a shared lock for the DeviceModule::recoveryMutex. This locks writing
* the list DeviceModule::writeRecoveryOpen, during a recovery.*/
boost::shared_lock<boost::shared_mutex> getRecoverySharedLock();
/**
* Wait for initial values coming from the device. This function will block until the device is opened and
* initialised, and initial values can be read from it.
Martin Killenberg
committed
*/
void waitForInitialValues();
Martin Killenberg
committed
Martin Killenberg
committed
std::list<EntityOwner*> getInputModulesRecursively(std::list<EntityOwner*> startList) override;
Martin Killenberg
committed
size_t getCircularNetworkHash() override;
/**
* Return associated device alias resp. URI
*/
std::string getDeviceAliasOrURI() const { return deviceAliasOrURI; }
protected:
// populate virtualisedModuleFromCatalog based on the information in the
// device's catalogue
VirtualModule& virtualiseFromCatalog() const;
mutable VirtualModule virtualisedModuleFromCatalog{"INVALID", "", ModuleType::Invalid};
mutable bool virtualisedModuleFromCatalog_isValid{false};
std::string deviceAliasOrURI;
ChimeraTK::RegisterPath registerNamePrefix;
// List of proxies accessed through the operator[]. This is mutable since
// it is little more than a cache and thus does not change the logical state
// of this module
mutable std::map<std::string, detail::DeviceModuleProxy> proxies;
// create or return a proxy for a submodule (full hierarchy)
detail::DeviceModuleProxy& getProxy(const std::string& fullName) const;
/** A VariableGroup for exception status and message. It can be protected, as
* it is automatically connected to the control system in
* DeviceModule::defineConnections() */
struct DeviceError : public VariableGroup {
using VariableGroup::VariableGroup;
Martin Christoph Hierholzer
committed
StatusOutput status{this, "status", "Device status"};
ScalarOutput<std::string> message{this, "message", "", "Error message"};
};
DeviceError deviceError{this, "DeviceError", "Error status of the device"};
/** The thread waiting for reportException(). It runs handleException() */
boost::thread moduleThread;
/** Queue used for communication between reportException() and the
* moduleThread. */
cppext::future_queue<std::string> errorQueue{5};
Attention: In testable mode this mutex must only be locked when holding the testable mode mutex!*/
/** Version number of the last exception. Only access under the error mutex. */
VersionNumber exceptionVersionNumber = {};
//Intentionally not initialised with nullptr. It is propagated as long as the device is not successfully opened.
/** The error flag whether the device is functional. protected by the errorMutex. */
bool deviceHasError{true};
/** Use this function to read the exception version number. It is locking the variable mutex correctly for you. */
VersionNumber getExceptionVersionNumber();
/** This functions tries to open the device and set the deviceError. Once done it notifies the waiting thread(s).
* The function is running an endless loop inside its own thread (moduleThread). */
/** List of TransferElements to be written after the device has been recovered.
* See function addRecoveryAccessor() for details.*/
Martin Killenberg
committed
std::list<boost::shared_ptr<RecoveryHelper>> recoveryHelpers;
Martin Christoph Hierholzer
committed
Application* owner{nullptr};
Martin Christoph Hierholzer
committed
Martin Killenberg
committed
/* The list of initialisation handler callback functions */
std::list<std::function<void(DeviceModule*)>> initialisationHandlers;
Martin Killenberg
committed
/** Mutex for writing the DeviceModule::writeRecoveryOpen.*/
Martin Killenberg
committed
boost::shared_mutex recoveryMutex;
/** Latch to halt accessors until initial values can be received.
* Must be a latch and not a mutex as it is locked in a different thread than unlocked. */
bool isHoldingInitialValueLatch{true};
boost::latch initialValueLatch{1};
std::atomic<int64_t> synchronousTransferCounter{0};
Martin Killenberg
committed
std::atomic<uint64_t> writeOrderCounter{0};
Martin Killenberg
committed
std::list<RegisterPath> writeRegisterPaths;
std::list<RegisterPath> readRegisterPaths;
// Access to virtualiseFromCatalog() is needed by ServerHistory
friend struct history::ServerHistory;
// Access to virtualiseFromCatalog() is needed by MicroDAQ
Zenker, Dr. Klaus (FWKE) - 126506
committed
template<typename TRIGGERTYPE>
friend class MicroDAQ;
friend struct detail::DeviceModuleProxy;
Martin Christoph Hierholzer
committed
template<typename T>
friend class ExceptionHandlingDecorator;
Martin Christoph Hierholzer
committed
friend class ConnectingDeviceModule;
Martin Christoph Hierholzer
committed
friend class StatusAggregator;
Martin Christoph Hierholzer
committed
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
};
/*********************************************************************************************************************/
/**
*/
class ConnectingDeviceModule : public ModuleGroup {
public:
/**
* Create ConnectingDeviceModule which is connected to the control system at the path of the owner.
*
* deviceAliasOrURI: identifies the device by either the alias found in the DMAP file or directly a CDD.
*
* triggerPath specifies a control system variable which is used as a trigger where needed.
*
* initialisationHandler specifies a callback function to initialise the device (optional, default is none).
*
* pathInDevice specifies a module in the device register hierarchy which should be used and connected to the
* control system (optional, default is "/" which connects the entire device).
*
* Note about typical usage: A DeviceModule constructed with this constructer is often owned by the ModuleGroup
* which is using this device. The device should be a logical name mapped device so the variable hierarchy of the
* ModuleGroup and the Device can be matched. The logical device may be subdivided into several parts, e.g. if
* different parts of the device are used by independent ModuleGroups, or if different triggers are required. This
* is possible by use of the pathInDevice prefix. To avoid the creation of multiple DeviceBackends for the same
* device (which may not even be possible for some transport protocols) make sure that the device CDD is identical
* for all instances (the alias name does not matter, so multiple DMAP file entires pointing to the same device
* are possible if needed).
*
* Keep in mind that mulitple DeviceModules will perform independent and asynchronous recovery procedures after
* an exception, even when pointing to the same device.
*/
ConnectingDeviceModule(EntityOwner* owner, const std::string& deviceAliasOrCDD, const std::string& triggerPath = {},
Martin Killenberg
committed
std::function<void(DeviceModule*)> initialisationHandler = nullptr, const std::string& pathInDevice = "/");
Martin Christoph Hierholzer
committed
Martin Christoph Hierholzer
committed
/**
* Return the underlying DeviceModule
*/
DeviceModule& getDeviceModule() { return *_dm; }
Martin Christoph Hierholzer
committed
protected:
void defineConnections() override;
std::string pathToConnectTo;
std::string triggerPath;
std::string pathInDevice;
/// The DeviceModule represented by this ConnectingDeviceModule
Martin Killenberg
committed
DeviceModule* _dm;
Martin Christoph Hierholzer
committed
/// Initialisation handler to add to the DeviceModule. This must be done only in defineConnections(), as otherwise
/// the initialisation handler would need to be removed in the destructor which is not possible. Not doing so at
/// least creates issues with move operations, especially if the initialisation handler points to a moved object.
std::function<void(DeviceModule*)> _initHandler;
Martin Christoph Hierholzer
committed
/// Shared pointer holding the DeviceModule if (and only if) this ConnectingDeviceModule owns the DeviceModule
boost::shared_ptr<DeviceModule> _dmHolder;
Martin Christoph Hierholzer
committed
} /* namespace ChimeraTK */
#endif /* CHIMERATK_DEVICE_MODULE_H */