From 60cd131145d671aef594f54597c48fece401d507 Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Thu, 23 Apr 2026 12:18:20 +0200 Subject: [PATCH 01/15] simple opcua server: template for a simple server --- CMakeLists.txt | 3 +- modules/CMakeLists.txt | 4 + .../opcua_simple_server_module/CMakeLists.txt | 9 + .../opcua_simple_server_module/common.h | 21 ++ .../opcua_simple_server_module/constants.h | 31 +++ .../opcua_simple_server_module/module_dll.h | 20 ++ .../opcua_simple_server_impl.h | 54 +++++ .../opcua_simple_server_module_impl.h | 35 +++ .../src/CMakeLists.txt | 49 ++++ .../src/module_dll.cpp | 9 + .../src/opcua_simple_server_impl.cpp | 112 +++++++++ .../src/opcua_simple_server_module_impl.cpp | 44 ++++ .../tests/CMakeLists.txt | 24 ++ .../tests/test_app.cpp | 18 ++ .../tests/test_opcua_simple_server_module.cpp | 219 ++++++++++++++++++ 15 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 modules/opcua_simple_server_module/CMakeLists.txt create mode 100644 modules/opcua_simple_server_module/include/opcua_simple_server_module/common.h create mode 100644 modules/opcua_simple_server_module/include/opcua_simple_server_module/constants.h create mode 100644 modules/opcua_simple_server_module/include/opcua_simple_server_module/module_dll.h create mode 100644 modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h create mode 100644 modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h create mode 100644 modules/opcua_simple_server_module/src/CMakeLists.txt create mode 100644 modules/opcua_simple_server_module/src/module_dll.cpp create mode 100644 modules/opcua_simple_server_module/src/opcua_simple_server_impl.cpp create mode 100644 modules/opcua_simple_server_module/src/opcua_simple_server_module_impl.cpp create mode 100644 modules/opcua_simple_server_module/tests/CMakeLists.txt create mode 100644 modules/opcua_simple_server_module/tests/test_app.cpp create mode 100644 modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5df94b39..b2e019e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,8 +31,9 @@ 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_GENERIC_CLIENT "Enable ${REPO_NAME} generic client module" ${PROJECT_IS_TOP_LEVEL}) option(${REPO_OPTION_PREFIX}_ENABLE_SERVER "Enable ${REPO_NAME} server module" ${PROJECT_IS_TOP_LEVEL}) +option(${REPO_OPTION_PREFIX}_ENABLE_SIMPLE_SERVER "Enable ${REPO_NAME} simple 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 094e5272..2bb8d53b 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -16,3 +16,7 @@ endif() if (${REPO_OPTION_PREFIX}_ENABLE_SERVER) add_subdirectory(opcua_server_module) endif() + +if (${REPO_OPTION_PREFIX}_ENABLE_SIMPLE_SERVER) + add_subdirectory(opcua_simple_server_module) +endif() diff --git a/modules/opcua_simple_server_module/CMakeLists.txt b/modules/opcua_simple_server_module/CMakeLists.txt new file mode 100644 index 00000000..349c7874 --- /dev/null +++ b/modules/opcua_simple_server_module/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) +project(SimpleServerModule 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_simple_server_module/include/opcua_simple_server_module/common.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/common.h new file mode 100644 index 00000000..d62718bf --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_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_SIMPLE_SERVER_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(opcua_simple_server_module) +#define END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/modules/opcua_simple_server_module/include/opcua_simple_server_module/constants.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/constants.h new file mode 100644 index 00000000..474f091c --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_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_SIMPLE_SERVER_MODULE + +static const char* DAQ_OPCUA_SIMPLE_SERVER_ID = "OpenDAQSimpleOPCUA"; +static const char* DAQ_OPCUA_SIMPLE_SERVER_MODULE_NAME = "OpenDAQOPCUASimpleServerModule"; +static const char* DAQ_OPCUA_SIMPLE_SERVER_MODULE_ID = "OpenDAQOPCUASimpleServerModule"; + +static const uint16_t DAQ_OPCUA_SIMPLE_SERVER_DEFAULT_PORT = 4840; +static const char* DAQ_OPCUA_SIMPLE_SERVER_DEFAULT_PATH = "/"; + + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE diff --git a/modules/opcua_simple_server_module/include/opcua_simple_server_module/module_dll.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/module_dll.h new file mode 100644 index 00000000..df1a944f --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_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(OpcUaSimpleServerModule) diff --git a/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h new file mode 100644 index 00000000..c2eca402 --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h @@ -0,0 +1,54 @@ +/* + * 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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE + +class OpcUaSimpleServerImpl : public Server +{ +public: + explicit OpcUaSimpleServerImpl(const DevicePtr& rootDevice, + const PropertyObjectPtr& config, + const ContextPtr& context); + ~OpcUaSimpleServerImpl(); + static PropertyObjectPtr createDefaultConfig(const ContextPtr& context); + static ServerTypePtr createType(const ContextPtr& context); + static PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& config, const ContextPtr& context); + +protected: + PropertyObjectPtr getDiscoveryConfig() override; + void onStopServer() override; + static void populateDefaultConfigFromProvider(const ContextPtr& context, const PropertyObjectPtr& config); + + daq::opcua::TmsServer server; + ContextPtr context; +}; + +OPENDAQ_DECLARE_CLASS_FACTORY_WITH_INTERFACE( + INTERNAL_FACTORY, OpcUaSimpleServer, daq::IServer, + DevicePtr, rootDevice, + PropertyObjectPtr, config, + const ContextPtr&, context +) + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE diff --git a/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h new file mode 100644 index 00000000..41265112 --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h @@ -0,0 +1,35 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE + +class OpcUaSimpleServerModule final : public Module +{ +public: + OpcUaSimpleServerModule(ContextPtr context); + + DictPtr onGetAvailableServerTypes() override; + ServerPtr onCreateServer(const StringPtr& serverType, const PropertyObjectPtr& serverConfig, const DevicePtr& rootDevice) override; + +private: + std::mutex sync; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE diff --git a/modules/opcua_simple_server_module/src/CMakeLists.txt b/modules/opcua_simple_server_module/src/CMakeLists.txt new file mode 100644 index 00000000..34f4803a --- /dev/null +++ b/modules/opcua_simple_server_module/src/CMakeLists.txt @@ -0,0 +1,49 @@ +set(LIB_NAME opcua_simple_server_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + constants.h + module_dll.h + opcua_simple_server_module_impl.h + opcua_simple_server_impl.h +) + +set(SRC_Srcs module_dll.cpp + opcua_simple_server_module_impl.cpp + opcua_simple_server_impl.cpp +) + +opendaq_prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/opcua_simple_server_module_impl.h + ${MODULE_HEADERS_DIR}/opcua_simple_server_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + ${MODULE_HEADERS_DIR}/common.h + ${MODULE_HEADERS_DIR}/constants.h + module_dll.cpp + opcua_simple_server_module_impl.cpp + opcua_simple_server_impl.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} +) + +add_library(${OPENDAQ_SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +target_link_libraries(${LIB_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq + PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuatms_server +) + +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_simple_server_module/src/module_dll.cpp b/modules/opcua_simple_server_module/src/module_dll.cpp new file mode 100644 index 00000000..6053a880 --- /dev/null +++ b/modules/opcua_simple_server_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::opcua_simple_server_module; + +DEFINE_MODULE_EXPORTS(OpcUaSimpleServerModule) + diff --git a/modules/opcua_simple_server_module/src/opcua_simple_server_impl.cpp b/modules/opcua_simple_server_module/src/opcua_simple_server_impl.cpp new file mode 100644 index 00000000..05013262 --- /dev/null +++ b/modules/opcua_simple_server_module/src/opcua_simple_server_impl.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE +using namespace daq; +using namespace daq::opcua; + +OpcUaSimpleServerImpl::OpcUaSimpleServerImpl(const DevicePtr& rootDevice, + const PropertyObjectPtr& config, + const ContextPtr& context) + : Server(DAQ_OPCUA_SIMPLE_SERVER_ID, config, rootDevice, context) + , server(rootDevice, context) + , context(context) +{ + const uint16_t port = config.getPropertyValue("Port"); + + server.setOpcUaPort(port); + server.setOpcUaPath(config.getPropertyValue("Path")); + server.start(); +} + +OpcUaSimpleServerImpl::~OpcUaSimpleServerImpl() +{ +} + +void OpcUaSimpleServerImpl::populateDefaultConfigFromProvider(const ContextPtr& context, const PropertyObjectPtr& config) +{ + if (!context.assigned()) + return; + if (!config.assigned()) + return; + + auto options = context.getModuleOptions(DAQ_OPCUA_SIMPLE_SERVER_MODULE_ID); + for (const auto& [key, value] : options) + { + if (config.hasProperty(key)) + { + config->setPropertyValue(key, value); + } + } +} + +PropertyObjectPtr OpcUaSimpleServerImpl::createDefaultConfig(const ContextPtr& context) +{ + constexpr Int minPortValue = 0; + constexpr Int maxPortValue = 65535; + + auto defaultConfig = PropertyObject(); + + const auto portProp = IntPropertyBuilder("Port", DAQ_OPCUA_SIMPLE_SERVER_DEFAULT_PORT) + .setMinValue(minPortValue) + .setMaxValue(maxPortValue) + .build(); + defaultConfig.addProperty(portProp); + + defaultConfig.addProperty(StringProperty("Path", DAQ_OPCUA_SIMPLE_SERVER_DEFAULT_PATH)); + + populateDefaultConfigFromProvider(context, defaultConfig); + return defaultConfig; +} + +PropertyObjectPtr OpcUaSimpleServerImpl::populateDefaultConfig(const PropertyObjectPtr& config, const ContextPtr& context) +{ + const auto defConfig = createDefaultConfig(context); + for (const auto& prop : defConfig.getAllProperties()) + { + const auto name = prop.getName(); + if (config.hasProperty(name)) + defConfig.setPropertyValue(name, config.getPropertyValue(name)); + } + + return defConfig; +} + +PropertyObjectPtr OpcUaSimpleServerImpl::getDiscoveryConfig() +{ + auto discoveryConfig = PropertyObject(); + discoveryConfig.addProperty(StringProperty("ServiceName", "_opcua-tcp._tcp.local.")); + discoveryConfig.addProperty(StringProperty("ServiceCap", "OPENDAQ")); + discoveryConfig.addProperty(StringProperty("Path", config.getPropertyValue("Path"))); + discoveryConfig.addProperty(IntProperty("Port", config.getPropertyValue("Port"))); + discoveryConfig.addProperty(StringProperty("ProtocolVersion", "")); + return discoveryConfig; +} + +ServerTypePtr OpcUaSimpleServerImpl::createType(const ContextPtr& context) +{ + return ServerType(DAQ_OPCUA_SIMPLE_SERVER_ID, + "openDAQ simple OPC UA server", + "Publishes signal nodes over OPC UA protocol", + OpcUaSimpleServerImpl::createDefaultConfig(context)); +} + +void OpcUaSimpleServerImpl::onStopServer() +{ + server.stop(); +} + +OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE( + INTERNAL_FACTORY, OpcUaSimpleServer, daq::IServer, + daq::DevicePtr, rootDevice, + PropertyObjectPtr, config, + const ContextPtr&, context +) + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE diff --git a/modules/opcua_simple_server_module/src/opcua_simple_server_module_impl.cpp b/modules/opcua_simple_server_module/src/opcua_simple_server_module_impl.cpp new file mode 100644 index 00000000..6c394488 --- /dev/null +++ b/modules/opcua_simple_server_module/src/opcua_simple_server_module_impl.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include "opcua_simple_server_module/constants.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE + +OpcUaSimpleServerModule::OpcUaSimpleServerModule(ContextPtr context) + : Module(DAQ_OPCUA_SIMPLE_SERVER_MODULE_NAME, + daq::VersionInfo(OPCUA_SIMPLE_SERVER_MODULE_MAJOR_VERSION, OPCUA_SIMPLE_SERVER_MODULE_MINOR_VERSION, OPCUA_SIMPLE_SERVER_MODULE_PATCH_VERSION), + std::move(context), + DAQ_OPCUA_SIMPLE_SERVER_MODULE_ID) +{ +} + +DictPtr OpcUaSimpleServerModule::onGetAvailableServerTypes() +{ + auto result = Dict(); + + auto serverType = OpcUaSimpleServerImpl::createType(context); + result.set(serverType.getId(), serverType); + + return result; +} + +ServerPtr OpcUaSimpleServerModule::onCreateServer(const StringPtr& serverType, + const PropertyObjectPtr& serverConfig, + const DevicePtr& rootDevice) +{ + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Context parameter cannot be null."); + + PropertyObjectPtr config = serverConfig; + if (!config.assigned()) + config = OpcUaSimpleServerImpl::createDefaultConfig(context); + else + config = OpcUaSimpleServerImpl::populateDefaultConfig(config, context); + + ServerPtr server(OpcUaSimpleServer_Create(rootDevice, config, context)); + return server; +} + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE diff --git a/modules/opcua_simple_server_module/tests/CMakeLists.txt b/modules/opcua_simple_server_module/tests/CMakeLists.txt new file mode 100644 index 00000000..550e84e2 --- /dev/null +++ b/modules/opcua_simple_server_module/tests/CMakeLists.txt @@ -0,0 +1,24 @@ +set(MODULE_NAME opcua_simple_server_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_opcua_simple_server_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}::opcuaclient + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_mocks + ${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_simple_server_module/tests/test_app.cpp b/modules/opcua_simple_server_module/tests/test_app.cpp new file mode 100644 index 00000000..b7ced9b0 --- /dev/null +++ b/modules/opcua_simple_server_module/tests/test_app.cpp @@ -0,0 +1,18 @@ +#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_simple_server_module/tests/test_opcua_simple_server_module.cpp b/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp new file mode 100644 index 00000000..e5f8f60b --- /dev/null +++ b/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class OpcUaServerModuleTest : public testing::Test +{ +public: + void TearDown() override + { + } +}; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::modules::opcua_simple_server_module; + +static ModulePtr CreateModule(ContextPtr context = NullContext()) +{ + ModulePtr module; + createOpcUaSimpleServerModule(&module, context); + return module; +} + +static InstancePtr CreateTestInstance() +{ + const auto logger = Logger(); + const auto moduleManager = ModuleManager("[[none]]"); + const auto authenticationProvider = AuthenticationProvider(); + const auto context = Context(Scheduler(logger), logger, TypeManager(), moduleManager, authenticationProvider); + + const ModulePtr deviceModule(MockDeviceModule_Create(context)); + moduleManager.addModule(deviceModule); + + const ModulePtr fbModule(MockFunctionBlockModule_Create(context)); + moduleManager.addModule(fbModule); + + const ModulePtr daqServerModule = CreateModule(context); + moduleManager.addModule(daqServerModule); + + auto instance = InstanceCustom(context, "localInstance"); + for (const auto& deviceInfo : instance.getAvailableDevices()) + instance.addDevice(deviceInfo.getConnectionString()); + + for (const auto& [id, _] : instance.getAvailableFunctionBlockTypes()) + instance.addFunctionBlock(id); + + return instance; +} + +static PropertyObjectPtr CreateServerConfig(const InstancePtr& instance) +{ + auto config = instance.getAvailableServerTypes().get(DAQ_OPCUA_SIMPLE_SERVER_ID).createDefaultConfig(); + return config; +} + +TEST_F(OpcUaServerModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(OpcUaServerModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), DAQ_OPCUA_SIMPLE_SERVER_MODULE_NAME); +} + +TEST_F(OpcUaServerModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(OpcUaServerModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), OPCUA_SIMPLE_SERVER_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), OPCUA_SIMPLE_SERVER_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), OPCUA_SIMPLE_SERVER_MODULE_PATCH_VERSION); +} + +TEST_F(OpcUaServerModuleTest, 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(), 0u); + + DictPtr serverTypes; + ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); + ASSERT_EQ(serverTypes.getCount(), 1u); + ASSERT_TRUE(serverTypes.hasKey(DAQ_OPCUA_SIMPLE_SERVER_ID)); + ASSERT_EQ(serverTypes.get(DAQ_OPCUA_SIMPLE_SERVER_ID).getId(), DAQ_OPCUA_SIMPLE_SERVER_ID); + + // Check module info for module + ModuleInfoPtr moduleInfo; + ASSERT_NO_THROW(moduleInfo = module.getModuleInfo()); + ASSERT_NE(moduleInfo, nullptr); + ASSERT_EQ(moduleInfo.getName(), DAQ_OPCUA_SIMPLE_SERVER_MODULE_NAME); + ASSERT_EQ(moduleInfo.getId(), DAQ_OPCUA_SIMPLE_SERVER_MODULE_ID); + + // Check version info for module + VersionInfoPtr versionInfoModule; + ASSERT_NO_THROW(versionInfoModule = moduleInfo.getVersionInfo()); + ASSERT_NE(versionInfoModule, nullptr); + ASSERT_EQ(versionInfoModule.getMajor(), OPCUA_SIMPLE_SERVER_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoModule.getMinor(), OPCUA_SIMPLE_SERVER_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoModule.getPatch(), OPCUA_SIMPLE_SERVER_MODULE_PATCH_VERSION); + + // Check module and version info for server types + for (const auto& serverType : serverTypes) + { + ModuleInfoPtr moduleInfoServerType; + ASSERT_NO_THROW(moduleInfoServerType = serverType.second.getModuleInfo()); + ASSERT_NE(moduleInfoServerType, nullptr); + ASSERT_EQ(moduleInfoServerType.getName(), DAQ_OPCUA_SIMPLE_SERVER_MODULE_NAME); + ASSERT_EQ(moduleInfoServerType.getId(), DAQ_OPCUA_SIMPLE_SERVER_MODULE_ID); + + VersionInfoPtr versionInfoServerType; + ASSERT_NO_THROW(versionInfoServerType = moduleInfoServerType.getVersionInfo()); + ASSERT_NE(versionInfoServerType, nullptr); + ASSERT_EQ(versionInfoServerType.getMajor(), OPCUA_SIMPLE_SERVER_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoServerType.getMinor(), OPCUA_SIMPLE_SERVER_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoServerType.getPatch(), OPCUA_SIMPLE_SERVER_MODULE_PATCH_VERSION); + } +} + +TEST_F(OpcUaServerModuleTest, ServerConfig) +{ + auto module = CreateModule(); + + DictPtr serverTypes = module.getAvailableServerTypes(); + ASSERT_TRUE(serverTypes.hasKey(DAQ_OPCUA_SIMPLE_SERVER_ID)); + auto config = serverTypes.get(DAQ_OPCUA_SIMPLE_SERVER_ID).createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + + ASSERT_TRUE(config.hasProperty("Port")); + ASSERT_EQ(config.getPropertyValue("Port"), DAQ_OPCUA_SIMPLE_SERVER_DEFAULT_PORT); +} + +TEST_F(OpcUaServerModuleTest, CreateServer) +{ + auto device = CreateTestInstance(); + auto module = CreateModule(device.getContext()); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(module.createServer(DAQ_OPCUA_SIMPLE_SERVER_ID, device.getRootDevice(), config)); +} + +TEST_F(OpcUaServerModuleTest, CreateServerFromInstance) +{ + auto device = CreateTestInstance(); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(device.addServer(DAQ_OPCUA_SIMPLE_SERVER_ID, config)); +} + +TEST_F(OpcUaServerModuleTest, TestConnection) +{ + auto device = CreateTestInstance(); + auto config = CreateServerConfig(device); + device.addServer(DAQ_OPCUA_SIMPLE_SERVER_ID, config); + + OpcUaClient client("opc.tcp://localhost/"); + ASSERT_NO_THROW(client.connect()); +} + +TEST_F(OpcUaServerModuleTest, TestConnectionDifferentPort) +{ + auto device = CreateTestInstance(); + auto module = CreateModule(device.getContext()); + auto config = CreateServerConfig(device); + + config.setPropertyValue("Port", 4841); + + auto serverPtr = module.createServer(DAQ_OPCUA_SIMPLE_SERVER_ID, device.getRootDevice(), config); + + OpcUaClient client("opc.tcp://localhost:4841/"); + ASSERT_NO_THROW(client.connect()); +} + +TEST_F(OpcUaServerModuleTest, StopServer) +{ + auto device = CreateTestInstance(); + auto module = CreateModule(device.getContext()); + auto config = CreateServerConfig(device); + + auto serverPtr = module.createServer(DAQ_OPCUA_SIMPLE_SERVER_ID, device.getRootDevice(), config); + + OpcUaClient client("opc.tcp://localhost/"); + ASSERT_NO_THROW(client.connect()); + client.disconnect(); + + serverPtr.stop(); + ASSERT_THROW(client.connect(), OpcUaException); +} From 8732ce6d68bb50e9dcf5e49eb2709572d207dcca Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 6 May 2026 18:14:19 +0200 Subject: [PATCH 02/15] simple opcua server: base simple opcua server --- shared/libraries/CMakeLists.txt | 4 +- .../include/opcuaserver/opcuaserver.h | 3 +- .../opcua/opcuaserver/src/opcuaserver.cpp | 6 +- shared/libraries/opcuageneric/CMakeLists.txt | 9 +- .../opcua_simple_objects/CMakeLists.txt | 5 + .../include/opcua_simple_objects/common.h | 7 + .../include/opcua_simple_objects/constants.h | 9 + .../include/opcua_simple_objects/signal.h | 53 ++++++ .../opcua_simple_objects/src/CMakeLists.txt | 59 +++++++ .../opcua_simple_objects/src/signal.cpp | 145 ++++++++++++++++ .../opcua_simple_server/CMakeLists.txt | 9 + .../opcua_simple_server/simple_server.h | 56 +++++++ .../opcua_simple_server/src/CMakeLists.txt | 51 ++++++ .../opcua_simple_server/src/simple_server.cpp | 158 ++++++++++++++++++ .../opcua_simple_server/tests/CMakeLists.txt | 6 + .../tests/server_tests/CMakeLists.txt | 27 +++ .../tests/server_tests/test_simple_server.cpp | 43 +++++ .../tests/server_tests/testapp.cpp | 19 +++ .../tests/test_utils/CMakeLists.txt | 16 ++ .../test_utils/generic_opcua_test_helper.cpp | 87 ++++++++++ .../test_utils/generic_opcua_test_helper.h | 43 +++++ .../tests/test_utils/test_helpers.h | 117 +++++++++++++ 22 files changed, 925 insertions(+), 7 deletions(-) create mode 100644 shared/libraries/opcuageneric/opcua_simple_objects/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/common.h create mode 100644 shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/constants.h create mode 100644 shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h create mode 100644 shared/libraries/opcuageneric/opcua_simple_objects/src/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/src/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/testapp.cpp create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/CMakeLists.txt create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.cpp create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.h create mode 100644 shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h diff --git a/shared/libraries/CMakeLists.txt b/shared/libraries/CMakeLists.txt index 699dd04f..39b77a84 100644 --- a/shared/libraries/CMakeLists.txt +++ b/shared/libraries/CMakeLists.txt @@ -6,7 +6,5 @@ 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() +add_subdirectory(opcuageneric) diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h index 63a0068d..c3ee8a6a 100644 --- a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -64,7 +64,7 @@ class OpcUaServer final : public daq::utils::ThreadEx const UA_NodeId* methodId, void* methodContext); - OpcUaServer(); + OpcUaServer(bool addCustomTypes = true); ~OpcUaServer(); static constexpr uint16_t OPCUA_DEFAULT_PORT = 4840; @@ -201,6 +201,7 @@ class OpcUaServer final : public daq::utils::ThreadEx AuthenticationProviderPtr authenticationProvider; OnClientConnectedCallback clientConnectedHandler; OnClientDisconnectedCallback clientDisconnectedHandler; + bool addCustomTypes; }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 8a2909b4..5ed6f975 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -10,8 +10,9 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA -OpcUaServer::OpcUaServer() +OpcUaServer::OpcUaServer(bool addCustomTypes) : eventManager(std::make_shared(this)) + , addCustomTypes(addCustomTypes) { setPort(OPCUA_DEFAULT_PORT); createSessionContextCallback = [this](const OpcUaNodeId& sessionId, const UserPtr& authorizedUser) { return createSessionContextCallbackImp(sessionId, authorizedUser); }; @@ -157,7 +158,8 @@ void OpcUaServer::prepareServer() config->nodeLifecycle.generateChildNodeId = generateChildId; prepareAccessControl(config); - addTmsTypes(server); + if (addCustomTypes) + addTmsTypes(server); eventManager->registerEvents(); } diff --git a/shared/libraries/opcuageneric/CMakeLists.txt b/shared/libraries/opcuageneric/CMakeLists.txt index 9383e1fb..1a3b9148 100644 --- a/shared/libraries/opcuageneric/CMakeLists.txt +++ b/shared/libraries/opcuageneric/CMakeLists.txt @@ -3,7 +3,14 @@ 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_GENERIC_CLIENT) + add_subdirectory(opcuageneric_client) +endif() + +if (${REPO_OPTION_PREFIX}_ENABLE_SIMPLE_SERVER) + add_subdirectory(opcua_simple_objects) + add_subdirectory(opcua_simple_server) +endif() if (${REPO_OPTION_PREFIX}_ENABLE_TESTS) #add_subdirectory(tests) diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_objects/CMakeLists.txt new file mode 100644 index 00000000..3f2305ce --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcua_simple_objects VERSION ${${REPO_OPTION_PREFIX}_VERSION} LANGUAGES CXX) + +add_subdirectory(src) \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/common.h b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/common.h new file mode 100644 index 00000000..97b13279 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/common.h @@ -0,0 +1,7 @@ +#pragma once + +#define BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS \ + namespace daq::opcua::simple_objects \ + { + +#define END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS } \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/constants.h b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/constants.h new file mode 100644 index 00000000..55e74a90 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/constants.h @@ -0,0 +1,9 @@ +#pragma once + +namespace daq::opcua::simple_objects +{ + +// Constants +static const char* DEFAULT_LOCALE = "en_US"; + +} diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h new file mode 100644 index 00000000..ec6f84c2 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h @@ -0,0 +1,53 @@ +/* + * 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 "opcuashared/opcuanodeid.h" +#include "opcuashared/opcuavariant.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS +class SignalNode +{ +public: + explicit SignalNode(daq::opcua::OpcUaServerPtr server, + const OpcUaNodeId parentNodeId, + const SignalPtr& signal, + const PropertyObjectPtr& config = nullptr); + ~SignalNode() = default; + + void process(); + +protected: + static std::unordered_map converterMap; + + daq::opcua::OpcUaServerPtr server; + SignalPtr signal; + OpcUaNodeId parentNodeId; + OpcUaNodeId variableNodeId; + const uint16_t namespaceIndex = 1; + + void addVariableNode(); + OpcUaNodeId convertSampleTypeToDataTypeId(const daq::SampleType sampleType) const; + OpcUaVariant toVariant(const BaseObjectPtr& lastValue, SampleType sampleType) const; + +}; + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/src/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_objects/src/CMakeLists.txt new file mode 100644 index 00000000..5eb3f313 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/CMakeLists.txt @@ -0,0 +1,59 @@ +set(LIB_NAME opcua_simple_objects) +set(HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + + +set(SRC_PublicHeaders constants.h + common.h + signal.h +) +set(SRC_Cpp signal.cpp +) + +source_group("common" FILES ${HEADERS_DIR}/constants.h + ${HEADERS_DIR}/common.h +) +source_group("signal" FILES ${HEADERS_DIR}/signal.h + signal.cpp +) + +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(${REPO_OPTION_PREFIX}_ENABLE_TESTS) + target_compile_definitions(${LIB_NAME} + PRIVATE ${REPO_OPTION_PREFIX}_ENABLE_TESTS + ) +endif() + +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}::opcuaserver + ${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/opcua_simple_objects/src/signal.cpp b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp new file mode 100644 index 00000000..63285581 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include "opcuaserver/opcuaaddnodeparams.h" +#include "opcuaserver/opcuaserver.h" +#include "opcuashared/opcuanodeid.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS + +std::unordered_map SignalNode::converterMap = {{SampleType::Float32, UA_TYPES_FLOAT}, + {SampleType::Float64, UA_TYPES_DOUBLE}, + {SampleType::UInt8, UA_TYPES_BYTE}, + {SampleType::Int8, UA_TYPES_SBYTE}, + {SampleType::UInt16, UA_TYPES_UINT16}, + {SampleType::Int16, UA_TYPES_INT16}, + {SampleType::UInt32, UA_TYPES_UINT32}, + {SampleType::Int32, UA_TYPES_INT32}, + {SampleType::UInt64, UA_TYPES_UINT64}, + {SampleType::String, UA_TYPES_STRING}}; + +SignalNode::SignalNode(daq::opcua::OpcUaServerPtr server, + const OpcUaNodeId parentNodeId, + const SignalPtr& signal, + const PropertyObjectPtr& config) + : server(server) + , signal(signal) + , parentNodeId(parentNodeId) + +{ + addVariableNode(); +} + +void SignalNode::addVariableNode() +{ + OpcUaNodeId variableNodeId(namespaceIndex, signal.getGlobalId().toStdString()); + AddVariableNodeParams params(variableNodeId, parentNodeId); + + auto sigDesc = signal.getDescriptor(); + if (!sigDesc.assigned()) + DAQ_THROW_EXCEPTION(UninitializedException, "Signal descriptor is not assigned. Cannot determine data type for OPC UA variable node."); + + const auto dataType = convertSampleTypeToDataTypeId(sigDesc.getSampleType()); + if (dataType.isNull()) + DAQ_THROW_EXCEPTION(InvalidTypeException, "Signal sample type is not defined or not supported."); + + params.setDataType(dataType); + + auto browseName = signal.getGlobalId().toStdString(); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + params.setBrowseName(browseName); + + params.attr->displayName = UA_LOCALIZEDTEXT_ALLOC(DEFAULT_LOCALE, signal.getName().toStdString().c_str()); + params.attr->description = UA_LOCALIZEDTEXT_ALLOC(DEFAULT_LOCALE, signal.getDescription().toStdString().c_str()); + params.attr->writeMask = UA_ATTRIBUTEWRITEMASK_NONE; + params.attr->userWriteMask = UA_ATTRIBUTEWRITEMASK_NONE; + params.attr->accessLevel = UA_ACCESSLEVELMASK_READ; + params.attr->userAccessLevel = UA_ACCESSLEVELMASK_READ; + params.attr->valueRank = UA_VALUERANK_SCALAR; + params.attr->historizing = 0; + + + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASPROPERTY)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE)); + params.nodeContext = this; + this->variableNodeId = server->addVariableNode(params); +} + +OpcUaNodeId SignalNode::convertSampleTypeToDataTypeId(const SampleType sampleType) const +{ + OpcUaNodeId result; + if (converterMap.count(sampleType) > 0) + return OpcUaNodeId(UA_TYPES[converterMap[sampleType]].typeId); + else + return OpcUaNodeId(); +} + +void SignalNode::process() +{ + const auto lastValue = signal.getLastValue(); + auto sigDesc = signal.getDescriptor(); + if (!sigDesc.assigned()) + return; + const auto st = sigDesc.getSampleType(); + const auto dataType = convertSampleTypeToDataTypeId(st); + if (dataType.isNull()) + return; + const auto variant = toVariant(lastValue, st); + try { + server->writeValue(variableNodeId, variant); + } catch (OpcUaException& ex) { + + } +} + +OpcUaVariant SignalNode::toVariant(const BaseObjectPtr& lastValue, SampleType sampleType) const +{ + OpcUaVariant variant; + if (!lastValue.assigned()) + return variant; + + switch (sampleType) + { + case SampleType::Float32: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::Float64: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::Int8: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::UInt8: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::Int16: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::UInt16: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::Int32: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::UInt32: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::Int64: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::UInt64: + variant.setScalar(static_cast(lastValue.asPtr())); + break; + case SampleType::String: + { + const auto iStr = lastValue.asPtr(); + variant = OpcUaVariant(std::string(iStr.getCharPtr(), iStr.getLength()).c_str()); + } + break; + default: + break; + } + return variant; +} + +END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_server/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_server/CMakeLists.txt new file mode 100644 index 00000000..6816717d --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcua_simple_server 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/opcua_simple_server/include/opcua_simple_server/simple_server.h b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h new file mode 100644 index 00000000..8337d489 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h @@ -0,0 +1,56 @@ +/* + * 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 + +class GenericServer +{ +public: + GenericServer(const InstancePtr& instance); + GenericServer(const DevicePtr& device, const ContextPtr& context); + ~GenericServer(); + + void setOpcUaPort(uint16_t port); + void setOpcUaPath(const std::string& path); + void start(); + void stop(); + +protected: + const uint16_t namespaceIndex = 1; + DevicePtr device; + ContextPtr context; + + daq::opcua::OpcUaServerPtr server; + uint16_t opcUaPort = 4840; + std::string opcUaPath = "/"; + OpcUaNodeId rootDeviceNodeId; + + std::vector signalNodes; + // std::unordered_map registeredClientIds; + + void createDeviceNode(); + void fillDeviceNode(); + void addSignalNodes(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_server/src/CMakeLists.txt new file mode 100644 index 00000000..9d1e926f --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/CMakeLists.txt @@ -0,0 +1,51 @@ +set(LIB_NAME opcua_simple_server) +set(HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Cpp simple_server.cpp +) + +set(SRC_PublicHeaders +) + +set(SRC_PrivateHeaders simple_server.h +) + + +set(SRC_PublicHeaders ${SRC_PublicHeaders}) +set(SRC_Cpp ${SRC_Cpp}) + +source_group("server" FILES ${HEADERS_DIR}/simple_server.h + simple_server.cpp +) + +opendaq_prepend_include(${LIB_NAME} SRC_PrivateHeaders) +opendaq_prepend_include(${LIB_NAME} SRC_PublicHeaders) + + +add_library(${LIB_NAME} STATIC ${SRC_Cpp} + ${SRC_PublicHeaders} + ${SRC_PrivateHeaders} +) + +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() + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + + $ +) + +target_link_libraries(${LIB_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuaserver + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_simple_objects +) + +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/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp new file mode 100644 index 00000000..dcf3364e --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include +#include +#include +#include + +//using namespace daq::opcua; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +GenericServer::~GenericServer() +{ + stop(); +} + +GenericServer::GenericServer(const InstancePtr& instance) + : GenericServer(instance.getRootDevice(), instance.getContext()) +{ +} + +GenericServer::GenericServer(const DevicePtr& device, const ContextPtr& context) + : device(device) + , context(context) +{ +} + +void GenericServer::setOpcUaPort(uint16_t port) +{ + this->opcUaPort = port; +} + +void GenericServer::setOpcUaPath(const std::string& path) +{ + this->opcUaPath = path; +} + +void GenericServer::start() +{ + if (!device.assigned()) + DAQ_THROW_EXCEPTION(InvalidStateException, "Device is not set."); + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidStateException, "Context is not set."); + + auto info = device.getInfo(); + + server = std::make_shared(false); + server->setPort(opcUaPort); + server->setAuthenticationProvider(context.getAuthenticationProvider()); + // server->setClientConnectedHandler( + // [this](const std::string& clientId) + // { + // const auto loggerComponent = context.getLogger().getOrAddComponent("SimpleOPCUAServer"); + // LOG_I("New client connected, ID: {}", clientId); + // SizeT clientNumber = 0; + // if (device.assigned() && !device.isRemoved()) + // { + // device.getInfo().asPtr(true).addConnectedClient( + // &clientNumber, + // ConnectedClientInfo("", ProtocolType::Configuration, "OpenDAQOPCUA", "", "")); + // } + // registeredClientIds.insert({clientId, clientNumber}); + // } + // ); + // server->setClientDisconnectedHandler( + // [this](const std::string& clientId) + // { + // if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) + // { + // const auto loggerComponent = context.getLogger().getOrAddComponent("SimpleOPCUAServer"); + // LOG_I("Client disconnected, ID: {}", clientId); + // if (device.assigned() && !device.isRemoved() && it->second != 0) + // { + // device.getInfo().asPtr(true).removeConnectedClient(it->second); + // } + // registeredClientIds.erase(it); + // } + // } + // ); + // server->setAllowBrowsingNodeCallback(TmsServerObject::allowBrowsingNodeCallback); + // server->setGetUserAccessLevelCallback(TmsServerObject::getUserAccessLevelCallback); + // server->setGetUserRightsMaskCallback(TmsServerObject::getUserRightsMaskCallback); + // server->setGetUserExecutableCallback(TmsServerObject::getUserExecutableCallback); + server->prepare(); + + createDeviceNode(); + fillDeviceNode(); + addSignalNodes(); + + // auto serverCapability = ServerCapability("OpenDAQOPCUAConfiguration", "OpenDAQOPCUA", ProtocolType::Configuration); + // serverCapability.setPrefix("daq.opcua"); + // serverCapability.setConnectionType("TCP/IP"); + // serverCapability.setPort(opcUaPort); + // serverCapability.addProperty(StringProperty("Path", opcUaPath == "/" ? "" : opcUaPath)); + // info.asPtr(true).addServerCapability(serverCapability); + + server->start(); +} + +void GenericServer::stop() +{ + // if (device.assigned() && !device.isRemoved()) + // { + // const auto info = device.getInfo(); + // const auto infoInternal = info.asPtr(); + // if (info.hasServerCapability("OpenDAQOPCUAConfiguration")) + // infoInternal.removeServerCapability("OpenDAQOPCUAConfiguration"); + // for (const auto& [_, clientNumber] : registeredClientIds) + // { + // if (clientNumber != 0) + // infoInternal.removeConnectedClient(clientNumber); + // } + // } + // registeredClientIds.clear(); + + if (server) + server->stop(); + + server.reset(); +} + +void GenericServer::createDeviceNode() +{ + OpcUaNodeId rootDeviceNodeId(namespaceIndex, device.getGlobalId().toStdString()); + + AddObjectNodeParams params(rootDeviceNodeId, OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER)); + + auto browseName = device.getGlobalId().toStdString(); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + params.setBrowseName(browseName); + params.attr->displayName = UA_LOCALIZEDTEXT_ALLOC("en_US", device.getName().toStdString().c_str()); + params.attr->description = UA_LOCALIZEDTEXT_ALLOC("en_US", device.getDescription().toStdString().c_str()); + params.attr->writeMask = UA_ATTRIBUTEWRITEMASK_NONE; + params.attr->userWriteMask = UA_ATTRIBUTEWRITEMASK_NONE; + params.attr->eventNotifier = UA_EVENTNOTIFIER_SUBSCRIBE_TO_EVENT; + + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE)); + params.nodeContext = this; + this->rootDeviceNodeId = server->addObjectNode(params); +} + +void GenericServer::fillDeviceNode() +{ + +} + +void GenericServer::addSignalNodes() +{ + const auto sigList = device.getSignalsRecursive(search::Any()); + for (const auto& sig : sigList) + { + signalNodes.emplace_back(server, rootDeviceNodeId, sig); + } +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_server/tests/CMakeLists.txt new file mode 100644 index 00000000..f4d0a57a --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) +project(test_opcua_simple_server CXX) + +add_subdirectory(test_utils) +add_subdirectory(server_tests) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/CMakeLists.txt new file mode 100644 index 00000000..b0b1a6b1 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/CMakeLists.txt @@ -0,0 +1,27 @@ +set(MODULE_NAME opcua_simple_server) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_simple_server.cpp +) + +add_executable(${TEST_APP} testapp.cpp + ${TEST_SOURCES} +) + +if (MSVC) + target_compile_options(${TEST_APP} PRIVATE /bigobj) +endif() + +set_target_properties(${TEST_APP} PROPERTIES DEBUG_POSTFIX _debug) + +target_link_libraries(${TEST_APP} PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} gtest + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_test_utils + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuaclient + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_mocks + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_simple_server_test_utils +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp new file mode 100644 index 00000000..e68e3297 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include + +using SimpleServerTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace test_helpers; + + +TEST_F(SimpleServerTest, Create) +{ + auto daqInstance = SetupInstance(); + ASSERT_NO_THROW(GenericServer server(daqInstance)); +} + +TEST_F(SimpleServerTest, StartStop) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + ASSERT_NO_THROW(server.start()); + ASSERT_NO_THROW(server.stop()); +} + +TEST_F(SimpleServerTest, Connect) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + server.start(); + + auto client = OpcuaServerHelper::CreateAndConnectTestClient(); + ASSERT_TRUE(client->isConnected()); +} + +TEST_F(SimpleServerTest, Temp) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + ASSERT_NO_THROW(server.start()); + std::this_thread::sleep_for(std::chrono::seconds(600)); +} \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/testapp.cpp b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/testapp.cpp new file mode 100644 index 00000000..a257dacb --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/testapp.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/opcua_simple_server/tests/test_utils/CMakeLists.txt b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/CMakeLists.txt new file mode 100644 index 00000000..572af856 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/CMakeLists.txt @@ -0,0 +1,16 @@ +set(MODULE_NAME opcua_simple_server_test_utils) + +set(MODULE_SOURCES generic_opcua_test_helper.h + generic_opcua_test_helper.cpp + test_helpers.h +) + +add_library(${MODULE_NAME} STATIC ${MODULE_SOURCES}) +add_library(${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) + +target_include_directories(${MODULE_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(${MODULE_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuaclient + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq +) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.cpp b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.cpp new file mode 100644 index 00000000..e93145a2 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.cpp @@ -0,0 +1,87 @@ +#include +#include + + +using namespace daq::opcua; + +namespace test_helpers +{ +void OpcuaServerHelper::Init() +{ + client = CreateAndConnectTestClient(); +} + +void OpcuaServerHelper::Clear() +{ + client.reset(); +} + +OpcUaClientPtr OpcuaServerHelper::getClient() +{ + return client; +} + +void OpcuaServerHelper::writeChildNode(const OpcUaNodeId& parent, const std::string& browseName, const OpcUaVariant& variant) +{ + getClient()->writeValue(getChildNodeId(parent, browseName), variant); +} + +OpcUaVariant OpcuaServerHelper::readChildNode(const OpcUaNodeId& parent, const std::string& browseName) +{ + return getClient()->readValue(getChildNodeId(parent, browseName)); +} + +OpcUaNodeId OpcuaServerHelper::getChildNodeId(const OpcUaNodeId& parent, const std::string& browseName) +{ + OpcUaObject br; + br->requestedMaxReferencesPerNode = 0; + br->nodesToBrowse = UA_BrowseDescription_new(); + br->nodesToBrowseSize = 1; + br->nodesToBrowse[0].nodeId = parent.copyAndGetDetachedValue(); + br->nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL; + + OpcUaObject result = UA_Client_Service_browse(client->getUaClient(), *br); + + if (result->resultsSize == 0) + return OpcUaNodeId(UA_NODEID_NULL); + + auto references = result->results[0].references; + auto referenceCount = result->results[0].referencesSize; + + for (size_t i = 0; i < referenceCount; i++) + { + auto reference = references[i]; + std::string refBrowseName = utils::ToStdString(reference.browseName.name); + if (refBrowseName == browseName) + return OpcUaNodeId(reference.nodeId.nodeId); + } + + return OpcUaNodeId(UA_NODEID_NULL); +} + +OpcUaObject OpcuaServerHelper::browseNode(const daq::opcua::OpcUaNodeId& nodeId) +{ + OpcUaObject br; + br->requestedMaxReferencesPerNode = 0; + br->nodesToBrowse = UA_BrowseDescription_new(); + br->nodesToBrowseSize = 1; + br->nodesToBrowse[0].nodeId = nodeId.copyAndGetDetachedValue(); + br->nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL; + + OpcUaObject result = UA_Client_Service_browse(client->getUaClient(), *br); + if (result->resultsSize == 0) + throw OpcUaException(UA_STATUSCODE_BADUNEXPECTEDERROR, ""); + CheckStatusCodeException(result->results[0].statusCode); + return result; +} + +daq::opcua::OpcUaClientPtr OpcuaServerHelper::CreateAndConnectTestClient(const std::string& username, const std::string& password) +{ + OpcUaEndpoint endpoint("opc.tcp://127.0.0.1:4840", username, password); + + auto client = std::make_shared(endpoint); + client->connect(); + client->runIterate(); + return client; +} +} \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.h b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.h new file mode 100644 index 00000000..a73df5ee --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/generic_opcua_test_helper.h @@ -0,0 +1,43 @@ +/* + * 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 + +namespace test_helpers +{ + +class OpcuaServerHelper +{ +public: + OpcuaServerHelper() = default; + virtual void Init(); + virtual void Clear(); + + daq::opcua::OpcUaClientPtr getClient(); + void writeChildNode(const daq::opcua::OpcUaNodeId& parent, + const std::string& browseName, + const daq::opcua::OpcUaVariant& variant); + daq::opcua::OpcUaVariant readChildNode(const daq::opcua::OpcUaNodeId& parent, const std::string& browseName); + daq::opcua::OpcUaNodeId getChildNodeId(const daq::opcua::OpcUaNodeId& parent, const std::string& browseName); + daq::opcua::OpcUaObject browseNode(const daq::opcua::OpcUaNodeId& nodeId); + + static daq::opcua::OpcUaClientPtr CreateAndConnectTestClient(const std::string& username = "", const std::string& password = ""); + +protected: + daq::opcua::OpcUaClientPtr client; +}; +} \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h new file mode 100644 index 00000000..54b2738a --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h @@ -0,0 +1,117 @@ +/* + * 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 +#include "coreobjects/permission_mask_builder_factory.h" +#include "coreobjects/permissions_builder_factory.h" +#include "coreobjects/user_factory.h" +#include "opcuaserver/opcuasession.h" + +namespace test_helpers +{ + + inline daq::PermissionsBuilderPtr CreatePermissionsBuilder() + { + using namespace daq; + return PermissionsBuilder() + .inherit(false) + .assign("everyone", PermissionMaskBuilder()) + .assign("reader", PermissionMaskBuilder().read()) + .assign("writer", PermissionMaskBuilder().read().write()) + .assign("executor", PermissionMaskBuilder().read().execute()) + .assign("admin", PermissionMaskBuilder().read().write().execute()); + } + + inline auto CreateUsers() + { + using namespace daq; + auto users = List(); + const std::vector> templateForUser = { + {"common", ""}, {"reader", "reader"}, {"writer", "writer"}, {"executor", "executor"}, {"admin", "admin"}}; + for (const auto& [user, group] : templateForUser) + { + if (group.empty()) + users.pushBack(User(user + "User", user + "UserPass")); + else + users.pushBack(User(user + "User", user + "UserPass", {group})); + } + return users; + } + + inline daq::opcua::OpcUaSession createSession(uint32_t id, + const std::string& username, + std::string password = "", + std::vector roles = {}) + { + if (password == "") + password = username + "Pass"; + return daq::opcua::OpcUaSession(daq::opcua::OpcUaNodeId(id), nullptr, daq::User(username, password, roles)); + } + + inline daq::opcua::OpcUaSession createSessionCommon(const std::string& username, const std::string& password = "") + { + return createSession(0, username, password); + } + + inline daq::opcua::OpcUaSession createSessionReader(const std::string& username, const std::string& password = "") + { + return createSession(1, username, password, {"reader"}); + } + + inline daq::opcua::OpcUaSession createSessionWriter(const std::string& username, const std::string& password = "") + { + return createSession(2, username, password, {"writer"}); + } + + inline daq::opcua::OpcUaSession createSessionExecutor(const std::string& username, const std::string& password = "") + { + return createSession(3, username, password, {"executor"}); + } + + inline daq::opcua::OpcUaSession createSessionAdmin(const std::string& username, const std::string& password = "") + { + return createSession(4, username, password, {"admin"}); + } + + inline daq::InstancePtr SetupInstance(bool anonymousAllowed = true) + { + using namespace daq; + const auto logger = Logger(); + const auto moduleManager = ModuleManager("[[none]]"); + + const auto authenticationProvider = StaticAuthenticationProvider(anonymousAllowed, CreateUsers()); + const auto context = Context(nullptr, logger, TypeManager(), moduleManager, authenticationProvider); + + const ModulePtr deviceModule(MockDeviceModule_Create(context)); + moduleManager.addModule(deviceModule); + + const ModulePtr fbModule(MockFunctionBlockModule_Create(context)); + moduleManager.addModule(fbModule); + + auto instance = InstanceCustom(context, "localInstance"); + instance.addDevice("daq.root://default_client"); + instance.addDevice("daqmock://phys_device"); + instance.addFunctionBlock("mock_fb_uid"); + + return instance; + } +} From e3bdf2b26e35b652bf6b512e2f34b505994d565b Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 6 May 2026 18:15:40 +0200 Subject: [PATCH 03/15] simple opcua server: updating variable node values --- .../opcua_simple_server/simple_server.h | 17 +++++++- .../opcua_simple_server/src/simple_server.cpp | 43 ++++++++++++++++++- .../tests/server_tests/test_simple_server.cpp | 1 + .../tests/test_utils/test_helpers.h | 3 -- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h index 8337d489..89a7f1eb 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h +++ b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h @@ -15,11 +15,14 @@ */ #pragma once +#include +#include #include #include -#include +#include #include -#include +#include +#include BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -48,9 +51,19 @@ class GenericServer std::vector signalNodes; // std::unordered_map registeredClientIds; + uint64_t readingIntervalMs; + std::thread readingThread; + std::atomic readingRunning{false}; + std::condition_variable readingCv; + std::mutex readingMutex; + void createDeviceNode(); void fillDeviceNode(); void addSignalNodes(); + + void startReadingThread(); + void stopReadingThread(); + void readingLoop(); }; END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index dcf3364e..3153f810 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -23,6 +23,7 @@ GenericServer::GenericServer(const InstancePtr& instance) GenericServer::GenericServer(const DevicePtr& device, const ContextPtr& context) : device(device) , context(context) + , readingIntervalMs(100) { } @@ -96,6 +97,8 @@ void GenericServer::start() // info.asPtr(true).addServerCapability(serverCapability); server->start(); + + startReadingThread(); } void GenericServer::stop() @@ -113,11 +116,13 @@ void GenericServer::stop() // } // } // registeredClientIds.clear(); + stopReadingThread(); if (server) server->stop(); - + server.reset(); + signalNodes.clear(); } void GenericServer::createDeviceNode() @@ -155,4 +160,40 @@ void GenericServer::addSignalNodes() } } +void GenericServer::startReadingThread() +{ + readingRunning = true; + readingThread = std::thread([this] { readingLoop(); }); +} + +void GenericServer::stopReadingThread() +{ + { + std::lock_guard lock(readingMutex); + readingRunning = false; + } + readingCv.notify_all(); + if (readingThread.joinable()) + readingThread.join(); +} + +void GenericServer::readingLoop() + { + auto interruptibleSleep = [&](std::chrono::steady_clock::time_point nextTimePoint) + { + std::unique_lock lock(readingMutex); + readingCv.wait_until(lock, nextTimePoint, [this]() { return !readingRunning.load(); }); + }; + + while (readingRunning) + { + const auto nextTimePoint = std::chrono::steady_clock::now() + std::chrono::milliseconds(readingIntervalMs); + for (auto& signalNode : signalNodes) + { + signalNode.process(); + } + interruptibleSleep(nextTimePoint); + } + } + END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp index e68e3297..2bd5c449 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp @@ -39,5 +39,6 @@ TEST_F(SimpleServerTest, Temp) auto daqInstance = SetupInstance(); GenericServer server(daqInstance); ASSERT_NO_THROW(server.start()); + daqInstance.getDevices()[0].setPropertyValue("GeneratePackets", 1000000); std::this_thread::sleep_for(std::chrono::seconds(600)); } \ No newline at end of file diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h index 54b2738a..5517f4cc 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h @@ -108,10 +108,7 @@ namespace test_helpers moduleManager.addModule(fbModule); auto instance = InstanceCustom(context, "localInstance"); - instance.addDevice("daq.root://default_client"); instance.addDevice("daqmock://phys_device"); - instance.addFunctionBlock("mock_fb_uid"); - return instance; } } From 944d77abf7c1a16d80bd941ab57a5e34a600e43c Mon Sep 17 00:00:00 2001 From: Viacheslav Kalenikov Date: Wed, 6 May 2026 18:16:39 +0200 Subject: [PATCH 04/15] simple opcua server: tests --- .../tests/server_tests/test_simple_server.cpp | 161 +++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp index 2bd5c449..4031b12d 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include using SimpleServerTest = testing::Test; @@ -16,6 +19,28 @@ TEST_F(SimpleServerTest, Create) ASSERT_NO_THROW(GenericServer server(daqInstance)); } +TEST_F(SimpleServerTest, CreateFromDeviceAndContext) +{ + auto daqInstance = SetupInstance(); + ASSERT_NO_THROW(GenericServer server(daqInstance.getRootDevice(), daqInstance.getContext())); +} + +TEST_F(SimpleServerTest, StartThrowsWithNullDevice) +{ + auto daqInstance = SetupInstance(); + DevicePtr nullDevice; + GenericServer server(nullDevice, daqInstance.getContext()); + ASSERT_THROW(server.start(), std::exception); +} + +TEST_F(SimpleServerTest, StartThrowsWithNullContext) +{ + auto daqInstance = SetupInstance(); + ContextPtr nullContext; + GenericServer server(daqInstance.getRootDevice(), nullContext); + ASSERT_THROW(server.start(), std::exception); +} + TEST_F(SimpleServerTest, StartStop) { auto daqInstance = SetupInstance(); @@ -24,6 +49,22 @@ TEST_F(SimpleServerTest, StartStop) ASSERT_NO_THROW(server.stop()); } +TEST_F(SimpleServerTest, StopBeforeStart) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + ASSERT_NO_THROW(server.stop()); +} + +TEST_F(SimpleServerTest, DoubleStop) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + server.start(); + ASSERT_NO_THROW(server.stop()); + ASSERT_NO_THROW(server.stop()); +} + TEST_F(SimpleServerTest, Connect) { auto daqInstance = SetupInstance(); @@ -34,7 +75,125 @@ TEST_F(SimpleServerTest, Connect) ASSERT_TRUE(client->isConnected()); } -TEST_F(SimpleServerTest, Temp) +TEST_F(SimpleServerTest, Restart) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + + server.start(); + auto client1 = OpcuaServerHelper::CreateAndConnectTestClient(); + ASSERT_TRUE(client1->isConnected()); + client1->disconnect(); + server.stop(); + + server.start(); + auto client2 = OpcuaServerHelper::CreateAndConnectTestClient(); + ASSERT_TRUE(client2->isConnected()); + client2->disconnect(); + server.stop(); +} + +TEST_F(SimpleServerTest, CustomPort) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + server.setOpcUaPort(4841); + server.start(); + + OpcUaEndpoint endpoint("opc.tcp://127.0.0.1:4841"); + auto client = std::make_shared(endpoint); + ASSERT_NO_THROW(client->connect()); + ASSERT_TRUE(client->isConnected()); + + server.stop(); +} + +TEST_F(SimpleServerTest, AnonymousConnectionBlocked) +{ + auto daqInstance = SetupInstance(false); + GenericServer server(daqInstance); + server.start(); + + OpcUaEndpoint endpoint("opc.tcp://127.0.0.1:4840"); + auto client = std::make_shared(endpoint); + ASSERT_THROW(client->connect(), std::exception); + + server.stop(); +} + +TEST_F(SimpleServerTest, AuthenticatedConnectionSucceeds) +{ + auto daqInstance = SetupInstance(false); + GenericServer server(daqInstance); + server.start(); + + OpcUaEndpoint endpoint("opc.tcp://127.0.0.1:4840", "readerUser", "readerUserPass"); + auto client = std::make_shared(endpoint); + ASSERT_NO_THROW(client->connect()); + ASSERT_TRUE(client->isConnected()); + + server.stop(); +} + +TEST_F(SimpleServerTest, WrongCredentialsRejected) +{ + auto daqInstance = SetupInstance(false); + GenericServer server(daqInstance); + server.start(); + + OpcUaEndpoint endpoint("opc.tcp://127.0.0.1:4840", "adminUser", "wrongPassword"); + auto client = std::make_shared(endpoint); + ASSERT_THROW(client->connect(), std::exception); + + server.stop(); +} + +TEST_F(SimpleServerTest, DeviceNodeExistsInObjectsFolder) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + server.start(); + + OpcuaServerHelper helper; + helper.Init(); + + auto browseName = daqInstance.getRootDevice().getGlobalId().toStdString(); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + + auto deviceNodeId = helper.getChildNodeId(OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), browseName); + ASSERT_FALSE(deviceNodeId.isNull()); + + helper.Clear(); + server.stop(); +} + +TEST_F(SimpleServerTest, SignalNodesExistUnderDeviceNode) +{ + auto daqInstance = SetupInstance(); + const auto rootDevice = daqInstance.getRootDevice(); + const SizeT expectedCount = rootDevice.getSignalsRecursive(search::Any()).getCount(); + ASSERT_GT(expectedCount, 0u); + + GenericServer server(daqInstance); + server.start(); + + OpcuaServerHelper helper; + helper.Init(); + + auto browseName = rootDevice.getGlobalId().toStdString(); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + + auto deviceNodeId = helper.getChildNodeId(OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), browseName); + ASSERT_FALSE(deviceNodeId.isNull()); + + auto browseResult = helper.browseNode(deviceNodeId); + ASSERT_EQ(browseResult->results[0].referencesSize, expectedCount + 1); // +1 because of HasTypeDefinition + + helper.Clear(); + server.stop(); +} + +TEST_F(SimpleServerTest, DISABLED_Temp) { auto daqInstance = SetupInstance(); GenericServer server(daqInstance); From 8417424387f720f29e5cf3d9f9b36f2c3fc080ef Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 22 May 2026 18:04:38 +0200 Subject: [PATCH 05/15] simple server: switch to GenericServer, enable client tracking and server capabilities --- .../opcua_simple_server_impl.h | 7 +- .../src/CMakeLists.txt | 1 + .../tests/CMakeLists.txt | 1 + .../tests/test_opcua_simple_server_module.cpp | 51 ++++++ .../include/opcua_simple_objects/signal.h | 4 +- .../opcua_simple_objects/src/signal.cpp | 29 +++- .../opcua_simple_server/simple_server.h | 2 +- .../opcua_simple_server/src/simple_server.cpp | 150 +++++++++++------- 8 files changed, 180 insertions(+), 65 deletions(-) diff --git a/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h index c2eca402..fbf10dcb 100644 --- a/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h @@ -15,12 +15,13 @@ */ #pragma once +#include #include +#include #include #include #include -#include -#include +#include BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE @@ -40,7 +41,7 @@ class OpcUaSimpleServerImpl : public Server void onStopServer() override; static void populateDefaultConfigFromProvider(const ContextPtr& context, const PropertyObjectPtr& config); - daq::opcua::TmsServer server; + daq::opcua::GenericServer server; ContextPtr context; }; diff --git a/modules/opcua_simple_server_module/src/CMakeLists.txt b/modules/opcua_simple_server_module/src/CMakeLists.txt index 34f4803a..16b586de 100644 --- a/modules/opcua_simple_server_module/src/CMakeLists.txt +++ b/modules/opcua_simple_server_module/src/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(${OPENDAQ_SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) target_link_libraries(${LIB_NAME} PUBLIC ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuatms_server + PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_simple_server ) if (MSVC) diff --git a/modules/opcua_simple_server_module/tests/CMakeLists.txt b/modules/opcua_simple_server_module/tests/CMakeLists.txt index 550e84e2..5463e7d5 100644 --- a/modules/opcua_simple_server_module/tests/CMakeLists.txt +++ b/modules/opcua_simple_server_module/tests/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries(${TEST_APP} PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opend ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcuaclient ${OPENDAQ_SDK_TARGET_NAMESPACE}::opendaq_mocks ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_simple_server_test_utils ) add_test(NAME ${TEST_APP} diff --git a/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp b/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp index e5f8f60b..30271a09 100644 --- a/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp +++ b/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -217,3 +218,53 @@ TEST_F(OpcUaServerModuleTest, StopServer) serverPtr.stop(); ASSERT_THROW(client.connect(), OpcUaException); } + +TEST_F(OpcUaServerModuleTest, DeviceNodeExistsInObjectsFolder) +{ + auto daqInstance = CreateTestInstance(); + auto module = CreateModule(daqInstance.getContext()); + auto config = CreateServerConfig(daqInstance); + + auto serverPtr = module.createServer(DAQ_OPCUA_SIMPLE_SERVER_ID, daqInstance.getRootDevice(), config); + + test_helpers::OpcuaServerHelper helper; + helper.Init(); + + auto browseName = daqInstance.getRootDevice().getGlobalId().toStdString(); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + + auto deviceNodeId = helper.getChildNodeId(OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), browseName); + ASSERT_FALSE(deviceNodeId.isNull()); +} + +TEST_F(OpcUaServerModuleTest, SignalNodesExistUnderDeviceNode) +{ + auto daqInstance = CreateTestInstance(); + const auto rootDevice = daqInstance.getRootDevice(); + SizeT expectedCount = 0; + for (const auto& sig : rootDevice.getSignalsRecursive(search::Any())) + { + if (sig.getDescriptor().assigned()) + expectedCount++; + } + ASSERT_GT(expectedCount, 0u); + + auto module = CreateModule(rootDevice.getContext()); + auto config = CreateServerConfig(daqInstance); + auto serverPtr = module.createServer(DAQ_OPCUA_SIMPLE_SERVER_ID, daqInstance.getRootDevice(), config); + + test_helpers::OpcuaServerHelper helper; + helper.Init(); + + auto browseName = rootDevice.getGlobalId().toStdString(); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + + auto deviceNodeId = helper.getChildNodeId(OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), browseName); + ASSERT_FALSE(deviceNodeId.isNull()); + + auto browseResult = helper.browseNode(deviceNodeId); + + ASSERT_EQ(browseResult->results[0].referencesSize, expectedCount + 1); // +1 because of HasTypeDefinition + + helper.Clear(); +} diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h index ec6f84c2..38ab0033 100644 --- a/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h +++ b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h @@ -33,6 +33,8 @@ class SignalNode const PropertyObjectPtr& config = nullptr); ~SignalNode() = default; + static PropertyObjectPtr createDefaultConfig(); + void process(); protected: @@ -44,7 +46,7 @@ class SignalNode OpcUaNodeId variableNodeId; const uint16_t namespaceIndex = 1; - void addVariableNode(); + void addVariableNode(const PropertyObjectPtr& config); OpcUaNodeId convertSampleTypeToDataTypeId(const daq::SampleType sampleType) const; OpcUaVariant toVariant(const BaseObjectPtr& lastValue, SampleType sampleType) const; diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp index 63285581..eef4a857 100644 --- a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -1,6 +1,8 @@ #include #include #include +#include "coreobjects/property_factory.h" +#include "coreobjects/property_object_factory.h" #include "opcuaserver/opcuaaddnodeparams.h" #include "opcuaserver/opcuaserver.h" #include "opcuashared/opcuanodeid.h" @@ -16,6 +18,7 @@ std::unordered_map SignalNode::converterMap = {{SampleType {SampleType::UInt32, UA_TYPES_UINT32}, {SampleType::Int32, UA_TYPES_INT32}, {SampleType::UInt64, UA_TYPES_UINT64}, + {SampleType::Int64, UA_TYPES_INT64}, {SampleType::String, UA_TYPES_STRING}}; SignalNode::SignalNode(daq::opcua::OpcUaServerPtr server, @@ -27,10 +30,10 @@ SignalNode::SignalNode(daq::opcua::OpcUaServerPtr server, , parentNodeId(parentNodeId) { - addVariableNode(); + addVariableNode(config); } -void SignalNode::addVariableNode() +void SignalNode::addVariableNode(const PropertyObjectPtr& config) { OpcUaNodeId variableNodeId(namespaceIndex, signal.getGlobalId().toStdString()); AddVariableNodeParams params(variableNodeId, parentNodeId); @@ -45,8 +48,18 @@ void SignalNode::addVariableNode() params.setDataType(dataType); - auto browseName = signal.getGlobalId().toStdString(); - std::replace(browseName.begin(), browseName.end(), '/', '-'); + std::string browseName; + if (config.hasProperty("BrowseName")) + { + browseName = config.getPropertyValue("BrowseName").asPtr().toStdString(); + } + if (browseName.empty()) + { + browseName = signal.getGlobalId().toStdString(); + if (browseName.front() == '/') + browseName.erase(0, 1); + std::replace(browseName.begin(), browseName.end(), '/', '-'); + } params.setBrowseName(browseName); params.attr->displayName = UA_LOCALIZEDTEXT_ALLOC(DEFAULT_LOCALE, signal.getName().toStdString().c_str()); @@ -74,6 +87,14 @@ OpcUaNodeId SignalNode::convertSampleTypeToDataTypeId(const SampleType sampleTyp return OpcUaNodeId(); } +PropertyObjectPtr SignalNode::createDefaultConfig() +{ + auto config = PropertyObject(); + + config.addProperty(StringProperty("BrowseName", "")); + return config; +} + void SignalNode::process() { const auto lastValue = signal.getLastValue(); diff --git a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h index 89a7f1eb..e2d4e518 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h +++ b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h @@ -49,7 +49,7 @@ class GenericServer OpcUaNodeId rootDeviceNodeId; std::vector signalNodes; - // std::unordered_map registeredClientIds; + std::unordered_map registeredClientIds; uint64_t readingIntervalMs; std::thread readingThread; diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index 3153f810..c72117ef 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -6,6 +6,12 @@ #include #include +#define PROTOCOL_ID "OpenDAQOPCUASimple" +#define PROTOCOL_NAME "OpenDAQOPCUASimple" +#define LOGGER_COMPONENT_NAME "SimpleOPCUAServer" +#define PROTOCOL_PREFIX "daq.opcua.simple" +#define PROTOCOL_TYPE ProtocolType::Unknown + //using namespace daq::opcua; BEGIN_NAMESPACE_OPENDAQ_OPCUA @@ -49,52 +55,73 @@ void GenericServer::start() server = std::make_shared(false); server->setPort(opcUaPort); server->setAuthenticationProvider(context.getAuthenticationProvider()); - // server->setClientConnectedHandler( - // [this](const std::string& clientId) - // { - // const auto loggerComponent = context.getLogger().getOrAddComponent("SimpleOPCUAServer"); - // LOG_I("New client connected, ID: {}", clientId); - // SizeT clientNumber = 0; - // if (device.assigned() && !device.isRemoved()) - // { - // device.getInfo().asPtr(true).addConnectedClient( - // &clientNumber, - // ConnectedClientInfo("", ProtocolType::Configuration, "OpenDAQOPCUA", "", "")); - // } - // registeredClientIds.insert({clientId, clientNumber}); - // } - // ); - // server->setClientDisconnectedHandler( - // [this](const std::string& clientId) - // { - // if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) - // { - // const auto loggerComponent = context.getLogger().getOrAddComponent("SimpleOPCUAServer"); - // LOG_I("Client disconnected, ID: {}", clientId); - // if (device.assigned() && !device.isRemoved() && it->second != 0) - // { - // device.getInfo().asPtr(true).removeConnectedClient(it->second); - // } - // registeredClientIds.erase(it); - // } - // } - // ); - // server->setAllowBrowsingNodeCallback(TmsServerObject::allowBrowsingNodeCallback); - // server->setGetUserAccessLevelCallback(TmsServerObject::getUserAccessLevelCallback); - // server->setGetUserRightsMaskCallback(TmsServerObject::getUserRightsMaskCallback); - // server->setGetUserExecutableCallback(TmsServerObject::getUserExecutableCallback); + server->setClientConnectedHandler( + [this](const std::string& clientId) + { + const auto loggerComponent = context.getLogger().getOrAddComponent(LOGGER_COMPONENT_NAME); + LOG_I("New client connected, ID: {}", clientId); + SizeT clientNumber = 0; + if (device.assigned() && !device.isRemoved()) + { + device.getInfo().asPtr(true).addConnectedClient( + &clientNumber, + ConnectedClientInfo("", PROTOCOL_TYPE, PROTOCOL_NAME, "", "")); + } + registeredClientIds.insert({clientId, clientNumber}); + } + ); + server->setClientDisconnectedHandler( + [this](const std::string& clientId) + { + if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) + { + const auto loggerComponent = context.getLogger().getOrAddComponent(LOGGER_COMPONENT_NAME); + LOG_I("Client disconnected, ID: {}", clientId); + if (device.assigned() && !device.isRemoved() && it->second != 0) + { + device.getInfo().asPtr(true).removeConnectedClient(it->second); + } + registeredClientIds.erase(it); + } + } + ); + // TODO : set proper callbacks for access control + server->setAllowBrowsingNodeCallback([](UA_Server* server, + UA_AccessControl* ac, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext) { return true; }); + server->setGetUserAccessLevelCallback([](UA_Server* server, + UA_AccessControl* ac, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext) { return UA_Byte(UA_ACCESSLEVELMASK_READ); }); + server->setGetUserRightsMaskCallback([](UA_Server* server, + UA_AccessControl* ac, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext) { return UA_UInt32(0); }); + server->setGetUserExecutableCallback([](UA_Server* server, + UA_AccessControl* ac, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext) { return false; }); server->prepare(); createDeviceNode(); fillDeviceNode(); addSignalNodes(); - // auto serverCapability = ServerCapability("OpenDAQOPCUAConfiguration", "OpenDAQOPCUA", ProtocolType::Configuration); - // serverCapability.setPrefix("daq.opcua"); - // serverCapability.setConnectionType("TCP/IP"); - // serverCapability.setPort(opcUaPort); - // serverCapability.addProperty(StringProperty("Path", opcUaPath == "/" ? "" : opcUaPath)); - // info.asPtr(true).addServerCapability(serverCapability); + auto serverCapability = ServerCapability(PROTOCOL_ID, PROTOCOL_NAME, PROTOCOL_TYPE); + serverCapability.setPrefix(PROTOCOL_PREFIX); + serverCapability.setConnectionType("TCP/IP"); + serverCapability.setPort(opcUaPort); + serverCapability.addProperty(StringProperty("Path", opcUaPath == "/" ? "" : opcUaPath)); + info.asPtr(true).addServerCapability(serverCapability); server->start(); @@ -103,19 +130,19 @@ void GenericServer::start() void GenericServer::stop() { - // if (device.assigned() && !device.isRemoved()) - // { - // const auto info = device.getInfo(); - // const auto infoInternal = info.asPtr(); - // if (info.hasServerCapability("OpenDAQOPCUAConfiguration")) - // infoInternal.removeServerCapability("OpenDAQOPCUAConfiguration"); - // for (const auto& [_, clientNumber] : registeredClientIds) - // { - // if (clientNumber != 0) - // infoInternal.removeConnectedClient(clientNumber); - // } - // } - // registeredClientIds.clear(); + if (device.assigned() && !device.isRemoved()) + { + const auto info = device.getInfo(); + const auto infoInternal = info.asPtr(); + if (info.hasServerCapability(PROTOCOL_ID)) + infoInternal.removeServerCapability(PROTOCOL_ID); + for (const auto& [_, clientNumber] : registeredClientIds) + { + if (clientNumber != 0) + infoInternal.removeConnectedClient(clientNumber); + } + } + registeredClientIds.clear(); stopReadingThread(); if (server) @@ -148,7 +175,7 @@ void GenericServer::createDeviceNode() void GenericServer::fillDeviceNode() { - + // TODO } void GenericServer::addSignalNodes() @@ -156,7 +183,16 @@ void GenericServer::addSignalNodes() const auto sigList = device.getSignalsRecursive(search::Any()); for (const auto& sig : sigList) { - signalNodes.emplace_back(server, rootDeviceNodeId, sig); + auto config = simple_objects::SignalNode::createDefaultConfig(); + config.setPropertyValue("BrowseName", ""); + try { + simple_objects::SignalNode node(server, rootDeviceNodeId, sig, config); + signalNodes.push_back(std::move(node)); + } catch (const DaqException& e) { + const auto loggerComponent = context.getLogger().getOrAddComponent(LOGGER_COMPONENT_NAME); + LOG_E("Failed to create signal node for signal '{}', error: {}", sig.getName(), e.what()); + } + } } @@ -185,13 +221,15 @@ void GenericServer::readingLoop() readingCv.wait_until(lock, nextTimePoint, [this]() { return !readingRunning.load(); }); }; + auto prewPoint = std::chrono::steady_clock::now(); while (readingRunning) { - const auto nextTimePoint = std::chrono::steady_clock::now() + std::chrono::milliseconds(readingIntervalMs); + const auto nextTimePoint = prewPoint + std::chrono::milliseconds(readingIntervalMs); for (auto& signalNode : signalNodes) { signalNode.process(); } + prewPoint = nextTimePoint; interruptibleSleep(nextTimePoint); } } From b746b61098da63cc9378b3fff5ddcb612bc027d1 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Thu, 4 Jun 2026 18:54:39 +0200 Subject: [PATCH 06/15] opendaq_ref update --- opendaq_ref | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendaq_ref b/opendaq_ref index 881016eb..a910ec2a 100644 --- a/opendaq_ref +++ b/opendaq_ref @@ -1 +1 @@ -e6f13d80fa7c64697e5549fc1d897b03ad813415 +get-last-value-with-timestamp From 4d6b20f757ef89433dda95f73e25f3dc5e23ec21 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Thu, 4 Jun 2026 19:05:44 +0200 Subject: [PATCH 07/15] simple server: getLastVatue -> getLastValueWithTimestamp --- .../include/opcuaserver/opcuaserver.h | 2 ++ .../opcua/opcuaserver/src/opcuaserver.cpp | 5 ++++ .../opcua_simple_objects/src/signal.cpp | 23 ++++++++++++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h index c3ee8a6a..1ad9db06 100644 --- a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -128,6 +129,7 @@ class OpcUaServer final : public daq::utils::ThreadEx void setAccessLevel(const OpcUaNodeId& nodeId, UA_Byte accessLevel); void writeValue(const OpcUaNodeId& nodeId, const OpcUaVariant& var); + void writeDataValue(const OpcUaNodeId& nodeId, const OpcUaDataValue& value); OpcUaVariant readValue(const OpcUaNodeId& nodeId); OpcUaNodeId readDataType(const OpcUaNodeId& typeNodeId); diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp index 5ed6f975..d5c9531d 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -516,6 +516,11 @@ void OpcUaServer::writeValue(const OpcUaNodeId& nodeId, const OpcUaVariant& valu CheckStatusCodeException(UA_Server_writeValue(server, *nodeId, *value)); } +void OpcUaServer::writeDataValue(const OpcUaNodeId& nodeId, const OpcUaDataValue& value) +{ + CheckStatusCodeException(UA_Server_writeDataValue(server, *nodeId, *value)); +} + OpcUaVariant OpcUaServer::readValue(const OpcUaNodeId& nodeId) { OpcUaVariant value; diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp index eef4a857..2d6cb639 100644 --- a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -5,6 +5,7 @@ #include "coreobjects/property_object_factory.h" #include "opcuaserver/opcuaaddnodeparams.h" #include "opcuaserver/opcuaserver.h" +#include "opcuashared/opcuadatavalue.h" #include "opcuashared/opcuanodeid.h" BEGIN_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_OBJECTS @@ -97,7 +98,11 @@ PropertyObjectPtr SignalNode::createDefaultConfig() void SignalNode::process() { - const auto lastValue = signal.getLastValue(); + BaseObjectPtr lastValue; + const BaseObjectPtr timestamp = signal.getLastValueWithTimestamp(lastValue); + if (!lastValue.assigned()) + return; + auto sigDesc = signal.getDescriptor(); if (!sigDesc.assigned()) return; @@ -105,9 +110,21 @@ void SignalNode::process() const auto dataType = convertSampleTypeToDataTypeId(st); if (dataType.isNull()) return; - const auto variant = toVariant(lastValue, st); + + auto variant = toVariant(lastValue, st); + + OpcUaDataValue dataValue; + dataValue.getValue().value = variant.getDetachedValue(); + dataValue.getValue().hasValue = true; + + if (const auto tsInt = timestamp.asPtrOrNull(); tsInt.assigned()) + { + dataValue.getValue().hasSourceTimestamp = true; + dataValue.getValue().sourceTimestamp = OpcUaDataValue::fromUnixTimeUs(static_cast(static_cast(tsInt))); + } + try { - server->writeValue(variableNodeId, variant); + server->writeDataValue(variableNodeId, dataValue); } catch (OpcUaException& ex) { } From 22286ff43be26feb658adbb4fee22258da8fb199 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 12 Jun 2026 15:52:53 +0200 Subject: [PATCH 08/15] opendaq_ref update --- opendaq_ref | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendaq_ref b/opendaq_ref index a910ec2a..ba2906d0 100644 --- a/opendaq_ref +++ b/opendaq_ref @@ -1 +1 @@ -get-last-value-with-timestamp +main From 79f221bae41b9d32f0d8a116e38b331552be43a2 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 12 Jun 2026 16:40:19 +0200 Subject: [PATCH 09/15] unused arg --- .../libraries/opcuageneric/opcua_simple_objects/src/signal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp index 2d6cb639..bec8fa9f 100644 --- a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -125,7 +125,7 @@ void SignalNode::process() try { server->writeDataValue(variableNodeId, dataValue); - } catch (OpcUaException& ex) { + } catch (OpcUaException& /*ex*/) { } } From 4bc9a06fe959ba47d3ec435c013cac3ed50fbdf6 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 12 Jun 2026 16:56:20 +0200 Subject: [PATCH 10/15] include --- .../libraries/opcuageneric/opcua_simple_objects/src/signal.cpp | 1 + .../opcuageneric/opcua_simple_server/src/simple_server.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp index bec8fa9f..365bcaba 100644 --- a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -1,3 +1,4 @@ +#include #include #include #include diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index c72117ef..35fea0de 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -1,3 +1,4 @@ +#include #include #include #include From 59899f3f8a44431e37865ebacce33dc89cfe6363 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 19 Jun 2026 16:02:05 +0200 Subject: [PATCH 11/15] opendaq_ref update --- opendaq_ref | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendaq_ref b/opendaq_ref index ba2906d0..106e27d4 100644 --- a/opendaq_ref +++ b/opendaq_ref @@ -1 +1 @@ -main +other/workaround-missing-headers From d5b1d4e6982ecd43995231767b4a425cf70b6e40 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 19 Jun 2026 16:37:55 +0200 Subject: [PATCH 12/15] simple server: guard registeredClientIds with mutex to fix data race --- .../opcua_simple_server/simple_server.h | 1 + .../opcua_simple_server/src/simple_server.cpp | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h index e2d4e518..9b8127c5 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h +++ b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h @@ -50,6 +50,7 @@ class GenericServer std::vector signalNodes; std::unordered_map registeredClientIds; + std::mutex connectedClientsMutex; uint64_t readingIntervalMs; std::thread readingThread; diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index 35fea0de..d9e80274 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -59,6 +59,7 @@ void GenericServer::start() server->setClientConnectedHandler( [this](const std::string& clientId) { + std::lock_guard lock(connectedClientsMutex); const auto loggerComponent = context.getLogger().getOrAddComponent(LOGGER_COMPONENT_NAME); LOG_I("New client connected, ID: {}", clientId); SizeT clientNumber = 0; @@ -74,6 +75,7 @@ void GenericServer::start() server->setClientDisconnectedHandler( [this](const std::string& clientId) { + std::lock_guard lock(connectedClientsMutex); if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) { const auto loggerComponent = context.getLogger().getOrAddComponent(LOGGER_COMPONENT_NAME); @@ -131,19 +133,22 @@ void GenericServer::start() void GenericServer::stop() { - if (device.assigned() && !device.isRemoved()) { - const auto info = device.getInfo(); - const auto infoInternal = info.asPtr(); - if (info.hasServerCapability(PROTOCOL_ID)) - infoInternal.removeServerCapability(PROTOCOL_ID); - for (const auto& [_, clientNumber] : registeredClientIds) + std::lock_guard lock(connectedClientsMutex); + if (device.assigned() && !device.isRemoved()) { - if (clientNumber != 0) - infoInternal.removeConnectedClient(clientNumber); + const auto info = device.getInfo(); + const auto infoInternal = info.asPtr(); + if (info.hasServerCapability(PROTOCOL_ID)) + infoInternal.removeServerCapability(PROTOCOL_ID); + for (const auto& [_, clientNumber] : registeredClientIds) + { + if (clientNumber != 0) + infoInternal.removeConnectedClient(clientNumber); + } } + registeredClientIds.clear(); } - registeredClientIds.clear(); stopReadingThread(); if (server) From fb3ce050fe7e0201571a0027d4bc84452586c1ad Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 19 Jun 2026 16:53:11 +0200 Subject: [PATCH 13/15] simple server: store SignalNodes by unique_ptr to fix dangling nodeContext --- .../include/opcua_simple_server/simple_server.h | 3 ++- .../opcuageneric/opcua_simple_server/src/simple_server.cpp | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h index 9b8127c5..01ca7559 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h +++ b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -48,7 +49,7 @@ class GenericServer std::string opcUaPath = "/"; OpcUaNodeId rootDeviceNodeId; - std::vector signalNodes; + std::vector> signalNodes; std::unordered_map registeredClientIds; std::mutex connectedClientsMutex; diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index d9e80274..aed6a5a2 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -192,7 +192,7 @@ void GenericServer::addSignalNodes() auto config = simple_objects::SignalNode::createDefaultConfig(); config.setPropertyValue("BrowseName", ""); try { - simple_objects::SignalNode node(server, rootDeviceNodeId, sig, config); + auto node = std::make_unique(server, rootDeviceNodeId, sig, config); signalNodes.push_back(std::move(node)); } catch (const DaqException& e) { const auto loggerComponent = context.getLogger().getOrAddComponent(LOGGER_COMPONENT_NAME); @@ -233,7 +233,7 @@ void GenericServer::readingLoop() const auto nextTimePoint = prewPoint + std::chrono::milliseconds(readingIntervalMs); for (auto& signalNode : signalNodes) { - signalNode.process(); + signalNode->process(); } prewPoint = nextTimePoint; interruptibleSleep(nextTimePoint); From acdb980ba548738faaf3c9eca7934bc7d4a12cf9 Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 19 Jun 2026 17:03:49 +0200 Subject: [PATCH 14/15] simple server: guard start() against double-start Co-Authored-By: Claude Opus 4.8 --- .../opcua_simple_server_module_impl.h | 3 --- .../opcuageneric/opcua_simple_objects/src/signal.cpp | 2 +- .../opcua_simple_server/src/simple_server.cpp | 2 ++ .../tests/server_tests/test_simple_server.cpp | 9 +++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h index 41265112..65949bf2 100644 --- a/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h @@ -27,9 +27,6 @@ class OpcUaSimpleServerModule final : public Module DictPtr onGetAvailableServerTypes() override; ServerPtr onCreateServer(const StringPtr& serverType, const PropertyObjectPtr& serverConfig, const DevicePtr& rootDevice) override; - -private: - std::mutex sync; }; END_NAMESPACE_OPENDAQ_OPCUA_SIMPLE_SERVER_MODULE diff --git a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp index 365bcaba..aca7e2b8 100644 --- a/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -58,7 +58,7 @@ void SignalNode::addVariableNode(const PropertyObjectPtr& config) if (browseName.empty()) { browseName = signal.getGlobalId().toStdString(); - if (browseName.front() == '/') + if (!browseName.empty() && browseName.front() == '/') browseName.erase(0, 1); std::replace(browseName.begin(), browseName.end(), '/', '-'); } diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index aed6a5a2..c160b696 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -46,6 +46,8 @@ void GenericServer::setOpcUaPath(const std::string& path) void GenericServer::start() { + if (server) + DAQ_THROW_EXCEPTION(InvalidStateException, "Server is already started."); if (!device.assigned()) DAQ_THROW_EXCEPTION(InvalidStateException, "Device is not set."); if (!context.assigned()) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp index 4031b12d..cc9dcb3c 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp @@ -65,6 +65,15 @@ TEST_F(SimpleServerTest, DoubleStop) ASSERT_NO_THROW(server.stop()); } +TEST_F(SimpleServerTest, DoubleStart) +{ + auto daqInstance = SetupInstance(); + GenericServer server(daqInstance); + server.start(); + ASSERT_THROW(server.start(), std::exception); + ASSERT_NO_THROW(server.stop()); +} + TEST_F(SimpleServerTest, Connect) { auto daqInstance = SetupInstance(); From 554b6983214f539c4560f1f47962d63b0e446f0a Mon Sep 17 00:00:00 2001 From: Viacheslau Kalenikau Date: Fri, 19 Jun 2026 17:17:21 +0200 Subject: [PATCH 15/15] simple server: stop server before device-info cleanup in stop() Co-Authored-By: Claude Opus 4.8 --- .../opcua_simple_server/src/simple_server.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp index c160b696..0655c41e 100644 --- a/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -135,6 +135,11 @@ void GenericServer::start() void GenericServer::stop() { + stopReadingThread(); + + if (server) + server->stop(); + { std::lock_guard lock(connectedClientsMutex); if (device.assigned() && !device.isRemoved()) @@ -151,10 +156,6 @@ void GenericServer::stop() } registeredClientIds.clear(); } - stopReadingThread(); - - if (server) - server->stop(); server.reset(); signalNodes.clear();