From 97c02497e57b56502bfde79f15d4259ae18f41b8 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Tue, 24 Mar 2026 11:04:51 +0100 Subject: [PATCH 01/29] opcua generic client: generic client module, generic client device --- modules/CMakeLists.txt | 1 + .../CMakeLists.txt | 9 + .../opcua_generic_client_module/common.h | 21 ++ .../opcua_generic_client_module/constants.h | 31 ++ .../opcua_generic_client_module/module_dll.h | 20 ++ .../opcua_generic_client_module_impl.h | 49 +++ .../src/CMakeLists.txt | 50 +++ .../src/module_dll.cpp | 9 + .../src/opcua_generic_client_module_impl.cpp | 318 ++++++++++++++++++ .../tests/CMakeLists.txt | 22 ++ .../tests/test_app.cpp | 19 ++ .../test_opcua_generic_client_module.cpp | 168 +++++++++ shared/libraries/CMakeLists.txt | 1 + shared/libraries/opcuageneric/CMakeLists.txt | 10 + .../opcuageneric_client/CMakeLists.txt | 9 + .../include/opcuageneric_client/constants.h | 27 ++ .../generic_client_device_impl.h | 57 ++++ .../opcuageneric_client/opcuageneric.h | 27 ++ .../opcuageneric_client/src/CMakeLists.txt | 52 +++ .../src/generic_client_device_impl.cpp | 126 +++++++ .../opcuageneric_client/tests/CMakeLists.txt | 48 +++ .../opcuageneric_client/tests/test_app.cpp | 19 ++ .../tests/test_daq_test_helper.h | 42 +++ .../test_opcua_generic_client_device.cpp | 78 +++++ 24 files changed, 1213 insertions(+) create mode 100644 modules/opcua_generic_client_module/CMakeLists.txt create mode 100644 modules/opcua_generic_client_module/include/opcua_generic_client_module/common.h create mode 100644 modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h create mode 100644 modules/opcua_generic_client_module/include/opcua_generic_client_module/module_dll.h create mode 100644 modules/opcua_generic_client_module/include/opcua_generic_client_module/opcua_generic_client_module_impl.h create mode 100644 modules/opcua_generic_client_module/src/CMakeLists.txt create mode 100644 modules/opcua_generic_client_module/src/module_dll.cpp create mode 100644 modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp create mode 100644 modules/opcua_generic_client_module/tests/CMakeLists.txt create mode 100644 modules/opcua_generic_client_module/tests/test_app.cpp create mode 100644 modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp create mode 100644 shared/libraries/opcuageneric/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcuageneric.h create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/test_app.cpp create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index a1cb0ef..e1e3580 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -6,6 +6,7 @@ endif() if (${REPO_OPTION_PREFIX}_ENABLE_CLIENT) add_subdirectory(opcua_client_module) + add_subdirectory(opcua_generic_client_module) endif() if (${REPO_OPTION_PREFIX}_ENABLE_SERVER) diff --git a/modules/opcua_generic_client_module/CMakeLists.txt b/modules/opcua_generic_client_module/CMakeLists.txt new file mode 100644 index 0000000..63e42a7 --- /dev/null +++ b/modules/opcua_generic_client_module/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) +project(ClientModule VERSION ${${REPO_OPTION_PREFIX}_VERSION} LANGUAGES C CXX) + +add_subdirectory(src) + +if (${REPO_OPTION_PREFIX}_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/modules/opcua_generic_client_module/include/opcua_generic_client_module/common.h b/modules/opcua_generic_client_module/include/opcua_generic_client_module/common.h new file mode 100644 index 0000000..4f1cc67 --- /dev/null +++ b/modules/opcua_generic_client_module/include/opcua_generic_client_module/common.h @@ -0,0 +1,21 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#define BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(opcua_generic_client_module) +#define END_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h b/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h new file mode 100644 index 0000000..d4377e9 --- /dev/null +++ b/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h @@ -0,0 +1,31 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE + +static const char* DaqOpcUaGenericDeviceTypeId = "OpenDAQOPCUAGenericStreaming"; +static const char* DaqOpcUaGenericDevicePrefix = "daq.opcua.generic"; +static const char* OpcUaGenericScheme = "opc.tcp"; +static const char* DaqOpcUaGenericProtocolId = "OPCUAGenericClient"; + +static const char* MODULE_NAME = "OpenDAQOPCUAGenericClientModule"; +static const char* MODULE_ID = "OpenDAQOPCUAGenericClientModule"; + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE diff --git a/modules/opcua_generic_client_module/include/opcua_generic_client_module/module_dll.h b/modules/opcua_generic_client_module/include/opcua_generic_client_module/module_dll.h new file mode 100644 index 0000000..db00d74 --- /dev/null +++ b/modules/opcua_generic_client_module/include/opcua_generic_client_module/module_dll.h @@ -0,0 +1,20 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +DECLARE_MODULE_EXPORTS(OpcUaGenericClientModule) diff --git a/modules/opcua_generic_client_module/include/opcua_generic_client_module/opcua_generic_client_module_impl.h b/modules/opcua_generic_client_module/include/opcua_generic_client_module/opcua_generic_client_module_impl.h new file mode 100644 index 0000000..a30df90 --- /dev/null +++ b/modules/opcua_generic_client_module/include/opcua_generic_client_module/opcua_generic_client_module_impl.h @@ -0,0 +1,49 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE + +class OpcUaGenericClientModule final : public Module +{ +public: + OpcUaGenericClientModule(ContextPtr context); + + ListPtr onGetAvailableDevices() override; + DictPtr onGetAvailableDeviceTypes() override; + DevicePtr onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) override; + bool acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + Bool onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) override; + +private: + StringPtr formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config, std::string& host, int& port, std::string& hostType); + static DeviceTypePtr createDeviceType(); + static PropertyObjectPtr createDefaultConfig(); + static PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& config); + static DeviceInfoPtr populateDiscoveredDevice(const discovery::MdnsDiscoveredDevice& discoveredDevice); + discovery::DiscoveryClient discoveryClient; + + std::mutex sync; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE diff --git a/modules/opcua_generic_client_module/src/CMakeLists.txt b/modules/opcua_generic_client_module/src/CMakeLists.txt new file mode 100644 index 0000000..bd78aef --- /dev/null +++ b/modules/opcua_generic_client_module/src/CMakeLists.txt @@ -0,0 +1,50 @@ +set(LIB_NAME opcua_generic_client_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + constants.h + module_dll.h + opcua_generic_client_module_impl.h +) + +set(SRC_Srcs module_dll.cpp + opcua_generic_client_module_impl.cpp +) + +opendaq_prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/opcua_generic_client_module_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + ${MODULE_HEADERS_DIR}/common.h + ${MODULE_HEADERS_DIR}/constants.h + module_dll.cpp + opcua_generic_client_module_impl.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} +) +add_library(${OPENDAQ_SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +endif() + +target_link_libraries(${LIB_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq + Boost::uuid + PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::discovery + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuageneric_client +) + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +endif() + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +opendaq_generate_version_header(${LIB_NAME}) diff --git a/modules/opcua_generic_client_module/src/module_dll.cpp b/modules/opcua_generic_client_module/src/module_dll.cpp new file mode 100644 index 0000000..a7d6f22 --- /dev/null +++ b/modules/opcua_generic_client_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::opcua_generic_client_module; + +DEFINE_MODULE_EXPORTS(OpcUaGenericClientModule) + diff --git a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp new file mode 100644 index 0000000..b148509 --- /dev/null +++ b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp @@ -0,0 +1,318 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE + +static const std::regex RegexIpv6Hostname(R"(^(.+://)?(\[[a-fA-F0-9:]+(?:\%[a-zA-Z0-9_\.-~]+)?\])(?::(\d+))?(/.*)?$)"); +static const std::regex RegexIpv4Hostname(R"(^(.+://)?([^:/\s]+)(?::(\d+))?(/.*)?$)"); + +using namespace discovery; +using namespace daq::opcua; +using namespace daq::opcua::generic; + +OpcUaGenericClientModule::OpcUaGenericClientModule(ContextPtr context) + : Module(MODULE_NAME, + daq::VersionInfo(OPCUA_GENERIC_CLIENT_MODULE_MAJOR_VERSION, + OPCUA_GENERIC_CLIENT_MODULE_MINOR_VERSION, + OPCUA_GENERIC_CLIENT_MODULE_PATCH_VERSION), + std::move(context), + MODULE_ID) + , discoveryClient({"OPENDAQ"}) +{ + loggerComponent = this->context.getLogger().getOrAddComponent(DaqOpcUaGenericProtocolId); + discoveryClient.initMdnsClient(List("_opcua-tcp._tcp.local.")); +} + +ListPtr OpcUaGenericClientModule::onGetAvailableDevices() +{ + auto availableDevices = List(); + for (const auto& device : discoveryClient.discoverMdnsDevices()) + availableDevices.pushBack(populateDiscoveredDevice(device)); + return availableDevices; +} + +DictPtr OpcUaGenericClientModule::onGetAvailableDeviceTypes() +{ + auto result = Dict(); + + auto deviceType = createDeviceType(); + result.set(deviceType.getId(), deviceType); + return result; +} + +DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) +{ + if (!connectionString.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException); + + PropertyObjectPtr configPtr = config; + if (!configPtr.assigned()) + configPtr = createDefaultConfig(); + else + configPtr = populateDefaultConfig(configPtr); + + if (!acceptsConnectionParameters(connectionString, configPtr)) + DAQ_THROW_EXCEPTION(InvalidParameterException); + + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Context is not available."); + + std::string host; + std::string hostType; + int port; + formConnectionString(connectionString, configPtr, host, port, hostType); + + std::scoped_lock lock(sync); + + DevicePtr device(createWithImplementation(context, parent, configPtr)); + + // auto deviceType = createDeviceType(); + // checkErrorInfo(deviceType.asPtr()->setModuleInfo(moduleInfo)); + // device.asPtr().setMirroredDeviceType(deviceType); + + // Set the connection info for the device + DeviceInfoPtr deviceInfo = device.getInfo(); + deviceInfo.asPtr().setProtectedPropertyValue("connectionString", connectionString); + ServerCapabilityConfigPtr connectionInfo = deviceInfo.getConfigurationConnectionInfo(); + + const auto addressInfo = AddressInfoBuilder().setAddress(host) + .setReachabilityStatus(AddressReachabilityStatus::Reachable) + .setType(hostType) + .setConnectionString(connectionString) + .build(); + + connectionInfo.setProtocolId(DaqOpcUaGenericDeviceTypeId) + .setProtocolName(DaqOpcUaGenericProtocolId) + .setProtocolType(ProtocolType::Streaming) + .setConnectionType("TCP/IP") + .addAddress(host) + .setPort(port) + .setPrefix(DaqOpcUaGenericDevicePrefix) + .setConnectionString(connectionString) + .addAddressInfo(addressInfo) + .freeze(); + + return device; +} + +PropertyObjectPtr OpcUaGenericClientModule::populateDefaultConfig(const PropertyObjectPtr& config) +{ + const auto defConfig = createDefaultConfig(); + for (const auto& prop : defConfig.getAllProperties()) + { + const auto name = prop.getName(); + if (config.hasProperty(name)) + defConfig.setPropertyValue(name, config.getPropertyValue(name)); + } + + return defConfig; +} + +DeviceInfoPtr OpcUaGenericClientModule::populateDiscoveredDevice(const MdnsDiscoveredDevice& discoveredDevice) +{ + auto cap = ServerCapability(DaqOpcUaGenericDeviceTypeId, DaqOpcUaGenericProtocolId, ProtocolType::Configuration); + + for (const auto& ipAddress : discoveredDevice.ipv4Addresses) + { + auto connectionStringIpv4 = fmt::format("{}://{}:{}{}", + DaqOpcUaGenericDevicePrefix, + ipAddress, + discoveredDevice.servicePort, + discoveredDevice.getPropertyOrDefault("path", "/")); + cap.addConnectionString(connectionStringIpv4); + cap.addAddress(ipAddress); + + const auto addressInfo = AddressInfoBuilder().setAddress(ipAddress) + .setReachabilityStatus(AddressReachabilityStatus::Unknown) + .setType("IPv4") + .setConnectionString(connectionStringIpv4) + .build(); + cap.addAddressInfo(addressInfo); + } + + for (const auto& ipAddress : discoveredDevice.ipv6Addresses) + { + auto connectionStringIpv6 = fmt::format("{}://{}:{}{}", + DaqOpcUaGenericDevicePrefix, + ipAddress, + discoveredDevice.servicePort, + discoveredDevice.getPropertyOrDefault("path", "/")); + cap.addConnectionString(connectionStringIpv6); + cap.addAddress(ipAddress); + + const auto addressInfo = AddressInfoBuilder().setAddress(ipAddress) + .setReachabilityStatus(AddressReachabilityStatus::Unknown) + .setType("IPv6") + .setConnectionString(connectionStringIpv6) + .build(); + cap.addAddressInfo(addressInfo); + } + + cap.setConnectionType("TCP/IP"); + cap.setPrefix(DaqOpcUaGenericDevicePrefix); + cap.setProtocolVersion(discoveredDevice.getPropertyOrDefault("protocolVersion", "")); + if (discoveredDevice.servicePort > 0) + cap.setPort(discoveredDevice.servicePort); + + return populateDiscoveredDeviceInfo(DiscoveryClient::populateDiscoveredInfoProperties, discoveredDevice, cap, createDeviceType()); +} + +StringPtr OpcUaGenericClientModule::formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config, std::string& host, int& port, std::string& hostType) +{ + std::string urlString = connectionString.toStdString(); + std::smatch match; + + std::string prefix = ""; + std::string path = "/"; + + if (config.assigned()) + { + if (config.hasProperty(PROPERTY_NAME_OPCUA_PORT)) + port = config.getPropertyValue(PROPERTY_NAME_OPCUA_PORT); + + if (config.hasProperty(PROPERTY_NAME_OPCUA_PATH)) + path = String(config.getPropertyValue(PROPERTY_NAME_OPCUA_PATH)).toStdString(); + } + + bool parsed = false; + parsed = std::regex_search(urlString, match, RegexIpv6Hostname); + hostType = "IPv6"; + + if (!parsed) + { + parsed = std::regex_search(urlString, match, RegexIpv4Hostname); + hostType = "IPv4"; + } + + if (parsed) + { + prefix = match[1]; + host = match[2]; + + if (match[3].matched) + port = std::stoi(match[3]); + + if (match[4].matched) + path = match[4]; + } + else + DAQ_THROW_EXCEPTION(InvalidParameterException, "Host name not found in url: {}", connectionString); + + if (prefix != std::string(DaqOpcUaGenericDevicePrefix) + "://") + DAQ_THROW_EXCEPTION(InvalidParameterException, "OpcUa does not support connection string with prefix {}", prefix); + + + if (config.assigned()) + { + if (config.hasProperty(PROPERTY_NAME_OPCUA_PORT)) + config.setPropertyValue(PROPERTY_NAME_OPCUA_PORT, port); + + if (config.hasProperty(PROPERTY_NAME_OPCUA_PATH)) + config.setPropertyValue(PROPERTY_NAME_OPCUA_PATH, path); + + if (config.hasProperty(PROPERTY_NAME_OPCUA_HOST)) + config.setPropertyValue(PROPERTY_NAME_OPCUA_HOST, host); + } + + return std::string(OpcUaGenericScheme) + "://" + host + ":" + std::to_string(port) + path; +} + +bool OpcUaGenericClientModule::acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config) +{ + std::string connStr = connectionString; + auto found = connStr.find(std::string(DaqOpcUaGenericDevicePrefix) + "://"); + return found == 0; +} + +Bool OpcUaGenericClientModule::onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) +{ + if (target.getProtocolId() != DaqOpcUaGenericDeviceTypeId) + return false; + + if (source.getConnectionType() != "TCP/IP") + return false; + + if (!source.getAddresses().assigned() || !source.getAddresses().getCount()) + { + LOG_W("Source server capability address is not available when filling in missing OPC UA capability information.") + return false; + } + + const auto addrInfos = source.getAddressInfo(); + if (!addrInfos.assigned() || !addrInfos.getCount()) + { + LOG_W("Source server capability addressInfo is not available when filling in missing OPC UA capability information.") + return false; + } + + auto port = target.getPort(); + if (port == -1) + { + port = DEFAULT_OPCUA_PORT; + target.setPort(port); + LOG_W("OPC UA server capability is missing port. Defaulting to {}.", DEFAULT_OPCUA_PORT) + } + + const auto path = target.hasProperty(PROPERTY_NAME_OPCUA_PATH) ? target.getPropertyValue(PROPERTY_NAME_OPCUA_PATH) : DEFAULT_OPCUA_PATH; + const auto targetAddress = target.getAddresses(); + for (const auto& addrInfo : addrInfos) + { + const auto address = addrInfo.getAddress(); + if (auto it = std::find(targetAddress.begin(), targetAddress.end(), address); it != targetAddress.end()) + continue; + + const auto prefix = target.getPrefix(); + StringPtr connectionString; + if (source.getPrefix() == prefix) + connectionString = addrInfo.getConnectionString(); + else + connectionString = fmt::format("{}://{}:{}{}", prefix, address, port, path); + + const auto targetAddrInfo = AddressInfoBuilder() + .setAddress(address) + .setReachabilityStatus(addrInfo.getReachabilityStatus()) + .setType(addrInfo.getType()) + .setConnectionString(connectionString) + .build(); + + target.addAddressInfo(targetAddrInfo) + .setConnectionString(connectionString) + .addAddress(address); + } + + return true; +} + +DeviceTypePtr OpcUaGenericClientModule::createDeviceType() +{ + return DeviceTypeBuilder() + .setId(DaqOpcUaGenericDeviceTypeId) + .setName("OpcUa enabled device") + .setDescription("Network device connected over OpcUa protocol") + .setConnectionStringPrefix(DaqOpcUaGenericDevicePrefix) + .setDefaultConfig(createDefaultConfig()) + .build(); +} + +PropertyObjectPtr OpcUaGenericClientModule::createDefaultConfig() +{ + return OpcuaGenericClientDeviceImpl::createDefaultConfig(); +} + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE diff --git a/modules/opcua_generic_client_module/tests/CMakeLists.txt b/modules/opcua_generic_client_module/tests/CMakeLists.txt new file mode 100644 index 0000000..9f7c89c --- /dev/null +++ b/modules/opcua_generic_client_module/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +set(MODULE_NAME opcua_generic_client_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_opcua_generic_client_module.cpp + test_app.cpp +) + +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} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (COMMAND setup_target_for_coverage AND OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${TEST_APP}coverage ${TEST_APP} ${TEST_APP}coverage) +endif() diff --git a/modules/opcua_generic_client_module/tests/test_app.cpp b/modules/opcua_generic_client_module/tests/test_app.cpp new file mode 100644 index 0000000..a257dac --- /dev/null +++ b/modules/opcua_generic_client_module/tests/test_app.cpp @@ -0,0 +1,19 @@ +#include + +#include +#include + +int main(int argc, char** args) +{ + { + daq::ModuleManager("."); + } + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new DaqMemCheckListener()); + + auto res = RUN_ALL_TESTS(); + + return res; +} diff --git a/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp b/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp new file mode 100644 index 0000000..9a2a98a --- /dev/null +++ b/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp @@ -0,0 +1,168 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using OpcUaGenericClientModuleTest = testing::Test; +using namespace daq; + +static ModulePtr CreateModule() +{ + ModulePtr module; + createModule(&module, NullContext()); + return module; +} + +TEST_F(OpcUaGenericClientModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(OpcUaGenericClientModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), "OpenDAQOPCUAGenericClientModule"); +} + +TEST_F(OpcUaGenericClientModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(OpcUaGenericClientModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), OPCUA_GENERIC_CLIENT_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), OPCUA_GENERIC_CLIENT_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), OPCUA_GENERIC_CLIENT_MODULE_PATCH_VERSION); +} + +TEST_F(OpcUaGenericClientModuleTest, EnumerateDevices) +{ + auto module = CreateModule(); + + ListPtr deviceInfo; + ASSERT_NO_THROW(deviceInfo = module.getAvailableDevices()); +} + +TEST_F(OpcUaGenericClientModuleTest, CreateDeviceConnectionStringNull) +{ + auto module = CreateModule(); + + DevicePtr device; + ASSERT_THROW(device = module.createDevice(nullptr, nullptr), ArgumentNullException); +} + +TEST_F(OpcUaGenericClientModuleTest, CreateDeviceConnectionStringEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("", nullptr), InvalidParameterException); +} + +TEST_F(OpcUaGenericClientModuleTest, CreateDeviceConnectionStringInvalid) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("fdfdfdfdde", nullptr), InvalidParameterException); +} + +TEST_F(OpcUaGenericClientModuleTest, CreateDeviceConnectionStringInvalidId) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("daqref://devicett3axxr1", nullptr), InvalidParameterException); +} + +TEST_F(OpcUaGenericClientModuleTest, GetAvailableComponentTypes) +{ + const auto module = CreateModule(); + + DictPtr functionBlockTypes; + ASSERT_NO_THROW(functionBlockTypes = module.getAvailableFunctionBlockTypes()); + ASSERT_EQ(functionBlockTypes.getCount(), 0u); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAGenericStreaming")); + ASSERT_EQ(deviceTypes.get("OpenDAQOPCUAGenericStreaming").getId(), "OpenDAQOPCUAGenericStreaming"); + + DictPtr serverTypes; + ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); + ASSERT_EQ(serverTypes.getCount(), 0u); + + // Check module info for module + ModuleInfoPtr moduleInfo; + ASSERT_NO_THROW(moduleInfo = module.getModuleInfo()); + ASSERT_NE(moduleInfo, nullptr); + ASSERT_EQ(moduleInfo.getName(), "OpenDAQOPCUAGenericClientModule"); + ASSERT_EQ(moduleInfo.getId(), "OpenDAQOPCUAGenericClientModule"); + + // Check version info for module + VersionInfoPtr versionInfoModule; + ASSERT_NO_THROW(versionInfoModule = moduleInfo.getVersionInfo()); + ASSERT_NE(versionInfoModule, nullptr); + ASSERT_EQ(versionInfoModule.getMajor(), OPCUA_GENERIC_CLIENT_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoModule.getMinor(), OPCUA_GENERIC_CLIENT_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoModule.getPatch(), OPCUA_GENERIC_CLIENT_MODULE_PATCH_VERSION); + + // Check module and version info for device types + for (const auto& deviceType : deviceTypes) + { + ModuleInfoPtr moduleInfoDeviceType; + ASSERT_NO_THROW(moduleInfoDeviceType = deviceType.second.getModuleInfo()); + ASSERT_NE(moduleInfoDeviceType, nullptr); + ASSERT_EQ(moduleInfoDeviceType.getName(), "OpenDAQOPCUAGenericClientModule"); + ASSERT_EQ(moduleInfoDeviceType.getId(), "OpenDAQOPCUAGenericClientModule"); + + VersionInfoPtr versionInfoDeviceType; + ASSERT_NO_THROW(versionInfoDeviceType = moduleInfoDeviceType.getVersionInfo()); + ASSERT_NE(versionInfoDeviceType, nullptr); + ASSERT_EQ(versionInfoDeviceType.getMajor(), OPCUA_GENERIC_CLIENT_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoDeviceType.getMinor(), OPCUA_GENERIC_CLIENT_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoDeviceType.getPatch(), OPCUA_GENERIC_CLIENT_MODULE_PATCH_VERSION); + } +} + +TEST_F(OpcUaGenericClientModuleTest, DefaultDeviceConfig) +{ + const auto module = CreateModule(); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAGenericStreaming")); + auto config = deviceTypes.get("OpenDAQOPCUAGenericStreaming").createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + ASSERT_EQ(config.getAllProperties().getCount(), 5u); +} + +TEST_F(OpcUaGenericClientModuleTest, CreateFunctionBlockIdNull) +{ + auto module = CreateModule(); + + FunctionBlockPtr functionBlock; + ASSERT_THROW(functionBlock = module.createFunctionBlock(nullptr, nullptr, "fb"), ArgumentNullException); +} + +TEST_F(OpcUaGenericClientModuleTest, CreateFunctionBlockIdEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createFunctionBlock("", nullptr, "fb"), NotFoundException); +} diff --git a/shared/libraries/CMakeLists.txt b/shared/libraries/CMakeLists.txt index cb96157..dfe2f8e 100644 --- a/shared/libraries/CMakeLists.txt +++ b/shared/libraries/CMakeLists.txt @@ -2,3 +2,4 @@ opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) add_subdirectory(opcua) add_subdirectory(opcuatms) +add_subdirectory(opcuageneric) diff --git a/shared/libraries/opcuageneric/CMakeLists.txt b/shared/libraries/opcuageneric/CMakeLists.txt new file mode 100644 index 0000000..9383e1f --- /dev/null +++ b/shared/libraries/opcuageneric/CMakeLists.txt @@ -0,0 +1,10 @@ +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) + +message(STATUS "${OPENDAQ_SDK_NAME} version: ${OPENDAQ_PACKAGE_VERSION}") +add_compile_definitions(OPENDAQ_OPCUA_PACKAGE_VERSION="${OPENDAQ_PACKAGE_VERSION}") + +add_subdirectory(opcuageneric_client) + +if (${REPO_OPTION_PREFIX}_ENABLE_TESTS) + #add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcuageneric/opcuageneric_client/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/CMakeLists.txt new file mode 100644 index 0000000..662e357 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcuatms_client VERSION ${${REPO_OPTION_PREFIX}_VERSION} LANGUAGES CXX) + +add_subdirectory(src) + +if (${REPO_OPTION_PREFIX}_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h new file mode 100644 index 0000000..2cc4de5 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h @@ -0,0 +1,27 @@ +#pragma once + +#include "opcuageneric.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +static const char* GENERIC_OPCUA_CLIENT_DEVICE_NAME = "GenericOPCUAClientPseudoDevice"; +static const char* DaqOpcUaGenericDevicePrefix = "daq.opcua.generic"; +static const char* OpcUaGenericScheme = "opc.tcp"; + +// Property names +static constexpr const char* PROPERTY_NAME_OPCUA_HOST = "Host"; +static constexpr const char* PROPERTY_NAME_OPCUA_PORT = "Port"; +static constexpr const char* PROPERTY_NAME_OPCUA_PATH = "Path"; +static constexpr const char* PROPERTY_NAME_OPCUA_USERNAME = "Username"; +static constexpr const char* PROPERTY_NAME_OPCUA_PASSWORD = "Password"; + +// Defaults +static constexpr const char* DEFAULT_OPCUA_HOST = "127.0.0.1"; +static constexpr uint16_t DEFAULT_OPCUA_PORT = 4840; +static constexpr const char* DEFAULT_OPCUA_USERNAME = ""; +static constexpr const char* DEFAULT_OPCUA_PASSWORD = ""; +static constexpr const char* DEFAULT_OPCUA_PATH = ""; + +static constexpr const char* GENERIC_OPCUA_MONITORED_ITEM_FB_NAME = "MonitoredItem"; + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h new file mode 100644 index 0000000..67d2744 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h @@ -0,0 +1,57 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include "opcuaclient/opcuaclient.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +class OpcuaGenericClientDeviceImpl : public Device +{ +public: + explicit OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, const ComponentPtr& parent, const PropertyObjectPtr& config); + static PropertyObjectPtr createDefaultConfig(); +protected: + static std::atomic localIndex; + static std::string getLocalId(); + + void removed() override; + DeviceInfoPtr onGetInfo() override; + + bool allowAddFunctionBlocksFromModules() override + { + return true; + }; + DictPtr onGetAvailableFunctionBlockTypes() override; + FunctionBlockPtr onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) override; + + void initNestedFbTypes(); + + DictObjectPtr nestedFbTypes; + + StringPtr connectionString; + EnumerationPtr connectionStatus; + + std::atomic connectedDone{false}; + std::unordered_map deviceMap; // device name -> signal list JSON + + daq::opcua::OpcUaClientPtr client; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcuageneric.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcuageneric.h new file mode 100644 index 0000000..f728108 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcuageneric.h @@ -0,0 +1,27 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#define BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC \ + namespace daq::opcua::generic \ + { +#define END_NAMESPACE_OPENDAQ_OPCUA_GENERIC } + diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt new file mode 100644 index 0000000..471a33e --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt @@ -0,0 +1,52 @@ +set(LIB_NAME opcuageneric_client) +set(HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + + +set(SRC_PublicHeaders constants.h + opcuageneric.h + generic_client_device_impl.h +) +set(SRC_Cpp generic_client_device_impl.cpp) + +source_group("device" FILES ${HEADERS_DIR}/generic_client_device_impl.h + generic_client_device_impl.cpp +) +source_group("common" FILES ${HEADERS_DIR}/constants.h + ${HEADERS_DIR}/opcuageneric.h +) + +opendaq_prepend_include(${LIB_NAME} SRC_PublicHeaders) + +add_library(${LIB_NAME} STATIC ${SRC_Cpp} + ${SRC_PublicHeaders} +) + +add_library(${OPENDAQ_SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +if(BUILD_64Bit OR BUILD_ARM) + set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) +else() + set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) +endif() + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +elseif (MINGW AND CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(${LIB_NAME} PRIVATE -Wa,-mbig-obj) +endif() + +target_link_libraries(${LIB_NAME} + PUBLIC + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuaclient + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + + $ +) + +set_target_properties(${LIB_NAME} PROPERTIES PUBLIC_HEADER "${SRC_PublicHeaders}") + +opendaq_set_output_lib_name(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp new file mode 100644 index 0000000..7200661 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp @@ -0,0 +1,126 @@ +#include + +#include +#include +#include +#include "opcuashared/opcuaendpoint.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +std::atomic OpcuaGenericClientDeviceImpl::localIndex = 0; + +OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, const ComponentPtr& parent, const PropertyObjectPtr& config) + : Device(ctx, parent, getLocalId()), + connectionStatus(Enumeration("ConnectionStatusType", "Connected", this->context.getTypeManager())) +{ + this->name = GENERIC_OPCUA_CLIENT_DEVICE_NAME; + + const auto host = config.getPropertyValue(PROPERTY_NAME_OPCUA_HOST).asPtr().toStdString(); + const auto port = config.getPropertyValue(PROPERTY_NAME_OPCUA_PORT).asPtr().getValue(DEFAULT_OPCUA_PORT); + const auto path = config.getPropertyValue(PROPERTY_NAME_OPCUA_PATH).asPtr().toStdString(); + + connectionString = std::string(OpcUaGenericScheme) + "://" + host + ":" + std::to_string(port) + path; + + auto endpoint = OpcUaEndpoint(connectionString); + + endpoint.setUsername(config.getPropertyValue(PROPERTY_NAME_OPCUA_USERNAME)); + endpoint.setPassword(config.getPropertyValue(PROPERTY_NAME_OPCUA_PASSWORD)); + + try + { + client = std::make_shared(endpoint); + client->connect(); + client->runIterate(); + } + catch (const OpcUaException& e) + { + switch (e.getStatusCode()) + { + case UA_STATUSCODE_BADUSERACCESSDENIED: + case UA_STATUSCODE_BADIDENTITYTOKENINVALID: + DAQ_THROW_EXCEPTION(AuthenticationFailedException, e.what()); + default: + DAQ_THROW_EXCEPTION(NotFoundException, e.what()); + } + } + + + initComponentStatus(); + initNestedFbTypes(); +} + +PropertyObjectPtr OpcuaGenericClientDeviceImpl::createDefaultConfig() +{ + auto defaultConfig = PropertyObject(); + + defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_HOST, DEFAULT_OPCUA_HOST)); + defaultConfig.addProperty(IntProperty(PROPERTY_NAME_OPCUA_PORT, DEFAULT_OPCUA_PORT)); + defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_PATH, DEFAULT_OPCUA_PATH)); + defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_USERNAME, DEFAULT_OPCUA_USERNAME)); + defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_PASSWORD, DEFAULT_OPCUA_PASSWORD)); + + return defaultConfig; +} + +void OpcuaGenericClientDeviceImpl::removed() +{ + Device::removed(); +} + +DeviceInfoPtr OpcuaGenericClientDeviceImpl::onGetInfo() +{ + return DeviceInfo(connectionString, GENERIC_OPCUA_CLIENT_DEVICE_NAME); +} + + +void OpcuaGenericClientDeviceImpl::initNestedFbTypes() +{ + nestedFbTypes = Dict(); + // Add a function block type for monitoring an OPCUA node + // { + // const auto fbType = MonitoredItemFbImpl::CreateType(); + // nestedFbTypes.set(fbType.getId(), fbType); + // } +} + + +DictPtr OpcuaGenericClientDeviceImpl::onGetAvailableFunctionBlockTypes() +{ + return nestedFbTypes; +} + +FunctionBlockPtr OpcuaGenericClientDeviceImpl::onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) +{ + FunctionBlockPtr nestedFunctionBlock; + { + if (nestedFbTypes.hasKey(typeId)) + { + // auto fbTypePtr = nestedFbTypes.getOrDefault(typeId); + // if (fbTypePtr.getName() == GENERIC_OPCUA_MONITORED_ITEM_FB_NAME) + // { + // nestedFunctionBlock = createWithImplementation(...); + // } + // else + // { + // setComponentStatusWithMessage(ComponentStatus::Error, "Function block type is not available: " + typeId.toStdString()); + // return nestedFunctionBlock; + // } + } + if (nestedFunctionBlock.assigned()) + { + addNestedFunctionBlock(nestedFunctionBlock); + setComponentStatus(ComponentStatus::Ok); + } + else + { + DAQ_THROW_EXCEPTION(NotFoundException, "Function block type is not available: " + typeId.toStdString()); + } + } + return nestedFunctionBlock; +} + +std::string OpcuaGenericClientDeviceImpl::getLocalId() +{ + return std::string(GENERIC_OPCUA_CLIENT_DEVICE_NAME + std::to_string(localIndex++)); +} +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt new file mode 100644 index 0000000..5960be6 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt @@ -0,0 +1,48 @@ +set(MODULE_NAME opcuageneric_client) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_app.cpp + test_daq_test_helper.h + test_opcua_generic_client_device.cpp +) + +set(SUPPORTS_ASAN 0) + +if(CXX_COMPILER_ID EQUAL Clang OR CXX_COMPILER_ID EQUAL AppleClang OR CXX_COMPILER_ID EQUAL GNU) + set(SUPPORTS_ASAN 1) + set(ASAN_COMPILE_FLAGS -fsanitize=address -fno-omit-frame-pointer) +endif() + +add_executable(${TEST_APP} ${TEST_SOURCES}) + +set_target_properties(${TEST_APP} PROPERTIES DEBUG_POSTFIX _debug) + +if (WIN32) + set(BCRYPT_LIB bcrypt.dll) +endif() + +target_link_libraries(${TEST_APP} PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_test_utils gtest + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuageneric_client + ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_generic_client_module +) + +if(SUPPORTS_ASAN) + target_link_libraries(${TEST_APP} PRIVATE asan) + target_compile_options(${TEST_APP} PRIVATE -Wall -Werror ${CGOV_COMPILE_FLAGS} ${ASAN_COMPILE_FLAGS}) +endif() + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (MSVC) + target_compile_options(${TEST_APP} PRIVATE /wd4324 /bigobj) +elseif (MINGW AND CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(${TEST_APP} PRIVATE -Wa,-mbig-obj) +endif() + +if (COMMAND setup_target_for_coverage AND OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}testcoverage ${TEST_APP} ${MODULE_NAME}testcoverage) +endif() diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_app.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_app.cpp new file mode 100644 index 0000000..5e029a5 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_app.cpp @@ -0,0 +1,19 @@ +#include + +#include +#include + +int main(int argc, char** args) +{ + { + daq::ModuleManager("."); + } + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new DaqMemCheckListener()); + + auto res = RUN_ALL_TESTS(); + + return res; +} diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h new file mode 100644 index 0000000..7b27791 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include + +namespace daq::opcua::generic +{ +class DaqTestHelper +{ +public: + daq::InstancePtr daqInstance; + daq::DevicePtr device; + + void StartUp(std::string connectionStr = "daq.opcua.generic://127.0.0.1") + { + DaqInstanceInit(); + DaqOpcuaGenericClientDeviceInit(connectionStr); + } + + daq::InstancePtr DaqInstanceInit() + { + if (!daqInstance.assigned()) + daqInstance = daq::Instance(); + return daqInstance; + } + + daq::GenericDevicePtr DaqOpcuaGenericClientDeviceInit(std::string connectionStr) + { + if (!device.assigned()) + device = daqInstance.addDevice(connectionStr); + + return device; + } + + static daq::ModulePtr CreateModule() + { + daq::ModulePtr module; + createModule(&module, daq::NullContext()); + return module; + } +}; +} diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp new file mode 100644 index 0000000..5228b1f --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include +#include +#include "opcuageneric_client/constants.h" +#include "test_daq_test_helper.h" + +namespace daq::opcua::generic +{ +class GenericOpcuaClientDeviceTest : public testing::Test, public DaqTestHelper +{ +}; +} // namespace daq::modules::mqtt_streaming_module + +using namespace daq; +using namespace daq::opcua::generic; + +TEST_F(GenericOpcuaClientDeviceTest, DefaultDeviceConfig) +{ + const auto module = CreateModule(); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAGenericStreaming")); + auto defaultConfig = deviceTypes.get("OpenDAQOPCUAGenericStreaming").createDefaultConfig(); + ASSERT_TRUE(defaultConfig.assigned()); + + ASSERT_EQ(defaultConfig.getAllProperties().getCount(), 5u); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_HOST)); + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_PORT)); + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_PATH)); + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_USERNAME)); + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_PASSWORD)); + + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_HOST).getValueType(), CoreType::ctString); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_PORT).getValueType(), CoreType::ctInt); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_PATH).getValueType(), CoreType::ctString); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_USERNAME).getValueType(), CoreType::ctString); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_PASSWORD).getValueType(), CoreType::ctString); + + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_HOST), DEFAULT_OPCUA_HOST); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_PORT), DEFAULT_OPCUA_PORT); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_PATH), DEFAULT_OPCUA_PATH); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_USERNAME), DEFAULT_OPCUA_USERNAME); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_PASSWORD), DEFAULT_OPCUA_PASSWORD); +} + +TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) +{ + const auto instance = Instance(); + daq::GenericDevicePtr device; + ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1")); + ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), + Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); + ASSERT_EQ(device.getInfo().getName(), GENERIC_OPCUA_CLIENT_DEVICE_NAME); + auto devices = instance.getDevices(); + bool contain = false; + daq::GenericDevicePtr deviceFromList; + for (const auto& d : devices) + { + contain = (d.getName() == GENERIC_OPCUA_CLIENT_DEVICE_NAME); + if (contain) + { + deviceFromList = d; + break; + } + } + ASSERT_TRUE(contain); + ASSERT_TRUE(deviceFromList.assigned()); + ASSERT_EQ(deviceFromList.getInfo().getName(), device.getInfo().getName()); + ASSERT_TRUE(deviceFromList == device); +} From 2d97dba4dc69ac6b3b9714fffcf2416af6124a86 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 25 Mar 2026 18:58:16 +0100 Subject: [PATCH 02/29] opcua generic client: MonitoredItem FB --- .../include/opcuageneric_client/constants.h | 24 +- .../generic_client_device_impl.h | 2 +- .../opcua_monitored_item_fb_impl.h | 99 +++++ .../opcuageneric_client/src/CMakeLists.txt | 12 +- .../src/generic_client_device_impl.cpp | 33 +- .../src/opcua_monitored_item_fb_impl.cpp | 390 ++++++++++++++++++ .../opcuageneric_client/tests/CMakeLists.txt | 3 + .../tests/opcuaservertesthelper.cpp | 359 ++++++++++++++++ .../tests/opcuaservertesthelper.h | 106 +++++ .../tests/test_daq_test_helper.h | 6 +- .../test_opcua_generic_client_device.cpp | 43 +- .../tests/test_opcua_monitored_item_fb.cpp | 255 ++++++++++++ 12 files changed, 1307 insertions(+), 25 deletions(-) create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h index 2cc4de5..647afbc 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h @@ -4,24 +4,46 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC +// Constants static const char* GENERIC_OPCUA_CLIENT_DEVICE_NAME = "GenericOPCUAClientPseudoDevice"; static const char* DaqOpcUaGenericDevicePrefix = "daq.opcua.generic"; static const char* OpcUaGenericScheme = "opc.tcp"; +static const char* OPCUA_LOCAL_MONITORED_ITEM_FB_ID_PREFIX = "MonitoredItemFb"; +static constexpr const char* OPCUA_VALUE_SIGNAL_LOCAL_ID = "OpcUaValueSignal"; +static constexpr const char* OPCUA_TS_SIGNAL_LOCAL_ID = "OpcUaDomainSignal"; + + + +// FB names +static constexpr const char* GENERIC_OPCUA_MONITORED_ITEM_FB_NAME = "MonitoredItem"; + // Property names +// ---------- +// Device and module static constexpr const char* PROPERTY_NAME_OPCUA_HOST = "Host"; static constexpr const char* PROPERTY_NAME_OPCUA_PORT = "Port"; static constexpr const char* PROPERTY_NAME_OPCUA_PATH = "Path"; static constexpr const char* PROPERTY_NAME_OPCUA_USERNAME = "Username"; static constexpr const char* PROPERTY_NAME_OPCUA_PASSWORD = "Password"; +// MonitoredItem FB +static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID_TYPE = "NodeIDType"; +static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID = "NodeID"; +static constexpr const char* PROPERTY_NAME_OPCUA_NAMESPACE_INDEX = "NamespaceIndex"; +static constexpr const char* PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL = "SamplingInterval"; +// ---------- + // Defaults +// ---------- +// Device and module static constexpr const char* DEFAULT_OPCUA_HOST = "127.0.0.1"; static constexpr uint16_t DEFAULT_OPCUA_PORT = 4840; static constexpr const char* DEFAULT_OPCUA_USERNAME = ""; static constexpr const char* DEFAULT_OPCUA_PASSWORD = ""; static constexpr const char* DEFAULT_OPCUA_PATH = ""; -static constexpr const char* GENERIC_OPCUA_MONITORED_ITEM_FB_NAME = "MonitoredItem"; +static constexpr const uint32_t DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL = 100; // in milliseconds +// ---------- END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h index 67d2744..3b30c36 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h @@ -29,7 +29,7 @@ class OpcuaGenericClientDeviceImpl : public Device static PropertyObjectPtr createDefaultConfig(); protected: static std::atomic localIndex; - static std::string getLocalId(); + static std::string generateLocalId(); void removed() override; DeviceInfoPtr onGetInfo() override; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h new file mode 100644 index 0000000..9c4b5e3 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -0,0 +1,99 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include "opcuaclient/opcuaclient.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +class OpcUaMonitoredItemFbImpl final : public FunctionBlock +{ + friend class GenericOpcuaMonitoredItemTest; +public: + + // only string NodeIDs are supported at the moment + enum class NodeIDType : int + { + //Numeric = 0, + String = 0, + //Guid, + //Opaque, + _count + }; + + explicit OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const FunctionBlockTypePtr& type, + daq::opcua::OpcUaClientPtr client, + const PropertyObjectPtr& config = nullptr); + ~OpcUaMonitoredItemFbImpl(); + /*DAQ_OPCUA_MODULE_API*/ static FunctionBlockTypePtr CreateType(); +protected: + + struct FbConfig { + NodeIDType nodeIdType; + std::string nodeId; + uint32_t namespaceIndex; + uint32_t samplingInterval; + }; + + static std::atomic localIndex; + static std::unordered_map supportedDataTypes; + + DataDescriptorPtr outputSignalDescriptor; + SignalConfigPtr outputSignal; + SignalConfigPtr outputDomainSignal; + std::atomic configValid; + std::string configMsg; + std::atomic nodeValidationError; + std::string nodeValidationErrorMsg; + std::atomic valueValidationError; + + FbConfig config; + daq::opcua::OpcUaClientPtr client; + OpcUaNodeId nodeId; + OpcUaNodeId nodeDataType; + + std::thread readerThread; + std::atomic running; + + void removed() override; + static std::string generateLocalId(); + + void adjustSignalDescriptor(); + void createSignal(); + void reconfigureSignal(const FbConfig& prevConfig); + SignalConfigPtr createDomainSignal(); + + void initProperties(const PropertyObjectPtr& config); + void readProperties(); + void propertyChanged(); + + void updateStatuses(); + + void validateNode(); + bool validateValueDataType(const OpcUaVariant& value); + + void runReaderThread(); + void readerLoop(); + + daq::DataPacketPtr buildDataPacket(const OpcUaVariant& value); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt index 471a33e..c98c49b 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt @@ -5,14 +5,20 @@ set(HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) set(SRC_PublicHeaders constants.h opcuageneric.h generic_client_device_impl.h + opcua_monitored_item_fb_impl.h +) +set(SRC_Cpp generic_client_device_impl.cpp + opcua_monitored_item_fb_impl.cpp ) -set(SRC_Cpp generic_client_device_impl.cpp) +source_group("common" FILES ${HEADERS_DIR}/constants.h + ${HEADERS_DIR}/opcuageneric.h +) source_group("device" FILES ${HEADERS_DIR}/generic_client_device_impl.h generic_client_device_impl.cpp ) -source_group("common" FILES ${HEADERS_DIR}/constants.h - ${HEADERS_DIR}/opcuageneric.h +source_group("function_block" FILES ${HEADERS_DIR}/opcua_monitored_item_fb_impl.h + opcua_monitored_item_fb_impl.cpp ) opendaq_prepend_include(${LIB_NAME} SRC_PublicHeaders) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp index 7200661..db9557d 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "opcuashared/opcuaendpoint.h" BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC @@ -10,7 +11,7 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC std::atomic OpcuaGenericClientDeviceImpl::localIndex = 0; OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, const ComponentPtr& parent, const PropertyObjectPtr& config) - : Device(ctx, parent, getLocalId()), + : Device(ctx, parent, generateLocalId()), connectionStatus(Enumeration("ConnectionStatusType", "Connected", this->context.getTypeManager())) { this->name = GENERIC_OPCUA_CLIENT_DEVICE_NAME; @@ -77,10 +78,10 @@ void OpcuaGenericClientDeviceImpl::initNestedFbTypes() { nestedFbTypes = Dict(); // Add a function block type for monitoring an OPCUA node - // { - // const auto fbType = MonitoredItemFbImpl::CreateType(); - // nestedFbTypes.set(fbType.getId(), fbType); - // } + { + const auto fbType = OpcUaMonitoredItemFbImpl::CreateType(); + nestedFbTypes.set(fbType.getId(), fbType); + } } @@ -95,16 +96,16 @@ FunctionBlockPtr OpcuaGenericClientDeviceImpl::onAddFunctionBlock(const StringPt { if (nestedFbTypes.hasKey(typeId)) { - // auto fbTypePtr = nestedFbTypes.getOrDefault(typeId); - // if (fbTypePtr.getName() == GENERIC_OPCUA_MONITORED_ITEM_FB_NAME) - // { - // nestedFunctionBlock = createWithImplementation(...); - // } - // else - // { - // setComponentStatusWithMessage(ComponentStatus::Error, "Function block type is not available: " + typeId.toStdString()); - // return nestedFunctionBlock; - // } + auto fbTypePtr = nestedFbTypes.getOrDefault(typeId); + if (fbTypePtr.getName() == GENERIC_OPCUA_MONITORED_ITEM_FB_NAME) + { + nestedFunctionBlock = createWithImplementation(context, functionBlocks, fbTypePtr, client, config); + } + else + { + setComponentStatusWithMessage(ComponentStatus::Error, "Function block type is not available: " + typeId.toStdString()); + return nestedFunctionBlock; + } } if (nestedFunctionBlock.assigned()) { @@ -119,7 +120,7 @@ FunctionBlockPtr OpcuaGenericClientDeviceImpl::onAddFunctionBlock(const StringPt return nestedFunctionBlock; } -std::string OpcuaGenericClientDeviceImpl::getLocalId() +std::string OpcuaGenericClientDeviceImpl::generateLocalId() { return std::string(GENERIC_OPCUA_CLIENT_DEVICE_NAME + std::to_string(localIndex++)); } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp new file mode 100644 index 0000000..d633c3f --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -0,0 +1,390 @@ +#include +#include +#include "opendaq/binary_data_packet_factory.h" +#include "opendaq/packet_factory.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +std::atomic OpcUaMonitoredItemFbImpl::localIndex = 0; + +std::unordered_map OpcUaMonitoredItemFbImpl::supportedDataTypes = {{OpcUaNodeId(), daq::SampleType::Undefined}, + {OpcUaNodeId(0, UA_NS0ID_FLOAT), daq::SampleType::Float32}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLE), daq::SampleType::Float64}, + {OpcUaNodeId(0, UA_NS0ID_SBYTE), daq::SampleType::Int8}, + {OpcUaNodeId(0, UA_NS0ID_BYTE), daq::SampleType::UInt8}, + {OpcUaNodeId(0, UA_NS0ID_INT16), daq::SampleType::Int16}, + {OpcUaNodeId(0, UA_NS0ID_UINT16), daq::SampleType::UInt16}, + {OpcUaNodeId(0, UA_NS0ID_INT32), daq::SampleType::Int32}, + {OpcUaNodeId(0, UA_NS0ID_UINT32), daq::SampleType::UInt32}, + {OpcUaNodeId(0, UA_NS0ID_INT64), daq::SampleType::Int64}, + {OpcUaNodeId(0, UA_NS0ID_UINT64), daq::SampleType::UInt64}, + {OpcUaNodeId(0, UA_NS0ID_STRING), daq::SampleType::String}}; + +namespace +{ + PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& defaultConfig, const PropertyObjectPtr& config) + { + auto newConfig = PropertyObject(); + for (const auto& prop : defaultConfig.getAllProperties()) + { + newConfig.addProperty(prop.asPtr(true).clone()); + const auto propName = prop.getName(); + newConfig.setPropertyValue(propName, config.hasProperty(propName) ? config.getPropertyValue(propName) : prop.getValue()); + } + return newConfig; + } + + template + retT readProperty(const PropertyObjectPtr objPtr, const std::string& propertyName, const retT defaultValue) + { + retT returnValue{defaultValue}; + if (objPtr.hasProperty(propertyName)) + { + auto property = objPtr.getPropertyValue(propertyName).asPtrOrNull(); + if (property.assigned()) + { + returnValue = property.getValue(defaultValue); + } + } + return returnValue; + } +} + +OpcUaMonitoredItemFbImpl::OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const FunctionBlockTypePtr& type, + daq::opcua::OpcUaClientPtr client, + const PropertyObjectPtr& config) + : FunctionBlock(type, ctx, parent, generateLocalId()) + , configValid(false) + , nodeValidationError(false) + , valueValidationError(false) + , client(client) + , nodeId() + , running(false) +{ + initComponentStatus(); + if (config.assigned()) + initProperties(populateDefaultConfig(type.createDefaultConfig(), config)); + else + initProperties(type.createDefaultConfig()); + + nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; + + validateNode(); + adjustSignalDescriptor(); + createSignal(); + updateStatuses(); + runReaderThread(); +} + +OpcUaMonitoredItemFbImpl::~OpcUaMonitoredItemFbImpl() +{ + if (readerThread.joinable()) + { + running = false; + readerThread.join(); + } +} + +void OpcUaMonitoredItemFbImpl::removed() +{ + if (readerThread.joinable()) + { + running = false; + readerThread.join(); + } + FunctionBlock::removed(); +} + +FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() +{ + auto defaultConfig = PropertyObject(); + { + auto builder = + SelectionPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, List("String"), static_cast(NodeIDType::String)) + .setDescription("Defines the type of the NodeID of the OPCUA node to monitor. By default it is set to String. Other " + "formats are not supported at the moment."); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = StringPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID, String("")) + .setDescription(fmt::format("Specifies the NodeID of the OPCUA node to monitor. The format of the NodeID should correspond " + "to the type specified in the \"{}\" property.", PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = IntPropertyBuilder(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, Integer(0)) + .setDescription("Specifies the namespace index of the OPCUA node to monitor. This property is optional and can " + "be left empty. If not set, the first occurence of NodeID will be used"); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = + IntPropertyBuilder(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, Integer(DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL)) + .setDescription(fmt::format( + "Specifies the sampling interval in milliseconds for monitoring the OPCUA node. By default it is set to {} ms.", + DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL)); + defaultConfig.addProperty(builder.build()); + } + + const auto fbType = + FunctionBlockType(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, + GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, + "Monitors a specified OPCUA node and outputs the value and timestamp as signals.", + defaultConfig); + return fbType; +} + +std::string OpcUaMonitoredItemFbImpl::generateLocalId() +{ + return std::string(OPCUA_LOCAL_MONITORED_ITEM_FB_ID_PREFIX + std::to_string(localIndex++)); +} + +void OpcUaMonitoredItemFbImpl::adjustSignalDescriptor() +{ + if (nodeValidationError == false && supportedDataTypes.count(nodeDataType) != 0) + { + outputSignalDescriptor = DataDescriptorBuilder().setSampleType(supportedDataTypes[nodeDataType]).build(); + } + else + { + outputSignalDescriptor = DataDescriptorBuilder().setSampleType(daq::SampleType::Undefined).build(); + } +} + +void OpcUaMonitoredItemFbImpl::initProperties(const PropertyObjectPtr& config) +{ + for (const auto& prop : config.getAllProperties()) + { + const auto propName = prop.getName(); + if (!objPtr.hasProperty(propName)) + { + if (const auto internalProp = prop.asPtrOrNull(true); internalProp.assigned()) + { + objPtr.addProperty(internalProp.clone()); + objPtr.setPropertyValue(propName, prop.getValue()); + objPtr.getOnPropertyValueWrite(prop.getName()) += + [this](PropertyObjectPtr&, PropertyValueEventArgsPtr&) { propertyChanged(); }; + } + } + else + { + objPtr.setPropertyValue(propName, prop.getValue()); + } + } + readProperties(); +} + +void OpcUaMonitoredItemFbImpl::readProperties() +{ + auto lock = this->getRecursiveConfigLock(); + configValid = true; + configMsg.clear(); + + config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment + config.nodeId = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID, ""); + if (config.nodeId.empty()) + { + configMsg = fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID); + configValid = false; + } + + config.namespaceIndex = readProperty(objPtr, PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 0); + config.samplingInterval = + readProperty(objPtr, PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); + if (config.samplingInterval <= 0) + { + configMsg = fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL); + configValid = false; + config.samplingInterval = DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL; + } + + updateStatuses(); +} + +void OpcUaMonitoredItemFbImpl::propertyChanged() +{ + auto lock = this->getRecursiveConfigLock(); + auto prevConfig = config; + readProperties(); + + nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; + + validateNode(); + adjustSignalDescriptor(); + reconfigureSignal(prevConfig); + updateStatuses(); +} + +void OpcUaMonitoredItemFbImpl::updateStatuses() +{ + if (configValid == false) + { + setComponentStatusWithMessage(ComponentStatus::Error, "Configuration is invalid! " + configMsg); + } + else if (nodeValidationError) + { + setComponentStatusWithMessage(ComponentStatus::Error, "Node is invalid! " + nodeValidationErrorMsg); + } + else + { + setComponentStatusWithMessage(ComponentStatus::Ok, "Parsing succeeded"); + } +} + +void OpcUaMonitoredItemFbImpl::validateNode() +{ + nodeValidationError = false; + valueValidationError = false; + nodeValidationErrorMsg.clear(); + try { + auto nodeExist = client->nodeExists(nodeId); + if (!nodeExist) + { + nodeValidationError = true; + nodeValidationErrorMsg = fmt::format("Node {} does not exist", nodeId.toString()); + } + else if (const auto nodeClass = client->readNodeClass(nodeId); nodeClass != UA_NodeClass::UA_NODECLASS_VARIABLE) + { + nodeValidationError = true; + nodeValidationErrorMsg = fmt::format("Node {} is not a variable node", nodeId.toString()); + } + else if (nodeDataType = client->readDataType(nodeId); supportedDataTypes.count(nodeDataType) == 0) + { + nodeValidationError = true; + nodeValidationErrorMsg = fmt::format("Node {} has unsupported DataType ({})", nodeId.toString(), nodeDataType.toString()); + } + else + { + nodeValidationError = false; + } + } + catch (OpcUaException& ex) + { + nodeValidationError = true; + if (ex.getStatusCode() == UA_STATUSCODE_BADUSERACCESSDENIED) + { + nodeValidationErrorMsg = fmt::format("Access denied for node {}", nodeId.toString()); + } + else + { + nodeValidationErrorMsg = fmt::format("Exception was thrown while node {} validatiion", nodeId.toString()); + } + } +} + +bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaVariant& value) +{ + OpcUaNodeId valueDataType(value.getValue().type->typeId); + if (valueDataType != nodeDataType) + { + nodeDataType = std::move(valueDataType); + adjustSignalDescriptor(); + outputSignal.setDescriptor(outputSignalDescriptor); + } + + valueValidationError = !(value.isNumber() || value.isString()); + return !valueValidationError; +} + +void OpcUaMonitoredItemFbImpl::createSignal() +{ + auto lock = this->getRecursiveConfigLock(); + LOG_I("Creating a signal..."); + + outputSignal = createAndAddSignal(OPCUA_VALUE_SIGNAL_LOCAL_ID, outputSignalDescriptor); + outputSignal.setDomainSignal(createDomainSignal()); +} + +void OpcUaMonitoredItemFbImpl::reconfigureSignal(const FbConfig& prevConfig) +{ + auto lock = this->getRecursiveConfigLock(); +} + +SignalConfigPtr OpcUaMonitoredItemFbImpl::createDomainSignal() +{ + const auto domainSignalDsc = DataDescriptorBuilder() + .setSampleType(SampleType::UInt64) + .setRule(ExplicitDataRule()) + .setUnit(Unit("s", -1, "seconds", "time")) + .setTickResolution(Ratio(1, 1'000'000)) + .setOrigin("1970-01-01T00:00:00") + .setName("Time") + .build(); + outputDomainSignal = createAndAddSignal(OPCUA_TS_SIGNAL_LOCAL_ID, domainSignalDsc, false); + return outputDomainSignal; +} + +void OpcUaMonitoredItemFbImpl::runReaderThread() +{ + running = true; + readerThread = std::thread([this] { readerLoop(); }); +} + +void OpcUaMonitoredItemFbImpl::readerLoop() +{ + while (running) + { + { + //auto lockProcessing = std::scoped_lock(processingMutex); + if (configValid && nodeValidationError == false) + { + + OpcUaVariant opcUaVariant; + try { + opcUaVariant = client->readValue(nodeId); + if (!validateValueDataType(opcUaVariant)) + { + // updateStatuses? + } + else + { + const auto dp = buildDataPacket(opcUaVariant); + outputSignal.sendPacket(dp); + } + } + catch (OpcUaException&) + { + LOG_E("Exeption while reading \"{}\"", nodeId.toString()); + } + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(config.samplingInterval)); + } +} + +DataPacketPtr OpcUaMonitoredItemFbImpl::buildDataPacket(const OpcUaVariant& value) +{ + DataPacketPtr dp; + if (value.isString()) + { + const auto convertedValue = value.toString(); + dp = daq::BinaryDataPacket(nullptr, outputSignalDescriptor, convertedValue.size()); + std::memcpy(dp.getRawData(), convertedValue.data(), convertedValue.size()); + } + else if (value.isInteger()) + { + dp = daq::DataPacket(outputSignalDescriptor, 1); + *(static_cast(dp.getRawData())) = value.toInteger(); + } + else if (value.isReal()) + { + if (value.getValue().type->typeKind == UA_TYPES_FLOAT) + { + dp = daq::DataPacket(outputSignalDescriptor, 1); + *(static_cast(dp.getRawData())) = value.toFloat(); + } + else if (value.getValue().type->typeKind == UA_TYPES_DOUBLE) + { + dp = daq::DataPacket(outputSignalDescriptor, 1); + *(static_cast(dp.getRawData())) = value.toDouble(); + } + } + return dp; +} + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt index 5960be6..d4e40d0 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt @@ -2,8 +2,11 @@ set(MODULE_NAME opcuageneric_client) set(TEST_APP test_${MODULE_NAME}) set(TEST_SOURCES test_app.cpp + opcuaservertesthelper.h + opcuaservertesthelper.cpp test_daq_test_helper.h test_opcua_generic_client_device.cpp + test_opcua_monitored_item_fb.cpp ) set(SUPPORTS_ASAN 0) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp new file mode 100644 index 0000000..68984e9 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp @@ -0,0 +1,359 @@ +#include "opcuaservertesthelper.h" +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaServerTestHelper::OpcUaServerTestHelper() + : sessionTimeoutMs(-1) +{ +} + +OpcUaServerTestHelper::~OpcUaServerTestHelper() +{ + stop(); +} + +void OpcUaServerTestHelper::setSessionTimeout(double sessionTimeoutMs) +{ + this->sessionTimeoutMs = sessionTimeoutMs; +} + +void OpcUaServerTestHelper::runServer() +{ + while (serverRunning) + UA_Server_run_iterate(server, true); + + UA_Server_run_shutdown(server); + UA_Server_delete(server); +} + +void OpcUaServerTestHelper::onConfigure(const OnConfigureCallback& callback) +{ + onConfigureCallback = callback; +} + +void OpcUaServerTestHelper::startServer() +{ + serverRunning = true; + + UA_ServerConfig initConfig; + std::memset(&initConfig, 0, sizeof(UA_ServerConfig)); + + if (onConfigureCallback) + onConfigureCallback(&initConfig); + + UA_ServerConfig_setMinimal(&initConfig, port, nullptr); + server = UA_Server_newWithConfig(&initConfig); + UA_ServerConfig* config = UA_Server_getConfig(server); + + if (sessionTimeoutMs > 0) + config->maxSessionTimeout = sessionTimeoutMs; + +#ifdef UA_ENABLE_WEBSOCKET_SERVER + UA_ServerConfig_addNetworkLayerWS(config, 80, 0, 0); +#endif // UA_ENABLE_WEBSOCKET_SERVER + + createModel(); + + UA_Server_run_startup(server); + +#if SYNTH_SERVER_DEBUG + runServer(); +#else + serverThreadPtr = std::make_unique(&OpcUaServerTestHelper::runServer, this); +#endif +} + +void OpcUaServerTestHelper::stop() +{ + serverRunning = false; + if (serverThreadPtr) + { + serverThreadPtr->join(); + serverThreadPtr.reset(); + } +} + +std::string OpcUaServerTestHelper::getServerUrl() const +{ + std::stringstream ss; + ss << "opc.tcp://127.0.0.1"; + ss << ":"; + ss << port; + return ss.str(); +} + +void OpcUaServerTestHelper::createModel() +{ + auto uaObjectsFolder = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); + + publishFolder("f1", &uaObjectsFolder); + + { + UA_Int32 myInt32 = 33; + publishVariable("f1.i", &myInt32, &UA_TYPES[UA_TYPES_INT32], OpcUaNodeId(1, "f1").getPtr()); + } + + { + UA_SByte myInt8 = -8; + publishVariable(".i8", &myInt8, &UA_TYPES[UA_TYPES_SBYTE], &uaObjectsFolder); + + UA_Byte myUInt8 = 8; + publishVariable(".ui8", &myUInt8, &UA_TYPES[UA_TYPES_BYTE], &uaObjectsFolder); + } + + { + UA_Int16 myInt16 = -16; + publishVariable(".i16", &myInt16, &UA_TYPES[UA_TYPES_INT16], &uaObjectsFolder); + + UA_UInt16 myUInt16 = 16; + publishVariable(".ui16", &myUInt16, &UA_TYPES[UA_TYPES_UINT16], &uaObjectsFolder); + } + + { + UA_Int32 myInt32 = -32; + publishVariable(".i32", &myInt32, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder); + + UA_UInt32 myUInt32 = 32; + publishVariable(".ui32", &myUInt32, &UA_TYPES[UA_TYPES_UINT32], &uaObjectsFolder); + } + + { + UA_Int64 myInt64 = -64; + publishVariable(".i64", &myInt64, &UA_TYPES[UA_TYPES_INT64], &uaObjectsFolder); + + UA_UInt64 myUInt64 = 64; + publishVariable(".ui64", &myUInt64, &UA_TYPES[UA_TYPES_UINT64], &uaObjectsFolder); + } + + UA_Boolean myBool = true; + publishVariable(".b", &myBool, &UA_TYPES[UA_TYPES_BOOLEAN], &uaObjectsFolder); + + UA_Double myDouble = (UA_Double) 1885 / (UA_Double) 14442; + publishVariable(".d", &myDouble, &UA_TYPES[UA_TYPES_DOUBLE], &uaObjectsFolder); + + UA_Float myFloat = (UA_Float) 1 / (UA_Float) 3; + publishVariable(".f", &myFloat, &UA_TYPES[UA_TYPES_FLOAT], &uaObjectsFolder); + + UA_String myString = UA_STRING_ALLOC("Hello Dewesoft"); + publishVariable(".s", &myString, &UA_TYPES[UA_TYPES_STRING], &uaObjectsFolder); + UA_String_clear(&myString); + + UA_Guid myGuid = UA_GUID("8a336ac1-8632-482c-a565-23e6a9ad1abc"); + publishVariable(".g", &myGuid, &UA_TYPES[UA_TYPES_GUID], &uaObjectsFolder); + + UA_StatusCode myStatus = UA_STATUSCODE_GOODSUBSCRIPTIONTRANSFERRED; + publishVariable(".sc", &myStatus, &UA_TYPES[UA_TYPES_STATUSCODE], &uaObjectsFolder); + + // vectors + + UA_Int32 myVecInt32[] = {12, 13, 15, 18}; + publishVariable(".i32v", &myVecInt32, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder, "en_US", 1, 4); + + UA_Int16 myVecInt16[] = {65, 18, 12, 17, 33, 10023, 12, 1, 0, -1}; + publishVariable(".i16v", &myVecInt16, &UA_TYPES[UA_TYPES_INT16], &uaObjectsFolder, "en_US", 1, 10); + + UA_Int64 myVecInt64[] = {55, 1993}; + publishVariable(".i64v", &myVecInt64, &UA_TYPES[UA_TYPES_INT64], &uaObjectsFolder, "en_US", 1, 2); + + UA_Boolean myVecBool[] = {true, false}; + publishVariable(".bv", &myVecBool, &UA_TYPES[UA_TYPES_BOOLEAN], &uaObjectsFolder, "en_US", 1, 2); + + UA_Double myVecDouble[] = {(UA_Double) 1993 / (UA_Double) 6625, (UA_Double) 185 / (UA_Double) 1443, (UA_Double) 1.44, (UA_Double) 9948}; + publishVariable(".dv", &myVecDouble, &UA_TYPES[UA_TYPES_DOUBLE], &uaObjectsFolder, "en_US", 1, 4); + + UA_Float myVecFloat[] = {(UA_Float) 7 / (UA_Float) 2, (UA_Float) 1 / (UA_Float) 5}; + publishVariable(".fv", &myVecFloat, &UA_TYPES[UA_TYPES_FLOAT], &uaObjectsFolder, "en_US", 1, 2); + + // methods + + publishMethod("hello.dewesoft", &uaObjectsFolder); + + // structures + + OpcUaNodeId structureNodeId(1, ".sctA"); + + UA_Int32 myInt32 = 56; + publishVariable(".sctA", &myInt32, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder); + + myInt32 = 5641; + publishVariable(".sctA.i32", &myInt32, &UA_TYPES[UA_TYPES_INT32], structureNodeId.getPtr()); + + UA_Double mySctDouble = (UA_Double) 9844 / (UA_Double) 19774; + publishVariable(".sctA.d", &mySctDouble, &UA_TYPES[UA_TYPES_DOUBLE], structureNodeId.getPtr()); + + UA_String mySctString = UA_STRING_ALLOC("Hello Dewesoft @ struct"); + publishVariable(".sctA.s", &mySctString, &UA_TYPES[UA_TYPES_STRING], structureNodeId.getPtr()); + UA_String_clear(&mySctString); +} + +void OpcUaServerTestHelper::publishVariable(std::string identifier, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale, + uint16_t nodeIndex, + size_t dimension) +{ + OpcUaObject attr = UA_VariableAttributes_default; + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->dataType = type->typeId; + attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + + OpcUaNodeId newNodeId(nodeIndex, identifier); + + OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), identifier.c_str()); + + if (dimension > 1) + { + attr->valueRank = 1; + attr->arrayDimensionsSize = 1; + attr->arrayDimensions = static_cast(UA_Array_new(1, &UA_TYPES[UA_TYPES_UINT32])); + attr->arrayDimensions[0] = UA_UInt32(dimension); + UA_Variant_setArrayCopy(&attr->value, value, dimension, type); + } + else + { + UA_Variant_setScalarCopy(&attr->value, value, type); + } + auto status = UA_Server_addVariableNode(server, + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + *qualifiedName, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), + *attr, + NULL, + NULL); + + CheckStatusCodeException(status); +} + +void OpcUaServerTestHelper::writeNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value) +{ + CheckStatusCodeException(UA_Server_writeValue(server, *nodeId, *value)); +} + +void OpcUaServerTestHelper::publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale, int nodeIndex) +{ + OpcUaObject attr = UA_ObjectAttributes_default; + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier); + + OpcUaNodeId newNodeId(nodeIndex, identifier); + + OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), identifier); + + auto status = UA_Server_addObjectNode(server, + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + *qualifiedName, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), + *attr, + NULL, + NULL); + + CheckStatusCodeException(status); +} + +void OpcUaServerTestHelper::publishMethod(std::string identifier, UA_NodeId* parentNodeId, const char* locale, int nodeIndex) +{ + OpcUaObject inputArgument; + inputArgument->description = UA_LOCALIZEDTEXT_ALLOC("en-US", "Input"); + inputArgument->name = UA_STRING_ALLOC("Input"); + inputArgument->dataType = UA_TYPES[UA_TYPES_STRING].typeId; + inputArgument->valueRank = -1; + + OpcUaObject outputArgument; + outputArgument->description = UA_LOCALIZEDTEXT_ALLOC("en-US", "Output"); + outputArgument->name = UA_STRING_ALLOC("Output"); + outputArgument->dataType = UA_TYPES[UA_TYPES_STRING].typeId; + outputArgument->valueRank = -1; + + OpcUaObject attr = UA_MethodAttributes_default; + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->executable = true; + attr->userExecutable = true; + + OpcUaNodeId newNodeId(nodeIndex, identifier); + + UA_Server_addMethodNode(server, + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASORDEREDCOMPONENT), + UA_QUALIFIEDNAME(1, (char*) identifier.c_str()), + *attr, + helloMethodCallback, + 1, + inputArgument.get(), + 1, + outputArgument.get(), + NULL, + NULL); +} + +UA_StatusCode OpcUaServerTestHelper::helloMethodCallback(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionHandle, + const UA_NodeId* methodId, + void* methodContext, + const UA_NodeId* objectId, + void* objectContext, + size_t inputSize, + const UA_Variant* input, + size_t outputSize, + UA_Variant* output) +{ + UA_String* inputStr = (UA_String*) input->data; + std::string out = "Hello!"; + + if (inputStr->length > 0) + { + std::string in = utils::ToStdString(*inputStr); + std::stringstream ss; + ss << out << " (R:" << in << ")"; + out = ss.str(); + } + + UA_String tmp = UA_STRING_ALLOC(out.c_str()); + UA_Variant_setScalarCopy(output, &tmp, &UA_TYPES[UA_TYPES_STRING]); + UA_String_clear(&tmp); + return UA_STATUSCODE_GOOD; +} + +/*SampleServerTest*/ + +void BaseClientTest::SetUp() +{ + testing::Test::SetUp(); + testHelper.startServer(); +} +void BaseClientTest::TearDown() +{ + testHelper.stop(); + testing::Test::TearDown(); +} + +std::string BaseClientTest::getServerUrl() const +{ + return testHelper.getServerUrl(); +} + +OpcUaClientPtr BaseClientTest::prepareAndConnectClient(int timeout) +{ + std::shared_ptr client = std::make_shared(getServerUrl()); + + if (timeout >= 0) + client->setTimeout(timeout); + + client->connect(); + return client; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h new file mode 100644 index 0000000..f5fdd25 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h @@ -0,0 +1,106 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "opcuaclient/opcuaclient.h" +#include "opcuashared/opcua.h" +#include "opcuashared/opcuacommon.h" +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +#define ASSERT_EQ_STATUS(status, expectedStatus) ASSERT_EQ(status, (UA_StatusCode) expectedStatus) + +class OpcUaServerTestHelper final +{ +public: + using OnConfigureCallback = std::function; + + OpcUaServerTestHelper(); + ~OpcUaServerTestHelper(); + + void setSessionTimeout(double sessionTimeoutMs); + + void onConfigure(const OnConfigureCallback& callback); + void startServer(); + void stop(); + + std::string getServerUrl() const; + + void publishVariable(std::string identifier, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale = "en_US", + uint16_t nodeIndex = 1, + size_t dimension = 1); + + void writeNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value); + +private: + void runServer(); + void createModel(); + void publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale = "en_US", int nodeIndex = 1); + void publishMethod(std::string identifier, UA_NodeId* parentNodeId, const char* locale = "en_US", int nodeIndex = 1); + + static UA_StatusCode helloMethodCallback(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionHandle, + const UA_NodeId* methodId, + void* methodContext, + const UA_NodeId* objectId, + void* objectContext, + size_t inputSize, + const UA_Variant* input, + size_t outputSize, + UA_Variant* output); + + double sessionTimeoutMs; + UA_Server* server{}; + std::unique_ptr serverThreadPtr; + std::atomic serverRunning = false; + + UA_UInt16 port = 4842u; + OnConfigureCallback onConfigureCallback; +}; + +class BaseClientTest : public testing::Test +{ +protected: + void SetUp() override; + void TearDown() override; + OpcUaServerTestHelper testHelper; + + std::string getServerUrl() const; + + static void IterateAndWaitForPromise(OpcUaClient& client, const std::future& future) + { + using namespace std::chrono; + while (client.iterate(milliseconds(10)) == UA_STATUSCODE_GOOD && + future.wait_for(milliseconds(1)) != std::future_status::ready) + { + }; + } + + OpcUaClientPtr prepareAndConnectClient(int timeout = -1); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h index 7b27791..3cedeaa 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_daq_test_helper.h @@ -11,7 +11,7 @@ class DaqTestHelper daq::InstancePtr daqInstance; daq::DevicePtr device; - void StartUp(std::string connectionStr = "daq.opcua.generic://127.0.0.1") + void StartUp(std::string connectionStr = "daq.opcua.generic://127.0.0.1:4842", daq::PropertyObjectPtr config = nullptr) { DaqInstanceInit(); DaqOpcuaGenericClientDeviceInit(connectionStr); @@ -24,10 +24,10 @@ class DaqTestHelper return daqInstance; } - daq::GenericDevicePtr DaqOpcuaGenericClientDeviceInit(std::string connectionStr) + daq::GenericDevicePtr DaqOpcuaGenericClientDeviceInit(std::string connectionStr, daq::PropertyObjectPtr config = nullptr) { if (!device.assigned()) - device = daqInstance.addDevice(connectionStr); + device = daqInstance.addDevice(connectionStr, config); return device; } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp index 5228b1f..0ea8072 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp @@ -6,12 +6,25 @@ #include #include #include "opcuageneric_client/constants.h" +#include "opcuaservertesthelper.h" #include "test_daq_test_helper.h" namespace daq::opcua::generic { class GenericOpcuaClientDeviceTest : public testing::Test, public DaqTestHelper { +protected: + void SetUp() override + { + testing::Test::SetUp(); + testHelper.startServer(); + } + void TearDown() override + { + testHelper.stop(); + testing::Test::TearDown(); + } + OpcUaServerTestHelper testHelper; }; } // namespace daq::modules::mqtt_streaming_module @@ -55,7 +68,7 @@ TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) { const auto instance = Instance(); daq::GenericDevicePtr device; - ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1")); + ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842")); ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); ASSERT_EQ(device.getInfo().getName(), GENERIC_OPCUA_CLIENT_DEVICE_NAME); @@ -76,3 +89,31 @@ TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) ASSERT_EQ(deviceFromList.getInfo().getName(), device.getInfo().getName()); ASSERT_TRUE(deviceFromList == device); } + +TEST_F(GenericOpcuaClientDeviceTest, RemovingDevice) +{ + const auto instance = Instance(); + daq::GenericDevicePtr device; + { + ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842")); + ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), + Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); + ASSERT_NO_THROW(instance.removeDevice(device)); + } + + { + ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842")); + ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), + Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); + ASSERT_NO_THROW(instance.removeDevice(device)); + } +} + +TEST_F(GenericOpcuaClientDeviceTest, CheckDeviceFunctionalBlocks) +{ + StartUp(); + daq::DictPtr fbTypes; + ASSERT_NO_THROW(fbTypes = device.getAvailableFunctionBlockTypes()); + ASSERT_GE(fbTypes.getCount(), 1); + ASSERT_TRUE(fbTypes.hasKey(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME)); +} diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp new file mode 100644 index 0000000..0dbcada --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -0,0 +1,255 @@ +#include +#include +#include +#include +#include +#include +#include "opcuageneric_client/constants.h" +#include "opcuaservertesthelper.h" +#include "test_daq_test_helper.h" + +namespace daq::opcua::generic +{ + class GenericOpcuaMonitoredItemHelper : public DaqTestHelper + { + public: + daq::FunctionBlockPtr fb; + OpcUaServerTestHelper testHelper; + + void CreateMonitoredItemFB(std::string nodeId, uint32_t index, uint32_t interval = 100) + { + auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, nodeId); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, index); + config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, interval); + + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + } + + void CreateMonitoredItemFB(daq::PropertyObjectPtr config) + { + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + } + + auto okStatus() + { + return Enumeration("ComponentStatusType", "Ok", daqInstance.getContext().getTypeManager()); + } + + auto errStatus() + { + return Enumeration("ComponentStatusType", "Error", daqInstance.getContext().getTypeManager()); + } + + protected: + void SetUp() + { + testHelper.startServer(); + } + + void TearDown() + { + if (fb.assigned()) + device.removeFunctionBlock(fb); + testHelper.stop(); + } + }; + + class GenericOpcuaMonitoredItemTest : public testing::Test, public GenericOpcuaMonitoredItemHelper + { + protected: + void SetUp() override + { + testing::Test::SetUp(); + GenericOpcuaMonitoredItemHelper::SetUp(); + } + void TearDown() override + { + GenericOpcuaMonitoredItemHelper::TearDown(); + testing::Test::TearDown(); + } + }; + + using H = std::variant; + + template + struct HelperValueType + { + using type = T; + }; + + // clang-format off +template struct TypeName { static std::string Get() { return "unknown"; } }; +template<> struct TypeName { static std::string Get() { return "float"; } }; +template<> struct TypeName { static std::string Get() { return "double"; } }; +template<> struct TypeName { static std::string Get() { return "int64_t"; } }; +template<> struct TypeName { static std::string Get() { return "uint64_t"; } }; +template<> struct TypeName { static std::string Get() { return "int32_t"; } }; +template<> struct TypeName { static std::string Get() { return "uint32_t"; } }; +template<> struct TypeName { static std::string Get() { return "int16_t"; } }; +template<> struct TypeName { static std::string Get() { return "uint16_t"; } }; +template<> struct TypeName { static std::string Get() { return "int8_t"; } }; +template<> struct TypeName { static std::string Get() { return "uint8_t"; } }; +template<> struct TypeName { static std::string Get() { return "string"; } }; +// clang-format on + +std::string ParamNameGenerator(const testing::TestParamInfo>& info) +{ + return std::visit( + [](auto& h) + { + using T = typename HelperValueType>::type; + std::string name = "Type_" + TypeName::Get(); + return name; + }, + info.param.second); +} +class GenericOpcuaMonitoredItemPTest : public ::testing::TestWithParam>, public GenericOpcuaMonitoredItemHelper +{ +protected: + void SetUp() override + { + testing::Test::SetUp(); + GenericOpcuaMonitoredItemHelper::SetUp(); + } + void TearDown() override + { + GenericOpcuaMonitoredItemHelper::TearDown(); + testing::Test::TearDown(); + } +}; + +} // namespace daq::modules::mqtt_streaming_module + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::generic; + +TEST_F(GenericOpcuaMonitoredItemTest, DefaultConfig) +{ + daq::PropertyObjectPtr defaultConfig = OpcUaMonitoredItemFbImpl::CreateType().createDefaultConfig(); + + ASSERT_TRUE(defaultConfig.assigned()); + + EXPECT_EQ(defaultConfig.getAllProperties().getCount(), 4u); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE).getValueType(), CoreType::ctInt); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE).asPtr(), + static_cast(OpcUaMonitoredItemFbImpl::NodeIDType::String)); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE).getVisible()); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID).getValueType(), CoreType::ctString); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID).asPtr().getLength(), 0u); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID).getVisible()); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX).getValueType(), CoreType::ctInt); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX).asPtr(), 0); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX).getVisible()); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL).getValueType(), CoreType::ctInt); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL) + .asPtr() + .getValue(DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL), + DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL).getVisible()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, CreationWithDefaultConfig) +{ + StartUp(); + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME)); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, CreationWithPartialConfig) +{ + StartUp(); + { + auto config = PropertyObject(); + config.addProperty(StringProperty(PROPERTY_NAME_OPCUA_NODE_ID, String("unknownNodeId"))); + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + device.removeFunctionBlock(fb); + } + + { + auto config = PropertyObject(); + config.addProperty(StringProperty(PROPERTY_NAME_OPCUA_NODE_ID, String(".i32"))); + config.addProperty(IntProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1)); + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + device.removeFunctionBlock(fb); + } + fb = nullptr; + +} + +TEST_F(GenericOpcuaMonitoredItemTest, CreationWithCustomConfig) +{ + StartUp(); + auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, ".i32"); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); + config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 100); + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); +} + +TEST_P(GenericOpcuaMonitoredItemPTest, ReadValue) +{ + constexpr uint32_t interval = 50; + StartUp(); + auto param = GetParam(); + + CreateMonitoredItemFB(param.first.getIdentifier(), param.first.getNamespaceIndex(), interval); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + std::visit( + [&](auto& templateParam) + { + using T = std::decay_t; + + OpcUaVariant variant; + if constexpr (std::is_same_v) + variant = OpcUaVariant(templateParam.c_str()); + else + variant.setScalar(templateParam); + + ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); + + std::this_thread::sleep_for(std::chrono::milliseconds(interval * 3)); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + auto val = fb.getSignals()[0].getLastValue(); + + if constexpr (std::is_same_v) + ASSERT_DOUBLE_EQ(val, templateParam); + else if constexpr (std::is_same_v) + ASSERT_FLOAT_EQ(val, templateParam); + else + ASSERT_EQ(val, templateParam); + }, + param.second); +} + +INSTANTIATE_TEST_SUITE_P(ReadNumericValue, + GenericOpcuaMonitoredItemPTest, + ::testing::Values(std::pair{OpcUaNodeId(1, ".ui8"), H{uint8_t{88}}}, + std::pair{OpcUaNodeId(1, ".i8"), H{int8_t{-88}}}, + std::pair{OpcUaNodeId(1, ".ui16"), H{uint16_t{456}}}, + std::pair{OpcUaNodeId(1, ".i16"), H{int16_t{-456}}}, + std::pair{OpcUaNodeId(1, ".ui32"), H{uint32_t{85535}}}, + std::pair{OpcUaNodeId(1, ".i32"), H{int32_t{-85535}}}, + std::pair{OpcUaNodeId(1, ".ui64"), H{uint64_t{8055535}}}, + std::pair{OpcUaNodeId(1, ".i64"), H{int64_t{-8055535}}}, + std::pair{OpcUaNodeId(1, ".d"), H{double{123.456789}}}, + std::pair{OpcUaNodeId(1, ".f"), H{float{float(1) / 3}}}, + std::pair{OpcUaNodeId(1, ".s"), H{std::string{"String with a value"}}}), + ParamNameGenerator); \ No newline at end of file From a4ae980a4eb8e71097bed3c9af4f5fa8d7c0c376 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Fri, 27 Mar 2026 11:29:01 +0100 Subject: [PATCH 03/29] MonitoredItem FB: reading values with timestamps; tests; --- .../include/opcuaclient/opcuaclient.h | 5 +- .../include/opcuaclient/opcuareadvalueid.h | 41 --- .../opcua/opcuaclient/src/CMakeLists.txt | 2 - .../opcua/opcuaclient/src/opcuaclient.cpp | 42 +-- .../opcuaclient/src/opcuareadvalueid.cpp | 14 - .../include/opcuashared/opcuadatavalue.h | 27 +- .../opcua/opcuashared/src/opcuadatavalue.cpp | 48 ++- .../opcuashared/tests/test_opcuadatavalue.cpp | 48 ++- .../opcua_monitored_item_fb_impl.h | 20 +- .../src/opcua_monitored_item_fb_impl.cpp | 135 +++++++-- .../opcuageneric_client/tests/CMakeLists.txt | 1 + .../tests/test_opcua_monitored_item_fb.cpp | 273 ++++++++++++++++-- .../opcuageneric_client/tests/timer.h | 50 ++++ 13 files changed, 535 insertions(+), 171 deletions(-) delete mode 100644 shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h delete mode 100644 shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/tests/timer.h diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h index 13c67d4..729456c 100644 --- a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h @@ -29,7 +29,6 @@ #include #include -#include #include #include #include @@ -40,6 +39,7 @@ #include #include "opcuashared/node/opcuanodemethod.h" +#include "opcuashared/opcuadatavalue.h" #include @@ -126,6 +126,7 @@ class OpcUaClient bool nodeExists(const OpcUaNodeId& nodeId); OpcUaVariant readValue(const OpcUaNodeId& node); + OpcUaDataValue readDataValue(const OpcUaNodeId& node); UA_NodeClass readNodeClass(const OpcUaNodeId& nodeId); std::string readBrowseName(const OpcUaNodeId& nodeId); std::string readDisplayName(const OpcUaNodeId& nodeId); @@ -147,8 +148,6 @@ class OpcUaClient OpcUaObject readNodeAttributes(const OpcUaObject& request); - void readNodeAttributes(const std::vector& request); - Subscription* createSubscription(const OpcUaObject& request, const StatusChangeNotificationCallbackType& statusChangeCallback = nullptr); diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h deleted file mode 100644 index 0593203..0000000 --- a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2022-2025 openDAQ d.o.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once -#include -#include -#include -#include - -BEGIN_NAMESPACE_OPENDAQ_OPCUA - -struct OpcUaReadValueId : public OpcUaObject -{ -public: - using AttributeIdType = decltype(UA_ReadValueId::attributeId); -}; - -struct OpcUaReadValueIdWithCallback : public OpcUaReadValueId -{ - using ProcessFunctionType = std::function; - OpcUaReadValueIdWithCallback(const OpcUaNodeId& nodeId, - const ProcessFunctionType& processFunction, - AttributeIdType attributeId = UA_ATTRIBUTEID_VALUE); - - ProcessFunctionType processFunction; -}; - -END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt b/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt index 8bc86e7..3796a8c 100644 --- a/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt +++ b/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt @@ -5,7 +5,6 @@ set(SOURCE_CPPS opcuaclient.cpp opcuatimertaskhelper.cpp opcuanodefactory.cpp opcuaasyncexecthread.cpp - opcuareadvalueid.cpp opcuacallmethodrequest.cpp subscriptions.cpp monitored_item_create_request.cpp @@ -27,7 +26,6 @@ set(SOURCE_HEADERS opcuaclient.h opcuatimertaskhelper.h opcuanodefactory.h opcuaasyncexecthread.h - opcuareadvalueid.h opcuacallmethodrequest.h subscriptions.h monitored_item_create_request.h diff --git a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp index ebb9ccb..5fc9729 100644 --- a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp +++ b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp @@ -394,34 +394,34 @@ OpcUaVariant OpcUaClient::readValue(const OpcUaNodeId& node) return val; } -OpcUaObject OpcUaClient::readNodeAttributes(const OpcUaObject& request) +OpcUaDataValue OpcUaClient::readDataValue(const OpcUaNodeId& node) { - return UA_Client_Service_read(getLockedUaClient(), *request); -} + OpcUaObject request; -void OpcUaClient::readNodeAttributes(const std::vector& requests) -{ - size_t requestCnt = std::size(requests); - if (requestCnt == 0) - return; + request->nodesToRead = UA_ReadValueId_new(); + request->nodesToReadSize = 1; - OpcUaObject readRequest; - CheckStatusCodeException( - UA_Array_resize((void**) &readRequest->nodesToRead, &readRequest->nodesToReadSize, requestCnt, &UA_TYPES[UA_TYPES_READVALUEID])); + request->nodesToRead[0].nodeId = node.copyAndGetDetachedValue(); + request->nodesToRead[0].attributeId = UA_ATTRIBUTEID_VALUE; - for (size_t i = 0; i < requestCnt; i++) - UA_ReadValueId_copy(requests[i].get(), &readRequest->nodesToRead[i]); + request->timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH; - readRequest->timestampsToReturn = UA_TimestampsToReturn::UA_TIMESTAMPSTORETURN_NEITHER; + OpcUaObject response = UA_Client_Service_read(getLockedUaClient(), *request); + const auto status = response->responseHeader.serviceResult; + if (status != UA_STATUSCODE_GOOD) + throw OpcUaException(status, "Attribute read request failed"); + if (response->resultsSize != 1) + throw OpcUaException(UA_STATUSCODE_BADINVALIDSTATE, "Read request returned incorrect number of results"); - OpcUaObject response = readNodeAttributes(readRequest); - CheckStatusCodeException(response->responseHeader.serviceResult); - for (size_t i = 0; i < requestCnt; i++) - { - UA_DataValue* v = &response->results[i]; - requests[i].processFunction(v); - } + OpcUaDataValue result(*response->results); + + return result; +} + +OpcUaObject OpcUaClient::readNodeAttributes(const OpcUaObject& request) +{ + return UA_Client_Service_read(getLockedUaClient(), *request); } OpcUaObject OpcUaClient::callMethods(const OpcUaObject& request) diff --git a/shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp b/shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp deleted file mode 100644 index 735f9b7..0000000 --- a/shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "opcuaclient/opcuareadvalueid.h" - -BEGIN_NAMESPACE_OPENDAQ_OPCUA - -OpcUaReadValueIdWithCallback::OpcUaReadValueIdWithCallback(const OpcUaNodeId& nodeId, - const ProcessFunctionType& processFunction, - AttributeIdType attributeId) - : processFunction(processFunction) -{ - getValue().nodeId = nodeId.copyAndGetDetachedValue(); - getValue().attributeId = attributeId; -} - -END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h index ba0950f..e7bc6e6 100644 --- a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h @@ -16,7 +16,6 @@ #pragma once -#include "opcuacommon.h" #include "opcuavariant.h" BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -24,24 +23,30 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA class OpcUaDataValue; using OpcUaDataValuePtr = std::shared_ptr; -class OpcUaDataValue +class OpcUaDataValue : public OpcUaObject { public: - OpcUaDataValue(const UA_DataValue* dataValue); - virtual ~OpcUaDataValue(); + using OpcUaObject::OpcUaObject; + + static uint64_t toUnixTimeUs(UA_DateTime date); + + const UA_DataValue& getDataValue() const; bool hasValue() const; - const OpcUaVariant& getValue() const; - const UA_StatusCode& getStatusCode() const; + OpcUaVariant getValue() const; - bool isStatusOK() const; + UA_StatusCode getStatusCode() const; + + bool hasServerTimestamp() const; + UA_DateTime getServerTimestampUnixEpoch() const; // us - const UA_DataValue* getDataValue() const; - operator const UA_DataValue*() const; + bool hasSourceTimestamp() const; + UA_DateTime getSourceTimestampUnixEpoch() const; // us + + bool isStatusOK() const; protected: - const UA_DataValue* dataValue; - const OpcUaVariant variant; + }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp index 7665deb..7d85318 100644 --- a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp +++ b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp @@ -2,44 +2,60 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA -OpcUaDataValue::OpcUaDataValue(const UA_DataValue* dataValue) - : dataValue(dataValue) - , variant(dataValue->value, true) +OpcUaVariant OpcUaDataValue::getValue() const { + return OpcUaVariant(OpcUaObject::getValue().value, true); } -OpcUaDataValue::~OpcUaDataValue() +UA_StatusCode OpcUaDataValue::getStatusCode() const { + if (!getDataValue().hasStatus) + return UA_STATUSCODE_BADUNEXPECTEDERROR; + return OpcUaObject::getValue().status; } -bool OpcUaDataValue::hasValue() const +bool OpcUaDataValue::isStatusOK() const { - return dataValue->hasValue; + if (!getDataValue().hasStatus) + return false; + return (getStatusCode() == UA_STATUSCODE_GOOD); } -const OpcUaVariant& OpcUaDataValue::getValue() const +bool OpcUaDataValue::hasServerTimestamp() const { - return variant; + return getDataValue().hasServerTimestamp; } -const UA_StatusCode& OpcUaDataValue::getStatusCode() const +bool OpcUaDataValue::hasSourceTimestamp() const { - return dataValue->status; + return getDataValue().hasSourceTimestamp; } -bool OpcUaDataValue::isStatusOK() const +UA_DateTime OpcUaDataValue::getServerTimestampUnixEpoch() const { - return (getStatusCode() == UA_STATUSCODE_GOOD); + return (hasServerTimestamp()) ? toUnixTimeUs(getDataValue().serverTimestamp) : 0; +} + +UA_DateTime OpcUaDataValue::getSourceTimestampUnixEpoch() const +{ + return (hasSourceTimestamp()) ? toUnixTimeUs(getDataValue().sourceTimestamp) : 0; +} + +const UA_DataValue& OpcUaDataValue::getDataValue() const +{ + return OpcUaObject::getValue(); } -const UA_DataValue* OpcUaDataValue::getDataValue() const +bool OpcUaDataValue::hasValue() const { - return dataValue; + return getDataValue().hasValue; } -OpcUaDataValue::operator const UA_DataValue*() const +uint64_t OpcUaDataValue::toUnixTimeUs(UA_DateTime date) { - return getDataValue(); + if (date == 0) + return 0; + return static_cast((date - UA_DATETIME_UNIX_EPOCH) / UA_DATETIME_USEC); } END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp index 6c4f6e4..e3787b3 100644 --- a/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp @@ -12,11 +12,13 @@ TEST_F(OpcUaDataValueTest, CreateWithInt) UA_DataValue_init(&dataValue); dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + dataValue.hasStatus = true; UA_Int64 val = 0; UA_Variant_setScalarCopy(&dataValue.value, &val, &UA_TYPES[UA_TYPES_INT64]); + dataValue.hasValue = true; - OpcUaDataValue value(&dataValue); + OpcUaDataValue value(dataValue, true); ASSERT_TRUE(value.getValue().isInteger()); ASSERT_EQ(value.getStatusCode(), UA_STATUSCODE_BADAGGREGATELISTMISMATCH); @@ -30,17 +32,19 @@ TEST_F(OpcUaDataValueTest, CreateWithIntRawDataValue) UA_DataValue_init(&dataValue); dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + dataValue.hasStatus = true; UA_Int64 val = 0; UA_Variant_setScalarCopy(&dataValue.value, &val, &UA_TYPES[UA_TYPES_INT64]); + dataValue.hasValue = true; - OpcUaDataValue value(&dataValue); + OpcUaDataValue value(dataValue, true); - const UA_DataValue* rawDataValue = value.getDataValue(); - ASSERT_EQ(rawDataValue, &dataValue); + const UA_DataValue& rawDataValue = value.getDataValue(); + ASSERT_EQ(rawDataValue.value.data, dataValue.value.data); - rawDataValue = value; - ASSERT_EQ(rawDataValue, &dataValue); + // rawDataValue = value; + // ASSERT_EQ(rawDataValue, &dataValue); UA_DataValue_clear(&dataValue); } @@ -51,12 +55,15 @@ TEST_F(OpcUaDataValueTest, TestNoCopyBehaviour) UA_DataValue_init(&dataValue); dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + dataValue.hasStatus = true; + UA_Int64* val = UA_Int64_new(); *val = 1; UA_Variant_setScalar(&dataValue.value, val, &UA_TYPES[UA_TYPES_INT64]); + dataValue.hasValue = true; - OpcUaDataValue value(&dataValue); + OpcUaDataValue value(dataValue, true); ASSERT_EQ(value.getValue().toInteger(), 1); *val = 2; ASSERT_EQ(value.getValue().toInteger(), 2); @@ -66,4 +73,31 @@ TEST_F(OpcUaDataValueTest, TestNoCopyBehaviour) UA_DataValue_clear(&dataValue); } +TEST_F(OpcUaDataValueTest, TestCopyBehaviour) +{ + UA_DataValue dataValue; + UA_DataValue_init(&dataValue); + + dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + dataValue.hasStatus = true; + + UA_Int64* val = UA_Int64_new(); + *val = 1; + + UA_Variant_setScalar(&dataValue.value, val, &UA_TYPES[UA_TYPES_INT64]); + dataValue.hasValue = true; + + OpcUaDataValue value(dataValue); + ASSERT_EQ(value.getValue().toInteger(), 1); + *val = 2; + ASSERT_EQ(value.getValue().toInteger(), 1); + + ASSERT_NE(value.getValue().getValue().data, dataValue.value.data); + + UA_DataValue_clear(&dataValue); + ASSERT_EQ(value.getValue().toInteger(), 1); + value.getValue().setScalar(UA_Int64(5)); + ASSERT_EQ(value.getValue().toInteger(), 5); +} + END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index 9c4b5e3..5c08e77 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -37,6 +37,14 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock _count }; + enum class DomainSource : int + { + None = 0, + ServerTimestamp, + SourceTimestamp, + _count + }; + explicit OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, const ComponentPtr& parent, const FunctionBlockTypePtr& type, @@ -46,11 +54,18 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock /*DAQ_OPCUA_MODULE_API*/ static FunctionBlockTypePtr CreateType(); protected: + struct DataPackets + { + daq::DataPacketPtr dataPacket; + daq::DataPacketPtr domainDataPacket; + }; + struct FbConfig { NodeIDType nodeIdType; std::string nodeId; uint32_t namespaceIndex; uint32_t samplingInterval; + DomainSource domainSource; }; static std::atomic localIndex; @@ -88,12 +103,13 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock void updateStatuses(); void validateNode(); - bool validateValueDataType(const OpcUaVariant& value); + bool validateValueDataType(const OpcUaDataValue& value); void runReaderThread(); void readerLoop(); - daq::DataPacketPtr buildDataPacket(const OpcUaVariant& value); + DataPackets buildDataPacket(const OpcUaDataValue& value); + daq::DataPacketPtr buildDomainDataPacket(const OpcUaDataValue& value); }; END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index d633c3f..27f2c6c 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -203,6 +203,8 @@ void OpcUaMonitoredItemFbImpl::readProperties() config.samplingInterval = DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL; } + config.domainSource = DomainSource::ServerTimestamp; + updateStatuses(); } @@ -277,9 +279,9 @@ void OpcUaMonitoredItemFbImpl::validateNode() } } -bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaVariant& value) +bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value) { - OpcUaNodeId valueDataType(value.getValue().type->typeId); + OpcUaNodeId valueDataType(value.getValue().getValue().type->typeId); if (valueDataType != nodeDataType) { nodeDataType = std::move(valueDataType); @@ -287,7 +289,7 @@ bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaVariant& value) outputSignal.setDescriptor(outputSignalDescriptor); } - valueValidationError = !(value.isNumber() || value.isString()); + valueValidationError = !(value.getValue().isNumber() || value.getValue().isString()); return !valueValidationError; } @@ -297,12 +299,27 @@ void OpcUaMonitoredItemFbImpl::createSignal() LOG_I("Creating a signal..."); outputSignal = createAndAddSignal(OPCUA_VALUE_SIGNAL_LOCAL_ID, outputSignalDescriptor); - outputSignal.setDomainSignal(createDomainSignal()); + if (config.domainSource != DomainSource::None) + outputSignal.setDomainSignal(createDomainSignal()); } void OpcUaMonitoredItemFbImpl::reconfigureSignal(const FbConfig& prevConfig) { auto lock = this->getRecursiveConfigLock(); + + if (config.domainSource != DomainSource::None) + { + if (outputDomainSignal.assigned()) + { + outputSignal.setDomainSignal(nullptr); + removeSignal(outputDomainSignal); + outputDomainSignal = nullptr; + } + } + else if (!outputDomainSignal.assigned()) + { + outputSignal.setDomainSignal(createDomainSignal()); + } } SignalConfigPtr OpcUaMonitoredItemFbImpl::createDomainSignal() @@ -334,17 +351,20 @@ void OpcUaMonitoredItemFbImpl::readerLoop() if (configValid && nodeValidationError == false) { - OpcUaVariant opcUaVariant; - try { - opcUaVariant = client->readValue(nodeId); + OpcUaDataValue opcUaVariant; + try + { + opcUaVariant = client->readDataValue(nodeId); if (!validateValueDataType(opcUaVariant)) { - // updateStatuses? + // updateStatuses? } else { - const auto dp = buildDataPacket(opcUaVariant); - outputSignal.sendPacket(dp); + const auto dps = buildDataPacket(opcUaVariant); + outputSignal.sendPacket(dps.dataPacket); + if (dps.domainDataPacket.assigned() && outputDomainSignal.assigned()) + outputDomainSignal.sendPacket(dps.domainDataPacket); } } catch (OpcUaException&) @@ -357,34 +377,87 @@ void OpcUaMonitoredItemFbImpl::readerLoop() } } -DataPacketPtr OpcUaMonitoredItemFbImpl::buildDataPacket(const OpcUaVariant& value) +OpcUaMonitoredItemFbImpl::DataPackets OpcUaMonitoredItemFbImpl::buildDataPacket(const OpcUaDataValue& value) { - DataPacketPtr dp; - if (value.isString()) - { - const auto convertedValue = value.toString(); - dp = daq::BinaryDataPacket(nullptr, outputSignalDescriptor, convertedValue.size()); - std::memcpy(dp.getRawData(), convertedValue.data(), convertedValue.size()); - } - else if (value.isInteger()) + DataPackets dps; + dps.domainDataPacket = buildDomainDataPacket(value); + + if (value.getValue().isString()) { - dp = daq::DataPacket(outputSignalDescriptor, 1); - *(static_cast(dp.getRawData())) = value.toInteger(); + const auto convertedValue = value.getValue().toString(); + dps.dataPacket = daq::BinaryDataPacket(dps.domainDataPacket, outputSignalDescriptor, convertedValue.size()); + std::memcpy(dps.dataPacket.getRawData(), convertedValue.data(), convertedValue.size()); } - else if (value.isReal()) + else if (value.getValue().isInteger() || value.getValue().isReal()) { - if (value.getValue().type->typeKind == UA_TYPES_FLOAT) - { - dp = daq::DataPacket(outputSignalDescriptor, 1); - *(static_cast(dp.getRawData())) = value.toFloat(); - } - else if (value.getValue().type->typeKind == UA_TYPES_DOUBLE) + if (dps.domainDataPacket.assigned()) + dps.dataPacket = daq::DataPacketWithDomain(dps.domainDataPacket, outputSignalDescriptor, 1); + else + dps.dataPacket = daq::DataPacket(outputSignalDescriptor, 1); + + switch (value.getValue().getValue().type->typeKind) { - dp = daq::DataPacket(outputSignalDescriptor, 1); - *(static_cast(dp.getRawData())) = value.toDouble(); + case UA_TYPES_SBYTE: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_BYTE: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_INT16: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_UINT16: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_INT32: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_UINT32: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_INT64: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_UINT64: + *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + break; + case UA_TYPES_FLOAT: + *(static_cast(dps.dataPacket.getRawData())) = value.getValue().toFloat(); + break; + case UA_TYPES_DOUBLE: + *(static_cast(dps.dataPacket.getRawData())) = value.getValue().toDouble(); + break; + default: + break; } } - return dp; + + return dps; +} + +DataPacketPtr OpcUaMonitoredItemFbImpl::buildDomainDataPacket(const OpcUaDataValue& value) +{ + DataPacketPtr domainDp; + if (!outputDomainSignal.assigned()) + return domainDp; + + auto fillDmainPacket = [this](uint64_t ts) + { + DataPacketPtr domainDp = daq::DataPacket(outputDomainSignal.getDescriptor(), 1); + std::memcpy(domainDp.getRawData(), &ts, sizeof(ts)); + return domainDp; + }; + + if (config.domainSource == DomainSource::ServerTimestamp && value.hasServerTimestamp()) + { + domainDp = fillDmainPacket(value.getServerTimestampUnixEpoch()); + } + else if (config.domainSource == DomainSource::SourceTimestamp && value.hasSourceTimestamp()) + { + domainDp = fillDmainPacket(value.getSourceTimestampUnixEpoch()); + } + + return domainDp; } END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt index d4e40d0..2c53f55 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt @@ -5,6 +5,7 @@ set(TEST_SOURCES test_app.cpp opcuaservertesthelper.h opcuaservertesthelper.cpp test_daq_test_helper.h + timer.h test_opcua_generic_client_device.cpp test_opcua_monitored_item_fb.cpp ) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 0dbcada..5e0bfcb 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -6,7 +6,14 @@ #include #include "opcuageneric_client/constants.h" #include "opcuaservertesthelper.h" +#include "opendaq/reader_factory.h" +#include "opendaq/sample_type_traits.h" #include "test_daq_test_helper.h" +#include "timer.h" + +#define ASSERT_DOUBLE_NE(val1, val2) ASSERT_GT(std::abs((val1) - (val2)), 1e-9) + +#define ASSERT_FLOAT_NE(val1, val2) ASSERT_GT(std::fabs((val1) - (val2)), 1e-6f) namespace daq::opcua::generic { @@ -188,7 +195,6 @@ TEST_F(GenericOpcuaMonitoredItemTest, CreationWithPartialConfig) device.removeFunctionBlock(fb); } fb = nullptr; - } TEST_F(GenericOpcuaMonitoredItemTest, CreationWithCustomConfig) @@ -223,33 +229,254 @@ TEST_P(GenericOpcuaMonitoredItemPTest, ReadValue) else variant.setScalar(templateParam); + daq::BaseObjectPtr prevVal; + daq::BaseObjectPtr val; + { + // before writing + { + // waiting to be sure that FB has read initial value + helper::utils::Timer timer(interval * 3); + do + { + prevVal = fb.getSignals()[0].getLastValue(); + } while (!prevVal.assigned() && !timer.expired()); + ASSERT_TRUE(prevVal.assigned()); + } + { + // check that the initial and target values are different + if constexpr (std::is_same_v) + ASSERT_DOUBLE_NE(prevVal.asPtr().getValue(T(0)), templateParam); + else if constexpr (std::is_same_v) + ASSERT_FLOAT_NE(prevVal.asPtr().getValue(T(0)), templateParam); + else if constexpr (std::is_same_v) + ASSERT_NE(prevVal, templateParam); + else + ASSERT_NE(prevVal.asPtr().getValue(T(0)), templateParam); + } + } + + // write new value to the node ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); - std::this_thread::sleep_for(std::chrono::milliseconds(interval * 3)); - ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - auto val = fb.getSignals()[0].getLastValue(); + { + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + { + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + helper::utils::Timer timer(interval * 3); + do + { + val = fb.getSignals()[0].getLastValue(); + } while (val == prevVal && !timer.expired()); + } + { + // check that the target and read values are the same + if constexpr (std::is_same_v) + ASSERT_DOUBLE_EQ(val.asPtr().getValue(T(0)), templateParam); + else if constexpr (std::is_same_v) + ASSERT_FLOAT_EQ(val.asPtr().getValue(T(0)), templateParam); + else if constexpr (std::is_same_v) + ASSERT_EQ(val, templateParam); + else + ASSERT_EQ(val.asPtr().getValue(T(0)), templateParam); + } + } + }, + param.second); +} + +TEST_P(GenericOpcuaMonitoredItemPTest, ReadValueWithServerTimestampUsingLastValue) +{ + constexpr uint32_t interval = 50; + StartUp(); + auto param = GetParam(); + + CreateMonitoredItemFB(param.first.getIdentifier(), param.first.getNamespaceIndex(), interval); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - if constexpr (std::is_same_v) - ASSERT_DOUBLE_EQ(val, templateParam); - else if constexpr (std::is_same_v) - ASSERT_FLOAT_EQ(val, templateParam); + fb.getSignals()[0].getDomainSignal(); + + auto domainSig = fb.getSignals()[0].getDomainSignal(); + ASSERT_TRUE(domainSig.assigned()); + + auto getTime = []() + { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); + }; + + std::visit( + [&](auto& templateParam) + { + using T = std::decay_t; + + OpcUaVariant variant; + if constexpr (std::is_same_v) + variant = OpcUaVariant(templateParam.c_str()); else - ASSERT_EQ(val, templateParam); + variant.setScalar(templateParam); + + daq::BaseObjectPtr prevVal; + daq::BaseObjectPtr val; + { + // before writing + // waiting to be sure that FB has read initial value + helper::utils::Timer timer(interval * 3); + do + { + prevVal = fb.getSignals()[0].getLastValue(); + } while (!prevVal.assigned() && !timer.expired()); + ASSERT_TRUE(prevVal.assigned()); + } + + const auto timeBefore = getTime(); + + ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); + + { + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + { + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + helper::utils::Timer timer(interval * 3); + do + { + val = fb.getSignals()[0].getLastValue(); + } while (val == prevVal && !timer.expired()); + } + + auto domainVal = domainSig.getLastValue(); + const auto timeAfter = getTime(); + { + // check that the target and read values are the same + if constexpr (std::is_same_v) + ASSERT_DOUBLE_EQ(val.asPtr().getValue(T(0)), templateParam); + else if constexpr (std::is_same_v) + ASSERT_FLOAT_EQ(val.asPtr().getValue(T(0)), templateParam); + else if constexpr (std::is_same_v) + ASSERT_EQ(val, templateParam); + else + ASSERT_EQ(val.asPtr().getValue(T(0)), templateParam); + } + + // check the ts is between start and stop points + ASSERT_TRUE(domainVal.assigned()); + EXPECT_GE(timeAfter, domainVal.asPtr().getValue(uint64_t(0))); + EXPECT_LE(timeBefore, domainVal.asPtr().getValue(uint64_t(0))); + } + }, + param.second); +} + +TEST_P(GenericOpcuaMonitoredItemPTest, ReadValueWithServerTimestampUsingTailReader) +{ + constexpr uint32_t interval = 50; + StartUp(); + auto param = GetParam(); + + CreateMonitoredItemFB(param.first.getIdentifier(), param.first.getNamespaceIndex(), interval); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + fb.getSignals()[0].getDomainSignal(); + + auto domainSig = fb.getSignals()[0].getDomainSignal(); + ASSERT_TRUE(domainSig.assigned()); + + auto getTime = []() + { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); + }; + + std::visit( + [&](auto& templateParam) + { + using T = std::decay_t; + + // TailReader does not support String type, just skip test + if constexpr (!std::is_same_v) + { + OpcUaVariant variant; + variant.setScalar(templateParam); + + daq::BaseObjectPtr prevVal; + { + // before writing + // waiting to be sure that FB has read initial value + helper::utils::Timer timer(interval * 3); + do + { + prevVal = fb.getSignals()[0].getLastValue(); + } while (!prevVal.assigned() && !timer.expired()); + ASSERT_TRUE(prevVal.assigned()); + } + + const auto timeBefore = getTime(); + + auto reader = TailReaderBuilder() + .setSignal(fb.getSignals()[0]) + .setHistorySize(1) + .setValueReadType(SampleTypeFromType::SampleType) + .setDomainReadType(SampleType::UInt64) + .setSkipEvents(true) + .build(); + + ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); + + { + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + { + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + helper::utils::Timer timer(interval * 3); + while (!timer.expired()) + { + } + } + + SizeT count{1}; + T values{}; + uint64_t domain{}; + reader.readWithDomain(&values, &domain, &count); + const auto timeAfter = getTime(); + + { + // check that the target and read values are the same + if constexpr (std::is_same_v) + ASSERT_DOUBLE_EQ(values, templateParam); + else if constexpr (std::is_same_v) + ASSERT_FLOAT_EQ(values, templateParam); + else + ASSERT_EQ(values, templateParam); + } + + // check the ts is between start and stop points + EXPECT_GE(timeAfter, domain); + EXPECT_LE(timeBefore, domain); + } + } }, param.second); } -INSTANTIATE_TEST_SUITE_P(ReadNumericValue, - GenericOpcuaMonitoredItemPTest, - ::testing::Values(std::pair{OpcUaNodeId(1, ".ui8"), H{uint8_t{88}}}, - std::pair{OpcUaNodeId(1, ".i8"), H{int8_t{-88}}}, - std::pair{OpcUaNodeId(1, ".ui16"), H{uint16_t{456}}}, - std::pair{OpcUaNodeId(1, ".i16"), H{int16_t{-456}}}, - std::pair{OpcUaNodeId(1, ".ui32"), H{uint32_t{85535}}}, - std::pair{OpcUaNodeId(1, ".i32"), H{int32_t{-85535}}}, - std::pair{OpcUaNodeId(1, ".ui64"), H{uint64_t{8055535}}}, - std::pair{OpcUaNodeId(1, ".i64"), H{int64_t{-8055535}}}, - std::pair{OpcUaNodeId(1, ".d"), H{double{123.456789}}}, - std::pair{OpcUaNodeId(1, ".f"), H{float{float(1) / 3}}}, - std::pair{OpcUaNodeId(1, ".s"), H{std::string{"String with a value"}}}), - ParamNameGenerator); \ No newline at end of file +INSTANTIATE_TEST_SUITE_P( + ReadNumericValue, + GenericOpcuaMonitoredItemPTest, + ::testing::Values(std::pair{OpcUaNodeId(1, ".ui8"), H{uint8_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i8"), H{int8_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".ui16"), H{uint16_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i16"), H{int16_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".ui32"), H{uint32_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i32"), H{int32_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".ui64"), H{uint64_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i64"), H{int64_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".d"), H{double{123.456789}}}, + std::pair{OpcUaNodeId(1, ".f"), H{float{float(-85) / 3}}}, + std::pair{OpcUaNodeId(1, ".s"), H{std::string{"String with a value"}}}), + ParamNameGenerator); \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/timer.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/timer.h new file mode 100644 index 0000000..fe10396 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/timer.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +namespace helper::utils +{ +class Timer +{ +public: + Timer(size_t ms, bool start = true) + : period(ms), + firstStart(true) + { + if (start) + restart(); + } + + std::chrono::milliseconds remain() const + { + auto now = std::chrono::steady_clock::now(); + const auto elapsed_ms = std::chrono::duration_cast(now - start); + std::chrono::milliseconds newTout = (elapsed_ms >= timeout) ? std::chrono::milliseconds(0) : timeout - elapsed_ms; + return newTout; + } + + bool expired() + { + return (firstStart) ? true : (remain() == std::chrono::milliseconds(0)); + } + + explicit operator std::chrono::milliseconds() const noexcept + { + return remain(); + } + + void restart() + { + firstStart = false; + start = std::chrono::steady_clock::now(); + timeout = std::chrono::milliseconds(period); + } + +protected: + std::chrono::steady_clock::time_point start; + std::chrono::milliseconds timeout; + std::chrono::milliseconds period; + bool firstStart; +}; + +} // namespace helper::utils From 7e9a2682d744c72f09cea455424de1c22f93a922 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Fri, 27 Mar 2026 14:16:07 +0100 Subject: [PATCH 04/29] OpcUaDataValue reworking --- .../include/opcuashared/opcuadatavalue.h | 27 +++- .../include/opcuashared/opcuavariant.h | 75 ++------- .../opcua/opcuashared/src/opcuadatavalue.cpp | 45 +++++- .../opcua/opcuashared/src/opcuavariant.cpp | 149 +++++++++++++----- .../opcuashared/tests/test_opcuadatavalue.cpp | 25 ++- .../src/opcua_monitored_item_fb_impl.cpp | 32 ++-- 6 files changed, 215 insertions(+), 138 deletions(-) diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h index e7bc6e6..ca2032d 100644 --- a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h @@ -33,7 +33,6 @@ class OpcUaDataValue : public OpcUaObject const UA_DataValue& getDataValue() const; bool hasValue() const; - OpcUaVariant getValue() const; UA_StatusCode getStatusCode() const; @@ -45,8 +44,32 @@ class OpcUaDataValue : public OpcUaObject bool isStatusOK() const; -protected: + bool isInteger() const; + bool isString() const; + bool isDouble() const; + bool isNull() const; + bool isReal() const; + bool isNumber() const; + std::string toString() const; + int64_t toInteger() const; + + template + inline T readScalar() const + { + return VariantUtils::ReadScalar(this->value.value); + } + + template > + void setScalar(const T& value) + { + static_assert(UATYPE::DataType != nullptr, "Implement specialization of TypeToUaDataType"); + + this->clear(); + + const auto status = UA_Variant_setScalarCopy(&(this->value.value), &value, UATYPE::DataType); + CheckStatusCodeException(status); + } }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h index 64c7980..7bdd36e 100644 --- a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h @@ -16,7 +16,6 @@ #pragma once -#include "opcuacommon.h" #include BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -26,15 +25,17 @@ using OpcUaVariantPtr = std::shared_ptr; namespace VariantUtils { - inline bool IsScalar(const UA_Variant& value) - { - return UA_Variant_isScalar(&value); - } - - inline bool IsVector(const UA_Variant& value) - { - return !IsScalar(value); - } + bool IsScalar(const UA_Variant& value); + bool IsVector(const UA_Variant& value); + bool IsInteger(const UA_Variant& value); + bool isReal(const UA_Variant& value); + bool isNull(const UA_Variant& value); + + std::string ToString(const UA_Variant& value); + int64_t ToNumber(const UA_Variant& value); + OpcUaNodeId ToNodeId(const UA_Variant& value); + void ToInt32Variant(OpcUaVariant& variant); + void ToInt64Variant(OpcUaVariant& variant); template inline bool IsType(const UA_Variant& value) @@ -69,47 +70,6 @@ namespace VariantUtils throw std::runtime_error("Variant does not contain a scalar of specified return type"); return *static_cast(value.data); } - - inline std::string ToString(const UA_Variant& value) - { - return utils::ToStdString(ReadScalar(value)); - } - - inline int64_t ToNumber(const UA_Variant& value) - { - switch (value.type->typeKind) - { - case UA_TYPES_SBYTE: - return ReadScalar(value); - case UA_TYPES_BYTE: - return ReadScalar(value); - case UA_TYPES_INT16: - return ReadScalar(value); - case UA_TYPES_UINT16: - return ReadScalar(value); - case UA_TYPES_INT32: - case UA_TYPES_ENUMERATION: - return ReadScalar(value); - case UA_TYPES_UINT32: - return ReadScalar(value); - case UA_TYPES_INT64: - return ReadScalar(value); - case UA_TYPES_UINT64: - return ReadScalar(value); - - default: - throw std::runtime_error("Type not supported!"); - } - } - - inline OpcUaNodeId ToNodeId(const UA_Variant& value) - { - UA_NodeId nodeId = ReadScalar(value); - return OpcUaNodeId(nodeId); - } - - void ToInt32Variant(OpcUaVariant& variant); - void ToInt64Variant(OpcUaVariant& variant); } class OpcUaVariant : public OpcUaObject @@ -170,6 +130,8 @@ class OpcUaVariant : public OpcUaObject bool isNull() const; bool isReal() const; bool isNumber() const; + bool isScalar() const; + bool isVector() const; std::string toString() const; int64_t toInteger() const; @@ -177,17 +139,6 @@ class OpcUaVariant : public OpcUaObject float toFloat() const; bool toBool() const; OpcUaNodeId toNodeId() const; - - inline bool isScalar() const - { - return VariantUtils::IsScalar(value); - } - inline bool isVector() const - { - return VariantUtils::IsVector(value); - } - - static bool IsInteger(const UA_Variant& value); }; class OpcUaVariableConversionError : public OpcUaException diff --git a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp index 7d85318..1b5b050 100644 --- a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp +++ b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp @@ -2,11 +2,6 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA -OpcUaVariant OpcUaDataValue::getValue() const -{ - return OpcUaVariant(OpcUaObject::getValue().value, true); -} - UA_StatusCode OpcUaDataValue::getStatusCode() const { if (!getDataValue().hasStatus) @@ -58,4 +53,44 @@ uint64_t OpcUaDataValue::toUnixTimeUs(UA_DateTime date) return static_cast((date - UA_DATETIME_UNIX_EPOCH) / UA_DATETIME_USEC); } +bool OpcUaDataValue::isInteger() const +{ + return VariantUtils::IsInteger(this->value.value); +} + +bool OpcUaDataValue::isString() const +{ + return VariantUtils::HasScalarType(value.value) || + VariantUtils::HasScalarType(value.value); +} + +bool OpcUaDataValue::isDouble() const +{ + return VariantUtils::HasScalarType(value.value); +} + +bool OpcUaDataValue::isNull() const +{ + return VariantUtils::isNull(value.value); +} + +bool OpcUaDataValue::isReal() const +{ + return VariantUtils::isReal(value.value); +} + +bool OpcUaDataValue::isNumber() const +{ + return isInteger() || isReal(); +} + +std::string OpcUaDataValue::toString() const +{ + return VariantUtils::ToString(value.value); +} + +int64_t OpcUaDataValue::toInteger() const +{ + return VariantUtils::ToNumber(value.value); +} END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp b/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp index b013c83..9866b2b 100644 --- a/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp +++ b/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp @@ -1,7 +1,9 @@ #include "opcuashared/opcuavariant.h" +#include "opcuashared/opcuacommon.h" #include #include + BEGIN_NAMESPACE_OPENDAQ_OPCUA using namespace daq::opcua::utils; @@ -93,7 +95,7 @@ void OpcUaVariant::setValue(const UA_Variant& value, bool shallowCopy) bool OpcUaVariant::isInteger() const { - return OpcUaVariant::IsInteger(this->value); + return VariantUtils::IsInteger(this->value); } bool OpcUaVariant::isString() const @@ -119,22 +121,12 @@ bool OpcUaVariant::isNodeId() const bool OpcUaVariant::isNull() const { - return UA_Variant_isEmpty(&value); + return VariantUtils::isNull(value); } bool OpcUaVariant::isReal() const { - if (value.type == NULL) - return false; - - switch (value.type->typeKind) - { - case UA_TYPES_FLOAT: - case UA_TYPES_DOUBLE: - return true; - default: - return false; - } + return VariantUtils::isReal(value); } bool OpcUaVariant::isNumber() const @@ -142,7 +134,59 @@ bool OpcUaVariant::isNumber() const return isInteger() || isReal(); } -bool OpcUaVariant::IsInteger(const UA_Variant& value) +bool OpcUaVariant::isScalar() const +{ + return VariantUtils::IsScalar(value); +} + +bool OpcUaVariant::isVector() const +{ + return VariantUtils::IsVector(value); +} + +std::string OpcUaVariant::toString() const +{ + return VariantUtils::ToString(value); +} + +int64_t OpcUaVariant::toInteger() const +{ + return VariantUtils::ToNumber(this->value); +} + +double OpcUaVariant::toDouble() const +{ + return readScalar(); +} + +float OpcUaVariant::toFloat() const +{ + return readScalar(); +} + +bool OpcUaVariant::toBool() const +{ + return readScalar(); +} + +OpcUaNodeId OpcUaVariant::toNodeId() const +{ + return VariantUtils::ToNodeId(this->value); +} + +// VariantUtils + +bool VariantUtils::IsScalar(const UA_Variant& value) +{ + return UA_Variant_isScalar(&value); +} + +bool VariantUtils::IsVector(const UA_Variant& value) +{ + return !IsScalar(value); +} + +bool VariantUtils::IsInteger(const UA_Variant& value) { if (value.type && value.type->typeId.namespaceIndex == 0) // built-in types { @@ -164,50 +208,77 @@ bool OpcUaVariant::IsInteger(const UA_Variant& value) return false; } -std::string OpcUaVariant::toString() const +bool VariantUtils::isReal(const UA_Variant& value) { - if (isType()) - { - UA_LocalizedText localizedText = readScalar(); - return ToStdString(localizedText.text); - } + if (value.type == NULL) + return false; - if (isType()) + switch (value.type->typeKind) { - UA_QualifiedName localizedText = readScalar(); - return ToStdString(localizedText.name); + case UA_TYPES_FLOAT: + case UA_TYPES_DOUBLE: + return true; + default: + return false; } - - UA_String str = readScalar(); - return ToStdString(str); } -int64_t OpcUaVariant::toInteger() const +bool VariantUtils::isNull(const UA_Variant& value) { - return VariantUtils::ToNumber(this->value); + return UA_Variant_isEmpty(&value); } -double OpcUaVariant::toDouble() const +std::string VariantUtils::ToString(const UA_Variant& value) { - return readScalar(); + std::string result; + if (IsType(value)) + { + result = utils::ToStdString(ReadScalar(value).text); + } + else if (IsType(value)) + { + result = utils::ToStdString(ReadScalar(value).name); + } + else + { + result = utils::ToStdString(ReadScalar(value)); + } + return result; } -float OpcUaVariant::toFloat() const +int64_t VariantUtils::ToNumber(const UA_Variant& value) { - return readScalar(); -} + switch (value.type->typeKind) + { + case UA_TYPES_SBYTE: + return ReadScalar(value); + case UA_TYPES_BYTE: + return ReadScalar(value); + case UA_TYPES_INT16: + return ReadScalar(value); + case UA_TYPES_UINT16: + return ReadScalar(value); + case UA_TYPES_INT32: + case UA_TYPES_ENUMERATION: + return ReadScalar(value); + case UA_TYPES_UINT32: + return ReadScalar(value); + case UA_TYPES_INT64: + return ReadScalar(value); + case UA_TYPES_UINT64: + return ReadScalar(value); -bool OpcUaVariant::toBool() const -{ - return readScalar(); + default: + throw std::runtime_error("Type not supported!"); + } } -OpcUaNodeId OpcUaVariant::toNodeId() const +OpcUaNodeId VariantUtils::ToNodeId(const UA_Variant& value) { - return VariantUtils::ToNodeId(this->value); + UA_NodeId nodeId = ReadScalar(value); + return OpcUaNodeId(nodeId); } -// VariantUtils void VariantUtils::ToInt32Variant(OpcUaVariant& variant) { diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp index e3787b3..090a672 100644 --- a/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp @@ -1,6 +1,6 @@ +#include #include "gtest/gtest.h" #include "opcuashared/opcuadatavalue.h" -#include BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -20,7 +20,7 @@ TEST_F(OpcUaDataValueTest, CreateWithInt) OpcUaDataValue value(dataValue, true); - ASSERT_TRUE(value.getValue().isInteger()); + ASSERT_TRUE(value.isInteger()); ASSERT_EQ(value.getStatusCode(), UA_STATUSCODE_BADAGGREGATELISTMISMATCH); UA_DataValue_clear(&dataValue); @@ -43,9 +43,6 @@ TEST_F(OpcUaDataValueTest, CreateWithIntRawDataValue) const UA_DataValue& rawDataValue = value.getDataValue(); ASSERT_EQ(rawDataValue.value.data, dataValue.value.data); - // rawDataValue = value; - // ASSERT_EQ(rawDataValue, &dataValue); - UA_DataValue_clear(&dataValue); } @@ -64,11 +61,11 @@ TEST_F(OpcUaDataValueTest, TestNoCopyBehaviour) dataValue.hasValue = true; OpcUaDataValue value(dataValue, true); - ASSERT_EQ(value.getValue().toInteger(), 1); + ASSERT_EQ(value.toInteger(), 1); *val = 2; - ASSERT_EQ(value.getValue().toInteger(), 2); + ASSERT_EQ(value.toInteger(), 2); - ASSERT_EQ(value.getValue().getValue().data, dataValue.value.data); + ASSERT_EQ(value.getValue().value.data, dataValue.value.data); UA_DataValue_clear(&dataValue); } @@ -88,16 +85,16 @@ TEST_F(OpcUaDataValueTest, TestCopyBehaviour) dataValue.hasValue = true; OpcUaDataValue value(dataValue); - ASSERT_EQ(value.getValue().toInteger(), 1); + ASSERT_EQ(value.toInteger(), 1); *val = 2; - ASSERT_EQ(value.getValue().toInteger(), 1); + ASSERT_EQ(value.toInteger(), 1); - ASSERT_NE(value.getValue().getValue().data, dataValue.value.data); + ASSERT_NE(value.getValue().value.data, dataValue.value.data); UA_DataValue_clear(&dataValue); - ASSERT_EQ(value.getValue().toInteger(), 1); - value.getValue().setScalar(UA_Int64(5)); - ASSERT_EQ(value.getValue().toInteger(), 5); + ASSERT_EQ(value.toInteger(), 1); + value.setScalar(UA_Int64(5)); + ASSERT_EQ(value.toInteger(), 5); } END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 27f2c6c..2641e8d 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -281,7 +281,7 @@ void OpcUaMonitoredItemFbImpl::validateNode() bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value) { - OpcUaNodeId valueDataType(value.getValue().getValue().type->typeId); + OpcUaNodeId valueDataType(value.getValue().value.type->typeId); if (valueDataType != nodeDataType) { nodeDataType = std::move(valueDataType); @@ -289,7 +289,7 @@ bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value outputSignal.setDescriptor(outputSignalDescriptor); } - valueValidationError = !(value.getValue().isNumber() || value.getValue().isString()); + valueValidationError = !(value.isNumber() || value.isString()); return !valueValidationError; } @@ -382,50 +382,50 @@ OpcUaMonitoredItemFbImpl::DataPackets OpcUaMonitoredItemFbImpl::buildDataPacket( DataPackets dps; dps.domainDataPacket = buildDomainDataPacket(value); - if (value.getValue().isString()) + if (value.isString()) { - const auto convertedValue = value.getValue().toString(); + const auto convertedValue = value.toString(); dps.dataPacket = daq::BinaryDataPacket(dps.domainDataPacket, outputSignalDescriptor, convertedValue.size()); std::memcpy(dps.dataPacket.getRawData(), convertedValue.data(), convertedValue.size()); } - else if (value.getValue().isInteger() || value.getValue().isReal()) + else if (value.isInteger() || value.isReal()) { if (dps.domainDataPacket.assigned()) dps.dataPacket = daq::DataPacketWithDomain(dps.domainDataPacket, outputSignalDescriptor, 1); else dps.dataPacket = daq::DataPacket(outputSignalDescriptor, 1); - switch (value.getValue().getValue().type->typeKind) + switch (value.getValue().value.type->typeKind) { case UA_TYPES_SBYTE: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_BYTE: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_INT16: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_UINT16: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_INT32: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_UINT32: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_INT64: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_UINT64: - *(static_cast(dps.dataPacket.getRawData())) = VariantUtils::ReadScalar(value.getValue().getValue()); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_FLOAT: - *(static_cast(dps.dataPacket.getRawData())) = value.getValue().toFloat(); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; case UA_TYPES_DOUBLE: - *(static_cast(dps.dataPacket.getRawData())) = value.getValue().toDouble(); + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; default: break; From e103876ce1d0feb534fac8dfe27ffa02dda8af58 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Fri, 27 Mar 2026 16:40:23 +0100 Subject: [PATCH 05/29] MonitoredItem FB: TimestampSource property; tests for different sources --- .../include/opcuashared/opcuadatavalue.h | 1 + .../opcua/opcuashared/src/opcuadatavalue.cpp | 7 + .../include/opcuageneric_client/constants.h | 1 + .../src/opcua_monitored_item_fb_impl.cpp | 72 ++-- .../tests/opcuaservertesthelper.cpp | 8 +- .../tests/opcuaservertesthelper.h | 3 +- .../tests/test_opcua_monitored_item_fb.cpp | 369 ++++++++++-------- 7 files changed, 274 insertions(+), 187 deletions(-) diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h index ca2032d..42e6967 100644 --- a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h @@ -29,6 +29,7 @@ class OpcUaDataValue : public OpcUaObject using OpcUaObject::OpcUaObject; static uint64_t toUnixTimeUs(UA_DateTime date); + static UA_DateTime fromUnixTimeUs(uint64_t date); const UA_DataValue& getDataValue() const; diff --git a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp index 1b5b050..6a64097 100644 --- a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp +++ b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp @@ -53,6 +53,13 @@ uint64_t OpcUaDataValue::toUnixTimeUs(UA_DateTime date) return static_cast((date - UA_DATETIME_UNIX_EPOCH) / UA_DATETIME_USEC); } +UA_DateTime OpcUaDataValue::fromUnixTimeUs(uint64_t date) +{ + if (date == 0) + return 0; + return static_cast(date * UA_DATETIME_USEC + UA_DATETIME_UNIX_EPOCH); +} + bool OpcUaDataValue::isInteger() const { return VariantUtils::IsInteger(this->value.value); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h index 647afbc..da134bf 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h @@ -32,6 +32,7 @@ static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID_TYPE = "NodeIDType"; static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID = "NodeID"; static constexpr const char* PROPERTY_NAME_OPCUA_NAMESPACE_INDEX = "NamespaceIndex"; static constexpr const char* PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL = "SamplingInterval"; +static constexpr const char* PROPERTY_NAME_OPCUA_TS_MODE = "TimestampMode"; // ---------- // Defaults diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 2641e8d..79afa53 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -7,18 +7,19 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC std::atomic OpcUaMonitoredItemFbImpl::localIndex = 0; -std::unordered_map OpcUaMonitoredItemFbImpl::supportedDataTypes = {{OpcUaNodeId(), daq::SampleType::Undefined}, - {OpcUaNodeId(0, UA_NS0ID_FLOAT), daq::SampleType::Float32}, - {OpcUaNodeId(0, UA_NS0ID_DOUBLE), daq::SampleType::Float64}, - {OpcUaNodeId(0, UA_NS0ID_SBYTE), daq::SampleType::Int8}, - {OpcUaNodeId(0, UA_NS0ID_BYTE), daq::SampleType::UInt8}, - {OpcUaNodeId(0, UA_NS0ID_INT16), daq::SampleType::Int16}, - {OpcUaNodeId(0, UA_NS0ID_UINT16), daq::SampleType::UInt16}, - {OpcUaNodeId(0, UA_NS0ID_INT32), daq::SampleType::Int32}, - {OpcUaNodeId(0, UA_NS0ID_UINT32), daq::SampleType::UInt32}, - {OpcUaNodeId(0, UA_NS0ID_INT64), daq::SampleType::Int64}, - {OpcUaNodeId(0, UA_NS0ID_UINT64), daq::SampleType::UInt64}, - {OpcUaNodeId(0, UA_NS0ID_STRING), daq::SampleType::String}}; +std::unordered_map OpcUaMonitoredItemFbImpl::supportedDataTypes = { + {OpcUaNodeId(), daq::SampleType::Undefined}, + {OpcUaNodeId(0, UA_NS0ID_FLOAT), daq::SampleType::Float32}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLE), daq::SampleType::Float64}, + {OpcUaNodeId(0, UA_NS0ID_SBYTE), daq::SampleType::Int8}, + {OpcUaNodeId(0, UA_NS0ID_BYTE), daq::SampleType::UInt8}, + {OpcUaNodeId(0, UA_NS0ID_INT16), daq::SampleType::Int16}, + {OpcUaNodeId(0, UA_NS0ID_UINT16), daq::SampleType::UInt16}, + {OpcUaNodeId(0, UA_NS0ID_INT32), daq::SampleType::Int32}, + {OpcUaNodeId(0, UA_NS0ID_UINT32), daq::SampleType::UInt32}, + {OpcUaNodeId(0, UA_NS0ID_INT64), daq::SampleType::Int64}, + {OpcUaNodeId(0, UA_NS0ID_UINT64), daq::SampleType::UInt64}, + {OpcUaNodeId(0, UA_NS0ID_STRING), daq::SampleType::String}}; namespace { @@ -109,9 +110,11 @@ FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() } { - auto builder = StringPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID, String("")) - .setDescription(fmt::format("Specifies the NodeID of the OPCUA node to monitor. The format of the NodeID should correspond " - "to the type specified in the \"{}\" property.", PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); + auto builder = + StringPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID, String("")) + .setDescription(fmt::format("Specifies the NodeID of the OPCUA node to monitor. The format of the NodeID should correspond " + "to the type specified in the \"{}\" property.", + PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); defaultConfig.addProperty(builder.build()); } @@ -131,11 +134,18 @@ FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() defaultConfig.addProperty(builder.build()); } - const auto fbType = - FunctionBlockType(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, - GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, - "Monitors a specified OPCUA node and outputs the value and timestamp as signals.", - defaultConfig); + { + auto builder = SelectionPropertyBuilder(PROPERTY_NAME_OPCUA_TS_MODE, + List("None", "ServerTimestamp", "SourceTimestamp"), + static_cast(DomainSource::ServerTimestamp)) + .setDescription("Defines what to use as a domain signal. By default it is set to ServerTimestamp."); + defaultConfig.addProperty(builder.build()); + } + + const auto fbType = FunctionBlockType(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, + GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, + "Monitors a specified OPCUA node and outputs the value and timestamp as signals.", + defaultConfig); return fbType; } @@ -185,7 +195,7 @@ void OpcUaMonitoredItemFbImpl::readProperties() configValid = true; configMsg.clear(); - config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment + config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment config.nodeId = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID, ""); if (config.nodeId.empty()) { @@ -198,12 +208,22 @@ void OpcUaMonitoredItemFbImpl::readProperties() readProperty(objPtr, PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); if (config.samplingInterval <= 0) { - configMsg = fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL); + configMsg = fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", + PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL); configValid = false; config.samplingInterval = DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL; } - config.domainSource = DomainSource::ServerTimestamp; + const auto tmpDomainSource = + readProperty(objPtr, PROPERTY_NAME_OPCUA_TS_MODE, static_cast(DomainSource::ServerTimestamp)); + if (tmpDomainSource < static_cast(DomainSource::_count) && tmpDomainSource >= 0) + { + config.domainSource = static_cast(tmpDomainSource); + } + else + { + config.domainSource = DomainSource::ServerTimestamp; + } updateStatuses(); } @@ -243,7 +263,8 @@ void OpcUaMonitoredItemFbImpl::validateNode() nodeValidationError = false; valueValidationError = false; nodeValidationErrorMsg.clear(); - try { + try + { auto nodeExist = client->nodeExists(nodeId); if (!nodeExist) { @@ -347,10 +368,9 @@ void OpcUaMonitoredItemFbImpl::readerLoop() while (running) { { - //auto lockProcessing = std::scoped_lock(processingMutex); + // auto lockProcessing = std::scoped_lock(processingMutex); if (configValid && nodeValidationError == false) { - OpcUaDataValue opcUaVariant; try { diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp index 68984e9..0f101c0 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp @@ -233,11 +233,17 @@ void OpcUaServerTestHelper::publishVariable(std::string identifier, CheckStatusCodeException(status); } -void OpcUaServerTestHelper::writeNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value) +void OpcUaServerTestHelper::writeValueNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value) { CheckStatusCodeException(UA_Server_writeValue(server, *nodeId, *value)); } +void OpcUaServerTestHelper::writeDataValueNode(const OpcUaNodeId& nodeId, const OpcUaDataValue& value) +{ + CheckStatusCodeException(UA_Server_writeDataValue(server, *nodeId, *value)); +} + + void OpcUaServerTestHelper::publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale, int nodeIndex) { OpcUaObject attr = UA_ObjectAttributes_default; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h index f5fdd25..dbedf96 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h @@ -53,7 +53,8 @@ class OpcUaServerTestHelper final uint16_t nodeIndex = 1, size_t dimension = 1); - void writeNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value); + void writeValueNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value); + void writeDataValueNode(const OpcUaNodeId& nodeId, const OpcUaDataValue& value); private: void runServer(); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 5e0bfcb..e85a1fb 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -20,15 +20,17 @@ namespace daq::opcua::generic class GenericOpcuaMonitoredItemHelper : public DaqTestHelper { public: + using DS = OpcUaMonitoredItemFbImpl::DomainSource; daq::FunctionBlockPtr fb; OpcUaServerTestHelper testHelper; - void CreateMonitoredItemFB(std::string nodeId, uint32_t index, uint32_t interval = 100) + void CreateMonitoredItemFB(std::string nodeId, uint32_t index, uint32_t interval = 100, DS ds = DS::ServerTimestamp) { auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, nodeId); config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, index); config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, interval); + config.setPropertyValue(PROPERTY_NAME_OPCUA_TS_MODE, static_cast(ds)); ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); } @@ -48,6 +50,24 @@ namespace daq::opcua::generic return Enumeration("ComponentStatusType", "Error", daqInstance.getContext().getTypeManager()); } + auto getTime() + { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); + } + + auto readValueWithTout(daq::SignalPtr sig, size_t ms, const daq::BaseObjectPtr prevVal = nullptr) + { + daq::BaseObjectPtr value; + helper::utils::Timer timer(ms); + do + { + value = sig.getLastValue(); + } while ((prevVal.assigned() ? value == prevVal : !value.assigned()) && !timer.expired()); + + return value; + }; + protected: void SetUp() { @@ -138,7 +158,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, DefaultConfig) ASSERT_TRUE(defaultConfig.assigned()); - EXPECT_EQ(defaultConfig.getAllProperties().getCount(), 4u); + EXPECT_EQ(defaultConfig.getAllProperties().getCount(), 5u); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE).getValueType(), CoreType::ctInt); @@ -163,6 +183,12 @@ TEST_F(GenericOpcuaMonitoredItemTest, DefaultConfig) .getValue(DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL), DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL).getVisible()); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_TS_MODE)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_TS_MODE).getValueType(), CoreType::ctInt); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_TS_MODE).asPtr(), + static_cast(OpcUaMonitoredItemFbImpl::DomainSource::ServerTimestamp)); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_TS_MODE).getVisible()); } TEST_F(GenericOpcuaMonitoredItemTest, CreationWithDefaultConfig) @@ -223,11 +249,17 @@ TEST_P(GenericOpcuaMonitoredItemPTest, ReadValue) { using T = std::decay_t; - OpcUaVariant variant; + OpcUaDataValue dataValue; if constexpr (std::is_same_v) - variant = OpcUaVariant(templateParam.c_str()); + { + UA_String myString = UA_STRING_ALLOC(templateParam.c_str()); + dataValue.setScalar(myString); + UA_String_clear(&myString); + } else - variant.setScalar(templateParam); + { + dataValue.setScalar(templateParam); + } daq::BaseObjectPtr prevVal; daq::BaseObjectPtr val; @@ -256,7 +288,7 @@ TEST_P(GenericOpcuaMonitoredItemPTest, ReadValue) } // write new value to the node - ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); + ASSERT_NO_THROW(testHelper.writeDataValueNode(param.first, dataValue)); { // after writing @@ -286,197 +318,216 @@ TEST_P(GenericOpcuaMonitoredItemPTest, ReadValue) param.second); } -TEST_P(GenericOpcuaMonitoredItemPTest, ReadValueWithServerTimestampUsingLastValue) +INSTANTIATE_TEST_SUITE_P( + ReadNumericValue, + GenericOpcuaMonitoredItemPTest, + ::testing::Values(std::pair{OpcUaNodeId(1, ".ui8"), H{uint8_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i8"), H{int8_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".ui16"), H{uint16_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i16"), H{int16_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".ui32"), H{uint32_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i32"), H{int32_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".ui64"), H{uint64_t{std::numeric_limits::max()}}}, + std::pair{OpcUaNodeId(1, ".i64"), H{int64_t{std::numeric_limits::min()}}}, + std::pair{OpcUaNodeId(1, ".d"), H{double{123.456789}}}, + std::pair{OpcUaNodeId(1, ".f"), H{float{float(-85) / 3}}}, + std::pair{OpcUaNodeId(1, ".s"), H{std::string{"String with a value"}}}), + ParamNameGenerator); + +TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithServerTimestampUsingLastValue) { constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".i64"); + const auto value = int64_t{std::numeric_limits::min()}; StartUp(); - auto param = GetParam(); - CreateMonitoredItemFB(param.first.getIdentifier(), param.first.getNamespaceIndex(), interval); + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::ServerTimestamp); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - fb.getSignals()[0].getDomainSignal(); - auto domainSig = fb.getSignals()[0].getDomainSignal(); ASSERT_TRUE(domainSig.assigned()); - auto getTime = []() - { - using namespace std::chrono; - return duration_cast(system_clock::now().time_since_epoch()).count(); - }; + const OpcUaVariant variant(value); - std::visit( - [&](auto& templateParam) - { - using T = std::decay_t; + // before writing + // waiting to be sure that FB has read initial value + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); - OpcUaVariant variant; - if constexpr (std::is_same_v) - variant = OpcUaVariant(templateParam.c_str()); - else - variant.setScalar(templateParam); + const auto timeBefore = getTime(); - daq::BaseObjectPtr prevVal; - daq::BaseObjectPtr val; - { - // before writing - // waiting to be sure that FB has read initial value - helper::utils::Timer timer(interval * 3); - do - { - prevVal = fb.getSignals()[0].getLastValue(); - } while (!prevVal.assigned() && !timer.expired()); - ASSERT_TRUE(prevVal.assigned()); - } + ASSERT_NO_THROW(testHelper.writeValueNode(nodeId, variant)); - const auto timeBefore = getTime(); - - ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - { - // after writing - ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], interval * 3, prevVal); - { - // the FB needs time to read the new value from the node - // read the last value until it becomes different from the initial or until the timer expires - helper::utils::Timer timer(interval * 3); - do - { - val = fb.getSignals()[0].getLastValue(); - } while (val == prevVal && !timer.expired()); - } + auto domainVal = domainSig.getLastValue(); + const auto timeAfter = getTime(); - auto domainVal = domainSig.getLastValue(); - const auto timeAfter = getTime(); - { - // check that the target and read values are the same - if constexpr (std::is_same_v) - ASSERT_DOUBLE_EQ(val.asPtr().getValue(T(0)), templateParam); - else if constexpr (std::is_same_v) - ASSERT_FLOAT_EQ(val.asPtr().getValue(T(0)), templateParam); - else if constexpr (std::is_same_v) - ASSERT_EQ(val, templateParam); - else - ASSERT_EQ(val.asPtr().getValue(T(0)), templateParam); - } + // check that the target and read values are the same + ASSERT_EQ(val.asPtr().getValue(int64_t(0)), value); - // check the ts is between start and stop points - ASSERT_TRUE(domainVal.assigned()); - EXPECT_GE(timeAfter, domainVal.asPtr().getValue(uint64_t(0))); - EXPECT_LE(timeBefore, domainVal.asPtr().getValue(uint64_t(0))); - } - }, - param.second); + // check the ts is between start and stop points + ASSERT_TRUE(domainVal.assigned()); + EXPECT_GE(timeAfter, domainVal.asPtr().getValue(uint64_t(0))); + EXPECT_LE(timeBefore, domainVal.asPtr().getValue(uint64_t(0))); } -TEST_P(GenericOpcuaMonitoredItemPTest, ReadValueWithServerTimestampUsingTailReader) +TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithSourceTimestampUsingLastValue) { constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".i64"); + const auto value = int64_t{std::numeric_limits::min()}; StartUp(); - auto param = GetParam(); - CreateMonitoredItemFB(param.first.getIdentifier(), param.first.getNamespaceIndex(), interval); + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::SourceTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + auto domainSig = fb.getSignals()[0].getDomainSignal(); + ASSERT_TRUE(domainSig.assigned()); + + OpcUaDataValue dataValue; + dataValue.setScalar(value); + + // before writing + // waiting to be sure that FB has read initial value + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); + + const auto time = getTime(); + dataValue.getValue().hasSourceTimestamp = true; + dataValue.getValue().sourceTimestamp = OpcUaDataValue::fromUnixTimeUs(time); + ASSERT_NO_THROW(testHelper.writeDataValueNode(nodeId, dataValue)); + + // after writing ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - fb.getSignals()[0].getDomainSignal(); + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], interval * 3, prevVal); + + auto domainVal = domainSig.getLastValue(); + + // check that the target and read values are the same + ASSERT_EQ(val.asPtr().getValue(int64_t(0)), value); + + // check that the target and read TSes are the same + ASSERT_TRUE(domainVal.assigned()); + EXPECT_EQ(time, domainVal.asPtr().getValue(uint64_t(0))); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithServerTimestampUsingTailReader) +{ + constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".i64"); + const auto value = int64_t{std::numeric_limits::min()}; + StartUp(); + + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); auto domainSig = fb.getSignals()[0].getDomainSignal(); ASSERT_TRUE(domainSig.assigned()); - auto getTime = []() - { - using namespace std::chrono; - return duration_cast(system_clock::now().time_since_epoch()).count(); - }; + const OpcUaVariant variant(value); - std::visit( - [&](auto& templateParam) - { - using T = std::decay_t; + // before writing + // waiting to be sure that FB has read initial value + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); - // TailReader does not support String type, just skip test - if constexpr (!std::is_same_v) - { - OpcUaVariant variant; - variant.setScalar(templateParam); + const auto timeBefore = getTime(); - daq::BaseObjectPtr prevVal; - { - // before writing - // waiting to be sure that FB has read initial value - helper::utils::Timer timer(interval * 3); - do - { - prevVal = fb.getSignals()[0].getLastValue(); - } while (!prevVal.assigned() && !timer.expired()); - ASSERT_TRUE(prevVal.assigned()); - } + auto reader = TailReaderBuilder() + .setSignal(fb.getSignals()[0]) + .setHistorySize(1) + .setValueReadType(SampleType::Int64) + .setDomainReadType(SampleType::UInt64) + .setSkipEvents(true) + .build(); - const auto timeBefore = getTime(); + ASSERT_NO_THROW(testHelper.writeValueNode(nodeId, variant)); - auto reader = TailReaderBuilder() - .setSignal(fb.getSignals()[0]) - .setHistorySize(1) - .setValueReadType(SampleTypeFromType::SampleType) - .setDomainReadType(SampleType::UInt64) - .setSkipEvents(true) - .build(); + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - ASSERT_NO_THROW(testHelper.writeNode(param.first, variant)); + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + std::this_thread::sleep_for(std::chrono::milliseconds(interval * 3)); - { - // after writing - ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - { - // the FB needs time to read the new value from the node - // read the last value until it becomes different from the initial or until the timer expires - helper::utils::Timer timer(interval * 3); - while (!timer.expired()) - { - } - } - - SizeT count{1}; - T values{}; - uint64_t domain{}; - reader.readWithDomain(&values, &domain, &count); - const auto timeAfter = getTime(); + SizeT count{1}; + int64_t values{}; + uint64_t domain{}; + reader.readWithDomain(&values, &domain, &count); + const auto timeAfter = getTime(); - { - // check that the target and read values are the same - if constexpr (std::is_same_v) - ASSERT_DOUBLE_EQ(values, templateParam); - else if constexpr (std::is_same_v) - ASSERT_FLOAT_EQ(values, templateParam); - else - ASSERT_EQ(values, templateParam); - } - - // check the ts is between start and stop points - EXPECT_GE(timeAfter, domain); - EXPECT_LE(timeBefore, domain); - } - } - }, - param.second); + // check that the target and read values are the same + ASSERT_EQ(values, value); + + // check the ts is between start and stop points + EXPECT_GE(timeAfter, domain); + EXPECT_LE(timeBefore, domain); } -INSTANTIATE_TEST_SUITE_P( - ReadNumericValue, - GenericOpcuaMonitoredItemPTest, - ::testing::Values(std::pair{OpcUaNodeId(1, ".ui8"), H{uint8_t{std::numeric_limits::max()}}}, - std::pair{OpcUaNodeId(1, ".i8"), H{int8_t{std::numeric_limits::min()}}}, - std::pair{OpcUaNodeId(1, ".ui16"), H{uint16_t{std::numeric_limits::max()}}}, - std::pair{OpcUaNodeId(1, ".i16"), H{int16_t{std::numeric_limits::min()}}}, - std::pair{OpcUaNodeId(1, ".ui32"), H{uint32_t{std::numeric_limits::max()}}}, - std::pair{OpcUaNodeId(1, ".i32"), H{int32_t{std::numeric_limits::min()}}}, - std::pair{OpcUaNodeId(1, ".ui64"), H{uint64_t{std::numeric_limits::max()}}}, - std::pair{OpcUaNodeId(1, ".i64"), H{int64_t{std::numeric_limits::min()}}}, - std::pair{OpcUaNodeId(1, ".d"), H{double{123.456789}}}, - std::pair{OpcUaNodeId(1, ".f"), H{float{float(-85) / 3}}}, - std::pair{OpcUaNodeId(1, ".s"), H{std::string{"String with a value"}}}), - ParamNameGenerator); \ No newline at end of file +TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithSourceTimestampUsingTailReader) +{ + constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".i64"); + const auto value = int64_t{std::numeric_limits::min()}; + StartUp(); + + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::SourceTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + auto domainSig = fb.getSignals()[0].getDomainSignal(); + ASSERT_TRUE(domainSig.assigned()); + + OpcUaDataValue dataValue; + dataValue.setScalar(value); + + // before writing + // waiting to be sure that FB has read initial value + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); + + const auto time = getTime(); + + auto reader = TailReaderBuilder() + .setSignal(fb.getSignals()[0]) + .setHistorySize(1) + .setValueReadType(SampleType::Int64) + .setDomainReadType(SampleType::UInt64) + .setSkipEvents(true) + .build(); + + dataValue.getValue().hasSourceTimestamp = true; + dataValue.getValue().sourceTimestamp = OpcUaDataValue::fromUnixTimeUs(time); + ASSERT_NO_THROW(testHelper.writeDataValueNode(nodeId, dataValue)); + + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + std::this_thread::sleep_for(std::chrono::milliseconds(interval * 3)); + + SizeT count{1}; + int64_t values{}; + uint64_t domain{}; + reader.readWithDomain(&values, &domain, &count); + + // check that the target and read values are the same + ASSERT_EQ(values, value); + + // check that the target and read TSes are the same + EXPECT_EQ(time, domain); +} From da462ba33c35aef4506334ced33da336cbc050c6 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Fri, 27 Mar 2026 19:40:02 +0100 Subject: [PATCH 06/29] MonitoredItem: statuses; accessLevel; tests; --- .../include/opcuaclient/opcuaclient.h | 1 + .../opcua/opcuaclient/src/opcuaclient.cpp | 7 + .../opcua_monitored_item_fb_impl.h | 153 +++++++++++++++++- .../src/opcua_monitored_item_fb_impl.cpp | 132 ++++++++++----- .../tests/opcuaservertesthelper.cpp | 13 +- .../tests/opcuaservertesthelper.h | 3 +- .../tests/test_opcua_monitored_item_fb.cpp | 26 +++ 7 files changed, 284 insertions(+), 51 deletions(-) diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h index 729456c..b1376a1 100644 --- a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h @@ -128,6 +128,7 @@ class OpcUaClient OpcUaVariant readValue(const OpcUaNodeId& node); OpcUaDataValue readDataValue(const OpcUaNodeId& node); UA_NodeClass readNodeClass(const OpcUaNodeId& nodeId); + UA_Byte readAccessLevel(const OpcUaNodeId& nodeId); std::string readBrowseName(const OpcUaNodeId& nodeId); std::string readDisplayName(const OpcUaNodeId& nodeId); size_t readDimension(const OpcUaNodeId& nodeId); diff --git a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp index 5fc9729..b098685 100644 --- a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp +++ b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp @@ -436,6 +436,13 @@ UA_NodeClass OpcUaClient::readNodeClass(const OpcUaNodeId& nodeId) return nodeClass; } +UA_Byte OpcUaClient::readAccessLevel(const OpcUaNodeId& nodeId) +{ + UA_Byte accessLevel; + CheckStatusCodeException(UA_Client_readUserAccessLevelAttribute(getLockedUaClient(), *nodeId, &accessLevel)); + return accessLevel; +} + bool OpcUaClient::nodeExists(const OpcUaNodeId& node) { UA_NodeClass nodeClass; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index 5c08e77..dd9832a 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -22,6 +22,150 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC +namespace utils +{ + class Error + { + public: + Error(std::string name) + : name(std::move(name)) + , present(false) + , updated(true) + { + } + + void set(std::string msg) + { + updated = true; + present = true; + message = std::move(msg); + } + + void add(const std::string& msg) + { + if (message.empty()) + { + set(msg); + } + else + { + updated = true; + present = true; + message.reserve(msg.size() + 2); + message.append("; "); + message.append(msg); + } + } + + void reset() + { + if (present) + updated = true; + present = false; + message.clear(); + } + bool ok() const + { + return !present; + } + + const std::string& getMessage() const + { + return message; + } + + bool isUpdated() const + { + bool tmp = updated; + updated = false; + return tmp; + } + + protected: + const std::string name; + std::string message; + bool present; + mutable bool updated; + }; + + class StatusContainer + { + public: + StatusContainer() = default; + + bool addStatus(const std::string& name) + { + std::scoped_lock lock(mtx); + if (map.count(name)) + return false; + map.emplace(std::pair(name, Error(name))); + return true; + } + + bool isUpdated() + { + std::scoped_lock lock(mtx); + bool result = false; + for (const auto& e : map) + { + bool isUpdated = e.second.isUpdated(); + result |= isUpdated; + } + return result; + } + void resetAll() + { + std::scoped_lock lock(mtx); + for (auto& e : map) + e.second.reset(); + } + + void set(const std::string& name, std::string msg) + { + std::scoped_lock lock(mtx); + getStatus(name).set(std::move(msg)); + } + + void addToStatus(const std::string& name, const std::string& msg) + { + std::scoped_lock lock(mtx); + getStatus(name).add(msg); + } + + void reset(const std::string& name) + { + std::scoped_lock lock(mtx); + getStatus(name).reset(); + } + + bool ok(const std::string& name) const + { + std::scoped_lock lock(mtx); + return getStatus(name).ok(); + } + + std::string getMessage(const std::string& name) const + { + std::scoped_lock lock(mtx); + return getStatus(name).getMessage(); + } + + protected: + mutable std::mutex mtx; + std::unordered_map map; + + Error& getStatus(const std::string& name) + { + return map.at(name); + } + + const Error& getStatus(const std::string& name) const + { + return map.at(name); + } + }; +} + class OpcUaMonitoredItemFbImpl final : public FunctionBlock { friend class GenericOpcuaMonitoredItemTest; @@ -74,11 +218,6 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock DataDescriptorPtr outputSignalDescriptor; SignalConfigPtr outputSignal; SignalConfigPtr outputDomainSignal; - std::atomic configValid; - std::string configMsg; - std::atomic nodeValidationError; - std::string nodeValidationErrorMsg; - std::atomic valueValidationError; FbConfig config; daq::opcua::OpcUaClientPtr client; @@ -88,9 +227,12 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock std::thread readerThread; std::atomic running; + utils::StatusContainer statuses; + void removed() override; static std::string generateLocalId(); + void initStatusContainer(); void adjustSignalDescriptor(); void createSignal(); void reconfigureSignal(const FbConfig& prevConfig); @@ -103,6 +245,7 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock void updateStatuses(); void validateNode(); + bool validateResponse(const OpcUaDataValue& value); bool validateValueDataType(const OpcUaDataValue& value); void runReaderThread(); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 79afa53..5a04739 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -57,14 +57,12 @@ OpcUaMonitoredItemFbImpl::OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, daq::opcua::OpcUaClientPtr client, const PropertyObjectPtr& config) : FunctionBlock(type, ctx, parent, generateLocalId()) - , configValid(false) - , nodeValidationError(false) - , valueValidationError(false) , client(client) , nodeId() , running(false) { initComponentStatus(); + initStatusContainer(); if (config.assigned()) initProperties(populateDefaultConfig(type.createDefaultConfig(), config)); else @@ -98,6 +96,15 @@ void OpcUaMonitoredItemFbImpl::removed() FunctionBlock::removed(); } +void OpcUaMonitoredItemFbImpl::initStatusContainer() +{ + statuses.addStatus("config"); + statuses.addStatus("nodeValidation"); + statuses.addStatus("valueValidation"); + statuses.addStatus("responseValidation"); + statuses.addStatus("exeption"); +} + FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() { auto defaultConfig = PropertyObject(); @@ -156,7 +163,7 @@ std::string OpcUaMonitoredItemFbImpl::generateLocalId() void OpcUaMonitoredItemFbImpl::adjustSignalDescriptor() { - if (nodeValidationError == false && supportedDataTypes.count(nodeDataType) != 0) + if (statuses.ok("nodeValidation") && supportedDataTypes.count(nodeDataType) != 0) { outputSignalDescriptor = DataDescriptorBuilder().setSampleType(supportedDataTypes[nodeDataType]).build(); } @@ -192,15 +199,14 @@ void OpcUaMonitoredItemFbImpl::initProperties(const PropertyObjectPtr& config) void OpcUaMonitoredItemFbImpl::readProperties() { auto lock = this->getRecursiveConfigLock(); - configValid = true; - configMsg.clear(); + statuses.reset("config"); + config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment config.nodeId = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID, ""); if (config.nodeId.empty()) { - configMsg = fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID); - configValid = false; + statuses.addToStatus("config", fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID)); } config.namespaceIndex = readProperty(objPtr, PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 0); @@ -208,9 +214,8 @@ void OpcUaMonitoredItemFbImpl::readProperties() readProperty(objPtr, PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); if (config.samplingInterval <= 0) { - configMsg = fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", - PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL); - configValid = false; + statuses.addToStatus("config", fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", + PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL)); config.samplingInterval = DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL; } @@ -236,6 +241,7 @@ void OpcUaMonitoredItemFbImpl::propertyChanged() nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; + statuses.resetAll(); validateNode(); adjustSignalDescriptor(); reconfigureSignal(prevConfig); @@ -244,62 +250,98 @@ void OpcUaMonitoredItemFbImpl::propertyChanged() void OpcUaMonitoredItemFbImpl::updateStatuses() { - if (configValid == false) + if (!statuses.isUpdated()) + return; + + if (!statuses.ok("config")) + { + setComponentStatusWithMessage(ComponentStatus::Error, "Configuration is invalid! " + statuses.getMessage("config")); + } + else if (!statuses.ok("nodeValidation")) + { + setComponentStatusWithMessage(ComponentStatus::Error, "Node is invalid! " + statuses.getMessage("nodeValidation")); + } + else if (!statuses.ok("responseValidation")) + { + setComponentStatusWithMessage(ComponentStatus::Error, "Response error! " + statuses.getMessage("responseValidation")); + } + else if (!statuses.ok("valueValidation")) { - setComponentStatusWithMessage(ComponentStatus::Error, "Configuration is invalid! " + configMsg); + setComponentStatusWithMessage(ComponentStatus::Error, "Value error! " + statuses.getMessage("valueValidation")); } - else if (nodeValidationError) + else if (!statuses.ok("exeption")) { - setComponentStatusWithMessage(ComponentStatus::Error, "Node is invalid! " + nodeValidationErrorMsg); + setComponentStatusWithMessage(ComponentStatus::Error, statuses.getMessage("exeption")); } else { - setComponentStatusWithMessage(ComponentStatus::Ok, "Parsing succeeded"); + setComponentStatus(ComponentStatus::Ok); } } void OpcUaMonitoredItemFbImpl::validateNode() { - nodeValidationError = false; - valueValidationError = false; - nodeValidationErrorMsg.clear(); try { + statuses.reset("nodeValidation"); auto nodeExist = client->nodeExists(nodeId); if (!nodeExist) { - nodeValidationError = true; - nodeValidationErrorMsg = fmt::format("Node {} does not exist", nodeId.toString()); + statuses.addToStatus("nodeValidation", fmt::format("Node {} does not exist", nodeId.toString())); } else if (const auto nodeClass = client->readNodeClass(nodeId); nodeClass != UA_NodeClass::UA_NODECLASS_VARIABLE) { - nodeValidationError = true; - nodeValidationErrorMsg = fmt::format("Node {} is not a variable node", nodeId.toString()); + statuses.addToStatus("nodeValidation", fmt::format("Node {} is not a variable node", nodeId.toString())); } - else if (nodeDataType = client->readDataType(nodeId); supportedDataTypes.count(nodeDataType) == 0) + else if (const auto accessLevel = client->readAccessLevel(nodeId); (accessLevel & UA_ACCESSLEVELMASK_READ) == 0) { - nodeValidationError = true; - nodeValidationErrorMsg = fmt::format("Node {} has unsupported DataType ({})", nodeId.toString(), nodeDataType.toString()); + statuses.addToStatus("nodeValidation", fmt::format("There is no read permission to node {}", nodeId.toString())); } - else + else if (nodeDataType = client->readDataType(nodeId); supportedDataTypes.count(nodeDataType) == 0) { - nodeValidationError = false; + statuses.addToStatus("nodeValidation", fmt::format("Node {} has unsupported DataType ({})", nodeId.toString(), nodeDataType.toString())); } } catch (OpcUaException& ex) { - nodeValidationError = true; if (ex.getStatusCode() == UA_STATUSCODE_BADUSERACCESSDENIED) { - nodeValidationErrorMsg = fmt::format("Access denied for node {}", nodeId.toString()); + statuses.addToStatus("nodeValidation", fmt::format("Access denied for node {}", nodeId.toString())); } else { - nodeValidationErrorMsg = fmt::format("Exception was thrown while node {} validatiion", nodeId.toString()); + statuses.addToStatus("nodeValidation", fmt::format("Exception was thrown while node {} validatiion", nodeId.toString())); } } } +bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) +{ + if (value.getValue().hasStatus && value.getValue().hasStatus != UA_STATUSCODE_GOOD) + { + statuses.addToStatus("responseValidation", fmt::format("Reading value error: {}. ", value.getValue().hasStatus)); + return false; + } + if (!value.getValue().hasValue) + { + statuses.addToStatus("responseValidation", std::string("Reading value error: response without a value.")); + return false; + } + if (config.domainSource == DomainSource::ServerTimestamp && !value.getValue().hasServerTimestamp) + { + statuses.addToStatus("responseValidation", std::string("Reading value error: there is no required server timestamp")); + return false; + } + if (config.domainSource == DomainSource::SourceTimestamp && !value.getValue().hasSourceTimestamp) + { + statuses.addToStatus("responseValidation", std::string("Reading value error: there is no required source timestamp")); + return false; + } + + statuses.reset("responseValidation"); + return true; +} + bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value) { OpcUaNodeId valueDataType(value.getValue().value.type->typeId); @@ -310,8 +352,13 @@ bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value outputSignal.setDescriptor(outputSignalDescriptor); } - valueValidationError = !(value.isNumber() || value.isString()); - return !valueValidationError; + bool valid = (value.isNumber() || value.isString()); + if (valid) + statuses.reset("valueValidation"); + else + statuses.set("valueValidation", "Value has unsupported type."); + + return valid; } void OpcUaMonitoredItemFbImpl::createSignal() @@ -369,19 +416,17 @@ void OpcUaMonitoredItemFbImpl::readerLoop() { { // auto lockProcessing = std::scoped_lock(processingMutex); - if (configValid && nodeValidationError == false) + if (statuses.ok("config") && statuses.ok("nodeValidation")) { - OpcUaDataValue opcUaVariant; + OpcUaDataValue dataValue; try { - opcUaVariant = client->readDataValue(nodeId); - if (!validateValueDataType(opcUaVariant)) - { - // updateStatuses? - } - else + dataValue = client->readDataValue(nodeId); + + statuses.reset("exeption"); + if (validateResponse(dataValue) && validateValueDataType(dataValue)) { - const auto dps = buildDataPacket(opcUaVariant); + const auto dps = buildDataPacket(dataValue); outputSignal.sendPacket(dps.dataPacket); if (dps.domainDataPacket.assigned() && outputDomainSignal.assigned()) outputDomainSignal.sendPacket(dps.domainDataPacket); @@ -389,10 +434,11 @@ void OpcUaMonitoredItemFbImpl::readerLoop() } catch (OpcUaException&) { - LOG_E("Exeption while reading \"{}\"", nodeId.toString()); + statuses.set("exeption", "Exeption while reading."); } } } + updateStatuses(); std::this_thread::sleep_for(std::chrono::milliseconds(config.samplingInterval)); } } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp index 0f101c0..e73c026 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp @@ -129,6 +129,14 @@ void OpcUaServerTestHelper::createModel() publishVariable(".ui64", &myUInt64, &UA_TYPES[UA_TYPES_UINT64], &uaObjectsFolder); } + { + UA_Int64 myInt64 = -64; + publishVariable(".pi64", &myInt64, &UA_TYPES[UA_TYPES_INT64], &uaObjectsFolder, "en_US", 1, 1, 0); + + UA_UInt64 myUInt64 = 64; + publishVariable(".pui64", &myUInt64, &UA_TYPES[UA_TYPES_UINT64], &uaObjectsFolder, "en_US", 1, 1, 0); + } + UA_Boolean myBool = true; publishVariable(".b", &myBool, &UA_TYPES[UA_TYPES_BOOLEAN], &uaObjectsFolder); @@ -196,13 +204,14 @@ void OpcUaServerTestHelper::publishVariable(std::string identifier, UA_NodeId* parentNodeId, const char* locale, uint16_t nodeIndex, - size_t dimension) + size_t dimension, + UA_Byte accessLevel) { OpcUaObject attr = UA_VariableAttributes_default; attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); attr->dataType = type->typeId; - attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + attr->accessLevel = accessLevel; OpcUaNodeId newNodeId(nodeIndex, identifier); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h index dbedf96..b458853 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h @@ -51,7 +51,8 @@ class OpcUaServerTestHelper final UA_NodeId* parentNodeId, const char* locale = "en_US", uint16_t nodeIndex = 1, - size_t dimension = 1); + size_t dimension = 1, + UA_Byte accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE); void writeValueNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value); void writeDataValueNode(const OpcUaNodeId& nodeId, const OpcUaDataValue& value); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index e85a1fb..6616b9b 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -531,3 +531,29 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithSourceTimestampUsingTailReade // check that the target and read TSes are the same EXPECT_EQ(time, domain); } + +TEST_F(GenericOpcuaMonitoredItemTest, ReadProtectedValue) +{ + constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".pi64"); + const auto value = int64_t{std::numeric_limits::min()}; + StartUp(); + + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + + OpcUaDataValue dataValue; + dataValue.setScalar(value); + + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_FALSE(prevVal.assigned()); + + ASSERT_NO_THROW(testHelper.writeDataValueNode(nodeId, dataValue)); + + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], interval * 3, prevVal); + ASSERT_FALSE(val.assigned()); +} \ No newline at end of file From 4d4da218b5dbae70b0d147872ca6fbf8b36904ba Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Mon, 30 Mar 2026 10:10:12 +0200 Subject: [PATCH 07/29] new approach for Error and StatusContainer classes --- .../opcua_monitored_item_fb_impl.h | 167 ++---------- .../opcuageneric_client/status_container.h | 242 ++++++++++++++++++ .../opcuageneric_client/src/CMakeLists.txt | 2 + .../src/opcua_monitored_item_fb_impl.cpp | 80 +++--- 4 files changed, 299 insertions(+), 192 deletions(-) create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_container.h diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index dd9832a..6c65853 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -16,168 +16,25 @@ #pragma once #include -#include +#include #include +#include #include "opcuaclient/opcuaclient.h" BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC -namespace utils -{ - class Error - { - public: - Error(std::string name) - : name(std::move(name)) - , present(false) - , updated(true) - { - } - - void set(std::string msg) - { - updated = true; - present = true; - message = std::move(msg); - } - - void add(const std::string& msg) - { - if (message.empty()) - { - set(msg); - } - else - { - updated = true; - present = true; - message.reserve(msg.size() + 2); - message.append("; "); - message.append(msg); - } - } - - void reset() - { - if (present) - updated = true; - present = false; - message.clear(); - } - bool ok() const - { - return !present; - } - - const std::string& getMessage() const - { - return message; - } - - bool isUpdated() const - { - bool tmp = updated; - updated = false; - return tmp; - } - - protected: - const std::string name; - std::string message; - bool present; - mutable bool updated; - }; - - class StatusContainer - { - public: - StatusContainer() = default; - - bool addStatus(const std::string& name) - { - std::scoped_lock lock(mtx); - if (map.count(name)) - return false; - map.emplace(std::pair(name, Error(name))); - return true; - } - - bool isUpdated() - { - std::scoped_lock lock(mtx); - bool result = false; - for (const auto& e : map) - { - bool isUpdated = e.second.isUpdated(); - result |= isUpdated; - } - return result; - } - void resetAll() - { - std::scoped_lock lock(mtx); - for (auto& e : map) - e.second.reset(); - } - - void set(const std::string& name, std::string msg) - { - std::scoped_lock lock(mtx); - getStatus(name).set(std::move(msg)); - } - - void addToStatus(const std::string& name, const std::string& msg) - { - std::scoped_lock lock(mtx); - getStatus(name).add(msg); - } - - void reset(const std::string& name) - { - std::scoped_lock lock(mtx); - getStatus(name).reset(); - } - - bool ok(const std::string& name) const - { - std::scoped_lock lock(mtx); - return getStatus(name).ok(); - } - - std::string getMessage(const std::string& name) const - { - std::scoped_lock lock(mtx); - return getStatus(name).getMessage(); - } - - protected: - mutable std::mutex mtx; - std::unordered_map map; - - Error& getStatus(const std::string& name) - { - return map.at(name); - } - - const Error& getStatus(const std::string& name) const - { - return map.at(name); - } - }; -} - class OpcUaMonitoredItemFbImpl final : public FunctionBlock { friend class GenericOpcuaMonitoredItemTest; -public: +public: // only string NodeIDs are supported at the moment enum class NodeIDType : int { - //Numeric = 0, + // Numeric = 0, String = 0, - //Guid, - //Opaque, + // Guid, + // Opaque, _count }; @@ -196,15 +53,16 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock const PropertyObjectPtr& config = nullptr); ~OpcUaMonitoredItemFbImpl(); /*DAQ_OPCUA_MODULE_API*/ static FunctionBlockTypePtr CreateType(); -protected: +protected: struct DataPackets { daq::DataPacketPtr dataPacket; daq::DataPacketPtr domainDataPacket; }; - struct FbConfig { + struct FbConfig + { NodeIDType nodeIdType; std::string nodeId; uint32_t namespaceIndex; @@ -227,7 +85,12 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock std::thread readerThread; std::atomic running; - utils::StatusContainer statuses; + std::shared_ptr statuses; + utils::Error configErr; + utils::Error nodeValidationErr; + utils::Error responseValidationErr; + utils::Error valueValidationErr; + utils::Error exceptionErr; void removed() override; static std::string generateLocalId(); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_container.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_container.h new file mode 100644 index 0000000..bd72a80 --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_container.h @@ -0,0 +1,242 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +namespace utils +{ + class StatusContainer; + + class Error + { + public: + Error() = default; + Error(std::weak_ptr container, std::string name) + : container(std::move(container)) + , name(std::move(name)) + { + } + + bool isValid() const + { + return !container.expired() && !name.empty(); + } + + void set(std::string msg); + void add(const std::string& msg); + void reset(); + + bool ok() const; + explicit operator bool() const + { + return ok(); + } + std::string getMessage() const; + bool isUpdated() const; + const std::string& getName() const; + std::string buildStatusMessage() const; + + private: + std::weak_ptr container; + std::string name; + }; + + class StatusContainer : public std::enable_shared_from_this + { + public: + StatusContainer() = default; + + Error addStatus(const std::string& name) + { + std::scoped_lock lock(mtx); + if (entries.count(name)) + return Error{}; + entries.emplace(name, Entry{}); + return Error(weak_from_this(), name); + } + + Error getError(const std::string& name) const + { + std::scoped_lock lock(mtx); + if (!entries.count(name)) + return Error{}; + return Error(std::const_pointer_cast(shared_from_this()), name); + } + + bool isUpdated() const + { + std::scoped_lock lock(mtx); + bool result = false; + for (const auto& e : entries) + result |= isUpdatedImpl(e.second); + return result; + } + + void resetAll() + { + std::scoped_lock lock(mtx); + for (auto& e : entries) + { + if (e.second.present) + e.second.updated = true; + e.second.present = false; + e.second.message.clear(); + } + } + + private: + friend class Error; + + struct Entry + { + std::string message; + bool present = false; + mutable bool updated = true; + }; + + static bool isUpdatedImpl(const Entry& e) + { + bool u = e.updated; + e.updated = false; + return u; + } + + void set(const std::string& name, std::string msg) + { + std::scoped_lock lock(mtx); + auto& e = entries.at(name); + if (!e.present || e.message != msg) + { + e.updated = true; + e.present = true; + e.message = std::move(msg); + } + } + + void add(const std::string& name, const std::string& msg) + { + std::scoped_lock lock(mtx); + auto& e = entries.at(name); + e.updated = true; + e.present = true; + if (e.message.empty()) + { + e.message = msg; + } + else + { + e.message.reserve(e.message.size() + msg.size() + 2); + e.message.append("; "); + e.message.append(msg); + } + } + + void reset(const std::string& name) + { + std::scoped_lock lock(mtx); + auto& e = entries.at(name); + if (e.present) + { + e.updated = true; + e.present = false; + e.message.clear(); + } + } + + bool ok(const std::string& name) const + { + std::scoped_lock lock(mtx); + return !entries.at(name).present; + } + + std::string getMessage(const std::string& name) const + { + std::scoped_lock lock(mtx); + return entries.at(name).message; + } + + bool isUpdated(const std::string& name) const + { + std::scoped_lock lock(mtx); + return isUpdatedImpl(entries.at(name)); + } + + std::string buildStatusMessage(const std::string& name) const + { + std::scoped_lock lock(mtx); + return name + " error: " + entries.at(name).message; + } + + mutable std::mutex mtx; + std::unordered_map entries; + }; + + inline void Error::set(std::string msg) + { + if (auto c = container.lock()) + c->set(name, std::move(msg)); + } + + inline void Error::add(const std::string& msg) + { + if (auto c = container.lock()) + c->add(name, msg); + } + + inline void Error::reset() + { + if (auto c = container.lock()) + c->reset(name); + } + + inline bool Error::ok() const + { + auto c = container.lock(); + return !c || c->ok(name); + } + + inline std::string Error::getMessage() const + { + auto c = container.lock(); + return c ? c->getMessage(name) : std::string{}; + } + + inline bool Error::isUpdated() const + { + auto c = container.lock(); + return c && c->isUpdated(name); + } + + inline const std::string& Error::getName() const + { + return name; + } + + inline std::string Error::buildStatusMessage() const + { + auto c = container.lock(); + return c ? c->buildStatusMessage(name) : std::string{}; + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt index c98c49b..d750255 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt @@ -6,6 +6,7 @@ set(SRC_PublicHeaders constants.h opcuageneric.h generic_client_device_impl.h opcua_monitored_item_fb_impl.h + status_container.h ) set(SRC_Cpp generic_client_device_impl.cpp opcua_monitored_item_fb_impl.cpp @@ -13,6 +14,7 @@ set(SRC_Cpp generic_client_device_impl.cpp source_group("common" FILES ${HEADERS_DIR}/constants.h ${HEADERS_DIR}/opcuageneric.h + ${HEADERS_DIR}/status_container.h ) source_group("device" FILES ${HEADERS_DIR}/generic_client_device_impl.h generic_client_device_impl.cpp diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 5a04739..b13abd1 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -60,6 +60,7 @@ OpcUaMonitoredItemFbImpl::OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, , client(client) , nodeId() , running(false) + , statuses(std::make_shared()) { initComponentStatus(); initStatusContainer(); @@ -98,11 +99,11 @@ void OpcUaMonitoredItemFbImpl::removed() void OpcUaMonitoredItemFbImpl::initStatusContainer() { - statuses.addStatus("config"); - statuses.addStatus("nodeValidation"); - statuses.addStatus("valueValidation"); - statuses.addStatus("responseValidation"); - statuses.addStatus("exeption"); + configErr = statuses->addStatus("Config"); + nodeValidationErr = statuses->addStatus("Node validation"); + valueValidationErr = statuses->addStatus("Value validation"); + responseValidationErr = statuses->addStatus("Response validation"); + exceptionErr = statuses->addStatus("Exception"); } FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() @@ -163,7 +164,7 @@ std::string OpcUaMonitoredItemFbImpl::generateLocalId() void OpcUaMonitoredItemFbImpl::adjustSignalDescriptor() { - if (statuses.ok("nodeValidation") && supportedDataTypes.count(nodeDataType) != 0) + if (nodeValidationErr.ok() && supportedDataTypes.count(nodeDataType) != 0) { outputSignalDescriptor = DataDescriptorBuilder().setSampleType(supportedDataTypes[nodeDataType]).build(); } @@ -199,14 +200,13 @@ void OpcUaMonitoredItemFbImpl::initProperties(const PropertyObjectPtr& config) void OpcUaMonitoredItemFbImpl::readProperties() { auto lock = this->getRecursiveConfigLock(); - statuses.reset("config"); - + configErr.reset(); config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment config.nodeId = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID, ""); if (config.nodeId.empty()) { - statuses.addToStatus("config", fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID)); + configErr.add(fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID)); } config.namespaceIndex = readProperty(objPtr, PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 0); @@ -214,8 +214,8 @@ void OpcUaMonitoredItemFbImpl::readProperties() readProperty(objPtr, PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); if (config.samplingInterval <= 0) { - statuses.addToStatus("config", fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", - PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL)); + configErr.add(fmt::format("Invalid value for the \"{}\" property! Sampling interval must be a positive integer.", + PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL)); config.samplingInterval = DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL; } @@ -241,7 +241,7 @@ void OpcUaMonitoredItemFbImpl::propertyChanged() nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; - statuses.resetAll(); + statuses->resetAll(); validateNode(); adjustSignalDescriptor(); reconfigureSignal(prevConfig); @@ -250,28 +250,28 @@ void OpcUaMonitoredItemFbImpl::propertyChanged() void OpcUaMonitoredItemFbImpl::updateStatuses() { - if (!statuses.isUpdated()) + if (!statuses->isUpdated()) return; - if (!statuses.ok("config")) + if (!configErr.ok()) { - setComponentStatusWithMessage(ComponentStatus::Error, "Configuration is invalid! " + statuses.getMessage("config")); + setComponentStatusWithMessage(ComponentStatus::Error, configErr.buildStatusMessage()); } - else if (!statuses.ok("nodeValidation")) + else if (!nodeValidationErr.ok()) { - setComponentStatusWithMessage(ComponentStatus::Error, "Node is invalid! " + statuses.getMessage("nodeValidation")); + setComponentStatusWithMessage(ComponentStatus::Error, nodeValidationErr.buildStatusMessage()); } - else if (!statuses.ok("responseValidation")) + else if (!responseValidationErr.ok()) { - setComponentStatusWithMessage(ComponentStatus::Error, "Response error! " + statuses.getMessage("responseValidation")); + setComponentStatusWithMessage(ComponentStatus::Error, responseValidationErr.buildStatusMessage()); } - else if (!statuses.ok("valueValidation")) + else if (!valueValidationErr.ok()) { - setComponentStatusWithMessage(ComponentStatus::Error, "Value error! " + statuses.getMessage("valueValidation")); + setComponentStatusWithMessage(ComponentStatus::Error, valueValidationErr.buildStatusMessage()); } - else if (!statuses.ok("exeption")) + else if (!exceptionErr.ok()) { - setComponentStatusWithMessage(ComponentStatus::Error, statuses.getMessage("exeption")); + setComponentStatusWithMessage(ComponentStatus::Error, exceptionErr.buildStatusMessage()); } else { @@ -283,34 +283,34 @@ void OpcUaMonitoredItemFbImpl::validateNode() { try { - statuses.reset("nodeValidation"); + nodeValidationErr.reset(); auto nodeExist = client->nodeExists(nodeId); if (!nodeExist) { - statuses.addToStatus("nodeValidation", fmt::format("Node {} does not exist", nodeId.toString())); + nodeValidationErr.add(fmt::format("Node {} does not exist", nodeId.toString())); } else if (const auto nodeClass = client->readNodeClass(nodeId); nodeClass != UA_NodeClass::UA_NODECLASS_VARIABLE) { - statuses.addToStatus("nodeValidation", fmt::format("Node {} is not a variable node", nodeId.toString())); + nodeValidationErr.add(fmt::format("Node {} is not a variable node", nodeId.toString())); } else if (const auto accessLevel = client->readAccessLevel(nodeId); (accessLevel & UA_ACCESSLEVELMASK_READ) == 0) { - statuses.addToStatus("nodeValidation", fmt::format("There is no read permission to node {}", nodeId.toString())); + nodeValidationErr.add(fmt::format("There is no read permission to node {}", nodeId.toString())); } else if (nodeDataType = client->readDataType(nodeId); supportedDataTypes.count(nodeDataType) == 0) { - statuses.addToStatus("nodeValidation", fmt::format("Node {} has unsupported DataType ({})", nodeId.toString(), nodeDataType.toString())); + nodeValidationErr.add(fmt::format("Node {} has unsupported DataType ({})", nodeId.toString(), nodeDataType.toString())); } } catch (OpcUaException& ex) { if (ex.getStatusCode() == UA_STATUSCODE_BADUSERACCESSDENIED) { - statuses.addToStatus("nodeValidation", fmt::format("Access denied for node {}", nodeId.toString())); + nodeValidationErr.add(fmt::format("Access denied for node {}", nodeId.toString())); } else { - statuses.addToStatus("nodeValidation", fmt::format("Exception was thrown while node {} validatiion", nodeId.toString())); + nodeValidationErr.add(fmt::format("Exception was thrown while node {} validatiion", nodeId.toString())); } } } @@ -319,26 +319,26 @@ bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) { if (value.getValue().hasStatus && value.getValue().hasStatus != UA_STATUSCODE_GOOD) { - statuses.addToStatus("responseValidation", fmt::format("Reading value error: {}. ", value.getValue().hasStatus)); + responseValidationErr.add(fmt::format("Reading value error: {}. ", value.getValue().hasStatus)); return false; } if (!value.getValue().hasValue) { - statuses.addToStatus("responseValidation", std::string("Reading value error: response without a value.")); + responseValidationErr.add(std::string("Reading value error: response without a value.")); return false; } if (config.domainSource == DomainSource::ServerTimestamp && !value.getValue().hasServerTimestamp) { - statuses.addToStatus("responseValidation", std::string("Reading value error: there is no required server timestamp")); + responseValidationErr.add(std::string("Reading value error: there is no required server timestamp")); return false; } if (config.domainSource == DomainSource::SourceTimestamp && !value.getValue().hasSourceTimestamp) { - statuses.addToStatus("responseValidation", std::string("Reading value error: there is no required source timestamp")); + responseValidationErr.add(std::string("Reading value error: there is no required source timestamp")); return false; } - statuses.reset("responseValidation"); + responseValidationErr.reset(); return true; } @@ -354,9 +354,9 @@ bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value bool valid = (value.isNumber() || value.isString()); if (valid) - statuses.reset("valueValidation"); + valueValidationErr.reset(); else - statuses.set("valueValidation", "Value has unsupported type."); + valueValidationErr.set("Value has unsupported type."); return valid; } @@ -416,14 +416,14 @@ void OpcUaMonitoredItemFbImpl::readerLoop() { { // auto lockProcessing = std::scoped_lock(processingMutex); - if (statuses.ok("config") && statuses.ok("nodeValidation")) + if (configErr.ok() && nodeValidationErr.ok()) { OpcUaDataValue dataValue; try { dataValue = client->readDataValue(nodeId); - statuses.reset("exeption"); + exceptionErr.reset(); if (validateResponse(dataValue) && validateValueDataType(dataValue)) { const auto dps = buildDataPacket(dataValue); @@ -434,7 +434,7 @@ void OpcUaMonitoredItemFbImpl::readerLoop() } catch (OpcUaException&) { - statuses.set("exeption", "Exeption while reading."); + exceptionErr.set("Exception while reading."); } } } From 75bc8a40d34429ab8b50adbdbb350749d188bd90 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Mon, 30 Mar 2026 11:04:08 +0200 Subject: [PATCH 08/29] MonitoredItem: locks for the FB --- .../opcua_monitored_item_fb_impl.h | 1 + .../src/opcua_monitored_item_fb_impl.cpp | 17 +++++++++++++++-- .../tests/test_opcua_monitored_item_fb.cpp | 1 - 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index 6c65853..c767964 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -84,6 +84,7 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock std::thread readerThread; std::atomic running; + std::recursive_mutex processingMutex; std::shared_ptr statuses; utils::Error configErr; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index b13abd1..c9b7c46 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -164,6 +164,7 @@ std::string OpcUaMonitoredItemFbImpl::generateLocalId() void OpcUaMonitoredItemFbImpl::adjustSignalDescriptor() { + auto lockProcessing = std::scoped_lock(processingMutex); if (nodeValidationErr.ok() && supportedDataTypes.count(nodeDataType) != 0) { outputSignalDescriptor = DataDescriptorBuilder().setSampleType(supportedDataTypes[nodeDataType]).build(); @@ -200,6 +201,8 @@ void OpcUaMonitoredItemFbImpl::initProperties(const PropertyObjectPtr& config) void OpcUaMonitoredItemFbImpl::readProperties() { auto lock = this->getRecursiveConfigLock(); + auto lockProcessing = std::scoped_lock(processingMutex); + configErr.reset(); config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment @@ -236,6 +239,8 @@ void OpcUaMonitoredItemFbImpl::readProperties() void OpcUaMonitoredItemFbImpl::propertyChanged() { auto lock = this->getRecursiveConfigLock(); + auto lockProcessing = std::scoped_lock(processingMutex); + auto prevConfig = config; readProperties(); @@ -281,6 +286,7 @@ void OpcUaMonitoredItemFbImpl::updateStatuses() void OpcUaMonitoredItemFbImpl::validateNode() { + auto lockProcessing = std::scoped_lock(processingMutex); try { nodeValidationErr.reset(); @@ -317,6 +323,7 @@ void OpcUaMonitoredItemFbImpl::validateNode() bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) { + auto lockProcessing = std::scoped_lock(processingMutex); if (value.getValue().hasStatus && value.getValue().hasStatus != UA_STATUSCODE_GOOD) { responseValidationErr.add(fmt::format("Reading value error: {}. ", value.getValue().hasStatus)); @@ -344,6 +351,7 @@ bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value) { + auto lockProcessing = std::scoped_lock(processingMutex); OpcUaNodeId valueDataType(value.getValue().value.type->typeId); if (valueDataType != nodeDataType) { @@ -374,6 +382,7 @@ void OpcUaMonitoredItemFbImpl::createSignal() void OpcUaMonitoredItemFbImpl::reconfigureSignal(const FbConfig& prevConfig) { auto lock = this->getRecursiveConfigLock(); + auto lockProcessing = std::scoped_lock(processingMutex); if (config.domainSource != DomainSource::None) { @@ -392,6 +401,8 @@ void OpcUaMonitoredItemFbImpl::reconfigureSignal(const FbConfig& prevConfig) SignalConfigPtr OpcUaMonitoredItemFbImpl::createDomainSignal() { + auto lock = this->getRecursiveConfigLock(); + const auto domainSignalDsc = DataDescriptorBuilder() .setSampleType(SampleType::UInt64) .setRule(ExplicitDataRule()) @@ -414,8 +425,10 @@ void OpcUaMonitoredItemFbImpl::readerLoop() { while (running) { + uint32_t samplingInterval = 1; { - // auto lockProcessing = std::scoped_lock(processingMutex); + auto lockProcessing = std::scoped_lock(processingMutex); + samplingInterval = config.samplingInterval; if (configErr.ok() && nodeValidationErr.ok()) { OpcUaDataValue dataValue; @@ -439,7 +452,7 @@ void OpcUaMonitoredItemFbImpl::readerLoop() } } updateStatuses(); - std::this_thread::sleep_for(std::chrono::milliseconds(config.samplingInterval)); + std::this_thread::sleep_for(std::chrono::milliseconds(samplingInterval)); } } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 6616b9b..4223e92 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -7,7 +7,6 @@ #include "opcuageneric_client/constants.h" #include "opcuaservertesthelper.h" #include "opendaq/reader_factory.h" -#include "opendaq/sample_type_traits.h" #include "test_daq_test_helper.h" #include "timer.h" From d3938dac654325bd016a7f23adb28f91fc054616 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Mon, 30 Mar 2026 17:11:28 +0200 Subject: [PATCH 09/29] MonitoredItem: fixes --- .../src/opcua_monitored_item_fb_impl.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index c9b7c46..fc63411 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -384,7 +384,7 @@ void OpcUaMonitoredItemFbImpl::reconfigureSignal(const FbConfig& prevConfig) auto lock = this->getRecursiveConfigLock(); auto lockProcessing = std::scoped_lock(processingMutex); - if (config.domainSource != DomainSource::None) + if (config.domainSource == DomainSource::None) { if (outputDomainSignal.assigned()) { @@ -397,6 +397,10 @@ void OpcUaMonitoredItemFbImpl::reconfigureSignal(const FbConfig& prevConfig) { outputSignal.setDomainSignal(createDomainSignal()); } + if (outputSignal.getDescriptor() != outputSignalDescriptor) + { + outputSignal.setDescriptor(outputSignalDescriptor); + } } SignalConfigPtr OpcUaMonitoredItemFbImpl::createDomainSignal() @@ -408,7 +412,7 @@ SignalConfigPtr OpcUaMonitoredItemFbImpl::createDomainSignal() .setRule(ExplicitDataRule()) .setUnit(Unit("s", -1, "seconds", "time")) .setTickResolution(Ratio(1, 1'000'000)) - .setOrigin("1970-01-01T00:00:00") + .setOrigin("1970-01-01T00:00:00Z") .setName("Time") .build(); outputDomainSignal = createAndAddSignal(OPCUA_TS_SIGNAL_LOCAL_ID, domainSignalDsc, false); From da50137f39851266de1dc5e221465b970ea74ae3 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Mon, 30 Mar 2026 17:15:01 +0200 Subject: [PATCH 10/29] MonitoredItem: delay calculation --- .../src/opcua_monitored_item_fb_impl.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index fc63411..611e860 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -427,12 +427,13 @@ void OpcUaMonitoredItemFbImpl::runReaderThread() void OpcUaMonitoredItemFbImpl::readerLoop() { + auto start = std::chrono::high_resolution_clock::now(); while (running) { - uint32_t samplingInterval = 1; + auto nextTP = start; { auto lockProcessing = std::scoped_lock(processingMutex); - samplingInterval = config.samplingInterval; + nextTP += std::chrono::milliseconds(config.samplingInterval); if (configErr.ok() && nodeValidationErr.ok()) { OpcUaDataValue dataValue; @@ -456,7 +457,10 @@ void OpcUaMonitoredItemFbImpl::readerLoop() } } updateStatuses(); - std::this_thread::sleep_for(std::chrono::milliseconds(samplingInterval)); + auto sleepTime = std::chrono::duration_cast(nextTP - std::chrono::high_resolution_clock::now()); + start = nextTP; + sleepTime = (sleepTime.count() > 0) ? sleepTime : std::chrono::microseconds(0); + std::this_thread::sleep_for(sleepTime); } } From a2de47ae51d46a846307dc7041dbf1b54ade8c9e Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Mon, 30 Mar 2026 17:15:36 +0200 Subject: [PATCH 11/29] opendaq_ref --- opendaq_ref | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendaq_ref b/opendaq_ref index d0a6910..c2e0825 100644 --- a/opendaq_ref +++ b/opendaq_ref @@ -1 +1 @@ -4757349a1db30721bead0cb695ec3a147913aef4 +71b86c79f1d909f305129a82553a007516d478c3 From 2aa2b4a2f0cd5b5909cce32a4a82b71880267f16 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Tue, 31 Mar 2026 12:42:29 +0200 Subject: [PATCH 12/29] MonitoredItem FB: local system timestamp --- .../opcua_monitored_item_fb_impl.h | 1 + .../src/opcua_monitored_item_fb_impl.cpp | 8 +++- .../tests/test_opcua_monitored_item_fb.cpp | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index c767964..08176fe 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -43,6 +43,7 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock None = 0, ServerTimestamp, SourceTimestamp, + LocalSystemTimestamp, _count }; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 611e860..e7c9269 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -2,6 +2,7 @@ #include #include "opendaq/binary_data_packet_factory.h" #include "opendaq/packet_factory.h" +#include BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC @@ -144,7 +145,7 @@ FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() { auto builder = SelectionPropertyBuilder(PROPERTY_NAME_OPCUA_TS_MODE, - List("None", "ServerTimestamp", "SourceTimestamp"), + List("None", "ServerTimestamp", "SourceTimestamp", "LocalSystemTimestamp"), static_cast(DomainSource::ServerTimestamp)) .setDescription("Defines what to use as a domain signal. By default it is set to ServerTimestamp."); defaultConfig.addProperty(builder.build()); @@ -543,6 +544,11 @@ DataPacketPtr OpcUaMonitoredItemFbImpl::buildDomainDataPacket(const OpcUaDataVal { domainDp = fillDmainPacket(value.getSourceTimestampUnixEpoch()); } + else if (config.domainSource == DomainSource::LocalSystemTimestamp) + { + const uint64_t epochTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + domainDp = fillDmainPacket(epochTime); + } return domainDp; } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 4223e92..ecd84ac 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -422,6 +422,50 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithSourceTimestampUsingLastValue EXPECT_EQ(time, domainVal.asPtr().getValue(uint64_t(0))); } +TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithLocalSystemTimestampUsingLastValue) +{ + constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".i64"); + const auto value = int64_t{std::numeric_limits::min()}; + StartUp(); + + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::LocalSystemTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + auto domainSig = fb.getSignals()[0].getDomainSignal(); + ASSERT_TRUE(domainSig.assigned()); + + const OpcUaVariant variant(value); + + // before writing + // waiting to be sure that FB has read initial value + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); + + const auto timeBefore = getTime(); + + ASSERT_NO_THROW(testHelper.writeValueNode(nodeId, variant)); + + // after writing + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + // the FB needs time to read the new value from the node + // read the last value until it becomes different from the initial or until the timer expires + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], interval * 3, prevVal); + + auto domainVal = domainSig.getLastValue(); + const auto timeAfter = getTime(); + + // check that the target and read values are the same + ASSERT_EQ(val.asPtr().getValue(int64_t(0)), value); + + // check the ts is between start and stop points + ASSERT_TRUE(domainVal.assigned()); + EXPECT_GE(timeAfter, domainVal.asPtr().getValue(uint64_t(0))); + EXPECT_LE(timeBefore, domainVal.asPtr().getValue(uint64_t(0))); +} + TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithServerTimestampUsingTailReader) { constexpr uint32_t interval = 50; From 84a553abd164d0ce30205780b55f51817f4b2a31 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Tue, 31 Mar 2026 13:19:29 +0200 Subject: [PATCH 13/29] generic opcua module: ProtocolType::Unknown --- .../src/opcua_generic_client_module_impl.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp index b148509..7040dd1 100644 --- a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp +++ b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp @@ -31,7 +31,7 @@ OpcUaGenericClientModule::OpcUaGenericClientModule(ContextPtr context) OPCUA_GENERIC_CLIENT_MODULE_PATCH_VERSION), std::move(context), MODULE_ID) - , discoveryClient({"OPENDAQ"}) + , discoveryClient() { loggerComponent = this->context.getLogger().getOrAddComponent(DaqOpcUaGenericProtocolId); discoveryClient.initMdnsClient(List("_opcua-tcp._tcp.local.")); @@ -82,10 +82,6 @@ DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionSt DevicePtr device(createWithImplementation(context, parent, configPtr)); - // auto deviceType = createDeviceType(); - // checkErrorInfo(deviceType.asPtr()->setModuleInfo(moduleInfo)); - // device.asPtr().setMirroredDeviceType(deviceType); - // Set the connection info for the device DeviceInfoPtr deviceInfo = device.getInfo(); deviceInfo.asPtr().setProtectedPropertyValue("connectionString", connectionString); @@ -99,7 +95,7 @@ DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionSt connectionInfo.setProtocolId(DaqOpcUaGenericDeviceTypeId) .setProtocolName(DaqOpcUaGenericProtocolId) - .setProtocolType(ProtocolType::Streaming) + .setProtocolType(ProtocolType::Unknown) .setConnectionType("TCP/IP") .addAddress(host) .setPort(port) @@ -126,7 +122,7 @@ PropertyObjectPtr OpcUaGenericClientModule::populateDefaultConfig(const Property DeviceInfoPtr OpcUaGenericClientModule::populateDiscoveredDevice(const MdnsDiscoveredDevice& discoveredDevice) { - auto cap = ServerCapability(DaqOpcUaGenericDeviceTypeId, DaqOpcUaGenericProtocolId, ProtocolType::Configuration); + auto cap = ServerCapability(DaqOpcUaGenericDeviceTypeId, DaqOpcUaGenericProtocolId, ProtocolType::Unknown); for (const auto& ipAddress : discoveredDevice.ipv4Addresses) { From 7a8ac65bfa7891df5aadeb9f1986f7611ba95b71 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Tue, 31 Mar 2026 19:20:24 +0200 Subject: [PATCH 14/29] MonitoredItem: fixes --- .../opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index e7c9269..94235af 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -242,12 +242,13 @@ void OpcUaMonitoredItemFbImpl::propertyChanged() auto lock = this->getRecursiveConfigLock(); auto lockProcessing = std::scoped_lock(processingMutex); + statuses->resetAll(); + auto prevConfig = config; readProperties(); nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; - statuses->resetAll(); validateNode(); adjustSignalDescriptor(); reconfigureSignal(prevConfig); @@ -325,7 +326,7 @@ void OpcUaMonitoredItemFbImpl::validateNode() bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) { auto lockProcessing = std::scoped_lock(processingMutex); - if (value.getValue().hasStatus && value.getValue().hasStatus != UA_STATUSCODE_GOOD) + if (value.getValue().hasStatus && value.getValue().status != UA_STATUSCODE_GOOD) { responseValidationErr.add(fmt::format("Reading value error: {}. ", value.getValue().hasStatus)); return false; From 9b72f5209c8953e4fc574ac84c3100dffda482c2 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Tue, 31 Mar 2026 19:20:41 +0200 Subject: [PATCH 15/29] MonitoredItem: tests --- .../tests/test_opcua_monitored_item_fb.cpp | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index ecd84ac..9e068ff 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -234,6 +234,35 @@ TEST_F(GenericOpcuaMonitoredItemTest, CreationWithCustomConfig) ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); } +TEST_F(GenericOpcuaMonitoredItemTest, TwoFbCreation) +{ + StartUp(); + { + daq::FunctionBlockPtr fb; + + auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, ".i32"); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); + config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 100); + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + } + { + daq::FunctionBlockPtr fb; + + auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, ".i64"); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); + config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 100); + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + } + auto fbs = device.getFunctionBlocks(); + ASSERT_EQ(fbs.getCount(), 2u); +} + TEST_P(GenericOpcuaMonitoredItemPTest, ReadValue) { constexpr uint32_t interval = 50; @@ -599,4 +628,198 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadProtectedValue) daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], interval * 3, prevVal); ASSERT_FALSE(val.assigned()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithLocalSystemTimestampUsingTailReader) +{ + constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, ".i64"); + const auto value = int64_t{std::numeric_limits::min()}; + StartUp(); + + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), interval, DS::LocalSystemTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + auto domainSig = fb.getSignals()[0].getDomainSignal(); + ASSERT_TRUE(domainSig.assigned()); + + const OpcUaVariant variant(value); + + // waiting to be sure that FB has read initial value + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); + + const auto timeBefore = getTime(); + + auto reader = TailReaderBuilder() + .setSignal(fb.getSignals()[0]) + .setHistorySize(1) + .setValueReadType(SampleType::Int64) + .setDomainReadType(SampleType::UInt64) + .setSkipEvents(true) + .build(); + + ASSERT_NO_THROW(testHelper.writeValueNode(nodeId, variant)); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + std::this_thread::sleep_for(std::chrono::milliseconds(interval * 3)); + + SizeT count{1}; + int64_t values{}; + uint64_t domain{}; + reader.readWithDomain(&values, &domain, &count); + const auto timeAfter = getTime(); + + ASSERT_EQ(values, value); + + EXPECT_GE(timeAfter, domain); + EXPECT_LE(timeBefore, domain); +} + +TEST_F(GenericOpcuaMonitoredItemTest, TsModeNoneCreatesSingleSignal) +{ + StartUp(); + + CreateMonitoredItemFB(".i32", 1, 100, DS::None); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 1u); + EXPECT_FALSE(fb.getSignals()[0].getDomainSignal().assigned()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureTsModeTogglesDomainSignal) +{ + StartUp(); + + CreateMonitoredItemFB(".i32", 1, 100, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + EXPECT_TRUE(fb.getSignals()[0].getDomainSignal().assigned()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_TS_MODE, static_cast(DS::None)); + + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 1u); + EXPECT_FALSE(fb.getSignals()[0].getDomainSignal().assigned()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_TS_MODE, static_cast(DS::ServerTimestamp)); + + EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); + EXPECT_TRUE(fb.getSignals()[0].getDomainSignal().assigned()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNodeIdFromInvalidToValid) +{ + StartUp(); + + CreateMonitoredItemFB("nonExistent", 1, 100, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, std::string(".i32")); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], 300); + EXPECT_TRUE(val.assigned()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNodeIdFromValidToInvalid) +{ + StartUp(); + + CreateMonitoredItemFB(".i32", 1, 100, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, std::string("nonExistent")); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNamespaceIndex) +{ + StartUp(); + + CreateMonitoredItemFB(".i32", 1, 100, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 0); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, SignalDescriptorSampleTypeMatchesOpcUaDataType) +{ + StartUp(); + + const std::vector> cases = { + {OpcUaNodeId(1, ".f"), SampleType::Float32}, + {OpcUaNodeId(1, ".d"), SampleType::Float64}, + {OpcUaNodeId(1, ".i32"), SampleType::Int32}, + {OpcUaNodeId(1, ".i64"), SampleType::Int64}, + {OpcUaNodeId(1, ".s"), SampleType::String}, + }; + + for (const auto& [nodeId, expectedType] : cases) + { + SCOPED_TRACE("Node: " + nodeId.getIdentifier()); + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), 100); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + EXPECT_EQ(fb.getSignals()[0].getDescriptor().getSampleType(), expectedType); + device.removeFunctionBlock(fb); + fb = nullptr; + } +} + +TEST_F(GenericOpcuaMonitoredItemTest, UnsupportedDataTypeNode) +{ + StartUp(); + + // .b is a BOOLEAN node — not in supportedDataTypes + CreateMonitoredItemFB(".b", 1, 100, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], 300); + EXPECT_FALSE(val.assigned()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, FolderNode) +{ + StartUp(); + + // "f1" ns=1 is an ObjectFolder, not a VARIABLE node + CreateMonitoredItemFB("f1", 1, 100, DS::ServerTimestamp); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ZeroSamplingInterval) +{ + StartUp(); + + auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, std::string(".i32")); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); + config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 0); + + CreateMonitoredItemFB(config); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 10); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 0); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); } \ No newline at end of file From b1249baa7f2a25bf98d6c279f38f5f16b6229863 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Tue, 31 Mar 2026 22:34:15 +0200 Subject: [PATCH 16/29] generic opcua client: renaming --- .../include/opcua_generic_client_module/constants.h | 5 +++-- .../src/opcua_generic_client_module_impl.cpp | 12 ++++++------ .../tests/test_opcua_generic_client_module.cpp | 8 ++++---- .../tests/test_opcua_generic_client_device.cpp | 4 ++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h b/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h index d4377e9..e284ad8 100644 --- a/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h +++ b/modules/opcua_generic_client_module/include/opcua_generic_client_module/constants.h @@ -20,10 +20,11 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC_CLIENT_MODULE -static const char* DaqOpcUaGenericDeviceTypeId = "OpenDAQOPCUAGenericStreaming"; +static const char* DaqOpcUaGenericProtocolId = "OPCUAGeneric"; +static const char* DaqOpcUaGenericProtocolName = "OPCUAGeneric"; +static const char* DaqOpcUaGenericComponentName = "OPCUAGenericClient"; static const char* DaqOpcUaGenericDevicePrefix = "daq.opcua.generic"; static const char* OpcUaGenericScheme = "opc.tcp"; -static const char* DaqOpcUaGenericProtocolId = "OPCUAGenericClient"; static const char* MODULE_NAME = "OpenDAQOPCUAGenericClientModule"; static const char* MODULE_ID = "OpenDAQOPCUAGenericClientModule"; diff --git a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp index 7040dd1..ac2ef54 100644 --- a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp +++ b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp @@ -33,7 +33,7 @@ OpcUaGenericClientModule::OpcUaGenericClientModule(ContextPtr context) MODULE_ID) , discoveryClient() { - loggerComponent = this->context.getLogger().getOrAddComponent(DaqOpcUaGenericProtocolId); + loggerComponent = this->context.getLogger().getOrAddComponent(DaqOpcUaGenericComponentName); discoveryClient.initMdnsClient(List("_opcua-tcp._tcp.local.")); } @@ -93,8 +93,8 @@ DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionSt .setConnectionString(connectionString) .build(); - connectionInfo.setProtocolId(DaqOpcUaGenericDeviceTypeId) - .setProtocolName(DaqOpcUaGenericProtocolId) + connectionInfo.setProtocolId(DaqOpcUaGenericProtocolId) + .setProtocolName(DaqOpcUaGenericProtocolName) .setProtocolType(ProtocolType::Unknown) .setConnectionType("TCP/IP") .addAddress(host) @@ -122,7 +122,7 @@ PropertyObjectPtr OpcUaGenericClientModule::populateDefaultConfig(const Property DeviceInfoPtr OpcUaGenericClientModule::populateDiscoveredDevice(const MdnsDiscoveredDevice& discoveredDevice) { - auto cap = ServerCapability(DaqOpcUaGenericDeviceTypeId, DaqOpcUaGenericProtocolId, ProtocolType::Unknown); + auto cap = ServerCapability(DaqOpcUaGenericProtocolId, DaqOpcUaGenericProtocolName, ProtocolType::Unknown); for (const auto& ipAddress : discoveredDevice.ipv4Addresses) { @@ -238,7 +238,7 @@ bool OpcUaGenericClientModule::acceptsConnectionParameters(const StringPtr& conn Bool OpcUaGenericClientModule::onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) { - if (target.getProtocolId() != DaqOpcUaGenericDeviceTypeId) + if (target.getProtocolId() != DaqOpcUaGenericProtocolId) return false; if (source.getConnectionType() != "TCP/IP") @@ -298,7 +298,7 @@ Bool OpcUaGenericClientModule::onCompleteServerCapability(const ServerCapability DeviceTypePtr OpcUaGenericClientModule::createDeviceType() { return DeviceTypeBuilder() - .setId(DaqOpcUaGenericDeviceTypeId) + .setId(DaqOpcUaGenericProtocolId) .setName("OpcUa enabled device") .setDescription("Network device connected over OpcUa protocol") .setConnectionStringPrefix(DaqOpcUaGenericDevicePrefix) diff --git a/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp b/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp index 9a2a98a..892ae34 100644 --- a/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp +++ b/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp @@ -99,8 +99,8 @@ TEST_F(OpcUaGenericClientModuleTest, GetAvailableComponentTypes) DictPtr deviceTypes; ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); ASSERT_EQ(deviceTypes.getCount(), 1u); - ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAGenericStreaming")); - ASSERT_EQ(deviceTypes.get("OpenDAQOPCUAGenericStreaming").getId(), "OpenDAQOPCUAGenericStreaming"); + ASSERT_TRUE(deviceTypes.hasKey("OPCUAGeneric")); + ASSERT_EQ(deviceTypes.get("OPCUAGeneric").getId(), "OPCUAGeneric"); DictPtr serverTypes; ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); @@ -146,8 +146,8 @@ TEST_F(OpcUaGenericClientModuleTest, DefaultDeviceConfig) DictPtr deviceTypes; ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); ASSERT_EQ(deviceTypes.getCount(), 1u); - ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAGenericStreaming")); - auto config = deviceTypes.get("OpenDAQOPCUAGenericStreaming").createDefaultConfig(); + ASSERT_TRUE(deviceTypes.hasKey("OPCUAGeneric")); + auto config = deviceTypes.get("OPCUAGeneric").createDefaultConfig(); ASSERT_TRUE(config.assigned()); ASSERT_EQ(config.getAllProperties().getCount(), 5u); } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp index 0ea8072..c1703ec 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp @@ -39,8 +39,8 @@ TEST_F(GenericOpcuaClientDeviceTest, DefaultDeviceConfig) ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); ASSERT_EQ(deviceTypes.getCount(), 1u); - ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAGenericStreaming")); - auto defaultConfig = deviceTypes.get("OpenDAQOPCUAGenericStreaming").createDefaultConfig(); + ASSERT_TRUE(deviceTypes.hasKey("OPCUAGeneric")); + auto defaultConfig = deviceTypes.get("OPCUAGeneric").createDefaultConfig(); ASSERT_TRUE(defaultConfig.assigned()); ASSERT_EQ(defaultConfig.getAllProperties().getCount(), 5u); From df8b93a7e7276b135c7120737bf4faffc41b7add Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 1 Apr 2026 15:01:06 +0200 Subject: [PATCH 17/29] generic opcua client: device local id is opcua server application URI; device name is opcua server application name; --- .../src/opcua_generic_client_module_impl.cpp | 38 +++++++++- .../include/opcuaclient/opcuaclient.h | 6 ++ .../opcua/opcuaclient/src/opcuaclient.cpp | 36 +++++++++ .../generic_client_device_impl.h | 10 ++- .../src/generic_client_device_impl.cpp | 76 ++++++++++++++----- .../test_opcua_generic_client_device.cpp | 12 ++- 6 files changed, 152 insertions(+), 26 deletions(-) diff --git a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp index ac2ef54..9e5e440 100644 --- a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp +++ b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp @@ -76,11 +76,39 @@ DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionSt std::string host; std::string hostType; int port; - formConnectionString(connectionString, configPtr, host, port, hostType); + const auto opcuaConnStr = formConnectionString(connectionString, configPtr, host, port, hostType); std::scoped_lock lock(sync); - DevicePtr device(createWithImplementation(context, parent, configPtr)); + std::shared_ptr client; + std::string deviceName; + std::string deviceLocalId; + + try + { + client = std::make_shared(OpcUaEndpoint(opcuaConnStr, + configPtr.getPropertyValue(PROPERTY_NAME_OPCUA_USERNAME), + configPtr.getPropertyValue(PROPERTY_NAME_OPCUA_PASSWORD))); + const auto desc = client->readApplicationDescription(); + + deviceName = desc.name.empty() ? GENERIC_OPCUA_CLIENT_DEVICE_NAME : desc.name; + deviceLocalId = desc.uri.empty() ? "" : desc.uri; + std::replace(deviceLocalId.begin(), deviceLocalId.end(), '/', '-'); + } + + catch (const OpcUaException& e) + { + switch (e.getStatusCode()) + { + case UA_STATUSCODE_BADUSERACCESSDENIED: + case UA_STATUSCODE_BADIDENTITYTOKENINVALID: + DAQ_THROW_EXCEPTION(AuthenticationFailedException, e.what()); + default: + DAQ_THROW_EXCEPTION(NotFoundException, e.what()); + } + } + + DevicePtr device(createWithImplementation(context, parent, configPtr, client, deviceLocalId, deviceName)); // Set the connection info for the device DeviceInfoPtr deviceInfo = device.getInfo(); @@ -162,11 +190,13 @@ DeviceInfoPtr OpcUaGenericClientModule::populateDiscoveredDevice(const MdnsDisco cap.setConnectionType("TCP/IP"); cap.setPrefix(DaqOpcUaGenericDevicePrefix); - cap.setProtocolVersion(discoveredDevice.getPropertyOrDefault("protocolVersion", "")); + cap.setProtocolVersion(""); if (discoveredDevice.servicePort > 0) cap.setPort(discoveredDevice.servicePort); - return populateDiscoveredDeviceInfo(DiscoveryClient::populateDiscoveredInfoProperties, discoveredDevice, cap, createDeviceType()); + auto devInfo = populateDiscoveredDeviceInfo(DiscoveryClient::populateDiscoveredInfoProperties, discoveredDevice, cap, createDeviceType()); + devInfo.asPtr().setName(discoveredDevice.serviceInstance); + return devInfo; } StringPtr OpcUaGenericClientModule::formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config, std::string& host, int& port, std::string& hostType) diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h index b1376a1..1876ddb 100644 --- a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h @@ -95,6 +95,11 @@ class UaClientFactory class OpcUaClient { public: + struct ApplicationDescription{ + std::string name; + std::string uri; + }; + explicit OpcUaClient(const std::string& url); explicit OpcUaClient(const OpcUaEndpoint& endpoint); ~OpcUaClient(); @@ -102,6 +107,7 @@ class OpcUaClient static constexpr size_t CONNECTION_TIMEOUT_SECONDS = 10; void initialize(); + ApplicationDescription readApplicationDescription(); void connect(); void disconnect(bool doClear = true); void clear(); diff --git a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp index b098685..d6dd283 100644 --- a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp +++ b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp @@ -195,6 +195,42 @@ UA_Client* UaClientFactory::build() return client; } +OpcUaClient::ApplicationDescription OpcUaClient::readApplicationDescription() +{ + std::lock_guard guard(getLock()); + ApplicationDescription desc; + if (!uaclient) + initialize(); + + UA_StatusCode status = UA_STATUSCODE_GOOD; + + size_t endpointCount = 0; + UA_EndpointDescription *endpointArray = NULL; + + status = UA_Client_getEndpoints( + uaclient, + endpoint.getUrl().c_str(), + &endpointCount, + &endpointArray + ); + const auto url = endpoint.getUrl(); + if (OPCUA_STATUSCODE_SUCCEEDED(status)) + { + for (size_t i = 0; i < endpointCount; ++i) { + const std::string_view endpointUrl(reinterpret_cast(endpointArray[i].endpointUrl.data), endpointArray[i].endpointUrl.length); + if (url == endpointUrl) + { + const UA_ApplicationDescription& app = endpointArray[i].server; + desc.uri = std::string(reinterpret_cast(app.applicationUri.data), (int)app.applicationUri.length); + desc.name = utils::ToStdString(app.applicationName.text); + } + } + } + + UA_Array_delete(endpointArray, endpointCount, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + return desc; +} + void OpcUaClient::connect() { std::lock_guard guard(getLock()); diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h index 3b30c36..5a1fe29 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h @@ -25,7 +25,12 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC class OpcuaGenericClientDeviceImpl : public Device { public: - explicit OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, const ComponentPtr& parent, const PropertyObjectPtr& config); + explicit OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const PropertyObjectPtr& config, + std::shared_ptr client, + const std::string& localId, + const std::string& name); static PropertyObjectPtr createDefaultConfig(); protected: static std::atomic localIndex; @@ -42,10 +47,11 @@ class OpcuaGenericClientDeviceImpl : public Device FunctionBlockPtr onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) override; void initNestedFbTypes(); + void initProperties(const PropertyObjectPtr& config); + std::string getConnectionString() const; DictObjectPtr nestedFbTypes; - StringPtr connectionString; EnumerationPtr connectionStatus; std::atomic connectedDone{false}; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp index db9557d..12217a5 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp @@ -10,28 +10,45 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC std::atomic OpcuaGenericClientDeviceImpl::localIndex = 0; -OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, const ComponentPtr& parent, const PropertyObjectPtr& config) - : Device(ctx, parent, generateLocalId()), - connectionStatus(Enumeration("ConnectionStatusType", "Connected", this->context.getTypeManager())) +namespace { - this->name = GENERIC_OPCUA_CLIENT_DEVICE_NAME; - - const auto host = config.getPropertyValue(PROPERTY_NAME_OPCUA_HOST).asPtr().toStdString(); - const auto port = config.getPropertyValue(PROPERTY_NAME_OPCUA_PORT).asPtr().getValue(DEFAULT_OPCUA_PORT); - const auto path = config.getPropertyValue(PROPERTY_NAME_OPCUA_PATH).asPtr().toStdString(); + PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& defaultConfig, const PropertyObjectPtr& config) + { + auto newConfig = PropertyObject(); + for (const auto& prop : defaultConfig.getAllProperties()) + { + newConfig.addProperty(prop.asPtr(true).clone()); + const auto propName = prop.getName(); + newConfig.setPropertyValue(propName, config.hasProperty(propName) ? config.getPropertyValue(propName) : prop.getValue()); + } + return newConfig; + } +} - connectionString = std::string(OpcUaGenericScheme) + "://" + host + ":" + std::to_string(port) + path; +OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const PropertyObjectPtr& config, + std::shared_ptr client, + const std::string& localId, + const std::string& name) + : Device(ctx, parent, localId.empty() ? generateLocalId() : localId) + , connectionStatus(Enumeration("ConnectionStatusType", "Connected", this->context.getTypeManager())) + , client(client) +{ + if (this->client == nullptr) + DAQ_THROW_EXCEPTION(UninitializedException, "OpcUaClient is not initialized"); - auto endpoint = OpcUaEndpoint(connectionString); + this->name = name.empty() ? GENERIC_OPCUA_CLIENT_DEVICE_NAME : name; - endpoint.setUsername(config.getPropertyValue(PROPERTY_NAME_OPCUA_USERNAME)); - endpoint.setPassword(config.getPropertyValue(PROPERTY_NAME_OPCUA_PASSWORD)); + if (config.assigned()) + initProperties(populateDefaultConfig(createDefaultConfig(), config)); + else + initProperties(createDefaultConfig()); try { - client = std::make_shared(endpoint); - client->connect(); - client->runIterate(); + this->client->connect(); + this->client->runIterate(); } catch (const OpcUaException& e) { @@ -41,11 +58,10 @@ OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx case UA_STATUSCODE_BADIDENTITYTOKENINVALID: DAQ_THROW_EXCEPTION(AuthenticationFailedException, e.what()); default: - DAQ_THROW_EXCEPTION(NotFoundException, e.what()); + DAQ_THROW_EXCEPTION(GeneralErrorException, e.what()); } } - initComponentStatus(); initNestedFbTypes(); } @@ -63,6 +79,30 @@ PropertyObjectPtr OpcuaGenericClientDeviceImpl::createDefaultConfig() return defaultConfig; } +void OpcuaGenericClientDeviceImpl::initProperties(const PropertyObjectPtr& config) +{ + for (const auto& prop : config.getAllProperties()) + { + const auto propName = prop.getName(); + if (!objPtr.hasProperty(propName)) + { + auto propClone = PropertyBuilder(prop.getName()) + .setValueType(prop.getValueType()) + .setDescription(prop.getDescription()) + .setDefaultValue(prop.getValue()) + .setVisible(prop.getVisible()) + .setReadOnly(true) + .build(); + objPtr.addProperty(propClone); + } + } +} + +std::string OpcuaGenericClientDeviceImpl::getConnectionString() const +{ + return client->getEndpoint().getUrl(); +} + void OpcuaGenericClientDeviceImpl::removed() { Device::removed(); @@ -70,7 +110,7 @@ void OpcuaGenericClientDeviceImpl::removed() DeviceInfoPtr OpcuaGenericClientDeviceImpl::onGetInfo() { - return DeviceInfo(connectionString, GENERIC_OPCUA_CLIENT_DEVICE_NAME); + return DeviceInfo(getConnectionString(), GENERIC_OPCUA_CLIENT_DEVICE_NAME); } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp index c1703ec..3789ce8 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp @@ -67,17 +67,18 @@ TEST_F(GenericOpcuaClientDeviceTest, DefaultDeviceConfig) TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) { const auto instance = Instance(); + const std::string deviceName("open62541-based OPC UA Application"); daq::GenericDevicePtr device; ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842")); ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); - ASSERT_EQ(device.getInfo().getName(), GENERIC_OPCUA_CLIENT_DEVICE_NAME); + ASSERT_EQ(device.getInfo().getName(), deviceName); auto devices = instance.getDevices(); bool contain = false; daq::GenericDevicePtr deviceFromList; for (const auto& d : devices) { - contain = (d.getName() == GENERIC_OPCUA_CLIENT_DEVICE_NAME); + contain = (d.getName() == deviceName); if (contain) { deviceFromList = d; @@ -117,3 +118,10 @@ TEST_F(GenericOpcuaClientDeviceTest, CheckDeviceFunctionalBlocks) ASSERT_GE(fbTypes.getCount(), 1); ASSERT_TRUE(fbTypes.hasKey(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME)); } + +TEST_F(GenericOpcuaClientDeviceTest, CheckDeviceNameLocalId) +{ + StartUp(); + EXPECT_EQ(device.getLocalId().toStdString(), std::string("urn:open62541.server.application")); + EXPECT_EQ(device.getName().toStdString(), std::string("open62541-based OPC UA Application")); +} From 6a678b505b7fd3ffc039e70c796a756ca9ea0c80 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 1 Apr 2026 15:09:54 +0200 Subject: [PATCH 18/29] MonitoredItem: sleep duration fix --- .../src/opcua_monitored_item_fb_impl.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 94235af..1ec54b1 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -459,9 +459,11 @@ void OpcUaMonitoredItemFbImpl::readerLoop() } } updateStatuses(); - auto sleepTime = std::chrono::duration_cast(nextTP - std::chrono::high_resolution_clock::now()); + auto now = std::chrono::high_resolution_clock::now(); + std::chrono::microseconds sleepTime(0); + if (now < nextTP) + sleepTime = std::chrono::duration_cast(nextTP - now); start = nextTP; - sleepTime = (sleepTime.count() > 0) ? sleepTime : std::chrono::microseconds(0); std::this_thread::sleep_for(sleepTime); } } From a82418df6be8385322b3ab6db338b0560b7e7427 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 1 Apr 2026 17:51:12 +0200 Subject: [PATCH 19/29] generic opcua client: node_event_manager.* is mooved to tms; DAQMODULES_OPCUA_ENABLE_GENERIC_CLIENT flag; --- CMakeLists.txt | 1 + modules/CMakeLists.txt | 3 +++ .../opcua_generic_client_module/src/CMakeLists.txt | 1 - shared/libraries/CMakeLists.txt | 11 +++++++++-- shared/libraries/opcua/opcuaserver/src/CMakeLists.txt | 3 --- .../opcuageneric_client/tests/CMakeLists.txt | 1 - .../include/opcuatms_server}/node_event_manager.h | 0 .../opcuatms_server/objects/tms_server_object.h | 2 +- .../opcuatms/opcuatms_server/src/CMakeLists.txt | 2 ++ .../opcuatms_server}/src/node_event_manager.cpp | 2 +- 10 files changed, 17 insertions(+), 9 deletions(-) rename shared/libraries/{opcua/opcuaserver/include/opcuaserver => opcuatms/opcuatms_server/include/opcuatms_server}/node_event_manager.h (100%) rename shared/libraries/{opcua/opcuaserver => opcuatms/opcuatms_server}/src/node_event_manager.cpp (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index ffb97a0..a5b0438 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ opendaq_setup_project_specific_build_options(${REPO_OPTION_PREFIX}) option(${REPO_OPTION_PREFIX}_ENABLE_EXAMPLE_APP "Enable ${REPO_NAME} example applications" ${PROJECT_IS_TOP_LEVEL}) option(${REPO_OPTION_PREFIX}_ENABLE_TESTS "Enable ${REPO_NAME} testing" ${PROJECT_IS_TOP_LEVEL}) option(${REPO_OPTION_PREFIX}_ENABLE_CLIENT "Enable ${REPO_NAME} client module" ${PROJECT_IS_TOP_LEVEL}) +option(${REPO_OPTION_PREFIX}_ENABLE_GENERIC_CLIENT "Enable ${REPO_NAME} client module" ${PROJECT_IS_TOP_LEVEL}) option(${REPO_OPTION_PREFIX}_ENABLE_SERVER "Enable ${REPO_NAME} server module" ${PROJECT_IS_TOP_LEVEL}) option(OPCUA_ENABLE_ENCRYPTION "Enable OpcUa encryption" OFF) cmake_dependent_option(OPENDAQ_ENABLE_OPCUA_INTEGRATION_TESTS "Enable ${REPO_NAME} integration testing" ${PROJECT_IS_TOP_LEVEL} "${REPO_OPTION_PREFIX}_ENABLE_TESTS" OFF) diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index e1e3580..3aa9f4f 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -6,6 +6,9 @@ endif() if (${REPO_OPTION_PREFIX}_ENABLE_CLIENT) add_subdirectory(opcua_client_module) +endif() + +if (${REPO_OPTION_PREFIX}_ENABLE_GENERIC_CLIENT) add_subdirectory(opcua_generic_client_module) endif() diff --git a/modules/opcua_generic_client_module/src/CMakeLists.txt b/modules/opcua_generic_client_module/src/CMakeLists.txt index bd78aef..7910559 100644 --- a/modules/opcua_generic_client_module/src/CMakeLists.txt +++ b/modules/opcua_generic_client_module/src/CMakeLists.txt @@ -32,7 +32,6 @@ if (MSVC) endif() target_link_libraries(${LIB_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq - Boost::uuid PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::discovery ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuageneric_client ) diff --git a/shared/libraries/CMakeLists.txt b/shared/libraries/CMakeLists.txt index dfe2f8e..699dd04 100644 --- a/shared/libraries/CMakeLists.txt +++ b/shared/libraries/CMakeLists.txt @@ -1,5 +1,12 @@ opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) add_subdirectory(opcua) -add_subdirectory(opcuatms) -add_subdirectory(opcuageneric) + +if (${REPO_OPTION_PREFIX}_ENABLE_CLIENT OR ${REPO_OPTION_PREFIX}_ENABLE_SERVER) + add_subdirectory(opcuatms) +endif() + +if (${REPO_OPTION_PREFIX}_ENABLE_GENERIC_CLIENT) + add_subdirectory(opcuageneric) +endif() + diff --git a/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt b/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt index 6b73eb4..6ac2401 100644 --- a/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt +++ b/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt @@ -6,7 +6,6 @@ set(SOURCE_CPPS opcuaserver.cpp opcuataskqueue.cpp opcuaaddnodeparams.cpp opcuatmstypes.cpp - node_event_manager.cpp opcuaservernode.cpp opcuaservernodefactory.cpp event_attributes.cpp @@ -20,7 +19,6 @@ set(SOURCE_HEADERS common.h opcuataskqueue.h opcuatmstypes.h opcuaaddnodeparams.h - node_event_manager.h opcuaservernode.h opcuaservernodefactory.h event_attributes.h @@ -47,7 +45,6 @@ target_link_libraries(${MODULE_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::op ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_utils PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_daq_types ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq - ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuatms_server ) set_target_properties(${MODULE_NAME} PROPERTIES PUBLIC_HEADER "${SOURCE_HEADERS}") diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt index 2c53f55..e5df0b3 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/CMakeLists.txt @@ -27,7 +27,6 @@ endif() target_link_libraries(${TEST_APP} PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_test_utils gtest ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuageneric_client - ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_generic_client_module ) diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/node_event_manager.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/node_event_manager.h similarity index 100% rename from shared/libraries/opcua/opcuaserver/include/opcuaserver/node_event_manager.h rename to shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/node_event_manager.h diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h index 15e44a8..5d52eef 100644 --- a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include #include diff --git a/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt index 796c6be..9f77d75 100644 --- a/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt +++ b/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt @@ -2,6 +2,7 @@ set(LIB_NAME opcuatms_server) set(SRC_Cpp tms_server.cpp tms_server_context.cpp + node_event_manager.cpp ) set(SRC_PublicHeaders @@ -9,6 +10,7 @@ set(SRC_PublicHeaders set(SRC_PrivateHeaders tms_server.h tms_server_context.h + node_event_manager.h ) # objects diff --git a/shared/libraries/opcua/opcuaserver/src/node_event_manager.cpp b/shared/libraries/opcuatms/opcuatms_server/src/node_event_manager.cpp similarity index 99% rename from shared/libraries/opcua/opcuaserver/src/node_event_manager.cpp rename to shared/libraries/opcuatms/opcuatms_server/src/node_event_manager.cpp index c180e04..0578e91 100644 --- a/shared/libraries/opcua/opcuaserver/src/node_event_manager.cpp +++ b/shared/libraries/opcuatms/opcuatms_server/src/node_event_manager.cpp @@ -1,4 +1,4 @@ -#include "opcuaserver/node_event_manager.h" +#include "opcuatms_server/node_event_manager.h" #include BEGIN_NAMESPACE_OPENDAQ_OPCUA From 96022e353b42d0898ea53efaf4ef859f46e021a0 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Wed, 1 Apr 2026 16:38:51 +0300 Subject: [PATCH 20/29] MonitoredItem: test fix --- .../tests/test_opcua_monitored_item_fb.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 9e068ff..1c7a9ea 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -384,6 +384,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithServerTimestampUsingLastValue ASSERT_TRUE(prevVal.assigned()); const auto timeBefore = getTime(); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); ASSERT_NO_THROW(testHelper.writeValueNode(nodeId, variant)); @@ -396,6 +397,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithServerTimestampUsingLastValue auto domainVal = domainSig.getLastValue(); const auto timeAfter = getTime(); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); // check that the target and read values are the same ASSERT_EQ(val.asPtr().getValue(int64_t(0)), value); @@ -517,6 +519,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithServerTimestampUsingTailReade ASSERT_TRUE(prevVal.assigned()); const auto timeBefore = getTime(); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); auto reader = TailReaderBuilder() .setSignal(fb.getSignals()[0]) @@ -651,6 +654,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReadValueWithLocalSystemTimestampUsingTail ASSERT_TRUE(prevVal.assigned()); const auto timeBefore = getTime(); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); auto reader = TailReaderBuilder() .setSignal(fb.getSignals()[0]) @@ -770,7 +774,6 @@ TEST_F(GenericOpcuaMonitoredItemTest, SignalDescriptorSampleTypeMatchesOpcUaData for (const auto& [nodeId, expectedType] : cases) { - SCOPED_TRACE("Node: " + nodeId.getIdentifier()); CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), 100); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); EXPECT_EQ(fb.getSignals()[0].getDescriptor().getSampleType(), expectedType); From 7b208ba0a4aefcf5e5f4c031ef9c272366fa8be4 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 1 Apr 2026 19:41:12 +0200 Subject: [PATCH 21/29] generic opcua client: cmake fix (Boost::uuid) --- modules/opcua_generic_client_module/src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/opcua_generic_client_module/src/CMakeLists.txt b/modules/opcua_generic_client_module/src/CMakeLists.txt index 7910559..bd78aef 100644 --- a/modules/opcua_generic_client_module/src/CMakeLists.txt +++ b/modules/opcua_generic_client_module/src/CMakeLists.txt @@ -32,6 +32,7 @@ if (MSVC) endif() target_link_libraries(${LIB_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq + Boost::uuid PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::discovery ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuageneric_client ) From af985222524d376926674c3f8dfef1a407294faa Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 2 Apr 2026 11:21:08 +0200 Subject: [PATCH 22/29] generic opcua client: LocalId property for a device --- .../src/opcua_generic_client_module_impl.cpp | 13 ++++-- .../test_opcua_generic_client_module.cpp | 2 +- .../include/opcuageneric_client/constants.h | 1 + .../src/generic_client_device_impl.cpp | 6 +++ .../test_opcua_generic_client_device.cpp | 44 ++++++++++++++++++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp index 9e5e440..1b0c2c0 100644 --- a/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp +++ b/modules/opcua_generic_client_module/src/opcua_generic_client_module_impl.cpp @@ -82,7 +82,11 @@ DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionSt std::shared_ptr client; std::string deviceName; - std::string deviceLocalId; + // we let a user to set device local id in the config, but if it's not set, we will try to get it from the server's + // ApplicationDescription. If that's also not set, we will generate a local id for the device. + // A user need to have this opportunity to set local id in the config when they want to have a stable local id for the device across + // different runs of the application, and they don't want to rely on the server to provide it (especially it the server uses default values). + std::string deviceLocalId = configPtr.getPropertyValue(PROPERTY_NAME_OPCUA_MI_LOCAL_ID); try { @@ -92,8 +96,11 @@ DevicePtr OpcUaGenericClientModule::onCreateDevice(const StringPtr& connectionSt const auto desc = client->readApplicationDescription(); deviceName = desc.name.empty() ? GENERIC_OPCUA_CLIENT_DEVICE_NAME : desc.name; - deviceLocalId = desc.uri.empty() ? "" : desc.uri; - std::replace(deviceLocalId.begin(), deviceLocalId.end(), '/', '-'); + if (deviceLocalId.empty()) + { + deviceLocalId = desc.uri.empty() ? "" : desc.uri; + std::replace(deviceLocalId.begin(), deviceLocalId.end(), '/', '-'); + } } catch (const OpcUaException& e) diff --git a/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp b/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp index 892ae34..c19729e 100644 --- a/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp +++ b/modules/opcua_generic_client_module/tests/test_opcua_generic_client_module.cpp @@ -149,7 +149,7 @@ TEST_F(OpcUaGenericClientModuleTest, DefaultDeviceConfig) ASSERT_TRUE(deviceTypes.hasKey("OPCUAGeneric")); auto config = deviceTypes.get("OPCUAGeneric").createDefaultConfig(); ASSERT_TRUE(config.assigned()); - ASSERT_EQ(config.getAllProperties().getCount(), 5u); + ASSERT_EQ(config.getAllProperties().getCount(), 6u); } TEST_F(OpcUaGenericClientModuleTest, CreateFunctionBlockIdNull) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h index da134bf..007769e 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h @@ -26,6 +26,7 @@ static constexpr const char* PROPERTY_NAME_OPCUA_PORT = "Port"; static constexpr const char* PROPERTY_NAME_OPCUA_PATH = "Path"; static constexpr const char* PROPERTY_NAME_OPCUA_USERNAME = "Username"; static constexpr const char* PROPERTY_NAME_OPCUA_PASSWORD = "Password"; +static constexpr const char* PROPERTY_NAME_OPCUA_MI_LOCAL_ID = "LocalId"; // MonitoredItem FB static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID_TYPE = "NodeIDType"; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp index 12217a5..6c92487 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp @@ -17,6 +17,8 @@ namespace auto newConfig = PropertyObject(); for (const auto& prop : defaultConfig.getAllProperties()) { + if (prop.getName() == PROPERTY_NAME_OPCUA_MI_LOCAL_ID) + continue; newConfig.addProperty(prop.asPtr(true).clone()); const auto propName = prop.getName(); newConfig.setPropertyValue(propName, config.hasProperty(propName) ? config.getPropertyValue(propName) : prop.getValue()); @@ -75,6 +77,8 @@ PropertyObjectPtr OpcuaGenericClientDeviceImpl::createDefaultConfig() defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_PATH, DEFAULT_OPCUA_PATH)); defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_USERNAME, DEFAULT_OPCUA_USERNAME)); defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_PASSWORD, DEFAULT_OPCUA_PASSWORD)); + defaultConfig.addProperty(StringProperty(PROPERTY_NAME_OPCUA_MI_LOCAL_ID, "")); + return defaultConfig; } @@ -84,6 +88,8 @@ void OpcuaGenericClientDeviceImpl::initProperties(const PropertyObjectPtr& confi for (const auto& prop : config.getAllProperties()) { const auto propName = prop.getName(); + if (propName == PROPERTY_NAME_OPCUA_MI_LOCAL_ID) + continue; if (!objPtr.hasProperty(propName)) { auto propClone = PropertyBuilder(prop.getName()) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp index 3789ce8..8fe03ee 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp @@ -43,25 +43,28 @@ TEST_F(GenericOpcuaClientDeviceTest, DefaultDeviceConfig) auto defaultConfig = deviceTypes.get("OPCUAGeneric").createDefaultConfig(); ASSERT_TRUE(defaultConfig.assigned()); - ASSERT_EQ(defaultConfig.getAllProperties().getCount(), 5u); + ASSERT_EQ(defaultConfig.getAllProperties().getCount(), 6u); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_HOST)); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_PORT)); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_PATH)); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_USERNAME)); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_PASSWORD)); + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_MI_LOCAL_ID)); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_HOST).getValueType(), CoreType::ctString); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_PORT).getValueType(), CoreType::ctInt); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_PATH).getValueType(), CoreType::ctString); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_USERNAME).getValueType(), CoreType::ctString); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_PASSWORD).getValueType(), CoreType::ctString); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_MI_LOCAL_ID).getValueType(), CoreType::ctString); EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_HOST), DEFAULT_OPCUA_HOST); EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_PORT), DEFAULT_OPCUA_PORT); EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_PATH), DEFAULT_OPCUA_PATH); EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_USERNAME), DEFAULT_OPCUA_USERNAME); EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_PASSWORD), DEFAULT_OPCUA_PASSWORD); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_MI_LOCAL_ID), ""); } TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) @@ -89,6 +92,45 @@ TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) ASSERT_TRUE(deviceFromList.assigned()); ASSERT_EQ(deviceFromList.getInfo().getName(), device.getInfo().getName()); ASSERT_TRUE(deviceFromList == device); + + ASSERT_EQ(device.getAllProperties().getCount(), 5u); + ASSERT_TRUE(device.hasProperty(PROPERTY_NAME_OPCUA_HOST)); + ASSERT_TRUE(device.hasProperty(PROPERTY_NAME_OPCUA_PORT)); + ASSERT_TRUE(device.hasProperty(PROPERTY_NAME_OPCUA_PATH)); + ASSERT_TRUE(device.hasProperty(PROPERTY_NAME_OPCUA_USERNAME)); + ASSERT_TRUE(device.hasProperty(PROPERTY_NAME_OPCUA_PASSWORD)); + ASSERT_FALSE(device.hasProperty(PROPERTY_NAME_OPCUA_MI_LOCAL_ID)); +} + + +TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithLocalId) +{ + const auto module = CreateModule(); + const auto instance = Instance(); + const std::string deviceLocalId("myCustomLocalId"); + daq::GenericDevicePtr device; + auto defaultConfig = module.getAvailableDeviceTypes().get("OPCUAGeneric").createDefaultConfig(); + defaultConfig.setPropertyValue(PROPERTY_NAME_OPCUA_MI_LOCAL_ID, deviceLocalId); + ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842", defaultConfig)); + ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), + Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); + ASSERT_EQ(device.getLocalId(), deviceLocalId); + ASSERT_NE(device.getGlobalId().toStdString().find(deviceLocalId), std::string::npos); +} + +TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithoutLocalId) +{ + const auto module = CreateModule(); + const auto instance = Instance(); + const std::string deviceLocalId("urn:open62541.server.application"); + daq::GenericDevicePtr device; + auto defaultConfig = module.getAvailableDeviceTypes().get("OPCUAGeneric").createDefaultConfig(); + + ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842", defaultConfig)); + ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), + Enumeration("ComponentStatusType", "Ok", instance.getContext().getTypeManager())); + ASSERT_EQ(device.getLocalId(), deviceLocalId); + ASSERT_NE(device.getGlobalId().toStdString().find(deviceLocalId), std::string::npos); } TEST_F(GenericOpcuaClientDeviceTest, RemovingDevice) From 96d3eaaddb1356e968a9fd07fde1be2cb4bb3e4d Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 2 Apr 2026 14:04:56 +0200 Subject: [PATCH 23/29] MonitoredItem FB: disable node data type validation; new supported types --- .../opcua/opcuashared/src/opcuadatavalue.cpp | 1 + .../opcua_monitored_item_fb_impl.h | 4 +- .../src/opcua_monitored_item_fb_impl.cpp | 80 +++++++++++++++---- .../tests/test_opcua_monitored_item_fb.cpp | 6 +- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp index 6a64097..5d47e00 100644 --- a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp +++ b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp @@ -68,6 +68,7 @@ bool OpcUaDataValue::isInteger() const bool OpcUaDataValue::isString() const { return VariantUtils::HasScalarType(value.value) || + VariantUtils::HasScalarType(value.value) || VariantUtils::HasScalarType(value.value); } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index 08176fe..5005cf2 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -72,7 +72,9 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock }; static std::atomic localIndex; - static std::unordered_map supportedDataTypes; + static std::unordered_map supportedDataTypeNodeIds; + static std::unordered_map supportedDataTypeKinds; + static std::unordered_map dataTypeKindToDataTypeNodeId; DataDescriptorPtr outputSignalDescriptor; SignalConfigPtr outputSignal; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 1ec54b1..a370a64 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -4,11 +4,13 @@ #include "opendaq/packet_factory.h" #include +#define DISABLE_NODE_DATATYPE_VALIDATION + BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC std::atomic OpcUaMonitoredItemFbImpl::localIndex = 0; -std::unordered_map OpcUaMonitoredItemFbImpl::supportedDataTypes = { +std::unordered_map OpcUaMonitoredItemFbImpl::supportedDataTypeNodeIds = { {OpcUaNodeId(), daq::SampleType::Undefined}, {OpcUaNodeId(0, UA_NS0ID_FLOAT), daq::SampleType::Float32}, {OpcUaNodeId(0, UA_NS0ID_DOUBLE), daq::SampleType::Float64}, @@ -20,7 +22,42 @@ std::unordered_map OpcUaMonitoredItemFbImpl::suppo {OpcUaNodeId(0, UA_NS0ID_UINT32), daq::SampleType::UInt32}, {OpcUaNodeId(0, UA_NS0ID_INT64), daq::SampleType::Int64}, {OpcUaNodeId(0, UA_NS0ID_UINT64), daq::SampleType::UInt64}, - {OpcUaNodeId(0, UA_NS0ID_STRING), daq::SampleType::String}}; + {OpcUaNodeId(0, UA_NS0ID_STRING), daq::SampleType::String}, + {OpcUaNodeId(0, UA_NS0ID_LOCALIZEDTEXT), daq::SampleType::String}, + {OpcUaNodeId(0, UA_NS0ID_QUALIFIEDNAME), daq::SampleType::String}, + {OpcUaNodeId(0, UA_NS0ID_DATETIME), daq::SampleType::Int64}}; + +std::unordered_map OpcUaMonitoredItemFbImpl::supportedDataTypeKinds = { + {UA_DATATYPEKIND_FLOAT, daq::SampleType::Float32}, + {UA_DATATYPEKIND_DOUBLE, daq::SampleType::Float64}, + {UA_DATATYPEKIND_SBYTE, daq::SampleType::Int8}, + {UA_DATATYPEKIND_BYTE, daq::SampleType::UInt8}, + {UA_DATATYPEKIND_INT16, daq::SampleType::Int16}, + {UA_DATATYPEKIND_UINT16, daq::SampleType::UInt16}, + {UA_DATATYPEKIND_INT32, daq::SampleType::Int32}, + {UA_DATATYPEKIND_UINT32, daq::SampleType::UInt32}, + {UA_DATATYPEKIND_INT64, daq::SampleType::Int64}, + {UA_DATATYPEKIND_UINT64, daq::SampleType::UInt64}, + {UA_DATATYPEKIND_STRING, daq::SampleType::String}, + {UA_DATATYPEKIND_LOCALIZEDTEXT, daq::SampleType::String}, + {UA_DATATYPEKIND_QUALIFIEDNAME, daq::SampleType::String}, + {UA_DATATYPEKIND_DATETIME, daq::SampleType::Int64}}; + +std::unordered_map OpcUaMonitoredItemFbImpl::dataTypeKindToDataTypeNodeId = { + {UA_DATATYPEKIND_FLOAT, OpcUaNodeId(0, UA_NS0ID_FLOAT)}, + {UA_DATATYPEKIND_DOUBLE, OpcUaNodeId(0, UA_NS0ID_DOUBLE)}, + {UA_DATATYPEKIND_SBYTE, OpcUaNodeId(0, UA_NS0ID_SBYTE)}, + {UA_DATATYPEKIND_BYTE, OpcUaNodeId(0, UA_NS0ID_BYTE)}, + {UA_DATATYPEKIND_INT16, OpcUaNodeId(0, UA_NS0ID_INT16)}, + {UA_DATATYPEKIND_UINT16, OpcUaNodeId(0, UA_NS0ID_UINT16)}, + {UA_DATATYPEKIND_INT32, OpcUaNodeId(0, UA_NS0ID_INT32)}, + {UA_DATATYPEKIND_UINT32, OpcUaNodeId(0, UA_NS0ID_UINT32)}, + {UA_DATATYPEKIND_INT64, OpcUaNodeId(0, UA_NS0ID_INT64)}, + {UA_DATATYPEKIND_UINT64, OpcUaNodeId(0, UA_NS0ID_UINT64)}, + {UA_DATATYPEKIND_STRING, OpcUaNodeId(0, UA_NS0ID_STRING)}, + {UA_DATATYPEKIND_LOCALIZEDTEXT, OpcUaNodeId(0, UA_NS0ID_LOCALIZEDTEXT)}, + {UA_DATATYPEKIND_QUALIFIEDNAME, OpcUaNodeId(0, UA_NS0ID_QUALIFIEDNAME)}, + {UA_DATATYPEKIND_DATETIME, OpcUaNodeId(0, UA_NS0ID_DATETIME)}}; namespace { @@ -166,9 +203,9 @@ std::string OpcUaMonitoredItemFbImpl::generateLocalId() void OpcUaMonitoredItemFbImpl::adjustSignalDescriptor() { auto lockProcessing = std::scoped_lock(processingMutex); - if (nodeValidationErr.ok() && supportedDataTypes.count(nodeDataType) != 0) + if (nodeValidationErr.ok() && supportedDataTypeNodeIds.count(nodeDataType) != 0) { - outputSignalDescriptor = DataDescriptorBuilder().setSampleType(supportedDataTypes[nodeDataType]).build(); + outputSignalDescriptor = DataDescriptorBuilder().setSampleType(supportedDataTypeNodeIds[nodeDataType]).build(); } else { @@ -305,10 +342,12 @@ void OpcUaMonitoredItemFbImpl::validateNode() { nodeValidationErr.add(fmt::format("There is no read permission to node {}", nodeId.toString())); } +#ifndef DISABLE_NODE_DATATYPE_VALIDATION else if (nodeDataType = client->readDataType(nodeId); supportedDataTypes.count(nodeDataType) == 0) { nodeValidationErr.add(fmt::format("Node {} has unsupported DataType ({})", nodeId.toString(), nodeDataType.toString())); } +#endif } catch (OpcUaException& ex) { @@ -354,7 +393,11 @@ bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value) { auto lockProcessing = std::scoped_lock(processingMutex); - OpcUaNodeId valueDataType(value.getValue().value.type->typeId); + OpcUaNodeId valueDataType; + const UA_DataTypeKind typeKind = static_cast(value.getValue().value.type->typeKind); + if (dataTypeKindToDataTypeNodeId.count(typeKind)) + valueDataType = dataTypeKindToDataTypeNodeId.at(typeKind); + if (valueDataType != nodeDataType) { nodeDataType = std::move(valueDataType); @@ -362,11 +405,11 @@ bool OpcUaMonitoredItemFbImpl::validateValueDataType(const OpcUaDataValue& value outputSignal.setDescriptor(outputSignalDescriptor); } - bool valid = (value.isNumber() || value.isString()); + bool valid = (supportedDataTypeKinds.count(typeKind) != 0); if (valid) valueValidationErr.reset(); else - valueValidationErr.set("Value has unsupported type."); + valueValidationErr.set(fmt::format("Value has unsupported type ({}).", static_cast(typeKind))); return valid; } @@ -488,34 +531,37 @@ OpcUaMonitoredItemFbImpl::DataPackets OpcUaMonitoredItemFbImpl::buildDataPacket( switch (value.getValue().value.type->typeKind) { - case UA_TYPES_SBYTE: + case UA_DATATYPEKIND_SBYTE: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_BYTE: + case UA_DATATYPEKIND_BYTE: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_INT16: + case UA_DATATYPEKIND_INT16: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_UINT16: + case UA_DATATYPEKIND_UINT16: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_INT32: + case UA_DATATYPEKIND_INT32: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_UINT32: + case UA_DATATYPEKIND_UINT32: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_INT64: + case UA_DATATYPEKIND_INT64: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_UINT64: + case UA_DATATYPEKIND_UINT64: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_FLOAT: + case UA_DATATYPEKIND_DATETIME: + *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); + break; + case UA_DATATYPEKIND_FLOAT: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; - case UA_TYPES_DOUBLE: + case UA_DATATYPEKIND_DOUBLE: *(static_cast(dps.dataPacket.getRawData())) = value.readScalar(); break; default: diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 1c7a9ea..27e2eb3 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -774,8 +774,9 @@ TEST_F(GenericOpcuaMonitoredItemTest, SignalDescriptorSampleTypeMatchesOpcUaData for (const auto& [nodeId, expectedType] : cases) { - CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), 100); + CreateMonitoredItemFB(nodeId.getIdentifier(), nodeId.getNamespaceIndex(), 50); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + readValueWithTout(fb.getSignals()[0], 150); EXPECT_EQ(fb.getSignals()[0].getDescriptor().getSampleType(), expectedType); device.removeFunctionBlock(fb); fb = nullptr; @@ -787,8 +788,9 @@ TEST_F(GenericOpcuaMonitoredItemTest, UnsupportedDataTypeNode) StartUp(); // .b is a BOOLEAN node — not in supportedDataTypes - CreateMonitoredItemFB(".b", 1, 100, DS::ServerTimestamp); + CreateMonitoredItemFB(".b", 1, 10, DS::ServerTimestamp); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], 300); From 7e708b4f435b51ad14e59727d837e62ff4bcfd18 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 2 Apr 2026 16:10:21 +0200 Subject: [PATCH 24/29] MonitoredItem FB: status fix --- .../src/opcua_monitored_item_fb_impl.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index a370a64..26dbb19 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -367,22 +367,22 @@ bool OpcUaMonitoredItemFbImpl::validateResponse(const OpcUaDataValue& value) auto lockProcessing = std::scoped_lock(processingMutex); if (value.getValue().hasStatus && value.getValue().status != UA_STATUSCODE_GOOD) { - responseValidationErr.add(fmt::format("Reading value error: {}. ", value.getValue().hasStatus)); + responseValidationErr.set(fmt::format("Reading value error: {} - {}. ", value.getValue().status, UA_StatusCode_name(value.getValue().status))); return false; } if (!value.getValue().hasValue) { - responseValidationErr.add(std::string("Reading value error: response without a value.")); + responseValidationErr.set(std::string("Reading value error: response without a value.")); return false; } if (config.domainSource == DomainSource::ServerTimestamp && !value.getValue().hasServerTimestamp) { - responseValidationErr.add(std::string("Reading value error: there is no required server timestamp")); + responseValidationErr.set(std::string("Reading value error: there is no required server timestamp")); return false; } if (config.domainSource == DomainSource::SourceTimestamp && !value.getValue().hasSourceTimestamp) { - responseValidationErr.add(std::string("Reading value error: there is no required source timestamp")); + responseValidationErr.set(std::string("Reading value error: there is no required source timestamp")); return false; } From 38fe6119c1d1642c68ccdda74338444b4178dc1d Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 2 Apr 2026 17:33:04 +0200 Subject: [PATCH 25/29] OpcUaClient fix --- shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp index d6dd283..f81a755 100644 --- a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp +++ b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp @@ -62,7 +62,7 @@ OpcUaClient::OpcUaClient(const std::string& url) OpcUaClient::~OpcUaClient() { - disconnect(); + disconnect(true); } void OpcUaClient::initialize() From f0f762b87989a4ee7135dec26ea9396f83e35f5c Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 2 Apr 2026 17:35:11 +0200 Subject: [PATCH 26/29] generic opcua client: reconnection --- .../include/opcuageneric_client/constants.h | 3 +- .../generic_client_device_impl.h | 22 +++++- .../opcuageneric_client/status_adaptor.h | 72 +++++++++++++++++++ .../opcuageneric_client/src/CMakeLists.txt | 1 + .../src/generic_client_device_impl.cpp | 68 +++++++++++++++++- 5 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_adaptor.h diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h index 007769e..d4491da 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h @@ -45,7 +45,8 @@ static constexpr const char* DEFAULT_OPCUA_USERNAME = ""; static constexpr const char* DEFAULT_OPCUA_PASSWORD = ""; static constexpr const char* DEFAULT_OPCUA_PATH = ""; -static constexpr const uint32_t DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL = 100; // in milliseconds +static constexpr uint32_t DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL = 100; // in milliseconds +static constexpr uint32_t DEFAULT_RECONNECT_INTERVAL = 5000; // in milliseconds // ---------- END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h index 5a1fe29..46a4240 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/generic_client_device_impl.h @@ -16,9 +16,14 @@ #pragma once #include +#include +#include #include #include #include "opcuaclient/opcuaclient.h" +#include +#include +#include BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC @@ -30,7 +35,9 @@ class OpcuaGenericClientDeviceImpl : public Device const PropertyObjectPtr& config, std::shared_ptr client, const std::string& localId, - const std::string& name); + const std::string& name, + uint32_t reconnectIntervalMs = DEFAULT_RECONNECT_INTERVAL); + ~OpcuaGenericClientDeviceImpl(); static PropertyObjectPtr createDefaultConfig(); protected: static std::atomic localIndex; @@ -52,12 +59,23 @@ class OpcuaGenericClientDeviceImpl : public Device DictObjectPtr nestedFbTypes; - EnumerationPtr connectionStatus; + StatusAdaptor connectionStatus; std::atomic connectedDone{false}; std::unordered_map deviceMap; // device name -> signal list JSON daq::opcua::OpcUaClientPtr client; + + // Reconnect monitor + const uint32_t reconnectIntervalMs; + std::thread reconnectThread; + std::atomic reconnectRunning{false}; + std::condition_variable reconnectCv; + std::mutex reconnectMutex; + + void startReconnectMonitor(); + void stopReconnectMonitor(); + void reconnectMonitorLoop(); }; END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_adaptor.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_adaptor.h new file mode 100644 index 0000000..8194ddb --- /dev/null +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/status_adaptor.h @@ -0,0 +1,72 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include "opcuageneric_client/opcuageneric.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_GENERIC + +class StatusAdaptor +{ +public: + StatusAdaptor(const std::string typeName, + const std::string statusName, + ComponentStatusContainerPtr statusContainer, + std::string initState, + TypeManagerPtr typeManager) + : typeName(typeName), + statusName(statusName), + statusContainer(statusContainer), + typeManager(typeManager) + { + currentStatus = Enumeration(typeName, initState, typeManager); + currentMessage = ""; + statusContainer.template asPtr(true).addStatus(statusName, currentStatus); + } + + bool setStatus(const std::string& status, const std::string& message = "") + { + std::scoped_lock lock(statusMutex); + const auto newStatus = Enumeration(typeName, String(status), typeManager); + bool changed = (newStatus != currentStatus || message != currentMessage); + if (changed) + { + currentStatus = newStatus; + currentMessage = message; + statusContainer.template asPtr(true).setStatusWithMessage(statusName, currentStatus, message); + } + return changed; + } + std::string getStatus() + { + std::scoped_lock lock(statusMutex); + return currentStatus.getValue().toStdString(); + } + +private: + const std::string typeName; + const std::string statusName; + std::string currentMessage; + ComponentStatusContainerPtr statusContainer; + EnumerationPtr currentStatus; + TypeManagerPtr typeManager; + std::mutex statusMutex; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_GENERIC diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt index d750255..9b2c29b 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/CMakeLists.txt @@ -7,6 +7,7 @@ set(SRC_PublicHeaders constants.h generic_client_device_impl.h opcua_monitored_item_fb_impl.h status_container.h + status_adaptor.h ) set(SRC_Cpp generic_client_device_impl.cpp opcua_monitored_item_fb_impl.cpp diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp index 6c92487..c51ef2c 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp @@ -32,10 +32,12 @@ OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx const PropertyObjectPtr& config, std::shared_ptr client, const std::string& localId, - const std::string& name) + const std::string& name, + uint32_t reconnectIntervalMs) : Device(ctx, parent, localId.empty() ? generateLocalId() : localId) - , connectionStatus(Enumeration("ConnectionStatusType", "Connected", this->context.getTypeManager())) + , connectionStatus("ConnectionStatusType", "ConnectionStatus", statusContainer, "Connected", context.getTypeManager()) , client(client) + , reconnectIntervalMs(reconnectIntervalMs) { if (this->client == nullptr) DAQ_THROW_EXCEPTION(UninitializedException, "OpcUaClient is not initialized"); @@ -47,6 +49,8 @@ OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx else initProperties(createDefaultConfig()); + initComponentStatus(); + try { this->client->connect(); @@ -64,8 +68,13 @@ OpcuaGenericClientDeviceImpl::OpcuaGenericClientDeviceImpl(const ContextPtr& ctx } } - initComponentStatus(); initNestedFbTypes(); + startReconnectMonitor(); +} + +OpcuaGenericClientDeviceImpl::~OpcuaGenericClientDeviceImpl() +{ + stopReconnectMonitor(); } PropertyObjectPtr OpcuaGenericClientDeviceImpl::createDefaultConfig() @@ -111,9 +120,62 @@ std::string OpcuaGenericClientDeviceImpl::getConnectionString() const void OpcuaGenericClientDeviceImpl::removed() { + stopReconnectMonitor(); + client->disconnect(true); Device::removed(); } +void OpcuaGenericClientDeviceImpl::startReconnectMonitor() +{ + reconnectRunning = true; + reconnectThread = std::thread([this] { reconnectMonitorLoop(); }); +} + +void OpcuaGenericClientDeviceImpl::stopReconnectMonitor() +{ + { + std::lock_guard lock(reconnectMutex); + reconnectRunning = false; + } + reconnectCv.notify_all(); + if (reconnectThread.joinable()) + reconnectThread.join(); +} + +void OpcuaGenericClientDeviceImpl::reconnectMonitorLoop() +{ + auto interruptibleSleep = [&]() + { + std::unique_lock lock(reconnectMutex); + reconnectCv.wait_for(lock, std::chrono::milliseconds(reconnectIntervalMs), [this]() { return !reconnectRunning.load(); }); + }; + + while (reconnectRunning) + { + if (client->isConnected() == false) + { + connectionStatus.setStatus("Reconnecting"); + try + { + client->disconnect(); + client->connect(); + client->runIterate(); + connectionStatus.setStatus("Connected"); + } + catch (const OpcUaException& e) + { + if (e.getStatusCode() == UA_STATUSCODE_BADUSERACCESSDENIED || + e.getStatusCode() == UA_STATUSCODE_BADIDENTITYTOKENINVALID) + { + connectionStatus.setStatus("Unrecoverable"); + reconnectRunning = false; + } + } + } + interruptibleSleep(); + } +} + DeviceInfoPtr OpcuaGenericClientDeviceImpl::onGetInfo() { return DeviceInfo(getConnectionString(), GENERIC_OPCUA_CLIENT_DEVICE_NAME); From 7095b5a728b8e93ab8a0cf5aaa75070a04eaa55e Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 2 Apr 2026 18:35:46 +0200 Subject: [PATCH 27/29] generic opcua client: reconnection tests --- .../test_opcua_generic_client_device.cpp | 79 ++++++++++++++++++- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp index 8fe03ee..fc37eac 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_generic_client_device.cpp @@ -6,13 +6,45 @@ #include #include #include "opcuageneric_client/constants.h" +#include "opcuageneric_client/generic_client_device_impl.h" #include "opcuaservertesthelper.h" #include "test_daq_test_helper.h" +#include +#include namespace daq::opcua::generic { class GenericOpcuaClientDeviceTest : public testing::Test, public DaqTestHelper { +public: + bool waitForConnectionStatus(const std::string& expected, int timeoutMs = 10000) + { + auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (std::chrono::steady_clock::now() < deadline) + { + try + { + auto status = device.getStatusContainer().getStatus("ConnectionStatus"); + if (status == daq::Enumeration("ConnectionStatusType", expected, daqInstance.getContext().getTypeManager())) + return true; + } + catch (...) + { + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + return false; + } + + daq::DevicePtr createDeviceWithShortInterval(const std::string& serverUrl) + { + DaqInstanceInit(); + auto client = std::make_shared(serverUrl); + device = daq::createWithImplementation( + daqInstance.getContext(), daqInstance, nullptr, client, "", "", 100); + return device; + } + protected: void SetUp() override { @@ -69,7 +101,7 @@ TEST_F(GenericOpcuaClientDeviceTest, DefaultDeviceConfig) TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) { - const auto instance = Instance(); + const auto instance = DaqInstanceInit(); const std::string deviceName("open62541-based OPC UA Application"); daq::GenericDevicePtr device; ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842")); @@ -106,7 +138,7 @@ TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithDefaultConfig) TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithLocalId) { const auto module = CreateModule(); - const auto instance = Instance(); + const auto instance = DaqInstanceInit(); const std::string deviceLocalId("myCustomLocalId"); daq::GenericDevicePtr device; auto defaultConfig = module.getAvailableDeviceTypes().get("OPCUAGeneric").createDefaultConfig(); @@ -121,7 +153,7 @@ TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithLocalId) TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithoutLocalId) { const auto module = CreateModule(); - const auto instance = Instance(); + const auto instance = DaqInstanceInit(); const std::string deviceLocalId("urn:open62541.server.application"); daq::GenericDevicePtr device; auto defaultConfig = module.getAvailableDeviceTypes().get("OPCUAGeneric").createDefaultConfig(); @@ -135,7 +167,7 @@ TEST_F(GenericOpcuaClientDeviceTest, CreatingDeviceWithoutLocalId) TEST_F(GenericOpcuaClientDeviceTest, RemovingDevice) { - const auto instance = Instance(); + const auto instance = DaqInstanceInit(); daq::GenericDevicePtr device; { ASSERT_NO_THROW(device = instance.addDevice("daq.opcua.generic://127.0.0.1:4842")); @@ -167,3 +199,42 @@ TEST_F(GenericOpcuaClientDeviceTest, CheckDeviceNameLocalId) EXPECT_EQ(device.getLocalId().toStdString(), std::string("urn:open62541.server.application")); EXPECT_EQ(device.getName().toStdString(), std::string("open62541-based OPC UA Application")); } + +TEST_F(GenericOpcuaClientDeviceTest, ReconnectMonitor_StatusBecomesReconnectingAfterServerStop) +{ + DaqInstanceInit(); + createDeviceWithShortInterval(testHelper.getServerUrl()); + ASSERT_TRUE(waitForConnectionStatus("Connected")); + + testHelper.stop(); + + ASSERT_TRUE(waitForConnectionStatus("Reconnecting")); +} + +TEST_F(GenericOpcuaClientDeviceTest, ReconnectMonitor_ReconnectsAfterServerRestart) +{ + DaqInstanceInit(); + createDeviceWithShortInterval(testHelper.getServerUrl()); + ASSERT_TRUE(waitForConnectionStatus("Connected")); + + testHelper.stop(); + ASSERT_TRUE(waitForConnectionStatus("Reconnecting")); + + testHelper.startServer(); + ASSERT_TRUE(waitForConnectionStatus("Connected")); +} + +TEST_F(GenericOpcuaClientDeviceTest, ReconnectMonitor_StopsCleanlyOnDeviceRemoval) +{ + DaqInstanceInit(); + device = daqInstance.addDevice("daq.opcua.generic://127.0.0.1:4842"); + ASSERT_EQ(device.getStatusContainer().getStatus("ComponentStatus"), + daq::Enumeration("ComponentStatusType", "Ok", daqInstance.getContext().getTypeManager())); + + const auto t0 = std::chrono::steady_clock::now(); + ASSERT_NO_THROW(daqInstance.removeDevice(device)); + const auto elapsedMs = std::chrono::duration_cast(std::chrono::steady_clock::now() - t0).count(); + + // stopReconnectMonitor() uses cv.notify_all() so join should be near-instant + ASSERT_LT(elapsedMs, 1000); +} \ No newline at end of file From 739bacf8c91ca1ee07770b4ced16c93cfd44eeac Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 3 Apr 2026 10:53:06 +0300 Subject: [PATCH 28/29] generic opcua client: nullptr fix --- .../opcuageneric_client/src/generic_client_device_impl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp index c51ef2c..d127cdd 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/generic_client_device_impl.cpp @@ -121,8 +121,8 @@ std::string OpcuaGenericClientDeviceImpl::getConnectionString() const void OpcuaGenericClientDeviceImpl::removed() { stopReconnectMonitor(); - client->disconnect(true); Device::removed(); + client->disconnect(false); } void OpcuaGenericClientDeviceImpl::startReconnectMonitor() @@ -157,7 +157,7 @@ void OpcuaGenericClientDeviceImpl::reconnectMonitorLoop() connectionStatus.setStatus("Reconnecting"); try { - client->disconnect(); + client->disconnect(false); client->connect(); client->runIterate(); connectionStatus.setStatus("Connected"); From a930c0af4aab3c09868ae4079f33121ff3c1da33 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Fri, 3 Apr 2026 11:17:05 +0200 Subject: [PATCH 29/29] MonitoredItem FB: numeric node ID --- .../include/opcuageneric_client/constants.h | 3 +- .../opcua_monitored_item_fb_impl.h | 10 +- .../src/opcua_monitored_item_fb_impl.cpp | 66 +++++---- .../tests/opcuaservertesthelper.cpp | 81 +++++++---- .../tests/opcuaservertesthelper.h | 28 +++- .../tests/test_opcua_monitored_item_fb.cpp | 132 ++++++++++++++++-- 6 files changed, 242 insertions(+), 78 deletions(-) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h index d4491da..8a6bfd2 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/constants.h @@ -30,7 +30,8 @@ static constexpr const char* PROPERTY_NAME_OPCUA_MI_LOCAL_ID = "LocalId"; // MonitoredItem FB static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID_TYPE = "NodeIDType"; -static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID = "NodeID"; +static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID_STRING = "NodeIDString"; +static constexpr const char* PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC = "NodeIDNumeric"; static constexpr const char* PROPERTY_NAME_OPCUA_NAMESPACE_INDEX = "NamespaceIndex"; static constexpr const char* PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL = "SamplingInterval"; static constexpr const char* PROPERTY_NAME_OPCUA_TS_MODE = "TimestampMode"; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h index 5005cf2..83ef31e 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/include/opcuageneric_client/opcua_monitored_item_fb_impl.h @@ -28,11 +28,10 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock friend class GenericOpcuaMonitoredItemTest; public: - // only string NodeIDs are supported at the moment enum class NodeIDType : int { - // Numeric = 0, - String = 0, + Numeric = 0, + String = 1, // Guid, // Opaque, _count @@ -64,9 +63,7 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock struct FbConfig { - NodeIDType nodeIdType; - std::string nodeId; - uint32_t namespaceIndex; + OpcUaNodeId nodeId; uint32_t samplingInterval; DomainSource domainSource; }; @@ -82,7 +79,6 @@ class OpcUaMonitoredItemFbImpl final : public FunctionBlock FbConfig config; daq::opcua::OpcUaClientPtr client; - OpcUaNodeId nodeId; OpcUaNodeId nodeDataType; std::thread readerThread; diff --git a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp index 26dbb19..97bb875 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/src/opcua_monitored_item_fb_impl.cpp @@ -96,7 +96,6 @@ OpcUaMonitoredItemFbImpl::OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, const PropertyObjectPtr& config) : FunctionBlock(type, ctx, parent, generateLocalId()) , client(client) - , nodeId() , running(false) , statuses(std::make_shared()) { @@ -107,8 +106,6 @@ OpcUaMonitoredItemFbImpl::OpcUaMonitoredItemFbImpl(const ContextPtr& ctx, else initProperties(type.createDefaultConfig()); - nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; - validateNode(); adjustSignalDescriptor(); createSignal(); @@ -149,18 +146,26 @@ FunctionBlockTypePtr OpcUaMonitoredItemFbImpl::CreateType() auto defaultConfig = PropertyObject(); { auto builder = - SelectionPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, List("String"), static_cast(NodeIDType::String)) - .setDescription("Defines the type of the NodeID of the OPCUA node to monitor. By default it is set to String. Other " - "formats are not supported at the moment."); + SelectionPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, List("Numeric", "String"), static_cast(NodeIDType::String)) + .setDescription("Defines the type of the NodeID of the OPCUA node to monitor. By default it is set to String."); defaultConfig.addProperty(builder.build()); } { auto builder = - StringPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID, String("")) - .setDescription(fmt::format("Specifies the NodeID of the OPCUA node to monitor. The format of the NodeID should correspond " - "to the type specified in the \"{}\" property.", - PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); + StringPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID_STRING, String("")) + .setDescription(fmt::format("Specifies the NodeID of the OPCUA node to monitor. Used when \"{}\" is set to String.", + PROPERTY_NAME_OPCUA_NODE_ID_TYPE)) + .setVisible(EvalValue("$NodeIDType == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = + IntPropertyBuilder(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC, Integer(0)) + .setDescription(fmt::format("Specifies the numeric NodeID of the OPCUA node to monitor. Used when \"{}\" is set to Numeric.", + PROPERTY_NAME_OPCUA_NODE_ID_TYPE)) + .setVisible(EvalValue("$NodeIDType == 0")); defaultConfig.addProperty(builder.build()); } @@ -243,14 +248,24 @@ void OpcUaMonitoredItemFbImpl::readProperties() configErr.reset(); - config.nodeIdType = NodeIDType::String; // only string NodeIDs are supported at the moment - config.nodeId = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID, ""); - if (config.nodeId.empty()) + const auto tmpNodeIdType = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID_TYPE, 0); + const auto nodeIdType = (tmpNodeIdType == static_cast(NodeIDType::Numeric)) ? NodeIDType::Numeric : NodeIDType::String; + const auto namespaceIndex = readProperty(objPtr, PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 0); + + if (nodeIdType == NodeIDType::String) { - configErr.add(fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID)); + const auto nodeIdString = readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID_STRING, ""); + if (nodeIdString.empty()) + configErr.add(fmt::format("\"{}\" property is empty!", PROPERTY_NAME_OPCUA_NODE_ID_STRING)); + else + config.nodeId = OpcUaNodeId{static_cast(namespaceIndex), nodeIdString}; + } + else + { + const auto nodeIdNumeric = static_cast(readProperty(objPtr, PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC, 0)); + config.nodeId = OpcUaNodeId{static_cast(namespaceIndex), nodeIdNumeric}; } - config.namespaceIndex = readProperty(objPtr, PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 0); config.samplingInterval = readProperty(objPtr, PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, DEFAULT_OPCUA_MIFB_SAMPLING_INTERVAL); if (config.samplingInterval <= 0) @@ -283,9 +298,6 @@ void OpcUaMonitoredItemFbImpl::propertyChanged() auto prevConfig = config; readProperties(); - - nodeId = OpcUaNodeId{static_cast(this->config.namespaceIndex), this->config.nodeId}; - validateNode(); adjustSignalDescriptor(); reconfigureSignal(prevConfig); @@ -329,18 +341,18 @@ void OpcUaMonitoredItemFbImpl::validateNode() try { nodeValidationErr.reset(); - auto nodeExist = client->nodeExists(nodeId); + auto nodeExist = client->nodeExists(config.nodeId); if (!nodeExist) { - nodeValidationErr.add(fmt::format("Node {} does not exist", nodeId.toString())); + nodeValidationErr.add(fmt::format("Node {} does not exist", config.nodeId.toString())); } - else if (const auto nodeClass = client->readNodeClass(nodeId); nodeClass != UA_NodeClass::UA_NODECLASS_VARIABLE) + else if (const auto nodeClass = client->readNodeClass(config.nodeId); nodeClass != UA_NodeClass::UA_NODECLASS_VARIABLE) { - nodeValidationErr.add(fmt::format("Node {} is not a variable node", nodeId.toString())); + nodeValidationErr.add(fmt::format("Node {} is not a variable node", config.nodeId.toString())); } - else if (const auto accessLevel = client->readAccessLevel(nodeId); (accessLevel & UA_ACCESSLEVELMASK_READ) == 0) + else if (const auto accessLevel = client->readAccessLevel(config.nodeId); (accessLevel & UA_ACCESSLEVELMASK_READ) == 0) { - nodeValidationErr.add(fmt::format("There is no read permission to node {}", nodeId.toString())); + nodeValidationErr.add(fmt::format("There is no read permission to node {}", config.nodeId.toString())); } #ifndef DISABLE_NODE_DATATYPE_VALIDATION else if (nodeDataType = client->readDataType(nodeId); supportedDataTypes.count(nodeDataType) == 0) @@ -353,11 +365,11 @@ void OpcUaMonitoredItemFbImpl::validateNode() { if (ex.getStatusCode() == UA_STATUSCODE_BADUSERACCESSDENIED) { - nodeValidationErr.add(fmt::format("Access denied for node {}", nodeId.toString())); + nodeValidationErr.add(fmt::format("Access denied for node {}", config.nodeId.toString())); } else { - nodeValidationErr.add(fmt::format("Exception was thrown while node {} validatiion", nodeId.toString())); + nodeValidationErr.add(fmt::format("Exception was thrown while node {} validatiion", config.nodeId.toString())); } } } @@ -484,7 +496,7 @@ void OpcUaMonitoredItemFbImpl::readerLoop() OpcUaDataValue dataValue; try { - dataValue = client->readDataValue(nodeId); + dataValue = client->readDataValue(config.nodeId); exceptionErr.reset(); if (validateResponse(dataValue) && validateValueDataType(dataValue)) diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp index e73c026..f42fe76 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.cpp @@ -1,7 +1,7 @@ #include "opcuaservertesthelper.h" -#include -#include #include +#include +#include #include BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -137,6 +137,15 @@ void OpcUaServerTestHelper::createModel() publishVariable(".pui64", &myUInt64, &UA_TYPES[UA_TYPES_UINT64], &uaObjectsFolder, "en_US", 1, 1, 0); } + { + UA_Int32 val = -32; + publishVariable(1001, &val, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder); + } + { + UA_Int64 val = -64; + publishVariable(1002, &val, &UA_TYPES[UA_TYPES_INT64], &uaObjectsFolder); + } + UA_Boolean myBool = true; publishVariable(".b", &myBool, &UA_TYPES[UA_TYPES_BOOLEAN], &uaObjectsFolder); @@ -206,16 +215,41 @@ void OpcUaServerTestHelper::publishVariable(std::string identifier, uint16_t nodeIndex, size_t dimension, UA_Byte accessLevel) +{ + publishVariableImpl( + OpcUaNodeId(nodeIndex, identifier), identifier, value, type, parentNodeId, locale, nodeIndex, dimension, accessLevel); +} + +void OpcUaServerTestHelper::publishVariable(uint32_t numericId, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale, + uint16_t nodeIndex, + size_t dimension, + UA_Byte accessLevel) +{ + publishVariableImpl( + OpcUaNodeId(nodeIndex, numericId), std::to_string(numericId), value, type, parentNodeId, locale, nodeIndex, dimension, accessLevel); +} + +void OpcUaServerTestHelper::publishVariableImpl(OpcUaNodeId nodeId, + const std::string& name, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale, + uint16_t nodeIndex, + size_t dimension, + UA_Byte accessLevel) { OpcUaObject attr = UA_VariableAttributes_default; - attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); - attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, name.c_str()); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, name.c_str()); attr->dataType = type->typeId; attr->accessLevel = accessLevel; - OpcUaNodeId newNodeId(nodeIndex, identifier); - - OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), identifier.c_str()); + OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), name.c_str()); if (dimension > 1) { @@ -230,14 +264,14 @@ void OpcUaServerTestHelper::publishVariable(std::string identifier, UA_Variant_setScalarCopy(&attr->value, value, type); } auto status = UA_Server_addVariableNode(server, - *newNodeId, - *parentNodeId, - UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), - *qualifiedName, - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), - *attr, - NULL, - NULL); + *nodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + *qualifiedName, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), + *attr, + NULL, + NULL); CheckStatusCodeException(status); } @@ -252,7 +286,6 @@ void OpcUaServerTestHelper::writeDataValueNode(const OpcUaNodeId& nodeId, const CheckStatusCodeException(UA_Server_writeDataValue(server, *nodeId, *value)); } - void OpcUaServerTestHelper::publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale, int nodeIndex) { OpcUaObject attr = UA_ObjectAttributes_default; @@ -264,14 +297,14 @@ void OpcUaServerTestHelper::publishFolder(const char* identifier, UA_NodeId* par OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), identifier); auto status = UA_Server_addObjectNode(server, - *newNodeId, - *parentNodeId, - UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), - *qualifiedName, - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), - *attr, - NULL, - NULL); + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + *qualifiedName, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), + *attr, + NULL, + NULL); CheckStatusCodeException(status); } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h index b458853..804a549 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/opcuaservertesthelper.h @@ -17,13 +17,13 @@ #pragma once #include +#include +#include #include +#include #include "opcuaclient/opcuaclient.h" #include "opcuashared/opcua.h" #include "opcuashared/opcuacommon.h" -#include -#include -#include BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -54,6 +54,15 @@ class OpcUaServerTestHelper final size_t dimension = 1, UA_Byte accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE); + void publishVariable(uint32_t numericId, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale = "en_US", + uint16_t nodeIndex = 1, + size_t dimension = 1, + UA_Byte accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE); + void writeValueNode(const OpcUaNodeId& nodeId, const OpcUaVariant& value); void writeDataValueNode(const OpcUaNodeId& nodeId, const OpcUaDataValue& value); @@ -63,6 +72,16 @@ class OpcUaServerTestHelper final void publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale = "en_US", int nodeIndex = 1); void publishMethod(std::string identifier, UA_NodeId* parentNodeId, const char* locale = "en_US", int nodeIndex = 1); + void publishVariableImpl(OpcUaNodeId nodeId, + const std::string& name, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale, + uint16_t nodeIndex, + size_t dimension, + UA_Byte accessLevel); + static UA_StatusCode helloMethodCallback(UA_Server* server, const UA_NodeId* sessionId, void* sessionHandle, @@ -96,8 +115,7 @@ class BaseClientTest : public testing::Test static void IterateAndWaitForPromise(OpcUaClient& client, const std::future& future) { using namespace std::chrono; - while (client.iterate(milliseconds(10)) == UA_STATUSCODE_GOOD && - future.wait_for(milliseconds(1)) != std::future_status::ready) + while (client.iterate(milliseconds(10)) == UA_STATUSCODE_GOOD && future.wait_for(milliseconds(1)) != std::future_status::ready) { }; } diff --git a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp index 27e2eb3..1b69115 100644 --- a/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp +++ b/shared/libraries/opcuageneric/opcuageneric_client/tests/test_opcua_monitored_item_fb.cpp @@ -25,8 +25,10 @@ namespace daq::opcua::generic void CreateMonitoredItemFB(std::string nodeId, uint32_t index, uint32_t interval = 100, DS ds = DS::ServerTimestamp) { + using NT = OpcUaMonitoredItemFbImpl::NodeIDType; auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); - config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, nodeId); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, static_cast(NT::String)); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, nodeId); config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, index); config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, interval); config.setPropertyValue(PROPERTY_NAME_OPCUA_TS_MODE, static_cast(ds)); @@ -34,6 +36,19 @@ namespace daq::opcua::generic ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); } + void CreateMonitoredItemFB(uint32_t numericNodeId, uint32_t nsIndex, uint32_t interval = 100, DS ds = DS::ServerTimestamp) + { + using NT = OpcUaMonitoredItemFbImpl::NodeIDType; + auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, static_cast(NT::Numeric)); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC, numericNodeId); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, nsIndex); + config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, interval); + config.setPropertyValue(PROPERTY_NAME_OPCUA_TS_MODE, static_cast(ds)); + + ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); + } + void CreateMonitoredItemFB(daq::PropertyObjectPtr config) { ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); @@ -157,7 +172,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, DefaultConfig) ASSERT_TRUE(defaultConfig.assigned()); - EXPECT_EQ(defaultConfig.getAllProperties().getCount(), 5u); + EXPECT_EQ(defaultConfig.getAllProperties().getCount(), 6u); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE)); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE).getValueType(), CoreType::ctInt); @@ -165,10 +180,15 @@ TEST_F(GenericOpcuaMonitoredItemTest, DefaultConfig) static_cast(OpcUaMonitoredItemFbImpl::NodeIDType::String)); EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_TYPE).getVisible()); - ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID)); - ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID).getValueType(), CoreType::ctString); - EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID).asPtr().getLength(), 0u); - EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID).getVisible()); + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING).getValueType(), CoreType::ctString); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING).asPtr().getLength(), 0u); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING).getVisible()); + + ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC)); + ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC).getValueType(), CoreType::ctInt); + EXPECT_EQ(defaultConfig.getPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC).asPtr(), 0); + EXPECT_FALSE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC).getVisible()); ASSERT_TRUE(defaultConfig.hasProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX)); ASSERT_EQ(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX).getValueType(), CoreType::ctInt); @@ -203,7 +223,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, CreationWithPartialConfig) StartUp(); { auto config = PropertyObject(); - config.addProperty(StringProperty(PROPERTY_NAME_OPCUA_NODE_ID, String("unknownNodeId"))); + config.addProperty(StringProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING, String("unknownNodeId"))); ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); @@ -212,7 +232,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, CreationWithPartialConfig) { auto config = PropertyObject(); - config.addProperty(StringProperty(PROPERTY_NAME_OPCUA_NODE_ID, String(".i32"))); + config.addProperty(StringProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING, String(".i32"))); config.addProperty(IntProperty(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1)); ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); EXPECT_EQ(fb.getSignals(daq::search::Any()).getCount(), 2u); @@ -226,7 +246,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, CreationWithCustomConfig) { StartUp(); auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); - config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, ".i32"); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, ".i32"); config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 100); ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); @@ -241,7 +261,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, TwoFbCreation) daq::FunctionBlockPtr fb; auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); - config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, ".i32"); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, ".i32"); config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 100); ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); @@ -252,7 +272,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, TwoFbCreation) daq::FunctionBlockPtr fb; auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); - config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, ".i64"); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, ".i64"); config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 100); ASSERT_NO_THROW(fb = device.addFunctionBlock(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME, config)); @@ -722,7 +742,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNodeIdFromInvalidToValid) ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); - fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, std::string(".i32")); + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, std::string(".i32")); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); @@ -738,7 +758,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNodeIdFromValidToInvalid) ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); - fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, std::string("nonExistent")); + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, std::string("nonExistent")); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); } @@ -812,7 +832,7 @@ TEST_F(GenericOpcuaMonitoredItemTest, ZeroSamplingInterval) StartUp(); auto config = device.getAvailableFunctionBlockTypes().get(GENERIC_OPCUA_MONITORED_ITEM_FB_NAME).createDefaultConfig(); - config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID, std::string(".i32")); + config.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, std::string(".i32")); config.setPropertyValue(PROPERTY_NAME_OPCUA_NAMESPACE_INDEX, 1); config.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 0); @@ -827,4 +847,88 @@ TEST_F(GenericOpcuaMonitoredItemTest, ZeroSamplingInterval) fb.setPropertyValue(PROPERTY_NAME_OPCUA_SAMPLING_INTERVAL, 0); ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, PropertyVisibilityTogglesWithNodeIdType) +{ + using NT = OpcUaMonitoredItemFbImpl::NodeIDType; + daq::PropertyObjectPtr defaultConfig = OpcUaMonitoredItemFbImpl::CreateType().createDefaultConfig(); + + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING).getVisible()); + EXPECT_FALSE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC).getVisible()); + + defaultConfig.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, static_cast(NT::Numeric)); + EXPECT_FALSE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING).getVisible()); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC).getVisible()); + + defaultConfig.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, static_cast(NT::String)); + EXPECT_TRUE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_STRING).getVisible()); + EXPECT_FALSE(defaultConfig.getProperty(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC).getVisible()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, NumericNodeIdCreation) +{ + StartUp(); + + CreateMonitoredItemFB(1001, 1); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, NumericNodeIdCreationInvalidNode) +{ + StartUp(); + + CreateMonitoredItemFB(9999, 1); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), errStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, NumericNodeIdReadValue) +{ + constexpr uint32_t interval = 50; + const OpcUaNodeId nodeId(1, uint32_t{1001}); + StartUp(); + + CreateMonitoredItemFB(1001, 1, interval); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + daq::BaseObjectPtr prevVal = readValueWithTout(fb.getSignals()[0], interval * 3); + ASSERT_TRUE(prevVal.assigned()); + + const OpcUaVariant variant(int32_t{42}); + ASSERT_NO_THROW(testHelper.writeValueNode(nodeId, variant)); + + daq::BaseObjectPtr val = readValueWithTout(fb.getSignals()[0], interval * 3, prevVal); + ASSERT_NE(val, prevVal); + EXPECT_EQ(val.asPtr().getValue(0), 42); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNodeIdTypeStringToNumeric) +{ + using NT = OpcUaMonitoredItemFbImpl::NodeIDType; + StartUp(); + + CreateMonitoredItemFB(".i32", 1, 100, DS::ServerTimestamp); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, static_cast(NT::Numeric)); + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_NUMERIC, 1001); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); +} + +TEST_F(GenericOpcuaMonitoredItemTest, ReconfigureNodeIdTypeNumericToString) +{ + using NT = OpcUaMonitoredItemFbImpl::NodeIDType; + StartUp(); + + CreateMonitoredItemFB(1001, 1, 100, DS::ServerTimestamp); + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); + + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_TYPE, static_cast(NT::String)); + fb.setPropertyValue(PROPERTY_NAME_OPCUA_NODE_ID_STRING, std::string(".i32")); + + ASSERT_EQ(fb.getStatusContainer().getStatus("ComponentStatus"), okStatus()); } \ No newline at end of file