-
Martin Christoph Hierholzer authoredMartin Christoph Hierholzer authored
DeviceModule.h 16.67 KiB
/*
* DeviceModule.h
*
* Created on: Jun 27, 2016
* Author: Martin Hierholzer
*/
#ifndef CHIMERATK_DEVICE_MODULE_H
#define CHIMERATK_DEVICE_MODULE_H
#include "ControlSystemModule.h"
#include "Module.h"
#include "ModuleGroup.h"
#include "RecoveryHelper.h"
#include "ScalarAccessor.h"
#include "StatusAccessor.h"
#include "StatusWithMessage.h"
#include "VariableGroup.h"
#include "VariableNetworkNode.h"
#include "VirtualModule.h"
#include <ChimeraTK/Device.h>
#include <ChimeraTK/ForwardDeclarations.h>
#include <ChimeraTK/RegisterPath.h>
#include <boost/thread/latch.hpp>
namespace ChimeraTK {
class Application;
class DeviceModule;
namespace history {
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:
/**
* 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);
Module::operator=(std::move(other));
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);
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 prepare() override;
void run() override;
void terminate() override;
VersionNumber getCurrentVersionNumber() const override { return currentVersionNumber; }
void setCurrentVersionNumber(VersionNumber versionNumber) override {
if(versionNumber > currentVersionNumber) currentVersionNumber = versionNumber;
}
VersionNumber currentVersionNumber{nullptr};
/** This function connects DeviceError VariableGroup to ContolSystem*/
void defineConnections() override;
mutable Device device;
DataValidity getDataValidity() const override { return DataValidity::ok; }
void incrementDataFaultCounter() override {
throw ChimeraTK::logic_error("incrementDataFaultCounter() called on a DeviceModule. This is probably "
"caused by incorrect ownership of variables/accessors or VariableGroups.");
}
void decrementDataFaultCounter() override {
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(). */
void addRecoveryAccessor(boost::shared_ptr<RecoveryHelper> recoveryAccessor);
/** 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();
/** 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.
*/
void waitForInitialValues();
std::list<EntityOwner*> getInputModulesRecursively(std::list<EntityOwner*> startList) override;
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() */
StatusWithMessage deviceError{this, "DeviceError/status", "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};
/** Mutex to protect deviceHasError.
Attention: In testable mode this mutex must only be locked when holding the testable mode mutex!*/
boost::shared_mutex errorMutex;
/** 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). */
void handleException();
/** List of TransferElements to be written after the device has been recovered.
* See function addRecoveryAccessor() for details.*/
std::list<boost::shared_ptr<RecoveryHelper>> recoveryHelpers;
Application* owner{nullptr};
mutable bool deviceIsInitialized = false;
/* The list of initialisation handler callback functions */
std::list<std::function<void(DeviceModule*)>> initialisationHandlers;
/** Mutex for writing the DeviceModule::writeRecoveryOpen.*/
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};
std::atomic<uint64_t> writeOrderCounter{0};
std::list<RegisterPath> writeRegisterPaths;
std::list<RegisterPath> readRegisterPaths;
friend class Application;
// Access to virtualiseFromCatalog() is needed by ServerHistory
friend struct history::ServerHistory;
// Access to virtualiseFromCatalog() is needed by MicroDAQ
template<typename TRIGGERTYPE>
friend class MicroDAQ;
friend struct detail::DeviceModuleProxy;
template<typename T>
friend class ExceptionHandlingDecorator;
friend class ConnectingDeviceModule;
friend class StatusAggregator;
};
/*********************************************************************************************************************/
/**
*/
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 = {},
std::function<void(DeviceModule*)> initialisationHandler = nullptr, const std::string& pathInDevice = "/");
/**
* Return the underlying DeviceModule
*/
DeviceModule& getDeviceModule() { return *_dm; }
protected:
void defineConnections() override;
std::string pathToConnectTo;
std::string triggerPath;
std::string pathInDevice;
/// The DeviceModule represented by this ConnectingDeviceModule
DeviceModule* _dm;
/// 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;
/// Shared pointer holding the DeviceModule if (and only if) this ConnectingDeviceModule owns the DeviceModule
boost::shared_ptr<DeviceModule> _dmHolder;
};
} /* namespace ChimeraTK */
#endif /* CHIMERATK_DEVICE_MODULE_H */