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..fbf10dcb --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_impl.h @@ -0,0 +1,55 @@ +/* + * 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 +#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::GenericServer 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..65949bf2 --- /dev/null +++ b/modules/opcua_simple_server_module/include/opcua_simple_server_module/opcua_simple_server_module_impl.h @@ -0,0 +1,32 @@ +/* + * 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; +}; + +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..16b586de --- /dev/null +++ b/modules/opcua_simple_server_module/src/CMakeLists.txt @@ -0,0 +1,50 @@ +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 + PRIVATE ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_simple_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..5463e7d5 --- /dev/null +++ b/modules/opcua_simple_server_module/tests/CMakeLists.txt @@ -0,0 +1,25 @@ +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} + ${OPENDAQ_SDK_TARGET_NAMESPACE}::opcua_simple_server_test_utils +) + +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..30271a09 --- /dev/null +++ b/modules/opcua_simple_server_module/tests/test_opcua_simple_server_module.cpp @@ -0,0 +1,270 @@ +#include +#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); +} + +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/opendaq_ref b/opendaq_ref index 881016eb..106e27d4 100644 --- a/opendaq_ref +++ b/opendaq_ref @@ -1 +1 @@ -e6f13d80fa7c64697e5549fc1d897b03ad813415 +other/workaround-missing-headers 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 1b9002d5..8f037804 100644 --- a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -77,7 +78,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; @@ -142,6 +143,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); @@ -222,6 +224,7 @@ class OpcUaServer final : public daq::utils::ThreadEx OnClientDisconnectedCallback clientDisconnectedHandler; std::vector> pendingClientInfoFutures; std::mutex pendingClientInfoFuturesMutex; + 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 0ba5f923..713d8a13 100644 --- a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -21,8 +21,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); }; @@ -217,7 +218,8 @@ void OpcUaServer::prepareServer() config->nodeLifecycle.generateChildNodeId = generateChildId; prepareAccessControl(config); - addTmsTypes(server); + if (addCustomTypes) + addTmsTypes(server); eventManager->registerEvents(); } @@ -574,6 +576,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/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..38ab0033 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/include/opcua_simple_objects/signal.h @@ -0,0 +1,55 @@ +/* + * 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; + + static PropertyObjectPtr createDefaultConfig(); + + 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(const PropertyObjectPtr& config); + 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..aca7e2b8 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_objects/src/signal.cpp @@ -0,0 +1,184 @@ +#include +#include +#include +#include +#include "coreobjects/property_factory.h" +#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 + +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::Int64, UA_TYPES_INT64}, + {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(config); +} + +void SignalNode::addVariableNode(const PropertyObjectPtr& config) +{ + 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); + + std::string browseName; + if (config.hasProperty("BrowseName")) + { + browseName = config.getPropertyValue("BrowseName").asPtr().toStdString(); + } + if (browseName.empty()) + { + browseName = signal.getGlobalId().toStdString(); + if (!browseName.empty() && 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()); + 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(); +} + +PropertyObjectPtr SignalNode::createDefaultConfig() +{ + auto config = PropertyObject(); + + config.addProperty(StringProperty("BrowseName", "")); + return config; +} + +void SignalNode::process() +{ + BaseObjectPtr lastValue; + const BaseObjectPtr timestamp = signal.getLastValueWithTimestamp(lastValue); + if (!lastValue.assigned()) + return; + + auto sigDesc = signal.getDescriptor(); + if (!sigDesc.assigned()) + return; + const auto st = sigDesc.getSampleType(); + const auto dataType = convertSampleTypeToDataTypeId(st); + if (dataType.isNull()) + return; + + 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->writeDataValue(variableNodeId, dataValue); + } 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..01ca7559 --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/include/opcua_simple_server/simple_server.h @@ -0,0 +1,71 @@ +/* + * 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 +#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; + std::mutex connectedClientsMutex; + + 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/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..0655c41e --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/src/simple_server.cpp @@ -0,0 +1,246 @@ +#include +#include +#include +#include +#include +#include +#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 + +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) + , readingIntervalMs(100) +{ +} + +void GenericServer::setOpcUaPort(uint16_t port) +{ + this->opcUaPort = port; +} + +void GenericServer::setOpcUaPath(const std::string& path) +{ + this->opcUaPath = 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()) + 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) + { + std::lock_guard lock(connectedClientsMutex); + 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) + { + std::lock_guard lock(connectedClientsMutex); + 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(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(); + + startReadingThread(); +} + +void GenericServer::stop() +{ + stopReadingThread(); + + if (server) + server->stop(); + + { + std::lock_guard lock(connectedClientsMutex); + 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(); + } + + server.reset(); + signalNodes.clear(); +} + +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() +{ + // TODO +} + +void GenericServer::addSignalNodes() +{ + const auto sigList = device.getSignalsRecursive(search::Any()); + for (const auto& sig : sigList) + { + auto config = simple_objects::SignalNode::createDefaultConfig(); + config.setPropertyValue("BrowseName", ""); + try { + 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); + LOG_E("Failed to create signal node for signal '{}', error: {}", sig.getName(), e.what()); + } + + } +} + +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(); }); + }; + + auto prewPoint = std::chrono::steady_clock::now(); + while (readingRunning) + { + const auto nextTimePoint = prewPoint + std::chrono::milliseconds(readingIntervalMs); + for (auto& signalNode : signalNodes) + { + signalNode->process(); + } + prewPoint = nextTimePoint; + interruptibleSleep(nextTimePoint); + } + } + +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..cc9dcb3c --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/server_tests/test_simple_server.cpp @@ -0,0 +1,212 @@ +#include +#include +#include +#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, 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(); + GenericServer server(daqInstance); + ASSERT_NO_THROW(server.start()); + 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, 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(); + GenericServer server(daqInstance); + server.start(); + + auto client = OpcuaServerHelper::CreateAndConnectTestClient(); + ASSERT_TRUE(client->isConnected()); +} + +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); + 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/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..5517f4cc --- /dev/null +++ b/shared/libraries/opcuageneric/opcua_simple_server/tests/test_utils/test_helpers.h @@ -0,0 +1,114 @@ +/* + * 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("daqmock://phys_device"); + return instance; + } +}