diff --git a/modules/opcua_client_module/src/opcua_client_module_impl.cpp b/modules/opcua_client_module/src/opcua_client_module_impl.cpp index 0ceb21a..631b726 100644 --- a/modules/opcua_client_module/src/opcua_client_module_impl.cpp +++ b/modules/opcua_client_module/src/opcua_client_module_impl.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,7 @@ DevicePtr OpcUaClientModule::onCreateDevice(const StringPtr& connectionString, .addAddressInfo(addressInfo) .freeze(); + device.asPtr(true).setAsRoot(); return device; } diff --git a/modules/opcua_client_module/tests/CMakeLists.txt b/modules/opcua_client_module/tests/CMakeLists.txt index 7deb68f..6b684dd 100644 --- a/modules/opcua_client_module/tests/CMakeLists.txt +++ b/modules/opcua_client_module/tests/CMakeLists.txt @@ -2,6 +2,7 @@ set(MODULE_NAME opcua_client_module) set(TEST_APP test_${MODULE_NAME}) set(TEST_SOURCES test_opcua_client_module.cpp + test_opcua_device_module.cpp test_app.cpp ) @@ -10,6 +11,7 @@ add_executable(${TEST_APP} ${TEST_SOURCES} target_link_libraries(${TEST_APP} PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_test_utils gtest ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_mocks ) add_test(NAME ${TEST_APP} diff --git a/modules/opcua_client_module/tests/test_opcua_device_module.cpp b/modules/opcua_client_module/tests/test_opcua_device_module.cpp new file mode 100644 index 0000000..c783a10 --- /dev/null +++ b/modules/opcua_client_module/tests/test_opcua_device_module.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include +#include + +using OpcuaDeviceModulesTest = testing::Test; + +using namespace daq; + +static ModulePtr CreateClientOpcUaModule(const ContextPtr& context) +{ + ModulePtr module; + createOpcUaClientModule(&module, context); + return module; +} + +static InstancePtr CreateServerInstance() +{ + auto instance = InstanceBuilder().setDefaultRootDeviceLocalId("local").build(); + ContextPtr context = instance.getContext(); + ModuleManagerPtr manager = instance.getModuleManager(); + + manager.addModule(ModulePtr(MockDeviceModule_Create(context))); + manager.addModule(ModulePtr(MockFunctionBlockModule_Create(context))); + + instance.addDevice("daqmock://phys_device"); + instance.addFunctionBlock("mock_fb_uid"); + instance.addServer("OpenDAQOPCUA", nullptr); + return instance; +} + +static InstancePtr CreateClientInstance() +{ + auto instance = InstanceBuilder().setModulePath("[[none]]").build(); + ContextPtr context = instance.getContext(); + ModuleManagerPtr manager = instance.getModuleManager(); + + manager.addModule(CreateClientOpcUaModule(context)); + + auto config = instance.createDefaultAddDeviceConfig(); + PropertyObjectPtr general = config.getPropertyValue("General"); + general.setPropertyValue("StreamingConnectionHeuristic", 2); + + instance.addDevice("daq.opcua://127.0.0.1", config); + return instance; +} + +const std::set& getDefaultComponentsIds() +{ + static const std::set ids = [] + { + std::set result; + result.insert("Sig"); + result.insert("FB"); + result.insert("IP"); + result.insert("Dev"); + result.insert("IO"); + result.insert("Synchronization"); + result.insert("Srv"); + return result; + }(); + return ids; +} + +TEST_F(OpcuaDeviceModulesTest, ComponentActiveChangedRecursive) +{ + auto server = CreateServerInstance(); + auto client = CreateClientInstance(); + + // Get the client's mirror of server device (this is a root device) + auto clientDevice = client.getDevices()[0]; + + // Get all components in clientDevice subtree + auto clientDeviceComponents = clientDevice.getItems(search::Recursive(search::Any())); + + // checking that the local active change is not propogating to the remote + { + // Set client (Instance) active to false + client.setActive(false); + + // client itself should be inactive + ASSERT_FALSE(client.getActive()) << "client should be inactive"; + + // clientDevice should still be active (it's a root device, doesn't receive parentActive) + ASSERT_TRUE(clientDevice.getActive()) << "clientDevice should remain active as it's a root device"; + + // All clientDevice subtree components should still be active + for (const auto& comp : clientDeviceComponents) + { + ASSERT_TRUE(comp.getLocalActive()) << "Component should be local active: " << comp.getGlobalId(); + ASSERT_TRUE(comp.getParentActive()) << "Component parent should be inactive: " << comp.getGlobalId(); + ASSERT_TRUE(comp.getActive()) << "Component should be inactive: " << comp.getGlobalId(); + } + + // undo changaings + client.setActive(true); + } + + // checking that the local active change is not propogating to the remote + { + // Set client (Instance) active to false + clientDevice.setActive(false); + + // client itself should be inactive + ASSERT_FALSE(clientDevice.getActive()) << "client clientDevice be inactive"; + + // clientDevice should still be active (it's a root device, doesn't receive parentActive) + ASSERT_TRUE(client.getActive()) << "client should remain active as its not child of client"; + + // All clientDevice subtree components should still be active + for (const auto& comp : clientDeviceComponents) + { + if (comp == clientDevice) + { + ASSERT_FALSE(comp.getLocalActive()) << "Component should be local inactive: " << comp.getGlobalId(); + ASSERT_TRUE(comp.getParentActive()) << "Component parent should be active: " << comp.getGlobalId(); + } + else + { + if (getDefaultComponentsIds().count(comp.getLocalId())) + continue; + ASSERT_TRUE(comp.getLocalActive()) << "Component should be local active: " << comp.getGlobalId(); + ASSERT_FALSE(comp.getParentActive()) << "Component parent should be inactive: " << comp.getGlobalId(); + } + ASSERT_FALSE(comp.getActive()) << "Component should be inactive: " << comp.getGlobalId(); + + } + } +} + +TEST_F(OpcuaDeviceModulesTest, ComponentActiveChangedRecursiveGateway) +{ + // Create leaf server + auto leaf = InstanceBuilder().setDefaultRootDeviceLocalId("leaf").build(); + leaf.addServer("OpenDAQOPCUA", nullptr); + + // Create gateway that connects to leaf + auto gateway = Instance(); + auto gatewayServerConfig = gateway.getAvailableServerTypes().get("OpenDAQOPCUA").createDefaultConfig(); + gatewayServerConfig.setPropertyValue("Port", 4841); + gateway.addDevice("daq.opcua://127.0.0.1"); + gateway.addServer("OpenDAQOPCUA", gatewayServerConfig); + + // Create client that connects to gateway + auto client = Instance(); + auto clientGatewayDevice = client.addDevice("daq.opcua://127.0.0.1:4841"); + + // Get the leaf device through gateway + auto clientLeafDevice = clientGatewayDevice.getDevices()[0]; + + auto clientDeviceComponents = clientGatewayDevice.getItems(search::Recursive(search::InterfaceId(IDevice::Id))); + + // Set clientGatewayDevice active to false (from client side) + clientGatewayDevice.setActive(false); + + // clientGatewayDevice itself should be inactive + ASSERT_FALSE(clientGatewayDevice.getActive()) << "clientGatewayDevice should be inactive"; + + // clientLeafDevice should still be active (it's a root device) + ASSERT_TRUE(clientLeafDevice.getActive()) << "clientLeafDevice should remain active as it's a root device"; + + for (const auto& comp : clientDeviceComponents) + { + if (comp == clientGatewayDevice) + { + ASSERT_FALSE(comp.getLocalActive()) << "Component should be local inactive: " << comp.getGlobalId(); + ASSERT_TRUE(comp.getParentActive()) << "Component parent should be active: " << comp.getGlobalId(); + ASSERT_FALSE(comp.getActive()) << "Component should be inactive: " << comp.getGlobalId(); + } + else if (const auto dev = comp.asPtrOrNull(true); dev.assigned()) + { + ASSERT_TRUE(comp.getLocalActive()) << "Component should be local active: " << comp.getGlobalId(); + ASSERT_TRUE(comp.getParentActive()) << "Component parent should be active: " << comp.getGlobalId(); + ASSERT_TRUE(comp.getActive()) << "Component should be active: " << comp.getGlobalId(); + } + } +} diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h index b6e1575..ed795fe 100644 --- a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h @@ -60,6 +60,8 @@ class TmsClientComponentBaseImpl : public TmsClientPropertyObjectBaseImpl // Component overrides ErrCode INTERFACE_FUNC getActive(Bool* active) override; + ErrCode INTERFACE_FUNC getLocalActive(Bool* active) override; + ErrCode INTERFACE_FUNC getParentActive(Bool* active) override; ErrCode INTERFACE_FUNC setActive(Bool active) override; ErrCode INTERFACE_FUNC getName(IString** name) override; ErrCode INTERFACE_FUNC setName(IString* name) override; diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h index 8af26ab..22a6495 100644 --- a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h @@ -38,6 +38,7 @@ class TmsClientDeviceImpl : public TmsClientComponentBaseImpl ErrCode TmsClientComponentBaseImpl::getActive(Bool* active) { + OPENDAQ_PARAM_NOT_NULL(active); + try { *active = this->template readValue("Active"); @@ -28,6 +30,53 @@ ErrCode TmsClientComponentBaseImpl::getActive(Bool* active) return OPENDAQ_SUCCESS; } +template +ErrCode TmsClientComponentBaseImpl::getLocalActive(Bool* active) +{ + OPENDAQ_PARAM_NOT_NULL(active); + + if (!this->hasReference("LocalActive")) + return this->getActive(active); + + try + { + *active = this->template readValue("LocalActive"); + } + catch(...) + { + *active = true; + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to get local active of component \"{}\". The default value was returned \"true\"", this->globalId); + } + + return OPENDAQ_SUCCESS; +} + +template +ErrCode TmsClientComponentBaseImpl::getParentActive(Bool* active) +{ + OPENDAQ_PARAM_NOT_NULL(active); + + if (!this->hasReference("ParentActive")) + { + *active = True; + return OPENDAQ_SUCCESS; + } + + try + { + *active = this->template readValue("ParentActive"); + } + catch(...) + { + *active = true; + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to get parent active of component \"{}\". The default value was returned \"true\"", this->globalId); + } + + return OPENDAQ_SUCCESS; +} + template ErrCode TmsClientComponentBaseImpl::setActive(Bool active) { diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp index 941de19..19990b7 100644 --- a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp @@ -477,6 +477,19 @@ PropertyObjectPtr TmsClientDeviceImpl::onCreateDefaultAddDeviceConfig() return PropertyObject(); } +ErrCode TmsClientDeviceImpl::getParentActive(Bool* active) +{ + OPENDAQ_PARAM_NOT_NULL(active); + + if (this->isRootDevice) + { + *active = True; + return OPENDAQ_SUCCESS; + } + + return Super::getParentActive(active); +} + void TmsClientDeviceImpl::findAndCreateFunctionBlocks() { std::map orderedFunctionBlocks; diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h index b221f73..034f652 100644 --- a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h @@ -156,6 +156,10 @@ void TmsServerComponent::bindCallbacks() }); } + this->addReadCallback("LocalActive", [this] { return VariantConverter::ToVariant( this->object.getLocalActive()); }); + + this->addReadCallback("ParentActive", [this] { return VariantConverter::ToVariant( this->object.getParentActive()); }); + this->addReadCallback("Visible", [this] { return VariantConverter::ToVariant( this->object.getVisible()); }); if (!this->object.template supportsInterface() || !this->object.isFrozen()) @@ -230,17 +234,47 @@ void TmsServerComponent::registerToTmsServerContext() template void TmsServerComponent::addChildNodes() { - OpcUaNodeId newNodeId(0); - AddVariableNodeParams params(newNodeId, this->nodeId); - params.setBrowseName("Visible"); - params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_BOOLEAN].typeId)); - params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); - - OpcUaObject attr = UA_VariableAttributes_default; - attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; - params.attr = attr; - - this->server->addVariableNode(params); + { + OpcUaNodeId newNodeId(0); + AddVariableNodeParams params(newNodeId, this->nodeId); + params.setBrowseName("Visible"); + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_BOOLEAN].typeId)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + + OpcUaObject attr = UA_VariableAttributes_default; + attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + params.attr = attr; + + this->server->addVariableNode(params); + } + + { + OpcUaNodeId newNodeId(0); + AddVariableNodeParams params(newNodeId, this->nodeId); + params.setBrowseName("LocalActive"); + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_BOOLEAN].typeId)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + + OpcUaObject attr = UA_VariableAttributes_default; + attr->accessLevel = UA_ACCESSLEVELMASK_READ; + params.attr = attr; + + this->server->addVariableNode(params); + } + + { + OpcUaNodeId newNodeId(0); + AddVariableNodeParams params(newNodeId, this->nodeId); + params.setBrowseName("ParentActive"); + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_BOOLEAN].typeId)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + + OpcUaObject attr = UA_VariableAttributes_default; + attr->accessLevel = UA_ACCESSLEVELMASK_READ; + params.attr = attr; + + this->server->addVariableNode(params); + } tmsPropertyObject->registerToExistingOpcUaNode(this->nodeId); if (tmsComponentConfig) diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt b/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt index 96f9de3..2055f04 100644 --- a/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt @@ -41,7 +41,7 @@ target_link_libraries(${TEST_APP} PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuatms_server ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuatms_client ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_mocks - ${BCRYPT_LIB} + ${BCRYPT_LIB} ) if(SUPPORTS_ASAN)