diff --git a/external/opcua_daq_types/CMakeLists.txt b/external/opcua_daq_types/CMakeLists.txt new file mode 100644 index 0000000..6b0d8de --- /dev/null +++ b/external/opcua_daq_types/CMakeLists.txt @@ -0,0 +1,220 @@ +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcua_daq_types VERSION 1.0.0 DESCRIPTION "DAQ types" LANGUAGES C) + +option(HBK_NODESET "Adds an HBK Nodeset on top of the DAQ ESP spec" ON) + +# Define empty list of nodeset dependencies +list(APPEND OPC_UA_NODESET_DEPENDENCIES) +set(OPCUA_NODESET_NAMES) +set(OPCUA_NODESET_URIS) + + +# +# Fetch the openDAQ OPC-UA Companion Specification NodeSet +# +set(daq_spec_REQUIREDVERSION "3.0.2") +get_custom_fetch_content_params(daqspec FC_PARAMS) + +FetchContent_Declare(daqspec + GIT_REPOSITORY https://github.com/openDAQ/opc-ua-companion-spec.git + GIT_TAG v${daq_spec_REQUIREDVERSION} + ${FC_PARAMS} +) +FetchContent_GetProperties(daqspec) +if(NOT daqspec_POPULATED) + message(STATUS "Fetching daq specification ${daq_spec_REQUIREDVERSION}...") + FetchContent_MakeAvailable(daqspec) + set(COMPANION_SPECIFICATIONS_DIRPREFIX "${daqspec_SOURCE_DIR}/opendaq") +endif() + +# python interpreter is required for the code genaration process +find_package(Python3 REQUIRED) +set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) + +set(DI_NAME_SPACE_INDEX 2) +set(NAME_SPACE_INDEX ${DI_NAME_SPACE_INDEX}) +set(GENERATE_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/open62541") + +# Define some empty list necessary for code generation +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS) +list(APPEND OPC_UA_NODESET_SOURCES) +list(APPEND OPC_UA_NODESET_HEADERS) + +# +# Generate DI nodeset +# +message(STATUS "Generating DI nodeset...") +ua_generate_nodeset_and_datatypes( + NAME "di" + TARGET_PREFIX "${PROJECT_NAME}" + IMPORT_BSD "UA_TYPES#${open62541_NODESET_DIR}/Schema/Opc.Ua.Types.bsd" + FILE_CSV "${open62541_NODESET_DIR}/DI/OpcUaDiModel.csv" + FILE_BSD "${open62541_NODESET_DIR}/DI/Opc.Ua.Di.Types.bsd" + OUTPUT_DIR "${GENERATE_OUTPUT_DIR}" + NAMESPACE_MAP "${NAME_SPACE_INDEX}:http://opcfoundation.org/UA/DI/" + FILE_NS "${open62541_NODESET_DIR}/DI/Opc.Ua.Di.NodeSet2.xml" + INTERNAL +) +list(APPEND OPC_UA_NODESET_DEPENDENCIES ${PROJECT_NAME}-ns-di) +list(APPEND OPC_UA_NODESET_SOURCES ${UA_NODESET_DI_SOURCES} ${UA_TYPES_DI_SOURCES}) +list(APPEND OPC_UA_NODESET_HEADERS ${UA_NODESET_DI_HEADERS} ${UA_TYPES_DI_HEADERS} ${GENERATE_OUTPUT_DIR}/di_nodeids.h) +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS NAMESPACE_DI=${NAME_SPACE_INDEX}) +list(APPEND OPCUA_NODESET_NAMES "di") +list(APPEND OPCUA_NODESET_URIS "http://opcfoundation.org/UA/DI/") + +MATH(EXPR NAME_SPACE_INDEX "${NAME_SPACE_INDEX}+1") + + +# +# Generate DAQ Base Types (BT) nodeset if needed +# +message(STATUS "Generating DAQ BT nodeset...") +ua_generate_nodeset_and_datatypes( + NAME "daqbt" + TARGET_PREFIX "${PROJECT_NAME}" + IMPORT_BSD "UA_TYPES#${open62541_NODESET_DIR}/Schema/Opc.Ua.Types.bsd" + FILE_CSV "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.NodeIds.csv" + FILE_BSD "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.Types.bsd" + OUTPUT_DIR "${GENERATE_OUTPUT_DIR}" + NAMESPACE_MAP "${NAME_SPACE_INDEX}:https://docs.opendaq.io/specifications/opc-ua/daq/bt" + FILE_NS "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.NodeSet2.xml" + DEPENDS "di" + INTERNAL +) +list(APPEND OPC_UA_NODESET_DEPENDENCIES ${PROJECT_NAME}-ns-daqbt) +list(APPEND OPC_UA_NODESET_SOURCES ${UA_NODESET_DAQBT_SOURCES} ${UA_TYPES_DAQBT_SOURCES}) +list(APPEND OPC_UA_NODESET_HEADERS ${UA_NODESET_DAQBT_HEADERS} ${UA_TYPES_DAQBT_HEADERS} ${GENERATE_OUTPUT_DIR}/daqbt_nodeids.h) +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS NAMESPACE_DAQBT=${NAME_SPACE_INDEX}) +list(APPEND OPCUA_NODESET_NAMES "daqbt") +list(APPEND OPCUA_NODESET_URIS "https://docs.opendaq.io/specifications/opc-ua/daq/bt") +MATH(EXPR NAME_SPACE_INDEX "${NAME_SPACE_INDEX}+1") + +# +# Generate DAQ Basic Signal Processing (BSP) nodeset if needed +# +message(STATUS "Generating DAQ BSP nodeset...") +ua_generate_nodeset_and_datatypes( + NAME "daqbsp" + TARGET_PREFIX "${PROJECT_NAME}" + IMPORT_BSD "UA_TYPES#${open62541_NODESET_DIR}/Schema/Opc.Ua.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBT#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.Types.bsd" + FILE_CSV "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bsp.NodeIds.csv" + FILE_BSD "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bsp.Types.bsd" + OUTPUT_DIR "${GENERATE_OUTPUT_DIR}" + NAMESPACE_MAP "${NAME_SPACE_INDEX}:https://docs.opendaq.io/specifications/opc-ua/daq/bsp" + FILE_NS "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bsp.NodeSet2.xml" + DEPENDS "daqbt" + INTERNAL +) +list(APPEND OPC_UA_NODESET_DEPENDENCIES ${PROJECT_NAME}-ns-daqbsp) +list(APPEND OPC_UA_NODESET_SOURCES ${UA_NODESET_DAQBSP_SOURCES} ${UA_TYPES_DAQBSP_SOURCES}) +list(APPEND OPC_UA_NODESET_HEADERS ${UA_NODESET_DAQBSP_HEADERS} ${UA_TYPES_DAQBSP_HEADERS} ${GENERATE_OUTPUT_DIR}/daqbsp_nodeids.h) +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS NAMESPACE_DAQBSP=${NAME_SPACE_INDEX}) +list(APPEND OPCUA_NODESET_NAMES "daqbsp") +list(APPEND OPCUA_NODESET_URIS "https://docs.opendaq.io/specifications/opc-ua/daq/bsp") +MATH(EXPR NAME_SPACE_INDEX "${NAME_SPACE_INDEX}+1") + +# +# Generate DAQ Device nodeset if needed +# +message(STATUS "Generating DAQ Device nodeset...") +ua_generate_nodeset_and_datatypes( + NAME "daqdevice" + TARGET_PREFIX "${PROJECT_NAME}" + IMPORT_BSD "UA_TYPES#${open62541_NODESET_DIR}/Schema/Opc.Ua.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBT#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBSP#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bsp.Types.bsd" + FILE_CSV "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Device.NodeIds.csv" + FILE_BSD "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Device.Types.bsd" + OUTPUT_DIR "${GENERATE_OUTPUT_DIR}" + NAMESPACE_MAP "${NAME_SPACE_INDEX}:https://docs.opendaq.io/specifications/opc-ua/daq/device" + FILE_NS "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Device.NodeSet2.xml" + DEPENDS "daqbsp" + INTERNAL +) +list(APPEND OPC_UA_NODESET_DEPENDENCIES ${PROJECT_NAME}-ns-daqdevice) +list(APPEND OPC_UA_NODESET_SOURCES ${UA_NODESET_DAQDEVICE_SOURCES} ${UA_TYPES_DAQDEVICE_SOURCES}) +list(APPEND OPC_UA_NODESET_HEADERS ${UA_NODESET_DAQDEVICE_HEADERS} ${UA_TYPES_DAQDEVICE_HEADERS} ${GENERATE_OUTPUT_DIR}/daqdevice_nodeids.h) +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS NAMESPACE_DAQDEVICE=${NAME_SPACE_INDEX}) +list(APPEND OPCUA_NODESET_NAMES "daqdevice") +list(APPEND OPCUA_NODESET_URIS "https://docs.opendaq.io/specifications/opc-ua/daq/device") +MATH(EXPR NAME_SPACE_INDEX "${NAME_SPACE_INDEX}+1") + +# +# Generate DAQ ESP nodeset if needed +# +message(STATUS "Generating DAQ ESP nodeset...") +ua_generate_nodeset_and_datatypes( + NAME "daqesp" + TARGET_PREFIX "${PROJECT_NAME}" + IMPORT_BSD "UA_TYPES#${open62541_NODESET_DIR}/Schema/Opc.Ua.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBT#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBSP#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bsp.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQDEVICE#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Device.Types.bsd" + FILE_CSV "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Esp.NodeIds.csv" + FILE_BSD "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Esp.Types.bsd" + OUTPUT_DIR "${GENERATE_OUTPUT_DIR}" + NAMESPACE_MAP "${NAME_SPACE_INDEX}:https://docs.opendaq.io/specifications/opc-ua/daq/esp" + FILE_NS "${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Esp.NodeSet2.xml" + DEPENDS "daqdevice" + INTERNAL +) +list(APPEND OPC_UA_NODESET_DEPENDENCIES ${PROJECT_NAME}-ns-daqesp) +list(APPEND OPC_UA_NODESET_SOURCES ${UA_NODESET_DAQESP_SOURCES} ${UA_TYPES_DAQESP_SOURCES}) +list(APPEND OPC_UA_NODESET_HEADERS ${UA_NODESET_DAQESP_HEADERS} ${UA_TYPES_DAQESP_HEADERS} ${GENERATE_OUTPUT_DIR}/daqesp_nodeids.h) +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS NAMESPACE_DAQESP=${NAME_SPACE_INDEX}) +list(APPEND OPCUA_NODESET_NAMES "daqesp") +list(APPEND OPCUA_NODESET_URIS "https://docs.opendaq.io/specifications/opc-ua/daq/esp") +MATH(EXPR NAME_SPACE_INDEX "${NAME_SPACE_INDEX}+1") + +# Add Vendor Specs if wanted +if(HBK_NODESET) + include(hbk/CMakeLists.txt) +endif() +# +# Create custom target that will also execute the node compilations (because of the ALL keyword below) +# +list(LENGTH OPC_UA_NODESET_DEPENDENCIES OPC_UA_NODESET_DEPENDENCY_LENGTH) + +set(OPCUA_NODESET_INCLUDES "") +set(OPCUA_ENDPOINT_REGISTRATIONS "") +set(LOOP_RUN_INDEX 0) +foreach(NODESET_NAME ${OPCUA_NODESET_NAMES}) + list(GET OPCUA_NODESET_URIS ${LOOP_RUN_INDEX} OPCUA_NODESET_URI) + set(OPCUA_NODESET_INCLUDES "${OPCUA_NODESET_INCLUDES}#include\n") + string(TOUPPER ${NODESET_NAME} OPCUA_NODESET_NAME_UPPERCASE) + if(LOOP_RUN_INDEX EQUAL 0) + set(OPCUA_ENDPOINT_REGISTRATIONS "${OPCUA_ENDPOINT_REGISTRATIONS} if(nameSpace == \"${OPCUA_NODESET_URI}\")\n endpoint.registerCustomTypes(UA_TYPES_${OPCUA_NODESET_NAME_UPPERCASE}_COUNT, UA_TYPES_${OPCUA_NODESET_NAME_UPPERCASE});\n") + else() + set(OPCUA_ENDPOINT_REGISTRATIONS "${OPCUA_ENDPOINT_REGISTRATIONS} else if(nameSpace == \"${OPCUA_NODESET_URI}\")\n endpoint.registerCustomTypes(UA_TYPES_${OPCUA_NODESET_NAME_UPPERCASE}_COUNT, UA_TYPES_${OPCUA_NODESET_NAME_UPPERCASE});\n") + endif() + MATH(EXPR LOOP_RUN_INDEX "${LOOP_RUN_INDEX}+1") +endforeach() + +message(STATUS "Generating opcua nodeset include methods at ${GENERATE_OUTPUT_DIR}/daq_opcua_nodesets.h") +configure_file("daq_opcua_nodesets.h.in" ${GENERATE_OUTPUT_DIR}/daq_opcua_nodesets.h) +list(APPEND OPC_UA_NODESET_HEADERS ${GENERATE_OUTPUT_DIR}/daq_opcua_nodesets.h) + +add_library(${PROJECT_NAME} + STATIC + ${OPC_UA_NODESET_SOURCES} + ${OPC_UA_NODESET_HEADERS} +) + +add_library(${SDK_TARGET_NAMESPACE}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_link_libraries(${PROJECT_NAME} PUBLIC $) + +# Make sure that generation is run before but don't link libraries +add_dependencies(${PROJECT_NAME} ${OPC_UA_NODESET_DEPENDENCIES}) + +target_compile_definitions(${PROJECT_NAME} PUBLIC ${OPC_UA_NODESET_COMPILE_FLAGS}) + +# Ignore warnings defined in "external/bbopen62541" +target_compile_options(${PROJECT_NAME} PRIVATE ${OPEN62541_DISABLED_WARNINGS}) + +# Let libraries linking to us know where to find the generated headers +target_include_directories(${PROJECT_NAME} INTERFACE + $ + $ +) diff --git a/external/opcua_daq_types/daq_opcua_nodesets.h.in b/external/opcua_daq_types/daq_opcua_nodesets.h.in new file mode 100644 index 0000000..e382a23 --- /dev/null +++ b/external/opcua_daq_types/daq_opcua_nodesets.h.in @@ -0,0 +1,15 @@ +#pragma once +#include +@OPCUA_NODESET_INCLUDES@ + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +static void registerDaqTypes(OpcUaEndpoint& endpoint, ListPtr nameSpaces) +{ + for (auto nameSpace : nameSpaces) + { +@OPCUA_ENDPOINT_REGISTRATIONS@ + } +} + +END_NAMESPACE_OPENDAQ_OPCUA \ No newline at end of file diff --git a/external/opcua_daq_types/hbk/001-add-default-values-for-double-variables.patch b/external/opcua_daq_types/hbk/001-add-default-values-for-double-variables.patch new file mode 100644 index 0000000..59898c4 --- /dev/null +++ b/external/opcua_daq_types/hbk/001-add-default-values-for-double-variables.patch @@ -0,0 +1,144 @@ +diff --git a/opendaq/hbk/Opc.Ua.Daq.Vendor.Hbk.NodeSet2.xml b/opendaq/hbk/Opc.Ua.Daq.Vendor.Hbk.NodeSet2.xml +index fec43da..61474e8 100644 +--- a/opendaq/hbk/Opc.Ua.Daq.Vendor.Hbk.NodeSet2.xml ++++ b/opendaq/hbk/Opc.Ua.Daq.Vendor.Hbk.NodeSet2.xml +@@ -3596,6 +3596,9 @@ + i=78 + ns=1;i=5127 + ++ ++ 0 ++ + + + EngineeringUnits +@@ -3687,6 +3690,9 @@ + ns=1;i=7162 + ns=1;i=5127 + ++ ++ 0 ++ + + + DefaultValue +@@ -5103,6 +5109,9 @@ + ns=2;i=2004 + ns=1;i=5013 + ++ ++ 0 ++ + + + DefaultValue +@@ -5203,6 +5212,9 @@ + ns=2;i=2004 + ns=1;i=5013 + ++ ++ 0 ++ + + + DefaultValue +@@ -6186,6 +6198,9 @@ + i=78 + ns=1;i=5134 + ++ ++ 0 ++ + + + EngineeringUnits +@@ -6277,6 +6292,9 @@ + ns=1;i=7202 + ns=1;i=5134 + ++ ++ 0 ++ + + + DefaultValue +@@ -7926,6 +7944,9 @@ + i=80 + ns=1;i=1003 + ++ ++ 0 ++ + + + DefaultValue +@@ -9496,6 +9517,9 @@ + i=80 + ns=1;i=6072 + ++ ++ 0 ++ + + + DefaultValue +@@ -9729,6 +9753,9 @@ + ns=1;i=6912 + ns=1;i=1005 + ++ ++ 0 ++ + + + DefaultValue +@@ -9821,6 +9848,9 @@ + ns=1;i=6913 + ns=1;i=1005 + ++ ++ 0 ++ + + + DefaultValue +@@ -9868,6 +9898,9 @@ + i=78 + ns=1;i=1048 + ++ ++ 0 ++ + + + EngineeringUnits +@@ -10193,6 +10226,9 @@ + ns=2;i=2004 + ns=1;i=1048 + ++ ++ 0 ++ + + + DefaultValue +@@ -16552,6 +16588,9 @@ + ns=1;i=7455 + ns=1;i=5098 + ++ ++ 0 ++ + + + EngineeringUnits +@@ -16643,6 +16682,9 @@ + ns=2;i=2004 + ns=1;i=5098 + ++ ++ 0 ++ + + + DefaultValue diff --git a/external/opcua_daq_types/hbk/CMakeLists.txt b/external/opcua_daq_types/hbk/CMakeLists.txt new file mode 100644 index 0000000..28441bd --- /dev/null +++ b/external/opcua_daq_types/hbk/CMakeLists.txt @@ -0,0 +1,41 @@ +set(daq_hbk_REQUIREDVERSION "3.0.12") + +message(STATUS "Fetching hbk daq specification ${daq_hbk_REQUIREDVERSION}...") + +opendaq_dependency( + NAME daqhbkspec + REQUIRED_VERSION ${daq_hbk_REQUIREDVERSION} + GIT_REPOSITORY https://github.com/hbkworld/opc-ua-specs.git + GIT_REF v${daq_hbk_REQUIREDVERSION} + PATCH_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/hbk/001-add-default-values-for-double-variables.patch +) + +list(APPEND VENDOR_COMPANION_SPECIFICATION_DIRPREFIX "${daqhbkspec_SOURCE_DIR}/opendaq/hbk") + +message(STATUS "Generating DAQ Hbk nodeset...") + +ua_generate_nodeset_and_datatypes( + NAME "daqhbk" + TARGET_PREFIX "${PROJECT_NAME}" + IMPORT_BSD "UA_TYPES#${open62541_NODESET_DIR}/Schema/Opc.Ua.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBT#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bt.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQBSP#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Bsp.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQDEVICE#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Device.Types.bsd" + IMPORT_BSD "UA_TYPES_DAQESP#${COMPANION_SPECIFICATIONS_DIRPREFIX}/Opc.Ua.Daq.Esp.Types.bsd" + FILE_CSV "${VENDOR_COMPANION_SPECIFICATION_DIRPREFIX}/Opc.Ua.Daq.Vendor.Hbk.NodeIds.csv" + FILE_BSD "${VENDOR_COMPANION_SPECIFICATION_DIRPREFIX}/Opc.Ua.Daq.Vendor.Hbk.Types.bsd" + OUTPUT_DIR "${GENERATE_OUTPUT_DIR}" + NAMESPACE_MAP "${NAME_SPACE_INDEX}:https://docs.opendaq.io/specifications/opc-ua/daq/vendor/hbk" + FILE_NS "${VENDOR_COMPANION_SPECIFICATION_DIRPREFIX}/Opc.Ua.Daq.Vendor.Hbk.NodeSet2.xml" + DEPENDS "daqesp" + INTERNAL +) + +list(APPEND OPC_UA_NODESET_DEPENDENCIES ${PROJECT_NAME}-ns-daqhbk) +list(APPEND OPC_UA_NODESET_SOURCES ${UA_NODESET_DAQHBK_SOURCES} ${UA_TYPES_DAQHBK_SOURCES}) +list(APPEND OPC_UA_NODESET_HEADERS ${UA_NODESET_DAQHBK_HEADERS} ${UA_TYPES_DAQHBK_HEADERS} ${GENERATE_OUTPUT_DIR}/daqhbk_nodeids.h) +list(APPEND OPC_UA_NODESET_COMPILE_FLAGS NAMESPACE_DAQHBK=${NAME_SPACE_INDEX}) +list(APPEND OPCUA_NODESET_NAMES "daqhbk") +list(APPEND OPCUA_NODESET_URIS "https://docs.opendaq.io/specifications/opc-ua/daq/vendor/hbk") +MATH(EXPR NAME_SPACE_INDEX "${NAME_SPACE_INDEX}+1") \ No newline at end of file diff --git a/external/open62541/CMakeLists.txt b/external/open62541/CMakeLists.txt new file mode 100644 index 0000000..1081547 --- /dev/null +++ b/external/open62541/CMakeLists.txt @@ -0,0 +1,83 @@ +if (UNIX AND CMAKE_COMPILER_IS_CLANGXX) + # Round about way of disabling clang sanitizers as open62541 doesn't have a way to do that properly + set(UA_BUILD_UNIT_TESTS OFF CACHE BOOL "Build the unit tests") + set(UA_ENABLE_UNIT_TESTS_MEMCHECK ON CACHE BOOL "Use Valgrind (Linux) or DrMemory (Windows) to detect memory leaks when running the unit tests") +endif() + +set(UA_NAMESPACE_ZERO FULL CACHE STRING "" FORCE) +set(UA_ENABLE_TYPEDESCRIPTION ON CACHE STRING "" FORCE) +set(UA_ENABLE_STATUSCODE_DESCRIPTIONS ON CACHE STRING "" FORCE) +set(UA_MULTITHREADING 100 CACHE STRING "" FORCE) + +if (CMAKE_COMPILER_IS_GNUCXX AND NOT DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF) +endif() + +set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL YES) + +opendaq_dependency( + NAME open62541 + REQUIRED_VERSION 1.3.6 + GIT_REPOSITORY https://github.com/openDAQ/open62541.git + GIT_REF v1.3.6-opendaq-4 + GIT_SUBMODULES "" + EXPECT_TARGET open62541::open62541 +) + +if (open62541_FETCHED) + + target_compile_definitions(open62541 PRIVATE UA_ENABLE_AMALGAMATION=OFF) + + if (WIN32) + target_compile_definitions(open62541 PUBLIC UA_ARCHITECTURE="win32") + endif() + + # dont treat warnings as errors + + set(OPEN62541_DISABLED_WARNINGS "-Wno-error") + if(MSVC) + set(OPEN62541_DISABLED_WARNINGS /WX- /wd4100 /wd4214 /wd4200 /wd4204 /wd4057 /wd4701 /wd4133) + endif() + + target_compile_options(open62541 PRIVATE ${OPEN62541_DISABLED_WARNINGS}) + target_compile_options(open62541-plugins PRIVATE ${OPEN62541_DISABLED_WARNINGS}) + target_compile_options(open62541-object PRIVATE ${OPEN62541_DISABLED_WARNINGS}) + + set(OPEN62541_DISABLED_WARNINGS ${OPEN62541_DISABLED_WARNINGS} PARENT_SCOPE) + + # fix library folders + + set_target_properties(doc doc_latex doc_pdf PROPERTIES FOLDER "${CMAKE_FOLDER}/doc") + + set(LIB_TARGETS + open62541-plugins + open62541 + open62541-object + ) + + foreach(LIB_TARGET ${LIB_TARGETS}) + set_target_properties(${LIB_TARGET} PROPERTIES FOLDER "${CMAKE_FOLDER}/lib") + endforeach() + + if (UA_ENABLE_AMALGAMATION) + set_target_properties(open62541-amalgamation-header PROPERTIES FOLDER "${CMAKE_FOLDER}/lib") + set_target_properties(open62541-amalgamation-source PROPERTIES FOLDER "${CMAKE_FOLDER}/lib") + endif() + + set(GENERATOR_TARGETS + open62541-generator-namespace + open62541-generator-statuscode + open62541-generator-transport + open62541-generator-types + open62541-code-generation + ) + + foreach(TARGET_NAME ${GENERATOR_TARGETS}) + set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "${CMAKE_FOLDER}/generators") + endforeach() + + set(open62541_TOOLS_DIR "${open62541_SOURCE_DIR}/tools" PARENT_SCOPE) + set(open62541_NODESET_DIR "${open62541_SOURCE_DIR}/deps/ua-nodeset" PARENT_SCOPE) + include("${open62541_BINARY_DIR}/open62541Macros.cmake") + +endif() diff --git a/modules/opcua_client_module/CMakeLists.txt b/modules/opcua_client_module/CMakeLists.txt new file mode 100644 index 0000000..4a134c4 --- /dev/null +++ b/modules/opcua_client_module/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(ClientModule VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES C CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/modules/opcua_client_module/include/opcua_client_module/common.h b/modules/opcua_client_module/include/opcua_client_module/common.h new file mode 100644 index 0000000..1a02ecf --- /dev/null +++ b/modules/opcua_client_module/include/opcua_client_module/common.h @@ -0,0 +1,21 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#define BEGIN_NAMESPACE_OPENDAQ_OPCUA_CLIENT_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(opcua_client_module) +#define END_NAMESPACE_OPENDAQ_OPCUA_CLIENT_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/modules/opcua_client_module/include/opcua_client_module/module_dll.h b/modules/opcua_client_module/include/opcua_client_module/module_dll.h new file mode 100644 index 0000000..a2cc14e --- /dev/null +++ b/modules/opcua_client_module/include/opcua_client_module/module_dll.h @@ -0,0 +1,20 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +DECLARE_MODULE_EXPORTS(OpcUaClientModule) diff --git a/modules/opcua_client_module/include/opcua_client_module/opcua_client_module_impl.h b/modules/opcua_client_module/include/opcua_client_module/opcua_client_module_impl.h new file mode 100644 index 0000000..aca54a9 --- /dev/null +++ b/modules/opcua_client_module/include/opcua_client_module/opcua_client_module_impl.h @@ -0,0 +1,49 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_CLIENT_MODULE + +class OpcUaClientModule final : public Module +{ +public: + OpcUaClientModule(ContextPtr context); + + ListPtr onGetAvailableDevices() override; + DictPtr onGetAvailableDeviceTypes() override; + DevicePtr onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) override; + bool acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + Bool onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) override; + +private: + StringPtr formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config, std::string& host, int& port, std::string& hostType); + static DeviceTypePtr createDeviceType(); + static PropertyObjectPtr createDefaultConfig(); + static PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& config); + static DeviceInfoPtr populateDiscoveredDevice(const discovery::MdnsDiscoveredDevice& discoveredDevice); + discovery::DiscoveryClient discoveryClient; + + std::mutex sync; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_CLIENT_MODULE diff --git a/modules/opcua_client_module/src/CMakeLists.txt b/modules/opcua_client_module/src/CMakeLists.txt new file mode 100644 index 0000000..f665a9a --- /dev/null +++ b/modules/opcua_client_module/src/CMakeLists.txt @@ -0,0 +1,43 @@ +set(LIB_NAME opcua_client_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + module_dll.h + opcua_client_module_impl.h +) + +set(SRC_Srcs module_dll.cpp + opcua_client_module_impl.cpp +) + +prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/opcua_client_module_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + module_dll.cpp + opcua_client_module_impl.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} +) +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +endif() + +target_link_libraries(${LIB_NAME} PUBLIC daq::opendaq + PRIVATE daq::discovery + daq::opcuatms_client +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +create_version_header(${LIB_NAME}) + diff --git a/modules/opcua_client_module/src/module_dll.cpp b/modules/opcua_client_module/src/module_dll.cpp new file mode 100644 index 0000000..71987e6 --- /dev/null +++ b/modules/opcua_client_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::opcua_client_module; + +DEFINE_MODULE_EXPORTS(OpcUaClientModule) + diff --git a/modules/opcua_client_module/src/opcua_client_module_impl.cpp b/modules/opcua_client_module/src/opcua_client_module_impl.cpp new file mode 100644 index 0000000..de589c3 --- /dev/null +++ b/modules/opcua_client_module/src/opcua_client_module_impl.cpp @@ -0,0 +1,313 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_CLIENT_MODULE + +static const char* DaqOpcUaDeviceTypeId = "OpenDAQOPCUAConfiguration"; +static const char* DaqOpcUaDevicePrefix = "daq.opcua"; +static const char* OpcUaScheme = "opc.tcp"; + +static const std::regex RegexIpv6Hostname(R"(^(.+://)?(\[[a-fA-F0-9:]+(?:\%[a-zA-Z0-9_\.-~]+)?\])(?::(\d+))?(/.*)?$)"); +static const std::regex RegexIpv4Hostname(R"(^(.+://)?([^:/\s]+)(?::(\d+))?(/.*)?$)"); + +using namespace discovery; +using namespace daq::opcua; + +OpcUaClientModule::OpcUaClientModule(ContextPtr context) + : Module("OpenDAQOPCUAClientModule", + daq::VersionInfo(OPCUA_CLIENT_MODULE_MAJOR_VERSION, OPCUA_CLIENT_MODULE_MINOR_VERSION, OPCUA_CLIENT_MODULE_PATCH_VERSION), + std::move(context), + "OpenDAQOPCUAClientModule") + , discoveryClient( + {"OPENDAQ"} + ) +{ + loggerComponent = this->context.getLogger().getOrAddComponent("OPCUAClient"); + discoveryClient.initMdnsClient(List("_opcua-tcp._tcp.local.")); +} + +ListPtr OpcUaClientModule::onGetAvailableDevices() +{ + auto availableDevices = List(); + for (const auto& device : discoveryClient.discoverMdnsDevices()) + availableDevices.pushBack(populateDiscoveredDevice(device)); + return availableDevices; +} + +DictPtr OpcUaClientModule::onGetAvailableDeviceTypes() +{ + auto result = Dict(); + + auto deviceType = createDeviceType(); + result.set(deviceType.getId(), deviceType); + return result; +} + +DevicePtr OpcUaClientModule::onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) +{ + if (!connectionString.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException); + + PropertyObjectPtr configPtr = config; + if (!configPtr.assigned()) + configPtr = createDefaultConfig(); + else + configPtr = populateDefaultConfig(configPtr); + + if (!acceptsConnectionParameters(connectionString, configPtr)) + DAQ_THROW_EXCEPTION(InvalidParameterException); + + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Context is not available."); + + std::string host; + std::string hostType; + int port; + auto formedConnectionString = formConnectionString(connectionString, configPtr, host, port, hostType); + + std::scoped_lock lock(sync); + + auto endpoint = OpcUaEndpoint(formedConnectionString); + + endpoint.setUsername(configPtr.getPropertyValue("Username")); + endpoint.setPassword(configPtr.getPropertyValue("Password")); + + TmsClient client(context, parent, endpoint); + auto device = client.connect(); + + // Set the connection info for the device + DeviceInfoPtr deviceInfo = device.getInfo(); + deviceInfo.asPtr().setProtectedPropertyValue("connectionString", connectionString); + ServerCapabilityConfigPtr connectionInfo = deviceInfo.getConfigurationConnectionInfo(); + + const auto addressInfo = AddressInfoBuilder().setAddress(host) + .setReachabilityStatus(AddressReachabilityStatus::Reachable) + .setType(hostType) + .setConnectionString(connectionString) + .build(); + + connectionInfo.setProtocolId(DaqOpcUaDeviceTypeId) + .setProtocolName("OpenDAQOPCUA") + .setProtocolType(ProtocolType::Configuration) + .setConnectionType("TCP/IP") + .addAddress(host) + .setPort(port) + .setPrefix(DaqOpcUaDevicePrefix) + .setConnectionString(connectionString) + .addAddressInfo(addressInfo) + .freeze(); + + return device; +} + +PropertyObjectPtr OpcUaClientModule::populateDefaultConfig(const PropertyObjectPtr& config) +{ + const auto defConfig = createDefaultConfig(); + for (const auto& prop : defConfig.getAllProperties()) + { + const auto name = prop.getName(); + if (config.hasProperty(name)) + defConfig.setPropertyValue(name, config.getPropertyValue(name)); + } + + return defConfig; +} + +DeviceInfoPtr OpcUaClientModule::populateDiscoveredDevice(const MdnsDiscoveredDevice& discoveredDevice) +{ + auto cap = ServerCapability(DaqOpcUaDeviceTypeId, "OpenDAQOPCUA", ProtocolType::Configuration); + + for (const auto& ipAddress : discoveredDevice.ipv4Addresses) + { + auto connectionStringIpv4 = fmt::format("{}://{}:{}{}", + DaqOpcUaDevicePrefix, + ipAddress, + discoveredDevice.servicePort, + discoveredDevice.getPropertyOrDefault("path", "/")); + cap.addConnectionString(connectionStringIpv4); + cap.addAddress(ipAddress); + + const auto addressInfo = AddressInfoBuilder().setAddress(ipAddress) + .setReachabilityStatus(AddressReachabilityStatus::Unknown) + .setType("IPv4") + .setConnectionString(connectionStringIpv4) + .build(); + cap.addAddressInfo(addressInfo); + } + + for (const auto& ipAddress : discoveredDevice.ipv6Addresses) + { + auto connectionStringIpv6 = fmt::format("{}://{}:{}{}", + DaqOpcUaDevicePrefix, + ipAddress, + discoveredDevice.servicePort, + discoveredDevice.getPropertyOrDefault("path", "/")); + cap.addConnectionString(connectionStringIpv6); + cap.addAddress(ipAddress); + + const auto addressInfo = AddressInfoBuilder().setAddress(ipAddress) + .setReachabilityStatus(AddressReachabilityStatus::Unknown) + .setType("IPv6") + .setConnectionString(connectionStringIpv6) + .build(); + cap.addAddressInfo(addressInfo); + } + + cap.setConnectionType("TCP/IP"); + cap.setPrefix(DaqOpcUaDevicePrefix); + cap.setProtocolVersion(discoveredDevice.getPropertyOrDefault("protocolVersion", "")); + if (discoveredDevice.servicePort > 0) + cap.setPort(discoveredDevice.servicePort); + + return populateDiscoveredDeviceInfo(DiscoveryClient::populateDiscoveredInfoProperties, discoveredDevice, cap, createDeviceType()); +} + +StringPtr OpcUaClientModule::formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config, std::string& host, int& port, std::string& hostType) +{ + std::string urlString = connectionString.toStdString(); + std::smatch match; + + std::string prefix = ""; + std::string path = "/"; + + if (config.assigned() ) + { + if (config.hasProperty("Port")) + port = config.getPropertyValue("Port"); + + if (config.hasProperty("Path")) + path = String(config.getPropertyValue("Path")).toStdString(); + } + + bool parsed = false; + parsed = std::regex_search(urlString, match, RegexIpv6Hostname); + hostType = "IPv6"; + + if (!parsed) + { + parsed = std::regex_search(urlString, match, RegexIpv4Hostname); + hostType = "IPv4"; + } + + if (parsed) + { + prefix = match[1]; + host = match[2]; + + if (match[3].matched) + port = std::stoi(match[3]); + + if (match[4].matched) + path = match[4]; + } + else + DAQ_THROW_EXCEPTION(InvalidParameterException, "Host name not found in url: {}", connectionString); + + if (prefix != std::string(DaqOpcUaDevicePrefix) + "://") + DAQ_THROW_EXCEPTION(InvalidParameterException, "OpcUa does not support connection string with prefix {}", prefix); + + return std::string(OpcUaScheme) + "://" + host + ":" + std::to_string(port) + path; +} + +bool OpcUaClientModule::acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config) +{ + std::string connStr = connectionString; + auto found = connStr.find(std::string(DaqOpcUaDevicePrefix) + "://"); + return found == 0; +} + +Bool OpcUaClientModule::onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) +{ + if (target.getProtocolId() != "OpenDAQOPCUAConfiguration") + return false; + + if (source.getConnectionType() != "TCP/IP") + return false; + + if (!source.getAddresses().assigned() || !source.getAddresses().getCount()) + { + LOG_W("Source server capability address is not available when filling in missing OPC UA capability information.") + return false; + } + + const auto addrInfos = source.getAddressInfo(); + if (!addrInfos.assigned() || !addrInfos.getCount()) + { + LOG_W("Source server capability addressInfo is not available when filling in missing OPC UA capability information.") + return false; + } + + auto port = target.getPort(); + if (port == -1) + { + port = 4840; + target.setPort(port); + LOG_W("OPC UA server capability is missing port. Defaulting to 4840.") + } + + const auto path = target.hasProperty("Path") ? target.getPropertyValue("Path") : ""; + const auto targetAddress = target.getAddresses(); + for (const auto& addrInfo : addrInfos) + { + const auto address = addrInfo.getAddress(); + if (auto it = std::find(targetAddress.begin(), targetAddress.end(), address); it != targetAddress.end()) + continue; + + const auto prefix = target.getPrefix(); + StringPtr connectionString; + if (source.getPrefix() == prefix) + connectionString = addrInfo.getConnectionString(); + else + connectionString = fmt::format("{}://{}:{}{}", prefix, address, port, path); + + const auto targetAddrInfo = AddressInfoBuilder() + .setAddress(address) + .setReachabilityStatus(addrInfo.getReachabilityStatus()) + .setType(addrInfo.getType()) + .setConnectionString(connectionString) + .build(); + + target.addAddressInfo(targetAddrInfo) + .setConnectionString(connectionString) + .addAddress(address); + } + + return true; +} + +DeviceTypePtr OpcUaClientModule::createDeviceType() +{ + return DeviceTypeBuilder() + .setId(DaqOpcUaDeviceTypeId) + .setName("OpcUa enabled device") + .setDescription("Network device connected over OpcUa protocol") + .setConnectionStringPrefix(DaqOpcUaDevicePrefix) + .setDefaultConfig(createDefaultConfig()) + .build(); +} + +PropertyObjectPtr OpcUaClientModule::createDefaultConfig() +{ + auto config = PropertyObject(); + + config.addProperty(StringProperty("Username", "")); + config.addProperty(StringProperty("Password", "")); + config.addProperty(IntProperty("Port", 4840)); + + return config; +} + +END_NAMESPACE_OPENDAQ_OPCUA_CLIENT_MODULE diff --git a/modules/opcua_client_module/tests/CMakeLists.txt b/modules/opcua_client_module/tests/CMakeLists.txt new file mode 100644 index 0000000..326d216 --- /dev/null +++ b/modules/opcua_client_module/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +set(MODULE_NAME opcua_client_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_opcua_client_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${TEST_APP}coverage ${TEST_APP} ${TEST_APP}coverage) +endif() diff --git a/modules/opcua_client_module/tests/test_app.cpp b/modules/opcua_client_module/tests/test_app.cpp new file mode 100644 index 0000000..e2ba246 --- /dev/null +++ b/modules/opcua_client_module/tests/test_app.cpp @@ -0,0 +1,21 @@ +#include +#include + +#include +#include +#include + +int main(int argc, char** args) +{ + daq::daqInitializeCoreObjectsTesting(); + daqInitModuleManagerLibrary(); + + 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_client_module/tests/test_opcua_client_module.cpp b/modules/opcua_client_module/tests/test_opcua_client_module.cpp new file mode 100644 index 0000000..eecd8ec --- /dev/null +++ b/modules/opcua_client_module/tests/test_opcua_client_module.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +using OpcUaClientModuleTest = testing::Test; +using namespace daq; + +static ModulePtr CreateModule() +{ + ModulePtr module; + createModule(&module, NullContext()); + return module; +} + +TEST_F(OpcUaClientModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(OpcUaClientModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), "OpenDAQOPCUAClientModule"); +} + +TEST_F(OpcUaClientModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(OpcUaClientModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), OPCUA_CLIENT_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), OPCUA_CLIENT_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), OPCUA_CLIENT_MODULE_PATCH_VERSION); +} + +TEST_F(OpcUaClientModuleTest, EnumerateDevices) +{ + auto module = CreateModule(); + + ListPtr deviceInfo; + ASSERT_NO_THROW(deviceInfo = module.getAvailableDevices()); +} + +//TEST_F(OpcUaClientModuleTest, CreateConnectionString) +//{ +// auto context = NullContext(); +// ModulePtr module; +// createModule(&module, context); +// +// StringPtr connectionString; +// +// ServerCapabilityConfigPtr serverCapabilityIgnored = ServerCapability("test", "test", ProtocolType::Unknown); +// ASSERT_NO_THROW(connectionString = module.createConnectionString(serverCapabilityIgnored)); +// ASSERT_FALSE(connectionString.assigned()); +// +// ServerCapabilityConfigPtr serverCapability = ServerCapability("OpenDAQOPCUAConfiguration", "OpenDAQOPCUA", ProtocolType::Configuration); +// ASSERT_THROW(module.createConnectionString(serverCapability), InvalidParameterException); +// +// serverCapability.addAddress("123.123.123.123"); +// ASSERT_EQ(module.createConnectionString(serverCapability), "daq.opcua://123.123.123.123:4840"); +// +// serverCapability.setPort(1234); +// ASSERT_NO_THROW(connectionString = module.createConnectionString(serverCapability)); +// ASSERT_EQ(connectionString, "daq.opcua://123.123.123.123:1234"); +//} + +TEST_F(OpcUaClientModuleTest, CreateDeviceConnectionStringNull) +{ + auto module = CreateModule(); + + DevicePtr device; + ASSERT_THROW(device = module.createDevice(nullptr, nullptr), ArgumentNullException); +} + +TEST_F(OpcUaClientModuleTest, CreateDeviceConnectionStringEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("", nullptr), InvalidParameterException); +} + +TEST_F(OpcUaClientModuleTest, CreateDeviceConnectionStringInvalid) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("fdfdfdfdde", nullptr), InvalidParameterException); +} + +TEST_F(OpcUaClientModuleTest, CreateDeviceConnectionStringInvalidId) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("daqref://devicett3axxr1", nullptr), InvalidParameterException); +} + +TEST_F(OpcUaClientModuleTest, GetAvailableComponentTypes) +{ + const auto module = CreateModule(); + + DictPtr functionBlockTypes; + ASSERT_NO_THROW(functionBlockTypes = module.getAvailableFunctionBlockTypes()); + ASSERT_EQ(functionBlockTypes.getCount(), 0u); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAConfiguration")); + ASSERT_EQ(deviceTypes.get("OpenDAQOPCUAConfiguration").getId(), "OpenDAQOPCUAConfiguration"); + + DictPtr serverTypes; + ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); + ASSERT_EQ(serverTypes.getCount(), 0u); + + // Check module info for module + ModuleInfoPtr moduleInfo; + ASSERT_NO_THROW(moduleInfo = module.getModuleInfo()); + ASSERT_NE(moduleInfo, nullptr); + ASSERT_EQ(moduleInfo.getName(), "OpenDAQOPCUAClientModule"); + ASSERT_EQ(moduleInfo.getId(), "OpenDAQOPCUAClientModule"); + + // Check version info for module + VersionInfoPtr versionInfoModule; + ASSERT_NO_THROW(versionInfoModule = moduleInfo.getVersionInfo()); + ASSERT_NE(versionInfoModule, nullptr); + ASSERT_EQ(versionInfoModule.getMajor(), OPCUA_CLIENT_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoModule.getMinor(), OPCUA_CLIENT_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoModule.getPatch(), OPCUA_CLIENT_MODULE_PATCH_VERSION); + + // Check module and version info for device types + for (const auto& deviceType : deviceTypes) + { + ModuleInfoPtr moduleInfoDeviceType; + ASSERT_NO_THROW(moduleInfoDeviceType = deviceType.second.getModuleInfo()); + ASSERT_NE(moduleInfoDeviceType, nullptr); + ASSERT_EQ(moduleInfoDeviceType.getName(), "OpenDAQOPCUAClientModule"); + ASSERT_EQ(moduleInfoDeviceType.getId(), "OpenDAQOPCUAClientModule"); + + VersionInfoPtr versionInfoDeviceType; + ASSERT_NO_THROW(versionInfoDeviceType = moduleInfoDeviceType.getVersionInfo()); + ASSERT_NE(versionInfoDeviceType, nullptr); + ASSERT_EQ(versionInfoDeviceType.getMajor(), OPCUA_CLIENT_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoDeviceType.getMinor(), OPCUA_CLIENT_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoDeviceType.getPatch(), OPCUA_CLIENT_MODULE_PATCH_VERSION); + } +} + +TEST_F(OpcUaClientModuleTest, DefaultDeviceConfig) +{ + const auto module = CreateModule(); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQOPCUAConfiguration")); + auto config = deviceTypes.get("OpenDAQOPCUAConfiguration").createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + ASSERT_EQ(config.getAllProperties().getCount(), 3u); +} + +TEST_F(OpcUaClientModuleTest, CreateFunctionBlockIdNull) +{ + auto module = CreateModule(); + + FunctionBlockPtr functionBlock; + ASSERT_THROW(functionBlock = module.createFunctionBlock(nullptr, nullptr, "fb"), ArgumentNullException); +} + +TEST_F(OpcUaClientModuleTest, CreateFunctionBlockIdEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createFunctionBlock("", nullptr, "fb"), NotFoundException); +} diff --git a/modules/opcua_server_module/CMakeLists.txt b/modules/opcua_server_module/CMakeLists.txt new file mode 100644 index 0000000..5ff8681 --- /dev/null +++ b/modules/opcua_server_module/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(ServerModule VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/modules/opcua_server_module/include/opcua_server_module/common.h b/modules/opcua_server_module/include/opcua_server_module/common.h new file mode 100644 index 0000000..1fb8458 --- /dev/null +++ b/modules/opcua_server_module/include/opcua_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_SERVER_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(opcua_server_module) +#define END_NAMESPACE_OPENDAQ_OPCUA_SERVER_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/modules/opcua_server_module/include/opcua_server_module/module_dll.h b/modules/opcua_server_module/include/opcua_server_module/module_dll.h new file mode 100644 index 0000000..a3fdaa2 --- /dev/null +++ b/modules/opcua_server_module/include/opcua_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(OpcUaServerModule) diff --git a/modules/opcua_server_module/include/opcua_server_module/opcua_server_impl.h b/modules/opcua_server_module/include/opcua_server_module/opcua_server_impl.h new file mode 100644 index 0000000..9f9cd4f --- /dev/null +++ b/modules/opcua_server_module/include/opcua_server_module/opcua_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_SERVER_MODULE + +class OpcUaServerImpl : public Server +{ +public: + explicit OpcUaServerImpl(const DevicePtr& rootDevice, + const PropertyObjectPtr& config, + const ContextPtr& context); + ~OpcUaServerImpl(); + 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, OpcUaServer, daq::IServer, + DevicePtr, rootDevice, + PropertyObjectPtr, config, + const ContextPtr&, context +) + +END_NAMESPACE_OPENDAQ_OPCUA_SERVER_MODULE diff --git a/modules/opcua_server_module/include/opcua_server_module/opcua_server_module_impl.h b/modules/opcua_server_module/include/opcua_server_module/opcua_server_module_impl.h new file mode 100644 index 0000000..34c3926 --- /dev/null +++ b/modules/opcua_server_module/include/opcua_server_module/opcua_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_SERVER_MODULE + +class OpcUaServerModule final : public Module +{ +public: + OpcUaServerModule(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_SERVER_MODULE diff --git a/modules/opcua_server_module/src/CMakeLists.txt b/modules/opcua_server_module/src/CMakeLists.txt new file mode 100644 index 0000000..3eab755 --- /dev/null +++ b/modules/opcua_server_module/src/CMakeLists.txt @@ -0,0 +1,44 @@ +set(LIB_NAME opcua_server_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + module_dll.h + opcua_server_module_impl.h + opcua_server_impl.h +) + +set(SRC_Srcs module_dll.cpp + opcua_server_module_impl.cpp + opcua_server_impl.cpp +) + +prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/opcua_server_module_impl.h + ${MODULE_HEADERS_DIR}/opcua_server_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + module_dll.cpp + opcua_server_module_impl.cpp + opcua_server_impl.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} + opcua_server.natvis +) + +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +target_link_libraries(${LIB_NAME} PUBLIC daq::opendaq + PRIVATE daq::opcuatms_server +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +create_version_header(${LIB_NAME}) + diff --git a/modules/opcua_server_module/src/module_dll.cpp b/modules/opcua_server_module/src/module_dll.cpp new file mode 100644 index 0000000..f6b2849 --- /dev/null +++ b/modules/opcua_server_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::opcua_server_module; + +DEFINE_MODULE_EXPORTS(OpcUaServerModule) + diff --git a/modules/opcua_server_module/src/opcua_server.natvis b/modules/opcua_server_module/src/opcua_server.natvis new file mode 100644 index 0000000..04e3e34 --- /dev/null +++ b/modules/opcua_server_module/src/opcua_server.natvis @@ -0,0 +1,12 @@ + + + + OpcUaServerImpl, <{refCount}> + + (daq::ServerImpl*)this,nd + server + config + context + + + \ No newline at end of file diff --git a/modules/opcua_server_module/src/opcua_server_impl.cpp b/modules/opcua_server_module/src/opcua_server_impl.cpp new file mode 100644 index 0000000..70ddbc4 --- /dev/null +++ b/modules/opcua_server_module/src/opcua_server_impl.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SERVER_MODULE +using namespace daq; +using namespace daq::opcua; + +OpcUaServerImpl::OpcUaServerImpl(const DevicePtr& rootDevice, + const PropertyObjectPtr& config, + const ContextPtr& context) + : Server("OpenDAQOPCUA", 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(); +} + +OpcUaServerImpl::~OpcUaServerImpl() +{ +} + +void OpcUaServerImpl::populateDefaultConfigFromProvider(const ContextPtr& context, const PropertyObjectPtr& config) +{ + if (!context.assigned()) + return; + if (!config.assigned()) + return; + + auto options = context.getModuleOptions("OpenDAQOPCUAServerModule"); + for (const auto& [key, value] : options) + { + if (config.hasProperty(key)) + { + config->setPropertyValue(key, value); + } + } +} + +PropertyObjectPtr OpcUaServerImpl::createDefaultConfig(const ContextPtr& context) +{ + constexpr Int minPortValue = 0; + constexpr Int maxPortValue = 65535; + + auto defaultConfig = PropertyObject(); + + const auto portProp = IntPropertyBuilder("Port", 4840) + .setMinValue(minPortValue) + .setMaxValue(maxPortValue) + .build(); + defaultConfig.addProperty(portProp); + + defaultConfig.addProperty(StringProperty("Path", "/")); + + populateDefaultConfigFromProvider(context, defaultConfig); + return defaultConfig; +} + +PropertyObjectPtr OpcUaServerImpl::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 OpcUaServerImpl::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 OpcUaServerImpl::createType(const ContextPtr& context) +{ + return ServerType("OpenDAQOPCUA", + "openDAQ OPC UA server", + "Publishes device structure over OPC UA protocol", + OpcUaServerImpl::createDefaultConfig(context)); +} + +void OpcUaServerImpl::onStopServer() +{ + server.stop(); +} + +OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE( + INTERNAL_FACTORY, OpcUaServer, daq::IServer, + daq::DevicePtr, rootDevice, + PropertyObjectPtr, config, + const ContextPtr&, context +) + +END_NAMESPACE_OPENDAQ_OPCUA_SERVER_MODULE diff --git a/modules/opcua_server_module/src/opcua_server_module_impl.cpp b/modules/opcua_server_module/src/opcua_server_module_impl.cpp new file mode 100644 index 0000000..ef50d40 --- /dev/null +++ b/modules/opcua_server_module/src/opcua_server_module_impl.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_SERVER_MODULE + +OpcUaServerModule::OpcUaServerModule(ContextPtr context) + : Module("OpenDAQOPCUAServerModule", + daq::VersionInfo(OPCUA_SERVER_MODULE_MAJOR_VERSION, OPCUA_SERVER_MODULE_MINOR_VERSION, OPCUA_SERVER_MODULE_PATCH_VERSION), + std::move(context), + "OpenDAQOPCUAServerModule") +{ +} + +DictPtr OpcUaServerModule::onGetAvailableServerTypes() +{ + auto result = Dict(); + + auto serverType = OpcUaServerImpl::createType(context); + result.set(serverType.getId(), serverType); + + return result; +} + +ServerPtr OpcUaServerModule::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 = OpcUaServerImpl::createDefaultConfig(context); + else + config = OpcUaServerImpl::populateDefaultConfig(config, context); + + ServerPtr server(OpcUaServer_Create(rootDevice, config, context)); + return server; +} + +END_NAMESPACE_OPENDAQ_OPCUA_SERVER_MODULE diff --git a/modules/opcua_server_module/tests/CMakeLists.txt b/modules/opcua_server_module/tests/CMakeLists.txt new file mode 100644 index 0000000..d262445 --- /dev/null +++ b/modules/opcua_server_module/tests/CMakeLists.txt @@ -0,0 +1,29 @@ +set(MODULE_NAME opcua_server_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_opcua_server_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + daq::opcuaclient + daq::opendaq_mocks + ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + Taskflow::Taskflow +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (MSVC) # Ignoring warning for the Taskflow + target_compile_options(${TEST_APP} PRIVATE /wd4324) +endif() + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${TEST_APP}coverage ${TEST_APP} ${TEST_APP}coverage) +endif() diff --git a/modules/opcua_server_module/tests/test_app.cpp b/modules/opcua_server_module/tests/test_app.cpp new file mode 100644 index 0000000..681e0d3 --- /dev/null +++ b/modules/opcua_server_module/tests/test_app.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include +#include + +int main(int argc, char** args) +{ + daq::daqInitializeCoreObjectsTesting(); + daqInitModuleManagerLibrary(); + daqInitOpenDaqLibrary(); + + 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_server_module/tests/test_opcua_server_module.cpp b/modules/opcua_server_module/tests/test_opcua_server_module.cpp new file mode 100644 index 0000000..f8ce236 --- /dev/null +++ b/modules/opcua_server_module/tests/test_opcua_server_module.cpp @@ -0,0 +1,218 @@ +#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; + +static ModulePtr CreateModule(ContextPtr context = NullContext()) +{ + ModulePtr module; + createOpcUaServerModule(&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("OpenDAQOPCUA").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(), "OpenDAQOPCUAServerModule"); +} + +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_SERVER_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), OPCUA_SERVER_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), OPCUA_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("OpenDAQOPCUA")); + ASSERT_EQ(serverTypes.get("OpenDAQOPCUA").getId(), "OpenDAQOPCUA"); + + // Check module info for module + ModuleInfoPtr moduleInfo; + ASSERT_NO_THROW(moduleInfo = module.getModuleInfo()); + ASSERT_NE(moduleInfo, nullptr); + ASSERT_EQ(moduleInfo.getName(), "OpenDAQOPCUAServerModule"); + ASSERT_EQ(moduleInfo.getId(), "OpenDAQOPCUAServerModule"); + + // Check version info for module + VersionInfoPtr versionInfoModule; + ASSERT_NO_THROW(versionInfoModule = moduleInfo.getVersionInfo()); + ASSERT_NE(versionInfoModule, nullptr); + ASSERT_EQ(versionInfoModule.getMajor(), OPCUA_SERVER_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoModule.getMinor(), OPCUA_SERVER_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoModule.getPatch(), OPCUA_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(), "OpenDAQOPCUAServerModule"); + ASSERT_EQ(moduleInfoServerType.getId(), "OpenDAQOPCUAServerModule"); + + VersionInfoPtr versionInfoServerType; + ASSERT_NO_THROW(versionInfoServerType = moduleInfoServerType.getVersionInfo()); + ASSERT_NE(versionInfoServerType, nullptr); + ASSERT_EQ(versionInfoServerType.getMajor(), OPCUA_SERVER_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoServerType.getMinor(), OPCUA_SERVER_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoServerType.getPatch(), OPCUA_SERVER_MODULE_PATCH_VERSION); + } +} + +TEST_F(OpcUaServerModuleTest, ServerConfig) +{ + auto module = CreateModule(); + + DictPtr serverTypes = module.getAvailableServerTypes(); + ASSERT_TRUE(serverTypes.hasKey("OpenDAQOPCUA")); + auto config = serverTypes.get("OpenDAQOPCUA").createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + + ASSERT_TRUE(config.hasProperty("Port")); + ASSERT_EQ(config.getPropertyValue("Port"), 4840); +} + +TEST_F(OpcUaServerModuleTest, CreateServer) +{ + auto device = CreateTestInstance(); + auto module = CreateModule(device.getContext()); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(module.createServer("OpenDAQOPCUA", device.getRootDevice(), config)); +} + +TEST_F(OpcUaServerModuleTest, CreateServerFromInstance) +{ + auto device = CreateTestInstance(); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(device.addServer("OpenDAQOPCUA", config)); +} + +TEST_F(OpcUaServerModuleTest, TestConnection) +{ + auto device = CreateTestInstance(); + auto config = CreateServerConfig(device); + device.addServer("OpenDAQOPCUA", 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("OpenDAQOPCUA", 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("OpenDAQOPCUA", device.getRootDevice(), config); + + OpcUaClient client("opc.tcp://localhost/"); + ASSERT_NO_THROW(client.connect()); + client.disconnect(); + + serverPtr.stop(); + ASSERT_THROW(client.connect(), OpcUaException); +} diff --git a/shared/libraries/opcua/CMakeLists.txt b/shared/libraries/opcua/CMakeLists.txt new file mode 100644 index 0000000..5b96f8e --- /dev/null +++ b/shared/libraries/opcua/CMakeLists.txt @@ -0,0 +1,17 @@ +set_cmake_folder_context(TARGET_FOLDER_NAME) + +add_subdirectory(opcuashared) +add_subdirectory(opcuaserver) +add_subdirectory(opcuaclient) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() + +#GENERATE_DOCUMENTATION( +# NAME "OpcUa" +# INPUT +# opcuashared/include +# opcuaserver/include +# opcuaclient/include +#) diff --git a/shared/libraries/opcua/OWNERS.txt b/shared/libraries/opcua/OWNERS.txt new file mode 100644 index 0000000..48c5c08 --- /dev/null +++ b/shared/libraries/opcua/OWNERS.txt @@ -0,0 +1,12 @@ +# This is a list of owners of the subtree. + +# if any of the changes are made to this subtree at least one +# of the owners should review and confirm those changes. + +# Owners are listed at the bottom of this file. + +# Names should be added to this file like so: +# Name or Organization + +Dewesoft d.o.o. +Anže Škerjanc diff --git a/shared/libraries/opcua/opcuaclient/CMakeLists.txt b/shared/libraries/opcua/opcuaclient/CMakeLists.txt new file mode 100644 index 0000000..94b3492 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME client) +project(OpcUaClient CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/attribute_reader.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/attribute_reader.h new file mode 100644 index 0000000..f489834 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/attribute_reader.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class AttributeReader; +using AttributeReaderPtr = std::shared_ptr; + +class AttributeReader +{ +public: + AttributeReader(const OpcUaClientPtr& client, size_t maxBatchSize = 0); + + void setAttibutes(const tsl::ordered_set& attributes); + void addAttribute(const OpcUaAttribute& attribute); + OpcUaVariant getValue(const OpcUaNodeId& nodeId, UA_AttributeId attributeId); + OpcUaVariant getValue(const OpcUaAttribute& attribute); + bool hasAnyValue(const OpcUaNodeId& nodeId); + void clearResults(); + void clearAttributes(); + void read(); + +private: + using ResultMap = std::unordered_map>; + + void readBatch(tsl::ordered_set::iterator& attrIterator, size_t size); + void addBatchToResultMap(tsl::ordered_set::iterator attrIterator, const OpcUaObject& response); + + OpcUaClientPtr client; + tsl::ordered_set attributes; + ResultMap resultMap; + size_t maxBatchSize = 0; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/browse_request.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browse_request.h new file mode 100644 index 0000000..56a811b --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browse_request.h @@ -0,0 +1,36 @@ +/* + * 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 + +struct BrowseRequest : public OpcUaObject +{ + using OpcUaObject::OpcUaObject; + +public: + + explicit BrowseRequest(size_t nodesToBrowseSize); + BrowseRequest(const OpcUaNodeId& nodeId, OpcUaNodeClass nodeClassMask = OpcUaNodeClass::All, const OpcUaNodeId& referenceTypeId = OpcUaNodeId(UA_NS0ID_REFERENCES), const OpcUaBrowseDirection browseDirection = OpcUaBrowseDirection::Forward); + + void resizeNodesToBrowse(size_t newNodesToBrowseSize); +}; + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuabrowser.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuabrowser.h new file mode 100644 index 0000000..69068d1 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuabrowser.h @@ -0,0 +1,84 @@ +/* + * 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 "opcuashared/opcua.h" +#include "opcuashared/opcuanodeid.h" +#include "opcuaclient/opcuaclient.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +struct OpcUaBrowseTransaction +{ + virtual UA_BrowseResult* getResults() = 0; + virtual size_t getResultsSize() const = 0; + virtual const UA_ResponseHeader& getResponseHeader() const = 0; +}; + +template +struct OpcUaBrowseTransactionT : public OpcUaBrowseTransaction +{ + OpcUaObject response; + + UA_BrowseResult* getResults() override + { + return response->results; + }; + + size_t getResultsSize() const override + { + return response->resultsSize; + }; + + const UA_ResponseHeader& getResponseHeader() const override + { + return response->responseHeader; + }; +}; + +using OpcUaBrowseTransactionPtr = std::shared_ptr; +using OpcUaBrowseTransaction_First = OpcUaBrowseTransactionT; +using OpcUaBrowseTransaction_FirstPtr = std::shared_ptr; +using OpcUaBrowseTransaction_Next = OpcUaBrowseTransactionT; +using OpcUaBrowseTransaction_NextPtr = std::shared_ptr; + +class OpcUaBrowser +{ +public: + OpcUaBrowser(const OpcUaNodeId& nodeId, const OpcUaClientPtr& client); + OpcUaBrowser(const OpcUaObject& request, const OpcUaClientPtr& client); + std::vector& browse(); + tsl::ordered_map> referencesByNodeId(); + tsl::ordered_map> referencesByBrowseName(); + +private: + OpcUaBrowseTransactionPtr browseTransaction(); + OpcUaBrowseTransactionPtr browseNextTransaction(UA_ByteString* contPoint); + OpcUaObject prepareNextRequest(UA_ByteString* contPoint); + void validateTransactionStatus(const OpcUaBrowseTransactionPtr& transaction); + + OpcUaObject request; + OpcUaClientPtr client; + std::vector transactions; + std::vector references; + +}; + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuanodefactorybrowser.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuanodefactorybrowser.h new file mode 100644 index 0000000..db74681 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuanodefactorybrowser.h @@ -0,0 +1,79 @@ +/* + * 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 "opcuashared/node/opcuanodemethod.h" +#include "opcuashared/opcuanodecollection.h" +#include "opcuaclient/browser/opcuanodevisitor.h" +#include "opcuaclient/opcuanodefactory.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +struct OpcUaBrowserResults +{ + OpcUaNodeCollection nodes; + void clear() + { + nodes.clear(); + } +}; + +class OpcUaNodeFactoryBrowser; +using OpcUaNodeFactoryBrowserPtr = std::shared_ptr; + +class OpcUaNodeFactoryBrowser : public OpcUaNodeVisitor +{ +public: + OpcUaNodeFactoryBrowser(const IOpcUaNodeFactoryPtr& nodeFactory, const OpcUaClientPtr& client); + ~OpcUaNodeFactoryBrowser(); + + void browseTree(); + + void browseTree(const OpcUaNodePtr& startNodeFolder); + + void browseTree(const OpcUaNodeId& startNodeId); + + void browse(const OpcUaNodePtr& startNodeFolder); + + void browse(const OpcUaNodeId& startNodeId); + + void browse(const OpcUaNodePtr& startNodeFolder, bool recursive, OpcUaNodeClass nodeClassMask); + + void browse(const OpcUaNodeId& startNodeId, bool recursive, OpcUaNodeClass nodeClassMask); + + void addToList(const OpcUaNodePtr& node); + + const OpcUaNodeCollection& getNodes() const; + size_t getNodesSize(); + + const IOpcUaNodeFactoryPtr& getNodeFactory(); + +protected: + void traverse(const UA_ReferenceDescription& reference) override; + void applyNode(const UA_ReferenceDescription& referenceDescription) override; + + OpcUaBrowserResults results; + IOpcUaNodeFactoryPtr nodeFactory; + + OpcUaNodeId currentParent; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuanodevisitor.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuanodevisitor.h new file mode 100644 index 0000000..ac980c7 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuanodevisitor.h @@ -0,0 +1,67 @@ +/* + * 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 + +/* + Builds tree of nodes of given server by using browse service +*/ +#include + +#include +#include "opcuaclient/browser/opcuabrowser.h" +#include "opcuaclient/opcuaclient.h" +#include "opcuashared/node/opcuanodemethod.h" +#include "opcuashared/opcua.h" +#include "opcuashared/opcuanodeid.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaNodeVisitor +{ +public: + explicit OpcUaNodeVisitor(const OpcUaClientPtr& client, OpcUaNodeClass nodeClassMask = OpcUaNodeClass::All, bool recursive = true); + virtual ~OpcUaNodeVisitor(); + + OpcUaNodeClass getBrowseMask(); + void setBrowseMask(OpcUaNodeClass nodeClassMask); + + bool getRecursive(); + void setRecursive(bool recursive); + void setRequestedMaxReferencesPerNode(uint32_t requestedMaxReferencesPerNode); + + const OpcUaClientPtr& getClient(); + virtual void traverse(const OpcUaNodeId& nodeId); + +protected: + virtual void traverse(const UA_ReferenceDescription& reference); + void browseAndApplyNodes(const OpcUaNodeId& startNodeId, OpcUaBrowser& browser); + virtual void applyNode(const UA_ReferenceDescription& referenceDescription); + virtual void applyVariable(const UA_ReferenceDescription& referenceDescription); + virtual void applyObject(const UA_ReferenceDescription& referenceDescription); + virtual void applyMethod(const UA_ReferenceDescription& referenceDescription); + + OpcUaNodeId getOpcUaNodeId(const UA_ReferenceDescription& referenceDescription); + + OpcUaClientPtr client; + uint32_t requestedMaxReferencesPerNode{0}; + +private: + OpcUaNodeClass browseMask; + bool recursive; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuatransactionbrowser.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuatransactionbrowser.h new file mode 100644 index 0000000..9251d03 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/browser/opcuatransactionbrowser.h @@ -0,0 +1,83 @@ +/* + * 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 "opcuashared/opcua.h" +#include "opcuashared/node/opcuanodeid.h" +#include "opcuaclient/opcuaconnectionbroker.h" +#include + +BEGIN_NAMESPACE_OPCUA + +struct OpcUaBrowseTransaction +{ + virtual UA_BrowseResult* getResults() = 0; + virtual size_t getResultsSize() = 0; + virtual const UA_ResponseHeader& getResponseHeader() = 0; +}; + +template +struct OpcUaBrowseTransactionT : public OpcUaBrowseTransaction +{ + OpcUaObject response; + + UA_BrowseResult* getResults() override + { + return response->results; + }; + + size_t getResultsSize() override + { + return response->resultsSize; + }; + + const UA_ResponseHeader& getResponseHeader() override + { + return response->responseHeader; + }; +}; + +using OpcUaBrowseTransactionPtr = std::shared_ptr; +using OpcUaBrowseTransaction_First = OpcUaBrowseTransactionT; +using OpcUaBrowseTransaction_FirstPtr = std::shared_ptr; +using OpcUaBrowseTransaction_Next = OpcUaBrowseTransactionT; +using OpcUaBrowseTransaction_NextPtr = std::shared_ptr; + +class OpcUaBrowser +{ +public: + OpcUaBrowser(OpcUaObject& request, const OpcUaConnectionBrokerPtr& connection); + std::vector browse(); + +private: + OpcUaBrowseTransactionPtr browseTransaction(); + void dummy(); + void browseNextTransaction2(); + //OpcUaBrowseTransactionPtr browseNextTransaction1(); + //OpcUaBrowseTransactionPtr browseNextTransaction(UA_ByteString* contPoint); + //std::unique_ptr> prepareNextRequest(UA_ByteString* contPoint); + void validateTransactionStatus(const OpcUaBrowseTransactionPtr& transaction); + void validateTransactionStatus(UA_StatusCode status); + + OpcUaObject& request; + const OpcUaConnectionBrokerPtr& connection; + std::vector transactions; +}; + +END_NAMESPACE_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/cached_reference_browser.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/cached_reference_browser.h new file mode 100644 index 0000000..a7dea81 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/cached_reference_browser.h @@ -0,0 +1,81 @@ +/* + * 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 + +class CachedReferenceBrowser; +using CachedReferenceBrowserPtr = std::shared_ptr; + +struct CachedReferences +{ + tsl::ordered_map> byNodeId; + tsl::ordered_map> byBrowseName; +}; + +struct BrowseFilter +{ + OpcUaNodeId referenceTypeId = OpcUaNodeId(); + OpcUaNodeId typeDefinition = OpcUaNodeId(); + UA_BrowseDirection direction = UA_BROWSEDIRECTION_BOTH; + UA_NodeClass nodeClass = UA_NODECLASS_UNSPECIFIED; +}; + +class CachedReferenceBrowser +{ +public: + CachedReferenceBrowser(const OpcUaClientPtr& client, size_t maxNodesPerBrowse = 0); + + const CachedReferences& browse(const OpcUaNodeId& nodeId); + void browseMultiple(const std::vector& nodes); + void invalidate(const OpcUaNodeId& nodeId); + void invalidateRecursive(const OpcUaNodeId& nodeId); + + bool isSubtypeOf(const OpcUaNodeId& typeId, const OpcUaNodeId& baseType); + OpcUaNodeId getTypeDefinition(const OpcUaNodeId& nodeId); + bool hasReference(const OpcUaNodeId& nodeId, const std::string& browseName); + OpcUaNodeId getChildNodeId(const OpcUaNodeId& nodeId, const std::string& browseName); + CachedReferences browseFiltered(const OpcUaNodeId& nodeId, const BrowseFilter& filter); + +private: + void invalidate(const OpcUaNodeId& nodeId, bool recursive); + bool isCached(const OpcUaNodeId& nodeId); + void markAsCached(const OpcUaNodeId& nodeId); + size_t browseBatch(const std::vector& nodes, size_t startIndex, size_t size, std::vector& browseNext); + void processBrowseResults(const std::vector& nodes, + size_t startIndex, + size_t requestedSize, + UA_BrowseResult* results, + size_t resultSize, + std::vector& browseNextOut); + bool getContinuationPoint(UA_BrowseResult* results, UA_ByteString** continuationPointOut); + + OpcUaClientPtr client; + size_t maxNodesPerBrowse; + std::unordered_map references; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/chdatagather/_alignsyncchdatagather.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/chdatagather/_alignsyncchdatagather.h new file mode 100644 index 0000000..cbbf949 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/chdatagather/_alignsyncchdatagather.h @@ -0,0 +1,44 @@ +/* + * 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 "chdatagather.h" +#include "opcuaclient/node/opcuanodevalueclient.h" + +BEGIN_NAMESPACE_OPCUA + +class AlignSyncChannelDataGather : public ChDataGather +{ +public: + AlignSyncChannelDataGather(OpcUaClient* client); + + virtual int64_t getSamplesAcquired(); + + void start() override; + + void getData(RelativeTimeInSeconds seconds) override; + +protected: + void fillSyncChannelList(); + std::unordered_set syncChannelList; + int64_t srLCMDiv; + OpcUaRatio maxSyncSampleRate; + int64_t previousSamplesAcquired; +}; + +END_NAMESPACE_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/chdatagather/_chdatagather.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/chdatagather/_chdatagather.h new file mode 100644 index 0000000..5d47370 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/chdatagather/_chdatagather.h @@ -0,0 +1,44 @@ +/* + * 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 "opcuashared/opcua.h" + +BEGIN_NAMESPACE_OPCUA + +class OpcUaClient; + +class ChDataGather; +using ChDataGatherPtr = std::shared_ptr; + +class ChDataGather +{ +public: + ChDataGather(OpcUaClient* client); + + OpcUaClient* getClient(); + + virtual void start(); + + virtual void getData(RelativeTimeInSeconds seconds); + + virtual void stop(); +protected: + OpcUaClient* client; +}; + +END_NAMESPACE_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/event_filter.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/event_filter.h new file mode 100644 index 0000000..eaa91d9 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/event_filter.h @@ -0,0 +1,83 @@ +/* + * 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 + +class EventFilter : public OpcUaObject +{ +public: + using OpcUaObject::OpcUaObject; + + EventFilter(size_t selectClausesSize); + + void resizeSelectClauses(size_t selectClausesSize); + + void setSelectClause(size_t index, const OpcUaObject& simpleAttributeOperand); + void setSelectClause(size_t index, OpcUaObject&& simpleAttributeOperand); +}; + +class SimpleAttributeOperand +{ +public: + static OpcUaObject CreateEventIdValue() + { + return CreateStandardEventValue("EventId"); + } + + static OpcUaObject CreateEventTypeValue() + + { + return CreateStandardEventValue("EventType"); + } + + static OpcUaObject CreateSourceNodeValue() + { + return CreateStandardEventValue("SourceNode"); + } + + static OpcUaObject CreateSourceNameValue() + { + return CreateStandardEventValue("SourceName"); + } + + static OpcUaObject CreateTimeValue() + { + return CreateStandardEventValue("Time"); + } + + static OpcUaObject CreateReceiveTimeValue() + { + return CreateStandardEventValue("ReceiveTime"); + } + + static OpcUaObject CreateMessageValue() + { + return CreateStandardEventValue("Message"); + } + + static OpcUaObject CreateSeverityValue() + { + return CreateStandardEventValue("Severity"); + } + + static OpcUaObject CreateStandardEventValue(const char* attributeName); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/monitored_item_create_request.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/monitored_item_create_request.h new file mode 100644 index 0000000..fe53e00 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/monitored_item_create_request.h @@ -0,0 +1,44 @@ +/* + * 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 + +class MonitoredItemCreateRequest : public OpcUaObject +{ +public: + using OpcUaObject::OpcUaObject; + MonitoredItemCreateRequest(); +}; + +class EventMonitoredItemCreateRequest : public MonitoredItemCreateRequest +{ +public: + using MonitoredItemCreateRequest::MonitoredItemCreateRequest; + EventMonitoredItemCreateRequest(); + EventMonitoredItemCreateRequest(const OpcUaNodeId& nodeId); + + void setItemToMonitor(const OpcUaNodeId& nodeId); + + void setEventFilter(const OpcUaObject& eventFilter); + void setEventFilter(OpcUaObject&& eventFilter); + void setEventFilter(UA_EventFilter* eventFilter); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaasyncexecthread.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaasyncexecthread.h new file mode 100644 index 0000000..a11351f --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaasyncexecthread.h @@ -0,0 +1,41 @@ +/* + * 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 + +class OpcUaAsyncExecThread +{ +public: + OpcUaAsyncExecThread(); + OpcUaAsyncExecThread(std::thread&& thread); + ~OpcUaAsyncExecThread(); + + void reset(); + + OpcUaAsyncExecThread& operator=(std::thread&& thread); + +protected: + void joinThread(); + + std::thread thread; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuacallmethodrequest.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuacallmethodrequest.h new file mode 100644 index 0000000..563b5b6 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuacallmethodrequest.h @@ -0,0 +1,46 @@ +/* + * 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 + +struct OpcUaCallMethodRequest : public OpcUaObject +{ + using OpcUaObject::OpcUaObject; + +public: + OpcUaCallMethodRequest(const OpcUaNodeId& methodId, + const OpcUaNodeId& objectId, + size_t inputArgumentsSize = 0, + UA_Variant* inputArguments = nullptr); +}; + +struct OpcUaCallMethodRequestWithCallback : public OpcUaCallMethodRequest +{ +public: + using ProcessFunctionType = std::function; + OpcUaCallMethodRequestWithCallback(const OpcUaNodeId& methodId, + const OpcUaNodeId& objectId, + const ProcessFunctionType& processFunction, + size_t inputArgumentsSize = 0, + UA_Variant* inputArguments = nullptr); + ProcessFunctionType processFunction = nullptr; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h new file mode 100644 index 0000000..13c67d4 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuaclient.h @@ -0,0 +1,174 @@ +/* + * 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. + */ + +/* + * + * Handles client / server communication. Separated implementation for two reasons: 1) it can be instantiated by client or browser and + * shared 2) connection issues are isolated from OPC UA services implementation. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "opcuashared/node/opcuanodemethod.h" + +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaClient; +using OpcUaClientPtr = std::shared_ptr; + +class ClientLockGuard +{ +public: + ClientLockGuard(OpcUaClient* client = nullptr) noexcept; + virtual ~ClientLockGuard() noexcept; + operator UA_Client*(); + + ClientLockGuard(ClientLockGuard&& other) noexcept; + ClientLockGuard& operator=(ClientLockGuard&& other) noexcept; + + ClientLockGuard(const ClientLockGuard&) = delete; + ClientLockGuard& operator=(const ClientLockGuard&) = delete; + +protected: + OpcUaClient* client; +}; + +class UaClientFactory +{ +public: + UaClientFactory(); + static UA_Client* Create(const OpcUaClientSecurityConfig* securityConfig = nullptr, + UA_LogLevel logLevel = UA_LOGLEVEL_WARNING, + const UA_DataTypeArray* customDataTypes = nullptr); + + void setSecurityConfig(const OpcUaClientSecurityConfig* securityConfig); + void setCustomDataTypes(const UA_DataTypeArray* customDataTypes); + void setLogLevel(const UA_LogLevel logLevel); + + UA_Client* build(); + +private: + void configureClient(); + void configureClientSecurity(); + void configureClientAppUri(); + void configureClientConfigDefaults(); + + UA_ClientConfig* config; + const OpcUaClientSecurityConfig* securityConfig = nullptr; + const UA_DataTypeArray* customDataTypes = nullptr; + UA_Client* client; + UA_LogLevel logLevel; +}; + +class OpcUaClient +{ +public: + explicit OpcUaClient(const std::string& url); + explicit OpcUaClient(const OpcUaEndpoint& endpoint); + ~OpcUaClient(); + + static constexpr size_t CONNECTION_TIMEOUT_SECONDS = 10; + + void initialize(); + void connect(); + void disconnect(bool doClear = true); + void clear(); + bool isConnected(); + UA_Client* getUaClient(); + ClientLockGuard getLockedUaClient(); + const OpcUaEndpoint& getEndpoint() const; + void setTimeout(uint32_t timoutMs); + uint32_t getTimeout() const; + void setConnectivityCheckInterval(uint32_t connectivityCheckInterval); + + std::recursive_mutex& getLock(); + + void runIterate(std::chrono::milliseconds period = std::chrono::milliseconds(20), std::chrono::milliseconds iterateTimeout = std::chrono::milliseconds(0)); + void stopIterate(); + + UA_StatusCode iterate(std::chrono::milliseconds timeout = std::chrono::milliseconds(0)); + + OpcUaCallbackIdent scheduleTimerTask(double intervalMs, const OpcUaTimerTaskType& task); + void removeTimerTask(OpcUaCallbackIdent ident); + bool timerTaskExists(OpcUaCallbackIdent ident); + + bool nodeExists(const OpcUaNodeId& nodeId); + OpcUaVariant readValue(const OpcUaNodeId& node); + UA_NodeClass readNodeClass(const OpcUaNodeId& nodeId); + std::string readBrowseName(const OpcUaNodeId& nodeId); + std::string readDisplayName(const OpcUaNodeId& nodeId); + size_t readDimension(const OpcUaNodeId& nodeId); + void writeDisplayName(const OpcUaNodeId& nodeId, const std::string& displayName); + void writeDisplayName(const OpcUaNodeId& nodeId, const OpcUaObject& displayName); + std::string readDescription(const OpcUaNodeId& nodeId); + void writeDescription(const OpcUaNodeId& nodeId, const std::string& description); + void writeDescription(const OpcUaNodeId& nodeId, const OpcUaObject& description); + OpcUaNodeId readDataType(const OpcUaNodeId& nodeId); + + OpcUaObject callMethods(const OpcUaObject& request); + + OpcUaObject callMethod(const OpcUaCallMethodRequest& callRequest); + + void callMethods(const std::vector& container); + + void writeValue(const OpcUaNodeId& nodeId, const OpcUaVariant& value); + + OpcUaObject readNodeAttributes(const OpcUaObject& request); + + void readNodeAttributes(const std::vector& request); + + Subscription* createSubscription(const OpcUaObject& request, + const StatusChangeNotificationCallbackType& statusChangeCallback = nullptr); + +protected: + static void timerTaskCallback(UA_Client* client, void* data); + + void executeIterateCallback(); + + UA_Client* uaclient{}; + OpcUaEndpoint endpoint; + uint32_t timeoutMs{CONNECTION_TIMEOUT_SECONDS * 1000}; + uint32_t connectivityCheckInterval{CONNECTION_TIMEOUT_SECONDS * 1000}; + + std::recursive_mutex lock; + + TimerTaskContextCollection timerTasks; + + std::chrono::milliseconds iterateTimeout; + + daq::utils::NamedTimerThread iterateThread; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuanodefactory.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuanodefactory.h new file mode 100644 index 0000000..8472b7a --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuanodefactory.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class IOpcUaNodeFactory; +using IOpcUaNodeFactoryPtr = std::shared_ptr; + +class IOpcUaNodeFactory +{ +public: + virtual ~IOpcUaNodeFactory() = default; + virtual OpcUaNodePtr instantiateNode(const UA_ReferenceDescription& reference, const OpcUaNodeId& parentNodeId, bool& traverseChild) = 0; +protected: + static OpcUaNodeId GetOpcUaNodeId(const UA_ReferenceDescription& referenceDescription); +}; + +class OpcUaNodeFactory; +using OpcUaNodeFactoryPtr = std::shared_ptr; + +class OpcUaNodeFactory : public IOpcUaNodeFactory +{ +public: + explicit OpcUaNodeFactory(const OpcUaClientPtr& client); + + OpcUaNodePtr instantiateNode(const UA_ReferenceDescription& reference, const OpcUaNodeId& parentNodeId, bool& traverseChild) override; + +protected: + void prepareMethodParams(OpcUaNodeMethodPtr nodeMethod); + void browseAndApplyMethodParams(OpcUaBrowser& browser, const OpcUaNodeMethodPtr& nodeMethod); + void applyMethodParam(const UA_ReferenceDescription& reference, const OpcUaNodeMethodPtr& nodeMethod); + + OpcUaClientPtr client; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h new file mode 100644 index 0000000..0593203 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuareadvalueid.h @@ -0,0 +1,41 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +struct OpcUaReadValueId : public OpcUaObject +{ +public: + using AttributeIdType = decltype(UA_ReadValueId::attributeId); +}; + +struct OpcUaReadValueIdWithCallback : public OpcUaReadValueId +{ + using ProcessFunctionType = std::function; + OpcUaReadValueIdWithCallback(const OpcUaNodeId& nodeId, + const ProcessFunctionType& processFunction, + AttributeIdType attributeId = UA_ATTRIBUTEID_VALUE); + + ProcessFunctionType processFunction; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuatimertaskcontextcollection.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuatimertaskcontextcollection.h new file mode 100644 index 0000000..7440c93 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuatimertaskcontextcollection.h @@ -0,0 +1,97 @@ +/* + * 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. + */ + +/* +Helper class for OpcUaTaskProcessor +*/ + +#pragma once + +#include + +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaClient; + +struct TimerTaskControl +{ + bool terminate{false}; + void terminateTimerTask() + { + terminate = true; + } +}; + +using OpcUaTaskType = std::function; +using OpcUaTimerTaskType = std::function; +using OpcUaCallbackIdent = UA_UInt64; + +class TimerTaskContextCollection +{ +public: + explicit TimerTaskContextCollection(); + + void* createContext(); + void deleteContext(void* context); + + void insertTimerTask(void* context, OpcUaCallbackIdent ident, const OpcUaTimerTaskType& task); + void removeTimerTask(OpcUaCallbackIdent ident); + + bool timerTaskExists(OpcUaCallbackIdent ident) const; + + static void getTaskExecData(void* context, OpcUaCallbackIdent* callbackIdent, OpcUaTimerTaskType** task); + +protected: + struct KeyType + { + KeyType() + : owner(nullptr) + { + } + + explicit KeyType(TimerTaskContextCollection* owner) + : owner(owner) + { + } + OpcUaCallbackIdent ident{}; + TimerTaskContextCollection* owner; + }; + + struct KeyTypeHash + { + std::size_t operator()(const KeyType* k) const + { + return std::hash()(k->ident); + } + }; + + struct KeyTypeEqualTo + { + bool operator()(const KeyType* a, const KeyType* b) const + { + assert(a != nullptr && b != nullptr); + return a->ident == b->ident; + } + }; + + std::unordered_map taskCallbacks; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuatimertaskhelper.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuatimertaskhelper.h new file mode 100644 index 0000000..bee05d7 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/opcuatimertaskhelper.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaTimerTaskHelper; +using OpcUaTimerTaskHelperPtr = std::shared_ptr; + +class OpcUaTimerTaskHelper // Similar API as CommonLib::TimerThread +{ +public: + using CallbackFunction = std::function; + + explicit OpcUaTimerTaskHelper(OpcUaClient& client, int intervalMs, const CallbackFunction& callback); + virtual ~OpcUaTimerTaskHelper(); + + virtual void start(); + virtual void stop(); + + double getIntervalMs() const; + void setIntervalMs(const double value); + + void terminate(); + const std::atomic& getTerminated() const; + + bool getStarted() const; + +protected: + virtual void execute(OpcUaClient& client, TimerTaskControl& control); + + double intervalMs; + std::atomic terminated; + std::optional callbackIdent; + OpcUaClient& client; + CallbackFunction callback; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/request_handler.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/request_handler.h new file mode 100644 index 0000000..bc6d77b --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/request_handler.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 + +class RequestHandler +{ +public: + explicit RequestHandler(const OpcUaClientPtr& client); + + // to be implemented + // this class should handle methods connected to UA_ReadRequest UA_WriteRequest UA_CallRequest +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/subscriptions.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/subscriptions.h new file mode 100644 index 0000000..0aa48fb --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/subscriptions.h @@ -0,0 +1,90 @@ +/* + * 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 OpcUaClient; +class Subscription; +class MonitoredItem; + +using StatusChangeNotificationCallbackType = + std::function; + +using EventNotificationCallbackType = std::function; + +using DataChangeNotificationCallbackType = + std::function; + +class Subscription +{ +protected: + Subscription(OpcUaClient* client, const StatusChangeNotificationCallbackType& statusChangeNotificationCallback = nullptr); + +public: + UA_UInt32 getSubscriptionId() const; + UA_Double getRevisedPublishingInterval() const; + UA_UInt32 getRevisedLifetimeCount() const; + UA_UInt32 getRevisedMaxKeepAliveCount() const; + + MonitoredItem* monitoredItemsCreateEvent(UA_TimestampsToReturn timestampsToReturn, + const UA_MonitoredItemCreateRequest& item, + const EventNotificationCallbackType& eventNotificationCallback); + + MonitoredItem* monitoredItemsCreateDataChange(UA_TimestampsToReturn timestampsToReturn, + const UA_MonitoredItemCreateRequest& item, + const DataChangeNotificationCallbackType& dataChangeNotificationCallback); + + const StatusChangeNotificationCallbackType& getStatusChangeNotificationCallback() const; + + static Subscription* CreateSubscription(OpcUaClient* client, + const OpcUaObject& request, + const StatusChangeNotificationCallbackType& statusChangeCallback); + +protected: + OpcUaClient* client; + OpcUaObject subscriptionResponse; + StatusChangeNotificationCallbackType statusChangeNotificationCallback; +}; + +class MonitoredItem +{ +public: + MonitoredItem(OpcUaClient* client, const EventNotificationCallbackType& eventNotificationCallback = nullptr); + MonitoredItem(OpcUaClient* client, const DataChangeNotificationCallbackType& dataChangeNotificationCallback = nullptr); + + UA_UInt32 getMonitoredItemId() const; + + const EventNotificationCallbackType& getEventNotificationCallback() const; + const DataChangeNotificationCallbackType& getDataChangeNotificationCallback() const; + +protected: + OpcUaClient* client; + OpcUaObject response; + EventNotificationCallbackType eventNotificationCallback; + DataChangeNotificationCallbackType dataChangeNotificationCallback; + + friend class Subscription; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/include/opcuaclient/taskprocessor/opcuataskprocessor.h b/shared/libraries/opcua/opcuaclient/include/opcuaclient/taskprocessor/opcuataskprocessor.h new file mode 100644 index 0000000..3132a52 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/include/opcuaclient/taskprocessor/opcuataskprocessor.h @@ -0,0 +1,74 @@ +/* + * 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 OpcUaTaskProcessor; +using OpcUaTaskProcessorPtr = std::shared_ptr; + +class OpcUaTaskProcessor : protected daq::utils::ThreadEx +{ +public: + explicit OpcUaTaskProcessor(const OpcUaClientPtr& client); + virtual ~OpcUaTaskProcessor(); + + void start() override; + void stop() override; + + bool connect(); + bool isConnected(); + void setConnectionTimeout(uint32_t timeoutMs); + const OpcUaClientPtr& getClient() const noexcept; + + void executeTask(const OpcUaTaskType& task); + + std::future executeTaskAwait(const OpcUaTaskType& task); + +protected: + using QueueItemType = std::pair, OpcUaTaskType>; + + void executeTask(std::promise& promise, const OpcUaTaskType& task) const; + + void execute() override; + + QueueItemType getNextTask(); + + OpcUaClientPtr client; + + std::atomic executingThreadId; + + std::queue queue; + std::mutex mutex; + + std::atomic connected; + UA_StatusCode prevStatus; + + std::condition_variable cvWait; + std::mutex waitMutex; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt b/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt new file mode 100644 index 0000000..f0dca5f --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/CMakeLists.txt @@ -0,0 +1,90 @@ +SET(MODULE_NAME opcuaclient) + +set(SOURCE_CPPS opcuaclient.cpp + opcuatimertaskcontextcollection.cpp + opcuatimertaskhelper.cpp + opcuanodefactory.cpp + opcuaasyncexecthread.cpp + opcuareadvalueid.cpp + opcuacallmethodrequest.cpp + subscriptions.cpp + monitored_item_create_request.cpp + event_filter.cpp + browse_request.cpp + request_handler.cpp + attribute_reader.cpp + cached_reference_browser.cpp +) + +set(SOURCE_BROWSER_CPPS browser/opcuanodevisitor.cpp + browser/opcuanodefactorybrowser.cpp + browser/opcuabrowser.cpp) + +set(SOURCE_TASKPROCESSOR_CPPS taskprocessor/opcuataskprocessor.cpp) + +set(SOURCE_HEADERS opcuaclient.h + opcuatimertaskcontextcollection.h + opcuatimertaskhelper.h + opcuanodefactory.h + opcuaasyncexecthread.h + opcuareadvalueid.h + opcuacallmethodrequest.h + subscriptions.h + monitored_item_create_request.h + event_filter.h + browse_request.h + request_handler.h + attribute_reader.h +) + +set(SOURCE_BROWSER_HEADERS + browser/opcuanodevisitor.h + browser/opcuanodefactorybrowser.h + browser/opcuabrowser.h) + +set(SOURCE_TASKPROCESSOR_HEADERS taskprocessor/opcuataskprocessor.h) + +set(INCLUDE_DIR ../include/${MODULE_NAME}) + +prepend_include(${INCLUDE_DIR} SOURCE_HEADERS) +prepend_include(${INCLUDE_DIR} SOURCE_BROWSER_HEADERS) +prepend_include(${INCLUDE_DIR} SOURCE_TASKPROCESSOR_HEADERS) + +set(ALL_SOURCES ${SOURCE_CPPS} + ${SOURCE_BROWSER_CPPS} + ${SOURCE_TASKPROCESSOR_CPPS} + ${SOURCE_HEADERS} + ${SOURCE_BROWSER_HEADERS} + ${SOURCE_TASKPROCESSOR_HEADERS} +) + +set(SOURCE_FILES ${ALL_SOURCES}) +add_library(${MODULE_NAME} STATIC ${ALL_SOURCES}) +add_library(${SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) + +target_include_directories(${MODULE_NAME} PUBLIC $ + $ +) + +target_link_libraries(${MODULE_NAME} PUBLIC daq::opcuashared + daq::opendaq_utils + tsl::ordered_map +) + +if(BUILD_64Bit) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "${MODULE_NAME}64") +endif() + +if(BUILD_64Bit OR BUILD_ARM) + set_target_properties(${MODULE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) +else() + set_target_properties(${MODULE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) +endif() + +if (MSVC) + source_group("Header Files\\Browser" "browser/.+[.]h") + source_group("Source Files\\Browser" "browser/.+[.]cpp") + + source_group("Header Files\\Taskprocessor" "taskprocessor/.+[.]h") + source_group("Source Files\\Taskprocessor" "taskprocessor/.+[.]cpp") +endif() diff --git a/shared/libraries/opcua/opcuaclient/src/attribute_reader.cpp b/shared/libraries/opcua/opcuaclient/src/attribute_reader.cpp new file mode 100644 index 0000000..10e9e57 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/attribute_reader.cpp @@ -0,0 +1,116 @@ +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +AttributeReader::AttributeReader(const OpcUaClientPtr& client, size_t maxBatchSize) + : client(client) + , maxBatchSize(maxBatchSize) +{ +} + +void AttributeReader::setAttibutes(const tsl::ordered_set& attributes) +{ + this->attributes = attributes; +} + +void AttributeReader::addAttribute(const OpcUaAttribute& attribute) +{ + attributes.insert(attribute); +} + +OpcUaVariant AttributeReader::getValue(const OpcUaNodeId& nodeId, UA_AttributeId attributeId) +{ + if (resultMap.count(nodeId) == 0 || resultMap[nodeId].count(attributeId) == 0) + throw OpcUaException(UA_STATUSCODE_BADNOTFOUND, "Attribute read result not found"); + + return resultMap[nodeId][attributeId]; +} + +OpcUaVariant AttributeReader::getValue(const OpcUaAttribute& attribute) +{ + return getValue(attribute.nodeId, attribute.attributeId); +} + +bool AttributeReader::hasAnyValue(const OpcUaNodeId& nodeId) +{ + return resultMap.count(nodeId) > 0; +} + +void AttributeReader::clearResults() +{ + resultMap.clear(); +} + +void AttributeReader::clearAttributes() +{ + attributes.clear(); +} + +void AttributeReader::read() +{ + if (attributes.empty()) + return; + + const size_t batchSize = (maxBatchSize > 0) ? maxBatchSize : attributes.size(); + size_t read = 0; + size_t toRead = batchSize; + auto attrIterator = attributes.begin(); + + while (read < attributes.size()) + { + toRead = batchSize; + if (read + toRead > attributes.size()) + toRead = attributes.size() - read; + + readBatch(attrIterator, toRead); + read += toRead; + } +} + +void AttributeReader::readBatch(tsl::ordered_set::iterator& attrIterator, size_t size) +{ + assert(size > 0); + auto batchStartIterator = attrIterator; + + OpcUaObject request; + request->nodesToReadSize = size; + request->nodesToRead = (UA_ReadValueId*) UA_Array_new(attributes.size(), &UA_TYPES[UA_TYPES_READVALUEID]); + + for (size_t i = 0; i < size; i++) + { + const auto& attribute = *attrIterator; + request->nodesToRead[i].nodeId = attribute.nodeId.copyAndGetDetachedValue(); + request->nodesToRead[i].attributeId = attribute.attributeId; + attrIterator++; + } + + OpcUaObject response = UA_Client_Service_read(client->getLockedUaClient(), *request); + const auto status = response->responseHeader.serviceResult; + + if (status != UA_STATUSCODE_GOOD) + throw OpcUaException(status, "Attribute read request failed"); + if (response->resultsSize != size) + throw OpcUaException(UA_STATUSCODE_BADINVALIDSTATE, "Read request returned incorrect number of results"); + + addBatchToResultMap(batchStartIterator, response); +} + +void AttributeReader::addBatchToResultMap(tsl::ordered_set::iterator attrIterator, + const OpcUaObject& response) +{ + for (size_t i = 0; i < response->resultsSize; i++) + { + const auto& attr = *attrIterator; + const auto& result = response->results[i]; + + if (resultMap.count(attr.nodeId) == 0) + resultMap[attr.nodeId] = {}; + + if (result.status == UA_STATUSCODE_GOOD) + resultMap[attr.nodeId][attr.attributeId] = OpcUaVariant(result.value); + + attrIterator++; + } +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/browse_request.cpp b/shared/libraries/opcua/opcuaclient/src/browse_request.cpp new file mode 100644 index 0000000..b730db9 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/browse_request.cpp @@ -0,0 +1,29 @@ +#include "opcuaclient/browse_request.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +BrowseRequest::BrowseRequest(size_t nodesToBrowseSize) + : OpcUaObject() +{ + resizeNodesToBrowse(nodesToBrowseSize); +} + +BrowseRequest::BrowseRequest(const OpcUaNodeId& nodeId, OpcUaNodeClass nodeClassMask, const OpcUaNodeId& referenceTypeId, const OpcUaBrowseDirection browseDirection) + : BrowseRequest(1u) +{ + value.nodesToBrowse[0].nodeId = nodeId.copyAndGetDetachedValue(); + value.nodesToBrowse[0].browseDirection = (UA_BrowseDirection) browseDirection; + value.nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL; + value.nodesToBrowse[0].referenceTypeId = referenceTypeId.copyAndGetDetachedValue(); + + value.nodesToBrowse[0].nodeClassMask = (UA_UInt32) nodeClassMask; + value.nodesToBrowse[0].includeSubtypes = UA_TRUE; +} + +void BrowseRequest::resizeNodesToBrowse(size_t newNodesToBrowseSize) +{ + CheckStatusCodeException(UA_Array_resize( + (void**) &value.nodesToBrowse, &value.nodesToBrowseSize, newNodesToBrowseSize, &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION])); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/browser/opcuabrowser.cpp b/shared/libraries/opcua/opcuaclient/src/browser/opcuabrowser.cpp new file mode 100644 index 0000000..b2699a4 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/browser/opcuabrowser.cpp @@ -0,0 +1,118 @@ +#include "opcuaclient/browser/opcuabrowser.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaBrowser::OpcUaBrowser(const OpcUaNodeId& nodeId, const OpcUaClientPtr& client) + : request(OpcUaObject()) + , client(client) +{ + request->requestedMaxReferencesPerNode = 0; + request->nodesToBrowse = UA_BrowseDescription_new(); + request->nodesToBrowseSize = 1; + request->nodesToBrowse[0].nodeId = nodeId.copyAndGetDetachedValue(); + request->nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL; +} + +OpcUaBrowser::OpcUaBrowser(const OpcUaObject& request, const OpcUaClientPtr& client) + : request(request) + , client(client) +{ +} + +std::vector& OpcUaBrowser::browse() +{ + transactions.clear(); + references.clear(); + + OpcUaBrowseTransactionPtr tr = browseTransaction(); + + while (tr->getResultsSize() > 0) + { + validateTransactionStatus(tr); + transactions.push_back(tr); + + for (size_t i = 0; i < tr->getResultsSize(); i++) + { + UA_BrowseResult& result = tr->getResults()[i]; + + if (result.statusCode == UA_STATUSCODE_BADUSERACCESSDENIED) + continue; + if (result.statusCode == UA_STATUSCODE_BADNODEIDUNKNOWN) + continue; + + CheckStatusCodeException(result.statusCode, "Browse result error"); + references.insert(references.end(), &result.references[0], &result.references[result.referencesSize]); + } + + UA_ByteString* contPoint = &tr->getResults()[0].continuationPoint; + if (contPoint->length == 0) + break; + + tr = this->browseNextTransaction(contPoint); + } + + return references; +} + +tsl::ordered_map> OpcUaBrowser::referencesByNodeId() +{ + auto refMap = tsl::ordered_map>(); + + for (const auto& ref : references) + { + OpcUaNodeId nodeId = ref.nodeId.nodeId; + refMap.insert({nodeId, OpcUaObject(ref)}); + } + + return refMap; +} + +tsl::ordered_map> OpcUaBrowser::referencesByBrowseName() +{ + auto refMap = tsl::ordered_map>(); + + for (const auto& ref : references) + { + std::string name = utils::ToStdString(ref.displayName.text); + refMap.insert({name, OpcUaObject(ref)}); + } + + return refMap; +} + +OpcUaBrowseTransactionPtr OpcUaBrowser::browseTransaction() +{ + auto transaction = std::make_shared(); + transaction->response = UA_Client_Service_browse(client->getLockedUaClient(), *request); + + return transaction; +} + +OpcUaBrowseTransactionPtr OpcUaBrowser::browseNextTransaction(UA_ByteString* contPoint) +{ + auto nextRequest = prepareNextRequest(contPoint); + + auto transaction = std::make_shared(); + transaction->response = UA_Client_Service_browseNext(client->getLockedUaClient(), *nextRequest); + + return transaction; +} + +OpcUaObject OpcUaBrowser::prepareNextRequest(UA_ByteString* contPoint) +{ + OpcUaObject nextRequest; + + nextRequest->releaseContinuationPoints = UA_FALSE; + nextRequest->continuationPointsSize = 1u; + nextRequest->continuationPoints = contPoint; + + return nextRequest; +} + +void OpcUaBrowser::validateTransactionStatus(const OpcUaBrowseTransactionPtr& transaction) +{ + CheckStatusCodeException(transaction->getResponseHeader().serviceResult, + "Browse transaction error, transaction:" + std::to_string(transactions.size() + 1)); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/browser/opcuanodefactorybrowser.cpp b/shared/libraries/opcua/opcuaclient/src/browser/opcuanodefactorybrowser.cpp new file mode 100644 index 0000000..85a7048 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/browser/opcuanodefactorybrowser.cpp @@ -0,0 +1,98 @@ +#include "opcuaclient/browser/opcuanodefactorybrowser.h" +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNodeFactoryBrowser::OpcUaNodeFactoryBrowser(const IOpcUaNodeFactoryPtr& nodeFactory, const OpcUaClientPtr& client) + : OpcUaNodeVisitor(client) + , nodeFactory(nodeFactory) +{ +} + +OpcUaNodeFactoryBrowser::~OpcUaNodeFactoryBrowser() +{ +} + +void OpcUaNodeFactoryBrowser::browseTree() +{ + auto startNode = OpcUaNodeObject::instantiateRoot(); + browseTree(startNode->getNodeId()); +} + +void OpcUaNodeFactoryBrowser::browseTree(const OpcUaNodePtr& startNodeFolder) +{ + browseTree(startNodeFolder->getNodeId()); +} + +void OpcUaNodeFactoryBrowser::browseTree(const OpcUaNodeId& startNodeId) +{ + browse(startNodeId, true, OpcUaNodeClass::All); +} + +void OpcUaNodeFactoryBrowser::browse(const OpcUaNodePtr& startNodeFolder) +{ + browse(startNodeFolder->getNodeId()); +} + +void OpcUaNodeFactoryBrowser::browse(const OpcUaNodeId& startNodeId) +{ + browse(startNodeId, false, OpcUaNodeClass::All); +} + +void OpcUaNodeFactoryBrowser::browse(const OpcUaNodePtr& startNodeFolder, bool recursive, OpcUaNodeClass nodeClassMask) +{ + browse(startNodeFolder->getNodeId(), recursive, nodeClassMask); +} + +void OpcUaNodeFactoryBrowser::browse(const OpcUaNodeId& startNodeId, bool recursive, OpcUaNodeClass nodeClassMask) +{ + results.clear(); + setBrowseMask(nodeClassMask); + setRecursive(recursive); + currentParent = startNodeId; + OpcUaNodeVisitor::traverse(startNodeId); +} + +void OpcUaNodeFactoryBrowser::addToList(const OpcUaNodePtr& node) +{ + if (node) + results.nodes.push_back(node); +} + +void OpcUaNodeFactoryBrowser::traverse(const UA_ReferenceDescription& reference) +{ + auto parent = currentParent; + currentParent = getOpcUaNodeId(reference); + OpcUaNodeVisitor::traverse(reference); + + currentParent = parent; +} + +void OpcUaNodeFactoryBrowser::applyNode(const UA_ReferenceDescription& reference) +{ + bool traverseChild; + auto node = nodeFactory->instantiateNode(reference, currentParent, traverseChild); + + if (traverseChild) + OpcUaNodeVisitor::applyNode(reference); + + addToList(node); +} + +const OpcUaNodeCollection& OpcUaNodeFactoryBrowser::getNodes() const +{ + return results.nodes; +} + +size_t OpcUaNodeFactoryBrowser::getNodesSize() +{ + return results.nodes.size(); +} + +const IOpcUaNodeFactoryPtr& OpcUaNodeFactoryBrowser::getNodeFactory() +{ + return nodeFactory; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/browser/opcuanodevisitor.cpp b/shared/libraries/opcua/opcuaclient/src/browser/opcuanodevisitor.cpp new file mode 100644 index 0000000..3f24911 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/browser/opcuanodevisitor.cpp @@ -0,0 +1,121 @@ +#include "opcuaclient/browser/opcuanodevisitor.h" +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNodeVisitor::OpcUaNodeVisitor(const OpcUaClientPtr& client, OpcUaNodeClass nodeClassMask, bool recursive) + : client(client) + , browseMask(nodeClassMask) + , recursive(recursive) +{ +} + +OpcUaNodeVisitor::~OpcUaNodeVisitor() +{ +} + +OpcUaNodeId OpcUaNodeVisitor::getOpcUaNodeId(const UA_ReferenceDescription& referenceDescription) +{ + return OpcUaNodeId(referenceDescription.nodeId.nodeId); +} + +const OpcUaClientPtr& OpcUaNodeVisitor::getClient() +{ + return client; +} + +void OpcUaNodeVisitor::setRequestedMaxReferencesPerNode(uint32_t requestedMaxReferencesPerNode) +{ + this->requestedMaxReferencesPerNode = requestedMaxReferencesPerNode; +} + +void OpcUaNodeVisitor::traverse(const UA_ReferenceDescription& reference) +{ + if (recursive) + traverse(getOpcUaNodeId(reference)); +} + +void OpcUaNodeVisitor::traverse(const OpcUaNodeId& startNodeId) +{ + BrowseRequest request(startNodeId, browseMask); + OpcUaBrowser browser(request, client); + + browseAndApplyNodes(startNodeId, browser); +} + +void OpcUaNodeVisitor::browseAndApplyNodes(const OpcUaNodeId& startNodeId, OpcUaBrowser& browser) +{ + try + { + auto& references = browser.browse(); + + for (auto& ref : references) + applyNode(ref); + } + catch (const OpcUaException& ex) + { + throw OpcUaException(ex.getStatusCode(), std::string(ex.what()) + ". Node: " + startNodeId.toString()); + } +} + +void OpcUaNodeVisitor::applyNode(const UA_ReferenceDescription& referenceDescription) +{ + switch (referenceDescription.nodeClass) + { + case UA_NODECLASS_VARIABLE: + applyVariable(referenceDescription); + break; + case UA_NODECLASS_OBJECT: + applyObject(referenceDescription); + break; + case UA_NODECLASS_METHOD: + applyMethod(referenceDescription); + break; + case UA_NODECLASS_UNSPECIFIED: + case UA_NODECLASS_OBJECTTYPE: + case UA_NODECLASS_VARIABLETYPE: + case UA_NODECLASS_REFERENCETYPE: + case UA_NODECLASS_DATATYPE: + case UA_NODECLASS_VIEW: + default: + break; + } +} + +void OpcUaNodeVisitor::applyVariable(const UA_ReferenceDescription& referenceDescription) +{ + traverse(referenceDescription); +} + +void OpcUaNodeVisitor::applyObject(const UA_ReferenceDescription& referenceDescription) +{ + traverse(referenceDescription); +} + +void OpcUaNodeVisitor::applyMethod(const UA_ReferenceDescription& referenceDescription) +{ + traverse(referenceDescription); +} + +OpcUaNodeClass OpcUaNodeVisitor::getBrowseMask() +{ + return browseMask; +} + +void OpcUaNodeVisitor::setBrowseMask(OpcUaNodeClass nodeClassMask) +{ + this->browseMask = nodeClassMask; +} + +bool OpcUaNodeVisitor::getRecursive() +{ + return recursive; +} + +void OpcUaNodeVisitor::setRecursive(bool recursive) +{ + this->recursive = recursive; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/cached_reference_browser.cpp b/shared/libraries/opcua/opcuaclient/src/cached_reference_browser.cpp new file mode 100644 index 0000000..5b72b0e --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/cached_reference_browser.cpp @@ -0,0 +1,232 @@ +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +CachedReferenceBrowser::CachedReferenceBrowser(const OpcUaClientPtr& client, size_t maxNodesPerBrowse) + : client(client) + , maxNodesPerBrowse(maxNodesPerBrowse) +{ +} + +const CachedReferences& CachedReferenceBrowser::browse(const OpcUaNodeId& nodeId) +{ + if (!isCached(nodeId)) + browseMultiple({nodeId}); + + return references[nodeId]; +} + +void CachedReferenceBrowser::invalidate(const OpcUaNodeId& nodeId, bool recursive) +{ + if (!isCached(nodeId)) + return; + + const auto children = references[nodeId].byNodeId; + references.erase(nodeId); + + if (!recursive) + return; + + for (const auto& [refNodeId, ref] : children) + { + if (ref->isForward) + invalidate(refNodeId); + } +} + +void CachedReferenceBrowser::invalidate(const OpcUaNodeId& nodeId) +{ + invalidate(nodeId, false); +} + +void CachedReferenceBrowser::invalidateRecursive(const OpcUaNodeId& nodeId) +{ + invalidate(nodeId, true); +} + +bool CachedReferenceBrowser::isSubtypeOf(const OpcUaNodeId& typeId, const OpcUaNodeId& baseType) +{ + if (typeId == baseType) + return true; + if (typeId.isNull()) + return false; + + const auto& references = browse(baseType); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (OpcUaNodeId(ref->referenceTypeId) == OpcUaNodeId(UA_NS0ID_HASSUBTYPE) && isSubtypeOf(typeId, refNodeId)) + return true; + } + + return false; +} + +OpcUaNodeId CachedReferenceBrowser::getTypeDefinition(const OpcUaNodeId& nodeId) +{ + const auto& references = browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (OpcUaNodeId(ref->referenceTypeId) == OpcUaNodeId(UA_NS0ID_HASTYPEDEFINITION)) + return ref->nodeId.nodeId; + } + + return OpcUaNodeId(); +} + +bool CachedReferenceBrowser::hasReference(const OpcUaNodeId& nodeId, const std::string& browseName) +{ + browse(nodeId); + return references[nodeId].byBrowseName.count(browseName) > 0; +} + +OpcUaNodeId CachedReferenceBrowser::getChildNodeId(const OpcUaNodeId& nodeId, const std::string& browseName) +{ + browse(nodeId); + const auto& node = references[nodeId].byBrowseName[browseName]; + return node->nodeId.nodeId; +} + +CachedReferences CachedReferenceBrowser::browseFiltered(const OpcUaNodeId& nodeId, const BrowseFilter& filter) +{ + const auto& references = browse(nodeId); + CachedReferences filtered; + + for (const auto& [browseName, ref] : references.byBrowseName) + { + auto typeId = OpcUaNodeId(ref->referenceTypeId); + + if (!filter.referenceTypeId.isNull() && filter.referenceTypeId != typeId) + continue; + if (!filter.typeDefinition.isNull() && !isSubtypeOf(ref->typeDefinition.nodeId, filter.typeDefinition)) + continue; + if (filter.direction == UA_BROWSEDIRECTION_FORWARD && !ref->isForward) + continue; + if (filter.direction == UA_BROWSEDIRECTION_INVERSE && ref->isForward) + continue; + if (filter.nodeClass != UA_NODECLASS_UNSPECIFIED && filter.nodeClass != ref->nodeClass) + continue; + + filtered.byNodeId.insert({ref->nodeId.nodeId, ref}); + filtered.byBrowseName.insert({browseName, ref}); + } + + return filtered; +} + +bool CachedReferenceBrowser::isCached(const OpcUaNodeId& nodeId) +{ + return references.count(nodeId) > 0; +} + +void CachedReferenceBrowser::markAsCached(const OpcUaNodeId& nodeId) +{ + if (references.count(nodeId) == 0) + references[nodeId] = {}; +} + +void CachedReferenceBrowser::browseMultiple(const std::vector& nodes) +{ + const size_t batchSize = (maxNodesPerBrowse > 0) ? maxNodesPerBrowse : nodes.size(); + std::vector browseNext; + size_t i = 0; + + while (i < nodes.size()) + i += browseBatch(nodes, i, batchSize, browseNext); + + if (!browseNext.empty()) + browseMultiple(browseNext); +} + +size_t CachedReferenceBrowser::browseBatch(const std::vector& nodes, + size_t startIndex, + size_t size, + std::vector& browseNext) +{ + if ((startIndex + size) > nodes.size()) + size = nodes.size() - startIndex; + + assert(size > 0); + + OpcUaObject request; + request->requestedMaxReferencesPerNode = 0; + request->nodesToBrowse = (UA_BrowseDescription*) UA_Array_new(size, &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]); + request->nodesToBrowseSize = size; + + for (size_t i = 0; i < size; i++) + { + const auto& nodeId = nodes[startIndex + i]; + markAsCached(nodeId); + + request->nodesToBrowse[i].nodeId = nodeId.copyAndGetDetachedValue(); + request->nodesToBrowse[i].resultMask = UA_BROWSERESULTMASK_ALL; + request->nodesToBrowse[i].browseDirection = UA_BROWSEDIRECTION_FORWARD; + } + + UA_ByteString* continuationPoint = nullptr; + OpcUaObject response = UA_Client_Service_browse(client->getLockedUaClient(), *request); + CheckStatusCodeException(response->responseHeader.serviceResult, "Browse result error"); + + processBrowseResults(nodes, startIndex, size, response->results, response->resultsSize, browseNext); + + while (getContinuationPoint(response->results, &continuationPoint)) + { + OpcUaObject nextRequest; + nextRequest->releaseContinuationPoints = UA_FALSE; + nextRequest->continuationPointsSize = 1u; + nextRequest->continuationPoints = continuationPoint; + OpcUaObject nextResponse = UA_Client_Service_browseNext(client->getLockedUaClient(), *nextRequest); + CheckStatusCodeException(response->responseHeader.serviceResult, "Browse result error"); + + processBrowseResults(nodes, startIndex, size, nextResponse->results, nextResponse->resultsSize, browseNext); + } + + return size; +} + +void CachedReferenceBrowser::processBrowseResults(const std::vector& nodes, + size_t startIndex, + size_t requestedSize, + UA_BrowseResult* results, + size_t resultSize, + std::vector& browseNextOut) +{ + assert(requestedSize == resultSize); + + for (size_t i = 0; i < resultSize; i++) + { + UA_BrowseResult& result = results[i]; + + if (result.statusCode == UA_STATUSCODE_BADUSERACCESSDENIED) + continue; + if (result.statusCode == UA_STATUSCODE_BADNODEIDUNKNOWN) + continue; + + CheckStatusCodeException(result.statusCode, "Browse result error"); + + const auto nodeId = nodes[startIndex + i]; + references[nodeId] = {}; + + for (size_t j = 0; j < result.referencesSize; j++) + { + const auto& ref = result.references[j]; + const std::string browseName = utils::ToStdString(ref.browseName.name); + const auto refId = OpcUaNodeId(ref.nodeId.nodeId); + + references[nodeId].byNodeId.insert({refId, ref}); + references[nodeId].byBrowseName.insert({browseName, ref}); + + if (ref.isForward && references.count(refId) == 0) + browseNextOut.push_back(refId); + } + } +} + +bool CachedReferenceBrowser::getContinuationPoint(UA_BrowseResult* results, UA_ByteString** continuationPointOut) +{ + *continuationPointOut = &results->continuationPoint; + return results->continuationPoint.length > 0; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/chdatagather/_alignsyncchdatagather.cpp b/shared/libraries/opcua/opcuaclient/src/chdatagather/_alignsyncchdatagather.cpp new file mode 100644 index 0000000..d4f26f6 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/chdatagather/_alignsyncchdatagather.cpp @@ -0,0 +1,117 @@ +#include +#include "opcuaclient/chdatagather/alignsyncchdatagather.h" +#include "opcuaclient/opcuaclient.h" + +BEGIN_NAMESPACE_OPCUA + +AlignSyncChannelDataGather::AlignSyncChannelDataGather(OpcUaClient* client) + : ChDataGather(client) + , previousSamplesAcquired(0) +{ +} + +void AlignSyncChannelDataGather::fillSyncChannelList() +{ + syncChannelList.clear(); + + for (const auto& clientBase : *getClient()) + { + if (clientBase->getClockMode() == ClockMode::MasterClock && + clientBase->getConnection()->getEndpoint().getServerType() == OpcUaServerType::Dewesoft) + { + for (const auto& node : clientBase->getAcqNodes()) + { + if (node->getChannelType() == ChannelType::Sync) + syncChannelList.insert(node); + } + } + } +} + +void AlignSyncChannelDataGather::start() +{ + fillSyncChannelList(); + + maxSyncSampleRate = OpcUaRatio(); + + for (const auto& node : syncChannelList) + { + OpcUaRatio rSR = node->getRemoteSampleRate(); + if (!maxSyncSampleRate.isValid() || maxSyncSampleRate < rSR) + maxSyncSampleRate = rSR; + } + + srLCMDiv = 1; + + for (const auto& node : syncChannelList) + { + OpcUaRatio rSR = node->getRemoteSampleRate(); + OpcUaRatio div = maxSyncSampleRate / rSR; + + if ((div.numerator % div.denominator) != 0) + { + throw std::runtime_error("Can't align channels with sample rate " + rSR.toString() + " and " + div.toString()); + } + + srLCMDiv = std::lcm(srLCMDiv, div.intVal()); + } +} + +void AlignSyncChannelDataGather::getData(RelativeTimeInSeconds seconds) +{ + auto samplesAcq = (getSamplesAcquired() / srLCMDiv) * srLCMDiv; + + for (const auto& c : *client) + { + for (const auto& node : c->getAcqNodes()) + { + if (syncChannelList.find(node) != syncChannelList.end()) + { + if (node->getTiming() && !node->getTiming()->masterClockDetails.isBufferOverrun()) // check if data are incomplete + { + OpcUaRatio rSR = node->getRemoteSampleRate(); + int64_t divider = (maxSyncSampleRate / rSR).intVal(); + int64_t totalSyncSamples = node->getChannel()->getBufferProxy()->getTotalSyncSamples(); + auto samplesAcqNode = samplesAcq / divider - totalSyncSamples; + node->getData(seconds, (size_t) samplesAcqNode); + + assert(totalSyncSamples + samplesAcqNode == node->getChannel()->getBufferProxy()->getTotalSyncSamples()); + } + } + else + node->getData(seconds); + } + } + previousSamplesAcquired = samplesAcq; +} + +int64_t AlignSyncChannelDataGather::getSamplesAcquired() +{ + int64_t samplesCountSync = INT64_MAX; + + for (const auto& node : syncChannelList) + { + if (node->getTiming()) + { + OpcUaRatio rSR = node->getRemoteSampleRate(); + int64_t divider = (maxSyncSampleRate / rSR).intVal(); + int64_t samplesCnt = node->getTiming()->masterClockDetails.getSamplesAcquired() * divider; + samplesCnt = + std::min(samplesCnt, + previousSamplesAcquired + + (node->getChannel()->getBufferProxy()->getDBBufSize() - 1) * + divider); // limit to channel buffer size, so you move datalost error handling only to daqthread + if (samplesCnt < samplesCountSync) + samplesCountSync = samplesCnt; + } + else + { + samplesCountSync = 0; + break; + } + } + + return (samplesCountSync < INT64_MAX) ? samplesCountSync : 0; +} + +END_NAMESPACE_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/chdatagather/_chdatagather.cpp b/shared/libraries/opcua/opcuaclient/src/chdatagather/_chdatagather.cpp new file mode 100644 index 0000000..25e2c7e --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/chdatagather/_chdatagather.cpp @@ -0,0 +1,35 @@ +#include "opcuaclient/chdatagather/chdatagather.h" +#include "opcuaclient/opcuaclient.h" + +BEGIN_NAMESPACE_OPCUA + +ChDataGather::ChDataGather(OpcUaClient* client) +{ + this->client = client; +} + +OpcUaClient* ChDataGather::getClient() +{ + return client; +} + +void ChDataGather::start() +{ +} + +void ChDataGather::getData(RelativeTimeInSeconds seconds) //TODO align start... yes/no +{ + for (const auto& c : *getClient()) + { + for (const auto& node : c->getAcqNodes()) + { + node->getData(seconds); + } + } +} + +void ChDataGather::stop() +{ +} + +END_NAMESPACE_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/event_filter.cpp b/shared/libraries/opcua/opcuaclient/src/event_filter.cpp new file mode 100644 index 0000000..849c0db --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/event_filter.cpp @@ -0,0 +1,44 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +EventFilter::EventFilter(size_t selectClausesSize) + : OpcUaObject() +{ + resizeSelectClauses(selectClausesSize); +} + +void EventFilter::resizeSelectClauses(size_t selectClausesSize) +{ + CheckStatusCodeException(UA_Array_resize((void**) &value.selectClauses, &value.selectClausesSize, selectClausesSize, &UA_TYPES[UA_TYPES_SIMPLEATTRIBUTEOPERAND])); +} + +void EventFilter::setSelectClause(size_t index, const OpcUaObject& simpleAttributeOperand) +{ + value.selectClauses[index] = simpleAttributeOperand.copyAndGetDetachedValue(); +} +void EventFilter::setSelectClause(size_t index, OpcUaObject&& simpleAttributeOperand) +{ + value.selectClauses[index] = simpleAttributeOperand.getDetachedValue(); +} + +/*SimpleAttributeOperand*/ + +OpcUaObject SimpleAttributeOperand::CreateStandardEventValue(const char* attributeName) +{ + OpcUaObject simpleAttributeOperand; + + simpleAttributeOperand->typeDefinitionId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEEVENTTYPE); + simpleAttributeOperand->browsePathSize = 1; + simpleAttributeOperand->browsePath = + (UA_QualifiedName*)UA_Array_new(simpleAttributeOperand->browsePathSize, &UA_TYPES[UA_TYPES_QUALIFIEDNAME]); + + simpleAttributeOperand->attributeId = UA_ATTRIBUTEID_VALUE; + simpleAttributeOperand->browsePath[0] = UA_QUALIFIEDNAME_ALLOC(0, attributeName); + + return simpleAttributeOperand; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/monitored_item_create_request.cpp b/shared/libraries/opcua/opcuaclient/src/monitored_item_create_request.cpp new file mode 100644 index 0000000..d4018dc --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/monitored_item_create_request.cpp @@ -0,0 +1,59 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +/*MonitoredItemCreateRequest*/ + +MonitoredItemCreateRequest::MonitoredItemCreateRequest() + : OpcUaObject(UA_MonitoredItemCreateRequest_default(UA_NODEID_NULL)) +{ +} + +/*EventMonitoredItemCreateRequest*/ + +EventMonitoredItemCreateRequest::EventMonitoredItemCreateRequest() + : MonitoredItemCreateRequest() +{ + value.itemToMonitor.attributeId = UA_ATTRIBUTEID_EVENTNOTIFIER; + value.monitoringMode = UA_MONITORINGMODE_REPORTING; +} + +EventMonitoredItemCreateRequest::EventMonitoredItemCreateRequest(const OpcUaNodeId& nodeId) + : EventMonitoredItemCreateRequest() +{ + setItemToMonitor(nodeId); +} + +void EventMonitoredItemCreateRequest::setItemToMonitor(const OpcUaNodeId& nodeId) +{ + UA_NodeId_clear(&value.itemToMonitor.nodeId); + value.itemToMonitor.nodeId = nodeId.copyAndGetDetachedValue(); +} + +void EventMonitoredItemCreateRequest::setEventFilter(const OpcUaObject& eventFilter) +{ + UA_EventFilter* uaEventFilter = UA_EventFilter_new(); + *uaEventFilter = eventFilter.copyAndGetDetachedValue(); + + setEventFilter(uaEventFilter); +} + +void EventMonitoredItemCreateRequest::setEventFilter(OpcUaObject&& eventFilter) +{ + UA_EventFilter* uaEventFilter = UA_EventFilter_new(); + *uaEventFilter = eventFilter.getDetachedValue(); + + setEventFilter(uaEventFilter); +} + +void EventMonitoredItemCreateRequest::setEventFilter(UA_EventFilter* eventFilter) +{ + UA_ExtensionObject_clear(&value.requestedParameters.filter); + + value.requestedParameters.filter.encoding = UA_EXTENSIONOBJECT_DECODED; + value.requestedParameters.filter.content.decoded.data = eventFilter; + value.requestedParameters.filter.content.decoded.type = &UA_TYPES[UA_TYPES_EVENTFILTER]; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuaasyncexecthread.cpp b/shared/libraries/opcua/opcuaclient/src/opcuaasyncexecthread.cpp new file mode 100644 index 0000000..edda861 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuaasyncexecthread.cpp @@ -0,0 +1,37 @@ +#include "opcuaclient/opcuaasyncexecthread.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaAsyncExecThread::OpcUaAsyncExecThread() +{ +} + +OpcUaAsyncExecThread::OpcUaAsyncExecThread(std::thread&& thread) + : thread(std::move(thread)) +{ +} + +OpcUaAsyncExecThread::~OpcUaAsyncExecThread() +{ + joinThread(); +} + +void OpcUaAsyncExecThread::reset() +{ + joinThread(); +} + +void OpcUaAsyncExecThread::joinThread() +{ + if (thread.joinable()) + thread.join(); +} + +OpcUaAsyncExecThread& OpcUaAsyncExecThread::operator=(std::thread&& thread) +{ + joinThread(); + this->thread = std::move(thread); + return *this; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuacallmethodrequest.cpp b/shared/libraries/opcua/opcuaclient/src/opcuacallmethodrequest.cpp new file mode 100644 index 0000000..4b9b560 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuacallmethodrequest.cpp @@ -0,0 +1,28 @@ +#include "opcuaclient/opcuacallmethodrequest.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaCallMethodRequest::OpcUaCallMethodRequest(const OpcUaNodeId& methodId, + const OpcUaNodeId& objectId, + size_t inputArgumentsSize, + UA_Variant* inputArguments) + : OpcUaObject() +{ + value.methodId = methodId.copyAndGetDetachedValue(); + value.objectId = objectId.copyAndGetDetachedValue(); + value.inputArgumentsSize = inputArgumentsSize; + CheckStatusCodeException( + UA_Array_copy(inputArguments, inputArgumentsSize, (void**) &value.inputArguments, &UA_TYPES[UA_TYPES_VARIANT])); +} + +OpcUaCallMethodRequestWithCallback::OpcUaCallMethodRequestWithCallback(const OpcUaNodeId& methodId, + const OpcUaNodeId& objectId, + const ProcessFunctionType& processFunction, + size_t inputArgumentsSize, + UA_Variant* inputArguments) + : OpcUaCallMethodRequest(methodId, objectId, inputArgumentsSize, inputArguments) + , processFunction(processFunction) +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp new file mode 100644 index 0000000..ebb9ccb --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuaclient.cpp @@ -0,0 +1,572 @@ +#include "opcuaclient/opcuaclient.h" +#include +#include + +#include +#include +#include +#include "opcuashared/opcuasecuritycommon.h" + +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using namespace daq::utils; + +// LockedClient +ClientLockGuard::ClientLockGuard(OpcUaClient* client) noexcept + : client(client) +{ + if (client != nullptr) + client->getLock().lock(); +} + +ClientLockGuard::ClientLockGuard(ClientLockGuard&& other) noexcept +{ + this->client = other.client; + other.client = nullptr; +} + +ClientLockGuard& ClientLockGuard::operator=(ClientLockGuard&& other) noexcept +{ + this->client = other.client; + other.client = nullptr; + return *this; +} + +ClientLockGuard::~ClientLockGuard() noexcept +{ + if (client != nullptr) + client->getLock().unlock(); +} + +ClientLockGuard::operator UA_Client*() +{ + return client != nullptr ? client->getUaClient() : nullptr; +} + +// OpcUaClient +OpcUaClient::OpcUaClient(const OpcUaEndpoint& endpoint) + : endpoint(endpoint) + , timerTasks() + , iterateThread("OpcUaClient") +{ + iterateThread.setCallback(std::bind(&OpcUaClient::executeIterateCallback, this)); + initialize(); +} + +OpcUaClient::OpcUaClient(const std::string& url) + : OpcUaClient(OpcUaEndpoint(url)) +{ +} + +OpcUaClient::~OpcUaClient() +{ + disconnect(); +} + +void OpcUaClient::initialize() +{ + std::lock_guard guard(getLock()); + + uaclient = UaClientFactory::Create(nullptr, UA_LOGLEVEL_WARNING, endpoint.getCustomDataTypes()); + + UA_Client_getConfig(uaclient)->clientContext = this; + + setTimeout(timeoutMs); + + setConnectivityCheckInterval(connectivityCheckInterval); +} + +UaClientFactory::UaClientFactory() +{ +} + +UA_Client* UaClientFactory::Create(const OpcUaClientSecurityConfig* securityConfig, + UA_LogLevel logLevel, + const UA_DataTypeArray* customDataTypes) +{ + UaClientFactory factory; + factory.setSecurityConfig(securityConfig); + factory.setLogLevel(logLevel); + factory.setCustomDataTypes(customDataTypes); + return factory.build(); +} + +void UaClientFactory::setSecurityConfig(const OpcUaClientSecurityConfig* securityConfig) +{ + this->securityConfig = securityConfig; +} + +void UaClientFactory::setLogLevel(const UA_LogLevel logLevel) +{ + this->logLevel = logLevel; +} + +void UaClientFactory::setCustomDataTypes(const UA_DataTypeArray* customDataTypes) +{ + this->customDataTypes = customDataTypes; +} + +void UaClientFactory::configureClient() +{ + // config->logger = UA_Log_Plog_withLevel(logLevel); + + if (securityConfig == NULL) + { + UA_StatusCode retval = UA_ClientConfig_setDefault(config); + if (retval != UA_STATUSCODE_GOOD) + throw OpcUaException(retval, "Failed to configure client defaults."); + } + else + configureClientSecurity(); + + config->customDataTypes = customDataTypes; + + configureClientConfigDefaults(); +} + +void UaClientFactory::configureClientSecurity() +{ + securityConfig->validate(); + + if (!securityConfig->hasCertificate()) + { + UA_StatusCode retval = UA_ClientConfig_setDefault(config); + if (retval != UA_STATUSCODE_GOOD) + throw OpcUaException(retval, "Failed to configure client defaults."); + } + else + { +#ifdef OPCUA_ENABLE_ENCRYPTION + UA_StatusCode retval = UA_ClientConfig_setDefaultEncryption(config, + securityConfig->certificate.getValue(), + securityConfig->privateKey.getValue(), + (securityConfig->trustAll) ? NULL : securityConfig->trustList.data(), + (securityConfig->trustAll) ? 0 : securityConfig->trustList.size(), + securityConfig->revocationList.data(), + securityConfig->revocationList.size()); + + if (retval != UA_STATUSCODE_GOOD) + throw OpcUaException(retval, "Failed to configure client encryption."); + + if (!securityConfig->trustAll && securityConfig->trustList.size() == 0) + config->certificateVerification.verifyCertificate = OpcUaSecurityCommon::verifyCertificateRejectAll; + + config->securityMode = securityConfig->securityMode; + configureClientAppUri(); +#else + throw OpcUaException(UA_STATUSCODE_BADINTERNALERROR, "Encryption was not enabled when building the project."); +#endif + } +} + +void UaClientFactory::configureClientAppUri() +{ + std::optional appUri = securityConfig->getAppUriOrParseFromCertificate(); + if (appUri.has_value()) + { + UA_String_clear(&config->clientDescription.applicationUri); + config->clientDescription.applicationUri = UA_STRING_ALLOC(appUri.value().c_str()); + } +} + +void UaClientFactory::configureClientConfigDefaults() +{ + config->timeout = OpcUaClient::CONNECTION_TIMEOUT_SECONDS * 1000; + config->connectivityCheckInterval = OpcUaClient::CONNECTION_TIMEOUT_SECONDS; +} + +UA_Client* UaClientFactory::build() +{ + client = UA_Client_new(); + config = UA_Client_getConfig(client); + + try + { + configureClient(); + } + catch (const OpcUaException&) + { + UA_Client_delete(client); + throw; + } + + return client; +} + +void OpcUaClient::connect() +{ + std::lock_guard guard(getLock()); + + if (!uaclient) + initialize(); + + UA_StatusCode status = UA_STATUSCODE_GOOD; + + if (endpoint.isAnonymous()) + status = UA_Client_connect(uaclient, endpoint.getUrl().c_str()); + else + status = + UA_Client_connectUsername(uaclient, endpoint.getUrl().c_str(), endpoint.getUsername().c_str(), endpoint.getPassword().c_str()); + + if (!OPCUA_STATUSCODE_SUCCEEDED(status)) + throw OpcUaException(status, "Failed to connect to OpcUa server"); +} + +void OpcUaClient::disconnect(bool doClear) +{ + stopIterate(); + std::lock_guard guard(getLock()); + + if (!uaclient) + return; + + UA_Client_disconnect(uaclient); + + if (doClear) + clear(); // to clear all internal states +} + +void OpcUaClient::clear() +{ + if (uaclient) + { + UA_Client_delete(uaclient); + uaclient = nullptr; + } +} + +UA_Client* OpcUaClient::getUaClient() +{ + return uaclient; +} + +ClientLockGuard OpcUaClient::getLockedUaClient() +{ + return ClientLockGuard(this); +} + +bool OpcUaClient::isConnected() +{ + std::lock_guard guard(getLock()); + if (!uaclient) + return false; + + UA_SecureChannelState channelState; + UA_SessionState sessionState; + UA_StatusCode connectStatus; + UA_Client_getState(uaclient, &channelState, &sessionState, &connectStatus); + + return OPCUA_STATUSCODE_SUCCEEDED(connectStatus) && channelState == UA_SECURECHANNELSTATE_OPEN; +} + +const OpcUaEndpoint& OpcUaClient::getEndpoint() const +{ + return endpoint; +} + +void OpcUaClient::setTimeout(uint32_t timeoutMs) +{ + std::lock_guard guard(getLock()); + this->timeoutMs = timeoutMs; + if (uaclient) + UA_Client_getConfig(uaclient)->timeout = timeoutMs; +} + +uint32_t OpcUaClient::getTimeout() const +{ + return this->timeoutMs; +} + +void OpcUaClient::setConnectivityCheckInterval(uint32_t connectivityCheckInterval) +{ + std::lock_guard guard(getLock()); + this->connectivityCheckInterval = connectivityCheckInterval; + if (uaclient) + { + auto* config = UA_Client_getConfig(uaclient); + + if (connectivityCheckInterval > config->secureChannelLifeTime) + THROW_RUNTIME_ERROR("Connectivity check interval [" << connectivityCheckInterval << "] exceeds secure channel life time [" + << config->secureChannelLifeTime << "]"); + + UA_Client_getConfig(uaclient)->connectivityCheckInterval = connectivityCheckInterval; + } +} + +std::recursive_mutex& OpcUaClient::getLock() +{ + return lock; +} + +void OpcUaClient::runIterate(std::chrono::milliseconds period, std::chrono::milliseconds iterateTimeout) +{ + assert(period.count() >= 0); + assert(iterateTimeout.count() >= 0); + + if (iterateThread.getStarted()) + throw std::runtime_error("Iterate already started."); + + iterateThread.setIntervalMs(period.count()); + this->iterateTimeout = iterateTimeout; + iterateThread.start(); +} + +void OpcUaClient::stopIterate() +{ + iterateThread.stop(); +} + +void OpcUaClient::executeIterateCallback() +{ + auto statusCode = iterate(iterateTimeout); + if (OPCUA_STATUSCODE_FAILED(statusCode)) + iterateThread.terminate(); +} + +UA_StatusCode OpcUaClient::iterate(std::chrono::milliseconds timeout) +{ + UA_Int32 timeoutMs = timeout.count(); + assert(timeoutMs >= 0); + return UA_Client_run_iterate_timer_tasks(getLockedUaClient(), timeoutMs, true); +} + +OpcUaCallbackIdent OpcUaClient::scheduleTimerTask(double intervalMs, const OpcUaTimerTaskType& task) +{ + OpcUaCallbackIdent callbackId; + + auto client = getLockedUaClient(); + + void* context = timerTasks.createContext(); + + UA_StatusCode status = UA_Client_addRepeatedCallback(client, OpcUaClient::timerTaskCallback, context, intervalMs, &callbackId); + + if (OPCUA_STATUSCODE_FAILED(status)) + { + timerTasks.deleteContext(context); + throw OpcUaException(status, "Failed to add repeated callback"); + } + + timerTasks.insertTimerTask(context, callbackId, task); + + return callbackId; +} + +void OpcUaClient::removeTimerTask(OpcUaCallbackIdent ident) +{ + auto client = getLockedUaClient(); + UA_Client_removeCallback(client, ident); + timerTasks.removeTimerTask(ident); +} + +bool OpcUaClient::timerTaskExists(OpcUaCallbackIdent ident) +{ + std::lock_guard guard(lock); + return timerTasks.timerTaskExists(ident); +} + +void OpcUaClient::timerTaskCallback(UA_Client* client, void* data) +{ + OpcUaCallbackIdent callbackIdent; + OpcUaTimerTaskType* task; + TimerTaskContextCollection::getTaskExecData(data, &callbackIdent, &task); + + OpcUaClient* opcUaClient = static_cast(UA_Client_getConfig(client)->clientContext); + TimerTaskControl control; + try + { + (*task)(*opcUaClient, control); + } + catch (const std::exception& e) + { + LOGE << "Error occured during executing user task. " << e.what(); + } + if (control.terminate) + opcUaClient->removeTimerTask(callbackIdent); +} + +OpcUaVariant OpcUaClient::readValue(const OpcUaNodeId& node) +{ + OpcUaVariant val; + CheckStatusCodeException(UA_Client_readValueAttribute(getLockedUaClient(), *node, val.get())); + + return val; +} + +OpcUaObject OpcUaClient::readNodeAttributes(const OpcUaObject& request) +{ + return UA_Client_Service_read(getLockedUaClient(), *request); +} + +void OpcUaClient::readNodeAttributes(const std::vector& requests) +{ + size_t requestCnt = std::size(requests); + if (requestCnt == 0) + return; + + OpcUaObject readRequest; + CheckStatusCodeException( + UA_Array_resize((void**) &readRequest->nodesToRead, &readRequest->nodesToReadSize, requestCnt, &UA_TYPES[UA_TYPES_READVALUEID])); + + for (size_t i = 0; i < requestCnt; i++) + UA_ReadValueId_copy(requests[i].get(), &readRequest->nodesToRead[i]); + + readRequest->timestampsToReturn = UA_TimestampsToReturn::UA_TIMESTAMPSTORETURN_NEITHER; + + OpcUaObject response = readNodeAttributes(readRequest); + CheckStatusCodeException(response->responseHeader.serviceResult); + + for (size_t i = 0; i < requestCnt; i++) + { + UA_DataValue* v = &response->results[i]; + requests[i].processFunction(v); + } +} + +OpcUaObject OpcUaClient::callMethods(const OpcUaObject& request) +{ + return UA_Client_Service_call(getLockedUaClient(), *request); +} + +UA_NodeClass OpcUaClient::readNodeClass(const OpcUaNodeId& nodeId) +{ + UA_NodeClass nodeClass; + CheckStatusCodeException(UA_Client_readNodeClassAttribute(getLockedUaClient(), *nodeId, &nodeClass)); + return nodeClass; +} + +bool OpcUaClient::nodeExists(const OpcUaNodeId& node) +{ + UA_NodeClass nodeClass; + auto status = UA_Client_readNodeClassAttribute(getLockedUaClient(), *node, &nodeClass); + if (status == UA_STATUSCODE_BADNODEIDUNKNOWN) + return false; + + CheckStatusCodeException(status); + + return true; +} + +std::string OpcUaClient::readBrowseName(const OpcUaNodeId& nodeId) +{ + OpcUaObject browseName; + CheckStatusCodeException(UA_Client_readBrowseNameAttribute(getLockedUaClient(), *nodeId, browseName.get())); + + return OpcUaNode::GetBrowseName(*browseName); +} + +std::string OpcUaClient::readDisplayName(const OpcUaNodeId& nodeId) +{ + OpcUaObject displayName; + CheckStatusCodeException(UA_Client_readDisplayNameAttribute(getLockedUaClient(), *nodeId, displayName.get())); + + return utils::ToStdString(displayName->text); +} + +size_t OpcUaClient::readDimension(const OpcUaNodeId& nodeId) +{ + OpcUaObject uaVar; + CheckStatusCodeException(UA_Client_readValueAttribute(getLockedUaClient(), *nodeId, uaVar.get())); + + if (uaVar->arrayLength > 0) + return uaVar->arrayLength; + else + return 1; +} + +void OpcUaClient::writeDisplayName(const OpcUaNodeId& nodeId, const std::string& displayName) +{ + OpcUaObject uaDisplayName = UA_LOCALIZEDTEXT_ALLOC("", displayName.c_str()); + writeDisplayName(nodeId, uaDisplayName); +} + +void OpcUaClient::writeDisplayName(const OpcUaNodeId& nodeId, const OpcUaObject& displayName) +{ + auto status = UA_Client_writeDisplayNameAttribute(getLockedUaClient(), *nodeId, displayName.get()); + CheckStatusCodeException(status); +} + +std::string OpcUaClient::readDescription(const OpcUaNodeId& nodeId) +{ + OpcUaObject description; + auto status = UA_Client_readDescriptionAttribute(getLockedUaClient(), *nodeId, description.get()); + CheckStatusCodeException(status); + return utils::ToStdString(description->text); +} + +void OpcUaClient::writeDescription(const OpcUaNodeId& nodeId, const std::string& description) +{ + OpcUaObject uaDescription = UA_LOCALIZEDTEXT_ALLOC("", description.c_str()); + writeDescription(nodeId, uaDescription); +} + +void OpcUaClient::writeDescription(const OpcUaNodeId& nodeId, const OpcUaObject& description) +{ + auto status = UA_Client_writeDescriptionAttribute(getLockedUaClient(), *nodeId, description.get()); + CheckStatusCodeException(status); +} + +OpcUaNodeId OpcUaClient::readDataType(const OpcUaNodeId& nodeId) +{ + OpcUaNodeId dataTypeId; + const auto status = UA_Client_readDataTypeAttribute(getLockedUaClient(), *nodeId, dataTypeId.get()); + CheckStatusCodeException(status); + return dataTypeId; +} + +OpcUaObject OpcUaClient::callMethod(const OpcUaCallMethodRequest& callRequest) +{ + OpcUaObject request; + + auto copyCallRequest = callRequest.copy(); + request->methodsToCall = copyCallRequest.get(); + request->methodsToCallSize = 1; + + OpcUaObject response = callMethods(request); + + request->methodsToCall = nullptr; + request->methodsToCallSize = 0; + + CheckStatusCodeException(response->responseHeader.serviceResult); + return response->results[0]; +} + +void OpcUaClient::callMethods(const std::vector& container) +{ + size_t nodesCnt = std::size(container); + if (nodesCnt == 0) + return; + + UA_CallMethodRequest* items = (UA_CallMethodRequest*) UA_Array_new(nodesCnt, &UA_TYPES[UA_TYPES_CALLMETHODREQUEST]); + for (size_t i = 0; i < nodesCnt; i++) + items[i] = *container[i]; // optimization. reinit before delete + + OpcUaObject request; + + request->methodsToCall = items; + request->methodsToCallSize = nodesCnt; + + OpcUaObject response = callMethods(request); + + for (size_t i = 0; i < nodesCnt; i++) + UA_CallMethodRequest_init(&items[i]); + + CheckStatusCodeException(response->responseHeader.serviceResult); + + for (size_t i = 0; i < nodesCnt; i++) + container[i].processFunction(response->results[i]); +} + +void OpcUaClient::writeValue(const OpcUaNodeId& nodeId, const OpcUaVariant& value) +{ + CheckStatusCodeException(UA_Client_writeValueAttribute(getLockedUaClient(), *nodeId, value.get())); +} + +Subscription* OpcUaClient::createSubscription(const OpcUaObject& request, + const StatusChangeNotificationCallbackType& statusChangeCallback) +{ + return Subscription::CreateSubscription(this, request, statusChangeCallback); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuanodefactory.cpp b/shared/libraries/opcua/opcuaclient/src/opcuanodefactory.cpp new file mode 100644 index 0000000..959be7f --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuanodefactory.cpp @@ -0,0 +1,102 @@ +#include + +#include "opcuaclient/opcuanodefactory.h" +#include "opcuashared/node/opcuanodeobject.h" +#include "opcuashared/node/opcuadatatype.h" +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +/*IOpcUaNodeFactory*/ + +OpcUaNodeId IOpcUaNodeFactory::GetOpcUaNodeId(const UA_ReferenceDescription& referenceDescription) +{ + return OpcUaNodeId(referenceDescription.nodeId.nodeId); +} + +/*OpcUaNodeFactory*/ + +OpcUaNodeFactory::OpcUaNodeFactory(const OpcUaClientPtr& client) + : client(client) +{ +} + +OpcUaNodePtr OpcUaNodeFactory::instantiateNode(const UA_ReferenceDescription& reference, + const OpcUaNodeId& parentNodeId, + bool& traverseChild) +{ + traverseChild = true; + switch (reference.nodeClass) + { + case UA_NODECLASS_VARIABLE: + { + OpcUaNodeId uaTypeNodeId; + UA_StatusCode status = + UA_Client_readDataTypeAttribute(client->getLockedUaClient(), reference.nodeId.nodeId, uaTypeNodeId.get()); + + if (OPCUA_STATUSCODE_SUCCEEDED(status)) + return std::make_shared(reference, uaTypeNodeId); + else + return nullptr; + } + case UA_NODECLASS_METHOD: + { + assert(!parentNodeId.isNull()); + OpcUaNodeMethodPtr nodeMethod = std::make_shared(reference, parentNodeId); + + prepareMethodParams(nodeMethod); + + return nodeMethod; + } + case UA_NODECLASS_OBJECT: + return std::make_shared(reference); + case UA_NODECLASS_DATATYPE: + return std::make_shared(reference); + default: + return nullptr; + } +} + +void OpcUaNodeFactory::prepareMethodParams(OpcUaNodeMethodPtr nodeMethod) +{ + // conditions to browse the method argument nodes: referencetype = UA_NS0ID_HASPROPERTY (=46) && brosweName = InputArguments | + // OutputArguments + + BrowseRequest request(nodeMethod->getNodeId(), OpcUaNodeClass::Variable, OpcUaNodeId(UA_NS0ID_HASPROPERTY)); + + OpcUaBrowser browser(request, client); + browseAndApplyMethodParams(browser, nodeMethod); + + nodeMethod->initTypeDescription(); +} + +void OpcUaNodeFactory::browseAndApplyMethodParams(OpcUaBrowser& browser, const OpcUaNodeMethodPtr& nodeMethod) +{ + try + { + auto& references = browser.browse(); + + for (auto& ref : references) + applyMethodParam(ref, nodeMethod); + } + catch (const OpcUaException& ex) + { + throw OpcUaException(ex.getStatusCode(), + "Method initialization: " + std::string(ex.what()) + ". Node: " + nodeMethod->getNodeId().toString()); + } +} + +void OpcUaNodeFactory::applyMethodParam(const UA_ReferenceDescription& reference, const OpcUaNodeMethodPtr& nodeMethod) +{ + auto browseName = OpcUaNode::GetBrowseName(reference.browseName); + + if (browseName == "InputArguments") + nodeMethod->addInputParameter("", OpcUaNodeId()); + else if (browseName == "OutputArguments") + nodeMethod->addOutputParameter("", OpcUaNodeId()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp b/shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp new file mode 100644 index 0000000..735f9b7 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuareadvalueid.cpp @@ -0,0 +1,14 @@ +#include "opcuaclient/opcuareadvalueid.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaReadValueIdWithCallback::OpcUaReadValueIdWithCallback(const OpcUaNodeId& nodeId, + const ProcessFunctionType& processFunction, + AttributeIdType attributeId) + : processFunction(processFunction) +{ + getValue().nodeId = nodeId.copyAndGetDetachedValue(); + getValue().attributeId = attributeId; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuatimertaskcontextcollection.cpp b/shared/libraries/opcua/opcuaclient/src/opcuatimertaskcontextcollection.cpp new file mode 100644 index 0000000..f8c1ba5 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuatimertaskcontextcollection.cpp @@ -0,0 +1,62 @@ +#include "opcuaclient/opcuatimertaskcontextcollection.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +TimerTaskContextCollection::TimerTaskContextCollection() +{ +} + +void* TimerTaskContextCollection::createContext() +{ + return new KeyType(this); +} + +void TimerTaskContextCollection::deleteContext(void* context) +{ + delete static_cast(context); +} + +void TimerTaskContextCollection::insertTimerTask(void* context, OpcUaCallbackIdent ident, const OpcUaTimerTaskType& task) +{ + assert(context != nullptr); + auto keyType = static_cast(context); + assert(keyType->owner == this); + + keyType->ident = ident; + + taskCallbacks.insert(std::pair(keyType, task)); +} + +void TimerTaskContextCollection::removeTimerTask(OpcUaCallbackIdent ident) +{ + KeyType keyType; + keyType.ident = ident; + + auto it = taskCallbacks.find(&keyType); + if (it != taskCallbacks.end()) + { + KeyType* kt = it->first; + taskCallbacks.erase(&keyType); + deleteContext(kt); + } +} + +bool TimerTaskContextCollection::timerTaskExists(OpcUaCallbackIdent ident) const +{ + KeyType keyType; + keyType.ident = ident; + + return taskCallbacks.find(&keyType) != taskCallbacks.end(); +} + +void TimerTaskContextCollection::getTaskExecData(void* context, + OpcUaCallbackIdent* callbackIdent, + OpcUaTimerTaskType** task) +{ + KeyType* keyType = static_cast(context); + + *callbackIdent = keyType->ident; + *task = &keyType->owner->taskCallbacks[keyType]; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/opcuatimertaskhelper.cpp b/shared/libraries/opcua/opcuaclient/src/opcuatimertaskhelper.cpp new file mode 100644 index 0000000..6bb2589 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/opcuatimertaskhelper.cpp @@ -0,0 +1,80 @@ +#include "opcuaclient/opcuatimertaskhelper.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using namespace daq::utils; + +OpcUaTimerTaskHelper::OpcUaTimerTaskHelper(OpcUaClient& client, int intervalMs, const CallbackFunction& callback) + : intervalMs(intervalMs) + , terminated{} + , client(client) + , callback(callback) +{ + assert(callback); +} + +OpcUaTimerTaskHelper::~OpcUaTimerTaskHelper() +{ + stop(); +} + +void OpcUaTimerTaskHelper::start() +{ + if (!getStarted()) + { + terminated = false; + + using namespace std::placeholders; + callbackIdent = client.scheduleTimerTask(intervalMs, std::bind(&OpcUaTimerTaskHelper::execute, this, _1, _2)); + } +} + +void OpcUaTimerTaskHelper::stop() +{ + if (getStarted()) + { + terminated = true; + client.removeTimerTask(callbackIdent.value()); + } + callbackIdent.reset(); +} + +double OpcUaTimerTaskHelper::getIntervalMs() const +{ + return intervalMs; +} + +void OpcUaTimerTaskHelper::setIntervalMs(const double value) +{ + intervalMs = value; +} + +void OpcUaTimerTaskHelper::terminate() +{ + terminated = true; +} + +const std::atomic& OpcUaTimerTaskHelper::getTerminated() const +{ + return terminated; +} + +bool OpcUaTimerTaskHelper::getStarted() const +{ + return callbackIdent.has_value(); +} + +void OpcUaTimerTaskHelper::execute(OpcUaClient& client, TimerTaskControl& control) +{ + daq::utils::Finally checkTerminate( + [this, &control]() + { + if (this->terminated) + control.terminateTimerTask(); + }); + + callback(client); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/request_handler.cpp b/shared/libraries/opcua/opcuaclient/src/request_handler.cpp new file mode 100644 index 0000000..3a886b3 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/request_handler.cpp @@ -0,0 +1,9 @@ +#include "opcuaclient/request_handler.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +RequestHandler::RequestHandler(const OpcUaClientPtr& client) +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/subscriptions.cpp b/shared/libraries/opcua/opcuaclient/src/subscriptions.cpp new file mode 100644 index 0000000..2deca10 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/subscriptions.cpp @@ -0,0 +1,175 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +/*Subscription*/ + +Subscription::Subscription(OpcUaClient* client, const StatusChangeNotificationCallbackType& statusChangeNotificationCallback) + : client(client) + , statusChangeNotificationCallback(statusChangeNotificationCallback) +{ +} + +UA_UInt32 Subscription::getSubscriptionId() const +{ + return subscriptionResponse->subscriptionId; +} + +UA_Double Subscription::getRevisedPublishingInterval() const +{ + return subscriptionResponse->revisedPublishingInterval; +} + +UA_UInt32 Subscription::getRevisedLifetimeCount() const +{ + return subscriptionResponse->revisedLifetimeCount; +} + +UA_UInt32 Subscription::getRevisedMaxKeepAliveCount() const +{ + return subscriptionResponse->revisedMaxKeepAliveCount; +} + +const StatusChangeNotificationCallbackType& Subscription::getStatusChangeNotificationCallback() const +{ + return statusChangeNotificationCallback; +} + +static void DeleteSubscriptionCallback(UA_Client* client, UA_UInt32 subId, void* subContext) +{ + delete (Subscription*) subContext; +} + +static void StatusChangeNotificationCallback(UA_Client* client, + UA_UInt32 subId, + void* subContext, + UA_StatusChangeNotification* notification) +{ + auto subscription = (Subscription*) subContext; + if (subscription->getStatusChangeNotificationCallback()) + { + OpcUaClient* opcUaClient = static_cast(UA_Client_getContext(client)); + subscription->getStatusChangeNotificationCallback()(opcUaClient, subscription, notification); + } +} + +Subscription* Subscription::CreateSubscription(OpcUaClient* client, + const OpcUaObject& request, + const StatusChangeNotificationCallbackType& statusChangeCallback) +{ + auto subscription = new Subscription(client, statusChangeCallback); + + OpcUaObject response = UA_Client_Subscriptions_create( + client->getLockedUaClient(), *request, subscription, StatusChangeNotificationCallback, DeleteSubscriptionCallback); + + subscription->subscriptionResponse = response; + + CheckStatusCodeException(response->responseHeader.serviceResult, "Failed to create subscription"); + + return subscription; +} + +/*MonitoredItem*/ + +static void DeleteMonitoredItemCallback(UA_Client* client, UA_UInt32 subId, void* subContext, UA_UInt32 monId, void* monContext) +{ + delete (MonitoredItem*) monContext; +} + +static void EventNotificationCallback( + UA_Client* client, UA_UInt32 subId, void* subContext, UA_UInt32 monId, void* monContext, size_t nEventFields, UA_Variant* eventFields) +{ + auto monitoredItem = (MonitoredItem*) monContext; + if (monitoredItem->getEventNotificationCallback()) + { + OpcUaClient* opcUaClient = static_cast(UA_Client_getContext(client)); + auto subscription = (Subscription*) subContext; + monitoredItem->getEventNotificationCallback()(opcUaClient, subscription, monitoredItem, nEventFields, eventFields); + } +} + +static void DataChangeNotificationCallback( + UA_Client* client, UA_UInt32 subId, void* subContext, UA_UInt32 monId, void* monContext, UA_DataValue* value) +{ + auto monitoredItem = (MonitoredItem*) monContext; + if (monitoredItem->getDataChangeNotificationCallback()) + { + OpcUaClient* opcUaClient = static_cast(UA_Client_getContext(client)); + auto subscription = (Subscription*) subContext; + monitoredItem->getDataChangeNotificationCallback()(opcUaClient, subscription, monitoredItem, value); + } +} + +MonitoredItem* Subscription::monitoredItemsCreateEvent(UA_TimestampsToReturn timestampsToReturn, + const UA_MonitoredItemCreateRequest& item, + const EventNotificationCallbackType& eventNotificationCallback) +{ + auto monitoredItem = new MonitoredItem(client, eventNotificationCallback); + + UA_MonitoredItemCreateResult result = UA_Client_MonitoredItems_createEvent(client->getLockedUaClient(), + getSubscriptionId(), + UA_TIMESTAMPSTORETURN_BOTH, + item, + monitoredItem, + EventNotificationCallback, + DeleteMonitoredItemCallback); + + monitoredItem->response = result; + + CheckStatusCodeException(result.statusCode, "Failed to create monitored item"); + + return monitoredItem; +} + +MonitoredItem* Subscription::monitoredItemsCreateDataChange(UA_TimestampsToReturn timestampsToReturn, + const UA_MonitoredItemCreateRequest& item, + const DataChangeNotificationCallbackType& dataChangeNotificationCallback) +{ + auto monitoredItem = new MonitoredItem(client, dataChangeNotificationCallback); + + UA_MonitoredItemCreateResult result = UA_Client_MonitoredItems_createDataChange(client->getLockedUaClient(), + getSubscriptionId(), + UA_TIMESTAMPSTORETURN_BOTH, + item, + monitoredItem, + DataChangeNotificationCallback, + DeleteMonitoredItemCallback); + + monitoredItem->response = result; + + CheckStatusCodeException(result.statusCode, "Failed to create monitored item"); + + return monitoredItem; +} + +/*MonitoredItem*/ + +MonitoredItem::MonitoredItem(OpcUaClient* client, const EventNotificationCallbackType& eventNotificationCallback) + : client(client) + , eventNotificationCallback(eventNotificationCallback) +{ +} + +MonitoredItem::MonitoredItem(OpcUaClient* client, const DataChangeNotificationCallbackType& dataChangeNotificationCallback) + : client(client) + , dataChangeNotificationCallback(dataChangeNotificationCallback) +{ +} + +UA_UInt32 MonitoredItem::getMonitoredItemId() const +{ + return response->monitoredItemId; +} + +const EventNotificationCallbackType& MonitoredItem::getEventNotificationCallback() const +{ + return eventNotificationCallback; +} + +const DataChangeNotificationCallbackType& MonitoredItem::getDataChangeNotificationCallback() const +{ + return dataChangeNotificationCallback; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/src/taskprocessor/opcuataskprocessor.cpp b/shared/libraries/opcua/opcuaclient/src/taskprocessor/opcuataskprocessor.cpp new file mode 100644 index 0000000..0909116 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/src/taskprocessor/opcuataskprocessor.cpp @@ -0,0 +1,158 @@ +#include "opcuaclient/taskprocessor/opcuataskprocessor.h" + +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using namespace daq::utils; + +OpcUaTaskProcessor::OpcUaTaskProcessor(const OpcUaClientPtr& client) + : ThreadEx() + , client(client) + , prevStatus(UA_STATUSCODE_GOOD) +{ + setPriority(ThreadExPriority::timeCritical); +} + +OpcUaTaskProcessor::~OpcUaTaskProcessor() +{ + OpcUaTaskProcessor::stop(); +} + +bool OpcUaTaskProcessor::isConnected() +{ + return connected; +} + +void OpcUaTaskProcessor::start() +{ + connected = client->isConnected(); + ThreadEx::start(); +} + +void OpcUaTaskProcessor::stop() +{ + terminate(); + cvWait.notify_one(); + ThreadEx::stop(); +} + +void OpcUaTaskProcessor::executeTask(const OpcUaTaskType& task) +{ + auto future = executeTaskAwait(task); + future.get(); +} + +void OpcUaTaskProcessor::executeTask(std::promise& promise, const OpcUaTaskType& task) const +{ + try + { + task(*client); + promise.set_value(); + } + catch (...) + { + try + { + promise.set_exception(std::current_exception()); + } + catch (...) // set_exception() may throw too + { + } + } +} + +bool OpcUaTaskProcessor::connect() +{ + bool result = false; + + auto connectTask = [&result](OpcUaClient& client) + { + try + { + client.connect(); + } + catch (const OpcUaException& /*e*/) + { + // ignored + } + + result = client.isConnected(); + }; + + executeTask(connectTask); + return result; +} + +void OpcUaTaskProcessor::setConnectionTimeout(uint32_t timeoutMs) +{ + executeTask([timeoutMs](OpcUaClient& client) { client.setTimeout(timeoutMs); }); +} + +const OpcUaClientPtr& OpcUaTaskProcessor::getClient() const noexcept +{ + return client; +} + +std::future OpcUaTaskProcessor::executeTaskAwait(const OpcUaTaskType& task) +{ + if (std::this_thread::get_id() == executingThreadId) // to prevent deadlock. if its called inside task + { + std::promise promise; + executeTask(promise, task); + return promise.get_future(); + } + else + { + std::lock_guard guard(mutex); + queue.push(std::make_pair(std::promise(), task)); + return queue.back().first.get_future(); + } +} + +OpcUaTaskProcessor::QueueItemType OpcUaTaskProcessor::getNextTask() +{ + std::lock_guard guard(mutex); + OpcUaTaskProcessor::QueueItemType task; + if (!queue.empty()) + { + task = std::move(queue.front()); + queue.pop(); + } + + return task; +} + +void OpcUaTaskProcessor::execute() +{ + executingThreadId = std::this_thread::get_id(); + setThreadName("OpcUaTaskProcessor_Normal"); + while (!terminated) + { + QueueItemType task = getNextTask(); + if (task.second) + { + executeTask(task.first, task.second); + } + else + { + static constexpr std::chrono::milliseconds timeout(1); + UA_StatusCode status = client->iterate(timeout); + + if (OPCUA_STATUSCODE_FAILED(status) && prevStatus != status) // don't log repeating errors + LOGE << "OpcUa error occured during client iterate " << OPCUA_STATUSCODE_LOG_MESSAGE(status); + + prevStatus = status; + + if (OPCUA_STATUSCODE_FAILED(status)) // If client disconnects, we don't wait on UA_SELECT. So wait here + { + auto uniqueLock = std::unique_lock(waitMutex); + cvWait.wait_for(uniqueLock, std::chrono::milliseconds(timeout)); + } + } + + connected = client->isConnected(); + } +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/CMakeLists.txt b/shared/libraries/opcua/opcuaclient/tests/CMakeLists.txt new file mode 100644 index 0000000..f230794 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/CMakeLists.txt @@ -0,0 +1,44 @@ +set(MODULE_NAME opcuaclient) +set(TEST_APP test_${MODULE_NAME}) + +set(SRC_Cpp main.cpp + opcuaservertesthelper.cpp + test_opcuanodefactory.cpp + test_opcuanodefactorybrowser.cpp + test_opcuaasyncexecthread.cpp + test_opcuataskprocessor.cpp + test_opcuatimertaskhelper.cpp + test_opcuaclient.cpp + test_attribute_reader.cpp + test_cached_reference_browser.cpp +) + +set(SRC_Include opcuaservertesthelper.h +) + +opendaq_prepend_path("include" SRC_Include) +opendaq_prepend_path("src" SRC_Cpp) + +add_executable(${TEST_APP} ${SRC_Cpp} + ${SRC_Include} +) + +target_link_libraries(${TEST_APP} PRIVATE ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::test_utils + daq::opcuaserver #remove after moving test_opcuaclientbasedsacqcontrol to opcua server/client integration tests +) + +target_include_directories(${TEST_APP} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (MSVC) + target_compile_options(${TEST_APP} PRIVATE /bigobj) +endif() + +if(OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}coverage ${TEST_APP} ${MODULE_NAME}coverage) +endif() diff --git a/shared/libraries/opcua/opcuaclient/tests/include/opcuaservertesthelper.h b/shared/libraries/opcua/opcuaclient/tests/include/opcuaservertesthelper.h new file mode 100644 index 0000000..29a536b --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/include/opcuaservertesthelper.h @@ -0,0 +1,103 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "opcuaclient/opcuaclient.h" +#include "opcuashared/opcua.h" +#include "opcuashared/opcuacommon.h" +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +#define ASSERT_EQ_STATUS(status, expectedStatus) ASSERT_EQ(status, (UA_StatusCode) expectedStatus) + +class OpcUaServerTestHelper final +{ +public: + using OnConfigureCallback = std::function; + + OpcUaServerTestHelper(); + ~OpcUaServerTestHelper(); + + void setSessionTimeout(double sessionTimeoutMs); + + void onConfigure(const OnConfigureCallback& callback); + void startServer(); + void stop(); + + std::string getServerUrl() const; + + void publishVariable(std::string identifier, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale = "en_US", + uint16_t nodeIndex = 1, + size_t dimension = 1); + +private: + void runServer(); + void createModel(); + void publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale = "en_US", int nodeIndex = 1); + void publishMethod(std::string identifier, UA_NodeId* parentNodeId, const char* locale = "en_US", int nodeIndex = 1); + static UA_StatusCode helloMethodCallback(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionHandle, + const UA_NodeId* methodId, + void* methodContext, + const UA_NodeId* objectId, + void* objectContext, + size_t inputSize, + const UA_Variant* input, + size_t outputSize, + UA_Variant* output); + + double sessionTimeoutMs; + UA_Server* server{}; + std::unique_ptr serverThreadPtr; + std::atomic serverRunning = false; + + UA_UInt16 port = 4842u; + OnConfigureCallback onConfigureCallback; +}; + +class BaseClientTest : public testing::Test +{ +protected: + void SetUp() override; + void TearDown() override; + OpcUaServerTestHelper testHelper; + + std::string getServerUrl() const; + + static void IterateAndWaitForPromise(OpcUaClient& client, const std::future& future) + { + using namespace std::chrono; + while (client.iterate(milliseconds(10)) == UA_STATUSCODE_GOOD && + future.wait_for(milliseconds(1)) != std::future_status::ready) + { + }; + } + + OpcUaClientPtr prepareAndConnectClient(int timeout = -1); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/main.cpp b/shared/libraries/opcua/opcuaclient/tests/src/main.cpp new file mode 100644 index 0000000..457eff2 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/main.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int argc, char** args) +{ + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new MemCheckListener()); + + return RUN_ALL_TESTS(); +} diff --git a/shared/libraries/opcua/opcuaclient/tests/src/opcuaservertesthelper.cpp b/shared/libraries/opcua/opcuaclient/tests/src/opcuaservertesthelper.cpp new file mode 100644 index 0000000..4cd2445 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/opcuaservertesthelper.cpp @@ -0,0 +1,337 @@ +#include "opcuaservertesthelper.h" +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaServerTestHelper::OpcUaServerTestHelper() + : sessionTimeoutMs(-1) +{ +} + +OpcUaServerTestHelper::~OpcUaServerTestHelper() +{ + stop(); +} + +void OpcUaServerTestHelper::setSessionTimeout(double sessionTimeoutMs) +{ + this->sessionTimeoutMs = sessionTimeoutMs; +} + +void OpcUaServerTestHelper::runServer() +{ + while (serverRunning) + UA_Server_run_iterate(server, true); + + UA_Server_run_shutdown(server); + UA_Server_delete(server); +} + +void OpcUaServerTestHelper::onConfigure(const OnConfigureCallback& callback) +{ + onConfigureCallback = callback; +} + +void OpcUaServerTestHelper::startServer() +{ + serverRunning = true; + + UA_ServerConfig initConfig; + std::memset(&initConfig, 0, sizeof(UA_ServerConfig)); + + if (onConfigureCallback) + onConfigureCallback(&initConfig); + + UA_ServerConfig_setMinimal(&initConfig, port, nullptr); + server = UA_Server_newWithConfig(&initConfig); + UA_ServerConfig* config = UA_Server_getConfig(server); + + if (sessionTimeoutMs > 0) + config->maxSessionTimeout = sessionTimeoutMs; + +#ifdef UA_ENABLE_WEBSOCKET_SERVER + UA_ServerConfig_addNetworkLayerWS(config, 80, 0, 0); +#endif // UA_ENABLE_WEBSOCKET_SERVER + + createModel(); + + UA_Server_run_startup(server); + +#if SYNTH_SERVER_DEBUG + runServer(); +#else + serverThreadPtr = std::make_unique(&OpcUaServerTestHelper::runServer, this); +#endif +} + +void OpcUaServerTestHelper::stop() +{ + serverRunning = false; + if (serverThreadPtr) + { + serverThreadPtr->join(); + serverThreadPtr.reset(); + } +} + +std::string OpcUaServerTestHelper::getServerUrl() const +{ + std::stringstream ss; + ss << "opc.tcp://127.0.0.1"; + ss << ":"; + ss << port; + return ss.str(); +} + +void OpcUaServerTestHelper::createModel() +{ + auto uaObjectsFolder = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); + + publishFolder("f1", &uaObjectsFolder); + + UA_Int32 myInt32; + + myInt32 = 33; + publishVariable("f1.i", &myInt32, &UA_TYPES[UA_TYPES_INT32], OpcUaNodeId(1, "f1").getPtr()); + + myInt32 = 41; + publishVariable(".i32", &myInt32, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder); + + UA_UInt32 myUInt32 = 19; + publishVariable(".ui32", &myUInt32, &UA_TYPES[UA_TYPES_UINT32], &uaObjectsFolder); + + UA_Int16 myInt16 = 16; + publishVariable(".i16", &myInt16, &UA_TYPES[UA_TYPES_INT16], &uaObjectsFolder); + + UA_UInt16 myUInt16 = 33; + publishVariable(".ui16", &myUInt16, &UA_TYPES[UA_TYPES_UINT16], &uaObjectsFolder); + + UA_Int64 myInt64 = 64; + publishVariable(".i64", &myInt64, &UA_TYPES[UA_TYPES_INT64], &uaObjectsFolder); + + UA_Boolean myBool = true; + publishVariable(".b", &myBool, &UA_TYPES[UA_TYPES_BOOLEAN], &uaObjectsFolder); + + UA_Double myDouble = (UA_Double) 1885 / (UA_Double) 14442; + publishVariable(".d", &myDouble, &UA_TYPES[UA_TYPES_DOUBLE], &uaObjectsFolder); + + UA_Float myFloat = (UA_Float) 1 / (UA_Float) 3; + publishVariable(".f", &myFloat, &UA_TYPES[UA_TYPES_FLOAT], &uaObjectsFolder); + + UA_String myString = UA_STRING_ALLOC("Hello Dewesoft"); + publishVariable(".s", &myString, &UA_TYPES[UA_TYPES_STRING], &uaObjectsFolder); + UA_String_clear(&myString); + + UA_Guid myGuid = UA_GUID("8a336ac1-8632-482c-a565-23e6a9ad1abc"); + publishVariable(".g", &myGuid, &UA_TYPES[UA_TYPES_GUID], &uaObjectsFolder); + + UA_StatusCode myStatus = UA_STATUSCODE_GOODSUBSCRIPTIONTRANSFERRED; + publishVariable(".sc", &myStatus, &UA_TYPES[UA_TYPES_STATUSCODE], &uaObjectsFolder); + + // vectors + + UA_Int32 myVecInt32[] = {12, 13, 15, 18}; + publishVariable(".i32v", &myVecInt32, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder, "en_US", 1, 4); + + UA_Int16 myVecInt16[] = {65, 18, 12, 17, 33, 10023, 12, 1, 0, -1}; + publishVariable(".i16v", &myVecInt16, &UA_TYPES[UA_TYPES_INT16], &uaObjectsFolder, "en_US", 1, 10); + + UA_Int64 myVecInt64[] = {55, 1993}; + publishVariable(".i64v", &myVecInt64, &UA_TYPES[UA_TYPES_INT64], &uaObjectsFolder, "en_US", 1, 2); + + UA_Boolean myVecBool[] = {true, false}; + publishVariable(".bv", &myVecBool, &UA_TYPES[UA_TYPES_BOOLEAN], &uaObjectsFolder, "en_US", 1, 2); + + UA_Double myVecDouble[] = {(UA_Double) 1993 / (UA_Double) 6625, (UA_Double) 185 / (UA_Double) 1443, (UA_Double) 1.44, (UA_Double) 9948}; + publishVariable(".dv", &myVecDouble, &UA_TYPES[UA_TYPES_DOUBLE], &uaObjectsFolder, "en_US", 1, 4); + + UA_Float myVecFloat[] = {(UA_Float) 7 / (UA_Float) 2, (UA_Float) 1 / (UA_Float) 5}; + publishVariable(".fv", &myVecFloat, &UA_TYPES[UA_TYPES_FLOAT], &uaObjectsFolder, "en_US", 1, 2); + + // methods + + publishMethod("hello.dewesoft", &uaObjectsFolder); + + // structures + + OpcUaNodeId structureNodeId(1, ".sctA"); + + myInt32 = 56; + publishVariable(".sctA", &myInt32, &UA_TYPES[UA_TYPES_INT32], &uaObjectsFolder); + + myInt32 = 5641; + publishVariable(".sctA.i32", &myInt32, &UA_TYPES[UA_TYPES_INT32], structureNodeId.getPtr()); + + UA_Double mySctDouble = (UA_Double) 9844 / (UA_Double) 19774; + publishVariable(".sctA.d", &mySctDouble, &UA_TYPES[UA_TYPES_DOUBLE], structureNodeId.getPtr()); + + UA_String mySctString = UA_STRING_ALLOC("Hello Dewesoft @ struct"); + publishVariable(".sctA.s", &mySctString, &UA_TYPES[UA_TYPES_STRING], structureNodeId.getPtr()); + UA_String_clear(&mySctString); +} + +void OpcUaServerTestHelper::publishVariable(std::string identifier, + const void* value, + const UA_DataType* type, + UA_NodeId* parentNodeId, + const char* locale, + uint16_t nodeIndex, + size_t dimension) +{ + OpcUaObject attr = UA_VariableAttributes_default; + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->dataType = type->typeId; + attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + + OpcUaNodeId newNodeId(nodeIndex, identifier); + + OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), identifier.c_str()); + + if (dimension > 1) + { + attr->valueRank = 1; + attr->arrayDimensionsSize = 1; + attr->arrayDimensions = static_cast(UA_Array_new(1, &UA_TYPES[UA_TYPES_UINT32])); + attr->arrayDimensions[0] = UA_UInt32(dimension); + UA_Variant_setArrayCopy(&attr->value, value, dimension, type); + } + else + { + UA_Variant_setScalarCopy(&attr->value, value, type); + } + auto status = UA_Server_addVariableNode(server, + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + *qualifiedName, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), + *attr, + NULL, + NULL); + + CheckStatusCodeException(status); +} + +void OpcUaServerTestHelper::publishFolder(const char* identifier, UA_NodeId* parentNodeId, const char* locale, int nodeIndex) +{ + OpcUaObject attr = UA_ObjectAttributes_default; + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier); + + OpcUaNodeId newNodeId(nodeIndex, identifier); + + OpcUaObject qualifiedName = UA_QUALIFIEDNAME_ALLOC(UA_UInt16(nodeIndex), identifier); + + auto status = UA_Server_addObjectNode(server, + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + *qualifiedName, + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), + *attr, + NULL, + NULL); + + CheckStatusCodeException(status); +} + +void OpcUaServerTestHelper::publishMethod(std::string identifier, UA_NodeId* parentNodeId, const char* locale, int nodeIndex) +{ + OpcUaObject inputArgument; + inputArgument->description = UA_LOCALIZEDTEXT_ALLOC("en-US", "Input"); + inputArgument->name = UA_STRING_ALLOC("Input"); + inputArgument->dataType = UA_TYPES[UA_TYPES_STRING].typeId; + inputArgument->valueRank = -1; + + OpcUaObject outputArgument; + outputArgument->description = UA_LOCALIZEDTEXT_ALLOC("en-US", "Output"); + outputArgument->name = UA_STRING_ALLOC("Output"); + outputArgument->dataType = UA_TYPES[UA_TYPES_STRING].typeId; + outputArgument->valueRank = -1; + + OpcUaObject attr = UA_MethodAttributes_default; + attr->description = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->displayName = UA_LOCALIZEDTEXT_ALLOC(locale, identifier.c_str()); + attr->executable = true; + attr->userExecutable = true; + + OpcUaNodeId newNodeId(nodeIndex, identifier); + + UA_Server_addMethodNode(server, + *newNodeId, + *parentNodeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASORDEREDCOMPONENT), + UA_QUALIFIEDNAME(1, (char*) identifier.c_str()), + *attr, + helloMethodCallback, + 1, + inputArgument.get(), + 1, + outputArgument.get(), + NULL, + NULL); +} + +UA_StatusCode OpcUaServerTestHelper::helloMethodCallback(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionHandle, + const UA_NodeId* methodId, + void* methodContext, + const UA_NodeId* objectId, + void* objectContext, + size_t inputSize, + const UA_Variant* input, + size_t outputSize, + UA_Variant* output) +{ + UA_String* inputStr = (UA_String*) input->data; + std::string out = "Hello!"; + + if (inputStr->length > 0) + { + std::string in = utils::ToStdString(*inputStr); + std::stringstream ss; + ss << out << " (R:" << in << ")"; + out = ss.str(); + } + + UA_String tmp = UA_STRING_ALLOC(out.c_str()); + UA_Variant_setScalarCopy(output, &tmp, &UA_TYPES[UA_TYPES_STRING]); + UA_String_clear(&tmp); + return UA_STATUSCODE_GOOD; +} + +/*SampleServerTest*/ + +void BaseClientTest::SetUp() +{ + testing::Test::SetUp(); + testHelper.startServer(); +} +void BaseClientTest::TearDown() +{ + testHelper.stop(); + testing::Test::TearDown(); +} + +std::string BaseClientTest::getServerUrl() const +{ + return testHelper.getServerUrl(); +} + +OpcUaClientPtr BaseClientTest::prepareAndConnectClient(int timeout) +{ + std::shared_ptr client = std::make_shared(getServerUrl()); + + if (timeout >= 0) + client->setTimeout(timeout); + + client->connect(); + return client; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_attribute_reader.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_attribute_reader.cpp new file mode 100644 index 0000000..07c52b4 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_attribute_reader.cpp @@ -0,0 +1,232 @@ +#include +#include +#include "opcuaclient/opcuaclient.h" +#include "opcuaservertesthelper.h" +#include "opcuashared/opcuacommon.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using AttributeReaderTest = BaseClientTest; + +TEST_F(AttributeReaderTest, TwoAttributes) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto attr1 = OpcUaAttribute(OpcUaNodeId(1, ".i64"), UA_ATTRIBUTEID_VALUE); + const auto attr2 = OpcUaAttribute(OpcUaNodeId(1, ".i32"), UA_ATTRIBUTEID_VALUE); + + auto reader = AttributeReader(client); + reader.addAttribute(attr1); + reader.addAttribute(attr2); + reader.read(); + + auto i64 = reader.getValue(attr1).toInteger(); + ASSERT_EQ(i64, 64); + + auto i32 = reader.getValue(attr2).toInteger(); + ASSERT_EQ(i32, 41); +} + +TEST_F(AttributeReaderTest, NotRead) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto attr = OpcUaAttribute(OpcUaNodeId(1, ".i64"), UA_ATTRIBUTEID_VALUE); + + auto reader = AttributeReader(client); + reader.addAttribute(attr); + + ASSERT_THROW(reader.getValue(attr), OpcUaException); + + reader.read(); + ASSERT_NO_THROW(reader.getValue(attr)); +} + +TEST_F(AttributeReaderTest, Missing) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + const auto idI32 = OpcUaNodeId(1, ".i32"); + + auto reader = AttributeReader(client); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.read(); + + ASSERT_NO_THROW(reader.getValue(idI64, UA_ATTRIBUTEID_VALUE)); + ASSERT_THROW(reader.getValue(idI32, UA_ATTRIBUTEID_VALUE), OpcUaException); + ASSERT_THROW(reader.getValue(idI64, UA_ATTRIBUTEID_DISPLAYNAME), OpcUaException); +} + +TEST_F(AttributeReaderTest, NoAttributes) +{ + auto client = std::make_shared(getServerUrl()); + + auto reader = AttributeReader(client); + ASSERT_NO_THROW(reader.read()); +} + +TEST_F(AttributeReaderTest, Clear) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + + auto reader = AttributeReader(client); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.read(); + + auto i64 = reader.getValue(idI64, UA_ATTRIBUTEID_VALUE).toInteger(); + ASSERT_EQ(i64, 64); +} + +TEST_F(AttributeReaderTest, FailedRequest) +{ + auto client = std::make_shared(getServerUrl()); + ASSERT_FALSE(client->isConnected()); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + + auto reader = AttributeReader(client); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + ASSERT_THROW(reader.read(), OpcUaException); +} + +TEST_F(AttributeReaderTest, SameAttribute) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto attr = OpcUaAttribute(OpcUaNodeId(1, ".i64"), UA_ATTRIBUTEID_VALUE); + + auto reader = AttributeReader(client); + reader.addAttribute(attr); + reader.addAttribute(attr); + reader.read(); + + auto i64 = reader.getValue(attr).toInteger(); + ASSERT_EQ(i64, 64); +} + +TEST_F(AttributeReaderTest, MaxNodesPerRead) +{ + const size_t maxNodesPerRead = 3; + + testHelper.stop(); + testHelper.onConfigure([&](UA_ServerConfig* config) { config->maxNodesPerRead = maxNodesPerRead; }); + testHelper.startServer(); + + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const size_t maxBatchSize = + static_cast(client->readValue(OpcUaNodeId(UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD)).toInteger()); + ASSERT_EQ(maxBatchSize, maxNodesPerRead); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + const auto idI32 = OpcUaNodeId(1, ".i32"); + const auto idI16 = OpcUaNodeId(1, ".i16"); + const auto idProductUri = OpcUaNodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_PRODUCTURI); + + auto reader = AttributeReader(client, maxBatchSize); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({idI64, UA_ATTRIBUTEID_DISPLAYNAME}); + reader.addAttribute({idI32, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({idI32, UA_ATTRIBUTEID_DISPLAYNAME}); + reader.addAttribute({idProductUri, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({idProductUri, UA_ATTRIBUTEID_BROWSENAME}); + reader.addAttribute({idI16, UA_ATTRIBUTEID_VALUE}); + reader.read(); + + OpcUaVariant variant; + + variant = reader.getValue(idI64, UA_ATTRIBUTEID_VALUE); + ASSERT_EQ(64, variant.toInteger()); + + variant = reader.getValue(idI64, UA_ATTRIBUTEID_DISPLAYNAME); + ASSERT_EQ(".i64", variant.toString()); + + variant = reader.getValue(idI32, UA_ATTRIBUTEID_VALUE); + ASSERT_EQ(41, variant.toInteger()); + + variant = reader.getValue(idI32, UA_ATTRIBUTEID_DISPLAYNAME); + ASSERT_EQ(".i32", variant.toString()); + + variant = reader.getValue(idProductUri, UA_ATTRIBUTEID_VALUE); + ASSERT_EQ("http://open62541.org", variant.toString()); + + variant = reader.getValue(idProductUri, UA_ATTRIBUTEID_BROWSENAME); + ASSERT_EQ("ProductUri", variant.toString()); + + variant = reader.getValue(idI16, UA_ATTRIBUTEID_VALUE); + ASSERT_EQ(16, variant.toInteger()); +} + +TEST_F(AttributeReaderTest, MultipleReads) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + const auto idI32 = OpcUaNodeId(1, ".i32"); + + auto reader = AttributeReader(client); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({idI64, UA_ATTRIBUTEID_BROWSENAME}); + reader.read(); + + reader.addAttribute({idI32, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({idI32, UA_ATTRIBUTEID_BROWSENAME}); + reader.read(); + + ASSERT_NO_THROW(reader.getValue({idI64, UA_ATTRIBUTEID_VALUE})); + ASSERT_NO_THROW(reader.getValue({idI64, UA_ATTRIBUTEID_BROWSENAME})); + ASSERT_NO_THROW(reader.getValue({idI32, UA_ATTRIBUTEID_VALUE})); + ASSERT_NO_THROW(reader.getValue({idI32, UA_ATTRIBUTEID_BROWSENAME})); +} + +TEST_F(AttributeReaderTest, ClearAttributes) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + const auto idI32 = OpcUaNodeId(1, ".i32"); + + auto reader = AttributeReader(client); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.clearAttributes(); + reader.addAttribute({idI64, UA_ATTRIBUTEID_BROWSENAME}); + reader.read(); + + ASSERT_NO_THROW(reader.getValue({idI64, UA_ATTRIBUTEID_BROWSENAME})); + ASSERT_THROW(reader.getValue({idI64, UA_ATTRIBUTEID_VALUE}), OpcUaException); +} + +TEST_F(AttributeReaderTest, ClearResults) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto idI64 = OpcUaNodeId(1, ".i64"); + const auto idI32 = OpcUaNodeId(1, ".i32"); + + auto reader = AttributeReader(client); + reader.addAttribute({idI64, UA_ATTRIBUTEID_VALUE}); + reader.clearAttributes(); + reader.addAttribute({idI64, UA_ATTRIBUTEID_BROWSENAME}); + reader.read(); + + ASSERT_NO_THROW(reader.getValue({idI64, UA_ATTRIBUTEID_BROWSENAME})); + ASSERT_THROW(reader.getValue({idI64, UA_ATTRIBUTEID_VALUE}), OpcUaException); +} + + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_cached_reference_browser.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_cached_reference_browser.cpp new file mode 100644 index 0000000..985a331 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_cached_reference_browser.cpp @@ -0,0 +1,176 @@ +#include "testutils/testutils.h" +#include "opcuaclient/opcuaclient.h" +#include "opcuaservertesthelper.h" +#include "opcuashared/opcua.h" +#include "opcuashared/opcuacommon.h" +#include "opcuaclient/cached_reference_browser.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using CachedReferenceBrowserTest = BaseClientTest; + +TEST_F(CachedReferenceBrowserTest, ObjectsFolder) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + auto nodeId = OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER); + + auto browser = CachedReferenceBrowser(client); + ASSERT_NO_THROW(browser.browse(nodeId)); + + auto references = browser.browse(nodeId); + ASSERT_GT(references.byNodeId.size(), 0u); + + ASSERT_NO_THROW(browser.invalidateRecursive(nodeId)); +} + +TEST_F(CachedReferenceBrowserTest, ServerStatus) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + auto nodeId = OpcUaNodeId(UA_NS0ID_SERVER_SERVERSTATUS); + + auto browser = CachedReferenceBrowser(client); + auto serverStatusRefs = browser.browse(OpcUaNodeId(UA_NS0ID_SERVER_SERVERSTATUS)); + + ASSERT_EQ(serverStatusRefs.byBrowseName.size(), 7u); + ASSERT_EQ(serverStatusRefs.byNodeId.size(), serverStatusRefs.byBrowseName.size()); + + auto begin = serverStatusRefs.byBrowseName.begin(); + ASSERT_EQ("ServerStatusType", (begin + 0).key()); + ASSERT_EQ("ShutdownReason", (begin + 1).key()); + ASSERT_EQ("SecondsTillShutdown", (begin + 2).key()); + ASSERT_EQ("BuildInfo", (begin + 3).key()); + ASSERT_EQ("State", (begin + 4).key()); + ASSERT_EQ("CurrentTime", (begin + 5).key()); + ASSERT_EQ("StartTime", (begin + 6).key()); + + auto buildInfoRefs = browser.browse(OpcUaNodeId(UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO)); + ASSERT_EQ(buildInfoRefs.byBrowseName.size(), 7u); + + begin = buildInfoRefs.byBrowseName.begin(); + ASSERT_EQ("BuildInfoType", (begin + 0).key()); + ASSERT_EQ("BuildDate", (begin + 1).key()); + ASSERT_EQ("BuildNumber", (begin + 2).key()); + ASSERT_EQ("SoftwareVersion", (begin + 3).key()); + ASSERT_EQ("ManufacturerName", (begin + 4).key()); + ASSERT_EQ("ProductUri", (begin + 5).key()); + ASSERT_EQ("ProductName", (begin + 6).key()); + + auto buildDateRefs = browser.browse(OpcUaNodeId(UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_BUILDDATE)); + ASSERT_EQ(buildDateRefs.byBrowseName.size(), 1u); + + begin = buildDateRefs.byBrowseName.begin(); + ASSERT_EQ("BaseDataVariableType", (begin + 0).key()); +} + +TEST_F(CachedReferenceBrowserTest, Invalidate) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + auto nodeObjectsFolder = OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER); + auto browser = CachedReferenceBrowser(client); + auto references = browser.browse(nodeObjectsFolder); + + ASSERT_GT(references.byNodeId.size(), 0u); + + UA_Double doubleVal = 10.5; + testHelper.publishVariable(".anotherVar", &doubleVal, &UA_TYPES[UA_TYPES_DOUBLE], nodeObjectsFolder.getPtr()); + + auto referencesFromCache = browser.browse(nodeObjectsFolder); + ASSERT_EQ(references.byNodeId.size(), referencesFromCache.byNodeId.size()); + + browser.invalidate(nodeObjectsFolder); + + auto referencesAfterInvalidate = browser.browse(nodeObjectsFolder); + ASSERT_EQ(references.byNodeId.size() + 1, referencesAfterInvalidate.byNodeId.size()); +} + +TEST_F(CachedReferenceBrowserTest, IsSubtypeOf) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto shelvedStateMachineType = OpcUaNodeId(UA_NS0ID_SHELVEDSTATEMACHINETYPE); + const auto finiteStateMachineType = OpcUaNodeId(UA_NS0ID_FINITESTATEMACHINETYPE); + const auto stateMachineType = OpcUaNodeId(UA_NS0ID_STATEMACHINETYPE); + const auto serverType = OpcUaNodeId(UA_NS0ID_SERVERTYPE); + + auto browser = CachedReferenceBrowser(client); + + ASSERT_TRUE(browser.isSubtypeOf(shelvedStateMachineType, stateMachineType)); + ASSERT_TRUE(browser.isSubtypeOf(finiteStateMachineType, stateMachineType)); + ASSERT_TRUE(browser.isSubtypeOf(finiteStateMachineType, finiteStateMachineType)); + ASSERT_FALSE(browser.isSubtypeOf(shelvedStateMachineType, serverType)); +} + +TEST_F(CachedReferenceBrowserTest, HasReference) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto nodeObjectsFolder = OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER); + auto browser = CachedReferenceBrowser(client); + + ASSERT_TRUE(browser.hasReference(nodeObjectsFolder, ".b")); + ASSERT_TRUE(browser.hasReference(nodeObjectsFolder, ".ui16")); + ASSERT_FALSE(browser.hasReference(nodeObjectsFolder, "MissingNode")); +} + +TEST_F(CachedReferenceBrowserTest, GetChildNodeId) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto nodeObjectsFolder = OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER); + auto browser = CachedReferenceBrowser(client); + + ASSERT_EQ(browser.getChildNodeId(nodeObjectsFolder, ".b"), OpcUaNodeId(1, ".b")); + ASSERT_EQ(browser.getChildNodeId(nodeObjectsFolder, ".ui16"), OpcUaNodeId(1, ".ui16")); + ASSERT_EQ(browser.getChildNodeId(nodeObjectsFolder, "Server"), OpcUaNodeId(UA_NS0ID_SERVER)); +} + +TEST_F(CachedReferenceBrowserTest, BrowseFiltered) +{ + auto client = std::make_shared(getServerUrl()); + client->connect(); + + auto browser = CachedReferenceBrowser(client); + + const auto nodeId = OpcUaNodeId(UA_NS0ID_SERVER); + + BrowseFilter filter; + filter.referenceTypeId = OpcUaNodeId(UA_NS0ID_HASPROPERTY); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + const auto& children = browser.browseFiltered(nodeId, filter); + ASSERT_EQ(children.byNodeId.size(), 4u); +} + +TEST_F(CachedReferenceBrowserTest, MaxNodesPerBrowse) +{ + testHelper.stop(); + testHelper.onConfigure([&](UA_ServerConfig* config) { config->maxNodesPerBrowse = 5; }); + testHelper.startServer(); + + auto client = std::make_shared(getServerUrl()); + client->connect(); + + const auto nodeId = OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER); + const auto maxNodesPerBrowseId = OpcUaNodeId(UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERBROWSE); + const auto maxNodesPerBrowse = client->readValue(maxNodesPerBrowseId).toInteger(); + ASSERT_GT(maxNodesPerBrowse, 0); + + auto browser = CachedReferenceBrowser(client, static_cast(maxNodesPerBrowse)); + const auto& references = browser.browse(nodeId); + ASSERT_FALSE(references.byNodeId.empty()); + + auto missconfiguredBrowser = CachedReferenceBrowser(client); + ASSERT_THROW(missconfiguredBrowser.browse(nodeId), OpcUaException); +} + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_opcuaasyncexecthread.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuaasyncexecthread.cpp new file mode 100644 index 0000000..e947ddc --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuaasyncexecthread.cpp @@ -0,0 +1,64 @@ +#include "gtest/gtest.h" +#include +#include "opcuaclient/opcuaasyncexecthread.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaAsyncExecThreadTest = testing::Test; + +TEST_F(OpcUaAsyncExecThreadTest, Create) +{ + OpcUaAsyncExecThread asyncExecThread; +} + +TEST_F(OpcUaAsyncExecThreadTest, CreateAndRun) +{ + bool executed = false; + OpcUaAsyncExecThread asyncExecThread = std::thread([&executed]() { executed = true; }); +} + +TEST_F(OpcUaAsyncExecThreadTest, WaitOnReassign) +{ + bool executed = false; + OpcUaAsyncExecThread asyncExecThread = std::thread([&executed]() { + using namespace std::chrono_literals; + std::this_thread::sleep_for(50ms); + executed = true; + }); + asyncExecThread.reset(); + ASSERT_TRUE(executed); +} + +TEST_F(OpcUaAsyncExecThreadTest, WaitOnDestructor) +{ + bool executed = false; + { + OpcUaAsyncExecThread asyncExecThread = std::thread([&executed]() { + using namespace std::chrono_literals; + std::this_thread::sleep_for(50ms); + executed = true; + }); + } + ASSERT_TRUE(executed); +} + +TEST_F(OpcUaAsyncExecThreadTest, TwoThreads) +{ + bool executed1 = false; + bool executed2 = false; + + OpcUaAsyncExecThread asyncExecThread = std::thread([&executed1]() { + using namespace std::chrono_literals; + std::this_thread::sleep_for(50ms); + executed1 = true; + }); + + asyncExecThread = std::thread([&executed2]() { executed2 = true; }); + ASSERT_TRUE(executed1); + + asyncExecThread.reset(); + ASSERT_TRUE(executed2); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_opcuaclient.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuaclient.cpp new file mode 100644 index 0000000..d2d6fd7 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuaclient.cpp @@ -0,0 +1,236 @@ +#include +#include +#include "opcuaclient/opcuaclient.h" +#include "opcuaservertesthelper.h" +#include "opcuashared/opcua.h" +#include "opcuashared/opcuacommon.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +#define ASSERT_EQ_STATUS(status, expectedStatus) ASSERT_EQ(status, (UA_StatusCode) expectedStatus) + +using OpcUaClientTest = BaseClientTest; + +static void lockThread(OpcUaClient& client, bool& locked) +{ + locked = client.getLock().try_lock(); + if (locked) + client.getLock().unlock(); +} + +static void TestLock(OpcUaClient& client, bool expectedResult) +{ + bool locked; + std::thread t1(lockThread, std::ref(client), std::ref(locked)); + t1.join(); + ASSERT_EQ(locked, expectedResult); +} + +TEST_F(OpcUaClientTest, Lock) +{ + OpcUaClient client(getServerUrl()); + + TestLock(client, true); + { + auto lockedClient = client.getLockedUaClient(); + TestLock(client, false); + ASSERT_NE(lockedClient.operator UA_Client*(), nullptr); + { + auto lockedClient1 = std::move(lockedClient); + ASSERT_EQ(lockedClient.operator UA_Client*(), nullptr); + ASSERT_NE(lockedClient1.operator UA_Client*(), nullptr); + + TestLock(client, false); + } + TestLock(client, true); + } + TestLock(client, true); +} + +TEST_F(OpcUaClientTest, Connect) +{ + OpcUaClient client(getServerUrl()); + + client.connect(); + ASSERT_TRUE(client.isConnected()); + client.disconnect(); + ASSERT_FALSE(client.isConnected()); +} + +TEST_F_OPTIONAL(OpcUaClientTest, FirstConnectFails) +{ + testHelper.stop(); + + OpcUaClient client(getServerUrl()); + + client.setTimeout(500); + + ASSERT_THROW(client.connect(), OpcUaException); + ASSERT_FALSE(client.isConnected()); + + testHelper.startServer(); + + client.disconnect(); + ASSERT_THROW(client.connect(), OpcUaException); + ASSERT_TRUE(client.isConnected()); + + client.disconnect(); + ASSERT_FALSE(client.isConnected()); +} + +TEST_F(OpcUaClientTest, Timeout) +{ + OpcUaClient client(getServerUrl()); + + client.setTimeout(1234u); + ASSERT_EQ(client.getTimeout(), 1234u); + + client.connect(); + + client.setTimeout(1235u); + ASSERT_EQ(client.getTimeout(), 1235u); + + client.disconnect(); + + client.setTimeout(1236u); + ASSERT_EQ(client.getTimeout(), 1236u); +} + +TEST_F(OpcUaClientTest, CheckConnectedStatus) +{ + OpcUaClient client(getServerUrl()); + + client.connect(); + ASSERT_TRUE(client.isConnected()); + client.clear(); + ASSERT_FALSE(client.isConnected()); + + client.connect(); + client.disconnect(); + ASSERT_FALSE(client.isConnected()); +} + +TEST_F_OPTIONAL(OpcUaClientTest, ConnectTimeout) +{ + testHelper.stop(); + testHelper.setSessionTimeout(3000); + testHelper.startServer(); + + OpcUaClient client(getServerUrl()); + + ASSERT_THROW(client.connect(), OpcUaException); + ASSERT_TRUE(client.isConnected()); + + auto uaNode = OpcUaNodeId(1, ".d"); + ASSERT_TRUE(client.nodeExists(uaNode)); + + using namespace std::chrono_literals; + std::this_thread::sleep_for(70s); + + ASSERT_THROW(client.nodeExists(uaNode), OpcUaException); + + client.disconnect(); + ASSERT_THROW(client.connect(), OpcUaException); + ASSERT_TRUE(client.isConnected()); + + ASSERT_TRUE(client.nodeExists(uaNode)); + + client.disconnect(); + ASSERT_FALSE(client.isConnected()); +} + +TEST_F(OpcUaClientTest, ScheduleTimerTask) +{ + auto client = prepareAndConnectClient(); + + ASSERT_FALSE(client->timerTaskExists(1234)); + + std::promise promise; + bool executed = false; + auto ident1 = client->scheduleTimerTask(1, [&promise, &executed](const OpcUaClient& client, TimerTaskControl& control) { + if (!executed) + promise.set_value(); + executed = true; + }); + + ASSERT_TRUE(client->timerTaskExists(ident1)); + + IterateAndWaitForPromise(*client, promise.get_future()); // wait until task is executed at least once + + ASSERT_TRUE(client->timerTaskExists(ident1)); + + client->removeTimerTask(ident1); + + ASSERT_FALSE(client->timerTaskExists(ident1)); +} + +TEST_F(OpcUaClientTest, RemoveCurrentTimerTask) +{ + auto client = prepareAndConnectClient(); + + std::promise promise; + auto ident1 = client->scheduleTimerTask(1, [&promise](OpcUaClient& client, TimerTaskControl& control) { + control.terminateTimerTask(); + promise.set_value(); + }); + + IterateAndWaitForPromise(*client, promise.get_future()); // wait until task is executed at least once + + ASSERT_FALSE(client->timerTaskExists(ident1)); +} + +TEST_F(OpcUaClientTest, NodeExist) +{ + auto client = prepareAndConnectClient(); + + OpcUaNodeId uaNode(1, "hello.dewesoft"); + ASSERT_TRUE(client->nodeExists(uaNode)); + + uaNode = OpcUaNodeId(1, "f1"); + ASSERT_TRUE(client->nodeExists(uaNode)); + + uaNode = OpcUaNodeId(1, "unknown"); + ASSERT_FALSE(client->nodeExists(uaNode)); +} + +TEST_F(OpcUaClientTest, CallMethod) +{ + auto client = prepareAndConnectClient(); + + OpcUaVariant inputArg("Test"); + + auto callMethodResult = client->callMethod( + OpcUaCallMethodRequest(OpcUaNodeId(1, "hello.dewesoft"), OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), 1, inputArg.get())); + + ASSERT_EQ(callMethodResult->outputArgumentsSize, 1u); + ASSERT_EQ(OpcUaVariant(callMethodResult->outputArguments[0]).toString(), "Hello! (R:Test)"); +} + +TEST_F(OpcUaClientTest, CallMethodsWithCallback) +{ + auto client = prepareAndConnectClient(); + + std::vector callRequests; + std::string outputArgs; + + OpcUaVariant inputArg("Test"); + + callRequests.push_back(OpcUaCallMethodRequestWithCallback( + OpcUaNodeId(1, "hello.dewesoft"), + OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), + [&](const OpcUaCallMethodResult& callMethodResult) { + if (!callMethodResult.isStatusOK()) + throw std::runtime_error("call failed with status " + std::to_string(callMethodResult.getStatusCode())); + else if (!(callMethodResult.getOutputArgumentsSize() == 1 && callMethodResult.getOutputArgument(0).isString())) + throw std::runtime_error("First argument should be string"); + else + outputArgs = callMethodResult.getOutputArgument(0).toString(); + }, + 1, + inputArg.get())); + + ASSERT_NO_THROW(client->callMethods(callRequests)); + ASSERT_EQ(outputArgs, "Hello! (R:Test)"); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_opcuanodefactory.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuanodefactory.cpp new file mode 100644 index 0000000..928e4a4 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuanodefactory.cpp @@ -0,0 +1,108 @@ +#include +#include "opcuaservertesthelper.h" +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +#if IGNORE_FOR_DEVELOPMENT <= 0 + +using OpcUaNodeFactoryTest = BaseClientTest; + +static OpcUaObject PrepareReferenceDescription(OpcUaNodeClass nodeClass, const std::string& name, const OpcUaNodeId& nodeId, const OpcUaNodeId& typeId) +{ + OpcUaObject desc; + desc->nodeClass = static_cast(nodeClass); + desc->browseName = UA_QUALIFIEDNAME_ALLOC(1, name.c_str()); + desc->displayName = UA_LOCALIZEDTEXT_ALLOC("en_US", name.c_str()); + desc->nodeId = UA_EXPANDEDNODEID_NODEID(nodeId.copyAndGetDetachedValue()); + desc->typeDefinition = UA_EXPANDEDNODEID_NODEID(typeId.copyAndGetDetachedValue()); + return desc; +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryTest, Create) +{ + auto client = prepareAndConnectClient(); + ASSERT_NO_THROW(OpcUaNodeFactory factory(client)); +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryTest, InstatinateObject) +{ + auto client = prepareAndConnectClient(); + OpcUaNodeFactory factory(client); + + auto referenceDescription = PrepareReferenceDescription(OpcUaNodeClass::Object, "f1", OpcUaNodeId(1, "f1"), OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE))); + + bool traverse; + auto node = factory.instantiateNode(*referenceDescription, OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER)), traverse); + ASSERT_NE(node, nullptr); + ASSERT_EQ(node->getBrowseName(), "f1"); + ASSERT_EQ(node->getDisplayName(), "f1"); + ASSERT_EQ(node->getNodeId(), OpcUaNodeId(1, "f1")); + + auto nodeObject = std::dynamic_pointer_cast(node); + ASSERT_NE(nodeObject, nullptr); +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryTest, InstatinateVariable) +{ + auto client = prepareAndConnectClient(); + OpcUaNodeFactory factory(client); + + auto referenceDescription = PrepareReferenceDescription(OpcUaNodeClass::Variable, ".i32", OpcUaNodeId(1, ".i32"), OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_BASEVARIABLETYPE))); + + bool traverse; + auto node = factory.instantiateNode(*referenceDescription, OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER)), traverse); + ASSERT_NE(node, nullptr); + ASSERT_EQ(node->getBrowseName(), ".i32"); + ASSERT_EQ(node->getDisplayName(), ".i32"); + ASSERT_EQ(node->getNodeId(), OpcUaNodeId(1, ".i32")); + + auto nodeValue = std::dynamic_pointer_cast(node); + ASSERT_NE(nodeValue, nullptr); + ASSERT_EQ(nodeValue->getDataTypeNodeId(), UA_TYPES[UA_TYPES_INT32].typeId); +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryTest, InstatinateMethod) +{ + auto client = prepareAndConnectClient(); + OpcUaNodeFactory factory(client); + + auto referenceDescription = PrepareReferenceDescription(OpcUaNodeClass::Method, "hello.dewesoft", OpcUaNodeId(1, "hello.dewesoft"), OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_METHODATTRIBUTES))); // TODO TYPES + + bool traverse; + auto node = factory.instantiateNode(*referenceDescription, OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER)), traverse); + ASSERT_NE(node, nullptr); + ASSERT_EQ(node->getBrowseName(), "hello.dewesoft"); + ASSERT_EQ(node->getDisplayName(), "hello.dewesoft"); + ASSERT_EQ(node->getNodeId(), OpcUaNodeId(1, "hello.dewesoft")); + + auto nodeMethod = std::dynamic_pointer_cast(node); + ASSERT_NE(nodeMethod, nullptr); + ASSERT_EQ(nodeMethod->inputParameters.size(), 1u); + ASSERT_EQ(nodeMethod->outputParameters.size(), 1u); +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryTest, InstatinateDataType) +{ + auto client = prepareAndConnectClient(); + OpcUaNodeFactory factory(client); + + auto referenceDescription = PrepareReferenceDescription(OpcUaNodeClass::DataType, "Boolean", OpcUaNodeId(0, 1), OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATATYPE))); + + bool traverse; + auto node = factory.instantiateNode(*referenceDescription, OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER)), traverse); + ASSERT_NE(node, nullptr); + ASSERT_EQ(node->getBrowseName(), "Boolean"); + ASSERT_EQ(node->getDisplayName(), "Boolean"); + ASSERT_EQ(node->getNodeId(), OpcUaNodeId(0, 1)); + + auto dataType = std::dynamic_pointer_cast(node); + ASSERT_NE(dataType, nullptr); +} + +#endif + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_opcuanodefactorybrowser.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuanodefactorybrowser.cpp new file mode 100644 index 0000000..21497d6 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuanodefactorybrowser.cpp @@ -0,0 +1,60 @@ +#include + +#include "opcuashared/opcua.h" +#include "opcuashared/opcuacommon.h" +#include "opcuaservertesthelper.h" +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaNodeFactoryBrowserTest = BaseClientTest; + +TEST_F_OPTIONAL(OpcUaNodeFactoryBrowserTest, Create) +{ + auto client = prepareAndConnectClient(); + auto nodeFactory = std::make_shared(client); + + ASSERT_NO_THROW(OpcUaNodeFactoryBrowser(nodeFactory, client)); +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryBrowserTest, Browse) +{ + auto client = prepareAndConnectClient(); + auto nodeFactory = std::make_shared(client); + OpcUaNodeFactoryBrowser browser(nodeFactory, client); + browser.browseTree(OpcUaNodeId(1, "f1")); + + ASSERT_EQ(browser.getNodes().size(), 1u); + + auto node = browser.getNodes()[0]; + ASSERT_EQ(node->getBrowseName(), "f1.i"); + ASSERT_EQ(node->getNodeClass(), OpcUaNodeClass::Variable); + + auto nodeVariable = std::dynamic_pointer_cast(node); + ASSERT_NE(nodeVariable, nullptr); + ASSERT_EQ(nodeVariable->getDataTypeNodeId(), UA_TYPES[UA_TYPES_INT32].typeId); +} + +TEST_F_OPTIONAL(OpcUaNodeFactoryBrowserTest, BrowseDataTypes) +{ + auto client = prepareAndConnectClient(); + auto nodeFactory = std::make_shared(client); + OpcUaNodeFactoryBrowser browser(nodeFactory, client); + browser.browseTree(OpcUaNodeId(UA_NS0ID_BASEDATATYPE)); + + ASSERT_GE(browser.getNodes().size(), 16u); + + auto nodes = browser.getNodes(); + auto it = std::find_if(nodes.cbegin(), nodes.cend(), [](const OpcUaNodePtr& node) {return node->getBrowseName() == "Boolean"; }); + ASSERT_NE(it, nodes.cend()); + + const auto& booleanNode = *it; + + ASSERT_EQ(booleanNode->getBrowseName(), "Boolean"); + ASSERT_EQ(booleanNode->getDisplayName(), "Boolean"); + ASSERT_EQ(booleanNode->getNodeId(), OpcUaNodeId(0, 1)); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_opcuataskprocessor.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuataskprocessor.cpp new file mode 100644 index 0000000..4e06527 --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuataskprocessor.cpp @@ -0,0 +1,195 @@ +#include + +#include +#include "opcuaclient/taskprocessor/opcuataskprocessor.h" +#include "opcuaservertesthelper.h" + +using namespace std::chrono_literals; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaTaskProcessorTest = BaseClientTest; + +static void TestRequest(OpcUaTaskProcessor& taskProcessor) +{ + std::exception_ptr exception; + taskProcessor.executeTask( + [&exception](OpcUaClient& client) + { + OpcUaNodeId uaNode(1, "hello.dewesoft"); + try + { + client.nodeExists(uaNode); + } + catch (const OpcUaException&) + { + exception = std::current_exception(); + } + }); + + if (exception) + std::rethrow_exception(exception); +} + +TEST_F(OpcUaTaskProcessorTest, Create) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + ASSERT_EQ(taskProcessor.getClient(), client); + + taskProcessor.start(); + + ASSERT_TRUE(taskProcessor.isConnected()); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, CreateAndStopInDestructor) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); +} + +TEST_F(OpcUaTaskProcessorTest, ExecuteTaskAwait) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + bool executed = false; + std::future future = taskProcessor.executeTaskAwait([&executed](const OpcUaClient& client) { executed = true; }); + + future.get(); + + ASSERT_TRUE(executed); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, ExecuteTask) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + bool executed = false; + taskProcessor.executeTask([&executed](const OpcUaClient& client) { executed = true; }); + + ASSERT_TRUE(executed); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, ExceptionInTask) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + ASSERT_THROW(taskProcessor.executeTask([](const OpcUaClient& client) { throw std::runtime_error("test exception"); }), + std::runtime_error); + + std::future future = + taskProcessor.executeTaskAwait([](const OpcUaClient& client) { throw std::runtime_error("test exception"); }); + + ASSERT_THROW(future.get(), std::runtime_error); + + bool executed = false; + taskProcessor.executeTask([&executed](const OpcUaClient& client) { executed = true; }); + + ASSERT_TRUE(executed); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, AddTaskInsideOfTask) // test for possible deadlock +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + int number = 0; + taskProcessor.executeTask( + [&taskProcessor, &number](const OpcUaClient& client) + { + taskProcessor.executeTask([&number](const OpcUaClient& client) { number++; }); + number++; + }); + + ASSERT_EQ(number, 2); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, Disconnect) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + ASSERT_TRUE(taskProcessor.isConnected()); + + testHelper.stop(); + + ASSERT_THROW(TestRequest(taskProcessor), OpcUaException); + + ASSERT_FALSE(taskProcessor.isConnected()); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, SetTimeout) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + taskProcessor.setConnectionTimeout(1234u); + + uint32_t timeout{}; + taskProcessor.executeTask([&timeout](OpcUaClient& client) { timeout = client.getTimeout(); }); + + ASSERT_EQ(timeout, 1234u); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTaskProcessorTest, AddTaskInsideOfTimerTask) +{ + auto client = prepareAndConnectClient(); + OpcUaTaskProcessor taskProcessor(client); + + taskProcessor.start(); + + std::promise promise; + int number = 0; + auto ident1 = client->scheduleTimerTask(1, + [&taskProcessor, &promise, &number](OpcUaClient& client, TimerTaskControl& control) + { + control.terminateTimerTask(); + + taskProcessor.executeTask([&number](const OpcUaClient& client) { number++; }); + number++; + + promise.set_value(); + }); + + ASSERT_NE(promise.get_future().wait_for(2s), std::future_status::timeout); // wait until task is executed + + ASSERT_EQ(number, 2); + + ASSERT_FALSE(client->timerTaskExists(ident1)); + + taskProcessor.stop(); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaclient/tests/src/test_opcuatimertaskhelper.cpp b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuatimertaskhelper.cpp new file mode 100644 index 0000000..6bb311e --- /dev/null +++ b/shared/libraries/opcua/opcuaclient/tests/src/test_opcuatimertaskhelper.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include "opcuaclient/opcuatimertaskhelper.h" + +#include "opcuaclient/taskprocessor/opcuataskprocessor.h" +#include "opcuaservertesthelper.h" + +using namespace daq::utils; +using namespace std::chrono_literals; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaTimerTaskHelperTest = BaseClientTest; + +TEST_F(OpcUaTimerTaskHelperTest, Create) +{ + OpcUaClientPtr client = std::make_unique(getServerUrl()); + client->connect(); + + OpcUaTaskProcessor taskProcessor(client); + taskProcessor.start(); + + std::promise promise; + std::atomic promiseIsSet = false; + OpcUaTimerTaskHelper timerTaskHelper(*client, 1, [&promise, &promiseIsSet](OpcUaClient& client) { + if (!promiseIsSet) + promise.set_value(); + promiseIsSet = true; + }); + + ASSERT_EQ(timerTaskHelper.getIntervalMs(), 1); + ASSERT_FALSE(timerTaskHelper.getTerminated()); + ASSERT_FALSE(timerTaskHelper.getStarted()); + + timerTaskHelper.start(); + + ASSERT_FALSE(timerTaskHelper.getTerminated()); + ASSERT_TRUE(timerTaskHelper.getStarted()); + + ASSERT_NE(promise.get_future().wait_for(2s), std::future_status::timeout); // wait until task is executed + + timerTaskHelper.terminate(); + + ASSERT_TRUE(timerTaskHelper.getTerminated()); + ASSERT_TRUE(timerTaskHelper.getStarted()); + + timerTaskHelper.stop(); + + ASSERT_FALSE(timerTaskHelper.getStarted()); + + taskProcessor.stop(); +} + +TEST_F(OpcUaTimerTaskHelperTest, ThrowExceptionInTask) +{ + OpcUaClientPtr client = std::make_unique(getServerUrl()); + client->connect(); + + OpcUaTaskProcessor taskProcessor(client); + taskProcessor.start(); + + std::promise promise; + std::atomic promiseIsSet = false; + OpcUaTimerTaskHelper timerTaskHelper(*client, 1, [&promise, &promiseIsSet](OpcUaClient& client) { + Finally finnaly([&promise, &promiseIsSet]() { + if (!promiseIsSet) + promise.set_value(); + promiseIsSet = true; + }); + + throw std::runtime_error("test error"); + }); + + ASSERT_EQ(timerTaskHelper.getIntervalMs(), 1); + ASSERT_FALSE(timerTaskHelper.getTerminated()); + ASSERT_FALSE(timerTaskHelper.getStarted()); + + timerTaskHelper.start(); + + ASSERT_FALSE(timerTaskHelper.getTerminated()); + ASSERT_TRUE(timerTaskHelper.getStarted()); + + ASSERT_NE(promise.get_future().wait_for(2s), std::future_status::timeout); // wait until task is executed + + timerTaskHelper.terminate(); + + ASSERT_TRUE(timerTaskHelper.getTerminated()); + ASSERT_TRUE(timerTaskHelper.getStarted()); + + timerTaskHelper.stop(); + + ASSERT_FALSE(timerTaskHelper.getStarted()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/CMakeLists.txt b/shared/libraries/opcua/opcuaserver/CMakeLists.txt new file mode 100644 index 0000000..3a87fe1 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME server) +project(OpcUaServer CXX) + +if (POLICY CMP0076) + cmake_policy(SET CMP0076 NEW) +endif() + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/common.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/common.h new file mode 100644 index 0000000..3cea232 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/common.h @@ -0,0 +1,26 @@ +/* + * 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 "opcuashared/opcua.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaServer; +using OpcUaServerPtr = std::shared_ptr; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/event_attributes.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/event_attributes.h new file mode 100644 index 0000000..fb6280a --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/event_attributes.h @@ -0,0 +1,63 @@ +/* + * 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 + +template <> +struct std::hash> +{ + std::size_t operator()(daq::opcua::OpcUaObject const& s) const noexcept + { + return UA_QualifiedName_hash(s.get()); + } +}; + +inline bool operator==(const daq::opcua::OpcUaObject& lhs, + const daq::opcua::OpcUaObject& rhs) +{ + return UA_QualifiedName_equal(lhs.get(), rhs.get()); +} + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class EventAttributes +{ +public: + EventAttributes(); + + using AttributesType = std::unordered_map, OpcUaObject>; + + const AttributesType& getAttributes() const; + + void setTime(UA_UtcTime time); + void setSeverity(UA_UInt16 eventSeverity); + + void setMessage(const std::string& message); + void setMessage(const char *locale, const char *text); + void setMessage(const OpcUaObject& message); + void setSourceName(const std::string& eventSource); + + void setAttribute(const OpcUaObject& attribute, const OpcUaObject& value); + void setAttribute(const std::string& attribute, const OpcUaObject& value); + +private: + AttributesType attributes; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/node_event_manager.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/node_event_manager.h new file mode 100644 index 0000000..fb2c395 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/node_event_manager.h @@ -0,0 +1,174 @@ +/* + * 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 "opcuaserver/opcuaserver.h" +#include "opcuaserver/server_event_manager.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class NodeEventManager; +using NodeEventManagerPtr = std::shared_ptr; + +struct NodeValueCallbackArgs; +using NodeValueCallback = std::function; + +class NodeEventManager +{ +public: + struct ReadArgs; + struct WriteArgs; + struct DataSourceReadArgs; + struct DataSourceWriteArgs; + struct MethodArgs; + + using ReadCallback = std::function; + using WriteCallback = std::function; + using DataSourceReadCallback = std::function; + using DataSourceWriteCallback = std::function; + using MethodCallback = std::function; + + NodeEventManager(const OpcUaNodeId& nodeId, OpcUaServerPtr& server); + + void onRead(ReadCallback callback); + void onWrite(WriteCallback callback); + void onDataSourceRead(DataSourceReadCallback callback); + void onDataSourceWrite(DataSourceWriteCallback callback); + void onMethodCall(MethodCallback callback); + void onDisplayNameChanged(DisplayNameChangedCallback callback); + void onDescriptionChanged(DescriptionChangedCallback callback); + +protected: + OpcUaNodeId nodeId; + OpcUaServerPtr server; + + WriteCallback writeCallback; + ReadCallback readCallback; + DataSourceWriteCallback dataSourceWriteCallback; + DataSourceReadCallback dataSourceReadCallback; + MethodCallback methodCallback; + +private: + static void OnWrite(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + const UA_NumericRange* range, + const UA_DataValue* value); + + static void OnRead(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeid, + void* nodeContext, + const UA_NumericRange* range, + const UA_DataValue* value); + + static UA_StatusCode OnDataSourceRead(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + UA_Boolean includeSourceTimeStamp, + const UA_NumericRange* range, + UA_DataValue* value); + + static UA_StatusCode OnDataSourceWrite(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + const UA_NumericRange* range, + const UA_DataValue* value); + + static UA_StatusCode OnMethod(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* methodId, + void* methodContext, + const UA_NodeId* objectId, + void* objectContext, + size_t inputSize, + const UA_Variant* input, + size_t outputSize, + UA_Variant* output); +}; + +struct NodeEventManager::ReadArgs +{ + UA_Server* server; + const UA_NodeId* sessionId; + void* sessionContext; + const UA_NodeId* nodeId; + void* nodeContext; + const UA_NumericRange* range; + const UA_DataValue* value; +}; + +struct NodeEventManager::WriteArgs +{ + UA_Server* server; + const UA_NodeId* sessionId; + void* sessionContext; + const UA_NodeId* nodeId; + void* nodeContext; + const UA_NumericRange* range; + const UA_DataValue* value; +}; + +struct NodeEventManager::DataSourceReadArgs +{ + UA_Server* server; + const UA_NodeId* sessionId; + void* sessionContext; + const UA_NodeId* nodeId; + void* nodeContext; + UA_Boolean includeSourceTimeStamp; + const UA_NumericRange* range; + UA_DataValue* value; +}; + +struct NodeEventManager::DataSourceWriteArgs +{ + UA_Server* server; + const UA_NodeId* sessionId; + void* sessionContext; + const UA_NodeId* nodeId; + void* nodeContext; + const UA_NumericRange* range; + const UA_DataValue* value; +}; + +struct NodeEventManager::MethodArgs +{ + UA_Server* server; + const UA_NodeId* sessionId; + void* sessionContext; + const UA_NodeId* methodId; + void* methodContext; + const UA_NodeId* objectId; + void* objectContext; + size_t inputSize; + const UA_Variant* input; + size_t outputSize; + UA_Variant* output; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaaddnodeparams.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaaddnodeparams.h new file mode 100644 index 0000000..27b8236 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaaddnodeparams.h @@ -0,0 +1,104 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using CreateOptionalNodeCallback = std::function; + +class AddNodeParams +{ +public: + AddNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId, const OpcUaNodeId& referenceTypeId); + void setBrowseName(const std::string& browseName); + + OpcUaNodeId requestedNewNodeId; + OpcUaNodeId parentNodeId; + OpcUaNodeId referenceTypeId; + OpcUaObject browseName{}; + void* nodeContext{}; +}; + +template +class GenericAddNodeParams : public AddNodeParams +{ +public: + GenericAddNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId, const OpcUaNodeId& referenceTypeId, const T& defaultAttributes); + + OpcUaObject attr; + CreateOptionalNodeCallback addOptionalNodeCallback; +}; + +class AddObjectNodeParams : public GenericAddNodeParams +{ +public: + AddObjectNodeParams(const OpcUaNodeId& requestedNewNodeId); + AddObjectNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId); + AddObjectNodeParams(const std::string& name, const OpcUaNodeId& parentNodeId); + + OpcUaNodeId typeDefinition = OpcUaNodeId(UA_NS0ID_BASEOBJECTTYPE); +}; + +class AddVariableNodeParams : public GenericAddNodeParams +{ +public: + AddVariableNodeParams(const OpcUaNodeId& requestedNewNodeId); + AddVariableNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId); + AddVariableNodeParams(const std::string& name, const OpcUaNodeId& parentNodeId); + + void setDataType(const OpcUaNodeId& dataTypeId); + + OpcUaNodeId typeDefinition = OpcUaNodeId(UA_NS0ID_BASEDATAVARIABLETYPE); +}; + +class AddMethodNodeParams : public GenericAddNodeParams +{ +public: + AddMethodNodeParams(const OpcUaNodeId& requestedNewNodeId); + AddMethodNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId); + AddMethodNodeParams(const std::string& name, const OpcUaNodeId& parentNodeId); + + ~AddMethodNodeParams(); + + UA_MethodCallback method{}; + size_t inputArgumentsSize{}; + UA_Argument* inputArguments{}; + size_t outputArgumentsSize{}; + UA_Argument* outputArguments{}; +}; + +class AddVariableTypeNodeParams : public GenericAddNodeParams +{ +public: + AddVariableTypeNodeParams(const OpcUaNodeId& requestedNewNodeId); + AddVariableTypeNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId); + + OpcUaNodeId typeDefinition = OpcUaNodeId(UA_NS0ID_BASEDATAVARIABLETYPE); +}; + +struct AddObjectTypeNodeParams : public GenericAddNodeParams +{ + AddObjectTypeNodeParams(const OpcUaNodeId& requestedNewNodeId); + AddObjectTypeNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h new file mode 100644 index 0000000..589e9c0 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserver.h @@ -0,0 +1,172 @@ +/* + * 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 +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaServer final : public daq::utils::ThreadEx +{ +public: + using OnClientConnectedCallback = std::function; + using OnClientDisconnectedCallback = std::function; + + OpcUaServer(); + ~OpcUaServer(); + + static constexpr uint16_t OPCUA_DEFAULT_PORT = 4840; + + uint16_t& getPort(); + void setPort(uint16_t port); + void setAuthenticationProvider(const AuthenticationProviderPtr& authenticationProvider); + void setClientConnectedHandler(const OnClientConnectedCallback& callback); + void setClientDisconnectedHandler(const OnClientDisconnectedCallback& callback); + + void setSecurityConfig(OpcUaServerSecurityConfig* config); + const OpcUaServerSecurityConfig* getSecurityConfig() const; + void prepare(); + bool isPrepared(); + + void start() override; + void stop() override; + + OpcUaServerNode getNode(const OpcUaNodeId& nodeId); + OpcUaServerObjectNode getRootNode(); + OpcUaServerObjectNode getObjectsNode(); + OpcUaServerObjectNode getTypesNode(); + OpcUaServerObjectNode getViewsNode(); + OpcUaServerObjectNode getObjectTypesNode(); + OpcUaServerObjectNode getVariableTypesNode(); + OpcUaServerObjectNode getDataTypesNode(); + OpcUaServerObjectNode getReferenceTypesNode(); + ServerEventManagerPtr getEventManager(); + + bool nodeExists(const OpcUaNodeId& nodeId); + bool nodeExists(const UA_NodeId& nodeId); + + OpcUaObject browse(const OpcUaObject& browseDescription); + + OpcUaNodeId addObjectNode(const AddObjectNodeParams& params); + OpcUaNodeId addVariableNode(const AddVariableNodeParams& params); + OpcUaNodeId addMethodNode(const AddMethodNodeParams& params); + OpcUaNodeId addObjectTypeNode(const AddObjectTypeNodeParams& params); + OpcUaNodeId addVariableTypeNode(const AddVariableTypeNodeParams& params); + + void triggerEvent(const OpcUaNodeId& eventType, const OpcUaNodeId& originNodeId, const EventAttributes& eventAttributes); + + void deleteNode(const OpcUaNode& node); + void deleteNode(const OpcUaNodeId& nodeId); + + OpcUaNodeClass readNodeClass(const OpcUaNodeId& nodeId) const; + OpcUaObject readBrowseName(const OpcUaNodeId& nodeId) const; + std::string readBrowseNameString(const OpcUaNodeId& nodeId) const; + + void setDisplayName(const OpcUaNodeId& nodeId, const OpcUaObject& localizedText); + void setDisplayName(const OpcUaNodeId& nodeId, const std::string& text); + OpcUaObject readDisplayName(const OpcUaNodeId& nodeId) const; + + void setDescription(const OpcUaNodeId& nodeId, const OpcUaObject& localizedText); + void setDescription(const OpcUaNodeId& nodeId, const std::string& text); + + void setAccessLevel(const OpcUaNodeId& nodeId, UA_Byte accessLevel); + + void writeValue(const OpcUaNodeId& nodeId, const OpcUaVariant& var); + OpcUaVariant readValue(const OpcUaNodeId& nodeId); + OpcUaNodeId readDataType(const OpcUaNodeId& typeNodeId); + + void addReference(const OpcUaNodeId& sourceId, const OpcUaNodeId& refTypeId, const OpcUaNodeId& targetId, bool isForward = true); + void deleteReference(const OpcUaNodeId& sourceId, const OpcUaNodeId& refTypeId, const OpcUaNodeId& targetId, bool isForward = true); + bool referenceExists(const OpcUaNodeId& sourceId, const OpcUaNodeId& refTypeId, const OpcUaNodeId& targetId, bool isForward = true); + + // session context + typedef std::function CreateSessionContextCallbackType; + CreateSessionContextCallbackType createSessionContextCallback; + typedef std::function DeleteSessionContextCallbackType; + DeleteSessionContextCallbackType deleteSessionContextCallback; + + std::unordered_set& getSessions(); // use only in server thread + + void* createSessionContextCallbackImp(const OpcUaNodeId& sessionId); + void deleteSessionContextCallbackImp(void* context); + + // TODO move locking to model + bool passwordLock(const std::string& password); + bool passwordUnlock(const std::string& password); + bool isPasswordLocked(); + + UA_Server* getUaServer() const noexcept; + +protected: + void execute() override; + +private: + UA_Server* createServer(); + void prepareServer(); + void prepareServerMinimal(UA_ServerConfig* config); + void prepareAccessControl(UA_ServerConfig* config); + void shutdownServer(); + UA_StatusCode validateIdentityToken(const UA_ExtensionObject* token); + bool isUsernameIdentityTokenValid(const UA_UserNameIdentityToken* token); + bool isAnonymousIdentityTokenValid(const UA_AnonymousIdentityToken* token); + void createSession(const OpcUaNodeId& sessionId, void** sessionContext); + + static UA_StatusCode activateSession(UA_Server* server, + UA_AccessControl* ac, + const UA_EndpointDescription* endpointDescription, + const UA_ByteString* secureChannelRemoteCertificate, + const UA_NodeId* sessionId, + const UA_ExtensionObject* userIdentityToken, + void** sessionContext); + static void closeSession(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext); + static UA_StatusCode generateChildId(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *sourceNodeId, const UA_NodeId *targetParentNodeId, const UA_NodeId *referenceTypeId, UA_NodeId *targetNodeId); + + // missing UA_Server void* member workaround... + static OpcUaServer* getServer(UA_Server* server); + + UA_StatusCode (*activateSession_default)(UA_Server* server, + UA_AccessControl* ac, + const UA_EndpointDescription* endpointDescription, + const UA_ByteString* secureChannelRemoteCertificate, + const UA_NodeId* sessionId, + const UA_ExtensionObject* userIdentityToken, + void** sessionContext){}; + + OpcUaServerLock serverLock; + uint16_t port{OPCUA_DEFAULT_PORT}; + UA_Server* server{}; + std::unordered_set sessionContext; + ServerEventManagerPtr eventManager; + AuthenticationProviderPtr authenticationProvider; + OnClientConnectedCallback clientConnectedHandler; + OnClientDisconnectedCallback clientDisconnectedHandler; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserverlock.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserverlock.h new file mode 100644 index 0000000..ff5f1ab --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaserverlock.h @@ -0,0 +1,52 @@ +/* + * 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 "opcuashared/opcua.h" +#include "opcuashared/opcuanodeid.h" +#include "opcuashared/opcuacommon.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaServerLock +{ +public: + OpcUaServerLock(); + ~OpcUaServerLock(); + + bool canControlAcq(const OpcUaNodeId& sessionId); + + bool passwordLock(const std::string& password, const OpcUaNodeId& sessionId = OpcUaNodeId()); + bool passwordUnlock(const std::string& password, const OpcUaNodeId& sessionId = OpcUaNodeId()); + bool isPasswordLocked(); + + bool hasConfigurationControlLock(const OpcUaNodeId& sessionId) const; + void refuseConfigurationControlLock(const OpcUaNodeId& sessionId); + bool lockConfigurationControl(const OpcUaNodeId& sessionId, const std::chrono::seconds timeout); + + bool hasActiveConfigurationControlLock() const; + +private: + bool hasConfigurationControlAccess(const OpcUaNodeId& sessionId) const; + bool canEditPasswordLock(const OpcUaNodeId& sessionId); + + std::string password; + + OpcUaNodeId configurationControlLockSessionId; + utils::DurationTimeStamp configurationControlLockValidTo; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaservernode.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaservernode.h new file mode 100644 index 0000000..55e1ca6 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaservernode.h @@ -0,0 +1,143 @@ +/* + * 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 + +// forward declaration +class OpcUaServer; + +class OpcUaServerObjectNode; +class OpcUaServerVariableNode; +class OpcUaServerMethodNode; +class OpcUaServerViewNode; + +class OpcUaServerDataTypeNode; +class OpcUaServerObjectTypeNode; +class OpcUaServerVariableTypeNode; +class OpcUaServerReferenceTypeNode; +class OpcUaServerEventTypeNode; + +class OpcUaServerNode +{ +public: + OpcUaServerNode(OpcUaServer& server, const OpcUaNodeId& nodeId); + virtual ~OpcUaServerNode() = default; + + OpcUaNodeId getNodeId() const; + + OpcUaNodeClass getNodeClass(); + OpcUaObject getBrowseName(); + + void setDisplayName(const std::string& displayName); + OpcUaObject getDisplayName(); + + std::vector> browse( + const OpcUaNodeId& referenceTypeId, + bool includeSubtypes = true, + OpcUaNodeClass nodeClassMask = OpcUaNodeClass::All, + UA_BrowseDirection browseDirection = UA_BrowseDirection::UA_BROWSEDIRECTION_FORWARD); + std::vector> browseChildNodes(); + std::unique_ptr getChildNode(const OpcUaObject& qualifiedName); + + OpcUaServerObjectNode addObject(AddObjectNodeParams& params); + OpcUaServerObjectNode addObject(const OpcUaNodeId& id, const std::string& browseName); + OpcUaServerVariableNode addVariable(AddVariableNodeParams& params); + OpcUaServerVariableNode addVariable(const OpcUaNodeId& id, const std::string& browseName); + + void remove(); + +protected: + OpcUaServer& server; + OpcUaNodeId nodeId; +}; + +class OpcUaServerObjectNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +class OpcUaServerVariableNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors + + template + void write(Arg&& arg) + { + OpcUaVariant var; + var.setScalar(std::forward(arg)); + writeVariantToServer(var); + } + + template + T read() + { + OpcUaVariant var = readVariantFromServer(); + return var.readScalar(); + } + +private: + void writeVariantToServer(const OpcUaVariant& var); + OpcUaVariant readVariantFromServer(); +}; + +class OpcUaServerMethodNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +class OpcUaServerViewNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +class OpcUaServerDataTypeNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +class OpcUaServerObjectTypeNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +class OpcUaServerVariableTypeNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +class OpcUaServerReferenceTypeNode : public OpcUaServerNode +{ +public: + using OpcUaServerNode::OpcUaServerNode; // inherit constructors +}; + +} // namespace opcua diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaservernodefactory.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaservernodefactory.h new file mode 100644 index 0000000..319063e --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuaservernodefactory.h @@ -0,0 +1,37 @@ +/* + * 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 + +class OpcUaServerNodeFactory +{ +public: + OpcUaServerNodeFactory(OpcUaServer& server); + + std::unique_ptr createServerNode(const OpcUaNodeId& nodeId); + std::unique_ptr createServerNode(const OpcUaNodeId& nodeId, OpcUaNodeClass nodeClass); + std::unique_ptr createServerNode(const OpcUaNodeId& nodeId, UA_NodeClass nodeClass); + +protected: + OpcUaServer& server; +}; + +} // namespace opcua diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuasession.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuasession.h new file mode 100644 index 0000000..e5b066b --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuasession.h @@ -0,0 +1,52 @@ +/* + * 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 "opcuashared/opcuanodeid.h" +#include "opcuaserver/opcuaserverlock.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaSession; +using OpcUaSessionPtr = std::shared_ptr; + +class OpcUaSession +{ +public: + explicit OpcUaSession(const OpcUaNodeId& sessionId, OpcUaServerLock* serverLock); + ~OpcUaSession(); + + bool canControlAcq(); + + bool hasConfigurationControlLock() const; + bool lockConfigurationControl(std::chrono::seconds timeout); + void refuseConfigurationControlLock(); + + bool passwordLock(const std::string& password); + bool passwordUnlock(const std::string& password); + + void setConfigurationLockTokenId(const OpcUaNodeId& configurationLockTokenId); + const OpcUaNodeId& getConfigurationLockTokenId() const; + +private: + OpcUaNodeId sessionId; + OpcUaServerLock* serverLock; + OpcUaNodeId configurationLockTokenId; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuataskqueue.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuataskqueue.h new file mode 100644 index 0000000..36dea76 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuataskqueue.h @@ -0,0 +1,44 @@ +/* + * 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 OpcUaTaskQueue +{ +public: + static constexpr unsigned int infinity = 0; + using Function = std::function; + + void push(Function&& func); + bool pop(Function& func); + bool pop(Function& func, unsigned int timeout_ms); + + void processTaskQueue(); + +private: + std::mutex mutex_; + std::condition_variable cv_; + std::deque q_; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuatmstypes.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuatmstypes.h new file mode 100644 index 0000000..57601d5 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/opcuatmstypes.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 +#include +#include + +#include + +#include "opcuashared/opcua.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +void addTmsTypes(UA_Server *server); + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/include/opcuaserver/server_event_manager.h b/shared/libraries/opcua/opcuaserver/include/opcuaserver/server_event_manager.h new file mode 100644 index 0000000..adcff98 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/include/opcuaserver/server_event_manager.h @@ -0,0 +1,67 @@ +/* + * 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 "opcuaserver/common.h" +#include "opcuaserver/opcuaservernode.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class ServerEventManager; +using ServerEventManagerPtr = std::shared_ptr; + +using CreatOptionalNodeCallback = std::function; +using DisplayNameChangedCallback = std::function& name, void* context)>; +using DescriptionChangedCallback = std::function& description, void* context)>; + +class ServerEventManager +{ +public: + ServerEventManager(const OpcUaServerPtr& server); + ServerEventManager(OpcUaServer* server); + + void registerEvents(); + + void onCreateOptionalNode(const CreatOptionalNodeCallback& callback); + + void onDisplayNameChanged(const OpcUaNodeId& nodeId, const DisplayNameChangedCallback& callback); + void removeOnDisplayNameChanged(const OpcUaNodeId& nodeId); + void onDescriptionChanged(const OpcUaNodeId& nodeId, const DescriptionChangedCallback& callback); + void removeOnDescriptionChanged(const OpcUaNodeId& nodeId); + +private: + OpcUaServer* server; + CreatOptionalNodeCallback createOptionalNodeCallback; + std::unordered_map displayNameCallbacks; + std::unordered_map descriptionCallbacks; + + UA_Boolean triggerCreateOptionalNode(const UA_NodeId* nodeId); + void triggerDisplayNameChanged(const UA_NodeId* nodeId, UA_LocalizedText* name, void* context); + void triggerDescriptionChanged(const UA_NodeId* nodeId, UA_LocalizedText* description, void* context); + + static UA_Boolean CreateOptionalNode(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* sourceNodeId, + const UA_NodeId* targetParentNodeId, + const UA_NodeId* referenceTypeId); + + static void DisplayNameChanged(UA_Server* server, UA_NodeId* nodeId, UA_LocalizedText* newDisplayName, void* context); + static void DescriptionChanged(UA_Server* server, UA_NodeId* nodeId, UA_LocalizedText* newDescription, void* context); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt b/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt new file mode 100644 index 0000000..4e62311 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/CMakeLists.txt @@ -0,0 +1,62 @@ +SET(MODULE_NAME opcuaserver) + +set(SOURCE_CPPS opcuaserver.cpp + opcuaserverlock.cpp + opcuasession.cpp + opcuataskqueue.cpp + opcuaaddnodeparams.cpp + opcuatmstypes.cpp + node_event_manager.cpp + opcuaservernode.cpp + opcuaservernodefactory.cpp + event_attributes.cpp + server_event_manager.cpp +) + +set(SOURCE_HEADERS common.h + opcuaserver.h + opcuaserverlock.h + opcuasession.h + opcuataskqueue.h + opcuatmstypes.h + opcuaaddnodeparams.h + node_event_manager.h + opcuaservernode.h + opcuaservernodefactory.h + event_attributes.h + server_event_manager.h +) + +set(INCLUDE_DIR ../include/${MODULE_NAME}) + +prepend_include(${INCLUDE_DIR} SOURCE_HEADERS) + +set(ALL_SOURCES ${SOURCE_CPPS} + ${SOURCE_HEADERS}) + +set(SOURCE_FILES ${ALL_SOURCES}) + +add_library(${MODULE_NAME} STATIC ${ALL_SOURCES}) +add_library(${SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) + +target_include_directories(${MODULE_NAME} PUBLIC $ + $ +) + +target_link_libraries(${MODULE_NAME} PUBLIC daq::opcuashared + daq::opendaq_utils + PRIVATE daq::opcua_daq_types + daq::coreobjects +) + +set_target_properties(${MODULE_NAME} PROPERTIES PUBLIC_HEADER "${SOURCE_HEADERS}") + +if(BUILD_64Bit) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "${MODULE_NAME}64") +endif() + +if(BUILD_64Bit OR BUILD_ARM) + set_target_properties(${MODULE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) +else() + set_target_properties(${MODULE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) +endif() diff --git a/shared/libraries/opcua/opcuaserver/src/event_attributes.cpp b/shared/libraries/opcua/opcuaserver/src/event_attributes.cpp new file mode 100644 index 0000000..6d43aae --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/event_attributes.cpp @@ -0,0 +1,63 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +EventAttributes::EventAttributes() +{ +} + +void EventAttributes::setTime(UA_UtcTime time) +{ + OpcUaVariant variant; + variant.setScalar(time); + setAttribute("Time", variant); +} + +void EventAttributes::setSeverity(UA_UInt16 eventSeverity) +{ + OpcUaVariant variant; + variant.setScalar(eventSeverity); + setAttribute("Severity", variant); +} + +void EventAttributes::setMessage(const std::string& message) +{ + setMessage("", message.c_str()); +} + +void EventAttributes::setMessage(const char* locale, const char* text) +{ + OpcUaObject message = UA_LOCALIZEDTEXT_ALLOC(locale, text); + setMessage(message); +} + +void EventAttributes::setMessage(const OpcUaObject& message) +{ + OpcUaVariant variant; + variant.setScalar(*message); + setAttribute("Message", variant); +} + +void EventAttributes::setSourceName(const std::string& eventSource) +{ + OpcUaVariant variant(eventSource.c_str()); + setAttribute("SourceName", variant); +} + +void EventAttributes::setAttribute(const std::string& attribute, const OpcUaObject& value) +{ + setAttribute(UA_QUALIFIEDNAME_ALLOC(0, attribute.c_str()), value); +} + +void EventAttributes::setAttribute(const OpcUaObject& attribute, const OpcUaObject& value) +{ + attributes[attribute] = value; +} + +const EventAttributes::AttributesType& EventAttributes::getAttributes() const +{ + return attributes; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/node_event_manager.cpp b/shared/libraries/opcua/opcuaserver/src/node_event_manager.cpp new file mode 100644 index 0000000..e0f8995 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/node_event_manager.cpp @@ -0,0 +1,193 @@ +#include "opcuaserver/node_event_manager.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +NodeEventManager::NodeEventManager(const OpcUaNodeId& nodeId, OpcUaServerPtr& server) + : nodeId(nodeId) + , server(server) +{ + UA_Server_setNodeContext(server->getUaServer(), *nodeId, this); +} + +void NodeEventManager::onRead(ReadCallback callback) +{ + readCallback = std::move(callback); + + UA_ValueCallback valueCallback; + UA_Server_getVariableNode_valueCallback(server->getUaServer(), *nodeId, &valueCallback); + valueCallback.onRead = OnRead; + UA_Server_setVariableNode_valueCallback(server->getUaServer(), *nodeId, valueCallback); +} + +void NodeEventManager::onWrite(WriteCallback callback) +{ + writeCallback = std::move(callback); + + UA_ValueCallback valueCallback; + + UA_Server_getVariableNode_valueCallback(server->getUaServer(), *nodeId, &valueCallback); + valueCallback.onWrite = OnWrite; + UA_Server_setVariableNode_valueCallback(server->getUaServer(), *nodeId, valueCallback); +} + +void NodeEventManager::onDataSourceRead(DataSourceReadCallback callback) +{ + dataSourceReadCallback = std::move(callback); + + UA_DataSource dataSource; + UA_Server_getVariableNode_dataSource(server->getUaServer(), *nodeId, &dataSource); + dataSource.read = OnDataSourceRead; + UA_Server_setVariableNode_dataSource(server->getUaServer(), *nodeId, dataSource); +} + +void NodeEventManager::onDataSourceWrite(DataSourceWriteCallback callback) +{ + dataSourceWriteCallback = std::move(callback); + + UA_DataSource dataSource; + UA_Server_getVariableNode_dataSource(server->getUaServer(), *nodeId, &dataSource); + dataSource.write = OnDataSourceWrite; + UA_Server_setVariableNode_dataSource(server->getUaServer(), *nodeId, dataSource); +} + +void NodeEventManager::onMethodCall(MethodCallback callback) +{ + methodCallback = std::move(callback); + + UA_Server_setMethodNodeCallback(server->getUaServer(), *nodeId, OnMethod); +} + +void NodeEventManager::onDisplayNameChanged(DisplayNameChangedCallback callback) +{ + server->getEventManager()->onDisplayNameChanged(nodeId, callback); +} + +void NodeEventManager::onDescriptionChanged(DescriptionChangedCallback callback) +{ + server->getEventManager()->onDescriptionChanged(nodeId, callback); +} + + +// c-style callback, required by open62541 interface + +void NodeEventManager::OnWrite(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + const UA_NumericRange* range, + const UA_DataValue* value) +{ + auto manager = (NodeEventManager*) nodeContext; + + WriteArgs args; + args.server = server; + args.sessionId = sessionId; + args.sessionContext = sessionContext; + args.nodeId = nodeId; + args.nodeContext = nodeContext; + args.range = range; + args.value = value; + + manager->writeCallback(args); +} + +void NodeEventManager::OnRead(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + const UA_NumericRange* range, + const UA_DataValue* value) +{ + auto manager = (NodeEventManager*) nodeContext; + + ReadArgs args; + args.server = server; + args.sessionId = sessionId; + args.sessionContext = sessionContext; + args.nodeId = nodeId; + args.nodeContext = nodeContext; + args.range = range; + args.value = value; + + manager->readCallback(args); +} + +UA_StatusCode NodeEventManager::OnDataSourceRead(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + UA_Boolean includeSourceTimeStamp, + const UA_NumericRange* range, + UA_DataValue* value) +{ + auto manager = (NodeEventManager*) nodeContext; + + DataSourceReadArgs args; + args.server = server; + args.sessionId = sessionId; + args.sessionContext = sessionContext; + args.nodeId = nodeId; + args.nodeContext = nodeContext; + args.includeSourceTimeStamp = includeSourceTimeStamp; + args.range = range; + args.value = value; + + return manager->dataSourceReadCallback(args); +} + +UA_StatusCode NodeEventManager::OnDataSourceWrite(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* nodeId, + void* nodeContext, + const UA_NumericRange* range, + const UA_DataValue* value) +{ + auto manager = (NodeEventManager*) nodeContext; + + DataSourceWriteArgs args; + args.server = server; + args.sessionId = sessionId; + args.sessionContext = sessionContext; + args.nodeId = nodeId; + args.nodeContext = nodeContext; + args.range = range; + args.value = value; + + return manager->dataSourceWriteCallback(args); +} + +UA_StatusCode NodeEventManager::OnMethod(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* methodId, + void* methodContext, + const UA_NodeId* objectId, + void* objectContext, + size_t inputSize, + const UA_Variant* input, + size_t outputSize, + UA_Variant* output) +{ + auto manager = (NodeEventManager*) methodContext; + + MethodArgs args; + args.server = server; + args.sessionId = sessionId; + args.sessionContext = sessionContext; + args.methodId = methodId; + args.methodContext = methodContext; + args.objectId = objectId; + args.objectContext = objectContext; + args.inputSize = inputSize; + args.input = input; + args.outputSize = outputSize; + args.output = output; + + return manager->methodCallback(args); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaaddnodeparams.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaaddnodeparams.cpp new file mode 100644 index 0000000..1a801f0 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuaaddnodeparams.cpp @@ -0,0 +1,140 @@ +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +/*RequestedNodeIdBaseOnName*/ + +static daq::opcua::OpcUaNodeId RequestedNodeIdBaseOnName(const std::string& name, const OpcUaNodeId& parentNodeId) +{ + if (parentNodeId.getValue().identifierType == UA_NODEIDTYPE_STRING) + { + std::string newNodeIdStr = utils::ToStdString(parentNodeId.getValue().identifier.string) + "/" + name; + return OpcUaNodeId(parentNodeId.getNamespaceIndex(), newNodeIdStr); + } + return UA_NODEID_NULL; +} + + +/*AddNodeParams*/ + +AddNodeParams::AddNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId, const OpcUaNodeId& referenceTypeId) + : requestedNewNodeId(requestedNewNodeId) + , parentNodeId(parentNodeId) + , referenceTypeId(referenceTypeId) +{ +} + +void AddNodeParams::setBrowseName(const std::string& browseName) +{ + this->browseName = UA_QUALIFIEDNAME_ALLOC(0, browseName.c_str()); +} + +/*GenericAddNodeParams*/ + +template +GenericAddNodeParams::GenericAddNodeParams(const OpcUaNodeId& requestedNewNodeId, + const OpcUaNodeId& parentNodeId, + const OpcUaNodeId& referenceTypeId, + const T& defaultAttributes) + : AddNodeParams(requestedNewNodeId, parentNodeId, referenceTypeId) + , attr(defaultAttributes) +{ +} + +/*AddObjectNodeParams*/ + +AddObjectNodeParams::AddObjectNodeParams(const OpcUaNodeId& requestedNewNodeId) + : AddObjectNodeParams(requestedNewNodeId, OpcUaNodeId()) +{ +} + +AddObjectNodeParams::AddObjectNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + requestedNewNodeId, parentNodeId, OpcUaNodeId(UA_NS0ID_HASCOMPONENT), UA_ObjectAttributes_default) +{ +} + +AddObjectNodeParams::AddObjectNodeParams(const std::string& name, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + RequestedNodeIdBaseOnName(name, parentNodeId), parentNodeId, OpcUaNodeId(UA_NS0ID_HASCOMPONENT), UA_ObjectAttributes_default) +{ +} + +/*AddVariableNodeParams*/ + +AddVariableNodeParams::AddVariableNodeParams(const std::string& name, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + RequestedNodeIdBaseOnName(name, parentNodeId), parentNodeId, OpcUaNodeId(UA_NS0ID_HASPROPERTY), UA_VariableAttributes_default) +{ +} + +AddVariableNodeParams::AddVariableNodeParams(const OpcUaNodeId& requestedNewNodeId) + : AddVariableNodeParams(requestedNewNodeId, OpcUaNodeId()) +{ +} + +AddVariableNodeParams::AddVariableNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + requestedNewNodeId, parentNodeId, OpcUaNodeId(UA_NS0ID_HASPROPERTY), UA_VariableAttributes_default) +{ +} + +void AddVariableNodeParams::setDataType(const OpcUaNodeId& dataTypeId) +{ + attr->dataType = *dataTypeId; +} + +/*AddMethodNodeParams*/ + +AddMethodNodeParams::AddMethodNodeParams(const OpcUaNodeId& requestedNewNodeId) + : AddMethodNodeParams(requestedNewNodeId, OpcUaNodeId()) +{ +} + +AddMethodNodeParams::AddMethodNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + requestedNewNodeId, parentNodeId, OpcUaNodeId(UA_NS0ID_HASPROPERTY), UA_MethodAttributes_default) +{ +} + +AddMethodNodeParams::AddMethodNodeParams(const std::string& name, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + RequestedNodeIdBaseOnName(name, parentNodeId), parentNodeId, OpcUaNodeId(UA_NS0ID_HASPROPERTY), UA_MethodAttributes_default) +{ +} + +AddMethodNodeParams::~AddMethodNodeParams() +{ + if(outputArguments) + UA_Array_delete(outputArguments, outputArgumentsSize, GetUaDataType()); + if(inputArguments) + UA_Array_delete(inputArguments, inputArgumentsSize, GetUaDataType()); +} + +/*AddVariableTypeNodeParams*/ + +AddVariableTypeNodeParams::AddVariableTypeNodeParams(const OpcUaNodeId& requestedNewNodeId) + : AddVariableTypeNodeParams(requestedNewNodeId, OpcUaNodeId()) +{ +} + +AddVariableTypeNodeParams::AddVariableTypeNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + requestedNewNodeId, parentNodeId, OpcUaNodeId(UA_NS0ID_HASSUBTYPE), UA_VariableTypeAttributes_default) +{ +} + +/*AddObjectTypeNodeParams*/ + +AddObjectTypeNodeParams::AddObjectTypeNodeParams(const OpcUaNodeId& requestedNewNodeId) + : AddObjectTypeNodeParams(requestedNewNodeId, OpcUaNodeId()) +{ +} + +AddObjectTypeNodeParams::AddObjectTypeNodeParams(const OpcUaNodeId& requestedNewNodeId, const OpcUaNodeId& parentNodeId) + : GenericAddNodeParams( + requestedNewNodeId, parentNodeId, OpcUaNodeId(UA_NS0ID_HASSUBTYPE), UA_ObjectTypeAttributes_default) +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp new file mode 100644 index 0000000..94b21c3 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserver.cpp @@ -0,0 +1,663 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaServer::OpcUaServer() + : eventManager(std::make_shared(this)) +{ + setPort(OPCUA_DEFAULT_PORT); + createSessionContextCallback = [this](const OpcUaNodeId& sessionId) { return createSessionContextCallbackImp(sessionId); }; + deleteSessionContextCallback = [this](void* context) { deleteSessionContextCallbackImp(context); }; + authenticationProvider = AuthenticationProvider(); +} + +void* OpcUaServer::createSessionContextCallbackImp(const OpcUaNodeId& sessionId) +{ + return new OpcUaSession(sessionId, &serverLock); +} + +void OpcUaServer::deleteSessionContextCallbackImp(void* context) +{ + delete static_cast(context); +} + +std::unordered_set& OpcUaServer::getSessions() +{ + return sessionContext; +} + +OpcUaServer::~OpcUaServer() +{ + OpcUaServer::stop(); +} + +uint16_t& OpcUaServer::getPort() +{ + return port; +} + +void OpcUaServer::setPort(uint16_t port) +{ + this->port = port; +} + +void OpcUaServer::setAuthenticationProvider(const AuthenticationProviderPtr& authenticationProvider) +{ + this->authenticationProvider = authenticationProvider; +} + +void OpcUaServer::setClientConnectedHandler(const OnClientConnectedCallback& callback) +{ + this->clientConnectedHandler = callback; +} + +void OpcUaServer::setClientDisconnectedHandler(const OnClientDisconnectedCallback& callback) +{ + this->clientDisconnectedHandler = callback; +} + +void OpcUaServer::setSecurityConfig(OpcUaServerSecurityConfig* config) +{ + throw std::runtime_error("method setSecurityConfig() is deprecated"); +} + +const OpcUaServerSecurityConfig* OpcUaServer::getSecurityConfig() const +{ + throw std::runtime_error("method getSecurityConfig() is deprecated"); +} + +void OpcUaServer::start() +{ + if (getStarted()) + throw OpcUaException(UA_STATUSCODE_BADINVALIDSTATE, "Thread is already started."); + + if (!isPrepared()) + prepare(); + + UA_StatusCode retval = UA_Server_run_startup(server); + CheckStatusCodeException(retval, "Failed to start server"); + + ThreadEx::start(); +} + +void OpcUaServer::stop() +{ + ThreadEx::stop(); + shutdownServer(); +} + +void OpcUaServer::prepare() +{ + try + { + if (getStarted()) + throw OpcUaException(UA_STATUSCODE_BADINVALIDSTATE, "Server is running"); + + if (isPrepared()) + shutdownServer(); + + prepareServer(); + } + catch (const OpcUaException&) + { + shutdownServer(); + throw; + } +} + +bool OpcUaServer::isPrepared() +{ + return server != nullptr; +} + +UA_Server* OpcUaServer::createServer() +{ + UA_ServerConfig config; + memset(&config, 0, sizeof(UA_ServerConfig)); + config.logger = UA_Log_Stdout_withLevel(UA_LOGLEVEL_WARNING); + UA_Nodestore_HashMap(&config.nodestore); + return UA_Server_newWithConfig(&config); +} + +void OpcUaServer::prepareServer() +{ + server = createServer(); + UA_ServerConfig* config = UA_Server_getConfig(server); + + prepareServerMinimal(config); + config->context = this; + config->nodeLifecycle.generateChildNodeId = generateChildId; + + prepareAccessControl(config); + addTmsTypes(server); + + eventManager->registerEvents(); +} + +void OpcUaServer::prepareServerMinimal(UA_ServerConfig* config) +{ + UA_StatusCode retval = UA_ServerConfig_setMinimal(config, getPort(), nullptr); + CheckStatusCodeException(retval, "Failed to configure server minimal."); +} + +void OpcUaServer::prepareAccessControl(UA_ServerConfig* config) +{ + config->accessControl.clear(&config->accessControl); + + auto status = + UA_AccessControl_default(config, false, NULL, &config->securityPolicies[config->securityPoliciesSize - 1].policyUri, 0, nullptr); + + CheckStatusCodeException(status, "Failed to configure access control."); + + activateSession_default = config->accessControl.activateSession; + config->accessControl.activateSession = activateSession; + config->accessControl.closeSession = closeSession; +} + +void OpcUaServer::shutdownServer() +{ + if (getStarted()) + { + UA_StatusCode status = UA_Server_run_shutdown(server); + CheckStatusCodeException(status); + } + + if (isPrepared()) + { + UA_Server_delete(server); + server = nullptr; + } + + assert(sessionContext.size() == 0); +} + +UA_StatusCode OpcUaServer::validateIdentityToken(const UA_ExtensionObject* token) +{ + if (token->content.decoded.type == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]) + { + if (isUsernameIdentityTokenValid((UA_UserNameIdentityToken*) token->content.decoded.data)) + return UA_STATUSCODE_GOOD; + } + else if (token->content.decoded.type == &UA_TYPES[UA_TYPES_ANONYMOUSIDENTITYTOKEN]) + { + if (isAnonymousIdentityTokenValid((UA_AnonymousIdentityToken*) token->content.decoded.data)) + return UA_STATUSCODE_GOOD; + } + else + { + return UA_STATUSCODE_BADIDENTITYTOKENINVALID; + } + + return UA_STATUSCODE_BADUSERACCESSDENIED; +} + +bool OpcUaServer::isUsernameIdentityTokenValid(const UA_UserNameIdentityToken* token) +{ + const auto username = utils::ToStdString(token->userName); + const auto password = utils::ToStdString(token->password); + + try + { + auto errorGuard = DAQ_ERROR_GUARD(); + authenticationProvider.authenticate(username, password); + } + catch (const DaqException&) + { + return false; + } + + return true; +} + +bool OpcUaServer::isAnonymousIdentityTokenValid(const UA_AnonymousIdentityToken* /*token*/) +{ + return authenticationProvider.isAnonymousAllowed(); +} + +void OpcUaServer::createSession(const OpcUaNodeId& sessionId, void** sessionContext) +{ + bool sessionContextAlreadyCreated = *sessionContext != nullptr; + + if (createSessionContextCallback && !sessionContextAlreadyCreated) + { + *sessionContext = createSessionContextCallback(sessionId); + if (*sessionContext != nullptr) + this->sessionContext.insert(*sessionContext); + } +} + +void OpcUaServer::execute() +{ + setThreadName("OpenDAQOPCUAServerModule"); + while (!terminated) + { + UA_Server_run_iterate(server, true); + } + shutdownServer(); +} + +OpcUaServerNode OpcUaServer::getNode(const OpcUaNodeId& nodeId) +{ + return OpcUaServerNode(*this, nodeId); +} +OpcUaServerObjectNode OpcUaServer::getRootNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_ROOTFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getObjectsNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getTypesNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_TYPESFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getViewsNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_VIEWSFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getObjectTypesNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_OBJECTTYPESFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getVariableTypesNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_VARIABLETYPESFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getDataTypesNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_DATATYPESFOLDER)); +} +OpcUaServerObjectNode OpcUaServer::getReferenceTypesNode() +{ + return OpcUaServerObjectNode(*this, OpcUaNodeId(UA_NS0ID_REFERENCETYPESFOLDER)); +} + +ServerEventManagerPtr OpcUaServer::getEventManager() +{ + return eventManager; +} + +bool OpcUaServer::nodeExists(const OpcUaNodeId& nodeId) +{ + return nodeExists(*nodeId); +} + +bool OpcUaServer::nodeExists(const UA_NodeId& nodeId) +{ + UA_NodeClass nodeClass; + return UA_Server_readNodeClass(server, nodeId, &nodeClass) == UA_STATUSCODE_GOOD; +} + +OpcUaObject OpcUaServer::browse(const OpcUaObject& browseDescription) +{ + const size_t MAX_REFERENCES = 0; // All + return UA_Server_browse(server, MAX_REFERENCES, browseDescription.get()); +} + +OpcUaNodeId OpcUaServer::addObjectNode(const AddObjectNodeParams& params) +{ + auto lock = std::lock_guard(getLock()); + eventManager->onCreateOptionalNode(params.addOptionalNodeCallback); + UA_NodeId outNodeId; + + auto status = UA_Server_addObjectNode(server, + *params.requestedNewNodeId, + *params.parentNodeId, + *params.referenceTypeId, + *params.browseName, + *params.typeDefinition, + *params.attr, + params.nodeContext, + &outNodeId); + + CheckStatusCodeException(status); + + return outNodeId; +} + +OpcUaNodeId OpcUaServer::addVariableNode(const AddVariableNodeParams& params) +{ + auto lock = std::lock_guard(getLock()); + eventManager->onCreateOptionalNode(params.addOptionalNodeCallback); + UA_NodeId outNodeId; + + auto status = UA_Server_addVariableNode(server, + *params.requestedNewNodeId, + *params.parentNodeId, + *params.referenceTypeId, + *params.browseName, + *params.typeDefinition, + *params.attr, + params.nodeContext, + &outNodeId); + + CheckStatusCodeException(status); + return outNodeId; +} + +OpcUaNodeId OpcUaServer::addMethodNode(const AddMethodNodeParams& params) +{ + UA_NodeId outNodeId; + CheckStatusCodeException(UA_Server_addMethodNode(server, + *params.requestedNewNodeId, + *params.parentNodeId, + *params.referenceTypeId, + *params.browseName, + *params.attr, + params.method, + params.inputArgumentsSize, + params.inputArguments, + params.outputArgumentsSize, + params.outputArguments, + params.nodeContext, + &outNodeId)); + + return outNodeId; +} + +OpcUaNodeId OpcUaServer::addObjectTypeNode(const AddObjectTypeNodeParams& params) +{ + UA_NodeId outNodeId; + CheckStatusCodeException(UA_Server_addObjectTypeNode(server, + *params.requestedNewNodeId, + *params.parentNodeId, + *params.referenceTypeId, + *params.browseName, + *params.attr, + params.nodeContext, + &outNodeId)); + + return outNodeId; +} + +OpcUaNodeId OpcUaServer::addVariableTypeNode(const AddVariableTypeNodeParams& params) +{ + UA_NodeId outNodeId; + CheckStatusCodeException(UA_Server_addVariableTypeNode(server, + *params.requestedNewNodeId, + *params.parentNodeId, + *params.referenceTypeId, + *params.browseName, + *params.typeDefinition, + *params.attr, + params.nodeContext, + &outNodeId)); + + return outNodeId; +} + +void OpcUaServer::triggerEvent(const OpcUaNodeId& eventType, const OpcUaNodeId& originNodeId, const EventAttributes& eventAttributes) +{ + OpcUaNodeId eventNodeId; + CheckStatusCodeException(UA_Server_createEvent(server, *eventType, eventNodeId.get()), "createEvent failed"); + + const auto& attributes = eventAttributes.getAttributes(); + for (const auto& attribute : attributes) + { + const OpcUaObject& propertyName = attribute.first; + const OpcUaObject& value = attribute.second; + CheckStatusCodeException(UA_Server_writeObjectProperty(server, *eventNodeId, *propertyName, *value), + "setting event attribute fails"); + } + + CheckStatusCodeException(UA_Server_triggerEvent(server, *eventNodeId, *originNodeId, NULL, UA_TRUE), "triggerEvent failed"); +} + +OpcUaObject OpcUaServer::readBrowseName(const OpcUaNodeId& nodeId) const +{ + OpcUaObject qualifiedName; + CheckStatusCodeException(UA_Server_readBrowseName(server, *nodeId, qualifiedName.get())); + return qualifiedName; +} + +std::string OpcUaServer::readBrowseNameString(const OpcUaNodeId& nodeId) const +{ + auto browseName = readBrowseName(nodeId); + return utils::ToStdString(browseName->name); +} + +OpcUaNodeClass OpcUaServer::readNodeClass(const OpcUaNodeId& nodeId) const +{ + UA_NodeClass nodeClass; + CheckStatusCodeException(UA_Server_readNodeClass(server, *nodeId, &nodeClass)); + return static_cast(nodeClass); +} + +void OpcUaServer::setDisplayName(const OpcUaNodeId& nodeId, const OpcUaObject& localizedText) +{ + const auto status = UA_Server_writeDisplayName(server, *nodeId, *localizedText); + CheckStatusCodeException(status); +} + +void OpcUaServer::setDisplayName(const OpcUaNodeId& nodeId, const std::string& text) +{ + OpcUaObject localizedText = UA_LOCALIZEDTEXT_ALLOC("", text.c_str()); + setDisplayName(nodeId, localizedText); +} + +OpcUaObject OpcUaServer::readDisplayName(const OpcUaNodeId& nodeId) const +{ + OpcUaObject localizedText; + CheckStatusCodeException(UA_Server_readDisplayName(server, *nodeId, localizedText.get())); + return localizedText; +} + +void OpcUaServer::setDescription(const OpcUaNodeId& nodeId, const OpcUaObject& localizedText) +{ + const auto status = UA_Server_writeDescription(server, *nodeId, *localizedText); + CheckStatusCodeException(status); +} + +void OpcUaServer::setDescription(const OpcUaNodeId& nodeId, const std::string& text) +{ + OpcUaObject localizedText = UA_LOCALIZEDTEXT_ALLOC("", text.c_str()); + setDescription(nodeId, localizedText); +} + +void OpcUaServer::setAccessLevel(const OpcUaNodeId& nodeId, UA_Byte accessLevel) +{ + const auto status = UA_Server_writeAccessLevel(server, *nodeId, accessLevel); + CheckStatusCodeException(status); +} + +void OpcUaServer::writeValue(const OpcUaNodeId& nodeId, const OpcUaVariant& value) +{ + CheckStatusCodeException(UA_Server_writeValue(server, *nodeId, *value)); +} + +OpcUaVariant OpcUaServer::readValue(const OpcUaNodeId& nodeId) +{ + OpcUaVariant value; + CheckStatusCodeException(UA_Server_readValue(server, *nodeId, value.get())); + return value; +} + +OpcUaNodeId OpcUaServer::readDataType(const OpcUaNodeId& typeNodeId) +{ + OpcUaNodeId dataTypeId; + const auto status = UA_Server_readDataType(getUaServer(), *typeNodeId, dataTypeId.get()); + CheckStatusCodeException(status); + return dataTypeId; +} + +void OpcUaServer::deleteNode(const OpcUaNode& node) +{ + deleteNode(node.getNodeId()); +} + +void OpcUaServer::deleteNode(const OpcUaNodeId& nodeId) +{ + UA_StatusCode status = UA_Server_deleteNode(server, *nodeId, true); + CheckStatusCodeException(status); +} + +void OpcUaServer::addReference(const OpcUaNodeId& sourceId, const OpcUaNodeId& refTypeId, const OpcUaNodeId& targetId, bool isForward) +{ + OpcUaObject extendedTargetId; + extendedTargetId->nodeId = targetId.copyAndGetDetachedValue(); + + UA_StatusCode status = UA_Server_addReference(server, *sourceId, *refTypeId, *extendedTargetId, isForward); + CheckStatusCodeException(status); +} + +void OpcUaServer::deleteReference(const OpcUaNodeId& sourceId, const OpcUaNodeId& refTypeId, const OpcUaNodeId& targetId, bool isForward) +{ + OpcUaObject extendedTargetId; + extendedTargetId->nodeId = targetId.copyAndGetDetachedValue(); + + UA_StatusCode status = UA_Server_deleteReference(server, *sourceId, *refTypeId, isForward, *extendedTargetId, true); + CheckStatusCodeException(status); +} + +bool OpcUaServer::referenceExists(const OpcUaNodeId& sourceId, const OpcUaNodeId& refTypeId, const OpcUaNodeId& targetId, bool isForward) +{ + OpcUaObject browseDesc; + browseDesc->browseDirection = UA_BROWSEDIRECTION_BOTH; + browseDesc->nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE | UA_NODECLASS_METHOD; + browseDesc->includeSubtypes = false; + browseDesc->nodeId = sourceId.copyAndGetDetachedValue(); + browseDesc->referenceTypeId = refTypeId.copyAndGetDetachedValue(); + browseDesc->resultMask = UA_BROWSERESULTMASK_ISFORWARD; + + OpcUaObject browseResult = UA_Server_browse(server, 0, browseDesc.get()); + + UA_NodeId_init(&browseDesc->nodeId); + UA_NodeId_init(&browseDesc->referenceTypeId); + + for (size_t i = 0; i < browseResult->referencesSize; i++) + { + const auto& referenceDesc = browseResult->references[i]; + if (referenceDesc.isForward == isForward && targetId == referenceDesc.nodeId.nodeId) + return true; + } + return false; +} + +OpcUaServer* OpcUaServer::getServer(UA_Server* server) +{ + auto config = UA_Server_getConfig(server); + assert(config != nullptr && config->context != nullptr); + return (OpcUaServer*) config->context; +} + +UA_StatusCode OpcUaServer::activateSession(UA_Server* server, + UA_AccessControl* ac, + const UA_EndpointDescription* endpointDescription, + const UA_ByteString* secureChannelRemoteCertificate, + const UA_NodeId* sessionId, + const UA_ExtensionObject* userIdentityToken, + void** sessionContext) +{ + OpcUaServer* serverInstance = getServer(server); + + // activateSession_default resets sessionContext. Subsequent calls should keep the context + void* unusedSessionContext = nullptr; + UA_StatusCode status = serverInstance->activateSession_default( + server, ac, endpointDescription, secureChannelRemoteCertificate, sessionId, userIdentityToken, &unusedSessionContext); + assert(unusedSessionContext == nullptr); + + switch (status) + { + case UA_STATUSCODE_GOOD: + case UA_STATUSCODE_BADUSERACCESSDENIED: + case UA_STATUSCODE_BADIDENTITYTOKENINVALID: + { + status = serverInstance->validateIdentityToken(userIdentityToken); + break; + } + } + + if (status == UA_STATUSCODE_GOOD) + { + serverInstance->createSession(*sessionId, sessionContext); + if (serverInstance->clientConnectedHandler) + serverInstance->clientConnectedHandler(OpcUaNodeId::getIdentifier(*sessionId)); + } + + return status; +} + +void OpcUaServer::closeSession(UA_Server* server, UA_AccessControl* ac, const UA_NodeId* sessionId, void* sessionContext) +{ + OpcUaNodeId sessionNodeId(*sessionId, true); + OpcUaServer* serverInstance = getServer(server); + + serverInstance->serverLock.refuseConfigurationControlLock(sessionNodeId); + + if (sessionContext != nullptr) + serverInstance->sessionContext.erase(sessionContext); + + if (serverInstance->deleteSessionContextCallback) + serverInstance->deleteSessionContextCallback(sessionContext); + + if (serverInstance->clientDisconnectedHandler) + serverInstance->clientDisconnectedHandler(OpcUaNodeId::getIdentifier(*sessionId)); +} + + +UA_StatusCode OpcUaServer::generateChildId(UA_Server* server, + const UA_NodeId* /*sessionId*/, + void* /*sessionContext*/, + const UA_NodeId* sourceNodeId, + const UA_NodeId* targetParentNodeId, + const UA_NodeId* /*referenceTypeId*/, + UA_NodeId* targetNodeId) +{ + if (targetParentNodeId->identifierType == UA_NODEIDTYPE_STRING) { + const std::string parentNodeIdStr = utils::ToStdString(targetParentNodeId->identifier.string); + std::string objectTypeIdStr; + + if (sourceNodeId->identifierType == UA_NODEIDTYPE_STRING) + { + objectTypeIdStr = utils::ToStdString(sourceNodeId->identifier.string); + } + else if (sourceNodeId->identifierType == UA_NODEIDTYPE_NUMERIC) + { + UA_QualifiedName* browseName = UA_QualifiedName_new(); + UA_Server_readBrowseName(server, *sourceNodeId, browseName); + objectTypeIdStr = utils::ToStdString(browseName->name); + UA_QualifiedName_delete(browseName); + } + else + { + return UA_STATUSCODE_GOOD; + } + + const auto newNodeIdStr = parentNodeIdStr + "/" + objectTypeIdStr; + *targetNodeId = UA_NODEID_STRING_ALLOC(targetParentNodeId->namespaceIndex, newNodeIdStr.c_str()); + } + + return UA_STATUSCODE_GOOD; +} + + +bool OpcUaServer::passwordLock(const std::string& password) +{ + return serverLock.passwordLock(password); +} + +bool OpcUaServer::passwordUnlock(const std::string& password) +{ + return serverLock.passwordUnlock(password); +} + +bool OpcUaServer::isPasswordLocked() +{ + return serverLock.isPasswordLocked(); +} + +UA_Server* OpcUaServer::getUaServer() const noexcept +{ + return server; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaserverlock.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaserverlock.cpp new file mode 100644 index 0000000..8755f20 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuaserverlock.cpp @@ -0,0 +1,91 @@ +#include "opcuaserver/opcuaserverlock.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaServerLock::OpcUaServerLock() +{ + configurationControlLockSessionId = OpcUaNodeId(); + refuseConfigurationControlLock(configurationControlLockSessionId); +} + +OpcUaServerLock::~OpcUaServerLock() +{ +} + +bool OpcUaServerLock::passwordLock(const std::string& password, const OpcUaNodeId& sessionId) +{ + if (!canEditPasswordLock(sessionId)) + return false; + + if (isPasswordLocked()) + return false; + + this->password = password; + return true; +} + +bool OpcUaServerLock::passwordUnlock(const std::string& password, const OpcUaNodeId& sessionId) +{ + if (!canEditPasswordLock(sessionId)) + return false; + + if (isPasswordLocked() && this->password != password) + return false; + + this->password = ""; + return true; +} + +bool OpcUaServerLock::isPasswordLocked() +{ + return password != ""; +} + +bool OpcUaServerLock::hasConfigurationControlLock(const OpcUaNodeId& sessionId) const +{ + return sessionId == configurationControlLockSessionId && hasActiveConfigurationControlLock(); +} + +void OpcUaServerLock::refuseConfigurationControlLock(const OpcUaNodeId& sessionId) +{ + if (sessionId == configurationControlLockSessionId) + lockConfigurationControl(sessionId, std::chrono::seconds(-1)); +} + +bool OpcUaServerLock::lockConfigurationControl(const OpcUaNodeId& sessionId, const std::chrono::seconds timeout) +{ + if (hasConfigurationControlAccess(sessionId)) + { + configurationControlLockSessionId = sessionId; + configurationControlLockValidTo = utils::GetDurationTimeStamp() + timeout; + return true; + } + return false; +} + +bool OpcUaServerLock::hasConfigurationControlAccess(const OpcUaNodeId& sessionId) const +{ + return hasConfigurationControlLock(sessionId) || !hasActiveConfigurationControlLock(); +} + +bool OpcUaServerLock::hasActiveConfigurationControlLock() const +{ + return utils::GetDurationTimeStamp() < configurationControlLockValidTo; +} + +bool OpcUaServerLock::canControlAcq(const OpcUaNodeId& sessionId) +{ + return !isPasswordLocked() && hasConfigurationControlAccess(sessionId); +} + +bool OpcUaServerLock::canEditPasswordLock(const OpcUaNodeId& sessionId) +{ + if (this->hasConfigurationControlLock(sessionId)) + return true; + + return !hasActiveConfigurationControlLock(); +} + + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaservernode.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaservernode.cpp new file mode 100644 index 0000000..89d1097 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuaservernode.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaServerNode::OpcUaServerNode(OpcUaServer& server, const OpcUaNodeId& nodeId) + : server(server) + , nodeId(nodeId) +{ + // check if node exists + server.readNodeClass(nodeId); +} + +OpcUaNodeId OpcUaServerNode::getNodeId() const +{ + return nodeId; +} + +OpcUaNodeClass OpcUaServerNode::getNodeClass() +{ + return server.readNodeClass(getNodeId()); +} + +OpcUaObject OpcUaServerNode::getBrowseName() +{ + return server.readBrowseName(getNodeId()); +} + +void OpcUaServerNode::setDisplayName(const std::string& displayName) +{ + OpcUaObject localizedText; + localizedText->text = UA_STRING_ALLOC(displayName.c_str()); + + server.setDisplayName(getNodeId(), localizedText); +} + +OpcUaObject OpcUaServerNode::getDisplayName() +{ + return server.readDisplayName(getNodeId()); +} + +std::vector> OpcUaServerNode::browse(const OpcUaNodeId& referenceTypeId, + bool includeSubtypes, + OpcUaNodeClass nodeClassMask, + UA_BrowseDirection browseDirection) +{ + OpcUaObject browseDescription; + browseDescription->nodeId = nodeId.copyAndGetDetachedValue(); + browseDescription->browseDirection = browseDirection; + browseDescription->referenceTypeId = referenceTypeId.copyAndGetDetachedValue(); + browseDescription->nodeClassMask = (UA_UInt32) nodeClassMask; + browseDescription->includeSubtypes = includeSubtypes; + browseDescription->resultMask = UA_BROWSERESULTMASK_NODECLASS; + + auto browseResult = server.browse(browseDescription); + CheckStatusCodeException(browseResult->statusCode, "Browse failed"); + + OpcUaServerNodeFactory nodeFactory(server); + + std::vector> result; + for (size_t i = 0; i < browseResult->referencesSize; i++) + { + const auto reference = browseResult->references[i]; + + OpcUaNodeId nodeId(reference.nodeId.nodeId); + result.push_back(nodeFactory.createServerNode(nodeId, reference.nodeClass)); + } + + return result; +} + +std::vector> OpcUaServerNode::browseChildNodes() +{ + return browse(OpcUaNodeId(UA_NS0ID_HIERARCHICALREFERENCES)); +} + +std::unique_ptr OpcUaServerNode::getChildNode(const OpcUaObject& qualifiedName) +{ + OpcUaObject result = UA_Server_browseSimplifiedBrowsePath(server.getUaServer(), *nodeId, 1, qualifiedName.get()); + CheckStatusCodeException(result->statusCode, "Browse failed"); + assert(result->targetsSize == 1); + OpcUaServerNodeFactory factory(server); + return factory.createServerNode(result->targets[0].targetId.nodeId); +} + +OpcUaServerObjectNode OpcUaServerNode::addObject(AddObjectNodeParams& params) +{ + params.parentNodeId = getNodeId(); + auto newNodeId = server.addObjectNode(params); + return OpcUaServerObjectNode(server, newNodeId); +} + +OpcUaServerObjectNode OpcUaServerNode::addObject(const OpcUaNodeId& nodeId, const std::string& browseName) +{ + AddObjectNodeParams nodeParams(nodeId); + nodeParams.setBrowseName(browseName); + return addObject(nodeParams); +} + +OpcUaServerVariableNode OpcUaServerNode::addVariable(AddVariableNodeParams& params) +{ + params.parentNodeId = getNodeId(); + auto newNodeId = server.addVariableNode(params); + return OpcUaServerVariableNode(server, newNodeId); +} + +OpcUaServerVariableNode OpcUaServerNode::addVariable(const OpcUaNodeId& nodeId, const std::string& browseName) +{ + AddVariableNodeParams nodeParams(nodeId); + nodeParams.setBrowseName(browseName); + + return addVariable(nodeParams); +} + +void OpcUaServerVariableNode::writeVariantToServer(const OpcUaVariant& var) +{ + server.writeValue(getNodeId(), var); +} + +OpcUaVariant OpcUaServerVariableNode::readVariantFromServer() +{ + return server.readValue(getNodeId()); +} + +void OpcUaServerNode::remove() +{ + server.deleteNode(nodeId); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuaservernodefactory.cpp b/shared/libraries/opcua/opcuaserver/src/opcuaservernodefactory.cpp new file mode 100644 index 0000000..f0afa58 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuaservernodefactory.cpp @@ -0,0 +1,42 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaServerNodeFactory::OpcUaServerNodeFactory(OpcUaServer& server) + : server(server) +{ +} + +std::unique_ptr OpcUaServerNodeFactory::createServerNode(const OpcUaNodeId& nodeId) +{ + return createServerNode(nodeId, server.readNodeClass(nodeId)); +} + +std::unique_ptr OpcUaServerNodeFactory::createServerNode(const OpcUaNodeId& nodeId, OpcUaNodeClass nodeClass) +{ + switch (nodeClass) + { + case OpcUaNodeClass::Object: + return std::make_unique(server, nodeId); + case OpcUaNodeClass::Variable: + return std::make_unique(server, nodeId); + case OpcUaNodeClass::Method: + return std::make_unique(server, nodeId); + case OpcUaNodeClass::ObjectType: + return std::make_unique(server, nodeId); + case OpcUaNodeClass::VariableType: + return std::make_unique(server, nodeId); + case OpcUaNodeClass::DataType: + return std::make_unique(server, nodeId); + default: + return std::make_unique(server, nodeId); + } +} + +std::unique_ptr OpcUaServerNodeFactory::createServerNode(const OpcUaNodeId& nodeId, UA_NodeClass nodeClass) +{ + return createServerNode(nodeId, static_cast(nodeClass)); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuasession.cpp b/shared/libraries/opcua/opcuaserver/src/opcuasession.cpp new file mode 100644 index 0000000..7a049df --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuasession.cpp @@ -0,0 +1,59 @@ +#include "opcuaserver/opcuasession.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaSession::OpcUaSession(const OpcUaNodeId& sessionId, OpcUaServerLock* serverLock) + : sessionId(sessionId) + , serverLock(serverLock) +{ +} + +OpcUaSession::~OpcUaSession() +{ +} + +bool OpcUaSession::hasConfigurationControlLock() const +{ + return serverLock->hasConfigurationControlLock(getConfigurationLockTokenId()); +} + +void OpcUaSession::refuseConfigurationControlLock() +{ + serverLock->refuseConfigurationControlLock(getConfigurationLockTokenId()); +} + +bool OpcUaSession::lockConfigurationControl(const std::chrono::seconds timeout) +{ + return serverLock->lockConfigurationControl(getConfigurationLockTokenId(), timeout); +} + +bool OpcUaSession::canControlAcq() +{ + return serverLock->canControlAcq(getConfigurationLockTokenId()); +} + +bool OpcUaSession::passwordLock(const std::string& password) +{ + return serverLock->passwordLock(password, getConfigurationLockTokenId()); +} + +bool OpcUaSession::passwordUnlock(const std::string& password) +{ + return serverLock->passwordUnlock(password, getConfigurationLockTokenId()); +} + +void OpcUaSession::setConfigurationLockTokenId(const OpcUaNodeId& configurationLockTokenId) +{ + this->configurationLockTokenId = configurationLockTokenId; +} + +const OpcUaNodeId& OpcUaSession::getConfigurationLockTokenId() const +{ + if (!configurationLockTokenId.isNull()) + return configurationLockTokenId; + + return sessionId; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuataskqueue.cpp b/shared/libraries/opcua/opcuaserver/src/opcuataskqueue.cpp new file mode 100644 index 0000000..4f4831d --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuataskqueue.cpp @@ -0,0 +1,64 @@ +#include "opcuaserver/opcuataskqueue.h" +#include "opcuashared/opcua.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +void OpcUaTaskQueue::push(Function&& func) +{ + { + std::lock_guard guard(mutex_); + q_.emplace_back(func); + } + cv_.notify_one(); +} + +bool OpcUaTaskQueue::pop(Function& func) +{ + std::lock_guard guard(mutex_); + if (q_.empty()) + { + return false; + } + else + { + func = q_.front(); + q_.pop_front(); + return true; + } +} + +bool OpcUaTaskQueue::pop(Function& func, const unsigned int timeout_ms) +{ + std::unique_lock lock(mutex_); + if (timeout_ms == infinity) + { + cv_.wait(lock, [& q = q_]() { return !q.empty(); }); + } + else + { + cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms), [& q = q_]() { return !q.empty(); }); + } + + if (q_.empty()) + { + return false; + } + else + { + func = q_.front(); + q_.pop_front(); + return true; + } +} + +void OpcUaTaskQueue::processTaskQueue() +{ + OpcUaTaskQueue::Function func; + + while (pop(func)) + { + func(); + } +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/opcuatmstypes.cpp b/shared/libraries/opcua/opcuaserver/src/opcuatmstypes.cpp new file mode 100644 index 0000000..7a5fb10 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/opcuatmstypes.cpp @@ -0,0 +1,62 @@ + +#include +#include +#ifdef NAMESPACE_DAQBT + #include + #include +#endif +#ifdef NAMESPACE_DAQBSP + #include + #include +#endif +#ifdef NAMESPACE_DAQDEVICE + #include + #include +#endif +#ifdef NAMESPACE_DAQESP + #include + #include +#endif +#include "opcuashared/opcuaexception.h" +#include "opcuashared/opcualog.h" +#include "opcuaserver/opcuatmstypes.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +void addTmsTypes(UA_Server *server) +{ + UA_StatusCode uaStatus = namespace_di_generated(server); + CheckStatusCodeException(uaStatus, "Failed to add OPC-UA for devices nodeset."); + LOGD << "OPC-UA for devices nodeSet was added successfully."; + +#ifdef NAMESPACE_DAQBT + uaStatus = namespace_daqbt_generated(server); + CheckStatusCodeException(uaStatus, "Failed to add TMS BT nodeset."); + LOGD << "TMS BT nodeset was added successfully."; +#endif + +#ifdef NAMESPACE_DAQBSP + uaStatus = namespace_daqbsp_generated(server); + CheckStatusCodeException(uaStatus, "Failed to add TMS BSP nodeset."); + LOGD << "TMS BSP nodeset was added successfully."; +#endif + +#ifdef NAMESPACE_DAQDEVICE + uaStatus = namespace_daqdevice_generated(server); + CheckStatusCodeException(uaStatus, "Failed to add TMS DEVICE nodeset."); + LOGD << "TMS DEVICE nodeset was added successfully."; +#endif + +#ifdef NAMESPACE_DAQESP + uaStatus = namespace_daqesp_generated(server); + CheckStatusCodeException(uaStatus, "Failed to add TMS ESP nodeset."); + LOGD << "TMS ESP nodeset was added successfully."; +#endif + + uaStatus = namespace_daqhbk_generated(server); + CheckStatusCodeException(uaStatus, "Failed to add TMS HBK nodeset."); + LOGD << "TMS HBK nodeset was added successfully."; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/src/server_event_manager.cpp b/shared/libraries/opcua/opcuaserver/src/server_event_manager.cpp new file mode 100644 index 0000000..0f32a63 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/src/server_event_manager.cpp @@ -0,0 +1,108 @@ +#include "opcuaserver/server_event_manager.h" +#include "opcuaserver/opcuaserver.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +ServerEventManager::ServerEventManager(const OpcUaServerPtr& server) + : server(server.get()) +{ +} + +ServerEventManager::ServerEventManager(OpcUaServer* server) + : server(server) +{ +} + +void ServerEventManager::registerEvents() +{ + auto config = UA_Server_getConfig(server->getUaServer()); + + config->nodeLifecycle.context = this; + config->displayNameChanged = DisplayNameChanged; + config->descriptionChanged = DescriptionChanged; + config->nodeLifecycle.createOptionalChild = CreateOptionalNode; +} + +UA_Boolean ServerEventManager::triggerCreateOptionalNode(const UA_NodeId* nodeId) +{ + const auto nodeIdObj = OpcUaNodeId(*nodeId); + if (createOptionalNodeCallback == nullptr) + return false; + + return createOptionalNodeCallback(nodeIdObj); +} + +void ServerEventManager::triggerDisplayNameChanged(const UA_NodeId* nodeId, UA_LocalizedText* name, void* context) +{ + const auto nodeIdObj = OpcUaNodeId(*nodeId); + if (displayNameCallbacks.count(nodeIdObj) == 0) + return; + + auto callback = displayNameCallbacks[nodeIdObj]; + callback(nodeIdObj, OpcUaObject(*name), context); +} + +void ServerEventManager::triggerDescriptionChanged(const UA_NodeId* nodeId, UA_LocalizedText* description, void* context) +{ + const auto nodeIdObj = OpcUaNodeId(*nodeId); + if (descriptionCallbacks.count(nodeIdObj) == 0) + return; + + auto callback = descriptionCallbacks[nodeIdObj]; + callback(nodeIdObj, OpcUaObject(*description), context); +} + +void ServerEventManager::onCreateOptionalNode(const CreatOptionalNodeCallback& callback) +{ + createOptionalNodeCallback = callback; +} + +void ServerEventManager::onDisplayNameChanged(const OpcUaNodeId& nodeId, const DisplayNameChangedCallback& callback) +{ + displayNameCallbacks.insert({nodeId, callback}); +} + +void ServerEventManager::onDescriptionChanged(const OpcUaNodeId& nodeId, const DescriptionChangedCallback& callback) +{ + descriptionCallbacks.insert({nodeId, callback}); +} + +void ServerEventManager::removeOnDisplayNameChanged(const OpcUaNodeId& nodeId) +{ + displayNameCallbacks.erase(nodeId); +} + +void ServerEventManager::removeOnDescriptionChanged(const OpcUaNodeId& nodeId) +{ + descriptionCallbacks.erase(nodeId); +} + +// Static callbacks + +UA_Boolean ServerEventManager::CreateOptionalNode(UA_Server* server, + const UA_NodeId* sessionId, + void* sessionContext, + const UA_NodeId* sourceNodeId, + const UA_NodeId* targetParentNodeId, + const UA_NodeId* referenceTypeId) +{ + auto& lifecycle = UA_Server_getConfig(server)->nodeLifecycle; + auto eventManager = (ServerEventManager*) lifecycle.context; + return eventManager->triggerCreateOptionalNode(sourceNodeId); +} + +void ServerEventManager::DisplayNameChanged(UA_Server* server, UA_NodeId* nodeId, UA_LocalizedText* newDisplayName, void* context) +{ + auto& lifecycle = UA_Server_getConfig(server)->nodeLifecycle; + auto eventManager = (ServerEventManager*) lifecycle.context; + eventManager->triggerDisplayNameChanged(nodeId, newDisplayName, context); +} + +void ServerEventManager::DescriptionChanged(UA_Server* server, UA_NodeId* nodeId, UA_LocalizedText* newDescription, void* context) +{ + auto& lifecycle = UA_Server_getConfig(server)->nodeLifecycle; + auto eventManager = (ServerEventManager*) lifecycle.context; + eventManager->triggerDescriptionChanged(nodeId, newDescription, context); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/tests/CMakeLists.txt b/shared/libraries/opcua/opcuaserver/tests/CMakeLists.txt new file mode 100644 index 0000000..2c4d336 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/CMakeLists.txt @@ -0,0 +1,29 @@ +set(MODULE_NAME opcuaserver) +set(TEST_APP test_${MODULE_NAME}) + +set(SRC_Cpp main.cpp + common_test_functions.h + test_opcuaserver.cpp + test_opcuaservernode.cpp + test_opcuaservernodefactory.cpp + test_opcuaserverlock.cpp + test_opcuasession.cpp +) + +add_executable(${TEST_APP} ${SRC_Cpp}) + +target_link_libraries(${TEST_APP} PRIVATE ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::opendaq_utils + daq::test_utils + daq::opcuaclient + daq::opcua_daq_types +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if(OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}coverage ${TEST_APP} ${MODULE_NAME}coverage) +endif() diff --git a/shared/libraries/opcua/opcuaserver/tests/common_test_functions.h b/shared/libraries/opcua/opcuaserver/tests/common_test_functions.h new file mode 100644 index 0000000..ec32e07 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/common_test_functions.h @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +static constexpr const char SERVER_URL[] = "opc.tcp://localhost"; + +inline OpcUaServer createServer() +{ + return OpcUaServer(); +} + +inline UA_Client* CreateClient() +{ + UA_Client* client = UA_Client_new(); + UA_ClientConfig_setDefault(UA_Client_getConfig(client)); + return client; +} + +inline std::shared_ptr CreateClientAndConnect() +{ + auto client = std::make_shared(SERVER_URL); + client->connect(); + assert(client->isConnected()); + return client; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/tests/main.cpp b/shared/libraries/opcua/opcuaserver/tests/main.cpp new file mode 100644 index 0000000..abe4b12 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/main.cpp @@ -0,0 +1,14 @@ +#include +#include + +int main(int argc, char** args) +{ + ::testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new MemCheckListener()); + + int res = RUN_ALL_TESTS(); + + return res; +} diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp new file mode 100644 index 0000000..6f557ce --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserver.cpp @@ -0,0 +1,469 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common_test_functions.h" +#include +#include +#include + +#include +#ifdef NAMESPACE_TMSBT + #include +#endif +#ifdef NAMESPACE_TMSBSP + #include +#endif +#ifdef NAMESPACE_TMSDEVICE + #include +#endif + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using namespace utils; +using namespace std::chrono_literals; + +using OpcUaServerTest = testing::Test; + +#define ASSERT_EQ_STATUS(status, expectedStatus) ASSERT_EQ(status, (UA_StatusCode) expectedStatus) +#define ASSERT_STATUSCODE_GOOD(status) ASSERT_EQ_STATUS(status, UA_STATUSCODE_GOOD) + +TEST_F(OpcUaServerTest, StartStopTest) +{ + OpcUaServer server = createServer(); + ASSERT_FALSE(server.isPrepared()); + ASSERT_FALSE(server.getStarted()); + + server.prepare(); + ASSERT_TRUE(server.isPrepared()); + ASSERT_FALSE(server.getStarted()); + + server.start(); + ASSERT_TRUE(server.isPrepared()); + ASSERT_TRUE(server.getStarted()); + + server.stop(); + ASSERT_FALSE(server.isPrepared()); + ASSERT_FALSE(server.getStarted()); +} + +TEST_F(OpcUaServerTest, StopWithoutStartTest) +{ + OpcUaServer server = createServer(); + + server.prepare(); + + server.stop(); + ASSERT_FALSE(server.isPrepared()); + ASSERT_FALSE(server.getStarted()); +} + +TEST_F(OpcUaServerTest, NodeExists) +{ + OpcUaServer server = createServer(); + server.prepare(); + + ASSERT_TRUE(server.nodeExists(OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER))); + + ASSERT_FALSE(server.nodeExists(OpcUaNodeId(1, "unknown"))); +} + +TEST_F(OpcUaServerTest, ReferenceExists) +{ + OpcUaServer server = createServer(); + server.prepare(); + + ASSERT_TRUE(server.referenceExists( + OpcUaNodeId(UA_NS0ID_ROOTFOLDER), OpcUaNodeId(UA_NS0ID_ORGANIZES), OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), true)); + ASSERT_TRUE(server.referenceExists( + OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), OpcUaNodeId(UA_NS0ID_ORGANIZES), OpcUaNodeId(UA_NS0ID_ROOTFOLDER), false)); + + ASSERT_FALSE(server.referenceExists( + OpcUaNodeId(UA_NS0ID_ROOTFOLDER), OpcUaNodeId(UA_NS0ID_ORGANIZES), OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER), false)); + ASSERT_FALSE(server.referenceExists( + OpcUaNodeId(UA_NS0ID_ROOTFOLDER), OpcUaNodeId(UA_NS0ID_ORGANIZES), OpcUaNodeId(UA_NS0ID_DATATYPESFOLDER), true)); +} + +TEST_F_OPTIONAL(OpcUaServerTest, ClientConnectTest) +{ + OpcUaServer server = createServer(); + server.start(); + + UA_Client* client = CreateClient(); + ASSERT_EQ_STATUS(UA_Client_connect(client, SERVER_URL), UA_STATUSCODE_GOOD); + UA_Client_delete(client); + + server.stop(); +} + +TEST_F_OPTIONAL(OpcUaServerTest, ClientConnectTestStopBeforeClientDisconnect) +{ + OpcUaServer server = createServer(); + server.start(); + + UA_Client* client = CreateClient(); + ASSERT_EQ_STATUS(UA_Client_connect(client, SERVER_URL), UA_STATUSCODE_GOOD); + + server.stop(); + + UA_Client_delete(client); +} + +TEST_F_OPTIONAL(OpcUaServerTest, ClientConnectTestSessionContext) +{ + OpcUaServer server = createServer(); + server.createSessionContextCallback = [](const OpcUaNodeId& sessionId) { return new int; }; + server.deleteSessionContextCallback = [](void* pointer) { delete (int*) pointer; }; + server.start(); + + UA_Client* client = CreateClient(); + ASSERT_EQ_STATUS(UA_Client_connect(client, SERVER_URL), UA_STATUSCODE_GOOD); + UA_Client_delete(client); + + server.stop(); +} + +TEST_F_OPTIONAL(OpcUaServerTest, ClientConnectTestSessionContextStopBeforeClientDisconnect) +{ + OpcUaServer server = createServer(); + server.createSessionContextCallback = [](const OpcUaNodeId& sessionId) { return new int; }; + server.deleteSessionContextCallback = [](void* pointer) { delete (int*) pointer; }; + server.start(); + + UA_Client* client = CreateClient(); + ASSERT_EQ_STATUS(UA_Client_connect(client, SERVER_URL), UA_STATUSCODE_GOOD); + + server.stop(); + + UA_Client_delete(client); +} + +TEST_F(OpcUaServerTest, ClientConnectDisconnectCallbacks) +{ + auto server = OpcUaServer(); + server.setPort(4840); + + std::string clientId; + bool clientConnected{false}; + bool clientDisconnected{false}; + server.setClientConnectedHandler( + [&clientId, &clientConnected](const std::string& id) + { + clientConnected = true; + clientId = id; + } + ); + server.setClientDisconnectedHandler( + [&clientId, &clientConnected, &clientDisconnected](const std::string& id) + { + if (clientConnected && id == clientId) + clientDisconnected = true; + } + ); + + server.start(); + + UA_Client* client = CreateClient(); + ASSERT_EQ_STATUS(UA_Client_connect(client, SERVER_URL), UA_STATUSCODE_GOOD); + + ASSERT_TRUE(clientConnected); + ASSERT_NE(clientId, ""); + + UA_Client_delete(client); + ASSERT_TRUE(clientDisconnected); + + server.stop(); +} + +TEST_F(OpcUaServerTest, ReadBrowseName) +{ + OpcUaServer server = createServer(); + server.prepare(); + + auto qualifiedName = server.readBrowseName(OpcUaNodeId(UA_NS0ID_SERVER)); + ASSERT_EQ(qualifiedName->namespaceIndex, 0); + ASSERT_EQ(ToStdString(qualifiedName->name), "Server"); + + ASSERT_THROW(server.readBrowseName(OpcUaNodeId(0, "Unknown")), OpcUaException); +} + +TEST_F(OpcUaServerTest, ReadDisplayName) +{ + OpcUaServer server = createServer(); + server.prepare(); + + ASSERT_EQ(ToStdString(server.readDisplayName(OpcUaNodeId(UA_NS0ID_SERVER))->text), "Server"); + + ASSERT_THROW(server.readDisplayName(OpcUaNodeId(0, "Unknown")), OpcUaException); +} + +TEST_F(OpcUaServerTest, AddFolderTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId nodeId(1, "FolderId"); + + AddObjectNodeParams params(nodeId, OpcUaNodeId(UA_NS0ID_SERVER)); + params.setBrowseName("Folder"); + + OpcUaNodeId outNodeId = server.addObjectNode(params); + ASSERT_EQ(OpcUaNodeId(1, "FolderId"), outNodeId); + + ASSERT_TRUE(server.nodeExists(nodeId)); + + OpcUaObject qualifiedName = server.readBrowseName(nodeId); + ASSERT_EQ(qualifiedName->namespaceIndex, 0); + ASSERT_EQ(ToStdString(qualifiedName->name), "Folder"); + + ASSERT_EQ(ToStdString(server.readDisplayName(nodeId)->text), "Folder"); + + ASSERT_THROW(server.addObjectNode(params), OpcUaException); // Duplicate +} + +TEST_F(OpcUaServerTest, AddVariableTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId nodeId(1, "VariableId"); + + AddVariableNodeParams params(nodeId, OpcUaNodeId(UA_NS0ID_SERVER)); + params.setBrowseName("Variable"); + + OpcUaNodeId outNodeId = server.addVariableNode(params); + ASSERT_EQ(OpcUaNodeId(1, "VariableId"), outNodeId); + + ASSERT_TRUE(server.nodeExists(nodeId)); + + OpcUaObject qualifiedName = server.readBrowseName(nodeId); + ASSERT_EQ(qualifiedName->namespaceIndex, 0); + ASSERT_EQ(ToStdString(qualifiedName->name), "Variable"); + + ASSERT_EQ(ToStdString(server.readDisplayName(nodeId)->text), "Variable"); + + ASSERT_THROW(server.addVariableNode(params), OpcUaException); // Duplicate +} + +TEST_F(OpcUaServerTest, AddMethodTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId nodeId(1, "MethodId"); + + AddMethodNodeParams params(nodeId, OpcUaNodeId(UA_NS0ID_SERVER)); + params.setBrowseName("Method"); + + OpcUaNodeId outNodeId = server.addMethodNode(params); + ASSERT_EQ(OpcUaNodeId(1, "MethodId"), outNodeId); + + ASSERT_TRUE(server.nodeExists(nodeId)); + + OpcUaObject qualifiedName = server.readBrowseName(nodeId); + ASSERT_EQ(qualifiedName->namespaceIndex, 0); + ASSERT_EQ(ToStdString(qualifiedName->name), "Method"); + + ASSERT_EQ(ToStdString(server.readDisplayName(nodeId)->text), "Method"); + + ASSERT_THROW(server.addMethodNode(params), OpcUaException); // Duplicate +} + +TEST_F(OpcUaServerTest, AddObjectType) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId nodeId(1, "ObjectTypeId"); + + AddObjectTypeNodeParams params(nodeId, OpcUaNodeId(UA_NS0ID_BASEOBJECTTYPE)); + params.setBrowseName("ObjectType"); + + OpcUaNodeId outNodeId = server.addObjectTypeNode(params); + ASSERT_EQ(OpcUaNodeId(1, "ObjectTypeId"), outNodeId); + + ASSERT_TRUE(server.nodeExists(nodeId)); + + OpcUaObject qualifiedName = server.readBrowseName(nodeId); + ASSERT_EQ(qualifiedName->namespaceIndex, 0); + ASSERT_EQ(ToStdString(qualifiedName->name), "ObjectType"); + + ASSERT_EQ(ToStdString(server.readDisplayName(nodeId)->text), "ObjectType"); + + ASSERT_THROW(server.addObjectTypeNode(params), OpcUaException); // Duplicate +} + +TEST_F(OpcUaServerTest, AddVariableType) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId nodeId(1, "VariableTypeId"); + + AddVariableTypeNodeParams params(nodeId, OpcUaNodeId(UA_NS0ID_BASEVARIABLETYPE)); + params.setBrowseName("VariableType"); + + OpcUaNodeId outNodeId = server.addVariableTypeNode(params); + ASSERT_EQ(OpcUaNodeId(1, "VariableTypeId"), outNodeId); + + ASSERT_TRUE(server.nodeExists(nodeId)); + + OpcUaObject qualifiedName = server.readBrowseName(nodeId); + ASSERT_EQ(qualifiedName->namespaceIndex, 0); + ASSERT_EQ(ToStdString(qualifiedName->name), "VariableType"); + + ASSERT_EQ(ToStdString(server.readDisplayName(nodeId)->text), "VariableType"); + + ASSERT_THROW(server.addVariableTypeNode(params), OpcUaException); // Duplicate +} + +TEST_F(OpcUaServerTest, AddDeleteReference) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId sourceId(0, UA_NS0ID_OBJECTSFOLDER); + OpcUaNodeId refTypeId(0, UA_NS0ID_HASCOMPONENT); + OpcUaNodeId targetId(0, UA_NS0ID_SERVER_VENDORSERVERINFO); + + ASSERT_FALSE(server.referenceExists(sourceId, refTypeId, targetId, true)); + + server.addReference(sourceId, refTypeId, targetId, true); + + ASSERT_TRUE(server.referenceExists(sourceId, refTypeId, targetId, true)); + + server.deleteReference(sourceId, refTypeId, targetId, true); + + ASSERT_FALSE(server.referenceExists(sourceId, refTypeId, targetId, true)); +} + +TEST_F(OpcUaServerTest, DINodeSetTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId uaNodeId(2, UA_DIID_TRANSFERRESULTDATADATATYPE); + + OpcUaObject qualifiedName; + ASSERT_NO_THROW(qualifiedName = server.readBrowseName(uaNodeId)); + ASSERT_EQ(ToStdString(qualifiedName->name), "TransferResultDataDataType"); +} +#ifdef NAMESPACE_TMSBT +TEST_F(OpcUaServerTest, TmsBtNodeSetTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId uaNodeId(3, UA_TMSBTID_DAQBASEOBJECTTYPE); + + OpcUaObject qualifiedName; + ASSERT_NO_THROW(qualifiedName = server.readBrowseName(uaNodeId)); + ASSERT_EQ(ToStdString(qualifiedName->name), "DaqBaseObjectType"); +} +#endif + +#ifdef NAMESPACE_TMSBSP +TEST_F(OpcUaServerTest, TmsBspNodeSetTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId uaNodeId(4, UA_TMSBSPID_SIGNALTYPE); + + OpcUaObject qualifiedName; + ASSERT_NO_THROW(qualifiedName = server.readBrowseName(uaNodeId)); + ASSERT_EQ(ToStdString(qualifiedName->name), "SignalType"); +} +#endif + +#ifdef NAMESPACE_TMSDEVICE +TEST_F(OpcUaServerTest, TmsDeviceNodeSetTest) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaNodeId uaNodeId(5, UA_TMSDEVICEID_DAQDEVICETYPE); + + OpcUaObject qualifiedName; + ASSERT_NO_THROW(qualifiedName = server.readBrowseName(uaNodeId)); + ASSERT_EQ(ToStdString(qualifiedName->name), "DaqDeviceType"); +} +#endif + +TEST_F(OpcUaServerTest, GetServerNodes) +{ + OpcUaServer server = createServer(); + server.prepare(); + + ASSERT_NO_THROW(server.getNode(OpcUaNodeId(UA_NS0ID_SERVER))); + ASSERT_NO_THROW(server.getRootNode()); + ASSERT_NO_THROW(server.getObjectsNode()); + ASSERT_NO_THROW(server.getTypesNode()); + ASSERT_NO_THROW(server.getViewsNode()); + ASSERT_NO_THROW(server.getObjectTypesNode()); + ASSERT_NO_THROW(server.getVariableTypesNode()); + ASSERT_NO_THROW(server.getDataTypesNode()); + ASSERT_NO_THROW(server.getReferenceTypesNode()); +} + +const size_t nSelectClauses = 1; + +static UA_SimpleAttributeOperand* setupSelectClauses(void) +{ + UA_SimpleAttributeOperand* selectClauses = + (UA_SimpleAttributeOperand*) UA_Array_new(nSelectClauses, &UA_TYPES[UA_TYPES_SIMPLEATTRIBUTEOPERAND]); + + selectClauses[0] = SimpleAttributeOperand::CreateMessageValue().getDetachedValue(); + + return selectClauses; +} + +TEST_F(OpcUaServerTest, TriggerEvent) +{ + OpcUaServer server = createServer(); + server.start(); + + auto client = CreateClientAndConnect(); + client->runIterate(); + + OpcUaObject request = UA_CreateSubscriptionRequest_default(); + + Subscription* subscription = client->createSubscription(request); + + /* Add a MonitoredItem */ + EventMonitoredItemCreateRequest item(UA_NODEID_NUMERIC(0, 2253)); + UA_EventFilter* filter = UA_EventFilter_new(); + UA_EventFilter_init(filter); + filter->selectClauses = setupSelectClauses(); + filter->selectClausesSize = nSelectClauses; + + item.setEventFilter(filter); + + std::promise promise; + subscription->monitoredItemsCreateEvent(UA_TIMESTAMPSTORETURN_BOTH, + *item, + [&promise](OpcUaClient* client, + Subscription* subContext, + MonitoredItem* monContext, + size_t nEventFields, + UA_Variant* eventFields) { + OpcUaVariant val(eventFields[0]); + UA_LocalizedText localizedText = val.readScalar(); + promise.set_value(ToStdString(localizedText.text)); + }); + + EventAttributes eventAttributes; + eventAttributes.setMessage("", "TestMessage"); + ASSERT_NO_THROW(server.triggerEvent(OpcUaNodeId(UA_NS0ID_BASEEVENTTYPE), OpcUaNodeId(UA_NS0ID_SERVER), eventAttributes)); + + auto future = promise.get_future(); + ASSERT_NE(future.wait_for(2s), std::future_status::timeout); + ASSERT_EQ(future.get(), "TestMessage"); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuaserverlock.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserverlock.cpp new file mode 100644 index 0000000..3418fec --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuaserverlock.cpp @@ -0,0 +1,332 @@ +#include "gtest/gtest.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaServerLockTest = testing::Test; + +TEST_F(OpcUaServerLockTest, Create) +{ + OpcUaServerLock serverLock; +} + +TEST_F(OpcUaServerLockTest, PasswordLock) +{ + OpcUaServerLock serverLock; + ASSERT_TRUE(serverLock.passwordLock("test")); +} + +TEST_F(OpcUaServerLockTest, PasswordLockTwoTimes) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + ASSERT_FALSE(serverLock.passwordLock("test1")); +} + +TEST_F(OpcUaServerLockTest, PasswordUnlock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + ASSERT_TRUE(serverLock.passwordUnlock("test")); +} + +TEST_F(OpcUaServerLockTest, PasswordUnlockWrongPassword) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + ASSERT_FALSE(serverLock.passwordUnlock("wrong password")); +} + +TEST_F(OpcUaServerLockTest, PasswordUnlockWithoutLock) +{ + OpcUaServerLock serverLock; + ASSERT_TRUE(serverLock.passwordUnlock("test3")); +} + +TEST_F(OpcUaServerLockTest, IsPasswordLocked) +{ + OpcUaServerLock serverLock; + ASSERT_FALSE(serverLock.isPasswordLocked()); +} + +TEST_F(OpcUaServerLockTest, IsPasswordLockedAfterLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + ASSERT_TRUE(serverLock.isPasswordLocked()); +} + +TEST_F(OpcUaServerLockTest, IsPasswordLockedAfterUnlock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + serverLock.passwordUnlock("test"); + ASSERT_FALSE(serverLock.isPasswordLocked()); +} + +TEST_F(OpcUaServerLockTest, IsPasswordLockedAfterWrongPasswordUnlock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + serverLock.passwordUnlock("wrong password"); + ASSERT_TRUE(serverLock.isPasswordLocked()); +} + +TEST_F(OpcUaServerLockTest, LockConfigurationControl) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + ASSERT_TRUE(serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10))); +} + +TEST_F(OpcUaServerLockTest, LockConfigurationControlExtendLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + ASSERT_TRUE(serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10))); +} + +TEST_F(OpcUaServerLockTest, LockConfigurationControlRejectAccess) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_FALSE(serverLock.lockConfigurationControl(sessionId2, std::chrono::seconds(10))); +} + +TEST_F(OpcUaServerLockTest, LockConfigurationControlNewSessionAfterTimeout) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(0)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_TRUE(serverLock.lockConfigurationControl(sessionId2, std::chrono::seconds(10))); +} + +TEST_F(OpcUaServerLockTest, LockConfigurationControlAfterPasswordLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + + OpcUaNodeId sessionId(1, 1); + ASSERT_TRUE(serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10))); +} + +TEST_F(OpcUaServerLockTest, RefuseConfigurationControlLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + + ASSERT_NO_THROW(serverLock.refuseConfigurationControlLock(sessionId)); +} + +TEST_F(OpcUaServerLockTest, SessionPasswordLock) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId(1, 1); + ASSERT_TRUE(serverLock.passwordLock("test", sessionId)); +} + +TEST_F(OpcUaServerLockTest, SessionPasswordLockConfigurationLock) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + ASSERT_TRUE(serverLock.passwordLock("test", sessionId1)); +} + +TEST_F(OpcUaServerLockTest, SessionPasswordLockConfigurationLockByAnotherSession) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_FALSE(serverLock.passwordLock("test", sessionId2)); +} + +TEST_F(OpcUaServerLockTest, SessionPasswordUnlock) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId(1, 1); + serverLock.passwordLock("test", sessionId); + ASSERT_TRUE(serverLock.passwordUnlock("test", sessionId)); +} + +TEST_F(OpcUaServerLockTest, SessionPasswordUnlockConfigurationLock) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + + serverLock.passwordLock("test", sessionId); + ASSERT_TRUE(serverLock.passwordUnlock("test", sessionId)); +} + +TEST_F(OpcUaServerLockTest, SessionPasswordUnlockConfigurationLockByAnotherSession) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + serverLock.passwordLock("test", sessionId1); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_FALSE(serverLock.passwordUnlock("test", sessionId2)); +} + +TEST_F(OpcUaServerLockTest, RefuseConfigurationControlLockWithoutLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + + OpcUaNodeId sessionId(1, 1); + ASSERT_NO_THROW(serverLock.refuseConfigurationControlLock(sessionId)); +} + +TEST_F(OpcUaServerLockTest, RefuseConfigurationControlLockLockedByAnotherSession) +{ + OpcUaServerLock serverLock; + + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_NO_THROW(serverLock.refuseConfigurationControlLock(sessionId2)); +} + +TEST_F(OpcUaServerLockTest, HasConfigurationControlLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + ASSERT_FALSE(serverLock.hasConfigurationControlLock(sessionId)); +} + +TEST_F(OpcUaServerLockTest, HasConfigurationControlLockAfterLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + ASSERT_TRUE(serverLock.hasConfigurationControlLock(sessionId)); +} + +TEST_F(OpcUaServerLockTest, HasConfigurationControlLockAfterLockTimeout) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(0)); //expires immediately + ASSERT_FALSE(serverLock.hasConfigurationControlLock(sessionId)); +} + +TEST_F(OpcUaServerLockTest, HasConfigurationControlLockAfterAnotherSessionLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_FALSE(serverLock.hasConfigurationControlLock(sessionId2)); +} + +TEST_F(OpcUaServerLockTest, HasActiveConfigurationControlLock) +{ + OpcUaServerLock serverLock; + ASSERT_FALSE(serverLock.hasActiveConfigurationControlLock()); +} + +TEST_F(OpcUaServerLockTest, HasActiveConfigurationControlLockAfterLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + + ASSERT_TRUE(serverLock.hasActiveConfigurationControlLock()); +} + +TEST_F(OpcUaServerLockTest, HasActiveConfigurationControlLockAfterTimeout) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(0)); + + ASSERT_FALSE(serverLock.hasActiveConfigurationControlLock()); +} + +TEST_F(OpcUaServerLockTest, HasActiveConfigurationControlLockAfterRefuseLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + serverLock.refuseConfigurationControlLock(sessionId); + + ASSERT_FALSE(serverLock.hasActiveConfigurationControlLock()); +} + +TEST_F(OpcUaServerLockTest, CanControlAcq) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + ASSERT_TRUE(serverLock.canControlAcq(sessionId)); +} + +TEST_F(OpcUaServerLockTest, CanControlAcqPasswordLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("Test"); + OpcUaNodeId sessionId(1, 1); + ASSERT_FALSE(serverLock.canControlAcq(sessionId)); +} + +TEST_F(OpcUaServerLockTest, CanControlAcqPasswordUnlock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("Test"); + serverLock.passwordUnlock("Test"); + OpcUaNodeId sessionId(1, 1); + ASSERT_TRUE(serverLock.canControlAcq(sessionId)); +} + +TEST_F(OpcUaServerLockTest, CanControlAcqConfigurationControlLock) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId(1, 1); + + serverLock.lockConfigurationControl(sessionId, std::chrono::seconds(10)); + ASSERT_TRUE(serverLock.canControlAcq(sessionId)); +} + +TEST_F(OpcUaServerLockTest, CanControlAcqConfigurationControlLockByOtherSession) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(10)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_FALSE(serverLock.canControlAcq(sessionId2)); +} + +TEST_F(OpcUaServerLockTest, CanControlAcqConfigurationControlLockByOtherSessionAfterTimeout) +{ + OpcUaServerLock serverLock; + OpcUaNodeId sessionId1(1, 1); + serverLock.lockConfigurationControl(sessionId1, std::chrono::seconds(0)); + + OpcUaNodeId sessionId2(1, 2); + ASSERT_TRUE(serverLock.canControlAcq(sessionId2)); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuaservernode.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuaservernode.cpp new file mode 100644 index 0000000..263a7fb --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuaservernode.cpp @@ -0,0 +1,128 @@ +#include +#include +#include +#include "common_test_functions.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using namespace utils; + +using OpcUaServerNodeTest = testing::Test; + +TEST_F(OpcUaServerNodeTest, CreateServerNode) +{ + OpcUaServer server = createServer(); + server.prepare(); + + ASSERT_NO_THROW(OpcUaServerNode(server, OpcUaNodeId(UA_NS0ID_SERVER))); +} + +TEST_F(OpcUaServerNodeTest, AddObject) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNode serverNode(server, OpcUaNodeId(UA_NS0ID_SERVER)); + ASSERT_NO_THROW(serverNode.addObject(OpcUaNodeId(1, "TestNode"), "TestBrowseName")); +} + +TEST_F(OpcUaServerNodeTest, GetNodeClass) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNode serverNode(server, OpcUaNodeId(UA_NS0ID_SERVER)); + ASSERT_EQ(serverNode.getNodeClass(), OpcUaNodeClass::Object); +} + +TEST_F(OpcUaServerNodeTest, GetBrowseName) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNode serverNode(server, OpcUaNodeId(UA_NS0ID_SERVER)); + auto browseName = serverNode.getBrowseName(); + ASSERT_EQ(ToStdString(browseName->name), "Server"); + ASSERT_EQ(browseName->namespaceIndex, 0); +} + +TEST_F(OpcUaServerNodeTest, GetDisplayName) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNode serverNode(server, OpcUaNodeId(UA_NS0ID_SERVER)); + auto displayName = serverNode.getDisplayName(); + + ASSERT_EQ(ToStdString(displayName->text), "Server"); +} + +TEST_F(OpcUaServerNodeTest, SetDisplayName) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNode serverNode(server, OpcUaNodeId(UA_NS0ID_SERVER)); + + ASSERT_NO_THROW(serverNode.setDisplayName("test")); + + auto displayName = serverNode.getDisplayName(); + ASSERT_EQ(ToStdString(displayName->text), "test"); +} + +TEST_F(OpcUaServerNodeTest, AddVariable) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNode serverNode(server, OpcUaNodeId(UA_NS0ID_SERVER)); + ASSERT_NO_THROW(serverNode.addVariable(OpcUaNodeId(1, "TestNode"), "TestBrowseName")); +} + +TEST_F(OpcUaServerNodeTest, ReadValue) +{ + OpcUaServer server = createServer(); + server.prepare(); + + AddVariableNodeParams params(OpcUaNodeId(1, "TestNode")); + params.setBrowseName("TestBrowseName"); + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_INT64].typeId)); + int64_t value = 5; + UA_Variant_setScalarCopy(¶ms.attr->value, &value, &UA_TYPES[UA_TYPES_INT64]); + + auto variableNode = server.getObjectsNode().addVariable(params); + ASSERT_EQ(variableNode.read(), 5); + ASSERT_THROW(variableNode.read(), std::runtime_error); +} + +TEST_F(OpcUaServerNodeTest, SetValue) +{ + OpcUaServer server = createServer(); + server.prepare(); + + AddVariableNodeParams params(OpcUaNodeId(1, "TestNode")); + params.setBrowseName("TestBrowseName"); + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_INT64].typeId)); + + auto variableNode = server.getObjectsNode().addVariable(params); + ASSERT_NO_THROW(variableNode.write(5)); + ASSERT_EQ(variableNode.read(), 5); +} + +TEST_F(OpcUaServerNodeTest, BrowseChildNodes) +{ + OpcUaServer server = createServer(); + server.prepare(); + + auto parentNode = server.getObjectsNode().addObject(OpcUaNodeId(0, "ParentNode"), "ParentNode"); + auto childNode = parentNode.addObject(OpcUaNodeId(0, "ChildNode"), "ChildNode"); + + auto result = parentNode.browseChildNodes(); + ASSERT_EQ(result.size(), 1u); + const auto& firstNode = result[0]; + ASSERT_NE(dynamic_cast(firstNode.get()), nullptr); + ASSERT_EQ(firstNode->getNodeId(), OpcUaNodeId(0, "ChildNode")); + ASSERT_EQ(ToStdString(firstNode->getBrowseName()->name), "ChildNode"); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuaservernodefactory.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuaservernodefactory.cpp new file mode 100644 index 0000000..5c84ca9 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuaservernodefactory.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include "common_test_functions.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaServerNodeFactoryTest = testing::Test; + +TEST_F(OpcUaServerNodeFactoryTest, CreateServerNodeFactory) +{ + OpcUaServer server = createServer(); + server.prepare(); + + ASSERT_NO_THROW(OpcUaServerNodeFactory nodeFactory(server)); +} + +TEST_F(OpcUaServerNodeFactoryTest, CreateObject) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNodeFactory nodeFactory(server); + + std::unique_ptr newNode; + ASSERT_NO_THROW(newNode = nodeFactory.createServerNode(OpcUaNodeId(UA_NS0ID_SERVER), OpcUaNodeClass::Object)); + + ASSERT_NE(dynamic_cast(newNode.get()), nullptr); +} + +TEST_F(OpcUaServerNodeFactoryTest, CreateVariable) +{ + OpcUaServer server = createServer(); + server.prepare(); + + OpcUaServerNodeFactory nodeFactory(server); + + std::unique_ptr newNode; + ASSERT_NO_THROW(newNode = nodeFactory.createServerNode(OpcUaNodeId(UA_NS0ID_SERVER_SERVERCAPABILITIES), OpcUaNodeClass::Variable)); + + ASSERT_NE(dynamic_cast(newNode.get()), nullptr); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuaserver/tests/test_opcuasession.cpp b/shared/libraries/opcua/opcuaserver/tests/test_opcuasession.cpp new file mode 100644 index 0000000..6d34565 --- /dev/null +++ b/shared/libraries/opcua/opcuaserver/tests/test_opcuasession.cpp @@ -0,0 +1,238 @@ +#include "gtest/gtest.h" +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + + +using OpcUaSessionTest = testing::Test; + +TEST_F(OpcUaSessionTest, Create) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); +} + +TEST_F(OpcUaSessionTest, LockConfigurationControl) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_TRUE(session.lockConfigurationControl(std::chrono::seconds(10))); +} + +TEST_F(OpcUaSessionTest, LockConfigurationControlExtendLock) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.lockConfigurationControl(std::chrono::seconds(10)); + ASSERT_TRUE(session.lockConfigurationControl(std::chrono::seconds(10))); +} + +TEST_F(OpcUaSessionTest, LockConfigurationControlRejectAccess) +{ + OpcUaServerLock serverLock; + + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(10)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_FALSE(session2.lockConfigurationControl(std::chrono::seconds(10))); +} + +TEST_F(OpcUaSessionTest, LockConfigurationControlNewSessionAfterTimeout) +{ + OpcUaServerLock serverLock; + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(0)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_TRUE(session2.lockConfigurationControl(std::chrono::seconds(10))); +} + +TEST_F(OpcUaSessionTest, LockConfigurationControlAfterPasswordLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_TRUE(session.lockConfigurationControl(std::chrono::seconds(10))); +} + +TEST_F(OpcUaSessionTest, RefuseConfigurationControlLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.lockConfigurationControl(std::chrono::seconds(10)); + + ASSERT_NO_THROW(session.refuseConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, PasswordLock) +{ + OpcUaServerLock serverLock; + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_TRUE(session.passwordLock("test")); +} + +TEST_F(OpcUaSessionTest, PasswordLockConfigurationLock) +{ + OpcUaServerLock serverLock; + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.lockConfigurationControl(std::chrono::seconds(10)); + ASSERT_TRUE(session.passwordLock("test")); +} + +TEST_F(OpcUaSessionTest, PasswordLockConfigurationLockByAnotherSession) +{ + OpcUaServerLock serverLock; + + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(10)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_FALSE(session2.passwordLock("test")); +} + +TEST_F(OpcUaSessionTest, PasswordUnlock) +{ + OpcUaServerLock serverLock; + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.passwordLock("test"); + ASSERT_TRUE(session.passwordUnlock("test")); +} + +TEST_F(OpcUaSessionTest, PasswordUnlockConfigurationLock) +{ + OpcUaServerLock serverLock; + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.lockConfigurationControl(std::chrono::seconds(10)); + + session.passwordLock("test"); + ASSERT_TRUE(session.passwordUnlock("test")); +} + +TEST_F(OpcUaSessionTest, PasswordUnlockConfigurationLockByAnotherSession) +{ + OpcUaServerLock serverLock; + + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(10)); + session1.passwordLock("test"); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_FALSE(session2.passwordUnlock("test")); +} + +TEST_F(OpcUaSessionTest, RefuseConfigurationControlLockWithoutLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("test"); + + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_NO_THROW(session.refuseConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, RefuseConfigurationControlLockLockedByAnotherSession) +{ + OpcUaServerLock serverLock; + + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(10)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_NO_THROW(session2.refuseConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, HasConfigurationControlLock) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_FALSE(session.hasConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, HasConfigurationControlLockAfterLock) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.lockConfigurationControl(std::chrono::seconds(10)); + ASSERT_TRUE(session.hasConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, HasConfigurationControlLockAfterLockTimeout) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + session.lockConfigurationControl(std::chrono::seconds(0)); //expires immediately + ASSERT_FALSE(session.hasConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, HasConfigurationControlLockAfterAnotherSessionLock) +{ + OpcUaServerLock serverLock; + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(10)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_FALSE(session2.hasConfigurationControlLock()); +} + +TEST_F(OpcUaSessionTest, CanControlAcq) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_TRUE(session.canControlAcq()); +} + +TEST_F(OpcUaSessionTest, CanControlAcqPasswordLock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("Test"); + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_FALSE(session.canControlAcq()); +} + +TEST_F(OpcUaSessionTest, CanControlAcqPasswordUnlock) +{ + OpcUaServerLock serverLock; + serverLock.passwordLock("Test"); + serverLock.passwordUnlock("Test"); + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + ASSERT_TRUE(session.canControlAcq()); +} + +TEST_F(OpcUaSessionTest, CanControlAcqConfigurationControlLock) +{ + OpcUaServerLock serverLock; + OpcUaSession session(OpcUaNodeId(1, 1), &serverLock); + + session.lockConfigurationControl(std::chrono::seconds(10)); + ASSERT_TRUE(session.canControlAcq()); +} + +TEST_F(OpcUaSessionTest, CanControlAcqConfigurationControlLockByOtherSession) +{ + OpcUaServerLock serverLock; + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(10)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_FALSE(session2.canControlAcq()); +} + +TEST_F(OpcUaSessionTest, CanControlAcqConfigurationControlLockByOtherSessionAfterTimeout) +{ + OpcUaServerLock serverLock; + OpcUaSession session1(OpcUaNodeId(1, 1), &serverLock); + session1.lockConfigurationControl(std::chrono::seconds(0)); + + OpcUaSession session2(OpcUaNodeId(1, 2), &serverLock); + ASSERT_TRUE(session2.canControlAcq()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/CMakeLists.txt b/shared/libraries/opcua/opcuashared/CMakeLists.txt new file mode 100644 index 0000000..8f97f58 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME shared) +project(OpcUaShared CXX C) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/bcrypt.h b/shared/libraries/opcua/opcuashared/include/opcuashared/bcrypt.h new file mode 100644 index 0000000..04c721f --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/bcrypt.h @@ -0,0 +1,121 @@ +/* + * 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. + */ + +/** + * @file bcrypt.h + * @author Jan Mikolič + * @date 23/04/2021 + * @version 1.0 + * + * @brief Wrapper class for BCrypt password hashing algorithm. + * + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +/** + * Wrapper class for BCrypt password hashing algorithm. + */ +class BCrypt +{ +private: + static const int InfoPartSize = 7; + static const int SaltPartSize = 22; + static const int HashPartSize = 31; + static const int SaltSize = InfoPartSize + SaltPartSize; + static const int HashSize = InfoPartSize + SaltPartSize + HashPartSize; + + std::random_device randomDevice; + std::uniform_int_distribution randomDist; + + std::vector randBytes(unsigned int length); + +public: + BCrypt(); + + /** + * Compute BCrypt hash of a password using random salt. + * + * @param password The password in plain text to be hashed + * @param rounds Base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithm + * @returns Hash string of the provided password + * @throws {BCryptException} if blowfish algorithm failed to generate the hash + */ + std::string hash(const std::string& password, unsigned int rounds = 10); + + /** + * Compute BCrypt hash of a password using static salt. Try to avoid using this method, but if you must have a static salt, then + * generate it with generateSalt(int size) method. + * + * @param password The password in plain text to be hashed + * @param salt The salt to be used for hashing the password + * @param rounds Base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithm + * @returns Hash string of the provided password + * @throws {BCryptException} if blowfish algorithm failed to generate the hash + * @throws {BCryptSaltSizeException} if salt size is too small + */ + std::string hash(const std::string& password, std::string salt, unsigned int rounds = 10) const; + + /** + * Verifies if the password matches a hash. + * + * @param hash BCrypt hash of the password + * @param password The password in plain text + * @returns true if password matches the hash + */ + static bool Verify(const std::string& hash, const std::string& password); + + /** + * Generate a random salt string of the specified size. + * + * @param size The size of the salt to be generated + * @returns Generated salt string + * @throws {BCryptException} if blowfish algorithm failed to generate the salt + * @throws {BCryptSaltSizeException} if size is too small + */ + std::string generateSalt(unsigned int size); + + /** + * Generate a salt string from provided byte array. + * + * @param bytes Byte array from which to generate the salt + * @param size The size of the provided byte arry + * @returns Generated salt string + * @throws {BCryptException} if blowfish algorithm failed to generate the salt + * @throws {BCryptSaltSizeException} if size is too small + */ + std::string generateSalt(char* bytes, int size) const; +}; + +class BCryptException : public std::runtime_error +{ +public: + explicit BCryptException(const std::string& message); +}; + +class BCryptSaltSizeException : public BCryptException +{ +public: + explicit BCryptSaltSizeException(unsigned int minSaltSize); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/bcrypt/crypt_blowfish.h b/shared/libraries/opcua/opcuashared/include/opcuashared/bcrypt/crypt_blowfish.h new file mode 100644 index 0000000..ec6d56d --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/bcrypt/crypt_blowfish.h @@ -0,0 +1,24 @@ +/* + * Written by Solar Designer in 2000-2011. + * No copyright is claimed, and the software is hereby placed in the public + * domain. In case this attempt to disclaim copyright and place the software + * in the public domain is deemed null and void, then the software is + * Copyright (c) 2000-2011 Solar Designer and it is hereby released to the + * general public under the following terms: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + * + * See crypt_blowfish.c for more information. + */ + +#ifndef _CRYPT_BLOWFISH_H +#define _CRYPT_BLOWFISH_H + +extern int _crypt_output_magic(const char *setting, char *output, int size); +extern char* _crypt_blowfish_rn(const char *key, const char *setting, char *output, int size); +extern char* _crypt_gensalt_blowfish_rn(const char *prefix, unsigned long count, const char *input, int size, char *output, int output_size); + +#endif diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuadatatype.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuadatatype.h new file mode 100644 index 0000000..4d90d85 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuadatatype.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaDataType; +using OpcUaDataTypePtr = std::shared_ptr; + +class OpcUaDataType : public OpcUaNode +{ +public: + explicit OpcUaDataType(const UA_ReferenceDescription& uaNodeDescription); + ~OpcUaDataType(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanode.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanode.h new file mode 100644 index 0000000..b5ac36b --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanode.h @@ -0,0 +1,69 @@ +/* + * 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. + */ + +/* + * Abstract representation of OPC UA node (can be object, variable, method ietc.) + */ + +#pragma once + +#include "opcuashared/opcua.h" +#include "opcuashared/opcuanodeid.h" +#include "opcuashared/opcuacommon.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaType; +using OpcUaTypePtr = std::shared_ptr; + +class OpcUaNode; +using OpcUaNodePtr = std::shared_ptr; + +class OpcUaNode +{ +public: + OpcUaNode(const OpcUaNodeId& uaNode, OpcUaNodeClass nodeClass); + OpcUaNode(const UA_ReferenceDescription& uaNodeDescription, OpcUaNodeClass nodeClass); + virtual ~OpcUaNode(); + + // NodeID + const OpcUaNodeId& getNodeId() const; + + // Attributes + const OpcUaNodeClass& getNodeClass() const; + void setNodeClass(const UA_NodeClass& nodeClass); + void setNodeClass(OpcUaNodeClass nodeClass); + const std::string& getBrowseName() const; + void setBrowseName(const std::string& browseName); + const std::string& getDisplayName() const; + void setDisplayName(const std::string& displayName); + + static std::string GetBrowseName(const UA_QualifiedName& browseName); + + void setType(const OpcUaTypePtr& type); + void setType(const OpcUaNodeId& objectTypeId); + const OpcUaNodeId& getTypeId() const; + +protected: + OpcUaNodeId nodeId; + std::string browseName; + std::string displayName; + OpcUaNodeClass nodeClass; + OpcUaNodeId typeId; // hasTypeDefinition (object and variable class) +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodemethod.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodemethod.h new file mode 100644 index 0000000..0fefe18 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodemethod.h @@ -0,0 +1,79 @@ +/* + * 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 "opcuanode.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + + +struct OpcUaChannelMethodParameter +{ +public: + std::string getName() const + { + return name; + } + void setName(const std::string& name) + { + this->name = name; + } + + OpcUaNodeId getDataTypeId() const + { + return dataTypeId; + } + + void setDataTypeId(const OpcUaNodeId& dataTypeId) + { + this->dataTypeId = dataTypeId; + } + +private: + std::string name; + OpcUaNodeId dataTypeId; +}; + +class OpcUaNodeMethod; +using OpcUaNodeMethodPtr = std::shared_ptr; + +class OpcUaNodeMethod : public OpcUaNode +{ +public: + OpcUaNodeMethod(const OpcUaNodeId& uaNode, const OpcUaNodeId& parentNodeId); + OpcUaNodeMethod(const UA_ReferenceDescription& uaNodeDescription, const OpcUaNodeId& parentNodeId); + ~OpcUaNodeMethod(); + + std::string getTypeDescription(); + void initTypeDescription(); + + void addInputParameter(const std::string& name, const OpcUaNodeId& dataTypeId); + void addOutputParameter(const std::string& name, const OpcUaNodeId& dataTypeId); + + std::list inputParameters; + std::list outputParameters; + + const OpcUaNodeId& getParentNodeId() const; + +protected: + std::string typeDescription; + OpcUaNodeId parentNodeId; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodeobject.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodeobject.h new file mode 100644 index 0000000..6f07406 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodeobject.h @@ -0,0 +1,39 @@ +/* + * 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 "opcuashared/node/opcuanode.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaNodeObject; +using OpcUaNodeObjectPtr = std::shared_ptr; + +class OpcUaNodeObject : public OpcUaNode +{ +public: + explicit OpcUaNodeObject(const OpcUaNodeId& uaNode); + explicit OpcUaNodeObject(const UA_ReferenceDescription& uaNodeDescription); + ~OpcUaNodeObject(); + + static OpcUaNodeObjectPtr instantiateRoot(); + static OpcUaNodeObjectPtr instantiateObjectsFolder(); + +private: +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodevariable.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodevariable.h new file mode 100644 index 0000000..8231bf1 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuanodevariable.h @@ -0,0 +1,50 @@ +/* + * 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 + +class OpcUaNodeVariable; +using OpcUaNodeVariablePtr = std::shared_ptr; + +class OpcUaNodeVariable : public OpcUaNode +{ +public: + OpcUaNodeVariable(const OpcUaNodeId& uaNode, const UA_DataType& uaDataType, size_t dimension = 1); + OpcUaNodeVariable(const OpcUaNodeId& uaNode, const OpcUaNodeId& dataTypeNodeId, size_t dimension = 1); + OpcUaNodeVariable(const UA_ReferenceDescription& uaNodeDescription, const UA_DataType& uaDataType, size_t dimension = 1); + OpcUaNodeVariable(const UA_ReferenceDescription& uaNodeDescription, const OpcUaNodeId& dataTypeNodeId, size_t dimension = 1); + ~OpcUaNodeVariable(); + + const OpcUaNodeId getDataTypeNodeId() const; + + OpcUaVariantPtr getVariant(); + void setVariant(const UA_Variant& value); + + size_t getDimension(); + void setDimension(size_t dimension); + +protected: + OpcUaNodeId dataTypeNodeId; + OpcUaVariantPtr variant; + size_t dimension; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuaobjecttype.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuaobjecttype.h new file mode 100644 index 0000000..282006c --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuaobjecttype.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaObjectType; +using OpcUaObjectTypePtr = std::shared_ptr; + +class OpcUaObjectType : public OpcUaType +{ +public: + explicit OpcUaObjectType(const OpcUaNodeId& typeId); + ~OpcUaObjectType(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuatype.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuatype.h new file mode 100644 index 0000000..7aadbbc --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuatype.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 + +class OpcUaType; +using OpcUaTypePtr = std::shared_ptr; + +class OpcUaType : public OpcUaNode +{ +public: + OpcUaType(const OpcUaNodeId& typeId, OpcUaNodeClass nodeClass); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuavariabletype.h b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuavariabletype.h new file mode 100644 index 0000000..2747ded --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/node/opcuavariabletype.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaVariableType; +using OpcUaVariableTypePtr = std::shared_ptr; + +class OpcUaVariableType : public OpcUaType +{ +public: + explicit OpcUaVariableType(const OpcUaNodeId& typeId); + ~OpcUaVariableType(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcua.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcua.h new file mode 100644 index 0000000..ba6923b --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcua.h @@ -0,0 +1,121 @@ +/* + * 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. + */ + +/* + * OPC UA library; is included in every class across the solution. + */ +#pragma once +#define BEGIN_NAMESPACE_OPENDAQ_OPCUA \ + namespace daq::opcua \ + { + +#define END_NAMESPACE_OPENDAQ_OPCUA \ + } + +#include + +// clang compiler on macOS emits warnings when compiling code in types.h, +// so we want to silence it here +#if defined(__GNUC__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wmissing-field-initializers" + #pragma GCC diagnostic ignored "-Wmissing-braces" +#endif + +#include +#include + +#if defined(__GNUC__) + #pragma GCC diagnostic pop +#endif + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +enum class OpcUaNodeClass +{ + Object = UA_NODECLASS_OBJECT, + Variable = UA_NODECLASS_VARIABLE, + Method = UA_NODECLASS_METHOD, + ObjectType = UA_NODECLASS_OBJECTTYPE, + VariableType = UA_NODECLASS_VARIABLETYPE, + ReferenceType = UA_NODECLASS_REFERENCETYPE, + DataType = UA_NODECLASS_DATATYPE, + View = UA_NODECLASS_VIEW, + + All = Object | Variable | Method | ObjectType | VariableType | ReferenceType | DataType | View // Mask +}; + +enum class OpcUaBrowseDirection +{ + Forward = UA_BROWSEDIRECTION_FORWARD, + Inverse = UA_BROWSEDIRECTION_INVERSE, + Both = UA_BROWSEDIRECTION_BOTH +}; + +#define OPCUA_STATUSCODE_FAILED(x) ((x) &0x80000000) +#define OPCUA_STATUSCODE_SUCCEEDED(x) (!OPCUA_STATUSCODE_FAILED(x)) +#define OPCUA_STATUSCODE_IS_GOOD(x) (x == UA_STATUSCODE_GOOD) +#define OPCUA_STATUSCODE_NOT_CONNECTED(x) ((x == UA_STATUSCODE_BADDISCONNECT) || (x == UA_STATUSCODE_BADCONNECTIONCLOSED) || (x == UA_STATUSCODE_BADNOTCONNECTED)) + +#define OPCUA_STATUSCODE_LOG_MESSAGE(STATUS_CODE) \ + "StatusCode 0x" << std::hex << std::uppercase << STATUS_CODE << std::nouppercase << std::dec << "(" << UA_StatusCode_name(STATUS_CODE) \ + << ")" + +#define THROW_RUNTIME_ERROR(x) \ + { \ + std::stringstream excStream; \ + excStream << x; \ + throw std::runtime_error(excStream.str().c_str()); \ + } + +inline bool operator==(const UA_String& l, const UA_String& r) +{ + return UA_String_equal(&l, &r); +} + +inline bool operator!=(const UA_String& l, const UA_String& r) +{ + return !(l == r); +} + +inline bool operator==(const UA_String& l, const char* r) +{ + UA_String rStr{}; + if (r) + { + rStr.length = strlen(r); + rStr.data = (UA_Byte*) r; + } + + return l == rStr; +} + +inline bool operator!=(const UA_String& l, const char* r) +{ + return !(l == r); +} + +inline bool operator==(const char* l, const UA_String& r) +{ + return r == l; +} + +inline bool operator!=(const char* l, const UA_String& r) +{ + return r != l; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcua_attribute.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcua_attribute.h new file mode 100644 index 0000000..231ca94 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcua_attribute.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +struct OpcUaAttribute +{ + OpcUaNodeId nodeId; + UA_AttributeId attributeId; + + OpcUaAttribute(const OpcUaNodeId& nodeId, UA_AttributeId attributeId) + : nodeId(nodeId) + , attributeId(attributeId) + { + } + + bool operator==(const OpcUaAttribute& other) const + { + return nodeId == other.nodeId && attributeId == other.attributeId; + } +}; + +END_NAMESPACE_OPENDAQ_OPCUA + +namespace std +{ + template <> + struct hash + { + size_t operator()(const daq::opcua::OpcUaAttribute& attr) const noexcept + { + UA_UInt32 hash = UA_NodeId_hash(attr.nodeId.get()); + return UA_ByteString_hash(hash, (const UA_Byte*) &attr.attributeId, sizeof(UA_AttributeId)); + } + }; +} diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacallmethodresult.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacallmethodresult.h new file mode 100644 index 0000000..a0194b7 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacallmethodresult.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 "opcuacommon.h" +#include "opcuavariant.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaCallMethodResult +{ +public: + OpcUaCallMethodResult(const UA_CallMethodResult& callMethodResult); + virtual ~OpcUaCallMethodResult(); + + const UA_StatusCode& getStatusCode() const; + bool isStatusOK() const; + + size_t getOutputArgumentsSize() const; + OpcUaVariant getOutputArgument(size_t i) const; + + const UA_CallMethodResult& getCallMethodResult() const; + operator const UA_CallMethodResult&() const; + +protected: + const UA_CallMethodResult& callMethodResult; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacollection.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacollection.h new file mode 100644 index 0000000..aabe253 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacollection.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. + */ + +/* +Common template for collection of endpoints, clients etc. +*/ +#pragma once +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +template > +class OpcUaCollection : public OpcUaCollectionT +{ + using OpcUaCollectionT::OpcUaCollectionT; + +public: + using BaseType = T; + + void remove(size_t index); + int indexOf(const T item); +}; + +template +void OpcUaCollection::remove(size_t index) +{ + OpcUaCollection::erase(OpcUaCollection::begin() + index); +} + +template +int OpcUaCollection::indexOf(const T item) +{ + size_t pos = std::find(OpcUaCollection::begin(), OpcUaCollection::end(), item) - OpcUaCollection::begin(); + if (pos < OpcUaCollection::size()) + return int(pos); + return -1; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacommon.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacommon.h new file mode 100644 index 0000000..39d55cc --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuacommon.h @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/* + * A bundle of static methods used in different projects accross the platform + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "open62541/util.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +namespace utils +{ + inline std::string ToStdString(const UA_String& value) + { + return std::string((const char*) value.data, value.length); + } + + inline std::string GuidToString(const UA_Guid& guid) + { + std::ostringstream oss; + oss << std::hex << std::setfill('0') + << std::setw(8) << guid.data1 << "-" + << std::setw(4) << guid.data2 << "-" + << std::setw(4) << guid.data3 << "-" + << std::setw(2) << static_cast(guid.data4[0]) + << std::setw(2) << static_cast(guid.data4[1]) << "-" + << std::setw(2) << static_cast(guid.data4[2]) + << std::setw(2) << static_cast(guid.data4[3]) + << std::setw(2) << static_cast(guid.data4[4]) + << std::setw(2) << static_cast(guid.data4[5]) + << std::setw(2) << static_cast(guid.data4[6]) + << std::setw(2) << static_cast(guid.data4[7]); + + return oss.str(); + } + + double ToSeconds(const UA_DateTime& time); + UA_StatusCode ToUaVariant(double value, const UA_NodeId& dataTypeNodeId, UA_Variant* var); + void ToUaVariant(const std::string& value, const UA_NodeId& dataTypeNodeId, UA_Variant* var); + using DurationTimeStamp = std::chrono::steady_clock::time_point; + DurationTimeStamp GetDurationTimeStamp(); + + OpcUaObject LoadFile(const std::string& path); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatatypearraylist.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatatypearraylist.h new file mode 100644 index 0000000..643f30d --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatatypearraylist.h @@ -0,0 +1,36 @@ +/* + * 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 + +class OpcUaDataTypeArrayList : protected std::list +{ +public: + using std::list::list; + + OpcUaDataTypeArrayList(const OpcUaDataTypeArrayList& copy); + OpcUaDataTypeArrayList& operator=(const OpcUaDataTypeArrayList& other); + + void add(const size_t typesSize, const UA_DataType* types); + const UA_DataTypeArray* getCustomDataTypes() const; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h new file mode 100644 index 0000000..ba0950f --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuadatavalue.h @@ -0,0 +1,47 @@ +/* + * 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 "opcuacommon.h" +#include "opcuavariant.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaDataValue; +using OpcUaDataValuePtr = std::shared_ptr; + +class OpcUaDataValue +{ +public: + OpcUaDataValue(const UA_DataValue* dataValue); + virtual ~OpcUaDataValue(); + + bool hasValue() const; + const OpcUaVariant& getValue() const; + const UA_StatusCode& getStatusCode() const; + + bool isStatusOK() const; + + const UA_DataValue* getDataValue() const; + operator const UA_DataValue*() const; + +protected: + const UA_DataValue* dataValue; + const OpcUaVariant variant; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaendpoint.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaendpoint.h new file mode 100644 index 0000000..71fc7e3 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaendpoint.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. + */ + +/* + +Endpoint defines the used network protocol and the necessary security +settings to be able to connect to the server. +One server can provide one or more endpoints. + +*/ + +#pragma once + +#include +#include +#include +#include "opcuacommon.h" +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaEndpoint; +using OpcUaEndpointPtr = std::shared_ptr; + +class OpcUaEndpoint +{ +public: + OpcUaEndpoint(const std::string& url); + OpcUaEndpoint(const std::string& url, const std::string& username, const std::string& password); + + void setName(const std::string& name); + const std::string getName() const; + + void setUrl(const std::string& url); + const std::string getUrl() const; + + void setUsername(const std::string& username); + const std::string getUsername() const; + + void setPassword(const std::string& password); + const std::string getPassword() const; + + void registerCustomTypes(const size_t typesSize, const UA_DataType* types); + const UA_DataTypeArray* getCustomDataTypes() const; + + bool isAnonymous(); + +private: + std::string name; + std::string url; + std::string username; + std::string password; + OpcUaDataTypeArrayList customDataTypeList; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaexception.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaexception.h new file mode 100644 index 0000000..0b37afe --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaexception.h @@ -0,0 +1,60 @@ +/* + * 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 "opcua.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaException : public std::runtime_error +{ +public: + OpcUaException(UA_StatusCode statusCode, char const* const message = "") + : std::runtime_error(message) + , statusCode(statusCode) + { + } + + OpcUaException(UA_StatusCode statusCode, const std::string& message) + : OpcUaException(statusCode, message.c_str()) + { + } + + UA_StatusCode getStatusCode() const + { + return statusCode; + } + +private: + UA_StatusCode statusCode; +}; + +inline void CheckStatusCodeException(UA_StatusCode code, const char* message = "") +{ + if (code != UA_STATUSCODE_GOOD) + { + throw OpcUaException(code, message); + } +} + +inline void CheckStatusCodeException(UA_StatusCode code, const std::string& message) +{ + CheckStatusCodeException(code, message.c_str()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcualog.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcualog.h new file mode 100644 index 0000000..a3331c6 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcualog.h @@ -0,0 +1,25 @@ +/* + * 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 LOGD std::stringstream() +#define LOGI LOGD +#define LOGV LOGD +#define LOGW LOGD +#define LOGE LOGD + +#define LOG(...) LOGD diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuanodecollection.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuanodecollection.h new file mode 100644 index 0000000..a26ab6b --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuanodecollection.h @@ -0,0 +1,42 @@ +/* + * 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 "opcua.h" +#include "opcuacollection.h" +#include "node/opcuanode.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaNodeCollection : public OpcUaCollection +{ + using OpcUaCollection::OpcUaCollection; + +public: + OpcUaNodeCollection selectNodes(OpcUaNodeClass nodeClassMask); + + OpcUaNodePtr locateNode(const OpcUaNodeId& nodeId) const; +}; + + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuanodeid.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuanodeid.h new file mode 100644 index 0000000..d360cd2 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuanodeid.h @@ -0,0 +1,159 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaIdentifierUniversal = std::string; + +using UA_NodeIdPtr = UA_NodeId*; + +enum class OpcUaIdentifierType +{ + Numeric = 0, + String = 3, + Guid = 4, + Undefined = -1 +}; + +static_assert(static_cast(OpcUaIdentifierType::Numeric) == UA_NODEIDTYPE_NUMERIC); +static_assert(static_cast(OpcUaIdentifierType::String) == UA_NODEIDTYPE_STRING); +static_assert(static_cast(OpcUaIdentifierType::Guid) == UA_NODEIDTYPE_GUID); + +#define OPCUANODEID_ROOTFOLDER OpcUaNodeId(0u, UA_NS0ID_ROOTFOLDER) +#define OPCUANODEID_OBJECTSFOLDER OpcUaNodeId(0u, UA_NS0ID_OBJECTSFOLDER) + +class OpcUaNodeId : public OpcUaObject +{ + using OpcUaObject::OpcUaObject; + +public: + OpcUaNodeId(); + explicit OpcUaNodeId(uint32_t identifier); + OpcUaNodeId(uint16_t namespaceIndex, const char* identifier); + OpcUaNodeId(uint16_t namespaceIndex, const std::string& identifier); + OpcUaNodeId(uint16_t namespaceIndex, uint32_t identifier); + + inline bool operator==(const OpcUaNodeId& rhs) const noexcept + { + return UA_NodeId_equal(this->getPtr(), rhs.getPtr()); + } + + inline bool operator!=(const OpcUaNodeId& rhs) const noexcept + { + return !UA_NodeId_equal(this->getPtr(), rhs.getPtr()); + } + + inline bool operator==(const UA_NodeId& rhs) const noexcept + { + return UA_NodeId_equal(this->getPtr(), &rhs); + } + + inline bool operator!=(const UA_NodeId& rhs) const noexcept + { + return !UA_NodeId_equal(this->getPtr(), &rhs); + } + + friend inline bool operator<(const OpcUaNodeId& l, const OpcUaNodeId& r) noexcept + { + const auto& lhs = l.getValue(); + const auto& rhs = r.getValue(); + if (lhs.namespaceIndex != rhs.namespaceIndex) + return lhs.namespaceIndex < rhs.namespaceIndex; + + if (lhs.identifierType != rhs.identifierType) + return lhs.identifierType < rhs.identifierType; + + switch (lhs.identifierType) + { + case UA_NODEIDTYPE_NUMERIC: + return lhs.identifier.numeric < rhs.identifier.numeric; + case UA_NODEIDTYPE_GUID: + return memcmp(&lhs.identifier.guid, &rhs.identifier.guid, sizeof(UA_Guid)) < 0; + case UA_NODEIDTYPE_BYTESTRING: + case UA_NODEIDTYPE_STRING: + if (lhs.identifier.string.length != rhs.identifier.string.length) + return lhs.identifier.string.length < rhs.identifier.string.length; + + return memcmp((char const*) lhs.identifier.string.data, + (char const*) rhs.identifier.string.data, + lhs.identifier.string.length) < 0; + } + return false; + } + + const UA_NodeId* getPtr() const noexcept; + UA_NodeId* getPtr() noexcept; + + uint16_t getNamespaceIndex() const noexcept; + uint32_t getIdentifierNumeric() const; + OpcUaIdentifierUniversal getIdentifier() const; + OpcUaIdentifierType getIdentifierType() const; + std::string toString() const; + + bool isNull() const noexcept; + + static void SetRandomSeed(); + static OpcUaNodeId CreateWithRandomGuid(); + + static OpcUaNodeId instantiateNode(uint16_t namespaceIndex, + OpcUaIdentifierUniversal identifierUniversal, + OpcUaIdentifierType identifierType); + static OpcUaIdentifierUniversal getIdentifier(const UA_NodeId& uaNodeId); + + static OpcUaIdentifierType getIdentifierType(const UA_NodeIdType& identifierType); + + template + inline OpcUaNodeId addSuffix(Args... args) const + { + std::stringstream ss; + concat(ss, getIdentifier(), args...); + return OpcUaNodeId(getNamespaceIndex(), ss.str()); + } + +private: + + template + static void concat(std::ostream& out, const T& first) + { + out << first; + } + + template + static void concat(std::ostream& out, const T& first, const Args&... rest) + { + out << first; + concat(out, rest...); + } +}; + +END_NAMESPACE_OPENDAQ_OPCUA + +namespace std +{ + template <> + struct hash + { + size_t operator()(const daq::opcua::OpcUaNodeId& k) const noexcept + { + return static_cast(UA_NodeId_hash(k.get())); + } + }; +} diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaobject.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaobject.h new file mode 100644 index 0000000..95dd8ac --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaobject.h @@ -0,0 +1,342 @@ +/* + * 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 + +template +struct TypeToUaDataType +{ + constexpr static const UA_DataType* DataType = nullptr; +}; + +template +inline constexpr const UA_DataType* GetUaDataType() +{ + static_assert(TypeToUaDataType::DataType != nullptr, "Implement specialization of TypeToUaDataType"); + return TypeToUaDataType::DataType; +} + +#define ADD_STANDARD_TYPE_MAPPING(TYPE, TYPE_INDEX) \ + template <> \ + struct TypeToUaDataType \ + { \ + constexpr static const UA_DataType* DataType = &UA_TYPES[TYPE_INDEX]; \ + }; + +#define ADD_CUSTOM_TYPE_MAPPING(TYPE, DATA_TYPE) \ + template <> \ + struct TypeToUaDataType \ + { \ + constexpr static const UA_DataType* DataType = DATA_TYPE; \ + }; + +#define ADD_STANDARD_TYPE_ALIAS_MAPPING(MAPPING_NAME, TYPE_INDEX) \ + struct MAPPING_NAME \ + { \ + constexpr static const UA_DataType* DataType = &UA_TYPES[TYPE_INDEX]; \ + }; + +template +class OpcUaObject +{ +public: + OpcUaObject(); + OpcUaObject(const OpcUaObject& other); + OpcUaObject(const UATYPE& value, bool shallowCopy = false); + OpcUaObject(UATYPE&& value); + + virtual ~OpcUaObject(); + + inline constexpr const UA_DataType* getUaDataType() + { + return GetUaDataType(); + } + + OpcUaObject& operator=(const OpcUaObject& other); + OpcUaObject& operator=(OpcUaObject&& other); + + void setValue(UATYPE&& value); + void setValue(const UATYPE& value, bool shallowCopy = false); + + const UATYPE& getValue() const noexcept; + UATYPE& getValue() noexcept; + + const UATYPE& operator*() const noexcept; + UATYPE& operator*() noexcept; + + const UATYPE* get() const noexcept; + UATYPE* get() noexcept; + + const UATYPE* operator->() const noexcept; + UATYPE* operator->() noexcept; + + void clear() noexcept; + UATYPE getDetachedValue() noexcept; + + OpcUaObject copy() const noexcept; + UATYPE copyAndGetDetachedValue() const noexcept; + + UATYPE* newDetachedPointer(); + UATYPE* newDetachedPointerCopy() const; + + void markDetached(bool detached); + +protected: + UATYPE value; + bool shallowCopy = false; +}; + +template +OpcUaObject::OpcUaObject() + : shallowCopy(false) +{ + UA_init(&value, getUaDataType()); +} + +template +OpcUaObject::OpcUaObject(const OpcUaObject& other) + : OpcUaObject() +{ + setValue(other.getValue()); +} + +template +OpcUaObject::OpcUaObject(const UATYPE& value, bool shallowCopy) + : OpcUaObject() +{ + setValue(value, shallowCopy); +} + +template +OpcUaObject::OpcUaObject(UATYPE&& value) + : OpcUaObject() +{ + setValue(std::forward(value)); + UA_init(&value, getUaDataType()); +} + +template +OpcUaObject::~OpcUaObject() +{ + clear(); +} + +template +OpcUaObject& OpcUaObject::operator=(const OpcUaObject& other) +{ + if (&other == this) + return *this; + + setValue(other.getValue()); + return *this; +} + +template +OpcUaObject& OpcUaObject::operator=(OpcUaObject&& other) +{ + clear(); + + value = other.value; + UA_init(&other.value, getUaDataType()); + + shallowCopy = other.shallowCopy; + return *this; +} + +template +void OpcUaObject::setValue(UATYPE&& value) +{ + clear(); + this->value = value; + UA_init(&value, getUaDataType()); +} + +template +void OpcUaObject::setValue(const UATYPE& value, bool shallowCopy) +{ + clear(); + UA_init(&this->value, getUaDataType()); + if (shallowCopy) + this->value = value; + else + UA_copy(&value, &this->value, getUaDataType()); + this->shallowCopy = shallowCopy; +} + +template +const UATYPE& OpcUaObject::getValue() const noexcept +{ + return this->value; +} + +template +UATYPE& OpcUaObject::getValue() noexcept +{ + return this->value; +} + +template +const UATYPE* OpcUaObject::get() const noexcept +{ + return &this->value; +} + +template +UATYPE* OpcUaObject::get() noexcept +{ + return &this->value; +} + +template +const UATYPE* OpcUaObject::operator->() const noexcept +{ + return &this->value; +} + +template +UATYPE* OpcUaObject::operator->() noexcept +{ + return &this->value; +} + +template +const UATYPE& OpcUaObject::operator*() const noexcept +{ + return getValue(); +} + +template +UATYPE& OpcUaObject::operator*() noexcept +{ + return getValue(); +} + +template +void OpcUaObject::clear() noexcept +{ + if (shallowCopy) + UA_init(&this->value, getUaDataType()); + else + UA_clear(&this->value, getUaDataType()); + + shallowCopy = false; +} + +template +UATYPE OpcUaObject::getDetachedValue() noexcept +{ + UATYPE val = getValue(); + shallowCopy = true; + clear(); + return val; +} + +template +OpcUaObject OpcUaObject::copy() const noexcept +{ + return OpcUaObject(value); +} + +template +UATYPE OpcUaObject::copyAndGetDetachedValue() const noexcept +{ + return copy().getDetachedValue(); +} + +template +inline UATYPE* OpcUaObject::newDetachedPointer() +{ + UATYPE* value = (UATYPE*) UA_new(TypeToUaDataType::DataType); + *value = this->getDetachedValue(); + return value; +} + +template +inline UATYPE* OpcUaObject::newDetachedPointerCopy() const +{ + UATYPE* value = (UATYPE*) UA_new(TypeToUaDataType::DataType); + *value = this->copyAndGetDetachedValue(); + return value; +} + +template +inline void OpcUaObject::markDetached(bool detached) +{ + this->shallowCopy = detached; +} + +ADD_STANDARD_TYPE_MAPPING(UA_Boolean, UA_TYPES_BOOLEAN) +ADD_STANDARD_TYPE_MAPPING(UA_SByte, UA_TYPES_SBYTE) +ADD_STANDARD_TYPE_MAPPING(UA_Byte, UA_TYPES_BYTE) +ADD_STANDARD_TYPE_MAPPING(UA_Int16, UA_TYPES_INT16) +ADD_STANDARD_TYPE_MAPPING(UA_UInt16, UA_TYPES_UINT16) +ADD_STANDARD_TYPE_MAPPING(UA_Int32, UA_TYPES_INT32) +ADD_STANDARD_TYPE_MAPPING(UA_UInt32, UA_TYPES_UINT32) +ADD_STANDARD_TYPE_MAPPING(UA_Int64, UA_TYPES_INT64) +ADD_STANDARD_TYPE_MAPPING(UA_UInt64, UA_TYPES_UINT64) +ADD_STANDARD_TYPE_MAPPING(UA_Variant, UA_TYPES_VARIANT) +ADD_STANDARD_TYPE_MAPPING(UA_String, UA_TYPES_STRING) +ADD_STANDARD_TYPE_MAPPING(UA_Double, UA_TYPES_DOUBLE) +ADD_STANDARD_TYPE_MAPPING(UA_Float, UA_TYPES_FLOAT) +ADD_STANDARD_TYPE_MAPPING(UA_BrowseRequest, UA_TYPES_BROWSEREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_BrowseResponse, UA_TYPES_BROWSERESPONSE) +ADD_STANDARD_TYPE_MAPPING(UA_BrowseNextRequest, UA_TYPES_BROWSENEXTREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_BrowseNextResponse, UA_TYPES_BROWSENEXTRESPONSE) +ADD_STANDARD_TYPE_MAPPING(UA_BrowseDescription, UA_TYPES_BROWSEDESCRIPTION) +ADD_STANDARD_TYPE_MAPPING(UA_BrowseResult, UA_TYPES_BROWSERESULT) +ADD_STANDARD_TYPE_MAPPING(UA_QualifiedName, UA_TYPES_QUALIFIEDNAME) +ADD_STANDARD_TYPE_MAPPING(UA_LocalizedText, UA_TYPES_LOCALIZEDTEXT) +ADD_STANDARD_TYPE_MAPPING(UA_ReadRequest, UA_TYPES_READREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_ReadResponse, UA_TYPES_READRESPONSE) +ADD_STANDARD_TYPE_MAPPING(UA_CallRequest, UA_TYPES_CALLREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_CallResponse, UA_TYPES_CALLRESPONSE) +ADD_STANDARD_TYPE_MAPPING(UA_NodeId, UA_TYPES_NODEID) +ADD_STANDARD_TYPE_MAPPING(UA_ReferenceDescription, UA_TYPES_REFERENCEDESCRIPTION) +ADD_STANDARD_TYPE_MAPPING(UA_ObjectAttributes, UA_TYPES_OBJECTATTRIBUTES) +ADD_STANDARD_TYPE_MAPPING(UA_MethodAttributes, UA_TYPES_METHODATTRIBUTES) +ADD_STANDARD_TYPE_MAPPING(UA_ObjectTypeAttributes, UA_TYPES_OBJECTTYPEATTRIBUTES) +ADD_STANDARD_TYPE_MAPPING(UA_VariableTypeAttributes, UA_TYPES_VARIABLETYPEATTRIBUTES) +ADD_STANDARD_TYPE_MAPPING(UA_VariableAttributes, UA_TYPES_VARIABLEATTRIBUTES) +ADD_STANDARD_TYPE_MAPPING(UA_DataTypeAttributes, UA_TYPES_DATATYPEATTRIBUTES) +ADD_STANDARD_TYPE_MAPPING(UA_ExpandedNodeId, UA_TYPES_EXPANDEDNODEID) +ADD_STANDARD_TYPE_MAPPING(UA_DataValue, UA_TYPES_DATAVALUE) +ADD_STANDARD_TYPE_MAPPING(UA_Argument, UA_TYPES_ARGUMENT) +ADD_STANDARD_TYPE_MAPPING(UA_ReadValueId, UA_TYPES_READVALUEID) +ADD_STANDARD_TYPE_MAPPING(UA_MonitoredItemCreateRequest, UA_TYPES_MONITOREDITEMCREATEREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_MonitoredItemCreateResult, UA_TYPES_MONITOREDITEMCREATERESULT) +ADD_STANDARD_TYPE_MAPPING(UA_CreateSubscriptionRequest, UA_TYPES_CREATESUBSCRIPTIONREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_CreateSubscriptionResponse, UA_TYPES_CREATESUBSCRIPTIONRESPONSE) +ADD_STANDARD_TYPE_MAPPING(UA_CallMethodRequest, UA_TYPES_CALLMETHODREQUEST) +ADD_STANDARD_TYPE_MAPPING(UA_CallMethodResult, UA_TYPES_CALLMETHODRESULT) +ADD_STANDARD_TYPE_MAPPING(UA_Range, UA_TYPES_RANGE) +ADD_STANDARD_TYPE_MAPPING(UA_ComplexNumberType, UA_TYPES_COMPLEXNUMBERTYPE) +ADD_STANDARD_TYPE_MAPPING(UA_DoubleComplexNumberType, UA_TYPES_DOUBLECOMPLEXNUMBERTYPE) +ADD_STANDARD_TYPE_MAPPING(UA_KeyValuePair, UA_TYPES_KEYVALUEPAIR) +ADD_STANDARD_TYPE_MAPPING(UA_ExtensionObject, UA_TYPES_EXTENSIONOBJECT) +ADD_STANDARD_TYPE_MAPPING(UA_RationalNumber, UA_TYPES_RATIONALNUMBER) +ADD_STANDARD_TYPE_MAPPING(UA_EventFilter, UA_TYPES_EVENTFILTER) +ADD_STANDARD_TYPE_MAPPING(UA_SimpleAttributeOperand, UA_TYPES_SIMPLEATTRIBUTEOPERAND) +ADD_STANDARD_TYPE_MAPPING(UA_BrowsePathResult, UA_TYPES_BROWSEPATHRESULT) +ADD_STANDARD_TYPE_MAPPING(UA_EnumField, UA_TYPES_ENUMFIELD) +ADD_STANDARD_TYPE_MAPPING(UA_EUInformation, UA_TYPES_EUINFORMATION) + +ADD_STANDARD_TYPE_ALIAS_MAPPING(UtcTimeTypeToUaDataType, UA_TYPES_UTCTIME); + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuasecurity_config.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuasecurity_config.h new file mode 100644 index 0000000..4517c18 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuasecurity_config.h @@ -0,0 +1,69 @@ +/* + * 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 + +typedef std::function AuthenticateUserCallback; + +class OpcUaSecurityConfig +{ +public: + UA_MessageSecurityMode securityMode = UA_MESSAGESECURITYMODE_NONE; + std::optional appUri; + OpcUaObject certificate; + OpcUaObject privateKey; + OpcUaVector trustList; + OpcUaVector revocationList; + bool trustAll = false; + + void validate() const; + bool hasCertificate() const; + bool hasPrivateKey() const; + std::optional getAppUriOrParseFromCertificate() const; + +protected: + OpcUaSecurityConfig(); + OpcUaSecurityConfig(const OpcUaSecurityConfig& config); + OpcUaSecurityConfig& operator=(const OpcUaSecurityConfig& config); + +}; + +class OpcUaServerSecurityConfig : public OpcUaSecurityConfig +{ +public: + AuthenticateUserCallback authenticateUser; + + OpcUaServerSecurityConfig(); +}; + +class OpcUaClientSecurityConfig : public OpcUaSecurityConfig +{ +public: + std::optional username; + std::optional password; + + bool isAnonymous() const; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuasecuritycommon.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuasecuritycommon.h new file mode 100644 index 0000000..67a03a6 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuasecuritycommon.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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaSecurityCommon +{ +public: + static std::optional parseCertificateUri(const UA_ByteString& certificate); + static UA_StatusCode verifyCertificateRejectAll(void* verificationContext, const UA_ByteString* certificate); +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h new file mode 100644 index 0000000..64c7980 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavariant.h @@ -0,0 +1,202 @@ +/* + * 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 "opcuacommon.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class OpcUaVariant; +using OpcUaVariantPtr = std::shared_ptr; + +namespace VariantUtils +{ + inline bool IsScalar(const UA_Variant& value) + { + return UA_Variant_isScalar(&value); + } + + inline bool IsVector(const UA_Variant& value) + { + return !IsScalar(value); + } + + template + inline bool IsType(const UA_Variant& value) + { + const auto expectedType = GetUaDataType(); + if (value.type == expectedType) + return true; + +#ifdef __APPLE__ + if (value.type != nullptr && value.type->typeKind == expectedType->typeKind) + { + if (value.type->typeName != nullptr && expectedType->typeName != nullptr) + return std::strcmp(value.type->typeName, expectedType->typeName) == 0; + return true; + } +#endif + return false; + } + + template + inline bool HasScalarType(const UA_Variant& value) + { + return IsScalar(value) && IsType(value); + } + + template + inline T ReadScalar(const UA_Variant& value) + { + if (!IsScalar(value)) + throw std::runtime_error("Variant is not a scalar"); + if (!IsType(value) && value.type->typeKind != UA_TYPES_ENUMERATION) + throw std::runtime_error("Variant does not contain a scalar of specified return type"); + return *static_cast(value.data); + } + + inline std::string ToString(const UA_Variant& value) + { + return utils::ToStdString(ReadScalar(value)); + } + + inline int64_t ToNumber(const UA_Variant& value) + { + switch (value.type->typeKind) + { + case UA_TYPES_SBYTE: + return ReadScalar(value); + case UA_TYPES_BYTE: + return ReadScalar(value); + case UA_TYPES_INT16: + return ReadScalar(value); + case UA_TYPES_UINT16: + return ReadScalar(value); + case UA_TYPES_INT32: + case UA_TYPES_ENUMERATION: + return ReadScalar(value); + case UA_TYPES_UINT32: + return ReadScalar(value); + case UA_TYPES_INT64: + return ReadScalar(value); + case UA_TYPES_UINT64: + return ReadScalar(value); + + default: + throw std::runtime_error("Type not supported!"); + } + } + + inline OpcUaNodeId ToNodeId(const UA_Variant& value) + { + UA_NodeId nodeId = ReadScalar(value); + return OpcUaNodeId(nodeId); + } + + void ToInt32Variant(OpcUaVariant& variant); + void ToInt64Variant(OpcUaVariant& variant); +} + +class OpcUaVariant : public OpcUaObject +{ +public: + using OpcUaObject::OpcUaObject; + + OpcUaVariant(); + + explicit OpcUaVariant(const uint16_t& value); + explicit OpcUaVariant(const uint32_t& value); + explicit OpcUaVariant(const int32_t& value); + explicit OpcUaVariant(const int64_t& value); + explicit OpcUaVariant(const char* value); + explicit OpcUaVariant(const double& value); + explicit OpcUaVariant(const bool& value); + explicit OpcUaVariant(const OpcUaNodeId& value); + OpcUaVariant(const UA_DataType* type, size_t dimension); + OpcUaVariant(const double& genericValue, const UA_DataType& originalType); + + template + inline bool isType() const + { + return VariantUtils::IsType(this->value); + } + + template + inline bool hasScalarType() + { + return VariantUtils::HasScalarType(this->value); + } + + template + T readScalar() const + { + return VariantUtils::ReadScalar(this->value); + } + + template > + void setScalar(const T& value) + { + static_assert(UATYPE::DataType != nullptr, "Implement specialization of TypeToUaDataType"); + + this->clear(); + + const auto status = UA_Variant_setScalarCopy(&this->value, &value, UATYPE::DataType); + CheckStatusCodeException(status); + } + + void setValue(UA_Variant&& value); + void setValue(const UA_Variant& value, bool shallowCopy = false); + + bool isInteger() const; + bool isString() const; + bool isDouble() const; + bool isBool() const; + bool isNodeId() const; + bool isNull() const; + bool isReal() const; + bool isNumber() const; + + std::string toString() const; + int64_t toInteger() const; + double toDouble() const; + float toFloat() const; + bool toBool() const; + OpcUaNodeId toNodeId() const; + + inline bool isScalar() const + { + return VariantUtils::IsScalar(value); + } + inline bool isVector() const + { + return VariantUtils::IsVector(value); + } + + static bool IsInteger(const UA_Variant& value); +}; + +class OpcUaVariableConversionError : public OpcUaException +{ +public: + OpcUaVariableConversionError(UA_StatusCode statusCode) + : OpcUaException(statusCode, "Conversion error") + { + } +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavector.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavector.h new file mode 100644 index 0000000..025b3ee --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuavector.h @@ -0,0 +1,218 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +template +class OpcUaVectorProxy; +template +class OpcUaVector; + +template +class OpcUaVectorProxy +{ +public: + OpcUaVector& self; + size_t key; + + OpcUaVectorProxy(OpcUaVector& self, size_t key); + + OpcUaVectorProxy& operator=(UATYPE& value); + operator UATYPE() const; + UATYPE* operator->() const; +}; + +template +class OpcUaVector +{ +public: + OpcUaVector(); + explicit OpcUaVector(size_t size); + OpcUaVector(std::initializer_list items); + + OpcUaVector& operator=(const OpcUaVector& vector); + OpcUaVectorProxy const operator[](size_t i) const; + OpcUaVectorProxy operator[](size_t i); + + void push_back(const UATYPE& value); + size_t size() const; + void resize(size_t size); + void clear(); + const UATYPE* data() const; + void set(size_t i, UATYPE& value); + const UATYPE& get(size_t i) const; + + ~OpcUaVector(); + +private: + friend class OpcUaVectorProxy; + std::vector items; + + void clearItemsRange(size_t from, size_t to); + void clearItemsRange(size_t from); + void copyItems(const std::vector& src); +}; + +template +inline OpcUaVectorProxy::OpcUaVectorProxy(OpcUaVector& self, size_t key) + : self(self) + , key(key) +{ +} + +template +inline OpcUaVectorProxy& OpcUaVectorProxy::operator=(UATYPE& value) +{ + self.set(key, value); + return *this; +} + +template +inline OpcUaVectorProxy::operator UATYPE() const +{ + return self.get(key); +} + +template +inline UATYPE* OpcUaVectorProxy::operator->() const +{ + return &self.get(key); +} + +template +inline OpcUaVector::OpcUaVector() +{ +} + +template +inline OpcUaVector::OpcUaVector(size_t size) + : items(size) +{ +} + +template +inline OpcUaVector::OpcUaVector(std::initializer_list items) + : items(items) +{ +} + +template +inline OpcUaVector& OpcUaVector::operator=(const OpcUaVector& vector) +{ + if (this == &vector) + return *this; + + clear(); + resize(vector.size()); + copyItems(vector.items); + + return *this; +} + +template +inline OpcUaVectorProxy const OpcUaVector::operator[](size_t i) const +{ + return OpcUaVectorProxy(*this, i); +} + +template +inline OpcUaVectorProxy OpcUaVector::operator[](size_t i) +{ + return OpcUaVectorProxy(*this, i); +} + +template +inline OpcUaVector::~OpcUaVector() +{ + clear(); +} + +template +inline void OpcUaVector::push_back(const UATYPE& value) +{ + UATYPE tmp; + UA_copy(&value, &tmp, TypeToUaDataType::DataType); + items.push_back(tmp); +} + +template +inline size_t OpcUaVector::size() const +{ + return items.size(); +} + +template +inline void OpcUaVector::resize(size_t size) +{ + if (size < items.size()) + clearItemsRange(size); + items.resize(size); +} + +template +inline void OpcUaVector::clear() +{ + for (size_t i = 0; i < items.size(); i++) + clearItemsRange(0); + items.clear(); +} + +template +inline const UATYPE* OpcUaVector::data() const +{ + return items.data(); +} + +template +inline void OpcUaVector::set(size_t i, UATYPE& value) +{ + UA_clear(&items[i], TypeToUaDataType::DataType); + UA_copy(&value, &items[i], TypeToUaDataType::DataType); +} + +template +inline const UATYPE& OpcUaVector::get(size_t i) const +{ + return items[i]; +} + +template +inline void OpcUaVector::clearItemsRange(size_t from, size_t to) +{ + for (size_t i = from; i < to; i++) + UA_clear(&items[i], TypeToUaDataType::DataType); +} + +template +inline void OpcUaVector::clearItemsRange(size_t from) +{ + clearItemsRange(from, items.size()); +} + +template +inline void OpcUaVector::copyItems(const std::vector& src) +{ + for (size_t i = 0; i < src.size(); i++) + UA_copy(&src[i], &items[i], TypeToUaDataType::DataType); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaversion.h b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaversion.h new file mode 100644 index 0000000..9c42d4e --- /dev/null +++ b/shared/libraries/opcua/opcuashared/include/opcuashared/opcuaversion.h @@ -0,0 +1,51 @@ +/* + * 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 "opcua.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +#pragma push_macro("major") +#pragma push_macro("minor") + +#undef major +#undef minor + +struct OpcUaVersion +{ + explicit OpcUaVersion(const char* version); + explicit constexpr OpcUaVersion(int major = 0, int minor = 0, int patch = 0) + : major(major) + , minor(minor) + , patch(patch) + { + } + + std::string toString() const; + + int major = 0; + int minor = 0; + int patch = 0; + + static bool Compatible(const OpcUaVersion& serverVersion, const OpcUaVersion& systemVersion); + static bool HasFeature(const OpcUaVersion& serverVersion, const OpcUaVersion& featureVersion); +}; + +#pragma pop_macro("minor") +#pragma pop_macro("major") + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/CMakeLists.txt b/shared/libraries/opcua/opcuashared/src/CMakeLists.txt new file mode 100644 index 0000000..e07798a --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/CMakeLists.txt @@ -0,0 +1,124 @@ +SET(MODULE_NAME opcuashared) + +set(RES_DIR_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../external/res") +set(CMAKE_GLOBALS_HEADER ${CMAKE_CURRENT_BINARY_DIR}/../include/${MODULE_NAME}/generated/cmake_globals.h) +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake_globals.h.template + ${CMAKE_GLOBALS_HEADER} @ONLY +) + +set(SOURCE_CPPS opcuavariant.cpp + opcuacommon.cpp + opcuasecuritycommon.cpp + opcuaendpoint.cpp + opcuanodecollection.cpp + opcuadatavalue.cpp + opcuacallmethodresult.cpp + opcuaversion.cpp + opcuasecurity_config.cpp + opcuadatatypearraylist.cpp + opcuanodeid.cpp + cmake_globals.h.template + OpcUaTypes.natvis + bcrypt.cpp + bcrypt/crypt_blowfish.c +) + +set(SOURCE_NODE_CPPS node/opcuanode.cpp + node/opcuanodeobject.cpp + node/opcuanodemethod.cpp + node/opcuanodevariable.cpp + node/opcuatype.cpp + node/opcuaobjecttype.cpp + node/opcuavariabletype.cpp + node/opcuadatatype.cpp +) + +set(SOURCE_HEADERS opcuavariant.h + opcua.h + opcualog.h + opcuacollection.h + opcuacommon.h + opcuasecuritycommon.h + opcuaendpoint.h + opcuanodecollection.h + opcuaexception.h + opcuadatavalue.h + opcuacallmethodresult.h + opcuaversion.h + opcuaobject.h + opcuasecurity_config.h + opcuadatatypearraylist.h + opcuavector.h + opcuanodeid.h + opcua_attribute.h + bcrypt.h + bcrypt/crypt_blowfish.h +) + +set(SOURCE_NODE_HEADERS node/opcuanode.h + node/opcuanodeobject.h + node/opcuanodemethod.h + node/opcuanodevariable.h + node/opcuatype.h + node/opcuaobjecttype.h + node/opcuavariabletype.h + node/opcuadatatype.h +) + +set(INCLUDE_DIR ../include/${MODULE_NAME}) + +prepend_include(${INCLUDE_DIR} SOURCE_HEADERS) +prepend_include(${INCLUDE_DIR} SOURCE_NODE_HEADERS) + +set(ALL_SOURCES ${SOURCE_CPPS} + ${SOURCE_NODE_CPPS} + ${SOURCE_HEADERS} + ${SOURCE_NODE_HEADERS} + ${CMAKE_GLOBALS_HEADER} +) + +if (MSVC) + set_source_files_properties(bcrypt/crypt_blowfish.h + bcrypt/crypt_blowfish.c + PROPERTIES LANGUAGE C COMPILE_FLAGS /wd4244) +endif() + +set(SOURCE_FILES ${ALL_SOURCES}) + +add_library(${MODULE_NAME} STATIC ${ALL_SOURCES}) +add_library(${SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) + +if(BUILD_64Bit OR BUILD_ARM) + set_target_properties(${MODULE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) +else() + set_target_properties(${MODULE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) +endif() + +target_include_directories(${MODULE_NAME} PUBLIC $ + $ + $ + PRIVATE ${PROJECT_SOURCE_DIR}/include/${MODULE_NAME}/generated +) + +target_link_libraries(${MODULE_NAME} PUBLIC open62541 + rapidjson + PRIVATE daq::opendaq_utils +) + +if (MSVC) + target_compile_definitions(${MODULE_NAME} PRIVATE _CRT_SECURE_NO_WARNINGS) + set_source_files_properties(opcuacommon.cpp PROPERTIES COMPILE_FLAGS /wd4100) +endif() + +set_target_properties(${MODULE_NAME} PROPERTIES PUBLIC_HEADER "${SOURCE_HEADERS}") +set_target_properties(${MODULE_NAME} PROPERTIES PUBLIC_NODE_HEADER "${SOURCE_NODE_HEADERS}") + +if(BUILD_64Bit) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "${MODULE_NAME}64") +endif() + +if (MSVC) + source_group("Header Files\\Node" "node/.+[.]h") + source_group("Source Files\\Node" "node/.+[.]cpp") +endif() diff --git a/shared/libraries/opcua/opcuashared/src/OpcUaTypes.natvis b/shared/libraries/opcua/opcuashared/src/OpcUaTypes.natvis new file mode 100644 index 0000000..a66da5c --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/OpcUaTypes.natvis @@ -0,0 +1,9 @@ + + + + data={data,[length]} + + data + + + \ No newline at end of file diff --git a/shared/libraries/opcua/opcuashared/src/bcrypt.cpp b/shared/libraries/opcua/opcuashared/src/bcrypt.cpp new file mode 100644 index 0000000..f519ccf --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/bcrypt.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" +{ + #include +} + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +BCrypt::BCrypt() + : randomDist(std::uniform_int_distribution(std::numeric_limits::min(), std::numeric_limits::max())) +{ +} + +std::vector BCrypt::randBytes(unsigned int length) +{ + std::vector bytes = std::vector(length); + for (unsigned int i = 0; i < length; i++) + { + bytes[i] = static_cast(randomDist(randomDevice)); + } + return bytes; +} + +std::string BCrypt::hash(const std::string& password, unsigned int rounds) +{ + std::string salt = generateSalt(SaltPartSize); + return hash(password, salt, rounds); +} + +std::string BCrypt::hash(const std::string& password, std::string salt, unsigned int rounds) const +{ + if (salt.size() < SaltPartSize) + throw BCryptSaltSizeException(SaltPartSize); + + char infoSalt[SaltSize + 1]; + infoSalt[0] = '$'; + infoSalt[1] = '2'; + infoSalt[2] = 'b'; + infoSalt[3] = '$'; + infoSalt[4] = '0' + (char) (rounds / 10); + infoSalt[5] = '0' + (char) (rounds % 10); + infoSalt[6] = '$'; + std::memcpy(&infoSalt[InfoPartSize], salt.data(), SaltPartSize); + + char hash[HashSize + 1]; + bool ok = _crypt_blowfish_rn(password.data(), infoSalt, &hash[0], HashSize + 1) != nullptr; + if (!ok) + throw BCryptException("Error while hashing the password."); + + return hash; +} + +bool BCrypt::Verify(const std::string& hash, const std::string& password) +{ + char test[HashSize + 1]; + bool ok = _crypt_blowfish_rn(password.data(), hash.data(), test, HashSize + 1); + if (!ok) + return false; + + return hash == test; +} + +std::string BCrypt::generateSalt(unsigned int size) +{ + std::vector bytes = randBytes(size); + return generateSalt(bytes.data(), (unsigned int) bytes.size()); +} + +std::string BCrypt::generateSalt(char* bytes, int size) const +{ + if (size < SaltPartSize) + throw BCryptSaltSizeException(SaltPartSize); + + char salt[SaltSize + 1]; + bool ok = _crypt_gensalt_blowfish_rn("$2b$", 8, bytes, size, &salt[0], SaltSize + 1); + if (!ok) + throw BCryptException("Error while generating bcrypt salt."); + + return std::string(&salt[InfoPartSize]); +} + +BCryptException::BCryptException(const std::string& message) + : std::runtime_error(message.c_str()) +{ +} + +BCryptSaltSizeException::BCryptSaltSizeException(unsigned int minSaltSize) + : BCryptException("Salt must be at least " + std::to_string(minSaltSize) + " characters long.") +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/bcrypt/crypt_blowfish.c b/shared/libraries/opcua/opcuashared/src/bcrypt/crypt_blowfish.c new file mode 100644 index 0000000..9dbead8 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/bcrypt/crypt_blowfish.c @@ -0,0 +1,907 @@ +/* + * The crypt_blowfish homepage is: + * + * http://www.openwall.com/crypt/ + * + * This code comes from John the Ripper password cracker, with reentrant + * and crypt(3) interfaces added, but optimizations specific to password + * cracking removed. + * + * Written by Solar Designer in 1998-2014. + * No copyright is claimed, and the software is hereby placed in the public + * domain. In case this attempt to disclaim copyright and place the software + * in the public domain is deemed null and void, then the software is + * Copyright (c) 1998-2014 Solar Designer and it is hereby released to the + * general public under the following terms: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + * + * It is my intent that you should be able to use this on your system, + * as part of a software package, or anywhere else to improve security, + * ensure compatibility, or for any other purpose. I would appreciate + * it if you give credit where it is due and keep your modifications in + * the public domain as well, but I don't require that in order to let + * you place this code and any modifications you make under a license + * of your choice. + * + * This implementation is fully compatible with OpenBSD's bcrypt.c for prefix + * "$2b$", originally by Niels Provos , and it uses + * some of his ideas. The password hashing algorithm was designed by David + * Mazieres . For information on the level of + * compatibility for bcrypt hash prefixes other than "$2b$", please refer to + * the comments in BF_set_key() below and to the included crypt(3) man page. + * + * There's a paper on the algorithm that explains its design decisions: + * + * http://www.usenix.org/events/usenix99/provos.html + * + * Some of the tricks in BF_ROUND might be inspired by Eric Young's + * Blowfish library (I can't be sure if I would think of something if I + * hadn't seen his code). + */ + +#include + +#include +#ifndef __set_errno +#define __set_errno(val) errno = (val) +#endif + +/* Just to make sure the prototypes match the actual definitions */ +#include + +#ifdef __i386__ +#define BF_ASM 0 +#define BF_SCALE 1 +#elif defined(__x86_64__) || defined(__alpha__) || defined(__hppa__) +#define BF_ASM 0 +#define BF_SCALE 1 +#else +#define BF_ASM 0 +#define BF_SCALE 0 +#endif + +typedef unsigned int BF_word; +typedef signed int BF_word_signed; + +/* Number of Blowfish rounds, this is also hardcoded into a few places */ +#define BF_N 16 + +typedef BF_word BF_key[BF_N + 2]; + +typedef struct { + BF_word S[4][0x100]; + BF_key P; +} BF_ctx; + +/* + * Magic IV for 64 Blowfish encryptions that we do at the end. + * The string is "OrpheanBeholderScryDoubt" on big-endian. + */ +static BF_word BF_magic_w[6] = { + 0x4F727068, 0x65616E42, 0x65686F6C, + 0x64657253, 0x63727944, 0x6F756274 +}; + +/* + * P-box and S-box tables initialized with digits of Pi. + */ +static BF_ctx BF_init_state = { + { + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + }, { + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + }, { + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + }, { + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + } + }, { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + } +}; + +static unsigned char BF_itoa64[64 + 1] = + "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +static unsigned char BF_atoi64[0x60] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 1, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 64, 64, 64, 64, 64, + 64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 64, 64, 64, 64, 64, + 64, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 64, 64, 64, 64, 64 +}; + +#define BF_safe_atoi64(dst, src) \ +{ \ + tmp = (unsigned char)(src); \ + if ((unsigned int)(tmp -= 0x20) >= 0x60) return -1; \ + tmp = BF_atoi64[tmp]; \ + if (tmp > 63) return -1; \ + (dst) = tmp; \ +} + +static int BF_decode(BF_word *dst, const char *src, int size) +{ + unsigned char *dptr = (unsigned char *)dst; + unsigned char *end = dptr + size; + const unsigned char *sptr = (const unsigned char *)src; + unsigned int tmp, c1, c2, c3, c4; + + do { + BF_safe_atoi64(c1, *sptr++); + BF_safe_atoi64(c2, *sptr++); + *dptr++ = (c1 << 2) | ((c2 & 0x30) >> 4); + if (dptr >= end) break; + + BF_safe_atoi64(c3, *sptr++); + *dptr++ = ((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2); + if (dptr >= end) break; + + BF_safe_atoi64(c4, *sptr++); + *dptr++ = ((c3 & 0x03) << 6) | c4; + } while (dptr < end); + + return 0; +} + +static void BF_encode(char *dst, const BF_word *src, int size) +{ + const unsigned char *sptr = (const unsigned char *)src; + const unsigned char *end = sptr + size; + unsigned char *dptr = (unsigned char *)dst; + unsigned int c1, c2; + + do { + c1 = *sptr++; + *dptr++ = BF_itoa64[c1 >> 2]; + c1 = (c1 & 0x03) << 4; + if (sptr >= end) { + *dptr++ = BF_itoa64[c1]; + break; + } + + c2 = *sptr++; + c1 |= c2 >> 4; + *dptr++ = BF_itoa64[c1]; + c1 = (c2 & 0x0f) << 2; + if (sptr >= end) { + *dptr++ = BF_itoa64[c1]; + break; + } + + c2 = *sptr++; + c1 |= c2 >> 6; + *dptr++ = BF_itoa64[c1]; + *dptr++ = BF_itoa64[c2 & 0x3f]; + } while (sptr < end); +} + +static void BF_swap(BF_word *x, int count) +{ + static int endianness_check = 1; + char *is_little_endian = (char *)&endianness_check; + BF_word tmp; + + if (*is_little_endian) + do { + tmp = *x; + tmp = (tmp << 16) | (tmp >> 16); + *x++ = ((tmp & 0x00FF00FF) << 8) | ((tmp >> 8) & 0x00FF00FF); + } while (--count); +} + +#if BF_SCALE +/* Architectures which can shift addresses left by 2 bits with no extra cost */ +#define BF_ROUND(L, R, N) \ + tmp1 = L & 0xFF; \ + tmp2 = L >> 8; \ + tmp2 &= 0xFF; \ + tmp3 = L >> 16; \ + tmp3 &= 0xFF; \ + tmp4 = L >> 24; \ + tmp1 = data.ctx.S[3][tmp1]; \ + tmp2 = data.ctx.S[2][tmp2]; \ + tmp3 = data.ctx.S[1][tmp3]; \ + tmp3 += data.ctx.S[0][tmp4]; \ + tmp3 ^= tmp2; \ + R ^= data.ctx.P[N + 1]; \ + tmp3 += tmp1; \ + R ^= tmp3; +#else +/* Architectures with no complicated addressing modes supported */ +#define BF_INDEX(S, i) \ + (*((BF_word *)(((unsigned char *)S) + (i)))) +#define BF_ROUND(L, R, N) \ + tmp1 = L & 0xFF; \ + tmp1 <<= 2; \ + tmp2 = L >> 6; \ + tmp2 &= 0x3FC; \ + tmp3 = L >> 14; \ + tmp3 &= 0x3FC; \ + tmp4 = L >> 22; \ + tmp4 &= 0x3FC; \ + tmp1 = BF_INDEX(data.ctx.S[3], tmp1); \ + tmp2 = BF_INDEX(data.ctx.S[2], tmp2); \ + tmp3 = BF_INDEX(data.ctx.S[1], tmp3); \ + tmp3 += BF_INDEX(data.ctx.S[0], tmp4); \ + tmp3 ^= tmp2; \ + R ^= data.ctx.P[N + 1]; \ + tmp3 += tmp1; \ + R ^= tmp3; +#endif + +/* + * Encrypt one block, BF_N is hardcoded here. + */ +#define BF_ENCRYPT \ + L ^= data.ctx.P[0]; \ + BF_ROUND(L, R, 0); \ + BF_ROUND(R, L, 1); \ + BF_ROUND(L, R, 2); \ + BF_ROUND(R, L, 3); \ + BF_ROUND(L, R, 4); \ + BF_ROUND(R, L, 5); \ + BF_ROUND(L, R, 6); \ + BF_ROUND(R, L, 7); \ + BF_ROUND(L, R, 8); \ + BF_ROUND(R, L, 9); \ + BF_ROUND(L, R, 10); \ + BF_ROUND(R, L, 11); \ + BF_ROUND(L, R, 12); \ + BF_ROUND(R, L, 13); \ + BF_ROUND(L, R, 14); \ + BF_ROUND(R, L, 15); \ + tmp4 = R; \ + R = L; \ + L = tmp4 ^ data.ctx.P[BF_N + 1]; + +#if BF_ASM +#define BF_body() \ + _BF_body_r(&data.ctx); +#else +#define BF_body() \ + L = R = 0; \ + ptr = data.ctx.P; \ + do { \ + ptr += 2; \ + BF_ENCRYPT; \ + *(ptr - 2) = L; \ + *(ptr - 1) = R; \ + } while (ptr < &data.ctx.P[BF_N + 2]); \ +\ + ptr = data.ctx.S[0]; \ + do { \ + ptr += 2; \ + BF_ENCRYPT; \ + *(ptr - 2) = L; \ + *(ptr - 1) = R; \ + } while (ptr < &data.ctx.S[3][0xFF]); +#endif + +static void BF_set_key(const char *key, BF_key expanded, BF_key initial, + unsigned char flags) +{ + const char *ptr = key; + unsigned int bug, i, j; + BF_word safety, sign, diff, tmp[2]; + +/* + * There was a sign extension bug in older revisions of this function. While + * we would have liked to simply fix the bug and move on, we have to provide + * a backwards compatibility feature (essentially the bug) for some systems and + * a safety measure for some others. The latter is needed because for certain + * multiple inputs to the buggy algorithm there exist easily found inputs to + * the correct algorithm that produce the same hash. Thus, we optionally + * deviate from the correct algorithm just enough to avoid such collisions. + * While the bug itself affected the majority of passwords containing + * characters with the 8th bit set (although only a percentage of those in a + * collision-producing way), the anti-collision safety measure affects + * only a subset of passwords containing the '\xff' character (not even all of + * those passwords, just some of them). This character is not found in valid + * UTF-8 sequences and is rarely used in popular 8-bit character encodings. + * Thus, the safety measure is unlikely to cause much annoyance, and is a + * reasonable tradeoff to use when authenticating against existing hashes that + * are not reliably known to have been computed with the correct algorithm. + * + * We use an approach that tries to minimize side-channel leaks of password + * information - that is, we mostly use fixed-cost bitwise operations instead + * of branches or table lookups. (One conditional branch based on password + * length remains. It is not part of the bug aftermath, though, and is + * difficult and possibly unreasonable to avoid given the use of C strings by + * the caller, which results in similar timing leaks anyway.) + * + * For actual implementation, we set an array index in the variable "bug" + * (0 means no bug, 1 means sign extension bug emulation) and a flag in the + * variable "safety" (bit 16 is set when the safety measure is requested). + * Valid combinations of settings are: + * + * Prefix "$2a$": bug = 0, safety = 0x10000 + * Prefix "$2b$": bug = 0, safety = 0 + * Prefix "$2x$": bug = 1, safety = 0 + * Prefix "$2y$": bug = 0, safety = 0 + */ + bug = (unsigned int)flags & 1; + safety = ((BF_word)flags & 2) << 15; + + sign = diff = 0; + + for (i = 0; i < BF_N + 2; i++) { + tmp[0] = tmp[1] = 0; + for (j = 0; j < 4; j++) { + tmp[0] <<= 8; + tmp[0] |= (unsigned char)*ptr; /* correct */ + tmp[1] <<= 8; + tmp[1] |= (BF_word_signed)(signed char)*ptr; /* bug */ +/* + * Sign extension in the first char has no effect - nothing to overwrite yet, + * and those extra 24 bits will be fully shifted out of the 32-bit word. For + * chars 2, 3, 4 in each four-char block, we set bit 7 of "sign" if sign + * extension in tmp[1] occurs. Once this flag is set, it remains set. + */ + if (j) + sign |= tmp[1] & 0x80; + if (!*ptr) + ptr = key; + else + ptr++; + } + diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */ + + expanded[i] = tmp[bug]; + initial[i] = BF_init_state.P[i] ^ tmp[bug]; + } + +/* + * At this point, "diff" is zero iff the correct and buggy algorithms produced + * exactly the same result. If so and if "sign" is non-zero, which indicates + * that there was a non-benign sign extension, this means that we have a + * collision between the correctly computed hash for this password and a set of + * passwords that could be supplied to the buggy algorithm. Our safety measure + * is meant to protect from such many-buggy to one-correct collisions, by + * deviating from the correct algorithm in such cases. Let's check for this. + */ + diff |= diff >> 16; /* still zero iff exact match */ + diff &= 0xffff; /* ditto */ + diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */ + sign <<= 9; /* move the non-benign sign extension flag to bit 16 */ + sign &= ~diff & safety; /* action needed? */ + +/* + * If we have determined that we need to deviate from the correct algorithm, + * flip bit 16 in initial expanded key. (The choice of 16 is arbitrary, but + * let's stick to it now. It came out of the approach we used above, and it's + * not any worse than any other choice we could make.) + * + * It is crucial that we don't do the same to the expanded key used in the main + * Eksblowfish loop. By doing it to only one of these two, we deviate from a + * state that could be directly specified by a password to the buggy algorithm + * (and to the fully correct one as well, but that's a side-effect). + */ + initial[0] ^= sign; +} + +static const unsigned char flags_by_subtype[26] = + {2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0}; + +static char *BF_crypt(const char *key, const char *setting, + char *output, int size, + BF_word min) +{ +#if BF_ASM + extern void _BF_body_r(BF_ctx *ctx); +#endif + struct { + BF_ctx ctx; + BF_key expanded_key; + union { + BF_word salt[4]; + BF_word output[6]; + } binary; + } data; + BF_word L, R; + BF_word tmp1, tmp2, tmp3, tmp4; + BF_word *ptr; + BF_word count; + int i; + + if (size < 7 + 22 + 31 + 1) { + __set_errno(ERANGE); + return NULL; + } + + if (setting[0] != '$' || + setting[1] != '2' || + setting[2] < 'a' || setting[2] > 'z' || + !flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a'] || + setting[3] != '$' || + setting[4] < '0' || setting[4] > '3' || + setting[5] < '0' || setting[5] > '9' || + (setting[4] == '3' && setting[5] > '1') || + setting[6] != '$') { + __set_errno(EINVAL); + return NULL; + } + + count = (BF_word)1 << ((setting[4] - '0') * 10 + (setting[5] - '0')); + if (count < min || BF_decode(data.binary.salt, &setting[7], 16)) { + __set_errno(EINVAL); + return NULL; + } + BF_swap(data.binary.salt, 4); + + BF_set_key(key, data.expanded_key, data.ctx.P, + flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a']); + + memcpy(data.ctx.S, BF_init_state.S, sizeof(data.ctx.S)); + + L = R = 0; + for (i = 0; i < BF_N + 2; i += 2) { + L ^= data.binary.salt[i & 2]; + R ^= data.binary.salt[(i & 2) + 1]; + BF_ENCRYPT; + data.ctx.P[i] = L; + data.ctx.P[i + 1] = R; + } + + ptr = data.ctx.S[0]; + do { + ptr += 4; + L ^= data.binary.salt[(BF_N + 2) & 3]; + R ^= data.binary.salt[(BF_N + 3) & 3]; + BF_ENCRYPT; + *(ptr - 4) = L; + *(ptr - 3) = R; + + L ^= data.binary.salt[(BF_N + 4) & 3]; + R ^= data.binary.salt[(BF_N + 5) & 3]; + BF_ENCRYPT; + *(ptr - 2) = L; + *(ptr - 1) = R; + } while (ptr < &data.ctx.S[3][0xFF]); + + do { + int done; + + for (i = 0; i < BF_N + 2; i += 2) { + data.ctx.P[i] ^= data.expanded_key[i]; + data.ctx.P[i + 1] ^= data.expanded_key[i + 1]; + } + + done = 0; + do { + BF_body(); + if (done) + break; + done = 1; + + tmp1 = data.binary.salt[0]; + tmp2 = data.binary.salt[1]; + tmp3 = data.binary.salt[2]; + tmp4 = data.binary.salt[3]; + for (i = 0; i < BF_N; i += 4) { + data.ctx.P[i] ^= tmp1; + data.ctx.P[i + 1] ^= tmp2; + data.ctx.P[i + 2] ^= tmp3; + data.ctx.P[i + 3] ^= tmp4; + } + data.ctx.P[16] ^= tmp1; + data.ctx.P[17] ^= tmp2; + } while (1); + } while (--count); + + for (i = 0; i < 6; i += 2) { + L = BF_magic_w[i]; + R = BF_magic_w[i + 1]; + + count = 64; + do { + BF_ENCRYPT; + } while (--count); + + data.binary.output[i] = L; + data.binary.output[i + 1] = R; + } + + memcpy(output, setting, 7 + 22 - 1); + output[7 + 22 - 1] = BF_itoa64[(int) + BF_atoi64[(int)setting[7 + 22 - 1] - 0x20] & 0x30]; + +/* This has to be bug-compatible with the original implementation, so + * only encode 23 of the 24 bytes. :-) */ + BF_swap(data.binary.output, 6); + BF_encode(&output[7 + 22], data.binary.output, 23); + output[7 + 22 + 31] = '\0'; + + return output; +} + +int _crypt_output_magic(const char *setting, char *output, int size) +{ + if (size < 3) + return -1; + + output[0] = '*'; + output[1] = '0'; + output[2] = '\0'; + + if (setting[0] == '*' && setting[1] == '0') + output[1] = '1'; + + return 0; +} + +/* + * Please preserve the runtime self-test. It serves two purposes at once: + * + * 1. We really can't afford the risk of producing incompatible hashes e.g. + * when there's something like gcc bug 26587 again, whereas an application or + * library integrating this code might not also integrate our external tests or + * it might not run them after every build. Even if it does, the miscompile + * might only occur on the production build, but not on a testing build (such + * as because of different optimization settings). It is painful to recover + * from incorrectly-computed hashes - merely fixing whatever broke is not + * enough. Thus, a proactive measure like this self-test is needed. + * + * 2. We don't want to leave sensitive data from our actual password hash + * computation on the stack or in registers. Previous revisions of the code + * would do explicit cleanups, but simply running the self-test after hash + * computation is more reliable. + * + * The performance cost of this quick self-test is around 0.6% at the "$2a$08" + * setting. + */ +char *_crypt_blowfish_rn(const char *key, const char *setting, + char *output, int size) +{ + const char *test_key = "8b \xd0\xc1\xd2\xcf\xcc\xd8"; + const char *test_setting = "$2a$00$abcdefghijklmnopqrstuu"; + static const char * const test_hashes[2] = + {"i1D709vfamulimlGcq0qq3UvuUasvEa\0\x55", /* 'a', 'b', 'y' */ + "VUrPmXD6q/nVSSp7pNDhCR9071IfIRe\0\x55"}; /* 'x' */ + const char *test_hash = test_hashes[0]; + char *retval; + const char *p; + int save_errno, ok; + struct { + char s[7 + 22 + 1]; + char o[7 + 22 + 31 + 1 + 1 + 1]; + } buf; + +/* Hash the supplied password */ + _crypt_output_magic(setting, output, size); + retval = BF_crypt(key, setting, output, size, 16); + save_errno = errno; + +/* + * Do a quick self-test. It is important that we make both calls to BF_crypt() + * from the same scope such that they likely use the same stack locations, + * which makes the second call overwrite the first call's sensitive data on the + * stack and makes it more likely that any alignment related issues would be + * detected by the self-test. + */ + memcpy(buf.s, test_setting, sizeof(buf.s)); + if (retval) { + unsigned int flags = flags_by_subtype[ + (unsigned int)(unsigned char)setting[2] - 'a']; + test_hash = test_hashes[flags & 1]; + buf.s[2] = setting[2]; + } + memset(buf.o, 0x55, sizeof(buf.o)); + buf.o[sizeof(buf.o) - 1] = 0; + p = BF_crypt(test_key, buf.s, buf.o, sizeof(buf.o) - (1 + 1), 1); + + ok = (p == buf.o && + !memcmp(p, buf.s, 7 + 22) && + !memcmp(p + (7 + 22), test_hash, 31 + 1 + 1 + 1)); + + { + const char *k = "\xff\xa3" "34" "\xff\xff\xff\xa3" "345"; + BF_key ae, ai, ye, yi; + BF_set_key(k, ae, ai, 2); /* $2a$ */ + BF_set_key(k, ye, yi, 4); /* $2y$ */ + ai[0] ^= 0x10000; /* undo the safety (for comparison) */ + ok = ok && ai[0] == 0xdb9c59bc && ye[17] == 0x33343500 && + !memcmp(ae, ye, sizeof(ae)) && + !memcmp(ai, yi, sizeof(ai)); + } + + __set_errno(save_errno); + if (ok) + return retval; + +/* Should not happen */ + _crypt_output_magic(setting, output, size); + __set_errno(EINVAL); /* pretend we don't support this hash type */ + return NULL; +} + +char *_crypt_gensalt_blowfish_rn(const char *prefix, unsigned long count, + const char *input, int size, char *output, int output_size) +{ + if (size < 16 || output_size < 7 + 22 + 1 || + (count && (count < 4 || count > 31)) || + prefix[0] != '$' || prefix[1] != '2' || + (prefix[2] != 'a' && prefix[2] != 'b' && prefix[2] != 'y')) { + if (output_size > 0) output[0] = '\0'; + __set_errno((output_size < 7 + 22 + 1) ? ERANGE : EINVAL); + return NULL; + } + + if (!count) count = 5; + + output[0] = '$'; + output[1] = '2'; + output[2] = prefix[2]; + output[3] = '$'; + output[4] = '0' + count / 10; + output[5] = '0' + count % 10; + output[6] = '$'; + + BF_encode(&output[7], (const BF_word *)input, 16); + output[7 + 22] = '\0'; + + return output; +} diff --git a/shared/libraries/opcua/opcuashared/src/cmake_globals.h.template b/shared/libraries/opcua/opcuashared/src/cmake_globals.h.template new file mode 100644 index 0000000..9a85e6a --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/cmake_globals.h.template @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +class CmakeGlobals +{ +public: + static const std::string ResDirPath; + + static std::string ResDirFile(std::string file) + { + return ResDirPath + "/" + file; + } +}; + +const std::string CmakeGlobals::ResDirPath = "@RES_DIR_PATH@/"; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuadatatype.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuadatatype.cpp new file mode 100644 index 0000000..e10462c --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuadatatype.cpp @@ -0,0 +1,14 @@ +#include "opcuashared/node/opcuadatatype.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaDataType::OpcUaDataType(const UA_ReferenceDescription& uaNodeDescription) + : OpcUaNode(uaNodeDescription, OpcUaNodeClass::DataType) +{ +} + +OpcUaDataType::~OpcUaDataType() +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuanode.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuanode.cpp new file mode 100644 index 0000000..6ba5c28 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuanode.cpp @@ -0,0 +1,86 @@ +#include "opcuashared/node/opcuanode.h" + +#include +#include "opcuashared/node/opcuatype.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNode::OpcUaNode(const OpcUaNodeId& nodeId, OpcUaNodeClass nodeClass) + : nodeId(std::move(nodeId)) + , nodeClass(nodeClass) +{ +} +OpcUaNode::OpcUaNode(const UA_ReferenceDescription& uaNodeDescription, OpcUaNodeClass nodeClass) + : nodeId(uaNodeDescription.nodeId.nodeId) + , browseName(OpcUaNode::GetBrowseName(uaNodeDescription.browseName)) + , displayName(utils::ToStdString(uaNodeDescription.displayName.text)) + , nodeClass(nodeClass) + , typeId(uaNodeDescription.typeDefinition.nodeId) +{ +} + +OpcUaNode::~OpcUaNode() +{ +} + +const OpcUaNodeClass& OpcUaNode::getNodeClass() const +{ + return nodeClass; +} + +void OpcUaNode::setNodeClass(const UA_NodeClass& nodeClass) +{ + setNodeClass((OpcUaNodeClass) nodeClass); +} + +void OpcUaNode::setNodeClass(OpcUaNodeClass nodeClass) +{ + this->nodeClass = nodeClass; +} + +const std::string& OpcUaNode::getBrowseName() const +{ + return browseName; +} + +void OpcUaNode::setBrowseName(const std::string& browseName) +{ + this->browseName = browseName; +} + +const std::string& OpcUaNode::getDisplayName() const +{ + return displayName; +} + +void OpcUaNode::setDisplayName(const std::string& displayName) +{ + this->displayName = displayName; +} + +const OpcUaNodeId& OpcUaNode::getNodeId() const +{ + return nodeId; +} + +std::string OpcUaNode::GetBrowseName(const UA_QualifiedName& browseName) +{ + return utils::ToStdString(browseName.name); +} + +void OpcUaNode::setType(const OpcUaTypePtr& type) +{ + this->typeId = type->getNodeId(); +} + +void OpcUaNode::setType(const OpcUaNodeId& typeId) +{ + this->typeId = typeId; +} + +const OpcUaNodeId& OpcUaNode::getTypeId() const +{ + return typeId; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuanodemethod.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuanodemethod.cpp new file mode 100644 index 0000000..80106d8 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuanodemethod.cpp @@ -0,0 +1,64 @@ +#include "opcuashared/node/opcuanodemethod.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNodeMethod::OpcUaNodeMethod(const OpcUaNodeId& uaNode, const OpcUaNodeId& parentNodeId) + : OpcUaNode(uaNode, OpcUaNodeClass::Method) + , parentNodeId(parentNodeId) +{ +} + +OpcUaNodeMethod::OpcUaNodeMethod(const UA_ReferenceDescription& uaNodeDescription, const OpcUaNodeId& parentNodeId) + : OpcUaNode(uaNodeDescription, OpcUaNodeClass::Method) + , parentNodeId(parentNodeId) +{ +} + +OpcUaNodeMethod::~OpcUaNodeMethod() +{ +} + +std::string OpcUaNodeMethod::getTypeDescription() +{ + return typeDescription; +} + +void OpcUaNodeMethod::initTypeDescription() +{ + bool inputEnabled = inputParameters.size(); + bool outputEnabled = outputParameters.size(); + + std::string type; + + if (inputEnabled && outputEnabled) + type = " (in/out)"; + else if (inputEnabled && !outputEnabled) + type = " (in)"; + else if (!inputEnabled && outputEnabled) + type = " (out)"; + + typeDescription = "Method" + type; +} + +const OpcUaNodeId& OpcUaNodeMethod::getParentNodeId() const +{ + return parentNodeId; +} + +void OpcUaNodeMethod::addInputParameter(const std::string& name, const OpcUaNodeId& dataTypeId) +{ + OpcUaChannelMethodParameter param; + param.setName(name); + param.setDataTypeId(dataTypeId); + inputParameters.push_back(param); +} + +void OpcUaNodeMethod::addOutputParameter(const std::string& name, const OpcUaNodeId& dataTypeId) +{ + OpcUaChannelMethodParameter param; + param.setName(name); + param.setDataTypeId(dataTypeId); + outputParameters.push_back(param); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuanodeobject.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuanodeobject.cpp new file mode 100644 index 0000000..64d5e43 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuanodeobject.cpp @@ -0,0 +1,30 @@ +#include "opcuashared/node/opcuanodeobject.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNodeObject::OpcUaNodeObject(const OpcUaNodeId& uaNode) + : OpcUaNode(uaNode, OpcUaNodeClass::Object) +{ +} + +OpcUaNodeObject::OpcUaNodeObject(const UA_ReferenceDescription& uaNodeDescription) + : OpcUaNode(uaNodeDescription, OpcUaNodeClass::Object) +{ +} + +OpcUaNodeObject::~OpcUaNodeObject() +{ +} + +OpcUaNodeObjectPtr OpcUaNodeObject::instantiateRoot() +{ + return std::make_shared(OPCUANODEID_ROOTFOLDER); +} + +OpcUaNodeObjectPtr OpcUaNodeObject::instantiateObjectsFolder() +{ + return std::make_shared(OPCUANODEID_OBJECTSFOLDER); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuanodevariable.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuanodevariable.cpp new file mode 100644 index 0000000..60229cd --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuanodevariable.cpp @@ -0,0 +1,61 @@ +#include "opcuashared/node/opcuanodevariable.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNodeVariable::OpcUaNodeVariable(const OpcUaNodeId& uaNode, const UA_DataType& uaDataType, size_t dimension) + : OpcUaNodeVariable(uaNode, uaDataType.typeId, dimension) +{ +} + +OpcUaNodeVariable::OpcUaNodeVariable(const OpcUaNodeId& uaNode, const OpcUaNodeId& dataTypeNodeId, size_t dimension) + : OpcUaNode(uaNode, OpcUaNodeClass::Variable) + , dataTypeNodeId(dataTypeNodeId) + , dimension(dimension) +{ +} + +OpcUaNodeVariable::OpcUaNodeVariable(const UA_ReferenceDescription& uaNodeDescription, const UA_DataType& uaDataType, size_t dimension) + : OpcUaNodeVariable(uaNodeDescription, uaDataType.typeId, dimension) +{ +} + +OpcUaNodeVariable::OpcUaNodeVariable(const UA_ReferenceDescription& uaNodeDescription, const OpcUaNodeId& dataTypeNodeId, size_t dimension) + : OpcUaNode(uaNodeDescription, OpcUaNodeClass::Variable) + , dataTypeNodeId(dataTypeNodeId) + , dimension(dimension) +{ +} + +OpcUaNodeVariable::~OpcUaNodeVariable() +{ +} + +const OpcUaNodeId OpcUaNodeVariable::getDataTypeNodeId() const +{ + return dataTypeNodeId; +} + +OpcUaVariantPtr OpcUaNodeVariable::getVariant() +{ + return variant; +} + +void OpcUaNodeVariable::setVariant(const UA_Variant& value) +{ + if (!variant) + variant = std::make_shared(value); + else + variant->setValue(value); +} + +size_t OpcUaNodeVariable::getDimension() +{ + return dimension; +} + +void OpcUaNodeVariable::setDimension(size_t dimension) +{ + this->dimension = dimension; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuaobjecttype.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuaobjecttype.cpp new file mode 100644 index 0000000..c5c15b7 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuaobjecttype.cpp @@ -0,0 +1,14 @@ +#include "opcuashared/node/opcuaobjecttype.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaObjectType::OpcUaObjectType(const OpcUaNodeId& typeId) + : OpcUaType(typeId, OpcUaNodeClass::ObjectType) +{ +} + +OpcUaObjectType::~OpcUaObjectType() +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuatype.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuatype.cpp new file mode 100644 index 0000000..cbd0d79 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuatype.cpp @@ -0,0 +1,12 @@ +#include "opcuashared/node/opcuatype.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaType::OpcUaType(const OpcUaNodeId& typeId, OpcUaNodeClass nodeClass) + : OpcUaNode(typeId, nodeClass) +{ + assert(!typeId.isNull()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/node/opcuavariabletype.cpp b/shared/libraries/opcua/opcuashared/src/node/opcuavariabletype.cpp new file mode 100644 index 0000000..2531718 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/node/opcuavariabletype.cpp @@ -0,0 +1,14 @@ +#include "opcuashared/node/opcuavariabletype.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaVariableType::OpcUaVariableType(const OpcUaNodeId& typeId) + : OpcUaType(typeId, OpcUaNodeClass::VariableType) +{ +} + +OpcUaVariableType::~OpcUaVariableType() +{ +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuacallmethodresult.cpp b/shared/libraries/opcua/opcuashared/src/opcuacallmethodresult.cpp new file mode 100644 index 0000000..00711bd --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuacallmethodresult.cpp @@ -0,0 +1,46 @@ +#include "opcuashared/opcuacallmethodresult.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaCallMethodResult::OpcUaCallMethodResult(const UA_CallMethodResult& callMethodResult) + : callMethodResult(callMethodResult) +{ +} + +OpcUaCallMethodResult::~OpcUaCallMethodResult() +{ +} + +size_t OpcUaCallMethodResult::getOutputArgumentsSize() const +{ + return callMethodResult.outputArgumentsSize; +} + +const UA_StatusCode& OpcUaCallMethodResult::getStatusCode() const +{ + return callMethodResult.statusCode; +} + +bool OpcUaCallMethodResult::isStatusOK() const +{ + return (getStatusCode() == UA_STATUSCODE_GOOD); +} + +OpcUaVariant OpcUaCallMethodResult::getOutputArgument(size_t i) const +{ + if (i < callMethodResult.outputArgumentsSize) + return OpcUaVariant(callMethodResult.outputArguments[i], true); + throw std::out_of_range("index of output argument is out of range"); +} + +const UA_CallMethodResult& OpcUaCallMethodResult::getCallMethodResult() const +{ + return callMethodResult; +} + +OpcUaCallMethodResult::operator const UA_CallMethodResult&() const +{ + return getCallMethodResult(); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuacommon.cpp b/shared/libraries/opcua/opcuashared/src/opcuacommon.cpp new file mode 100644 index 0000000..eb67c61 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuacommon.cpp @@ -0,0 +1,147 @@ +#include +#include +#include "opcuashared/opcuacommon.h" +#include +#include + +#include "open62541/plugin/nodestore.h" + +#ifdef OPCUA_ENABLE_ENCRYPTION +#include +#include +#include +#endif + +using namespace std::chrono; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +namespace utils +{ + double ToSeconds(const UA_DateTime& time) + { + return (double) (time) / (double) UA_DATETIME_SEC; + } + + UA_StatusCode ToUaVariant(double value, const UA_NodeId& dataTypeNodeId, UA_Variant* var) + { + if (dataTypeNodeId.namespaceIndex == 0) + { + auto dataType = UA_findDataType(&dataTypeNodeId); + if (dataType) + { + switch (dataType->typeKind) + { + case UA_TYPES_DOUBLE: + return UA_Variant_setScalarCopy(var, &value, dataType); + case UA_TYPES_FLOAT: + { + float f = (float) value; + return UA_Variant_setScalarCopy(var, &f, dataType); + } + case UA_TYPES_BOOLEAN: + { + bool b = (value != 0); + return UA_Variant_setScalarCopy(var, &b, dataType); + } + case UA_TYPES_INT16: + { + int16_t i16 = (int16_t) std::round(value); + return UA_Variant_setScalarCopy(var, &i16, dataType); + } + case UA_TYPES_UINT16: + { + uint16_t i16 = (uint16_t) std::round(value); + return UA_Variant_setScalarCopy(var, &i16, dataType); + } + case UA_TYPES_INT32: + { + int32_t i32 = (int32_t) std::round(value); + return UA_Variant_setScalarCopy(var, &i32, dataType); + } + case UA_TYPES_UINT32: + { + int32_t i32 = (uint32_t) std::round(value); + return UA_Variant_setScalarCopy(var, &i32, dataType); + } + case UA_TYPES_INT64: + case UA_TYPES_UINT64: + { + int64_t i64 = (int64_t) std::round(value); + return UA_Variant_setScalarCopy(var, &i64, dataType); + } + } + } + } + throw std::runtime_error("C Exception: unsupported value!"); + } + + void ToUaVariant(const std::string& value, const UA_NodeId& dataTypeNodeId, UA_Variant* var) + { + if (dataTypeNodeId.namespaceIndex == 0) + { + auto dataType = UA_findDataType(&dataTypeNodeId); + if (dataType) + { + switch (dataType->typeKind) + { + case UA_TYPES_STRING: + { + UA_String str = UA_STRING((char*) value.c_str()); + UA_Variant_setScalarCopy(var, &str, dataType); + return; + } + case UA_TYPES_LOCALIZEDTEXT: + { + UA_LocalizedText str = UA_LOCALIZEDTEXT((char*) "en_US", (char*) value.c_str()); + UA_Variant_setScalarCopy(var, &str, dataType); + return; + } + default: + return; + } + } + } + throw std::runtime_error("C Exception: unsupported value!"); + } + + DurationTimeStamp GetDurationTimeStamp() + { + return std::chrono::steady_clock::now(); + } + + OpcUaObject LoadFile(const std::string& path) + { + OpcUaObject fileContentsObj = UA_BYTESTRING_NULL; + UA_ByteString& fileContents = fileContentsObj.getValue(); + + /* Open the file */ + FILE* fp = fopen(path.c_str(), "rb"); + if (!fp) + { + errno = 0; /* We read errno also from the tcp layer... */ + throw std::invalid_argument("Can not open file " + path); + } + + /* Get the file length, allocate the data and read */ + fseek(fp, 0, SEEK_END); + fileContents.length = (size_t) ftell(fp); + fileContents.data = (UA_Byte*) UA_malloc(fileContents.length * sizeof(UA_Byte)); + if (fileContents.data) + { + fseek(fp, 0, SEEK_SET); + size_t read = fread(fileContents.data, sizeof(UA_Byte), fileContents.length, fp); + if (read != fileContents.length) + UA_ByteString_clear(&fileContents); + } + else + { + fileContents.length = 0; + } + fclose(fp); + + return fileContentsObj; + } +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuadatatypearraylist.cpp b/shared/libraries/opcua/opcuashared/src/opcuadatatypearraylist.cpp new file mode 100644 index 0000000..defc755 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuadatatypearraylist.cpp @@ -0,0 +1,42 @@ +#include "opcuashared/opcuadatatypearraylist.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaDataTypeArrayList::OpcUaDataTypeArrayList(const OpcUaDataTypeArrayList& copy) + : std::list() +{ + operator=(copy); +} + +OpcUaDataTypeArrayList& OpcUaDataTypeArrayList::operator=(const OpcUaDataTypeArrayList& other) +{ + if (this != &other) + { + clear(); + for (const auto& cur : other) + add(cur.typesSize, cur.types); + } + return *this; +} + +void OpcUaDataTypeArrayList::add(const size_t typesSize, const UA_DataType* types) +{ + UA_DataTypeArray dataTypeArray{nullptr, typesSize, types}; + + const UA_DataTypeArray* nextElement = nullptr; + if (!empty()) + nextElement = &front(); + + UA_DataTypeArray newDataTypeArray = {nextElement, typesSize, types}; + push_front(newDataTypeArray); +} + +const UA_DataTypeArray* OpcUaDataTypeArrayList::getCustomDataTypes() const +{ + if (empty()) + return nullptr; + + return &(*begin()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp new file mode 100644 index 0000000..7665deb --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuadatavalue.cpp @@ -0,0 +1,45 @@ +#include "opcuashared/opcuadatavalue.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaDataValue::OpcUaDataValue(const UA_DataValue* dataValue) + : dataValue(dataValue) + , variant(dataValue->value, true) +{ +} + +OpcUaDataValue::~OpcUaDataValue() +{ +} + +bool OpcUaDataValue::hasValue() const +{ + return dataValue->hasValue; +} + +const OpcUaVariant& OpcUaDataValue::getValue() const +{ + return variant; +} + +const UA_StatusCode& OpcUaDataValue::getStatusCode() const +{ + return dataValue->status; +} + +bool OpcUaDataValue::isStatusOK() const +{ + return (getStatusCode() == UA_STATUSCODE_GOOD); +} + +const UA_DataValue* OpcUaDataValue::getDataValue() const +{ + return dataValue; +} + +OpcUaDataValue::operator const UA_DataValue*() const +{ + return getDataValue(); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuaendpoint.cpp b/shared/libraries/opcua/opcuashared/src/opcuaendpoint.cpp new file mode 100644 index 0000000..3d7e83e --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuaendpoint.cpp @@ -0,0 +1,72 @@ +#include "opcuashared/opcuaendpoint.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaEndpoint::OpcUaEndpoint(const std::string& url) + : url(url) +{ +} + +OpcUaEndpoint::OpcUaEndpoint(const std::string& url, const std::string& username, const std::string& password) + : url(url) + , username(username) + , password(password) +{ +} + +void OpcUaEndpoint::setName(const std::string& name) +{ + this->name = name; +} + +const std::string OpcUaEndpoint::getName() const +{ + return name; +} + +void OpcUaEndpoint::setUrl(const std::string& url) +{ + this->url = url; +} + +const std::string OpcUaEndpoint::getUrl() const +{ + return url; +} + +void OpcUaEndpoint::setUsername(const std::string& username) +{ + this->username = username; +} + +const std::string OpcUaEndpoint::getUsername() const +{ + return username; +} + +void OpcUaEndpoint::setPassword(const std::string& password) +{ + this->password = password; +} + +const std::string OpcUaEndpoint::getPassword() const +{ + return password; +} + +const UA_DataTypeArray* OpcUaEndpoint::getCustomDataTypes()const +{ + return customDataTypeList.getCustomDataTypes(); +} + +bool OpcUaEndpoint::isAnonymous() +{ + return username.empty(); +} + +void OpcUaEndpoint::registerCustomTypes(const size_t typesSize, const UA_DataType* types) +{ + customDataTypeList.add(typesSize, types); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuanodecollection.cpp b/shared/libraries/opcua/opcuashared/src/opcuanodecollection.cpp new file mode 100644 index 0000000..988045f --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuanodecollection.cpp @@ -0,0 +1,29 @@ +#include "opcuashared/opcuanodecollection.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +/* OpcUaNodeCollection */ + +OpcUaNodeCollection OpcUaNodeCollection::selectNodes(OpcUaNodeClass nodeClassMask) +{ + OpcUaNodeCollection rtn; + + for (const auto& item : *this) + { + auto nc = static_cast(item->getNodeClass()); + if ((nc & (UA_UInt32)nodeClassMask) > 0) + rtn.push_back(item); + } + + return rtn; +} + +OpcUaNodePtr OpcUaNodeCollection::locateNode(const OpcUaNodeId& nodeId) const +{ + auto it = std::find_if(cbegin(), cend(), [&nodeId](const OpcUaNodePtr& node) { return node->getNodeId() == nodeId; }); + if (it != cend()) + return *it; + return nullptr; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuanodeid.cpp b/shared/libraries/opcua/opcuashared/src/opcuanodeid.cpp new file mode 100644 index 0000000..4fb9ccb --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuanodeid.cpp @@ -0,0 +1,137 @@ +#include "opcuashared/opcuanodeid.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaNodeId::OpcUaNodeId() + : OpcUaObject(UA_NODEID_NULL) +{ +} + +OpcUaNodeId::OpcUaNodeId(uint32_t identifier) + : OpcUaNodeId(0, identifier) +{ +} + +OpcUaNodeId::OpcUaNodeId(uint16_t namespaceIndex, const char* identifier) + : OpcUaObject(UA_NODEID_STRING_ALLOC(namespaceIndex, identifier)) +{ +} + +OpcUaNodeId::OpcUaNodeId(uint16_t namespaceIndex, const std::string& identifier) + : OpcUaNodeId(namespaceIndex, identifier.c_str()) +{ +} + +OpcUaNodeId::OpcUaNodeId(uint16_t namespaceIndex, uint32_t identifier) + : OpcUaObject(UA_NODEID_NUMERIC(namespaceIndex, identifier)) +{ +} + +const UA_NodeId* OpcUaNodeId::getPtr() const noexcept +{ + return &value; +} + +UA_NodeId* OpcUaNodeId::getPtr() noexcept +{ + return &value; +} + +uint16_t OpcUaNodeId::getNamespaceIndex() const noexcept +{ + return value.namespaceIndex; +} + +uint32_t OpcUaNodeId::getIdentifierNumeric() const +{ + return value.identifier.numeric; +} + +bool OpcUaNodeId::isNull() const noexcept +{ + return UA_NodeId_isNull(&value); +} + +OpcUaIdentifierUniversal OpcUaNodeId::getIdentifier() const +{ + return OpcUaNodeId::getIdentifier(value); +} + +OpcUaIdentifierType OpcUaNodeId::getIdentifierType() const +{ + return OpcUaNodeId::getIdentifierType(value.identifierType); +} + +std::string OpcUaNodeId::toString() const +{ + std::stringstream ss; + ss << "("; + ss << getNamespaceIndex(); + ss << ", "; + ss << getIdentifier(); + ss << ")"; + return ss.str(); +} + +OpcUaNodeId OpcUaNodeId::instantiateNode(uint16_t namespaceIndex, + OpcUaIdentifierUniversal identifierUniversal, + OpcUaIdentifierType identifierType) +{ + switch (identifierType) + { + case OpcUaIdentifierType::Numeric: + { + uint32_t id; + UA_readNumber((uint8_t*)identifierUniversal.c_str(), identifierUniversal.size(), &id); + return OpcUaNodeId(namespaceIndex, id); + } + case OpcUaIdentifierType::String: + return OpcUaNodeId(namespaceIndex, identifierUniversal); + default: + throw std::runtime_error("Unsupported OpcUaIdentifierType!"); + } +} + +OpcUaIdentifierUniversal OpcUaNodeId::getIdentifier(const UA_NodeId& uaNodeId) +{ + switch (getIdentifierType(uaNodeId.identifierType)) + { + case OpcUaIdentifierType::Numeric: + return std::to_string(uaNodeId.identifier.numeric); + case OpcUaIdentifierType::String: + return utils::ToStdString(uaNodeId.identifier.string); + case OpcUaIdentifierType::Guid: + return utils::GuidToString(uaNodeId.identifier.guid); + default: + throw std::runtime_error("C Exception: unsupported identifier type!"); + }; +} + +OpcUaIdentifierType OpcUaNodeId::getIdentifierType(const UA_NodeIdType& identifierType) +{ + switch (identifierType) + { + case UA_NODEIDTYPE_NUMERIC: + return OpcUaIdentifierType::Numeric; + case UA_NODEIDTYPE_STRING: + return OpcUaIdentifierType::String; + case UA_NODEIDTYPE_GUID: + return OpcUaIdentifierType::Guid; + case UA_NODEIDTYPE_BYTESTRING: + default: + return OpcUaIdentifierType::Undefined; + }; +} + +void OpcUaNodeId::SetRandomSeed() +{ + UA_random_seed((UA_UInt64) UA_DateTime_now()); +} + +OpcUaNodeId OpcUaNodeId::CreateWithRandomGuid() +{ + return OpcUaNodeId(UA_NODEID_GUID(1, UA_Guid_random())); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuasecurity_config.cpp b/shared/libraries/opcua/opcuashared/src/opcuasecurity_config.cpp new file mode 100644 index 0000000..1391002 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuasecurity_config.cpp @@ -0,0 +1,84 @@ +#include "opcuashared/opcua.h" +#include "opcuashared/opcuasecurity_config.h" + +#include "opcuashared/opcuasecuritycommon.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +OpcUaSecurityConfig::OpcUaSecurityConfig() + : certificate(UA_BYTESTRING_NULL) + , privateKey(UA_BYTESTRING_NULL) +{ +} + +OpcUaSecurityConfig::OpcUaSecurityConfig(const OpcUaSecurityConfig& config) +{ + operator=(config); +} + +OpcUaSecurityConfig& OpcUaSecurityConfig::operator=(const OpcUaSecurityConfig& config) +{ + if (this == &config) + return *this; + + appUri = config.appUri; + securityMode = config.securityMode; + certificate = config.certificate; + privateKey = config.privateKey; + trustList = config.trustList; + revocationList = config.revocationList; + trustAll = config.trustAll; + + return *this; +} + +void OpcUaSecurityConfig::validate() const +{ + if (securityMode == UA_MESSAGESECURITYMODE_INVALID) + throw OpcUaException(UA_STATUSCODE_BADSECURITYCHECKSFAILED, "Invalid security mode."); + + if (securityMode == UA_MESSAGESECURITYMODE_SIGN || securityMode == UA_MESSAGESECURITYMODE_SIGNANDENCRYPT) + { + if (!hasCertificate()) + throw OpcUaException(UA_STATUSCODE_BADSECURITYCHECKSFAILED, "Certificate is not set."); + + if (!hasPrivateKey()) + throw OpcUaException(UA_STATUSCODE_BADSECURITYCHECKSFAILED, "Private key not set."); + } +} + +bool OpcUaSecurityConfig::hasCertificate() const +{ + return certificate.getValue().data != NULL; +} + +bool OpcUaSecurityConfig::hasPrivateKey() const +{ + return privateKey.getValue().data != NULL; +} + +std::optional OpcUaSecurityConfig::getAppUriOrParseFromCertificate() const +{ + std::optional appUri; + if (appUri.has_value()) + appUri = appUri.value(); + else if (this->hasCertificate()) + appUri = OpcUaSecurityCommon::parseCertificateUri(certificate.getValue()); + return appUri; +} + +OpcUaServerSecurityConfig::OpcUaServerSecurityConfig() + : OpcUaSecurityConfig() +{ + authenticateUser = [](bool isAnonymous, std::string username, std::string password) + { + return UA_STATUSCODE_GOOD; + }; +} + +bool OpcUaClientSecurityConfig::isAnonymous() const +{ + return !(username.has_value() && password.has_value()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuasecuritycommon.cpp b/shared/libraries/opcua/opcuashared/src/opcuasecuritycommon.cpp new file mode 100644 index 0000000..b54c1a5 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuasecuritycommon.cpp @@ -0,0 +1,62 @@ +#include "opcuashared/opcuasecuritycommon.h" +#include "opcuashared/opcuacommon.h" + +#ifdef OPCUA_ENABLE_ENCRYPTION +#include +#include +#include +#endif + +using namespace std::chrono; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +std::optional OpcUaSecurityCommon::parseCertificateUri(const UA_ByteString& certificate) +{ + std::optional subjectUri; + +#ifndef OPCUA_ENABLE_ENCRYPTION + throw OpcUaException(UA_STATUSCODE_BADINTERNALERROR, "Encryption was not enabled when building the project."); +#else + if (certificate.data == NULL) + return subjectUri; + + const unsigned char* pData = certificate.data; + X509* certificateX509 = d2i_X509(NULL, &pData, (long) certificate.length); + if (certificateX509 == NULL) + return subjectUri; + + GENERAL_NAMES* names = (GENERAL_NAMES*) X509_get_ext_d2i(certificateX509, NID_subject_alt_name, NULL, NULL); + if (names == NULL) + { + X509_free(certificateX509); + return subjectUri; + } + + int namesCount = sk_GENERAL_NAME_num(names); + for (int i = 0; i < namesCount; i++) + { + GENERAL_NAME* name = sk_GENERAL_NAME_value(names, i); + if (name->type == GEN_URI) + { + size_t len = name->d.ia5->length; + void* data = name->d.ia5->data; + if (data != NULL) + subjectUri = std::string((const char*) data, len); + break; + } + } + + X509_free(certificateX509); + sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); +#endif + + return subjectUri; +} + +UA_StatusCode OpcUaSecurityCommon::verifyCertificateRejectAll(void* verificationContext, const UA_ByteString* certificate) +{ + return UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp b/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp new file mode 100644 index 0000000..de70be7 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuavariant.cpp @@ -0,0 +1,232 @@ +#include "opcuashared/opcuavariant.h" +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using namespace daq::opcua::utils; + +OpcUaVariant::OpcUaVariant() + : OpcUaObject() +{ +} + +OpcUaVariant::OpcUaVariant(const uint16_t& value) +{ + UA_Variant_setScalarCopy(&this->value, &value, &UA_TYPES[UA_TYPES_UINT16]); +} + +OpcUaVariant::OpcUaVariant(const uint32_t& value) +{ + UA_Variant_setScalarCopy(&this->value, &value, &UA_TYPES[UA_TYPES_UINT32]); +} + +OpcUaVariant::OpcUaVariant(const int32_t& value) + : OpcUaVariant() +{ + UA_Variant_setScalarCopy(&this->value, &value, &UA_TYPES[UA_TYPES_INT32]); +} + +OpcUaVariant::OpcUaVariant(const int64_t& value) + : OpcUaVariant() +{ + UA_Variant_setScalarCopy(&this->value, &value, &UA_TYPES[UA_TYPES_INT64]); +} + +OpcUaVariant::OpcUaVariant(const char* value) + : OpcUaVariant() +{ + UA_String* newString = UA_String_new(); + *newString = UA_STRING_ALLOC(value); + UA_Variant_setScalar(&this->value, newString, &UA_TYPES[UA_TYPES_STRING]); +} + +OpcUaVariant::OpcUaVariant(const double& value) + : OpcUaVariant() +{ + UA_Variant_setScalarCopy(&this->value, &value, &UA_TYPES[UA_TYPES_DOUBLE]); +} + +OpcUaVariant::OpcUaVariant(const bool& value) + : OpcUaVariant() +{ + UA_Variant_setScalarCopy(&this->value, &value, &UA_TYPES[UA_TYPES_BOOLEAN]); +} + +OpcUaVariant::OpcUaVariant(const OpcUaNodeId& value) + : OpcUaVariant() +{ + UA_Variant_setScalarCopy(&this->value, value.getPtr(), &UA_TYPES[UA_TYPES_NODEID]); +} + +OpcUaVariant::OpcUaVariant(const UA_DataType* type, size_t dimension) + : OpcUaVariant() +{ + value.type = type; + if (dimension > 1) + { + value.arrayLength = dimension; + value.arrayDimensions = static_cast(UA_Array_new(1, type)); + value.arrayDimensions[0] = UA_UInt32(dimension); + value.arrayDimensionsSize = 1; + } +} + +OpcUaVariant::OpcUaVariant(const double& genericValue, const UA_DataType& originalType) + : OpcUaVariant() +{ + UA_StatusCode status = ToUaVariant(genericValue, originalType.typeId, &this->value); + if (status != UA_STATUSCODE_GOOD) + throw OpcUaVariableConversionError(status); +} + + +void OpcUaVariant::setValue(UA_Variant&& value) +{ + OpcUaObject::setValue(std::move(value)); +} + +void OpcUaVariant::setValue(const UA_Variant& value, bool shallowCopy) +{ + OpcUaObject::setValue(value, shallowCopy); +} + +bool OpcUaVariant::isInteger() const +{ + return OpcUaVariant::IsInteger(this->value); +} + +bool OpcUaVariant::isString() const +{ + return VariantUtils::HasScalarType(value) || + VariantUtils::HasScalarType(value); +} + +bool OpcUaVariant::isDouble() const +{ + return VariantUtils::HasScalarType(value); +} + +bool OpcUaVariant::isBool() const +{ + return VariantUtils::HasScalarType(value); +} + +bool OpcUaVariant::isNodeId() const +{ + return VariantUtils::HasScalarType(value); +} + +bool OpcUaVariant::isNull() const +{ + return UA_Variant_isEmpty(&value); +} + +bool OpcUaVariant::isReal() const +{ + if (value.type == NULL) + return false; + + switch (value.type->typeKind) + { + case UA_TYPES_FLOAT: + case UA_TYPES_DOUBLE: + return true; + default: + return false; + } +} + +bool OpcUaVariant::isNumber() const +{ + return isInteger() || isReal(); +} + +bool OpcUaVariant::IsInteger(const UA_Variant& value) +{ + if (value.type && value.type->typeId.namespaceIndex == 0) // built-in types + { + switch (value.type->typeKind) + { + case UA_TYPES_SBYTE: + case UA_TYPES_BYTE: + case UA_TYPES_INT16: + case UA_TYPES_UINT16: + case UA_TYPES_INT32: + case UA_TYPES_UINT32: + case UA_TYPES_INT64: + case UA_TYPES_UINT64: + return true; + default: + return false; + } + } + return false; +} + +std::string OpcUaVariant::toString() const +{ + if (isType()) + { + UA_LocalizedText localizedText = readScalar(); + return ToStdString(localizedText.text); + } + + if (isType()) + { + UA_QualifiedName localizedText = readScalar(); + return ToStdString(localizedText.name); + } + + UA_String str = readScalar(); + return ToStdString(str); +} + +int64_t OpcUaVariant::toInteger() const +{ + return VariantUtils::ToNumber(this->value); +} + +double OpcUaVariant::toDouble() const +{ + return readScalar(); +} + +float OpcUaVariant::toFloat() const +{ + return readScalar(); +} + +bool OpcUaVariant::toBool() const +{ + return readScalar(); +} + +OpcUaNodeId OpcUaVariant::toNodeId() const +{ + return VariantUtils::ToNodeId(this->value); +} + +// VariantUtils + +void VariantUtils::ToInt32Variant(OpcUaVariant& variant) +{ + if (!variant.isNumber()) + throw OpcUaException(UA_STATUSCODE_BADTYPEMISMATCH, "Variant does not contain a numeric type."); + + UA_Int32 value = (UA_Int32) variant.toInteger(); + variant.setScalar(value); +} + +void VariantUtils::ToInt64Variant(OpcUaVariant& variant) +{ + if (!variant.isNumber()) + throw OpcUaException(UA_STATUSCODE_BADTYPEMISMATCH, "Variant does not contain a numeric type."); + + UA_Int64 value = (UA_Int64) variant.toInteger(); + variant.setScalar(value); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/src/opcuaversion.cpp b/shared/libraries/opcua/opcuashared/src/opcuaversion.cpp new file mode 100644 index 0000000..e11d941 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/src/opcuaversion.cpp @@ -0,0 +1,39 @@ +#include "opcuashared/opcuaversion.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +#pragma push_macro("major") +#pragma push_macro("minor") + +#undef major +#undef minor + +OpcUaVersion::OpcUaVersion(const char* version) +{ + std::sscanf(version, "%d.%d.%d", &major, &minor, &patch); +} + +std::string OpcUaVersion::toString() const +{ + char buffer[36]; + snprintf(buffer, sizeof(buffer), "%d.%d.%d", major, minor, patch); + return buffer; +} + +bool OpcUaVersion::Compatible(const OpcUaVersion& serverVersion, const OpcUaVersion& systemVersion) +{ + return serverVersion.major == systemVersion.major && serverVersion.minor <= systemVersion.minor; +} + +bool OpcUaVersion::HasFeature(const OpcUaVersion& serverVersion, const OpcUaVersion& featureVersion) +{ + if (serverVersion.major > featureVersion.major) + return true; + + return serverVersion.major == featureVersion.major && featureVersion.minor <= serverVersion.minor; +} + +#pragma pop_macro("minor") +#pragma pop_macro("major") + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/CMakeLists.txt b/shared/libraries/opcua/opcuashared/tests/CMakeLists.txt new file mode 100644 index 0000000..4980b7d --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/CMakeLists.txt @@ -0,0 +1,37 @@ +set(MODULE_NAME opcuashared) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES + main.cpp + test_string_utils.cpp + test_status_code.cpp + test_opcuaendpoint.cpp + test_opcuanodeid.cpp + test_opcua_variant.cpp + test_opcua_object.cpp + test_opcuadatavalue.cpp + test_opcuacallmethodresult.cpp + test_opcuaversion.cpp + test_opcua_vector.cpp + test_opcuadatatypearraylist.cpp + test_bcrypt.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES}) + +target_link_libraries(${TEST_APP} PRIVATE ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::test_utils +) + +if (MSVC) + target_compile_definitions(${TEST_APP} PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}coverage ${TEST_APP} ${MODULE_NAME}coverage) +endif() diff --git a/shared/libraries/opcua/opcuashared/tests/main.cpp b/shared/libraries/opcua/opcuashared/tests/main.cpp new file mode 100644 index 0000000..a4ca297 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/main.cpp @@ -0,0 +1,13 @@ +#include +#include + +int main(int argc, char** args) +{ + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new MemCheckListener()); + + int res = RUN_ALL_TESTS(); + return res; +} diff --git a/shared/libraries/opcua/opcuashared/tests/test_bcrypt.cpp b/shared/libraries/opcua/opcuashared/tests/test_bcrypt.cpp new file mode 100644 index 0000000..8b1574f --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_bcrypt.cpp @@ -0,0 +1,115 @@ +#include +#include + +using namespace daq::opcua; + +/** + * Test BCrypt class. + * + * One can generate more test cases with a help of the php script: commonlib/tests/tools/bcrypt_test_cases.php + */ + +struct BCryptTestCase +{ + std::string password; + std::string salt; + std::string hash; + int rounds; + + BCryptTestCase(std::string password, std::string salt, std::string hash, int rounds) + : password(std::move(password)) + , salt(std::move(salt)) + , hash(std::move(hash)) + , rounds(rounds) + { + } +}; + +static bool compareHashes(const std::string& a, const std::string& b) +{ + // we should ignore bcrypt version when comparing hashes + return a[0] == b[0] && a[1] == b[1] && strcmp(&a[3], &b[3]) == 0; +} + +class BCryptTest : public testing::Test, public ::testing::WithParamInterface +{ +protected: + BCrypt bcrypt; +}; + + +class BCryptTestHash : public BCryptTest +{ +}; + +class BCryptTestVerify : public BCryptTest +{ +}; + +TEST_P(BCryptTestHash, HashTest) +{ + BCryptTestCase testCase = GetParam(); + std::string hash = bcrypt.hash(testCase.password, testCase.salt, testCase.rounds); + ASSERT_TRUE(compareHashes(hash, testCase.hash)); +} + +TEST_P(BCryptTestVerify, VerifyTest) +{ + BCryptTestCase testCase = GetParam(); + ASSERT_TRUE(BCrypt::Verify(testCase.hash, testCase.password)); + ASSERT_FALSE(BCrypt::Verify(testCase.hash, testCase.password + "wrong")); +} + +INSTANTIATE_TEST_SUITE_P( + BCryptTestHashAll, + BCryptTestHash, + ::testing::Values( + BCryptTestCase("", "thisisasaltwhichis22ch", "$2y$06$thisisasaltwhichis22ceiWyJJQuCqQ6K/VMVNgl3WR2TnKke93O", 6), + BCryptTestCase("", "thisisasaltwhichis22ch", "$2y$08$thisisasaltwhichis22cetIGsXYPjot9MEiTlOQSjSqsUhO/ncum", 8), + BCryptTestCase("hello", "thisisasaltwhichis22ch", "$2y$06$thisisasaltwhichis22cenrvQa8HoAzcd4uM4IFyFZCnCW/K/Z8W", 6), + BCryptTestCase("hello", "thisisasaltwhichis22ch", "$2y$08$thisisasaltwhichis22ceGSq7Gjk/l24CRv3kIGqHqO1FkjVJqfa", 8), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "thisisasaltwhichis22ch", "$2y$06$thisisasaltwhichis22cewx9N0qHf5lH1qhen4ZSkh9G.9vF7bpC", 6), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "thisisasaltwhichis22ch", "$2y$08$thisisasaltwhichis22cehPvOT6ZlqH0g2TnuEL4wXbzDyVzqtJK", 8), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "thisisasaltwhichis22ch", "$2y$06$thisisasaltwhichis22ceeL2tbWfYYl6VVS.6kwSgf4Afnv5UVWu", 6), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "thisisasaltwhichis22ch", "$2y$08$thisisasaltwhichis22cejWbLJJzXmMWFyRkQhHGqvFWPPnh1era", 8), + BCryptTestCase("", "thisisasaltwhichis22chHello", "$2y$06$thisisasaltwhichis22ceiWyJJQuCqQ6K/VMVNgl3WR2TnKke93O", 6), + BCryptTestCase("", "thisisasaltwhichis22chHello", "$2y$08$thisisasaltwhichis22cetIGsXYPjot9MEiTlOQSjSqsUhO/ncum", 8), + BCryptTestCase("hello", "thisisasaltwhichis22chHello", "$2y$06$thisisasaltwhichis22cenrvQa8HoAzcd4uM4IFyFZCnCW/K/Z8W", 6), + BCryptTestCase("hello", "thisisasaltwhichis22chHello", "$2y$08$thisisasaltwhichis22ceGSq7Gjk/l24CRv3kIGqHqO1FkjVJqfa", 8), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "thisisasaltwhichis22chHello", "$2y$06$thisisasaltwhichis22cewx9N0qHf5lH1qhen4ZSkh9G.9vF7bpC", 6), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "thisisasaltwhichis22chHello", "$2y$08$thisisasaltwhichis22cehPvOT6ZlqH0g2TnuEL4wXbzDyVzqtJK", 8), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "thisisasaltwhichis22chHello", "$2y$06$thisisasaltwhichis22ceeL2tbWfYYl6VVS.6kwSgf4Afnv5UVWu", 6), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "thisisasaltwhichis22chHello", "$2y$08$thisisasaltwhichis22cejWbLJJzXmMWFyRkQhHGqvFWPPnh1era", 8), + BCryptTestCase("", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$06$MkjCFcpnaHo.ngQW8hDv1OE5e7EEsNBkPqjKjbeiAAGDZ32tMeoxi", 6), + BCryptTestCase("", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$08$MkjCFcpnaHo.ngQW8hDv1Oq3XwM.TU1ux4xZH9SWTvEeO6PaRKJhq", 8), + BCryptTestCase("hello", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$06$MkjCFcpnaHo.ngQW8hDv1OKSXItY4YgdUhR1XnCCIdVTG6/Z9Q8lq", 6), + BCryptTestCase("hello", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$08$MkjCFcpnaHo.ngQW8hDv1OI06YNkstj1fnz7Hb7hMALmyKde/b6XC", 8), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$06$MkjCFcpnaHo.ngQW8hDv1OWBmePzneCphnun8DXjkg0xSyFELeGQK", 6), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$08$MkjCFcpnaHo.ngQW8hDv1Opd3VPU7yrnM2bcYLxKXnz5GseN./Ihe", 8), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$06$MkjCFcpnaHo.ngQW8hDv1O2bkSBvdwGDuLKRRKAKDrJjDaEHkvbbS", 6), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "MkjCFcpnaHo.ngQW8hDv1Y", "$2y$08$MkjCFcpnaHo.ngQW8hDv1Ow2pDUKWKd3hQQKpiPMMzJY12zfFs1OG", 8), + BCryptTestCase("", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$06$V0bNN/hb14yIaLqiGzRq8.0q2LbRd5Bky7G/FobYdfIDZDt.3GfuS", 6), + BCryptTestCase("", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$08$V0bNN/hb14yIaLqiGzRq8.F9Eco7RM.M4t2WJoqcHpbKg9GmpD0BK", 8), + BCryptTestCase("hello", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$06$V0bNN/hb14yIaLqiGzRq8.h96biwYKLx4XkUR6uIq4MvxrGmMBo9q", 6), + BCryptTestCase("hello", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$08$V0bNN/hb14yIaLqiGzRq8./LDV//ottPlTJbrHPKM9GPrnc9VvHky", 8), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$06$V0bNN/hb14yIaLqiGzRq8.Rvv5BWMw9VGPlDfCkMY9CgANdtm.cki", 6), + BCryptTestCase("MpxzNrkTT)0OTBlU5TTY", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$08$V0bNN/hb14yIaLqiGzRq8.SiqvUcvyIVUuPmBOyv/h8iCzGwuzG2G", 8), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$06$V0bNN/hb14yIaLqiGzRq8.hRrCzpywIiN0DtynMdE4yrXf3cOwCJS", 6), + BCryptTestCase("2xNRPxj8EJfcN.Yxghkyt54Z39}Ylz0X4LwanXJB3i2mk<2Qs<9gWivWhpUe", "V0bNN/hb14yIaLqiGzRq8HBnUg/f7Ord6Q2ENsN.X3RmPgQR8wCfFDwKXJ3E", "$2y$08$V0bNN/hb14yIaLqiGzRq8.fKNrgl454jT6cqUTmCNI67wil/qB.Ya", 8) +)); + +INSTANTIATE_TEST_SUITE_P( + BCryptTestVerifyAll, + BCryptTestVerify, + ::testing::Values( + BCryptTestCase("", "", "$2y$06$vHGBnsrghALFuir57FW6g.eelIYPnAvZCqMcixaPD0ge7dxFg6Ldy", 6), + BCryptTestCase("", "", "$2y$08$RkQxUBJv0fMV32wHx1MWeu/79CQCvxNZbP.a2KkZG5D5PHsxguaM6", 8), + BCryptTestCase("hello", "", "$2y$06$kM1HuyiWFB0RUnI5e2tlCuhNeO.NxUE/QIyBpWoC0Av7xSJ0CNgci", 6), + BCryptTestCase("hello", "", "$2y$08$Q7KWcZNJfhG92UyuKj9aNesCozbY2nPde7vnNUrYbwW8cyISekTQu", 8), + BCryptTestCase("QOzQ-xgq(4gF(aw1", "", "$2y$06$JOoIFeD2H6uLtcfZ7y1Nv.7086L6d.QFRL90e7tx4x7/dy0q4oNJ2", 6), + BCryptTestCase("QOzQ-xgq(4gF(aw1", "", "$2y$08$bIbtUgz8hhye4Iw.gX2fc.yYQOik10qMMmue6i6ld6fJrr8VLMZn6", 8), + BCryptTestCase("8dK>paPE26/9{Zb29VyU", "", "$2y$06$znnkg1eVKFniEnL0QacK6OMi60p67ynDrLmi7vTOcSu0pFiD8MUbm", 6), + BCryptTestCase("8dK>paPE26/9{Zb29VyU", "", "$2y$08$nqwBRmGip.fSASDshXNKcOkHqa7BLndfZMjPAJT3CuKjxYUonTjbi", 8), + BCryptTestCase("LYb>ycmZvhM.(u{U76HVMoXW)Sf7gycmZvhM.(u{U76HVMoXW)Sf7g +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaObjectTest = testing::Test; + +TEST_F(OpcUaObjectTest, CreateSimpleType) +{ + OpcUaObject simpleVal; + + UA_Int16 value; + UA_Int16_init(&value); + + ASSERT_EQ(simpleVal.getValue(), value); +} + +TEST_F(OpcUaObjectTest, CreateComplexType) +{ + OpcUaObject complexVal; + + ASSERT_TRUE(UA_Variant_isEmpty(complexVal.get())); +} + +TEST_F(OpcUaObjectTest, SimpleTypeCopyConstructor) +{ + UA_Int32 ua_val = 5; + + OpcUaObject variant(ua_val); + + const UA_Int32& val = variant.getValue(); + + ASSERT_EQ(val, 5); +} + +TEST_F(OpcUaObjectTest, ComplexTypeCopyConstructor) +{ + UA_Variant ua_variant; + UA_Variant_init(&ua_variant); + + double value = 5; + UA_Variant_setScalarCopy(&ua_variant, &value, &UA_TYPES[UA_TYPES_DOUBLE]); + + OpcUaObject object(ua_variant); + + const UA_Variant& var = object.getValue(); + + UA_Variant_hasScalarType(&var, &UA_TYPES[UA_TYPES_DOUBLE]); + ASSERT_EQ(*static_cast(var.data), value); + + UA_Variant_clear(&ua_variant); +} + +TEST_F(OpcUaObjectTest, SimpleTypeMoveConstructor) +{ + UA_Int32 ua_val = 5; + + OpcUaObject variant(std::move(ua_val)); + + ASSERT_EQ(ua_val, 0); + + const UA_Int32& val = variant.getValue(); + + ASSERT_EQ(val, 5); +} + +TEST_F(OpcUaObjectTest, ComplexTypeMoveConstructor) +{ + UA_Variant ua_variant; + double value = 5; + UA_Variant_setScalarCopy(&ua_variant, &value, &UA_TYPES[UA_TYPES_DOUBLE]); + + OpcUaObject variant(std::move(ua_variant)); + ASSERT_TRUE(UA_Variant_isEmpty(&ua_variant)); + + const UA_Variant& var = variant.getValue(); + + UA_Variant_hasScalarType(&var, &UA_TYPES[UA_TYPES_DOUBLE]); + ASSERT_EQ(*static_cast(var.data), value); +} + +TEST_F(OpcUaObjectTest, SimpleTypeMoveAssignment) +{ + OpcUaObject object(5); + object = OpcUaObject(6); + + ASSERT_EQ(object.getValue(), 6); +} + +TEST_F(OpcUaObjectTest, ComplexTypeMoveAssignment) +{ + OpcUaObject object(UA_STRING_ALLOC("Test")); + object = OpcUaObject(UA_STRING_ALLOC("New val")); + + UA_String result = UA_STRING_ALLOC("New val"); + + ASSERT_TRUE(*object == result); + + UA_String_clear(&result); +} + +TEST_F(OpcUaObjectTest, SimpleTypeSetValueCopy) +{ + UA_Int16 ua_int = 14; + + OpcUaObject object; + object.setValue(ua_int); + + ASSERT_EQ(object.getValue(), 14); +} + +TEST_F(OpcUaObjectTest, ComplexTypeSetValueCopy) +{ + UA_String ua_str = UA_STRING_ALLOC("Test"); + + OpcUaObject object; + object.setValue(ua_str); + + UA_String_clear(&ua_str); + + ASSERT_TRUE(*object == "Test"); +} + +TEST_F(OpcUaObjectTest, SimpleTypeSetValueMove) +{ + OpcUaObject object; + object.setValue(14); + + ASSERT_EQ(object.getValue(), 14); +} + +TEST_F(OpcUaObjectTest, ComplexTypeSetValueMove) +{ + UA_String str = UA_STRING_ALLOC("Test"); + + OpcUaObject object; + object.setValue(std::move(str)); + + ASSERT_EQ(str.data, nullptr); + ASSERT_EQ(str.length, 0u); + + ASSERT_TRUE(*object == "Test"); +} + +TEST_F(OpcUaObjectTest, SimpleTypeCopyConstructOperator) +{ + OpcUaObject variant(5); + OpcUaObject variant1 = variant; + + ASSERT_EQ(variant.getValue(), 5); + ASSERT_EQ(variant1.getValue(), 5); +} + +TEST_F(OpcUaObjectTest, ComplexTypeCopyConstructOperator) +{ + OpcUaObject variant(UA_STRING_ALLOC("Test")); + OpcUaObject variant1 = variant; + + ASSERT_TRUE(variant.getValue() == "Test"); + ASSERT_TRUE(variant1.getValue() == "Test"); +} + +TEST_F(OpcUaObjectTest, SimpleTypeAssignmentOperator) +{ + OpcUaObject variant(5); + OpcUaObject variant1; + variant1 = variant; + + ASSERT_EQ(variant.getValue(), 5); + ASSERT_EQ(variant1.getValue(), 5); +} + +TEST_F(OpcUaObjectTest, ComplexTypeAssignmentOperator) +{ + OpcUaObject variant(UA_STRING_ALLOC("Test")); + OpcUaObject variant1; + + variant1 = variant; + + ASSERT_TRUE(variant.getValue() == "Test"); + ASSERT_TRUE(variant1.getValue() == "Test"); +} + +TEST_F(OpcUaObjectTest, ArrowOperator) +{ + OpcUaObject object(UA_STRING_ALLOC("Test")); + + const OpcUaObject& objectConst = object; + ASSERT_EQ(objectConst->length, 4u); + ASSERT_EQ(object->length, 4u); +} + +TEST_F(OpcUaObjectTest, SimpleTypeClear) +{ + OpcUaObject object(4); + + ASSERT_EQ(object.getValue(), 4); + + object.clear(); + + ASSERT_EQ(object.getValue(), 0); +} + +TEST_F(OpcUaObjectTest, ComplexTypeClear) +{ + OpcUaObject object(UA_STRING_ALLOC("Test")); + + ASSERT_TRUE(object.getValue() == "Test"); + + object.clear(); + + ASSERT_TRUE(object.getValue() == UA_STRING_NULL); +} + +TEST_F(OpcUaObjectTest, SimpleTypeGetDetachedValue) +{ + OpcUaObject object(4); + + ASSERT_EQ(object.getValue(), 4); + + object.getDetachedValue(); + + ASSERT_EQ(object.getValue(), 0); +} + +TEST_F(OpcUaObjectTest, ComplexTypeGetDetachedValue) +{ + UA_String text = UA_STRING_ALLOC("Test"); + + OpcUaObject object(text); + + UA_String value = object.getDetachedValue(); + + ASSERT_TRUE(value == text); + ASSERT_TRUE(object.getValue() == UA_STRING_NULL); + + UA_String_clear(&value); + UA_String_clear(&text); +} + +TEST_F(OpcUaObjectTest, SelfAssign) +{ + auto targetNodeId = OpcUaNodeId(1, 1000); + auto nodeId = OpcUaNodeId(1, 1000); + OpcUaNodeId* pNodeId = &nodeId; + nodeId = nodeId; + ASSERT_EQ(pNodeId, &nodeId); + ASSERT_EQ(targetNodeId, nodeId); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcua_security.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcua_security.cpp new file mode 100644 index 0000000..f7ed10c --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcua_security.cpp @@ -0,0 +1,458 @@ +#include "gtest/gtest.h" +#include "testutils/testutils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Dewesoft::Utils::Crypto::BCrypt; + +BEGIN_NAMESPACE_OPCUA + +class OpcUaSecurityTest : public MemCheckTest +{ + bool EnableMemoryLeakDump() + { + return false; + } +}; + +static std::string TestFile(std::string name) +{ + return CmakeGlobals::ResDirFile("testKeys/keys/" + name); +} + +class TestServer +{ +public: + TestServer() + { + server = std::make_shared(); + server->setPort(4840); + } + + ~TestServer() + { + if (isStarted()) + server->stop(); + nodes.clear(); + } + + void setSecurityConfig(OpcUaServerSecurityConfig* config) + { + server->setSecurityConfig(config); + } + + void defineStringVar(std::string name, std::string value) + { + UA_NodeId nodeId = UA_NODEID_STRING_ALLOC(1, name.c_str()); + UA_String valueStr = UA_STRING_ALLOC(value.c_str()); + + OpcUaNodeValueServerStringPtr node = std::make_shared(nodeId); + node->setBrowseName(name.c_str()); + node->setValue(valueStr, UA_DateTime_now()); + server->createServerNode(node, OpcUaNodeId(0, UA_NS0ID_SERVER)); + nodes.push_back(node); + + UA_String_clear(&valueStr); + UA_NodeId_clear(&nodeId); + } + + void start() + { + server->start(); + } + + void stop() + { + server->stop(); + } + + bool isStarted() + { + return server->getStarted(); + } + + std::shared_ptr getServer() + { + return server; + } + + static OpcUaServerSecurityConfig CreateSecurityConfig() + { + OpcUaServerSecurityConfig config; + config.certificate = OpcUaCommon::loadFile(TestFile("server-cert.der")); + config.privateKey = OpcUaCommon::loadFile(TestFile("server-private.der")); + config.trustList.push_back(OpcUaCommon::loadFile(TestFile("client-cert.der")).getValue()); + config.appUri = "urn:dewesoft.com"; + config.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + return config; + } + + static std::unordered_map CreateUsers() + { + BCrypt bcrypt; + unsigned int rounds = 8; + std::unordered_map users; + users["newton"] = bcrypt.hash("newton123", rounds); + users["galileo"] = bcrypt.hash("galileo123", rounds); + users["tesla"] = bcrypt.hash("tesla123", rounds); + users["pascal"] = bcrypt.hash("pascal123", rounds); + return users; + } + +private: + std::shared_ptr server; + std::vector nodes; +}; + +class TestClient +{ +public: + TestClient() + { + client = std::make_shared(OpcUaDataFetchStrategy::Buffered); + securityConfig = NULL; + } + + void setSecurityConfig(OpcUaClientSecurityConfig* config) + { + securityConfig = config; + } + + void connect() + { + OpcUaEndpoint endpoint("test", "opc.tcp://localhost:4840/", OpcUaServerType::General, ""); + endpoint.setSecurityConfig(securityConfig); + + OpcUaCollection endpoints; + endpoints.push_back(endpoint); + client->assignEndpoints(endpoints); + + client->connect(); + } + + void disconnect() + { + client->disconnect(); + } + + bool isConnected() + { + return client->size() > 0 && client->at(0)->isConnected(); + } + + std::string readStringVar(std::string name) + { + UA_NodeId nodeId = UA_NODEID_STRING_ALLOC(1, name.c_str()); + ReadResult r = client->at(0)->readValue(&nodeId); + + UA_Variant variant = r.value->getValue(); + UA_String* uaStr = (UA_String*) variant.data; + std::string value = std::string((char*) uaStr->data, uaStr->length); + + UA_NodeId_clear(&nodeId); + return value; + } + + std::shared_ptr getClient() + { + return client; + } + + static OpcUaClientSecurityConfig CreateSecurityConfig() + { + OpcUaClientSecurityConfig config; + config.certificate = OpcUaCommon::loadFile(TestFile("client-cert.der")); + config.privateKey = OpcUaCommon::loadFile(TestFile("client-private.der")); + config.trustList.push_back(OpcUaCommon::loadFile(TestFile("server-cert.der")).getValue()); + config.appUri = "urn:testclient.com"; + config.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + return config; + } + +private: + std::shared_ptr client; + OpcUaClientSecurityConfig* securityConfig; +}; + +TEST_F(OpcUaSecurityTest, LoadCertificateTest) +{ + OpcUaObject cert; + + ASSERT_NO_THROW(cert = OpcUaCommon::loadFile(TestFile("client-cert.der"))); + ASSERT_GT(cert.getValue().length, 0); + + ASSERT_ANY_THROW(cert = OpcUaCommon::loadFile(TestFile("client-cert-missing.der"))); + + ASSERT_NO_THROW(cert = OpcUaCommon::loadFile(TestFile("garbage-cert.der"))); + ASSERT_GT(cert.getValue().length, 0); +} + +TEST_F(OpcUaSecurityTest, SecurityConfigMemoryTest) +{ + OpcUaClientSecurityConfig a; + a.certificate = OpcUaCommon::loadFile(TestFile("client-cert.der")).getValue(); + + a.trustList.push_back(OpcUaCommon::loadFile(TestFile("server-cert.der")).getValue()); + a.trustList[0] = OpcUaCommon::loadFile(TestFile("client-cert.der")).getValue(); + + a.appUri = "urn:testclient.com"; + a.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + + OpcUaClientSecurityConfig b = a; +} + +TEST_F(OpcUaSecurityTest, PlainTextTest) +{ + std::string message = "Hello world, plain text communication works."; + + TestServer testServer; + testServer.setSecurityConfig(NULL); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + testServer.defineStringVar("testVar", message); + + TestClient testClient; + testClient.setSecurityConfig(NULL); + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + std::string received = testClient.readStringVar("testVar"); + ASSERT_EQ(received, message); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, SecurityModeNoneTest) +{ + std::string message = "Hello world, security mode UA_MESSAGESECURITYMODE_NONE works."; + + OpcUaServerSecurityConfig serverSecurity; + serverSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + testServer.defineStringVar("testVar", message); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + std::string received = testClient.readStringVar("testVar"); + ASSERT_EQ(received, message); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, SecurityModeSignEncryptTest) +{ + std::string message = "Hello world, security mode UA_MESSAGESECURITYMODE_SIGNANDENCRYPT works."; + + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + testServer.defineStringVar("testVar", message); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + std::string received = testClient.readStringVar("testVar"); + ASSERT_EQ(received, message); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, WrongSecurityModeTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, ExpiredCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.trustList.push_back(OpcUaCommon::loadFile(TestFile("client-cert-expired.der")).getValue()); + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.trustList.push_back(OpcUaCommon::loadFile(TestFile("server-cert.der")).getValue()); + clientSecurity.certificate = OpcUaCommon::loadFile(TestFile("client-cert-expired.der")); + clientSecurity.privateKey = OpcUaCommon::loadFile(TestFile("client-private-expired.der")); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + clientSecurity.appUri = "urn:testclient.com"; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, GarbageCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.trustList.push_back(OpcUaCommon::loadFile(TestFile("server-cert.der")).getValue()); + clientSecurity.certificate = OpcUaCommon::loadFile(TestFile("garbage-cert.der")).getValue(); + clientSecurity.privateKey = OpcUaCommon::loadFile(TestFile("client-private.der")).getValue(); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + clientSecurity.appUri = "urn:testclient.com"; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + serverSecurity.trustList.push_back(OpcUaCommon::loadFile(TestFile("garbage-cert.der")).getValue()); + ASSERT_THROW(testServer.start(), OpcUaException); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, UntrustedCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.trustList.push_back(OpcUaCommon::loadFile(TestFile("server-cert.der")).getValue()); + clientSecurity.certificate = OpcUaCommon::loadFile(TestFile("tesla-cert.der")); + clientSecurity.privateKey = OpcUaCommon::loadFile(TestFile("tesla-private.der")); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + clientSecurity.appUri = "urn:nikolatesla.com"; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + serverSecurity.trustList.push_back(OpcUaCommon::loadFile(TestFile("tesla-cert.der")).getValue()); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + testClient.connect(); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F(OpcUaSecurityTest, AuthenticationTest) +{ + std::unordered_map users = TestServer::CreateUsers(); + + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.authenticateUser = [&users](bool isAnonymous, std::string username, std::string password) -> UA_StatusCode { + if (!isAnonymous && users.count(username)) + { + std::string hash = users[username]; + if (BCrypt::Verify(hash, password)) + return UA_STATUSCODE_GOOD; + } + + return UA_STATUSCODE_BADUSERACCESSDENIED; + }; + + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + + // annonymous login + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + // wrong password + clientSecurity.username = "tesla"; + clientSecurity.password = "wrongPassword"; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + // wrong user + clientSecurity.username = "wrongUser"; + clientSecurity.password = "tesla123"; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + // crrect login + clientSecurity.username = "tesla"; + clientSecurity.password = "tesla123"; + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +END_NAMESPACE_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcua_variant.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcua_variant.cpp new file mode 100644 index 0000000..9c75c0e --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcua_variant.cpp @@ -0,0 +1,205 @@ +#include "gtest/gtest.h" +#include "opcuashared/opcuavariant.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaVariantTest = testing::Test; + +TEST_F(OpcUaVariantTest, CreateNull) +{ + OpcUaVariant variant; + ASSERT_FALSE(variant.isBool()); + ASSERT_FALSE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_FALSE(variant.isNodeId()); +} + +TEST_F(OpcUaVariantTest, CreateBool) +{ + OpcUaVariant variant(true); + ASSERT_TRUE(variant.isBool()); + ASSERT_FALSE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_FALSE(variant.isNodeId()); + ASSERT_EQ(variant.toBool(), true); +} + +TEST_F(OpcUaVariantTest, CreateDouble) +{ + OpcUaVariant variant(5.5); + ASSERT_FALSE(variant.isBool()); + ASSERT_TRUE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_FALSE(variant.isNodeId()); + ASSERT_EQ(variant.toDouble(), 5.5); +} + +TEST_F(OpcUaVariantTest, CreateInt) +{ + OpcUaVariant variant(int64_t(5)); + ASSERT_FALSE(variant.isBool()); + ASSERT_FALSE(variant.isDouble()); + ASSERT_TRUE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_FALSE(variant.isNodeId()); + ASSERT_EQ(variant.toInteger(), 5); +} + +TEST_F(OpcUaVariantTest, CreateString) +{ + OpcUaVariant variant("Hi"); + ASSERT_FALSE(variant.isBool()); + ASSERT_FALSE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_TRUE(variant.isString()); + ASSERT_FALSE(variant.isNodeId()); + ASSERT_STREQ(variant.toString().c_str(), "Hi"); +} + +TEST_F(OpcUaVariantTest, CreateString1) +{ + OpcUaVariant variant = OpcUaVariant("Test"); + ASSERT_FALSE(variant.isBool()); + ASSERT_FALSE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_TRUE(variant.isString()); + ASSERT_FALSE(variant.isNodeId()); + ASSERT_STREQ(variant.toString().c_str(), "Test"); +} + +TEST_F(OpcUaVariantTest, CreateNodeId) +{ + OpcUaVariant variant(OpcUaNodeId(1, "Test")); + + ASSERT_FALSE(variant.isBool()); + ASSERT_FALSE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_TRUE(variant.isNodeId()); + + ASSERT_EQ(variant.toNodeId(), OpcUaNodeId(1, "Test")); +} + +TEST_F(OpcUaVariantTest, CopyConstructor) +{ + UA_Variant* ua_variant = UA_Variant_new(); + double value = 5; + UA_Variant_setScalarCopy(ua_variant, &value, &UA_TYPES[UA_TYPES_DOUBLE]); + + OpcUaVariant variant(*ua_variant); + variant.setValue(*ua_variant); + + ASSERT_FALSE(variant.isBool()); + ASSERT_TRUE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_EQ(variant.toDouble(), 5); + UA_Variant_delete(ua_variant); +} + +TEST_F(OpcUaVariantTest, MoveConstructor) +{ + UA_Variant ua_variant; + double value = 5; + UA_Variant_setScalarCopy(&ua_variant, &value, &UA_TYPES[UA_TYPES_DOUBLE]); + + OpcUaVariant variant(std::move(ua_variant)); + ASSERT_FALSE(variant.isBool()); + ASSERT_TRUE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_EQ(variant.toDouble(), 5); + + ASSERT_TRUE(UA_Variant_isEmpty(&ua_variant)); +} + +TEST_F(OpcUaVariantTest, MoveAssignment) +{ + OpcUaVariant variant("Hi"); + variant = OpcUaVariant("Test"); + ASSERT_TRUE(variant.isString()); + ASSERT_STREQ(variant.toString().c_str(), "Test"); +} + +TEST_F(OpcUaVariantTest, SetValueCopy) +{ + UA_Variant ua_variant; + double value = 5; + UA_Variant_setScalarCopy(&ua_variant, &value, &UA_TYPES[UA_TYPES_DOUBLE]); + + OpcUaVariant variant; + variant.setValue(ua_variant); + ASSERT_FALSE(variant.isBool()); + ASSERT_TRUE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_EQ(variant.toDouble(), 5); + + UA_Variant_clear(&ua_variant); +} + +TEST_F(OpcUaVariantTest, SetValueMove) +{ + UA_Variant ua_variant; + double value = 5; + UA_Variant_setScalarCopy(&ua_variant, &value, &UA_TYPES[UA_TYPES_DOUBLE]); + + OpcUaVariant variant; + variant.setValue(std::move(ua_variant)); + ASSERT_FALSE(variant.isBool()); + ASSERT_TRUE(variant.isDouble()); + ASSERT_FALSE(variant.isInteger()); + ASSERT_FALSE(variant.isString()); + ASSERT_EQ(variant.toDouble(), 5); + + ASSERT_TRUE(UA_Variant_isEmpty(&ua_variant)); +} + +TEST_F(OpcUaVariantTest, CopyConstructOperator) +{ + OpcUaVariant variant(int64_t(5)); + OpcUaVariant variant1 = variant; + + ASSERT_TRUE(variant.isInteger()); + ASSERT_EQ(variant.toInteger(), 5); + + ASSERT_TRUE(variant1.isInteger()); + ASSERT_EQ(variant1.toInteger(), 5); +} + +TEST_F(OpcUaVariantTest, AssignmentOperator) +{ + OpcUaVariant variant(int64_t(5)); + OpcUaVariant variant1; + variant1 = variant; + + ASSERT_TRUE(variant.isInteger()); + ASSERT_EQ(variant.toInteger(), 5); + + ASSERT_TRUE(variant1.isInteger()); + ASSERT_EQ(variant1.toInteger(), 5); +} + +TEST_F(OpcUaVariantTest, ReadScalar) +{ + OpcUaVariant variant(static_cast(5)); + + ASSERT_EQ(variant.readScalar(), 5); + + ASSERT_THROW(variant.readScalar(), std::runtime_error); +} + +TEST_F(OpcUaVariantTest, SetScalar) +{ + OpcUaVariant variant; + + ASSERT_NO_THROW(variant.setScalar(5)); + + ASSERT_EQ(variant.readScalar(), 5); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcua_vector.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcua_vector.cpp new file mode 100644 index 0000000..18b2552 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcua_vector.cpp @@ -0,0 +1,107 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaVectorTest = testing::Test; + +TEST_F(OpcUaVectorTest, ScopeTest) +{ + OpcUaObject a = OpcUaObject(UA_STRING_ALLOC("Hello One")); + OpcUaObject b = OpcUaObject(UA_STRING_ALLOC("Hello Two")); + + { + OpcUaVector vect; + vect.push_back(a.getValue()); + vect.push_back(a.getValue()); + ASSERT_EQ(utils::ToStdString(vect[1]), utils::ToStdString(a.getValue())); + vect[1] = b.getValue(); + ASSERT_EQ(utils::ToStdString(vect[1]), utils::ToStdString(b.getValue())); + } +} + +TEST_F(OpcUaVectorTest, CopyTest) +{ + OpcUaVector a = {1, 2, 3, 4, 5}; + OpcUaVector b = {10, 20, 30}; + + a = b; + ASSERT_EQ(a.size(), b.size()); + ASSERT_EQ(a[0], b[0]); + ASSERT_EQ(a[1], b[1]); + ASSERT_EQ(a[2], b[2]); + + UA_Int32 b0 = 10; + UA_Int32 a0 = 99; + a[0] = a0; + ASSERT_EQ(a[0], a0); + ASSERT_EQ(b[0], b0); +} + +TEST_F(OpcUaVectorTest, ResizeTest) +{ + const size_t sizeA = 5; + OpcUaVector a = {1, 2, 3, 4, 5}; + const size_t sizeB = 3; + OpcUaVector b = {10, 20, 30}; + + size_t newSizeA = 3; + ASSERT_EQ(a.size(), sizeA); + a.resize(newSizeA); + ASSERT_EQ(a.size(), newSizeA); + + size_t newSizeB = 10; + ASSERT_EQ(b.size(), sizeB); + b.resize(newSizeB); + ASSERT_EQ(b.size(), newSizeB); + + OpcUaVector c; + c.resize(10); +} + +TEST_F(OpcUaVectorTest, AppendTest) +{ + std::vector a = {1, 2, 3, 4, 5}; + OpcUaVector b; + for (size_t i = 0; i < a.size(); i++) + b.push_back(a[i]); + + ASSERT_EQ(a.size(), b.size()); + bool areEqual = memcmp(a.data(), b.data(), sizeof(UA_Int32) * a.size()) == 0; + ASSERT_TRUE(areEqual); + + UA_Int32 last = 99; + b[b.size() - 1] = last; + areEqual = memcmp(a.data(), b.data(), sizeof(UA_Int32) * a.size()) == 0; + ASSERT_FALSE(areEqual); +} + +TEST_F(OpcUaVectorTest, SetGetTest) +{ + UA_Int32 a = 1; + UA_Int32 b = 2; + UA_Int32 c = 3; + + OpcUaVector vectA; + vectA.resize(10); + + vectA[0] = a; + ASSERT_EQ(vectA[0], a); + vectA[1] = b; + ASSERT_EQ(vectA[1], b); + vectA[1] = c; + ASSERT_EQ(vectA[1], c); + + OpcUaVector vectB; + vectB.resize(10); + + vectB.set(0, a); + ASSERT_EQ(vectB.get(0), a); + vectB.set(1, b); + ASSERT_EQ(vectB.get(1), b); + vectB.set(1, c); + ASSERT_EQ(vectB.get(1), c); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuacallmethodresult.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuacallmethodresult.cpp new file mode 100644 index 0000000..43b5d8f --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuacallmethodresult.cpp @@ -0,0 +1,44 @@ +#include "gtest/gtest.h" +#include "opcuashared/opcuacallmethodresult.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaCallMethodResultTest = testing::Test; + +TEST_F(OpcUaCallMethodResultTest, Create) +{ + UA_CallMethodResult callMethodResult; + UA_CallMethodResult_init(&callMethodResult); + + callMethodResult.statusCode = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + + callMethodResult.outputArgumentsSize = 2; + callMethodResult.outputArguments = (UA_Variant*) UA_Array_new(callMethodResult.outputArgumentsSize, &UA_TYPES[UA_TYPES_VARIANT]); + + UA_Int64 intVal = 0; + UA_Variant_setScalarCopy(&callMethodResult.outputArguments[0], &intVal, &UA_TYPES[UA_TYPES_INT64]); + + UA_String strVal = UA_STRING_ALLOC("Test"); + UA_Variant_setScalarCopy(&callMethodResult.outputArguments[1], &strVal, &UA_TYPES[UA_TYPES_STRING]); + UA_String_clear(&strVal); + + OpcUaCallMethodResult value(callMethodResult); + + ASSERT_EQ(value.getOutputArgumentsSize(), 2u); + ASSERT_EQ(value.getStatusCode(), UA_STATUSCODE_BADAGGREGATELISTMISMATCH); + + OpcUaVariant val1 = value.getOutputArgument(0); + ASSERT_TRUE(val1.isInteger()); + ASSERT_EQ(val1.toInteger(), 0); + + OpcUaVariant val2 = value.getOutputArgument(1); + ASSERT_TRUE(val2.isString()); + ASSERT_STREQ(val2.toString().c_str(), "Test"); + + ASSERT_THROW(value.getOutputArgument(2), std::out_of_range); + + UA_CallMethodResult_clear(&callMethodResult); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuadatatypearraylist.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuadatatypearraylist.cpp new file mode 100644 index 0000000..3676f59 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuadatatypearraylist.cpp @@ -0,0 +1,140 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +typedef struct { + UA_Float x; + UA_Float y; +} Struct1; +static UA_DataTypeMember struct1_members[2] = { + /* a */ + { + UA_TYPENAME("a") // .typeName + &UA_TYPES[UA_TYPES_FLOAT], // .memberType + 0, // .padding + false, // .isArray + false // .isOptional + }, + /* b */ + { + UA_TYPENAME("b") // .typeName + &UA_TYPES[UA_TYPES_FLOAT], // .memberType + 0, // .padding + false, // .isArray + false // .isOptional + } +}; +static const UA_DataType struct1Type = { + UA_TYPENAME("Struct1") // .typeName + {1, UA_NODEIDTYPE_NUMERIC, {4242}}, // .typeId + {1, UA_NODEIDTYPE_NUMERIC, {1}}, // .binaryEncodingId + sizeof(Struct1), // .memSize + UA_DATATYPEKIND_STRUCTURE, // .typeKind + true, // .pointerFree + false, // .overlayable + 2, // .membersize + struct1_members // .members +}; + +typedef struct { + UA_Float x; + UA_Float y; + UA_Float z; +} Struct2; +static UA_DataTypeMember struct2_members[3] = { + /* x */ + { + UA_TYPENAME("x") // .typeName + &UA_TYPES[UA_TYPES_FLOAT], // .memberType + 0, // .padding + false, // .isArray + false // .isOptional + }, + /* y */ + { + UA_TYPENAME("y") // .typeName + &UA_TYPES[UA_TYPES_FLOAT], // .memberType + 0, // .padding + false, // .isArray + false // .isOptional + }, + /* z */ + { + UA_TYPENAME("z") // .typeName + &UA_TYPES[UA_TYPES_FLOAT], // .memberType + 0, // .padding + false, // .isArray + false // .isOptional + }, +}; +static const UA_DataType struct2Type = { + UA_TYPENAME("Struct2") // .typeName + {1, UA_NODEIDTYPE_NUMERIC, {4243}}, // .typeId + {1, UA_NODEIDTYPE_NUMERIC, {2}}, // .binaryEncodingId + sizeof(Struct2), // .memSize + UA_DATATYPEKIND_STRUCTURE, // .typeKind + true, // .pointerFree + false, // .overlayable + 3, // .membersize + struct2_members // .members +}; + +TEST(OpcUaDataTypeArrayListTest, EmptyList) +{ + OpcUaDataTypeArrayList arrList; + ASSERT_EQ(arrList.getCustomDataTypes(), nullptr); +} + +TEST(OpcUaDataTypeArrayListTest, SingleListElement) +{ + UA_DataType types[1]; + types[0] = struct1Type; + + OpcUaDataTypeArrayList arrList; + + arrList.add(1, types); + const UA_DataTypeArray* dataType = arrList.getCustomDataTypes(); + + ASSERT_EQ(dataType->next, nullptr); + ASSERT_EQ(dataType->typesSize, 1u); +} + +TEST(OpcUaDataTypeArrayListTest, MultipleListElements) +{ + UA_DataType types1[1]; + types1[0] = struct1Type; + UA_DataType types2[1]; + types2[0] = struct2Type; + + OpcUaDataTypeArrayList arrList; + arrList.add(1, types1); + arrList.add(1, types2); + + const UA_DataTypeArray* dataType = arrList.getCustomDataTypes(); + int count = 0; + while (dataType != NULL) + { + ++count; + dataType = dataType->next; + } + + ASSERT_EQ(count, 2); +} + +TEST(OpcUaDataTypeArrayListTest, LargerTypeCount) +{ + UA_DataType types[2]; + types[0] = struct1Type; + types[1] = struct2Type; + + OpcUaDataTypeArrayList arrList; + arrList.add(2, types); + + const UA_DataTypeArray* dataType = arrList.getCustomDataTypes(); + + ASSERT_EQ(dataType->typesSize, 2u); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp new file mode 100644 index 0000000..6c4f6e4 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuadatavalue.cpp @@ -0,0 +1,69 @@ +#include "gtest/gtest.h" +#include "opcuashared/opcuadatavalue.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaDataValueTest = testing::Test; + +TEST_F(OpcUaDataValueTest, CreateWithInt) +{ + UA_DataValue dataValue; + UA_DataValue_init(&dataValue); + + dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + UA_Int64 val = 0; + + UA_Variant_setScalarCopy(&dataValue.value, &val, &UA_TYPES[UA_TYPES_INT64]); + + OpcUaDataValue value(&dataValue); + + ASSERT_TRUE(value.getValue().isInteger()); + ASSERT_EQ(value.getStatusCode(), UA_STATUSCODE_BADAGGREGATELISTMISMATCH); + + UA_DataValue_clear(&dataValue); +} + +TEST_F(OpcUaDataValueTest, CreateWithIntRawDataValue) +{ + UA_DataValue dataValue; + UA_DataValue_init(&dataValue); + + dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + UA_Int64 val = 0; + + UA_Variant_setScalarCopy(&dataValue.value, &val, &UA_TYPES[UA_TYPES_INT64]); + + OpcUaDataValue value(&dataValue); + + const UA_DataValue* rawDataValue = value.getDataValue(); + ASSERT_EQ(rawDataValue, &dataValue); + + rawDataValue = value; + ASSERT_EQ(rawDataValue, &dataValue); + + UA_DataValue_clear(&dataValue); +} + +TEST_F(OpcUaDataValueTest, TestNoCopyBehaviour) +{ + UA_DataValue dataValue; + UA_DataValue_init(&dataValue); + + dataValue.status = UA_STATUSCODE_BADAGGREGATELISTMISMATCH; + UA_Int64* val = UA_Int64_new(); + *val = 1; + + UA_Variant_setScalar(&dataValue.value, val, &UA_TYPES[UA_TYPES_INT64]); + + OpcUaDataValue value(&dataValue); + ASSERT_EQ(value.getValue().toInteger(), 1); + *val = 2; + ASSERT_EQ(value.getValue().toInteger(), 2); + + ASSERT_EQ(value.getValue().getValue().data, dataValue.value.data); + + UA_DataValue_clear(&dataValue); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuaendpoint.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuaendpoint.cpp new file mode 100644 index 0000000..d9ef274 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuaendpoint.cpp @@ -0,0 +1,64 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaEndpointTest = testing::Test; + +TEST_F(OpcUaEndpointTest, Create) +{ + auto endpoint = OpcUaEndpoint("opc.tcp://localhost:4840"); + ASSERT_EQ(endpoint.getUrl(), "opc.tcp://localhost:4840"); +} + +TEST_F(OpcUaEndpointTest, CreateUsernamePassword) +{ + auto endpoint = OpcUaEndpoint("opc.tcp://127.0.0.1", "un", "pass"); + + ASSERT_EQ(endpoint.getUrl(), "opc.tcp://127.0.0.1"); + ASSERT_EQ(endpoint.getUsername(), "un"); + ASSERT_EQ(endpoint.getPassword(), "pass"); +} + +TEST_F(OpcUaEndpointTest, SettersAndGetters) +{ + auto endpoint = OpcUaEndpoint("opc.tcp://localhost:4840"); + + endpoint.setUrl("opc.tcp://localhost:2000"); + ASSERT_EQ(endpoint.getUrl(), "opc.tcp://localhost:2000"); + + endpoint.setName("Name"); + ASSERT_EQ(endpoint.getName(), "Name"); + + endpoint.setUsername("username"); + ASSERT_EQ(endpoint.getUsername(), "username"); + + endpoint.setPassword("123"); + ASSERT_EQ(endpoint.getPassword(), "123"); +} + +TEST_F(OpcUaEndpointTest, RegisterCustomTypes) +{ + auto endpoint = OpcUaEndpoint("opc.tcp://127.0.0.1:4840"); + + endpoint.registerCustomTypes(UA_TYPES_COUNT, UA_TYPES); + auto typeList = endpoint.getCustomDataTypes(); + + ASSERT_EQ(typeList[0].typesSize, static_cast(UA_TYPES_COUNT)); + ASSERT_EQ(typeList[0].types, UA_TYPES); +} + +TEST_F(OpcUaEndpointTest, IsAnonymous) +{ + auto endpoint = OpcUaEndpoint("opc.tcp://127.0.0.1:4840"); + ASSERT_TRUE(endpoint.isAnonymous()); + + endpoint.setUsername("u"); + ASSERT_FALSE(endpoint.isAnonymous()); + + endpoint.setUsername(""); + endpoint.setPassword("u"); + ASSERT_TRUE(endpoint.isAnonymous()); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuanodeid.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuanodeid.cpp new file mode 100644 index 0000000..71402e7 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuanodeid.cpp @@ -0,0 +1,239 @@ +#include "gtest/gtest.h" +#include "opcuashared/opcuanodeid.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaNodeIdTest = testing::Test; + +TEST_F(OpcUaNodeIdTest, CreateNull) +{ + OpcUaNodeId nodeId; + ASSERT_TRUE(nodeId.isNull()); +} + +TEST_F(OpcUaNodeIdTest, CreateInt) +{ + OpcUaNodeId nodeId(1, 2); + + ASSERT_EQ(nodeId.getNamespaceIndex(), 1); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::Numeric); + ASSERT_EQ(nodeId.getIdentifier(), "2"); +} + +TEST_F(OpcUaNodeIdTest, CreateString) +{ + OpcUaNodeId nodeId(1, "TestIden"); + + ASSERT_EQ(nodeId.getNamespaceIndex(), 1); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(nodeId.getIdentifier(), "TestIden"); +} + +TEST_F(OpcUaNodeIdTest, CreateFromOpcUaNodeInt) +{ + UA_NodeId uaNode = UA_NODEID_NUMERIC(1, 2); + OpcUaNodeId nodeId(uaNode); + UA_NodeId_clear(&uaNode); + + ASSERT_EQ(nodeId.getNamespaceIndex(), 1); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::Numeric); + ASSERT_EQ(nodeId.getIdentifier(), "2"); +} + +TEST_F(OpcUaNodeIdTest, CreateFromOpcUaNodeString) +{ + UA_NodeId uaNode = UA_NODEID_STRING_ALLOC(1, "TestIden"); + OpcUaNodeId nodeId(uaNode); + UA_NodeId_clear(&uaNode); + + ASSERT_EQ(nodeId.getNamespaceIndex(), 1); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(nodeId.getIdentifier(), "TestIden"); +} + +TEST_F(OpcUaNodeIdTest, CreateFromOpcUaNodeInt1) +{ + UA_NodeId uaNode = UA_NODEID_NUMERIC(1, 2); + OpcUaNodeId nodeId = uaNode; + UA_NodeId_clear(&uaNode); + + ASSERT_EQ(nodeId.getNamespaceIndex(), 1); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::Numeric); + ASSERT_EQ(nodeId.getIdentifier(), "2"); +} + +TEST_F(OpcUaNodeIdTest, CreateFromOpcUaNodeString1) +{ + UA_NodeId uaNode = UA_NODEID_STRING_ALLOC(1, "TestIden"); + OpcUaNodeId nodeId = uaNode; + UA_NodeId_clear(&uaNode); + + ASSERT_EQ(nodeId.getNamespaceIndex(), 1); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(nodeId.getIdentifier(), "TestIden"); +} + +TEST_F(OpcUaNodeIdTest, AppendStringToInt) +{ + OpcUaNodeId nodeId(1, 2); + OpcUaNodeId newNode = nodeId.addSuffix("!!!"); + ASSERT_EQ(newNode.getNamespaceIndex(), 1); + ASSERT_EQ(newNode.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(newNode.getIdentifier(), "2!!!"); + + newNode = nodeId.addSuffix("_", "!!!"); + ASSERT_EQ(newNode.getNamespaceIndex(), 1); + ASSERT_EQ(newNode.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(newNode.getIdentifier(), "2_!!!"); +} + +TEST_F(OpcUaNodeIdTest, AppendStringToString) +{ + OpcUaNodeId nodeId(1, "2"); + OpcUaNodeId newNode = nodeId.addSuffix("!!!"); + ASSERT_EQ(newNode.getNamespaceIndex(), 1); + ASSERT_EQ(newNode.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(newNode.getIdentifier(), "2!!!"); + + newNode = nodeId.addSuffix("_", "!!!"); + ASSERT_EQ(newNode.getNamespaceIndex(), 1); + ASSERT_EQ(newNode.getIdentifierType(), OpcUaIdentifierType::String); + ASSERT_EQ(newNode.getIdentifier(), "2_!!!"); +} + +TEST_F(OpcUaNodeIdTest, TestEqual) +{ + OpcUaNodeId nodeIdStr1(1, "2"); + OpcUaNodeId nodeIdStr2(1, "2"); + OpcUaNodeId nodeIdStr3(1, "2222"); + OpcUaNodeId nodeIdStr4(2, "2"); + + OpcUaNodeId nodeIdInt1(1, 2); + OpcUaNodeId nodeIdInt2(1, 2); + OpcUaNodeId nodeIdInt3(1, 3); + OpcUaNodeId nodeIdInt4(2, 2); + + ASSERT_TRUE(nodeIdStr1 == nodeIdStr2); + ASSERT_FALSE(nodeIdStr1 != nodeIdStr2); + ASSERT_FALSE(nodeIdStr1 == nodeIdStr3); + ASSERT_TRUE(nodeIdStr1 != nodeIdStr3); + ASSERT_FALSE(nodeIdStr1 == nodeIdStr4); + ASSERT_TRUE(nodeIdStr1 != nodeIdStr4); + + ASSERT_TRUE(nodeIdInt1 == nodeIdInt2); + ASSERT_FALSE(nodeIdInt1 != nodeIdInt2); + ASSERT_FALSE(nodeIdInt1 == nodeIdInt3); + ASSERT_TRUE(nodeIdInt1 != nodeIdInt3); + ASSERT_FALSE(nodeIdInt1 == nodeIdInt4); + ASSERT_TRUE(nodeIdInt1 != nodeIdInt4); +} + +TEST_F(OpcUaNodeIdTest, TestEqualUAType) +{ + OpcUaNodeId nodeIdStr1(1, "2"); + OpcUaNodeId nodeIdStr2(1, "2222"); + + UA_NodeId uaNodeIdStr = UA_NODEID_STRING_ALLOC(1, "2222"); + + ASSERT_TRUE(nodeIdStr1 != uaNodeIdStr); + ASSERT_FALSE(nodeIdStr1 == uaNodeIdStr); + + ASSERT_FALSE(nodeIdStr2 != uaNodeIdStr); + ASSERT_TRUE(nodeIdStr2 == uaNodeIdStr); + + UA_NodeId_clear(&uaNodeIdStr); +} + +TEST_F(OpcUaNodeIdTest, TestLessOperator) +{ + OpcUaNodeId nodeIdStr1(1, "2"); + OpcUaNodeId nodeIdStr2(1, "2"); + OpcUaNodeId nodeIdStr3(1, "2222"); + OpcUaNodeId nodeIdStr4(2, "2"); + + OpcUaNodeId nodeIdInt1(1, 2); + OpcUaNodeId nodeIdInt2(1, 2); + OpcUaNodeId nodeIdInt3(1, 3); + OpcUaNodeId nodeIdInt4(2, 2); + + ASSERT_NE(nodeIdStr1 < nodeIdStr3, nodeIdStr3 < nodeIdStr1); + ASSERT_NE(nodeIdStr1 < nodeIdStr4, nodeIdStr4 < nodeIdStr1); + + ASSERT_NE(nodeIdInt1 < nodeIdInt3, nodeIdInt3 < nodeIdInt1); + ASSERT_NE(nodeIdInt1 < nodeIdInt4, nodeIdInt4 < nodeIdInt1); + + ASSERT_NE(nodeIdInt1 < nodeIdStr1, nodeIdStr1 < nodeIdInt1); + + ASSERT_FALSE(nodeIdInt1 < nodeIdInt1); + ASSERT_FALSE(nodeIdInt1 < nodeIdInt2); + ASSERT_FALSE(nodeIdInt2 < nodeIdInt1); + + ASSERT_FALSE(nodeIdStr1 < nodeIdStr1); + ASSERT_FALSE(nodeIdStr1 < nodeIdStr2); + ASSERT_FALSE(nodeIdStr2 < nodeIdStr1); +} + +TEST_F(OpcUaNodeIdTest, TestToString) +{ + OpcUaNodeId nodeIdStr(1, "2"); + OpcUaNodeId nodeIdInt(1, 2); + + ASSERT_EQ(nodeIdStr.toString(), "(1, 2)"); + ASSERT_EQ(nodeIdInt.toString(), "(1, 2)"); +} + +TEST_F(OpcUaNodeIdTest, TestInstantiateNode) +{ + OpcUaNodeId nodeIdInt = OpcUaNodeId::instantiateNode(2, "4", OpcUaIdentifierType::Numeric); + OpcUaNodeId nodeIdStr = OpcUaNodeId::instantiateNode(2, "4", OpcUaIdentifierType::String); + + ASSERT_EQ(nodeIdInt, OpcUaNodeId(2, 4)); + ASSERT_EQ(nodeIdStr, OpcUaNodeId(2, "4")); +} + +TEST_F(OpcUaNodeIdTest, TestShallowCopy) +{ + OpcUaNodeId nodeid = OpcUaNodeId(UA_TYPES[UA_TYPES_INT64].typeId, true); +} + +TEST_F(OpcUaNodeIdTest, HashTest) +{ + OpcUaNodeId nodeIdStr1(1, "str1"); + OpcUaNodeId nodeIdStr2(1, "str2"); + OpcUaNodeId nodeIdStr3(1, "str2"); + + OpcUaNodeId nodeIdInt1(1, 2); + OpcUaNodeId nodeIdInt2(1, 2); + + ASSERT_NE(std::hash{}(nodeIdStr1), std::hash{}(nodeIdStr2)); + ASSERT_EQ(std::hash{}(nodeIdStr2), std::hash{}(nodeIdStr3)); + ASSERT_NE(std::hash{}(nodeIdStr1), std::hash{}(nodeIdInt1)); + ASSERT_EQ(std::hash{}(nodeIdInt1), std::hash{}(nodeIdInt2)); +} + +TEST_F(OpcUaNodeIdTest, CreateWithRandomGuid) +{ + OpcUaNodeId nodeId = OpcUaNodeId::CreateWithRandomGuid(); + ASSERT_EQ(nodeId.getIdentifierType(), OpcUaIdentifierType::Guid); +} + +TEST_F(OpcUaNodeIdTest, TestIdentifierNumeric) +{ + auto node1 = OpcUaNodeId::instantiateNode(1, "-999", OpcUaIdentifierType::Numeric); + ASSERT_EQ(node1.getIdentifier(), "0"); + + auto node2 = OpcUaNodeId::instantiateNode(1, "4259502693", OpcUaIdentifierType::Numeric); + ASSERT_EQ(node2.getIdentifier(), "4259502693"); + + auto node3 = OpcUaNodeId::instantiateNode(1, " 756", OpcUaIdentifierType::Numeric); + ASSERT_EQ(node3.getIdentifier(), "0"); + + auto node4 = OpcUaNodeId::instantiateNode(1, "ab 756 t 89", OpcUaIdentifierType::Numeric); + ASSERT_EQ(node4.getIdentifier(), "0"); + + auto node6 = OpcUaNodeId::instantiateNode(1, " 3", OpcUaIdentifierType::Numeric); + ASSERT_EQ(node6.getIdentifier(), "0"); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_opcuaversion.cpp b/shared/libraries/opcua/opcuashared/tests/test_opcuaversion.cpp new file mode 100644 index 0000000..86d5ab1 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_opcuaversion.cpp @@ -0,0 +1,99 @@ +#include "gtest/gtest.h" +#include "opcuashared/opcuaversion.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaVersionTest = testing::Test; + +TEST_F(OpcUaVersionTest, CreateDefault) +{ + OpcUaVersion version; + ASSERT_EQ(version.major, 0); + ASSERT_EQ(version.minor, 0); + ASSERT_EQ(version.patch, 0); + + ASSERT_EQ(version.toString(), "0.0.0"); +} + +TEST_F(OpcUaVersionTest, Create) +{ + OpcUaVersion version(10, 20, 30); + ASSERT_EQ(version.major, 10); + ASSERT_EQ(version.minor, 20); + ASSERT_EQ(version.patch, 30); + + ASSERT_EQ(version.toString(), "10.20.30"); +} + +TEST_F(OpcUaVersionTest, CreateByMembers) +{ + OpcUaVersion version{10, 20}; + ASSERT_EQ(version.major, 10); + ASSERT_EQ(version.minor, 20); + ASSERT_EQ(version.patch, 0); + + ASSERT_EQ(version.toString(), "10.20.0"); +} + +TEST_F(OpcUaVersionTest, CreateFromString) +{ + OpcUaVersion version("10.20.30"); + ASSERT_EQ(version.major, 10); + ASSERT_EQ(version.minor, 20); + ASSERT_EQ(version.patch, 30); + + ASSERT_EQ(version.toString(), "10.20.30"); +} + +TEST_F(OpcUaVersionTest, CreateFromErrString) +{ + OpcUaVersion version("dummy"); + ASSERT_EQ(version.major, 0); + ASSERT_EQ(version.minor, 0); + ASSERT_EQ(version.patch, 0); + + ASSERT_EQ(version.toString(), "0.0.0"); +} + +TEST_F(OpcUaVersionTest, LongestToString) +{ + OpcUaVersion version((std::numeric_limits::min)(), (std::numeric_limits::min)(), (std::numeric_limits::min)()); + + ASSERT_EQ(version.toString(), "-2147483648.-2147483648.-2147483648"); +} + +TEST_F(OpcUaVersionTest, Compatible) +{ + OpcUaVersion v1(1, 1, 1); + OpcUaVersion v2(1, 1, 2); + OpcUaVersion v3(1, 2, 1); + OpcUaVersion v4(3, 2, 1); + + ASSERT_TRUE(OpcUaVersion::Compatible(v1, v2)); + ASSERT_TRUE(OpcUaVersion::Compatible(v2, v1)); + + ASSERT_TRUE(OpcUaVersion::Compatible(v1, v3)); + ASSERT_FALSE(OpcUaVersion::Compatible(v3, v1)); + + ASSERT_FALSE(OpcUaVersion::Compatible(v1, v4)); + ASSERT_FALSE(OpcUaVersion::Compatible(v4, v1)); +} + +TEST_F(OpcUaVersionTest, HasFeature) +{ + OpcUaVersion featureVersion(2, 4, 0); + + OpcUaVersion serverVersion1(1, 6, 1); + OpcUaVersion serverVersion2(2, 2, 2); + OpcUaVersion serverVersion3(2, 4, 5); + OpcUaVersion serverVersion4(2, 6, 1); + OpcUaVersion serverVersion5(3, 6, 1); + + ASSERT_FALSE(OpcUaVersion::HasFeature(serverVersion1, featureVersion)); + ASSERT_FALSE(OpcUaVersion::HasFeature(serverVersion2, featureVersion)); + ASSERT_TRUE(OpcUaVersion::HasFeature(serverVersion3, featureVersion)); + ASSERT_TRUE(OpcUaVersion::HasFeature(serverVersion4, featureVersion)); + ASSERT_TRUE(OpcUaVersion::HasFeature(serverVersion5, featureVersion)); // it has feature, but u can remove backword compatibility code +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_status_code.cpp b/shared/libraries/opcua/opcuashared/tests/test_status_code.cpp new file mode 100644 index 0000000..11306c5 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_status_code.cpp @@ -0,0 +1,38 @@ +#include "gtest/gtest.h" +#include "opcuashared/opcua.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using StatusCodeTest = testing::Test; + +TEST_F(StatusCodeTest, Succeeded) +{ + ASSERT_TRUE(OPCUA_STATUSCODE_SUCCEEDED(UA_STATUSCODE_GOOD)); + ASSERT_TRUE(OPCUA_STATUSCODE_SUCCEEDED(UA_STATUSCODE_GOODNODATA)); + ASSERT_FALSE(OPCUA_STATUSCODE_SUCCEEDED(UA_STATUSCODE_BADALREADYEXISTS)); +} + +TEST_F(StatusCodeTest, Failed) +{ + ASSERT_FALSE(OPCUA_STATUSCODE_FAILED(UA_STATUSCODE_GOOD)); + ASSERT_FALSE(OPCUA_STATUSCODE_FAILED(UA_STATUSCODE_GOODNODATA)); + ASSERT_TRUE(OPCUA_STATUSCODE_FAILED(UA_STATUSCODE_BADALREADYEXISTS)); +} + +TEST_F(StatusCodeTest, IsGood) +{ + ASSERT_TRUE(OPCUA_STATUSCODE_IS_GOOD(UA_STATUSCODE_GOOD)); + ASSERT_FALSE(OPCUA_STATUSCODE_IS_GOOD(UA_STATUSCODE_GOODNODATA)); + ASSERT_FALSE(OPCUA_STATUSCODE_IS_GOOD(UA_STATUSCODE_BADALREADYEXISTS)); +} + +TEST_F(StatusCodeTest, NotConnected) +{ + ASSERT_FALSE(OPCUA_STATUSCODE_NOT_CONNECTED(UA_STATUSCODE_GOOD)); + ASSERT_FALSE(OPCUA_STATUSCODE_NOT_CONNECTED(UA_STATUSCODE_GOODNODATA)); + ASSERT_FALSE(OPCUA_STATUSCODE_NOT_CONNECTED(UA_STATUSCODE_BADALREADYEXISTS)); + ASSERT_TRUE(OPCUA_STATUSCODE_NOT_CONNECTED(UA_STATUSCODE_BADDISCONNECT)); + ASSERT_TRUE(OPCUA_STATUSCODE_NOT_CONNECTED(UA_STATUSCODE_BADCONNECTIONCLOSED)); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tests/test_string_utils.cpp b/shared/libraries/opcua/opcuashared/tests/test_string_utils.cpp new file mode 100644 index 0000000..166d234 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tests/test_string_utils.cpp @@ -0,0 +1,50 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaStringTest = testing::Test; + +TEST_F(OpcUaStringTest, Compare) +{ + UA_String first = UA_STRING_STATIC("a"); + UA_String second = UA_STRING_STATIC("b"); + UA_String third = UA_STRING_STATIC("a"); + + ASSERT_TRUE(first == third); + ASSERT_FALSE(first == second); + + ASSERT_FALSE(first != third); + ASSERT_TRUE(first != second); +} + +TEST_F(OpcUaStringTest, CompareConstChar) +{ + UA_String first = UA_STRING_STATIC("a"); + ASSERT_TRUE(first == "a"); + ASSERT_FALSE(first != "a"); + ASSERT_FALSE(first == "b"); + ASSERT_TRUE(first != "b"); + + ASSERT_TRUE("a" == first); + ASSERT_FALSE("a" != first); + ASSERT_FALSE("b" == first); + ASSERT_TRUE("b" != first); +} + +TEST_F(OpcUaStringTest, ToStdString) +{ + UA_String test = UA_STRING_STATIC("test"); + std::string testStr = utils::ToStdString(test); + ASSERT_EQ(testStr, "test"); +} + +TEST_F(OpcUaStringTest, ToStdStringNull) +{ + UA_String test{}; + std::string testStr = utils::ToStdString(test); + ASSERT_EQ(testStr, ""); +} + + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcua/opcuashared/tools/schema/BB.NodeIds.csv b/shared/libraries/opcua/opcuashared/tools/schema/BB.NodeIds.csv new file mode 100644 index 0000000..35317ab --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tools/schema/BB.NodeIds.csv @@ -0,0 +1,4 @@ +BufferValue,1,DataType +BufferValue_Encoding_DefaultBinary,1,Object +PropertyInfoDataType,2,DataType +PropertyInfoDataType_Encoding_DefaultBinary,2,Object \ No newline at end of file diff --git a/shared/libraries/opcua/opcuashared/tools/schema/BB.Opc.Ua.Types.bsd b/shared/libraries/opcua/opcuashared/tools/schema/BB.Opc.Ua.Types.bsd new file mode 100644 index 0000000..3409033 --- /dev/null +++ b/shared/libraries/opcua/opcuashared/tools/schema/BB.Opc.Ua.Types.bsd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shared/libraries/opcua/tests/CMakeLists.txt b/shared/libraries/opcua/tests/CMakeLists.txt new file mode 100644 index 0000000..0ecb069 --- /dev/null +++ b/shared/libraries/opcua/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(OpcUaTests CXX) + +add_subdirectory(testopcua) diff --git a/shared/libraries/opcua/tests/testopcua/CMakeLists.txt b/shared/libraries/opcua/tests/testopcua/CMakeLists.txt new file mode 100644 index 0000000..e573e21 --- /dev/null +++ b/shared/libraries/opcua/tests/testopcua/CMakeLists.txt @@ -0,0 +1,29 @@ +set(MODULE_NAME opcua) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES + main.cpp + test_opcua_security.cpp + test_opcua_authentication.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES}) + +target_link_libraries(${TEST_APP} PRIVATE opcuashared + opcuaserver + opcuaclient + daq::test_utils +) + +if (MSVC) + target_compile_definitions(${TEST_APP} PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}testcoverage ${TEST_APP} ${MODULE_NAME}testcoverage) +endif() diff --git a/shared/libraries/opcua/tests/testopcua/main.cpp b/shared/libraries/opcua/tests/testopcua/main.cpp new file mode 100644 index 0000000..a4ca297 --- /dev/null +++ b/shared/libraries/opcua/tests/testopcua/main.cpp @@ -0,0 +1,13 @@ +#include +#include + +int main(int argc, char** args) +{ + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new MemCheckListener()); + + int res = RUN_ALL_TESTS(); + return res; +} diff --git a/shared/libraries/opcua/tests/testopcua/test_opcua_authentication.cpp b/shared/libraries/opcua/tests/testopcua/test_opcua_authentication.cpp new file mode 100644 index 0000000..ac54d36 --- /dev/null +++ b/shared/libraries/opcua/tests/testopcua/test_opcua_authentication.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include + +using namespace daq; +using namespace daq::opcua; + +using OpcUaAuthenticationTest = testing::Test; + +TEST_F(OpcUaAuthenticationTest, DefaultConnect) +{ + auto server = OpcUaServer(); + server.start(); + + auto client = OpcUaClient("opc.tcp://127.0.0.1"); + client.connect(); + ASSERT_TRUE(client.isConnected()); +} + +TEST_F(OpcUaAuthenticationTest, NoAnonymous) +{ + auto server = OpcUaServer(); + server.setPort(4840); + server.setAuthenticationProvider(AuthenticationProvider(false)); + server.start(); + + auto client = OpcUaClient("opc.tcp://127.0.0.1"); + ASSERT_THROW(client.connect(), OpcUaException); +} + +TEST_F(OpcUaAuthenticationTest, AuthenticationProvider) +{ + auto users = List(); + users.pushBack(User("jure", "jure123")); + users.pushBack(User("tomaz", "tomaz123")); + auto authenticationProvider = StaticAuthenticationProvider(true, users); + + auto server = OpcUaServer(); + server.setPort(4840); + server.setAuthenticationProvider(authenticationProvider); + server.start(); + + OpcUaClientPtr client; + + client = std::make_shared("opc.tcp://127.0.0.1"); + ASSERT_NO_THROW(client->connect()); + + client = std::make_shared(OpcUaEndpoint("opc.tcp://127.0.0.1", "jure", "wrongPass")); + ASSERT_THROW(client->connect(), OpcUaException); + + client = std::make_shared(OpcUaEndpoint("opc.tcp://127.0.0.1", "jure", "jure123")); + ASSERT_NO_THROW(client->connect()); + + client = std::make_shared(OpcUaEndpoint("opc.tcp://127.0.0.1", "tomaz", "tomaz123")); + ASSERT_NO_THROW(client->connect()); +} diff --git a/shared/libraries/opcua/tests/testopcua/test_opcua_security.cpp b/shared/libraries/opcua/tests/testopcua/test_opcua_security.cpp new file mode 100644 index 0000000..4ad9700 --- /dev/null +++ b/shared/libraries/opcua/tests/testopcua/test_opcua_security.cpp @@ -0,0 +1,566 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "opcuashared/opcuasecuritycommon.h" +#include + +using namespace daq::utils; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +using OpcUaSecurityTest = testing::Test; + +static std::string TestFile(std::string name) +{ + return CmakeGlobals::ResDirFile("testKeys/keys/" + name); +} + +class TestServer +{ +public: + TestServer() + { + server = std::make_shared(); + server->setPort(4840); + } + + ~TestServer() + { + if (isStarted()) + server->stop(); + nodes.clear(); + } + + void setSecurityConfig(OpcUaServerSecurityConfig* config) + { + server->setSecurityConfig(config); + } + + void start() + { + server->start(); + } + + void stop() + { + server->stop(); + } + + bool isStarted() + { + return server->getStarted(); + } + + std::shared_ptr getServer() + { + return server; + } + + static OpcUaServerSecurityConfig CreateSecurityConfig() + { + OpcUaServerSecurityConfig config; + config.certificate = utils::LoadFile(TestFile("server-cert.der")); + config.privateKey = utils::LoadFile(TestFile("server-private.der")); + config.trustList.push_back(utils::LoadFile(TestFile("client-cert.der")).getValue()); + config.appUri = "urn:dewesoft.com"; + config.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + config.trustAll = false; + return config; + } + + static std::unordered_map CreateUsers() + { + BCrypt bcrypt; + unsigned int rounds = 8; + std::unordered_map users; + users["newton"] = bcrypt.hash("newton123", rounds); + users["galileo"] = bcrypt.hash("galileo123", rounds); + users["tesla"] = bcrypt.hash("tesla123", rounds); + users["pascal"] = bcrypt.hash("pascal123", rounds); + return users; + } + +private: + std::shared_ptr server; + std::vector nodes; +}; + +class TestClient +{ +public: + TestClient() + { + securityConfig = NULL; + } + + void setSecurityConfig(OpcUaClientSecurityConfig* config) + { + securityConfig = config; + } + + void connect() + { + OpcUaEndpoint endpoint("opc.tcp://localhost:4840/"); + //endpoint.setSecurityConfig(securityConfig); + + client = std::make_shared(endpoint); + + if (timeout > 0) + client->setTimeout(timeout); + + client->connect(); + } + + void disconnect() + { + client->disconnect(); + client = nullptr; + } + + void setTimeout(int timeoutMs) + { + this->timeout = timeoutMs; + } + + bool isConnected() + { + return client && client->isConnected(); + } + + bool nodeExist(const OpcUaNodeId& nodeId) + { + auto result = client->nodeExists(nodeId); + return result; + } + + OpcUaClientPtr getClient() + { + return client; + } + + static OpcUaClientSecurityConfig CreateSecurityConfig() + { + OpcUaClientSecurityConfig config; + config.certificate = utils::LoadFile(TestFile("client-cert.der")); + config.privateKey = utils::LoadFile(TestFile("client-private.der")); + config.trustList.push_back(utils::LoadFile(TestFile("server-cert.der")).getValue()); + config.appUri = "urn:testclient.com"; + config.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + config.trustAll = false; + return config; + } + +private: + OpcUaClientPtr client; + OpcUaClientSecurityConfig* securityConfig; + int timeout = -1; +}; + +TEST_F_OPTIONAL(OpcUaSecurityTest, LoadCertificateTest) +{ + OpcUaObject cert; + + ASSERT_NO_THROW(cert = utils::LoadFile(TestFile("client-cert.der"))); + ASSERT_GT(cert.getValue().length, (size_t) 0); + + ASSERT_ANY_THROW(cert = utils::LoadFile(TestFile("client-cert-missing.der"))); + + ASSERT_NO_THROW(cert = utils::LoadFile(TestFile("garbage-cert.der"))); + ASSERT_GT(cert.getValue().length, (size_t) 0); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, SecurityConfigMemoryTest) +{ + OpcUaClientSecurityConfig a; + a.certificate = utils::LoadFile(TestFile("client-cert.der")).getValue(); + + a.trustList.push_back(utils::LoadFile(TestFile("server-cert.der")).getValue()); + a.trustList[0] = utils::LoadFile(TestFile("client-cert.der")).getValue(); + + a.appUri = "urn:testclient.com"; + a.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + + OpcUaClientSecurityConfig b = a; +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, SecurityModeNoneTest) +{ + OpcUaServerSecurityConfig serverSecurity; + serverSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + ASSERT_NO_THROW(testClient.nodeExist(OpcUaNodeId(UA_NS0ID_SERVER))); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, AuthenticationTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + +#ifndef OPCUA_ENABLE_ENCRYPTION + serverSecurity.certificate = UA_BYTESTRING_NULL; + serverSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + clientSecurity.certificate = UA_BYTESTRING_NULL; + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; +#endif + + std::unordered_map users = TestServer::CreateUsers(); + serverSecurity.authenticateUser = [&users](bool isAnonymous, std::string username, std::string password) -> UA_StatusCode { + if (!isAnonymous && users.count(username)) + { + std::string hash = users[username]; + if (BCrypt::Verify(hash, password)) + return UA_STATUSCODE_GOOD; + } + + return UA_STATUSCODE_BADUSERACCESSDENIED; + }; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username = "tesla"; + clientSecurity.password = "wrongPassword"; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username = "wrongUser"; + clientSecurity.password = "tesla123"; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username.reset(); + clientSecurity.password = "tesla123"; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username = ""; + clientSecurity.password = "tesla123"; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username = "tesla"; + clientSecurity.password = ""; + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username = "tesla"; + clientSecurity.password.reset(); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + clientSecurity.username = "tesla"; + clientSecurity.password = "tesla123"; + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +#ifdef OPCUA_ENABLE_ENCRYPTION + +TEST_F_OPTIONAL(OpcUaSecurityTest, SecurityModeSignEncryptTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_TRUE(testClient.isConnected()); + + ASSERT_NO_THROW(testClient.nodeExist(UA_NS0ID_SERVER)); + + testClient.disconnect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, WrongSecurityModeTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_NONE; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, ExpiredCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.trustList.push_back(LoadFile(TestFile("client-cert-expired.der")).getValue()); + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.trustList.push_back(LoadFile(TestFile("server-cert.der")).getValue()); + clientSecurity.certificate = LoadFile(TestFile("client-cert-expired.der")); + clientSecurity.privateKey = LoadFile(TestFile("client-private-expired.der")); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + clientSecurity.appUri = "urn:testclient.com"; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, GarbageCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.trustList.push_back(LoadFile(TestFile("server-cert.der")).getValue()); + clientSecurity.certificate = LoadFile(TestFile("garbage-cert.der")).getValue(); + clientSecurity.privateKey = LoadFile(TestFile("client-private.der")).getValue(); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + clientSecurity.appUri = "urn:testclient.com"; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + serverSecurity.trustList.push_back(LoadFile(TestFile("garbage-cert.der")).getValue()); + testServer.setSecurityConfig(&serverSecurity); + ASSERT_THROW(testServer.start(), OpcUaException); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, UntrustedCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + + OpcUaClientSecurityConfig clientSecurity; + clientSecurity.trustList.push_back(LoadFile(TestFile("server-cert.der")).getValue()); + clientSecurity.certificate = LoadFile(TestFile("tesla-cert.der")); + clientSecurity.privateKey = LoadFile(TestFile("tesla-private.der")); + clientSecurity.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + clientSecurity.appUri = "urn:nikolatesla.com"; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + TestClient testClient; + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + ASSERT_FALSE(testClient.isConnected()); + + testServer.stop(); + serverSecurity.trustList.push_back(LoadFile(TestFile("tesla-cert.der")).getValue()); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + testClient.connect(); + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, RevokedCertificateTest) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.trustList.clear(); + serverSecurity.trustList.push_back(LoadFile(TestFile("client-cert.der")).getValue()); + serverSecurity.trustList.push_back(LoadFile(TestFile("tesla-cert.der")).getValue()); + serverSecurity.revocationList.clear(); + serverSecurity.revocationList.push_back(LoadFile(TestFile("../ca/root.crl")).getValue()); + + auto tryConnectClient = [](std::string prefix) { + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + clientSecurity.certificate = LoadFile(TestFile(prefix + "-cert.der")); + clientSecurity.privateKey = LoadFile(TestFile(prefix + "-private.der")); + + TestClient client; + client.setSecurityConfig(&clientSecurity); + client.connect(); + bool connected = client.isConnected(); + client.disconnect(); + return connected; + }; + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + + ASSERT_TRUE(tryConnectClient("client")); + ASSERT_FALSE(tryConnectClient("tesla")); // todo: fix revocation list issue + + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, AppUriFromCertificate) +{ + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.appUri.reset(); + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + ASSERT_TRUE(testServer.isStarted()); + testServer.stop(); + ASSERT_FALSE(testServer.isStarted()); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, ParseCertificateUri) +{ + OpcUaObject serverCert = LoadFile(TestFile("server-cert.der")); + OpcUaObject clientCert = LoadFile(TestFile("client-cert.der")); + OpcUaObject garbageCert = LoadFile(TestFile("garbage-cert.der")); + + std::optional urn; + urn = OpcUaSecurityCommon::parseCertificateUri(serverCert.getValue()); + ASSERT_EQ(urn.value_or(""), "urn:dewesoft.com"); + urn = OpcUaSecurityCommon::parseCertificateUri(clientCert.getValue()); + ASSERT_EQ(urn.value_or(""), "urn:testclient.com"); + urn = OpcUaSecurityCommon::parseCertificateUri(garbageCert.getValue()); + ASSERT_EQ(urn.value_or(""), ""); +} + +TEST_F_OPTIONAL(OpcUaSecurityTest, TrustAllTest) +{ + OpcUaObject clientCert = LoadFile(TestFile("client-cert.der")); + OpcUaObject serverCert = LoadFile(TestFile("server-cert.der")); + + auto tryConnect = [](bool serverTrustAll, + std::vector serverTrustList, + bool clientTrustAll, + std::vector clientTrustList) { + OpcUaServerSecurityConfig serverSecurity = TestServer::CreateSecurityConfig(); + serverSecurity.trustAll = serverTrustAll; + serverSecurity.trustList.clear(); + for (size_t i = 0; i < serverTrustList.size(); i++) + serverSecurity.trustList.push_back(serverTrustList[i]); + + OpcUaClientSecurityConfig clientSecurity = TestClient::CreateSecurityConfig(); + clientSecurity.trustAll = clientTrustAll; + clientSecurity.trustList.clear(); + for (size_t i = 0; i < clientTrustList.size(); i++) + clientSecurity.trustList.push_back(clientTrustList[i]); + + TestServer testServer; + testServer.setSecurityConfig(&serverSecurity); + testServer.start(); + + TestClient testClient; + testClient.setTimeout(1000); + testClient.setSecurityConfig(&clientSecurity); + testClient.connect(); + bool connected = testClient.isConnected(); + testClient.disconnect(); + testServer.stop(); + + return connected; + }; + + bool connected; + + connected = tryConnect(false, {}, false, {}); + ASSERT_FALSE(connected); + connected = tryConnect(false, {clientCert.getValue()}, false, {}); + ASSERT_FALSE(connected); + connected = tryConnect(false, {}, false, {serverCert.getValue()}); + ASSERT_FALSE(connected); + connected = tryConnect(false, {clientCert.getValue()}, false, {serverCert.getValue()}); + ASSERT_TRUE(connected); + + connected = tryConnect(true, {}, false, {}); + ASSERT_FALSE(connected); + connected = tryConnect(true, {clientCert.getValue()}, false, {}); + ASSERT_FALSE(connected); + connected = tryConnect(true, {}, false, {serverCert.getValue()}); + ASSERT_TRUE(connected); + connected = tryConnect(true, {clientCert.getValue()}, false, {serverCert.getValue()}); + ASSERT_TRUE(connected); + + connected = tryConnect(false, {}, true, {}); + ASSERT_FALSE(connected); + connected = tryConnect(false, {clientCert.getValue()}, true, {}); + ASSERT_TRUE(connected); + connected = tryConnect(false, {}, true, {serverCert.getValue()}); + ASSERT_FALSE(connected); + connected = tryConnect(false, {clientCert.getValue()}, true, {serverCert.getValue()}); + ASSERT_TRUE(connected); + + connected = tryConnect(true, {}, true, {}); + ASSERT_TRUE(connected); + connected = tryConnect(true, {clientCert.getValue()}, true, {}); + ASSERT_TRUE(connected); + connected = tryConnect(true, {}, true, {serverCert.getValue()}); + ASSERT_TRUE(connected); + connected = tryConnect(true, {clientCert.getValue()}, true, {serverCert.getValue()}); + ASSERT_TRUE(connected); +} + +#endif // OPCUA_ENABLE_ENCRYPTION + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/CMakeLists.txt b/shared/libraries/opcuatms/CMakeLists.txt new file mode 100644 index 0000000..4300b51 --- /dev/null +++ b/shared/libraries/opcuatms/CMakeLists.txt @@ -0,0 +1,9 @@ +set_cmake_folder_context(TARGET_FOLDER_NAME) + +add_subdirectory(opcuatms) +add_subdirectory(opcuatms_server) +add_subdirectory(opcuatms_client) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcuatms/opcuatms/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms/CMakeLists.txt new file mode 100644 index 0000000..b7bd299 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcuatms VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/converter_maps.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converter_maps.h new file mode 100644 index 0000000..9b997c4 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converter_maps.h @@ -0,0 +1,329 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +namespace converters +{ + static OpcUaVariant convertToVariant(IntfID interfaceId, + const BaseObjectPtr& object, + const UA_DataType* targetType, + const ContextPtr& context); + static OpcUaVariant convertToArrayVariant(IntfID elementId, + const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& context); + static BaseObjectPtr convertToDaqObject(const OpcUaVariant& variant, const ContextPtr& context); + static BaseObjectPtr convertToDaqList(const OpcUaVariant& variant, const ContextPtr& context); + + static std::unordered_map> + idToVariantMap{{INumber::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType); }}, + {IString::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IUnit::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IRatio::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IBoolean::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IInteger::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IFloat::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IDict::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IDataDescriptor::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IDimension::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IDimensionRule::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IDataRule::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IFunctionBlockType::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IScaling::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IArgumentInfo::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IRange::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IComplexNumber::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IStruct::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IEnumeration::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToVariant(object, targetType, ctx); }}, + {IList::Id, [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) { + return VariantConverter::ToArrayVariant(object, targetType, ctx); + }}}; + + static std::unordered_map&, const UA_DataType*, const ContextPtr&)>> + idToArrayVariantMap{{INumber::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IString::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IUnit::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IRatio::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IBoolean::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IInteger::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IFloat::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IDataDescriptor::Id, + [](const ListPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IDimension::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IDimensionRule::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IDataRule::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IFunctionBlockType::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IScaling::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IRange::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IComplexNumber::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IStruct::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IEnumeration::Id, + [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) + { return VariantConverter::ToArrayVariant(object, targetType, ctx); }}, + {IArgumentInfo::Id, [](const BaseObjectPtr& object, const UA_DataType* targetType, const ContextPtr& ctx) { + return VariantConverter::ToArrayVariant(object, targetType, ctx); + }}}; + + // Does not include DataRule and DimensionRule due to ambiguity issues. + // Does not include generic struct converter. + static std::unordered_map> uaTypeToDaqObject{ + {OpcUaNodeId(0, UA_NS0ID_BOOLEAN), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_FLOAT), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_SBYTE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_BYTE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_INT16), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_UINT16), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_INT32), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_UINT32), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_INT64), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_UINT64), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_STRING), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_LOCALIZEDTEXT), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_QUALIFIEDNAME), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_EUINFORMATIONWITHQUANTITY), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_EUINFORMATION), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_RATIONALNUMBER), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_RATIONALNUMBER64), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_DATADESCRIPTORSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_STRUCTDESCRIPTORSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_DIMENSIONDESCRIPTORSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_FUNCTIONBLOCKINFOSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) + { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_POSTSCALINGSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_LINEARSCALINGDESCRIPTIONSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_ARGUMENT), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_RANGE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_COMPLEXNUMBERTYPE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLECOMPLEXNUMBERTYPE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqObject(var, context); }}}; + + static std::unordered_map(const OpcUaVariant&, const ContextPtr& context)>> + uaTypeToDaqList{ + {OpcUaNodeId(0, UA_NS0ID_BOOLEAN), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_FLOAT), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_SBYTE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_BYTE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_INT16), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_UINT16), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_INT32), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_UINT32), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_INT64), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_UINT64), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_STRING), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_LOCALIZEDTEXT), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_QUALIFIEDNAME), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBT, UA_TYPES_DAQBT_EUINFORMATIONWITHQUANTITY), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_TYPES_EUINFORMATION), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_RATIONALNUMBER), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_RATIONALNUMBER64), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_DATADESCRIPTORSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_STRUCTDESCRIPTORSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_DIMENSIONDESCRIPTORSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_FUNCTIONBLOCKINFOSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_POSTSCALINGSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_LINEARSCALINGDESCRIPTIONSTRUCTURE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_ARGUMENT), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_RANGE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_COMPLEXNUMBERTYPE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLECOMPLEXNUMBERTYPE), + [](const OpcUaVariant& var, const ContextPtr& context) { return VariantConverter::ToDaqList(var, context); }}}; + + static OpcUaVariant convertToVariant(IntfID interfaceId, + const BaseObjectPtr& object, + const UA_DataType* targetType, + const ContextPtr& context) + { + if (const auto it = idToVariantMap.find(interfaceId); it != idToVariantMap.cend()) + return it->second(object, targetType, context); + + return {}; + } + + static OpcUaVariant convertToArrayVariant(IntfID elementId, + const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& context) + { + if (const auto it = idToArrayVariantMap.find(elementId); it != idToArrayVariantMap.cend()) + return it->second(list, targetType, context); + + return {}; + } + + static BaseObjectPtr convertToDaqObject(const OpcUaVariant& variant, const ContextPtr& context) + { + if (variant.isNull()) + return nullptr; + + const auto typeId = variant.getValue().type->typeId; + + if (const auto it = uaTypeToDaqObject.find(OpcUaNodeId(typeId)); it != uaTypeToDaqObject.cend()) + return it->second(variant, context); + + return nullptr; + } + + static BaseObjectPtr convertToDaqList(const OpcUaVariant& variant, const ContextPtr& context) + { + if (variant.isNull()) + return nullptr; + + const auto typeId = variant.getValue().type->typeId; + if (const auto it = uaTypeToDaqList.find(OpcUaNodeId(typeId)); it != uaTypeToDaqList.cend()) + return it->second(variant, context); + + return nullptr; + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/list_conversion_utils.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/list_conversion_utils.h new file mode 100644 index 0000000..7a16cbf --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/list_conversion_utils.h @@ -0,0 +1,173 @@ +/* + * 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_TMS + +template <> +OpcUaVariant VariantConverter::ToVariant(const BaseObjectPtr& object, + const UA_DataType* targetType, + const ContextPtr& context); + +class ListConversionUtils +{ +public: + template + static OpcUaVariant ToArrayVariant(const ListPtr& list, const ContextPtr& context = nullptr); + template + static ListPtr VariantToList(const OpcUaVariant& variant, const ContextPtr& context = nullptr); + + template + static OpcUaVariant ToExtensionObjectArrayVariant(const ListPtr& list, const ContextPtr& context = nullptr); + template + static ListPtr ExtensionObjectVariantToList(const OpcUaVariant& variant, const ContextPtr& context = nullptr); + + static OpcUaVariant ToVariantTypeArrayVariant(const ListPtr& list, const ContextPtr& context = nullptr); + static ListPtr VariantTypeArrayToList(const OpcUaVariant& variant, const ContextPtr& context = nullptr); +}; + +inline OpcUaVariant ListConversionUtils::ToVariantTypeArrayVariant(const ListPtr& list, const ContextPtr& context) +{ + constexpr auto type = GetUaDataType(); + const auto arr = static_cast(UA_Array_new(list.getCount(), type)); + + for (SizeT i = 0; i < list.getCount(); i++) + { + auto tmsStruct = VariantConverter::ToVariant(list.getItemAt(i), nullptr, context); + arr[i] = tmsStruct.getDetachedValue(); + } + + auto variant = OpcUaVariant(); + UA_Variant_setArray(variant.get(), arr, list.getCount(), type); + return variant; +} + +inline ListPtr ListConversionUtils::VariantTypeArrayToList(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (!variant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto list = List(); + const auto data = static_cast(variant->data); + + for (size_t i = 0; i < variant->arrayLength; i++) + { + OpcUaVariant arrayField = OpcUaVariant(data[i]); + const auto obj = VariantConverter::ToDaqObject(arrayField, context); + list.pushBack(obj); + } + + return list; +} + +template +OpcUaVariant ListConversionUtils::ToArrayVariant(const ListPtr& list, const ContextPtr& context) +{ + constexpr auto type = GetUaDataType(); + auto arr = static_cast(UA_Array_new(list.getCount(), type)); + try + { + for (SizeT i = 0; i < list.getCount(); i++) + { + auto tmsStruct = StructConverter::ToTmsType(list.getItemAt(i), context); + arr[i] = tmsStruct.getDetachedValue(); + } + } + catch (...) + { + UA_Array_delete(arr, list.getCount(), type); + throw; + } + + auto variant = OpcUaVariant(); + UA_Variant_setArray(variant.get(), arr, list.getCount(), type); + return variant; +} + +template +ListPtr ListConversionUtils::VariantToList(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (!variant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto data = static_cast(variant->data); + auto list = List(); + + for (size_t i = 0; i < variant->arrayLength; i++) + { + const auto obj = StructConverter::ToDaqObject(data[i], context); + list.pushBack(obj); + } + + return list; +} + +template +OpcUaVariant ListConversionUtils::ToExtensionObjectArrayVariant(const ListPtr& list, const ContextPtr& context) +{ + constexpr auto type = GetUaDataType(); + const auto arr = static_cast(UA_Array_new(list.getCount(), type)); + + try + { + for (SizeT i = 0; i < list.getCount(); i++) + { + auto variant = VariantConverter::ToVariant(list.getItemAt(i), nullptr, context); + auto extensionObject = ExtensionObject(variant); + arr[i] = extensionObject.getDetachedValue(); + } + } + catch(...) + { + UA_Array_delete(arr, list.getCount(), type); + throw; + } + + auto variant = OpcUaVariant(); + UA_Variant_setArray(variant.get(), static_cast(arr), list.getCount(), type); + return variant; +} + +template +ListPtr ListConversionUtils::ExtensionObjectVariantToList(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (!variant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const auto data = static_cast(variant->data); + auto list = List(); + + for (size_t i = 0; i < variant->arrayLength; i++) + { + auto extensionObject = ExtensionObject(data[i]); + BaseObjectPtr object = nullptr; + if (extensionObject.isDecoded()) + object = VariantConverter::ToDaqObject(extensionObject.getAsVariant(), context); + + list.pushBack(object); + } + + return list; +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/property_object_conversion_utils.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/property_object_conversion_utils.h new file mode 100644 index 0000000..448e986 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/property_object_conversion_utils.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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class PropertyObjectConversionUtils +{ +public: + static OpcUaVariant ToDictVariant(const PropertyObjectPtr& obj); + static void ToPropertyObject(const OpcUaVariant& variant, PropertyObjectPtr& objOut); + static PropertyObjectPtr ClonePropertyObject(const PropertyObjectPtr& obj); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/selection_converter.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/selection_converter.h new file mode 100644 index 0000000..bc13c5a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/selection_converter.h @@ -0,0 +1,40 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// New implementations of converters should always be specializations of VariantConverter and StructConverter. +// Selections are temporary exceptions. + +class SelectionVariantConverter +{ +public: + static BaseObjectPtr ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context = nullptr); + static OpcUaVariant ToVariant(const BaseObjectPtr& selectionValues, const ContextPtr& context = nullptr); + +private: + static OpcUaObject ToKeyValuePair(const IntegerPtr& key, const BaseObjectPtr& value, const ContextPtr& context); + static OpcUaVariant ListToVariant(const ListPtr& selectionValues, const ContextPtr& context); + static OpcUaVariant DictToVariant(const DictPtr& selectionValues, const ContextPtr& context); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/struct_converter.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/struct_converter.h new file mode 100644 index 0000000..76a8009 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/struct_converter.h @@ -0,0 +1,33 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +template ::SmartPtr> +class StructConverter +{ +public: + static BlueberryTypePtr ToDaqObject(const TmsType& tmsStruct, const ContextPtr& context = nullptr); + static OpcUaObject ToTmsType(const BlueberryTypePtr& object, const ContextPtr& context = nullptr); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/variant_converter.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/variant_converter.h new file mode 100644 index 0000000..7f3c35c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/converters/variant_converter.h @@ -0,0 +1,42 @@ +/* + * 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_TMS + +template ::SmartPtr> +class VariantConverter +{ +public: + static BlueberryTypePtr ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context = nullptr); + static OpcUaVariant ToVariant(const BlueberryTypePtr& object, + const UA_DataType* targetType = nullptr, + const ContextPtr& context = nullptr); + + static ListPtr ToDaqList(const OpcUaVariant& variant, const ContextPtr& context = nullptr); + static OpcUaVariant ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType = nullptr, + const ContextPtr& context = nullptr); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/core_types_utils.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/core_types_utils.h new file mode 100644 index 0000000..19565d7 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/core_types_utils.h @@ -0,0 +1,44 @@ +/* + * 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 + +StringPtr ConvertToDaqCoreString(const UA_String& uaString); +OpcUaObject ConvertToOpcUaString(const StringPtr& str); +BinaryDataPtr CreateCoreBinaryDataFromUaByteString(const UA_ByteString& uaByteString); +OpcUaObject CreateUaByteStringFromCoreBinaryData(const BinaryDataPtr& binaryData); +SampleType SampleTypeFromTmsEnum(UA_SampleTypeEnumeration tmsEnum); +UA_SampleTypeEnumeration SampleTypeToTmsEnum(SampleType daqEnum); +ScaledSampleType ScaledSampleTypeFromTmsEnum(UA_SampleTypeEnumeration tmsEnum); +UA_SampleTypeEnumeration ScaledSampleTypeToTmsEnum(ScaledSampleType daqEnum); +OpcUaNodeId CoreTypeToUANodeID(CoreType type); +CoreType UANodeIdToCoreType(OpcUaNodeId nodeId); +OpcUaVariant DecodeIfExtensionObject(const OpcUaVariant& variant); +OpcUaVariant UnwrapIfVariant(const OpcUaVariant& variant); +const UA_DataType* GetUAStructureDataTypeByName(const std::string& structName); +const UA_DataType* GetUAEnumerationDataTypeByName(const std::string& enumerationName); +const std::string GetUATypeName(UA_UInt16 namespaceIndex, UA_UInt32 identifierNumeric); +bool nativeStructConversionSupported(const std::string& structName); + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/errors.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/errors.h new file mode 100644 index 0000000..6c9e840 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/errors.h @@ -0,0 +1,24 @@ +/* + * 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 OPENDAQ_ERRTYPE_OPCUA 0x09 + +#define OPENDAQ_ERR_OPCUA_GENERAL OPENDAQ_ERROR_CODE(OPENDAQ_ERRTYPE_OPCUA, 0x001) +#define OPENDAQ_ERR_OPCUA_OBJECT_NOT_DECODED OPENDAQ_ERROR_CODE(OPENDAQ_ERRTYPE_OPCUA, 0x002) +#define OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE OPENDAQ_ERROR_CODE(OPENDAQ_ERRTYPE_OPCUA, 0x003) diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/exceptions.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/exceptions.h new file mode 100644 index 0000000..0dd4192 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/exceptions.h @@ -0,0 +1,28 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +DEFINE_EXCEPTION(OpcUaGeneral, OPENDAQ_ERR_OPCUA_GENERAL, "General OpcUa error") +DEFINE_EXCEPTION(OpcUaObjectNotDecoded, OPENDAQ_ERR_OPCUA_OBJECT_NOT_DECODED, "Extension object is not decoded.") +DEFINE_EXCEPTION(OpcUaClientCallNotAvailable, OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE, "This function call is not available/implemented for usage on connected-to OpcUa servers.") + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/extension_object.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/extension_object.h new file mode 100644 index 0000000..b49869c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/extension_object.h @@ -0,0 +1,50 @@ +/* + * 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_TMS + +class ExtensionObject : public OpcUaObject +{ +public: + using Super = OpcUaObject; + using Super::Super; + + ExtensionObject(); + ExtensionObject(const OpcUaObject& extensionObject); + ExtensionObject(const OpcUaVariant& variant); + + void setFromVariant(const OpcUaVariant& variant); + OpcUaVariant getAsVariant(); + bool isDecoded() const; + + template + inline bool isType() const + { + return this->value.content.decoded.type == GetUaDataType(); + } + + template + T* asType() + { + return (T*) this->value.content.decoded.data; + } +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/opcuatms.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/opcuatms.h new file mode 100644 index 0000000..25ec288 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/opcuatms.h @@ -0,0 +1,29 @@ +/* + * 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 + +#define BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS \ + namespace daq::opcua::tms \ + { +#define END_NAMESPACE_OPENDAQ_OPCUA_TMS } diff --git a/shared/libraries/opcuatms/opcuatms/include/opcuatms/type_mappings.h b/shared/libraries/opcuatms/opcuatms/include/opcuatms/type_mappings.h new file mode 100644 index 0000000..ed3c4ad --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/include/opcuatms/type_mappings.h @@ -0,0 +1,40 @@ +/* + * 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 daq::opcua +{ + ADD_CUSTOM_TYPE_MAPPING(UA_BaseRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_BASERULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_ExplicitDomainRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_EXPLICITDOMAINRULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_CustomRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CUSTOMRULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_ConstantRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CONSTANTRULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_LinearRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARRULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_ListRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LISTRULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_LogRuleDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LOGRULEDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_EUInformationWithQuantity, &UA_TYPES_DAQBT[UA_TYPES_DAQBT_EUINFORMATIONWITHQUANTITY]) + ADD_CUSTOM_TYPE_MAPPING(UA_DataDescriptorStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_DATADESCRIPTORSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_StructDescriptorStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_STRUCTDESCRIPTORSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_DimensionDescriptorStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_DIMENSIONDESCRIPTORSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_RationalNumber64, &UA_TYPES_DAQBT[UA_TYPES_DAQBT_RATIONALNUMBER64]) + ADD_CUSTOM_TYPE_MAPPING(UA_PostScalingStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_POSTSCALINGSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_LinearScalingDescriptionStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARSCALINGDESCRIPTIONSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_FunctionBlockInfoStructure, &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_FUNCTIONBLOCKINFOSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_DaqKeyValuePair, &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]) + ADD_CUSTOM_TYPE_MAPPING(UA_SelectionEntryStructure, &UA_TYPES_DAQBT[UA_TYPES_DAQBT_SELECTIONENTRYSTRUCTURE]) + ADD_CUSTOM_TYPE_MAPPING(UA_DeviceDomainStructure, &UA_TYPES_DAQDEVICE[UA_TYPES_DAQDEVICE_DEVICEDOMAINSTRUCTURE]) +} diff --git a/shared/libraries/opcuatms/opcuatms/src/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms/src/CMakeLists.txt new file mode 100644 index 0000000..2daf1ac --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/CMakeLists.txt @@ -0,0 +1,78 @@ +set(MODULE_NAME opcuatms) + +set(SRC_PublicHeaders errors.h + opcuatms.h + core_types_utils.h + type_mappings.h + extension_object.h + converter_maps.h +) + +set(SRC_PrivateHeaders exceptions.h +) + +set(SRC_Cpp core_types_utils.cpp + extension_object.cpp +) + +# converters + +set(CONVERTERS_SRC_DIR "converters") + +set(SRC_Converters_Headers ${CONVERTERS_SRC_DIR}/struct_converter.h + ${CONVERTERS_SRC_DIR}/variant_converter.h + ${CONVERTERS_SRC_DIR}/list_conversion_utils.h + ${CONVERTERS_SRC_DIR}/selection_converter.h + ${CONVERTERS_SRC_DIR}/property_object_conversion_utils.h +) + +set(SRC_Converters ${CONVERTERS_SRC_DIR}/range_converter.cpp + ${CONVERTERS_SRC_DIR}/unit_converter.cpp + ${CONVERTERS_SRC_DIR}/complex_number_converter.cpp + ${CONVERTERS_SRC_DIR}/number_converter.cpp + ${CONVERTERS_SRC_DIR}/data_rule_converter.cpp + ${CONVERTERS_SRC_DIR}/dimension_rule_converter.cpp + ${CONVERTERS_SRC_DIR}/ratio_converter.cpp + ${CONVERTERS_SRC_DIR}/data_descriptor_converter.cpp + ${CONVERTERS_SRC_DIR}/scaling_converter.cpp + ${CONVERTERS_SRC_DIR}/dimension_converter.cpp + ${CONVERTERS_SRC_DIR}/function_block_type_converter.cpp + ${CONVERTERS_SRC_DIR}/core_types_converter.cpp + ${CONVERTERS_SRC_DIR}/base_object_converter.cpp + ${CONVERTERS_SRC_DIR}/dict_converter.cpp + ${CONVERTERS_SRC_DIR}/selection_converter.cpp + ${CONVERTERS_SRC_DIR}/argument_converter.cpp + ${CONVERTERS_SRC_DIR}/generic_struct_converter.cpp + ${CONVERTERS_SRC_DIR}/generic_enumeration_converter.cpp + ${CONVERTERS_SRC_DIR}/property_object_conversion_utils.cpp +) + +set(SRC_PublicHeaders ${SRC_PublicHeaders} ${SRC_Converters_Headers}) +set(SRC_Cpp ${SRC_Cpp} ${SRC_Converters}) + +source_group("converters" "${CONVERTERS_SRC_DIR}/*") + +# /structs + +prepend_include(${MODULE_NAME} SRC_PublicHeaders) +prepend_include(${MODULE_NAME} SRC_PrivateHeaders) + +add_library(${MODULE_NAME} STATIC ${SRC_PublicHeaders} + ${SRC_PrivateHeaders} + ${SRC_Cpp} +) + +add_library(${SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) + +target_include_directories(${MODULE_NAME} PUBLIC $ + $ + $ +) + +target_link_libraries(${MODULE_NAME} PUBLIC daq::opendaq + daq::opcuashared + daq::opcua_daq_types +) + +set_target_properties(${MODULE_NAME} PROPERTIES PUBLIC_HEADER "${SRC_PublicHeaders}") +opendaq_set_output_lib_name(${MODULE_NAME} ${PROJECT_VERSION_MAJOR}) diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/argument_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/argument_converter.cpp new file mode 100644 index 0000000..db522f3 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/argument_converter.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class VariantConverter; + +// UA_Argument + +template <> +ArgumentInfoPtr StructConverter::ToDaqObject(const UA_Argument& tmsStruct, const ContextPtr& /*context*/) +{ + return ArgumentInfo(ConvertToDaqCoreString(tmsStruct.name), UANodeIdToCoreType(OpcUaNodeId(tmsStruct.dataType))); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const ArgumentInfoPtr& object, + const ContextPtr& /*context*/) +{ + if (!object.assigned()) + return {}; + + OpcUaObject uaArg; + uaArg->description = UA_LOCALIZEDTEXT_ALLOC("", ""); + uaArg->name = UA_STRING_ALLOC(object.getName().getCharPtr()); + + auto type = object.getType(); + if (type == ctList || type == ctDict) + throw InvalidTypeException{"The OPC UA server does not yet support list or dictionary type arguments."}; + uaArg->dataType = CoreTypeToUANodeID(object.getType()).getDetachedValue(); + + // TODO: handle list and dict + // https://www.open62541.org/doc/0.2/tutorial_server_method.html + uaArg->valueRank = -1; + + return uaArg; +} + +// Variant converter + +template <> +ArgumentInfoPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (!decodedVariant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return StructConverter::ToDaqObject(*static_cast(decodedVariant->data)); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const ArgumentInfoPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_ARGUMENT]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + return ListConversionUtils::VariantToList(variant); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_ARGUMENT]) + return ListConversionUtils::ToArrayVariant(list); + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/base_object_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/base_object_converter.cpp new file mode 100644 index 0000000..180bdab --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/base_object_converter.cpp @@ -0,0 +1,178 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Definitions + +template class VariantConverter; + +template <> +BaseObjectPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context); + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& context); + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& context); +template <> +OpcUaVariant VariantConverter::ToVariant(const BaseObjectPtr& object, + const UA_DataType* targetType, + const ContextPtr& context); + +END_NAMESPACE_OPENDAQ_OPCUA_TMS + +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Variant converter + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (variant.isNull()) + return nullptr; + + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant, context); + if (variant.isType()) + return ListConversionUtils::VariantTypeArrayToList(variant, context); + + const auto list = converters::convertToDaqList(variant, context); + if (list.assigned()) + return list; + + const auto typeKind = variant.getValue().type->typeKind; + if (typeKind == UA_DATATYPEKIND_STRUCTURE || typeKind == UA_DATATYPEKIND_OPTSTRUCT) + return VariantConverter::ToDaqList(variant, context); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +BaseObjectPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (variant.isNull()) + return nullptr; + + if (!variant.isScalar()) + { + if (variant.isType()) + { + const auto extensionObject = ExtensionObject(*static_cast(variant->data)); + if (extensionObject.isType()) + return VariantConverter::ToDaqObject(variant, context); + if (extensionObject.isType()) + return SelectionVariantConverter::ToDaqObject(variant, context); + } + else + { + if (variant.isType()) + return VariantConverter::ToDaqObject(variant, context); + if (variant.isType()) + return SelectionVariantConverter::ToDaqObject(variant, context); + } + + return ToDaqList(variant, context); + } + + auto decoded = DecodeIfExtensionObject(variant); + auto unwrapped = UnwrapIfVariant(decoded); + + if (unwrapped.isNull()) + return nullptr; + + const auto typeKind = unwrapped.getValue().type->typeKind; + if (typeKind == UA_DATATYPEKIND_ENUM ) + return VariantConverter::ToDaqObject(unwrapped, context); + + const auto obj = converters::convertToDaqObject(unwrapped, context); + if (obj.assigned()) + return obj; + + if (typeKind == UA_DATATYPEKIND_STRUCTURE || typeKind == UA_DATATYPEKIND_OPTSTRUCT) + return VariantConverter::ToDaqObject(unwrapped, context); + + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& context) +{ + if (targetType == &UA_TYPES[UA_TYPES_EXTENSIONOBJECT]) + return ListConversionUtils::ToExtensionObjectArrayVariant(list, context); + if (targetType == &UA_TYPES[UA_TYPES_VARIANT]) + return ListConversionUtils::ToVariantTypeArrayVariant(list, context); + + const auto elementType = list.asPtr(); + IntfID elementId; + elementType->getElementInterfaceId(&elementId); + + bool idUnknown = IntfID::Compare(elementId, IUnknown::Id) || IntfID::Compare(elementId, IBaseObject::Id); + if (idUnknown && list.getCount() > 0) + elementId = list[0].asPtr().getInterfaceIds()[0]; + + auto var = converters::convertToArrayVariant(elementId, list, targetType, context); + if (!var.isNull()) + return var; + + return ListConversionUtils::ToVariantTypeArrayVariant(list, context); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const BaseObjectPtr& object, + const UA_DataType* targetType, + const ContextPtr& context) +{ + if (!object.assigned()) + return {}; + + const auto ids = object.asPtr().getInterfaceIds(); + auto wrapConvertedValue = targetType == &UA_TYPES[UA_TYPES_EXTENSIONOBJECT] || targetType == &UA_TYPES[UA_TYPES_VARIANT]; + wrapConvertedValue = wrapConvertedValue && !object.supportsInterface(); + + if (wrapConvertedValue) + { + for (auto id : ids) + { + OpcUaVariant converted = converters::convertToVariant(id, object, nullptr, context); + if (converted.isNull()) + continue; + + OpcUaVariant wrapped; + if (targetType == &UA_TYPES[UA_TYPES_VARIANT]) + { + UA_Variant_setScalar(&wrapped.getValue(), converted.newDetachedPointer(), targetType); + } + else + { + auto extensionObject = ExtensionObject(converted); + UA_Variant_setScalar(&wrapped.getValue(), extensionObject.newDetachedPointer(), targetType); + } + + return wrapped; + } + } + else + { + for (auto id : ids) + { + OpcUaVariant converted = converters::convertToVariant(id, object, targetType, context); + if (!converted.isNull()) + return converted; + } + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/complex_number_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/complex_number_converter.cpp new file mode 100644 index 0000000..ec59d5c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/complex_number_converter.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class StructConverter; +template class VariantConverter; + +// UA_DoubleComplexNumberType + +template <> +ComplexNumberPtr StructConverter::ToDaqObject(const UA_DoubleComplexNumberType& tmsStruct, + const ContextPtr& /*context*/) +{ + return ComplexNumber(tmsStruct.real, tmsStruct.imaginary); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const ComplexNumberPtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject complex; + complex->real = object.getReal(); + complex->imaginary = object.getImaginary(); + return complex; +} + +// UA_ComplexNumberType + +template <> +ComplexNumberPtr StructConverter::ToDaqObject(const UA_ComplexNumberType& tmsStruct, + const ContextPtr& /*context*/) +{ + return ComplexNumber(tmsStruct.real, tmsStruct.imaginary); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const ComplexNumberPtr& object, + const ContextPtr& /*context*/) +{ + OpcUaObject complex; + complex->real = object.getReal(); + complex->imaginary = object.getImaginary(); + return complex; +} + +// Variant converter + +template <> +ComplexNumberPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + return StructConverter::ToDaqObject( + *static_cast(decodedVariant->data)); + if (decodedVariant.isType()) + return StructConverter::ToDaqObject( + *static_cast(decodedVariant->data)); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const ComplexNumberPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_DOUBLECOMPLEXNUMBERTYPE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_COMPLEXNUMBERTYPE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_DOUBLECOMPLEXNUMBERTYPE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_COMPLEXNUMBERTYPE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/core_types_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/core_types_converter.cpp new file mode 100644 index 0000000..a49d059 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/core_types_converter.cpp @@ -0,0 +1,401 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Bool + +template <> +BoolPtr StructConverter::ToDaqObject(const UA_Boolean& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const BoolPtr& object, const ContextPtr& /*context*/) +{ + return OpcUaObject(static_cast(object)); +} + +template <> +BoolPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isNull()) + return {}; + return variant.toBool(); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const BoolPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_BOOLEAN]) + return OpcUaVariant(static_cast(object)); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + return ListConversionUtils::VariantToList(variant); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_BOOLEAN]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +// Int + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_Int64& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_UInt64& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_Int32& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_UInt32& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_UInt16& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_Int16& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_Byte& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + const int64_t val = object; + return {static_cast(val)}; +} + +template <> +IntegerPtr StructConverter::ToDaqObject(const UA_SByte& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const IntegerPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +IntegerPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isNull()) + return {}; + return variant.toInteger(); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const IntegerPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_INT64]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_UINT64]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_INT32]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_UINT32]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_INT16]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_UINT16]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_BYTE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_SBYTE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_INT64]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_UINT64]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_INT32]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_UINT32]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_INT16]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_UINT16]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_BYTE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_SBYTE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +// Float + +template <> +FloatPtr StructConverter::ToDaqObject(const UA_Double& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const FloatPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + +template <> +FloatPtr StructConverter::ToDaqObject(const UA_Float& value, const ContextPtr& /*context*/) +{ + return value; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const FloatPtr& object, const ContextPtr& /*context*/) +{ + return {static_cast(object)}; +} + + +template <> +FloatPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return variant.toFloat(); + return variant.toDouble(); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const FloatPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_DOUBLE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_FLOAT]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_DOUBLE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_FLOAT]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +// String + +template <> +StringPtr StructConverter::ToDaqObject(const UA_String& value, const ContextPtr& /*context*/) +{ + return utils::ToStdString(value); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const StringPtr& object, const ContextPtr& /*context*/) +{ + return {UA_STRING_ALLOC(object.getCharPtr())}; +} + +template <> +StringPtr StructConverter::ToDaqObject(const UA_LocalizedText& value, const ContextPtr& /*context*/) +{ + return utils::ToStdString(value.text); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const StringPtr& object, const ContextPtr& /*context*/) +{ + return {UA_LOCALIZEDTEXT_ALLOC("", object.getCharPtr())}; +} + +template <> +StringPtr StructConverter::ToDaqObject(const UA_QualifiedName& value, const ContextPtr& /*context*/) +{ + return utils::ToStdString(value.name); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const StringPtr& object, const ContextPtr& /*context*/) +{ + return {UA_QUALIFIEDNAME_ALLOC(0, object.getCharPtr())}; +} + +template <> +StringPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isNull()) + return {}; + return variant.toString(); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const StringPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_STRING]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_QUALIFIEDNAME]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_STRING]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/data_descriptor_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/data_descriptor_converter.cpp new file mode 100644 index 0000000..099d0fa --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/data_descriptor_converter.cpp @@ -0,0 +1,320 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Definitions + +template class StructConverter; +template class StructConverter; +template class VariantConverter; + +template <> +DataDescriptorPtr StructConverter::ToDaqObject(const UA_StructDescriptorStructure& tmsStruct, + const ContextPtr& /*context*/); + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataDescriptorPtr& object, const ContextPtr& /*context*/); + +template <> +DataDescriptorPtr StructConverter::ToDaqObject(const UA_DataDescriptorStructure& tmsStruct, + const ContextPtr& /*context*/); + +template <> +DataDescriptorPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/); + +template <> +OpcUaVariant VariantConverter::ToVariant(const DataDescriptorPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/); + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/); + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/); + +template <> +DataDescriptorPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/); + +template <> +OpcUaVariant VariantConverter::ToVariant(const DataDescriptorPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/); + +template <> +DataDescriptorPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/); + +template <> +OpcUaVariant VariantConverter::ToVariant(const DataDescriptorPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/); + +// Helper methods + +static void WriteDimensions(const ListPtr& dimensions, + UA_DimensionDescriptorStructure*& dimensionsOut, + size_t& dimensionsSizeOut) +{ + if (!dimensions.assigned() || dimensions.getCount() == 0) + return; + + dimensionsSizeOut = dimensions.getCount(); + dimensionsOut = static_cast(UA_Array_new(dimensions.getCount(), GetUaDataType())); + + for (SizeT i = 0; i < dimensions.getCount(); i++) + { + auto tmsDimension = StructConverter::ToTmsType(dimensions[i]); + dimensionsOut[i] = tmsDimension.getDetachedValue(); + } +} + +static ListPtr ReadDimensions(const UA_DimensionDescriptorStructure* dimensions, size_t dimensionsSize) +{ + auto list = List(); + for (size_t i = 0; i < dimensionsSize; i++) + { + auto dimension = StructConverter::ToDaqObject(dimensions[i]); + list.pushBack(dimension); + } + return list; +} + +static void WriteMetadata(const DictPtr& metadata, + UA_KeyValuePair*& metadataOut, + size_t& dimensionsSizeOut) +{ + metadataOut = static_cast(UA_Array_new(metadata.getCount(), &UA_TYPES[UA_TYPES_KEYVALUEPAIR])); + dimensionsSizeOut = metadata.getCount(); + size_t index = 0; + + for (const auto& [name, value] : metadata) + { + OpcUaObject pair; + pair->key = UA_QUALIFIEDNAME_ALLOC(1, name.getCharPtr()); + pair->value = VariantConverter::ToVariant(value).getDetachedValue(); + metadataOut[index] = pair.getDetachedValue(); + + index++; + } +} + +static DictPtr ReadMetadata(const UA_KeyValuePair* metadata, size_t metadataSize) +{ + auto dict = Dict(); + if (!metadata) + return dict; + + for (size_t i = 0; i < metadataSize; i++) + { + const auto pair = &metadata[i]; + if (const auto variant = OpcUaVariant(pair->value); variant.isString()) + dict.set(ConvertToDaqCoreString(pair->key.name), variant.toString()); + } + return dict; +} + +// UA_StructDescriptorStructure + +template <> +DataDescriptorPtr StructConverter::ToDaqObject(const UA_StructDescriptorStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + auto members = List(); + for (size_t i = 0; i < tmsStruct.fieldsSize; i++) + { + auto memberObject = ExtensionObject(tmsStruct.fields[i]); + auto memberVariant = memberObject.getAsVariant(); + auto member = VariantConverter::ToDaqObject(memberVariant); + members.pushBack(member); + } + + const auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Struct) + .setName(ConvertToDaqCoreString(tmsStruct.name)) + .setDimensions(ReadDimensions(tmsStruct.dimensions, tmsStruct.dimensionsSize)) + .setMetadata(ReadMetadata(tmsStruct.metadata, tmsStruct.metadataSize)) + .setStructFields(members); + + return descriptor.build(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataDescriptorPtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject tmsStruct; + tmsStruct->name = ConvertToOpcUaString(object.getName()).getDetachedValue(); + + WriteDimensions(object.getDimensions(), tmsStruct->dimensions, tmsStruct->dimensionsSize); + WriteMetadata(object.getMetadata(), tmsStruct->metadata, tmsStruct->metadataSize); + + auto members = object.getStructFields(); + tmsStruct->fieldsSize = members.getCount(); + tmsStruct->fields = static_cast(UA_Array_new(members.getCount(), GetUaDataType())); + + for (SizeT i = 0; i < members.getCount(); i++) + { + auto member = members[i]; + auto variant = VariantConverter::ToVariant(member); + auto memberObject = ExtensionObject(variant); + tmsStruct->fields[i] = memberObject.getDetachedValue(); + } + + return tmsStruct; +} + +// UA_DataDescriptorStructure + +template <> +DataDescriptorPtr StructConverter::ToDaqObject(const UA_DataDescriptorStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + const auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleTypeFromTmsEnum(tmsStruct.sampleType)) + .setName(ConvertToDaqCoreString(tmsStruct.name)) + .setDimensions(ReadDimensions(tmsStruct.dimensions, tmsStruct.dimensionsSize)) + .setMetadata(ReadMetadata(tmsStruct.metadata, tmsStruct.metadataSize)); + + if (tmsStruct.unit) + descriptor.setUnit(StructConverter::ToDaqObject(*tmsStruct.unit)); + + if (tmsStruct.valueRange) + descriptor.setValueRange(StructConverter::ToDaqObject(*tmsStruct.valueRange)); + + auto ruleObject = ExtensionObject(tmsStruct.rule); + if (ruleObject.isDecoded()) + descriptor.setRule(VariantConverter::ToDaqObject(ruleObject.getAsVariant())); + + if (tmsStruct.origin) + descriptor.setOrigin(ConvertToDaqCoreString(*tmsStruct.origin)); + + if (tmsStruct.tickResolution) + descriptor.setTickResolution(StructConverter::ToDaqObject(*tmsStruct.tickResolution)); + + if (tmsStruct.postScaling) + descriptor.setPostScaling(StructConverter::ToDaqObject(*tmsStruct.postScaling)); + + return descriptor.build(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataDescriptorPtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject tmsStruct; + tmsStruct->sampleType = SampleTypeToTmsEnum(object.getSampleType()); + + WriteDimensions(object.getDimensions(), tmsStruct->dimensions, tmsStruct->dimensionsSize); + WriteMetadata(object.getMetadata(), tmsStruct->metadata, tmsStruct->metadataSize); + + tmsStruct->name = ConvertToOpcUaString(object.getName()).getDetachedValue(); + + if (object.getUnit().assigned()) + tmsStruct->unit = StructConverter::ToTmsType(object.getUnit()).newDetachedPointer(); + + if (object.getValueRange().assigned()) + tmsStruct->valueRange = StructConverter::ToTmsType(object.getValueRange()).newDetachedPointer(); + + if (object.getRule().assigned()) + { + auto ruleObject = ExtensionObject(VariantConverter::ToVariant(object.getRule())); + tmsStruct->rule = ruleObject.getDetachedValue(); + } + + if (object.getOrigin().assigned()) + tmsStruct->origin = ConvertToOpcUaString(object.getOrigin()).newDetachedPointer(); + + if (object.getTickResolution().assigned()) + tmsStruct->tickResolution = StructConverter::ToTmsType(object.getTickResolution()).newDetachedPointer(); + + if (object.getPostScaling().assigned()) + tmsStruct->postScaling = StructConverter::ToTmsType(object.getPostScaling()).newDetachedPointer(); + + return tmsStruct; +} + +// Variant DataDescriptorPtr + +template <> +DataDescriptorPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(variant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(variant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const DataDescriptorPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType ==nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_BASEDATADESCRIPTORSTRUCTURE]) + { + if (object.getSampleType() == SampleType::Struct) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + variant.setScalar(*StructConverter::ToTmsType(object)); + } + else if (targetType ==&UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_DATADESCRIPTORSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType ==&UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_STRUCTDESCRIPTORSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_BASEDATADESCRIPTORSTRUCTURE]) + return ListConversionUtils::ToExtensionObjectArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_DATADESCRIPTORSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_STRUCTDESCRIPTORSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/data_rule_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/data_rule_converter.cpp new file mode 100644 index 0000000..2de529e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/data_rule_converter.cpp @@ -0,0 +1,309 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class VariantConverter; + +// UA_LinearRuleDescription + +template <> +DataRulePtr StructConverter::ToDaqObject(const UA_LinearRuleDescriptionStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "linear") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const NumberPtr delta = VariantConverter::ToDaqObject(OpcUaVariant(tmsStruct.delta)); + const NumberPtr start = VariantConverter::ToDaqObject(OpcUaVariant(tmsStruct.start)); + + return LinearDataRule(delta, start); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataRulePtr& object, const ContextPtr& /*context*/) +{ + const NumberPtr delta = object.getParameters().get("delta"); + const NumberPtr start = object.getParameters().get("start"); + + OpcUaObject uaRuleDescription; + + uaRuleDescription->type = UA_STRING_ALLOC("linear"); + uaRuleDescription->delta = VariantConverter::ToVariant(delta).getDetachedValue(); + uaRuleDescription->start = VariantConverter::ToVariant(start).getDetachedValue(); + + return uaRuleDescription; +} + +// UA_ConstantRuleDescription + +template <> +DataRulePtr StructConverter::ToDaqObject( + const UA_ConstantRuleDescriptionStructure& tmsStruct, const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "constant") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const NumberPtr value = VariantConverter::ToDaqObject(tmsStruct.value); + return ConstantDataRule(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataRulePtr& object, const ContextPtr& /*context*/) +{ + const NumberPtr value = Integer(0); // TODO: temporary solution until model is adapted + OpcUaObject uaRuleDescription; + + uaRuleDescription->type = UA_STRING_ALLOC("constant"); + uaRuleDescription->value = VariantConverter::ToVariant(value).getDetachedValue(); + + return uaRuleDescription; +} + +// UA_BaseRuleDescription + +template <> +DataRulePtr StructConverter::ToDaqObject(const UA_BaseRuleDescriptionStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "explicit") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return ExplicitDataRule(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataRulePtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject uaRuleDescription; + uaRuleDescription->type = UA_STRING_ALLOC("explicit"); + return uaRuleDescription; +} + +// UA_ExplicitDomainRuleDescription + +template <> +DataRulePtr StructConverter::ToDaqObject( + const UA_ExplicitDomainRuleDescriptionStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "explicit") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const NumberPtr minExpectedDelta = VariantConverter::ToDaqObject(OpcUaVariant(tmsStruct.minExpectedDelta)); + const NumberPtr maxExpectedDelta = VariantConverter::ToDaqObject(OpcUaVariant(tmsStruct.maxExpectedDelta)); + + return ExplicitDomainDataRule(minExpectedDelta, maxExpectedDelta); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataRulePtr& object, + const ContextPtr& /*context*/) +{ + const NumberPtr minExpectedDelta = object.getParameters().get("minExpectedDelta"); + const NumberPtr maxExpectedDelta = object.getParameters().get("maxExpectedDelta"); + + OpcUaObject uaRuleDescription; + + uaRuleDescription->type = UA_STRING_ALLOC("explicit"); + uaRuleDescription->minExpectedDelta = VariantConverter::ToVariant(minExpectedDelta).getDetachedValue(); + uaRuleDescription->maxExpectedDelta = VariantConverter::ToVariant(maxExpectedDelta).getDetachedValue(); + + return uaRuleDescription; +} + +// UA_CustomRuleDescription + +template <> +DataRulePtr StructConverter::ToDaqObject(const UA_CustomRuleDescriptionStructure& tmsStruct, + const ContextPtr& context) +{ + if (tmsStruct.type != "custom") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto params = Dict(); + for (size_t i = 0; i < tmsStruct.parametersSize; i++) + { + auto value = OpcUaVariant(tmsStruct.parameters[i].value); + auto key = OpcUaVariant(tmsStruct.parameters[i].key); + if (key.isString()) + { + try + { + params.set(key.toString(), VariantConverter::ToDaqObject(value, context)); + } + catch (...) + { + } + } + } + + return DataRuleBuilder().setType(DataRuleType::Other).setParameters(params).build(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DataRulePtr& object, const ContextPtr& context) +{ + OpcUaObject uaRuleDescription; + uaRuleDescription->type = UA_STRING_ALLOC("custom"); + auto params = object.getParameters(); + + const auto type = GetUaDataType(); + uaRuleDescription->parameters = static_cast(UA_Array_new(params.getCount(), type)); + uaRuleDescription->parametersSize = params.getCount(); + size_t index = 0; + + for (const auto& [name, value] : params) + { + OpcUaObject pair; + pair->key = VariantConverter::ToVariant(name).getDetachedValue(); + pair->value = VariantConverter::ToVariant(value, nullptr, context).getDetachedValue(); + uaRuleDescription->parameters[index] = pair.getDetachedValue(); + + index++; + } + + return uaRuleDescription; +} + +// Variant converters + +template <> +DataRulePtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const DataRulePtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr) + { + switch (object.getType()) + { + case DataRuleType::Linear: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + case DataRuleType::Constant: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + case DataRuleType::Explicit: + { + if (object.getParameters().hasKey("minExpectedDelta") && object.getParameters().hasKey("maxExpectedDelta")) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + case DataRuleType::Other: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + } + } + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CONSTANTRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_BASERULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CUSTOMRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_EXPLICITDOMAINRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr) + return ListConversionUtils::ToExtensionObjectArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CONSTANTRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_EXPLICITDOMAINRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_BASERULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CUSTOMRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/dict_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/dict_converter.cpp new file mode 100644 index 0000000..4092778 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/dict_converter.cpp @@ -0,0 +1,130 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +namespace dict_converter +{ + static OpcUaObject ToKeyValuePair(const BaseObjectPtr& key, const BaseObjectPtr& value, const ContextPtr& context) + { + OpcUaObject pair; + pair->key = VariantConverter::ToVariant(key, nullptr, context).getDetachedValue(); + pair->value = VariantConverter::ToVariant(value, nullptr, context).getDetachedValue(); + return pair; + } + + static DictPtr ExtensionObjectToDict(const OpcUaVariant& variant, const ContextPtr& context) + { + const auto data = static_cast(variant->data); + auto dict = Dict(); + + OpcUaVariant decodedVariant; + OpcUaVariant valueVariant; + OpcUaVariant keyVariant; + + for (size_t i = 0; i < variant->arrayLength; i++) + { + auto extensionObject = ExtensionObject(data[i]); + if (extensionObject.isDecoded()) + decodedVariant = extensionObject.getAsVariant(); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + if (!decodedVariant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const auto decodedData = static_cast(decodedVariant->data); + + keyVariant.setValue(decodedData->key); + const auto key = VariantConverter::ToDaqObject(keyVariant, context); + + valueVariant.setValue(decodedData->value); + const auto value = VariantConverter::ToDaqObject(valueVariant, context); + + dict.set(key, value); + } + + return dict; + } + + static DictPtr DaqKeyValuePairToDict(const OpcUaVariant& variant, const ContextPtr& context) + { + auto dict = Dict(); + OpcUaVariant valueVariant; + OpcUaVariant keyVariant; + const auto data = static_cast(variant->data); + + for (size_t i = 0; i < variant->arrayLength; i++) + { + const UA_DaqKeyValuePair keyValuePairData = data[i]; + + keyVariant.setValue(keyValuePairData.key); + const auto key = VariantConverter::ToDaqObject(keyVariant, context); + + valueVariant.setValue(keyValuePairData.value); + const auto value = VariantConverter::ToDaqObject(valueVariant, context); + + dict.set(key, value); + } + + return dict; + } +} + +template <> +DictPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (variant.isScalar()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + if (variant.isType()) + return dict_converter::ExtensionObjectToDict(variant, context); + if (variant.isType()) + return dict_converter::DaqKeyValuePairToDict(variant, context); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const DictPtr& object, + const UA_DataType* targetType, + const ContextPtr& context) +{ + if (targetType != nullptr && targetType != &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto variant = OpcUaVariant(); + if (object.getCount() == 0) + return variant; + + const auto type = GetUaDataType(); + const auto arr = static_cast(UA_Array_new(object.getCount(), type)); + + int i = 0; + for (auto [key, value] : object) + { + auto tmsStruct = dict_converter::ToKeyValuePair(key, value, context); + arr[i] = tmsStruct.getDetachedValue(); + ++i; + } + + UA_Variant_setArray(variant.get(), arr, object.getCount(), type); + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& /*list*/, + const UA_DataType* /*targetType*/, + const ContextPtr& /*context*/) +{ + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/dimension_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/dimension_converter.cpp new file mode 100644 index 0000000..5ba5cba --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/dimension_converter.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class VariantConverter; + +// UA_DimensionDescriptorStructure + +template <> +DimensionPtr StructConverter::ToDaqObject(const UA_DimensionDescriptorStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + const auto dimension = DimensionBuilder(); + + if (tmsStruct.name) + dimension->setName(ConvertToDaqCoreString(*tmsStruct.name)); + + if (tmsStruct.unit) + dimension->setUnit(StructConverter::ToDaqObject(*tmsStruct.unit)); + + auto ruleObject = ExtensionObject(tmsStruct.dimensionRule); + if (ruleObject.isDecoded()) + dimension->setRule(VariantConverter::ToDaqObject(ruleObject.getAsVariant())); + + return dimension.build(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DimensionPtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject dimension; + + if (object.getName().assigned()) + dimension->name = ConvertToOpcUaString(object.getName()).newDetachedPointer(); + + if (object.getUnit().assigned()) + dimension->unit = StructConverter::ToTmsType(object.getUnit()).newDetachedPointer(); + + if (object.getRule().assigned()) + { + auto ruleObject = ExtensionObject(VariantConverter::ToVariant(object.getRule())); + dimension->dimensionRule = ruleObject.getDetachedValue(); + } + + return dimension; +} + +// Variant converters + +template <> +DimensionPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (!decodedVariant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const DimensionPtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + if (targetType == nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_DIMENSIONDESCRIPTORSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + + return ListConversionUtils::VariantToList(variant); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_DIMENSIONDESCRIPTORSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/dimension_rule_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/dimension_rule_converter.cpp new file mode 100644 index 0000000..9338fa7 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/dimension_rule_converter.cpp @@ -0,0 +1,298 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class StructConverter; +template class StructConverter; +template class VariantConverter; + +// UA_LinearRuleDescription + +template <> +DimensionRulePtr StructConverter::ToDaqObject( + const UA_LinearRuleDescriptionStructure& tmsStruct, const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "linear") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + if (tmsStruct.size == nullptr) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const SizeT size = *tmsStruct.size; + const NumberPtr delta = VariantConverter::ToDaqObject(tmsStruct.delta); + const NumberPtr start = VariantConverter::ToDaqObject(tmsStruct.start); + return LinearDimensionRule(delta, start, size); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DimensionRulePtr& object, const ContextPtr& /*context*/) +{ + const SizeT size = object.getParameters().get("size"); + const NumberPtr delta = object.getParameters().get("delta"); + const NumberPtr start = object.getParameters().get("start"); + + OpcUaObject uaRuleDescription; + + uaRuleDescription->type = UA_STRING_ALLOC("linear"); + uaRuleDescription->delta = VariantConverter::ToVariant(delta).getDetachedValue(); + uaRuleDescription->start = VariantConverter::ToVariant(start).getDetachedValue(); + uaRuleDescription->size = UA_UInt32_new(); + *uaRuleDescription->size = size; + + return uaRuleDescription; +} + +// UA_LogRuleDescriptionStructure + +template <> +DimensionRulePtr StructConverter::ToDaqObject( + const UA_LogRuleDescriptionStructure& tmsStruct, const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "log") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const NumberPtr delta = VariantConverter::ToDaqObject(tmsStruct.delta); + const NumberPtr start = VariantConverter::ToDaqObject(tmsStruct.start); + return LogarithmicDimensionRule(delta, start, tmsStruct.base, tmsStruct.size); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DimensionRulePtr& object, const ContextPtr& /*context*/) +{ + const SizeT size = object.getParameters().get("size"); + const NumberPtr delta = object.getParameters().get("delta"); + const NumberPtr start = object.getParameters().get("start"); + const Int base = object.getParameters().get("base"); + + OpcUaObject uaRuleDescription; + + uaRuleDescription->type = UA_STRING_ALLOC("log"); + uaRuleDescription->size = size; + uaRuleDescription->delta = VariantConverter::ToVariant(delta).getDetachedValue(); + uaRuleDescription->start = VariantConverter::ToVariant(start).getDetachedValue(); + uaRuleDescription->base = base; + + return uaRuleDescription; +} + +// UA_ListRuleDescription + +template <> +DimensionRulePtr StructConverter::ToDaqObject( + const UA_ListRuleDescriptionStructure& tmsStruct, const ContextPtr& /*context*/) +{ + if (tmsStruct.type != "List") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const SizeT elementsSize = tmsStruct.elementsSize; + if (elementsSize == 0) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + ListPtr list = List(); + for (SizeT i = 0; i < elementsSize; i++) + { + NumberPtr value = VariantConverter::ToDaqObject(tmsStruct.elements[i]); + list.pushBack(value); + } + + return ListDimensionRule(list); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DimensionRulePtr& object, const ContextPtr& /*context*/) +{ + ListPtr daqList = object.getParameters().get("List"); + + OpcUaObject uaRuleDescription; + uaRuleDescription->elementsSize = daqList.getCount(); + uaRuleDescription->elements = static_cast(UA_Array_new(uaRuleDescription->elementsSize, GetUaDataType())); + uaRuleDescription->type = UA_STRING_ALLOC("List"); + + for (SizeT i = 0; i < daqList.getCount(); i++) + uaRuleDescription->elements[i] = VariantConverter::ToVariant(daqList[i]).getDetachedValue(); + + return uaRuleDescription; +} + +template <> +DimensionRulePtr StructConverter::ToDaqObject( + const UA_CustomRuleDescriptionStructure& tmsStruct, const ContextPtr& context) +{ + if (tmsStruct.type != "custom") + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto params = Dict(); + for (size_t i = 0; i < tmsStruct.parametersSize; i++) + { + auto value = OpcUaVariant(tmsStruct.parameters[i].value); + auto key = OpcUaVariant(tmsStruct.parameters[i].key); + if (key.isString()) + { + try + { + params.set(key.toString(), VariantConverter::ToDaqObject(value, context)); + } + catch (...) + { + } + } + } + + return DimensionRuleBuilder().setType(DimensionRuleType::Other).setParameters(params).build(); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const DimensionRulePtr& object, const ContextPtr& context) +{ + OpcUaObject uaRuleDescription; + uaRuleDescription->type = UA_STRING_ALLOC("custom"); + const auto params = object.getParameters(); + + const auto type = GetUaDataType(); + uaRuleDescription->parameters = static_cast(UA_Array_new(params.getCount(), type)); + uaRuleDescription->parametersSize = params.getCount(); + size_t index = 0; + + for (const auto& [name, value] : params) + { + OpcUaObject pair; + pair->key = VariantConverter::ToVariant(name).getDetachedValue(); + pair->value = VariantConverter::ToVariant(value, nullptr, context).getDetachedValue(); + uaRuleDescription->parameters[index] = pair.getDetachedValue(); + + index++; + } + + return uaRuleDescription; +} + +// Variant converter + +template <> +DimensionRulePtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + { + const auto tmsRuleDescription = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsRuleDescription); + } + + if (decodedVariant.isType()) + { + const auto tmsRuleDescription = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsRuleDescription); + } + + if (decodedVariant.isType()) + { + const auto tmsRuleDescription = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsRuleDescription); + } + + if (decodedVariant.isType()) + { + const auto tmsRuleDescription = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsRuleDescription); + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const DimensionRulePtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr) + { + switch (object.getType()) + { + case DimensionRuleType::Linear: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + case DimensionRuleType::Logarithmic: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + case DimensionRuleType::List: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + case DimensionRuleType::Other: + { + variant.setScalar(*StructConverter::ToTmsType(object)); + break; + } + } + } + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LOGRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LISTRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CUSTOMRULEDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr) + return ListConversionUtils::ToExtensionObjectArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LOGRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LISTRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_CUSTOMRULEDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/function_block_type_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/function_block_type_converter.cpp new file mode 100644 index 0000000..612d821 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/function_block_type_converter.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class VariantConverter; + +// UA_FunctionBlockTypeStructure + +template <> +FunctionBlockTypePtr StructConverter::ToDaqObject( + const UA_FunctionBlockInfoStructure& tmsStruct, const ContextPtr& /*context*/) +{ + const StringPtr id(ConvertToDaqCoreString(tmsStruct.id)); + const StringPtr name(ConvertToDaqCoreString(tmsStruct.name)); + const StringPtr description(ConvertToDaqCoreString(tmsStruct.description)); + + return FunctionBlockType(id, name, description); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const FunctionBlockTypePtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject type; + + type->id = ConvertToOpcUaString(object.getId()).getDetachedValue(); + type->name = ConvertToOpcUaString(object.getName()).getDetachedValue(); + type->description = ConvertToOpcUaString(object.getDescription()).getDetachedValue(); + + return type; +} + +// Variant converter + +template <> +FunctionBlockTypePtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (!decodedVariant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const FunctionBlockTypePtr& object, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_FUNCTIONBLOCKINFOSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + + return ListConversionUtils::VariantToList(variant); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_FUNCTIONBLOCKINFOSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/generic_enumeration_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/generic_enumeration_converter.cpp new file mode 100644 index 0000000..44a9105 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/generic_enumeration_converter.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +template <> +EnumerationPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (variant.isNull()) + return nullptr; + + if (!context.assigned() || !context.getTypeManager().assigned()) + DAQ_THROW_EXCEPTION(ConversionFailedException, "Generic numeration conversion requires the TypeManager."); + + const auto typeManager = context.getTypeManager(); + + // Get UAEnumerationType by name + const auto DataType = GetUAEnumerationDataTypeByName(variant->type->typeName); + + EnumerationTypePtr Type; + + if (typeManager.hasType(DataType->typeName)) + Type = typeManager.getType(DataType->typeName); + else + DAQ_THROW_EXCEPTION(ConversionFailedException, "EnumerationType is not present in Type Manager."); + + DictPtr dictEnumValues = Type.getAsDictionary(); + + auto listKeyword = dictEnumValues.getKeyList(); + auto listValues = dictEnumValues.getValueList(); + StringPtr keyword; + auto data = variant.toInteger(); + + for(int i = 0; i < static_cast(listKeyword.getCount()); i++) + { + if(listValues[i] == data) + keyword = listKeyword[i]; + } + + return Enumeration(DataType->typeName, keyword, typeManager); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const EnumerationPtr& object, + const UA_DataType* /*targetType*/, + const ContextPtr& /*context*/) +{ + const auto dataType = GetUAEnumerationDataTypeByName(object.getEnumerationType().getName()); + assert(dataType->memSize == sizeof(uint32_t)); + + const auto intVariant = VariantConverter::ToVariant(object.getIntValue(), &UA_TYPES[UA_TYPES_INT32]); + OpcUaVariant variant{}; + UA_Variant_setScalarCopy(&variant.getValue(), intVariant->data, dataType); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& context) +{ + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* /*targetType*/, + const ContextPtr& context) +{ + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/generic_struct_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/generic_struct_converter.cpp new file mode 100644 index 0000000..9404f95 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/generic_struct_converter.cpp @@ -0,0 +1,276 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +namespace detail +{ + static TypePtr createType(const BaseObjectPtr& obj) + { + const auto ct = obj.getCoreType(); + if (ct == ctStruct) + return obj.asPtr().getStructType(); + else if (ct == ctEnumeration) + return obj.asPtr().getEnumerationType(); + else + return SimpleType(ct); + } +} + +template <> +StructPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (variant.isNull()) + return nullptr; + + if (!context.assigned() || !context.getTypeManager().assigned()) + DAQ_THROW_EXCEPTION(ConversionFailedException, "Generic struct conversion requires the TypeManager."); + + const auto typeManager = context.getTypeManager(); + + const auto type = variant->type; + + const UA_DataTypeMember* members = type->members; + const UA_UInt32 membersSize = type->membersSize; + auto src = reinterpret_cast(variant->data); + + DictPtr daqMembers = Dict(); + ListPtr memberTypes = List(); + + for (SizeT i = 0; i < membersSize; ++i) + { + const UA_DataTypeMember* member = &members[i]; + const UA_DataType* memberType = member->memberType; + src += member->padding; + + // TODO: Refactor this + if (!member->isOptional) + { + if (!member->isArray) + { + OpcUaVariant memberVariant{}; + UA_Variant_setScalarCopy(&memberVariant.getValue(), reinterpret_cast(src), memberType); + const auto obj = VariantConverter::ToDaqObject(memberVariant, context); + daqMembers.set(member->memberName, obj); + src += memberType->memSize; + memberTypes.pushBack(detail::createType(obj)); + } + else + { + const size_t size = *reinterpret_cast(src); + src += sizeof(size_t); + OpcUaVariant memberVariant{}; + UA_Variant_setArrayCopy(&memberVariant.getValue(), *reinterpret_cast(src), size, memberType); + + const auto obj = VariantConverter::ToDaqObject(memberVariant, context); + daqMembers.set(member->memberName, obj); + memberTypes.pushBack(detail::createType(obj)); + src += sizeof(void*); + } + } + else + { + if (!member->isArray) + { + if(*reinterpret_cast(src) != nullptr) + { + OpcUaVariant memberVariant{}; + UA_Variant_setScalarCopy(&memberVariant.getValue(), *reinterpret_cast(src), memberType); + + const auto obj = VariantConverter::ToDaqObject(memberVariant, context); + daqMembers.set(member->memberName, obj); + memberTypes.pushBack(detail::createType(obj)); + } + else + { + daqMembers.set(member->memberName, nullptr); + // TODO: Set appropriate type + memberTypes.pushBack(SimpleType(ctUndefined)); + } + } + else + { + if(*reinterpret_cast(src + sizeof(size_t)) != nullptr) + { + const size_t size = *reinterpret_cast(src); + src += sizeof(size_t); + OpcUaVariant memberVariant{}; + UA_Variant_setArrayCopy(&memberVariant.getValue(), reinterpret_cast(src), size, memberType); + + const auto obj = VariantConverter::ToDaqObject(memberVariant, context); + daqMembers.set(member->memberName, obj); + memberTypes.pushBack(detail::createType(obj)); + } + else + { + daqMembers.set(member->memberName, nullptr); + src += sizeof(size_t*); + memberTypes.pushBack(SimpleType(ctUndefined)); + } + } + src += sizeof(void*); + } + } + + try + { + typeManager.addType(StructType(type->typeName, daqMembers.getKeyList(), memberTypes)); + } + catch (const std::exception& e) + { + const auto loggerComponent = context.getLogger().getOrAddComponent("GenericStructConverter"); + LOG_W("Couldn't add type {} to type manager: {}", type->typeName, e.what()); + } + catch (...) + { + const auto loggerComponent = context.getLogger().getOrAddComponent("GenericStructConverter"); + LOG_W("Couldn't add type {} to type manager!", type->typeName); + } + + return Struct(type->typeName, daqMembers, typeManager); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const StructPtr& object, const UA_DataType* /*targetType*/, const ContextPtr& context) +{ + const auto type = GetUAStructureDataTypeByName(object.getStructType().getName()); + if (type == nullptr) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const UA_DataTypeMember* members = type->members; + const UA_UInt32 membersSize = type->membersSize; + + void* data = UA_new(type); + const auto daqMembers = object.getAsDictionary(); + + if (membersSize != daqMembers.getCount()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto dst = reinterpret_cast(data); + for (SizeT i = 0; i < membersSize; ++i) + { + const UA_DataTypeMember* member = &members[i]; + const UA_DataType* memberType = member->memberType; + + if (!daqMembers.hasKey(member->memberName)) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto daqMember = daqMembers.get(member->memberName); + dst += member->padding; + + if (!daqMember.assigned()) + { + if (member->isOptional) + { + if (member->isArray) + dst += sizeof(size_t*); + dst += sizeof(void*); + continue; + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); + } + + OpcUaVariant variant = VariantConverter::ToVariant(daqMember, memberType, context); + if (variant->type != memberType && !(variant->data == UA_EMPTY_ARRAY_SENTINEL && variant->arrayLength == 0)) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + void* src = variant->data; + + if (!member->isArray) + { + if (!member->isOptional) + { + UA_copy(src, reinterpret_cast(dst), memberType); + dst += memberType->memSize; + } + else + { + [[maybe_unused]] + const UA_StatusCode retval = UA_Array_copy(src, 1, reinterpret_cast(dst), memberType); + dst += sizeof(void*); + } + } + else + { + auto *dst_size = reinterpret_cast(dst); + const size_t size = variant->arrayLength; + dst += sizeof(size_t); + const UA_StatusCode retval = UA_Array_copy(src, size, reinterpret_cast(dst), memberType); + if(retval == UA_STATUSCODE_GOOD) + *dst_size = size; + else + *dst_size = 0; + dst += sizeof(void*); + } + } + + OpcUaVariant variant{}; + UA_Variant_setScalar(&variant.getValue(), data, type); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& context) +{ + auto list = List(); + auto src = reinterpret_cast(variant->data); + + for (size_t i = 0; i < variant->arrayLength; i++) + { + OpcUaVariant varObj; + UA_Variant_setScalarCopy(&varObj.getValue(), reinterpret_cast(src), variant->type); + + const auto obj = ToDaqObject(varObj, context); + list.pushBack(obj); + src += variant->type->memSize; + } + + return list; +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* /*targetType*/, + const ContextPtr& context) +{ + if (list.empty()) + { + auto varObj = OpcUaVariant(); + varObj->data = UA_EMPTY_ARRAY_SENTINEL; + return varObj; + } + + auto firstConvertedStruct = ToVariant(list[0], nullptr, context); + auto type = firstConvertedStruct->type; + + auto arr = UA_Array_new(list.getCount(), type); + auto dst = reinterpret_cast(arr); + + UA_copy(firstConvertedStruct->data, reinterpret_cast(dst), firstConvertedStruct->type); + dst += firstConvertedStruct->type->memSize; + + for (SizeT i = 1; i < list.getCount(); i++) + { + auto convertedStruct = ToVariant(list[i], nullptr, context); + if (convertedStruct->type != type) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + UA_copy(convertedStruct->data, reinterpret_cast(dst), convertedStruct->type); + dst += convertedStruct->type->memSize; + } + + auto variant = OpcUaVariant(); + UA_Variant_setArray(variant.get(), arr, list.getCount(), type); + return variant; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/number_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/number_converter.cpp new file mode 100644 index 0000000..05a5f94 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/number_converter.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class VariantConverter; + +// Variant converter + +template <> +NumberPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isInteger()) + return VariantConverter::ToDaqObject(decodedVariant); + + if (decodedVariant.isDouble()) + return VariantConverter::ToDaqObject(decodedVariant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const NumberPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + if (targetType == nullptr) + { + if (object.getCoreType() == ctInt) + return VariantConverter::ToVariant(object.getIntValue()); + + if (object.getCoreType() == ctFloat) + return VariantConverter::ToVariant(object.getFloatValue()); + + DAQ_THROW_EXCEPTION(ConversionFailedException); + } + + //TODO: Add if statements for target types of int and float + + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + return ListConversionUtils::ExtensionObjectVariantToList(variant); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr) + return ListConversionUtils::ToExtensionObjectArrayVariant(list); + + //TODO: Add if statements for target types of int and float + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/property_object_conversion_utils.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/property_object_conversion_utils.cpp new file mode 100644 index 0000000..0fc572e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/property_object_conversion_utils.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +OpcUaVariant PropertyObjectConversionUtils::ToDictVariant(const PropertyObjectPtr& obj) +{ + if (!obj.assigned()) + { + auto variant = OpcUaVariant(); + UA_Variant_setArray(variant.get(), nullptr, 0, &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]); + return variant; + } + + auto dict = Dict(); + const auto properties = obj.getAllProperties(); + + for (const auto& prop : properties) + { + const auto key = prop.getName(); + const auto val = obj.getPropertyValue(key); + dict.set(key, val); + } + + return VariantConverter::ToVariant(dict); +} + +void PropertyObjectConversionUtils::ToPropertyObject(const OpcUaVariant& variant, PropertyObjectPtr& objOut) +{ + const auto dict = VariantConverter::ToDaqObject(variant); + + for (const auto& [name, value] : dict) + { + if (!objOut.hasProperty(name)) + { + auto property = PropertyBuilder(name).setValueType(value.getCoreType()) + .setDefaultValue(value) + .build(); + objOut.addProperty(property); + } + else + { + objOut.setPropertyValue(name, value); + } + } +} + +PropertyObjectPtr PropertyObjectConversionUtils::ClonePropertyObject(const PropertyObjectPtr& obj) +{ + // This is a workaround until PropertyObject implemnts IClonable. + auto serializer = JsonSerializer(); + auto deserializer = JsonDeserializer(); + obj.serialize(serializer); + const auto clone = deserializer.deserialize(serializer.getOutput()); + return clone; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/range_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/range_converter.cpp new file mode 100644 index 0000000..b50b09d --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/range_converter.cpp @@ -0,0 +1,75 @@ +#include + +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class VariantConverter; + +// UA_Range + +template <> +RangePtr StructConverter::ToDaqObject(const UA_Range& tmsStruct, const ContextPtr& /*context*/) +{ + return Range(tmsStruct.low, tmsStruct.high); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const RangePtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject range; + range->low = object.getLowValue(); + range->high = object.getHighValue(); + return range; +} + +// Variant converter + +template <> +RangePtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (!decodedVariant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return StructConverter::ToDaqObject(*static_cast(decodedVariant->data)); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const RangePtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_RANGE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + return ListConversionUtils::VariantToList(variant); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES[UA_TYPES_RANGE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/ratio_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/ratio_converter.cpp new file mode 100644 index 0000000..2d150dd --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/ratio_converter.cpp @@ -0,0 +1,116 @@ +#include + +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class StructConverter; +template class VariantConverter; + + +// UA_RationalNumber + +template <> +RatioPtr StructConverter::ToDaqObject(const UA_RationalNumber& tmsStruct, const ContextPtr& /*context*/) +{ + return Ratio(tmsStruct.numerator, tmsStruct.denominator); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const RatioPtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject ratio; + ratio->numerator = object.getNumerator(); + ratio->denominator = object.getDenominator(); + return ratio; +} + +// UA_RationalNumber64 + +template <> +RatioPtr StructConverter::ToDaqObject(const UA_RationalNumber64& tmsStruct, const ContextPtr& /*context*/) +{ + return Ratio(tmsStruct.numerator, tmsStruct.denominator); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const RatioPtr& object, + const ContextPtr& /*context*/) +{ + OpcUaObject ratio; + ratio->numerator = object.getNumerator(); + ratio->denominator = object.getDenominator(); + return ratio; +} + +// Variant converter + +template <> +RatioPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const RatioPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_RATIONALNUMBER64]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_RATIONALNUMBER]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_RATIONALNUMBER64]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_RATIONALNUMBER]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/scaling_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/scaling_converter.cpp new file mode 100644 index 0000000..f82d362 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/scaling_converter.cpp @@ -0,0 +1,170 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Definitions + +template class StructConverter; +template class StructConverter; +template class VariantConverter; + +template <> +ScalingPtr StructConverter::ToDaqObject( + const UA_LinearScalingDescriptionStructure& tmsStruct, const ContextPtr& /*context*/); +template <> +OpcUaObject StructConverter::ToTmsType( + const ScalingPtr& object, const ContextPtr& /*context*/); +template <> +ScalingPtr StructConverter::ToDaqObject(const UA_PostScalingStructure& tmsStruct, + const ContextPtr& /*context*/); +template <> +OpcUaObject StructConverter::ToTmsType(const ScalingPtr& object, + const ContextPtr& /*context*/); +template <> +ScalingPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/); +template <> +OpcUaVariant VariantConverter::ToVariant(const ScalingPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/); + +// UA_LinearScalingDescription + +template <> +ScalingPtr StructConverter::ToDaqObject( + const UA_LinearScalingDescriptionStructure& tmsStruct, const ContextPtr& /*context*/) +{ + auto scale = VariantConverter::ToDaqObject(tmsStruct.scale); + auto offset = VariantConverter::ToDaqObject(tmsStruct.offset); + return LinearScaling(scale, offset); +} + +template <> +OpcUaObject StructConverter::ToTmsType( + const ScalingPtr& object, const ContextPtr& /*context*/) +{ + if (object.getType() != ScalingType::Linear) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + OpcUaObject scaling; + scaling->type = UA_STRING_ALLOC("linear"); + + auto scale = object.getParameters().get("scale"); + scaling->scale = VariantConverter::ToVariant(scale).getDetachedValue(); + + auto offset = object.getParameters().get("offset"); + scaling->offset = VariantConverter::ToVariant(offset).getDetachedValue(); + + return scaling; +} + +// UA_PostScalingStructure + +template <> +ScalingPtr StructConverter::ToDaqObject(const UA_PostScalingStructure& tmsStruct, + const ContextPtr& /*context*/) +{ + auto scalingObject = ExtensionObject(tmsStruct.scalingDescription); + const auto scalingVariant = scalingObject.getAsVariant(); + const auto scaling = VariantConverter::ToDaqObject(scalingVariant); + + auto postScaling = ScalingBuilderCopy(scaling) + .setInputDataType(SampleTypeFromTmsEnum(tmsStruct.inputSampleType)) + .setOutputDataType(ScaledSampleTypeFromTmsEnum(tmsStruct.outputSampleType)) + .build(); + return postScaling; +} + +template <> +OpcUaObject StructConverter::ToTmsType(const ScalingPtr& object, + const ContextPtr& /*context*/) +{ + OpcUaObject uaPostScaling; + uaPostScaling->inputSampleType = SampleTypeToTmsEnum(object.getInputSampleType()); + uaPostScaling->outputSampleType = ScaledSampleTypeToTmsEnum(object.getOutputSampleType()); + + OpcUaObject uaLinearScalingDescription; + const NumberPtr scale = object.getParameters().get("scale"); + const NumberPtr offset = object.getParameters().get("offset"); + + uaLinearScalingDescription->type = UA_STRING_ALLOC("linear"); + uaLinearScalingDescription->scale = VariantConverter::ToVariant(scale).getDetachedValue(); + uaLinearScalingDescription->offset = VariantConverter::ToVariant(offset).getDetachedValue(); + + uaPostScaling->scalingDescription.encoding = UA_EXTENSIONOBJECT_DECODED; + uaPostScaling->scalingDescription.content.decoded.type = &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARSCALINGDESCRIPTIONSTRUCTURE]; + + const auto uaLinearScalingDescriptionPtr = UA_LinearScalingDescriptionStructure_new(); + *uaLinearScalingDescriptionPtr = uaLinearScalingDescription.getDetachedValue(); + uaPostScaling->scalingDescription.content.decoded.data = uaLinearScalingDescriptionPtr; + + return uaPostScaling; +} + +// Variant converters + +template <> +ScalingPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + if (decodedVariant.isType()) + { + const auto tmsStruct = static_cast(decodedVariant->data); + return StructConverter::ToDaqObject(*tmsStruct); + } + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const ScalingPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType ==&UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_POSTSCALINGSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARSCALINGDESCRIPTIONSTRUCTURE]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_POSTSCALINGSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_LINEARSCALINGDESCRIPTIONSTRUCTURE]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/selection_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/selection_converter.cpp new file mode 100644 index 0000000..e1e24db --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/selection_converter.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +BaseObjectPtr SelectionVariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& context) +{ + if (!variant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + auto data = static_cast(variant->data); + auto dict = Dict(); + + OpcUaVariant decodedVariant; + OpcUaVariant valueVariant; + bool isDictionary = false; + + for (size_t i = 0; i < variant->arrayLength; i++) + { + auto extensionObject = ExtensionObject(data[i]); + if (extensionObject.isDecoded()) + decodedVariant = extensionObject.getAsVariant(); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + if (!decodedVariant.isType()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const auto decodedData = static_cast(decodedVariant->data); + + const auto key = StructConverter::ToDaqObject(decodedData->key); + valueVariant.setValue(decodedData->value); + const auto value = VariantConverter::ToDaqObject(valueVariant, context); + + isDictionary = isDictionary || !(static_cast(i) == key); + dict.set(key, value); + } + + if (isDictionary) + return dict; + return dict.getValueList(); +} + +OpcUaVariant SelectionVariantConverter::ToVariant(const BaseObjectPtr& selectionValues, const ContextPtr& context) +{ + if (!selectionValues.assigned()) + DAQ_THROW_EXCEPTION(ConversionFailedException); + + const auto list = selectionValues.asPtrOrNull(); + if (list.assigned()) + return ListToVariant(list, context); + + const auto dict = selectionValues.asPtrOrNull(); + if (dict.assigned()) + return DictToVariant(dict, context); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +OpcUaObject SelectionVariantConverter::ToKeyValuePair(const IntegerPtr& key, + const BaseObjectPtr& value, + const ContextPtr& context) +{ + OpcUaObject pair; + pair->key = key; + pair->value = VariantConverter::ToVariant(value, nullptr, context).getDetachedValue(); + return pair; +} + +OpcUaVariant SelectionVariantConverter::ListToVariant(const ListPtr& selectionValues, const ContextPtr& context) +{ + auto variant = OpcUaVariant(); + if (selectionValues.getCount() == 0) + return variant; + + const auto type = GetUaDataType(); + const auto arr = static_cast(UA_Array_new(selectionValues.getCount(), type)); + + for (SizeT i = 0; i < selectionValues.getCount(); ++i) + { + auto tmsStruct = ToKeyValuePair(i, selectionValues[i], context); + arr[i] = tmsStruct.getDetachedValue(); + } + + UA_Variant_setArray(variant.get(), arr, selectionValues.getCount(), type); + return variant; +} + +OpcUaVariant SelectionVariantConverter::DictToVariant(const DictPtr& selectionValues, const ContextPtr& context) +{ + auto variant = OpcUaVariant(); + if (selectionValues.getCount() == 0) + return variant; + + const auto type = GetUaDataType(); + const auto arr = static_cast(UA_Array_new(selectionValues.getCount(), type)); + + int i = 0; + for (auto [key, value] : selectionValues) + { + auto tmsStruct = ToKeyValuePair(key, value, context); + arr[i] = tmsStruct.getDetachedValue(); + ++i; + } + + UA_Variant_setArray(variant.get(), arr, selectionValues.getCount(), type); + return variant; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/converters/unit_converter.cpp b/shared/libraries/opcuatms/opcuatms/src/converters/unit_converter.cpp new file mode 100644 index 0000000..265229c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/converters/unit_converter.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Template specializations + +template class StructConverter; +template class StructConverter; +template class VariantConverter; + +// UA_EUInformationWithQuantity + +template <> +UnitPtr StructConverter::ToDaqObject(const UA_EUInformationWithQuantity& tmsStruct, + const ContextPtr& /*context*/) +{ + const StringPtr displayName(ConvertToDaqCoreString(tmsStruct.displayName.text)); + const StringPtr description(ConvertToDaqCoreString(tmsStruct.description.text)); + const StringPtr quantity(ConvertToDaqCoreString(tmsStruct.quantity)); + + return Unit(displayName, tmsStruct.unitId, description, quantity); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const UnitPtr& object, + const ContextPtr& /*context*/) +{ + OpcUaObject tmsUnit; + + tmsUnit->namespaceUri = UA_STRING_ALLOC("http://www.opcfoundation.org/UA/units/un/cefact"); + tmsUnit->unitId = object.getId(); + tmsUnit->description = UA_LOCALIZEDTEXT_ALLOC("en-US", object.getName().getCharPtr()); + tmsUnit->displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", object.getSymbol().getCharPtr()); + tmsUnit->quantity = ConvertToOpcUaString(object.getQuantity()).getDetachedValue(); + + return tmsUnit; +} + +// UA_EUInformation + +template <> +UnitPtr StructConverter::ToDaqObject(const UA_EUInformation& tmsStruct, const ContextPtr& /*context*/) +{ + const StringPtr displayName(ConvertToDaqCoreString(tmsStruct.displayName.text)); + const StringPtr description(ConvertToDaqCoreString(tmsStruct.description.text)); + + return Unit(displayName, tmsStruct.unitId, description, ""); +} + +template <> +OpcUaObject StructConverter::ToTmsType(const UnitPtr& object, const ContextPtr& /*context*/) +{ + OpcUaObject tmsUnit; + + tmsUnit->unitId = object.getId(); + tmsUnit->description = UA_LOCALIZEDTEXT_ALLOC("en-US", object.getName().getCharPtr()); + tmsUnit->displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", object.getSymbol().getCharPtr()); + + return tmsUnit; +} + +// Variant converter + +template <> +UnitPtr VariantConverter::ToDaqObject(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + const auto decodedVariant = DecodeIfExtensionObject(variant); + + if (decodedVariant.isType()) + return StructConverter::ToDaqObject(*static_cast(decodedVariant->data)); + + if (decodedVariant.isType()) + return StructConverter::ToDaqObject(*static_cast(decodedVariant->data)); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToVariant(const UnitPtr& object, const UA_DataType* targetType, const ContextPtr& /*context*/) +{ + auto variant = OpcUaVariant(); + + if (targetType == nullptr || targetType == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_EUINFORMATIONWITHQUANTITY]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else if (targetType == &UA_TYPES[UA_TYPES_EUINFORMATION]) + variant.setScalar(*StructConverter::ToTmsType(object)); + else + DAQ_THROW_EXCEPTION(ConversionFailedException); + + return variant; +} + +template <> +ListPtr VariantConverter::ToDaqList(const OpcUaVariant& variant, const ContextPtr& /*context*/) +{ + if (variant.isType()) + return ListConversionUtils::ExtensionObjectVariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + if (variant.isType()) + return ListConversionUtils::VariantToList(variant); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +template <> +OpcUaVariant VariantConverter::ToArrayVariant(const ListPtr& list, + const UA_DataType* targetType, + const ContextPtr& /*context*/) +{ + if (targetType == nullptr || targetType == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_EUINFORMATIONWITHQUANTITY]) + return ListConversionUtils::ToArrayVariant(list); + if (targetType == &UA_TYPES[UA_TYPES_EUINFORMATION]) + return ListConversionUtils::ToArrayVariant(list); + + DAQ_THROW_EXCEPTION(ConversionFailedException); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/src/core_types_utils.cpp b/shared/libraries/opcuatms/opcuatms/src/core_types_utils.cpp new file mode 100644 index 0000000..5237909 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/core_types_utils.cpp @@ -0,0 +1,309 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace daq::opcua; +using namespace daq; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +namespace details +{ + +static std::unordered_map nodeIdToCoreTypeMap = { + {OpcUaNodeId(0, UA_NS0ID_BOOLEAN), ctBool}, + {OpcUaNodeId(0, UA_NS0ID_FLOAT), ctFloat}, + {OpcUaNodeId(0, UA_NS0ID_DOUBLE), ctFloat}, + {OpcUaNodeId(0, UA_NS0ID_SBYTE), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_BYTE), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_INT16), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_UINT16), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_INT32), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_UINT32), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_INT64), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_UINT64), ctInt}, + {OpcUaNodeId(0, UA_NS0ID_STRING), ctString}, + {OpcUaNodeId(0, UA_NS0ID_RATIONALNUMBER), ctRatio}, + {OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_RATIONALNUMBER64), ctRatio}, + {OpcUaNodeId(0, UA_DAQBTID_RATIONALNUMBER64), ctRatio}}; + +static std::unordered_set convertibleNativeStructs = { + "Unit", "Range", "ArgumentInfo", "ComplexNumber", "FunctionBlockType"}; +} + +StringPtr ConvertToDaqCoreString(const UA_String& uaString) +{ + if (uaString.length == 0 && uaString.data == nullptr) + return nullptr; + return String(reinterpret_cast(uaString.data), uaString.length); +} + +OpcUaObject ConvertToOpcUaString(const StringPtr& str) +{ + if (str.assigned()) + return OpcUaObject(UA_STRING_ALLOC(str.getCharPtr())); + return {}; +} + +BinaryDataPtr CreateCoreBinaryDataFromUaByteString(const UA_ByteString& uaByteString) +{ + if (uaByteString.length == 0 && uaByteString.data == nullptr) + return nullptr; + return BinaryData(reinterpret_cast(uaByteString.data), uaByteString.length); +} + +OpcUaObject CreateUaByteStringFromCoreBinaryData(const BinaryDataPtr& binaryData) +{ + OpcUaObject byteString; + byteString->data = (uint8_t*) UA_malloc(binaryData.getSize()); + byteString->length = binaryData.getSize(); + memcpy(byteString->data, binaryData.getAddress(), binaryData.getSize()); + return OpcUaObject(byteString); +} + +SampleType SampleTypeFromTmsEnum(UA_SampleTypeEnumeration tmsEnum) +{ + switch (tmsEnum) + { + case UA_SAMPLETYPEENUMERATION_INVALID: + return SampleType::Invalid; + case UA_SAMPLETYPEENUMERATION_FLOAT32: + return SampleType::Float32; + case UA_SAMPLETYPEENUMERATION_FLOAT64: + return SampleType::Float64; + case UA_SAMPLETYPEENUMERATION_UINT8: + return SampleType::UInt8; + case UA_SAMPLETYPEENUMERATION_INT8: + return SampleType::Int8; + case UA_SAMPLETYPEENUMERATION_UINT16: + return SampleType::UInt16; + case UA_SAMPLETYPEENUMERATION_INT16: + return SampleType::Int16; + case UA_SAMPLETYPEENUMERATION_UINT32: + return SampleType::UInt32; + case UA_SAMPLETYPEENUMERATION_INT32: + return SampleType::Int32; + case UA_SAMPLETYPEENUMERATION_UINT64: + return SampleType::UInt64; + case UA_SAMPLETYPEENUMERATION_INT64: + return SampleType::Int64; + case UA_SAMPLETYPEENUMERATION_COMPLEXFLOAT32: + return SampleType::ComplexFloat32; + case UA_SAMPLETYPEENUMERATION_COMPLEXFLOAT64: + return SampleType::ComplexFloat64; + case UA_SAMPLETYPEENUMERATION_BINARY: + return SampleType::Binary; + case UA_SAMPLETYPEENUMERATION_STRING: + return SampleType::String; + case UA_SAMPLETYPEENUMERATION_RANGEINT64: + return SampleType::RangeInt64; + default: + DAQ_THROW_EXCEPTION(ConversionFailedException); + } +} + +UA_SampleTypeEnumeration SampleTypeToTmsEnum(SampleType daqEnum) +{ + switch (daqEnum) + { + case SampleType::Invalid: + return UA_SAMPLETYPEENUMERATION_INVALID; + case SampleType::Float32: + return UA_SAMPLETYPEENUMERATION_FLOAT32; + case SampleType::Float64: + return UA_SAMPLETYPEENUMERATION_FLOAT64; + case SampleType::UInt8: + return UA_SAMPLETYPEENUMERATION_UINT8; + case SampleType::Int8: + return UA_SAMPLETYPEENUMERATION_INT8; + case SampleType::UInt16: + return UA_SAMPLETYPEENUMERATION_UINT16; + case SampleType::Int16: + return UA_SAMPLETYPEENUMERATION_INT16; + case SampleType::UInt32: + return UA_SAMPLETYPEENUMERATION_UINT32; + case SampleType::Int32: + return UA_SAMPLETYPEENUMERATION_INT32; + case SampleType::UInt64: + return UA_SAMPLETYPEENUMERATION_UINT64; + case SampleType::Int64: + return UA_SAMPLETYPEENUMERATION_INT64; + case SampleType::ComplexFloat32: + return UA_SAMPLETYPEENUMERATION_COMPLEXFLOAT32; + case SampleType::ComplexFloat64: + return UA_SAMPLETYPEENUMERATION_COMPLEXFLOAT64; + case SampleType::Binary: + return UA_SAMPLETYPEENUMERATION_BINARY; + case SampleType::String: + return UA_SAMPLETYPEENUMERATION_STRING; + case SampleType::RangeInt64: + return UA_SAMPLETYPEENUMERATION_RANGEINT64; + case SampleType::Struct: + return UA_SAMPLETYPEENUMERATION_INVALID; + case SampleType::Null: + DAQ_THROW_EXCEPTION(ConversionFailedException, + "SampleType \"Null\" is not convertible and reserved for \"DATA_DESCRIPTOR_CHANGED\" event packet." + ); + default: + DAQ_THROW_EXCEPTION(ConversionFailedException); + } +} + +ScaledSampleType ScaledSampleTypeFromTmsEnum(UA_SampleTypeEnumeration tmsEnum) +{ + switch (tmsEnum) + { + case UA_SAMPLETYPEENUMERATION_FLOAT32: + return ScaledSampleType::Float32; + case UA_SAMPLETYPEENUMERATION_FLOAT64: + return ScaledSampleType::Float64; + default: + DAQ_THROW_EXCEPTION(ConversionFailedException); + } +} + +UA_SampleTypeEnumeration ScaledSampleTypeToTmsEnum(ScaledSampleType daqEnum) +{ + switch (daqEnum) + { + case ScaledSampleType::Float32: + return UA_SAMPLETYPEENUMERATION_FLOAT32; + case ScaledSampleType::Float64: + return UA_SAMPLETYPEENUMERATION_FLOAT64; + default: + DAQ_THROW_EXCEPTION(ConversionFailedException); + } +} + +OpcUaNodeId CoreTypeToUANodeID(CoreType type) +{ + switch (type) + { + case ctBool: + return OpcUaNodeId(0, UA_NS0ID_BOOLEAN); + case ctInt: + return OpcUaNodeId(0, UA_NS0ID_INT64); + case ctFloat: + return OpcUaNodeId(0, UA_NS0ID_DOUBLE); + case ctString: + return OpcUaNodeId(0, UA_NS0ID_STRING); + case ctRatio: + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_RATIONALNUMBER64); + case ctProc: + case ctList: + case ctDict: + case ctObject: + case ctBinaryData: + case ctFunc: + case ctComplexNumber: + case ctUndefined: + default: + DAQ_THROW_EXCEPTION(ConversionFailedException, "Mapping between core type and node id is not available"); + } +} + +CoreType UANodeIdToCoreType(OpcUaNodeId nodeId) +{ + if (const auto it = details::nodeIdToCoreTypeMap.find(nodeId); it != details::nodeIdToCoreTypeMap.cend()) + return it->second; + DAQ_THROW_EXCEPTION(ConversionFailedException, "Mapping between node id and core type is not available."); +} + +OpcUaVariant DecodeIfExtensionObject(const OpcUaVariant& variant) +{ + if (variant.isType()) + { + const auto data = (UA_ExtensionObject*) variant->data; + auto extensionObject = tms::ExtensionObject(data[0]); + if (extensionObject.isDecoded()) + return extensionObject.getAsVariant(); + + DAQ_THROW_EXCEPTION(ConversionFailedException); + } + + return variant; +} + +OpcUaVariant UnwrapIfVariant(const OpcUaVariant& variant) +{ + if (variant.isType()) + { + const auto data = (UA_Variant*) variant->data; + return OpcUaVariant(data[0]); + } + + return variant; +} + +const UA_DataType* GetUAStructureDataTypeByName(const std::string& structName) +{ + // TODO: Create static list, add any custom types added automatically. + OpcUaDataTypeArrayList typeArr; + typeArr.add(UA_TYPES_COUNT, UA_TYPES); + typeArr.add(UA_TYPES_DI_COUNT, UA_TYPES_DI); + typeArr.add(UA_TYPES_DAQBT_COUNT, UA_TYPES_DAQBT); + typeArr.add(UA_TYPES_DAQBSP_COUNT, UA_TYPES_DAQBSP); + typeArr.add(UA_TYPES_DAQDEVICE_COUNT, UA_TYPES_DAQDEVICE); + typeArr.add(UA_TYPES_DAQESP_COUNT, UA_TYPES_DAQESP); + typeArr.add(UA_TYPES_DAQHBK_COUNT, UA_TYPES_DAQHBK); + + const UA_DataTypeArray* dataType = typeArr.getCustomDataTypes(); + while (dataType) + { + for (size_t i = 0; i < dataType->typesSize; ++i) + { + if (dataType->types[i].typeName == structName) + { + const auto typeKind = dataType->types[i].typeKind; + if (typeKind == UA_DATATYPEKIND_STRUCTURE || typeKind == UA_DATATYPEKIND_OPTSTRUCT) + return &dataType->types[i]; + } + } + dataType = dataType->next; + } + + return nullptr; +} + +const UA_DataType* GetUAEnumerationDataTypeByName(const std::string& enumerationName) +{ + // TODO: Create static list, add any custom types added automatically. + OpcUaDataTypeArrayList typeArr; + typeArr.add(UA_TYPES_COUNT, UA_TYPES); + typeArr.add(UA_TYPES_DI_COUNT, UA_TYPES_DI); + typeArr.add(UA_TYPES_DAQBT_COUNT, UA_TYPES_DAQBT); + typeArr.add(UA_TYPES_DAQBSP_COUNT, UA_TYPES_DAQBSP); + typeArr.add(UA_TYPES_DAQDEVICE_COUNT, UA_TYPES_DAQDEVICE); + typeArr.add(UA_TYPES_DAQESP_COUNT, UA_TYPES_DAQESP); + typeArr.add(UA_TYPES_DAQHBK_COUNT, UA_TYPES_DAQHBK); + + const UA_DataTypeArray* dataType = typeArr.getCustomDataTypes(); + while(dataType) + { + for(size_t i = 0; i < dataType->typesSize; ++i) + { + if (dataType->types[i].typeName == enumerationName) + { + const auto typeKind = dataType->types[i].typeKind; + if (typeKind == UA_DATATYPEKIND_ENUM) + return &dataType->types[i]; + } + } + dataType = dataType->next; + } + + return nullptr; +} + +bool nativeStructConversionSupported(const std::string& structName) +{ + return details::convertibleNativeStructs.count(structName); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms/src/extension_object.cpp b/shared/libraries/opcuatms/opcuatms/src/extension_object.cpp new file mode 100644 index 0000000..cc6e175 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/src/extension_object.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +ExtensionObject::ExtensionObject() + : Super() +{ +} + +ExtensionObject::ExtensionObject(const OpcUaObject& extensionObject) + : Super(extensionObject) +{ +} + +ExtensionObject::ExtensionObject(const daq::opcua::OpcUaVariant& variant) + : Super() +{ + this->setFromVariant(variant); +} + +void ExtensionObject::setFromVariant(const daq::opcua::OpcUaVariant& variant) +{ + + if (variant.isNull()) + UA_ExtensionObject_clear(&value); + else + UA_ExtensionObject_setValueCopy(&value, variant->data, variant->type); +} + +daq::opcua::OpcUaVariant ExtensionObject::getAsVariant() +{ + if (!isDecoded()) + throw OpcUaObjectNotDecodedException(); + + this->markDetached(true); + auto variant = OpcUaVariant(); + variant->data = this->value.content.decoded.data; + variant->type = this->value.content.decoded.type; + return variant; +} + +bool ExtensionObject::isDecoded() const +{ + return this->value.encoding == UA_EXTENSIONOBJECT_DECODED || this->value.encoding == UA_EXTENSIONOBJECT_DECODED_NODELETE; +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms/tests/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms/tests/CMakeLists.txt new file mode 100644 index 0000000..6acdfbb --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/CMakeLists.txt @@ -0,0 +1,30 @@ +set(MODULE_NAME opcuatms) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_core_types_utils.cpp + test_exception.cpp + test_variant_converter.cpp + test_variant_list_converter.cpp + test_extension_object.cpp + test_generic_struct_converter.cpp + test_property_object_conversion_utils.cpp +) + +add_executable(${TEST_APP} testapp.cpp + ${TEST_SOURCES} +) + +set_target_properties(${TEST_APP} PROPERTIES DEBUG_POSTFIX _debug) + +target_link_libraries(${TEST_APP} PRIVATE ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::test_utils +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}coverage ${TEST_APP} ${MODULE_NAME}coverage) +endif() diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_core_types_utils.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_core_types_utils.cpp new file mode 100644 index 0000000..78cdb9b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_core_types_utils.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include + +using CoreTypesUtilsTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; + +TEST_F(CoreTypesUtilsTest, ConvertToDaqCoreString) +{ + UA_String testString = UA_String_fromChars("test"); + StringPtr rtString = ConvertToDaqCoreString(testString); + ASSERT_EQ(rtString.toStdString(), "test"); + + UA_String_clear(&testString); +} + +TEST_F(CoreTypesUtilsTest, ConvertToDaqCoreStringNull) +{ + UA_String testString = UA_STRING_NULL; + StringPtr rtString = ConvertToDaqCoreString(testString); + ASSERT_FALSE(rtString.assigned()); + + UA_String_clear(&testString); +} + +TEST_F(CoreTypesUtilsTest, ConvertToDaqCoreStringEmpty) +{ + UA_String testString = UA_String_fromChars(""); + StringPtr rtString = ConvertToDaqCoreString(testString); + ASSERT_EQ(rtString.toStdString(), ""); + + UA_String_clear(&testString); +} + +TEST_F(CoreTypesUtilsTest, ConvertToOpcUaString) +{ + StringPtr rtString = "test"; + + OpcUaObject str = ConvertToOpcUaString(rtString); + rtString.release(); + + ASSERT_TRUE(*str == "test"); +} + +TEST_F(CoreTypesUtilsTest, ConvertToOpcUaStringNull) +{ + StringPtr rtString; + + OpcUaObject str = ConvertToOpcUaString(rtString); + rtString.release(); + + ASSERT_TRUE(*str == UA_STRING_NULL); +} + +TEST_F(CoreTypesUtilsTest, SampleTypeConverter) +{ + ASSERT_EQ(SampleTypeFromTmsEnum(SampleTypeToTmsEnum(SampleType::Int32)), SampleType::Int32); + ASSERT_EQ(SampleTypeFromTmsEnum(SampleTypeToTmsEnum(SampleType::Int64)), SampleType::Int64); + ASSERT_EQ(SampleTypeFromTmsEnum(SampleTypeToTmsEnum(SampleType::UInt64)), SampleType::UInt64); + ASSERT_THROW_MSG(SampleTypeToTmsEnum(SampleType::Null), + ConversionFailedException, + "SampleType \"Null\" is not convertible and reserved for \"DATA_DESCRIPTOR_CHANGED\" event packet."); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_dict_conversion_utils.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_dict_conversion_utils.cpp new file mode 100644 index 0000000..53f16f7 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_dict_conversion_utils.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include + +using DictConversionUtilsTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::tms; + + +static PropertyObjectPtr CreateTestPropertyObject() +{ + auto obj = PropertyObject(); + obj.addProperty(StringProperty("name", "")); + obj.addProperty(IntProperty("age", 0)); + obj.addProperty(FloatProperty("weight", 0.0)); + obj.addProperty(BoolProperty("isTheBest", false)); + return obj; +} + + +TEST_F(DictConversionUtilsTest, SimpleObject) +{ + auto obj = CreateTestPropertyObject(); + obj.setPropertyValue("name", "Jovanka"); + obj.setPropertyValue("age", 99); + obj.setPropertyValue("weight", 60.5); + obj.setPropertyValue("isTheBest", true); + + auto variant = PropertyObjectConversionUtils::ToDictVariant(obj); + + ASSERT_TRUE(variant->type == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]); + ASSERT_EQ(variant->arrayLength, obj.getAllProperties().getCount()); + + auto objOut = CreateTestPropertyObject(); + PropertyObjectConversionUtils::ToPropertyObject(variant, objOut); + ASSERT_TRUE(TestComparators::PropertyObjectEquals(obj, objOut)); +} + +TEST_F(DictConversionUtilsTest, EmptyObject) +{ + auto obj = CreateTestPropertyObject(); + + auto variant = PropertyObjectConversionUtils::ToDictVariant(obj); + + ASSERT_TRUE(variant->type == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]); + ASSERT_EQ(variant->arrayLength, obj.getAllProperties().getCount()); + + auto objOut = CreateTestPropertyObject(); + PropertyObjectConversionUtils::ToPropertyObject(variant, objOut); + ASSERT_TRUE(TestComparators::PropertyObjectEquals(obj, objOut)); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_exception.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_exception.cpp new file mode 100644 index 0000000..215bc9a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_exception.cpp @@ -0,0 +1,13 @@ +#include +#include + +using ExceptionTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; + +TEST_F(ExceptionTest, ThrowException) +{ + EXPECT_THROW(throw OpcUaGeneralException(), OpcUaGeneralException); + EXPECT_THROW(throwExceptionFromErrorCode(OPENDAQ_ERR_OPCUA_GENERAL), OpcUaGeneralException); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_extension_object.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_extension_object.cpp new file mode 100644 index 0000000..a5f4042 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_extension_object.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include + +using ExtensionObjectTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::tms; +using namespace daq::opcua; + +TEST_F(ExtensionObjectTest, IsDecoded) +{ + const RangePtr range = Range(0, 10); + const auto variant = VariantConverter::ToVariant(range); + + ExtensionObject eo(variant); + ASSERT_TRUE(eo.isDecoded()); +} + +TEST_F(ExtensionObjectTest, IsType) +{ + const RangePtr range = Range(0, 10); + auto variant = VariantConverter::ToVariant(range); + + ExtensionObject eo(variant); + ASSERT_TRUE(eo.isType()); + ASSERT_FALSE(eo.isType()); +} + +TEST_F(ExtensionObjectTest, VariantConversion) +{ + const RangePtr range = Range(0, 10); + const RangePtr rangeWrong = Range(10, 20); + const auto variant = VariantConverter::ToVariant(range); + + auto eo = ExtensionObject(); + eo.setFromVariant(variant); + const auto variantOut = eo.getAsVariant(); + const RangePtr rangeOut = VariantConverter::ToDaqObject(variantOut); + + ASSERT_TRUE(rangeOut.equals(range)); + ASSERT_FALSE(rangeOut.equals(rangeWrong)); +} + +TEST_F(ExtensionObjectTest, ExtensionObjectConstructor) +{ + UA_Range tmsRangeSrc = {0, 10}; + RangePtr rangeSrc = Range(0, 10); + RangePtr rangeWrong = Range(10, 20); + + OpcUaObject eoSrc; + UA_ExtensionObject_setValueCopy(eoSrc.get(), &tmsRangeSrc, &UA_TYPES[UA_TYPES_RANGE]); + + ExtensionObject eo(eoSrc); + const auto variantOut = eo.getAsVariant(); + const auto rangeOut = VariantConverter::ToDaqObject(variantOut); + + ASSERT_TRUE(rangeOut.equals(rangeSrc)); + ASSERT_FALSE(rangeOut.equals(rangeWrong)); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_generic_struct_converter.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_generic_struct_converter.cpp new file mode 100644 index 0000000..3e8b0df --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_generic_struct_converter.cpp @@ -0,0 +1,251 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using GenericStructConverterTest = testing::Test; + +using namespace daq; +using namespace opcua; +using namespace tms; +using namespace opcua; + +namespace test_helpers +{ + static ContextPtr setupContext() + { + auto typeManager = TypeManager(); + typeManager.addType(StructType("RationalNumber64", + List("Numerator", "Denominator"), + List(SimpleType(ctInt), SimpleType(ctInt)))); + + typeManager.addType(StructType("DeviceDomainStructure", + List("Resolution", "TicksSinceOrigin", "Origin", "Unit"), + List(SimpleType(ctRatio), SimpleType(ctInt), SimpleType(ctString), UnitStructType()))); + + typeManager.addType(StructType("DimensionDescriptorStructure", + List("Name", "DimensionRule", "Unit"), + List(SimpleType(ctString), SimpleType(ctStruct), UnitStructType()))); + + typeManager.addType(StructType("ListRuleDescriptionStructure", + List("Type", "Elements"), + List(SimpleType(ctString), SimpleType(ctList)))); + + typeManager.addType(StructType("CustomRuleDescriptionStructure", + List("Type", "Parameters"), + List(SimpleType(ctString), SimpleType(ctDict)))); + + typeManager.addType(StructType("AdditionalParametersType", + List("Parameters"), + List(SimpleType(ctList)))); + + typeManager.addType(StructType("KeyValuePair", + List("Key", "Value"), + List(SimpleType(ctString), SimpleType(ctUndefined)))); + + return Context(nullptr, Logger(), typeManager, nullptr, nullptr); + } +} + +TEST_F(GenericStructConverterTest, TestSimpleStruct) +{ + auto context = test_helpers::setupContext(); + DictPtr members = Dict(); + members.set("Numerator", 10); + members.set("Denominator", 50); + + const auto structure = Struct("RationalNumber64", members, context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + + const auto rational = static_cast(var->data); + ASSERT_EQ(rational->numerator, 10); + ASSERT_EQ(rational->denominator, 50); + + const auto convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure.getStructType().getName(), convertedStructure.getStructType().getName()); + ASSERT_EQ(structure.getAsDictionary(), convertedStructure.getAsDictionary()); +} + +TEST_F(GenericStructConverterTest, TestStructWithOtherStructs) +{ + auto context = test_helpers::setupContext(); + DictPtr members = Dict({{"Resolution", Ratio(10, 20)}, + {"TicksSinceOrigin", 1000}, + {"Origin", "origin"}, + {"Unit", Unit("symbol", -1, "name", "quantity")}}); + const auto structure = Struct("DeviceDomainStructure", members, context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + const auto deviceDomain = static_cast(var->data); + ASSERT_EQ(utils::ToStdString(deviceDomain->origin), "origin"); + ASSERT_EQ(utils::ToStdString(deviceDomain->unit.quantity), "quantity"); + ASSERT_EQ(deviceDomain->unit.unitId, -1); + ASSERT_EQ(deviceDomain->ticksSinceOrigin, 1000); + ASSERT_EQ(deviceDomain->resolution.numerator, 10); + + const auto convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure.getAsDictionary(), convertedStructure.getAsDictionary()); +} + +TEST_F(GenericStructConverterTest, TestDataDescriptorStruct) +{ + const auto context = test_helpers::setupContext(); + const auto dataDescriptor = DataDescriptorBuilder().build(); + const auto var = VariantConverter::ToVariant(dataDescriptor, nullptr, context); + const StructPtr dataDescriptorStruct = VariantConverter::ToDaqObject(var, context); + + ASSERT_EQ(dataDescriptor.asPtr().getStructType(), dataDescriptorStruct.getStructType()); + ASSERT_EQ(dataDescriptor.asPtr().getAsDictionary(), dataDescriptorStruct.getAsDictionary()); +} + +TEST_F(GenericStructConverterTest, TestStructWithOptionalsAssigned) +{ + auto context = test_helpers::setupContext(); + DictPtr members = Dict({{"Name", "name"}, + {"DimensionRule", + Struct("LinearRuleDescriptionStructure", + Dict( + {{"Type", "linear"}, {"Start", 10}, {"Delta", 10}, + {"Size", 10}}), + context.getTypeManager())}, + {"Unit", Unit("symbol", -1, "name", "quantity")}}); + const auto structure = Struct("DimensionDescriptorStructure", members, context.getTypeManager()); + + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + + const auto dimension = static_cast(var->data); + ASSERT_EQ(utils::ToStdString(*dimension->name), "name"); + ASSERT_EQ(utils::ToStdString(dimension->unit->quantity), "quantity"); + + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} + +TEST_F(GenericStructConverterTest, TestStructWithOptionalsUnassigned1) +{ + auto context = test_helpers::setupContext(); + context.getTypeManager().addType(StructType("LinearRuleDescriptionStructure", + List("Type", "Start", "Delta", "Size"), + List(SimpleType(ctString), + SimpleType(ctInt), + SimpleType(ctInt), + SimpleType(ctInt)))); + DictPtr members = Dict({{"Name", "name"}, + {"DimensionRule", + Struct("LinearRuleDescriptionStructure", + Dict( + {{"Type", "linear"}, {"Start", 10}, {"Delta", 10}, + {"Size", nullptr}}), + context.getTypeManager())}, + {"Unit", nullptr}}); + const auto structure = Struct("DimensionDescriptorStructure", members, context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + + const auto dimension = static_cast(var->data); + ASSERT_EQ(utils::ToStdString(*dimension->name), "name"); + ASSERT_EQ(dimension->unit, nullptr); + + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} + +TEST_F(GenericStructConverterTest, TestStructWithOptionalsUnassigned2) +{ + auto context = test_helpers::setupContext(); + DictPtr members = Dict({{"Name", "name"}, + {"DimensionRule", + Struct("LinearRuleDescriptionStructure", + Dict( + {{"Type", "linear"}, {"Start", 10}, {"Delta", 10}, + {"Size", nullptr}}), + context.getTypeManager())}, + {"Unit", nullptr}}); + const auto structure = Struct("DimensionDescriptorStructure", members, context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + + const auto dimension = static_cast(var->data); + ASSERT_EQ(utils::ToStdString(*dimension->name), "name"); + ASSERT_EQ(dimension->unit, nullptr); + + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} + +TEST_F(GenericStructConverterTest, TestStructWithArrays1) +{ + auto context = test_helpers::setupContext(); + const auto structure = Struct("ListRuleDescriptionStructure", + Dict({{"Type", "list"}, {"Elements", List("foo", "bar")}}), + context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + + const auto rule = static_cast(var->data); + ASSERT_EQ(utils::ToStdString(rule->type), "list"); + ASSERT_EQ(rule->elementsSize, 2u); + + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} + +TEST_F(GenericStructConverterTest, TestStructWithArrays2) +{ + auto context = test_helpers::setupContext(); + const auto structure = Struct("CustomRuleDescriptionStructure", + Dict({{"Type", "list"}, {"Parameters", Dict({{"foo", "bar"}, {"foo1", "bar1"}})}}), + context.getTypeManager()); + + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + + const auto rule = static_cast(var->data); + ASSERT_EQ(utils::ToStdString(rule->type), "list"); + ASSERT_EQ(rule->parametersSize, 2u); + + auto keyVariant = OpcUaVariant(rule->parameters[0].key); + ASSERT_EQ(VariantConverter::ToDaqObject(keyVariant, context), "foo"); + + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} + +TEST_F(GenericStructConverterTest, TestStructWithArrays3) +{ + auto context = test_helpers::setupContext(); + const auto keyValuePairList = List( + Struct("KeyValuePair", Dict({{"Key", "key1"}, {"Value", "value1"}}), context.getTypeManager()), + Struct("KeyValuePair", Dict({{"Key", "key1"}, {"Value", "value1"}}), context.getTypeManager())); + + const auto structure = Struct("AdditionalParametersType", + Dict({{"Parameters", keyValuePairList}}), + context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} + +TEST_F(GenericStructConverterTest, TestStructWithArraysEmptyList) +{ + auto context = test_helpers::setupContext(); + const auto keyValuePairList = List(); + + const auto structure = Struct("AdditionalParametersType", + Dict({{"Parameters", keyValuePairList}}), + context.getTypeManager()); + + auto var = VariantConverter::ToVariant(structure, nullptr, context); + const StructPtr convertedStructure = VariantConverter::ToDaqObject(var, context); + ASSERT_EQ(structure, convertedStructure); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_property_object_conversion_utils.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_property_object_conversion_utils.cpp new file mode 100644 index 0000000..59ad260 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_property_object_conversion_utils.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include + +using PropertyObjectConversionUtilsTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::tms; + + +static PropertyObjectPtr CreateTestPropertyObject() +{ + auto obj = PropertyObject(); + obj.addProperty(StringProperty("Name", "")); + obj.addProperty(IntProperty("age", 0)); + obj.addProperty(FloatProperty("weight", 0.0)); + obj.addProperty(BoolProperty("isTheBest", false)); + return obj; +} + + +TEST_F(PropertyObjectConversionUtilsTest, SimpleObject) +{ + auto obj = CreateTestPropertyObject(); + obj.setPropertyValue("Name", "Jovanka"); + obj.setPropertyValue("age", 99); + obj.setPropertyValue("weight", 60.5); + obj.setPropertyValue("isTheBest", true); + + auto variant = PropertyObjectConversionUtils::ToDictVariant(obj); + + ASSERT_TRUE(variant->type == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]); + ASSERT_EQ(variant->arrayLength, obj.getAllProperties().getCount()); + + auto objOut = CreateTestPropertyObject(); + PropertyObjectConversionUtils::ToPropertyObject(variant, objOut); + ASSERT_TRUE(TestComparators::PropertyObjectEquals(obj, objOut)); +} + +TEST_F(PropertyObjectConversionUtilsTest, EmptyObject) +{ + auto obj = CreateTestPropertyObject(); + + auto variant = PropertyObjectConversionUtils::ToDictVariant(obj); + + ASSERT_TRUE(variant->type == &UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR]); + ASSERT_EQ(variant->arrayLength, obj.getAllProperties().getCount()); + + auto objOut = CreateTestPropertyObject(); + PropertyObjectConversionUtils::ToPropertyObject(variant, objOut); + ASSERT_TRUE(TestComparators::PropertyObjectEquals(obj, objOut)); +} + +TEST_F(PropertyObjectConversionUtilsTest, CloneDefault) +{ + auto obj = CreateTestPropertyObject(); + auto clone = PropertyObjectConversionUtils::ClonePropertyObject(obj); + + ASSERT_TRUE(TestComparators::PropertyObjectEquals(obj, clone)); +} + +TEST_F(PropertyObjectConversionUtilsTest, Clone) +{ + auto obj = CreateTestPropertyObject(); + obj.setPropertyValue("Name", "Jovanka"); + obj.setPropertyValue("age", 99); + obj.setPropertyValue("weight", 60.5); + obj.setPropertyValue("isTheBest", true); + + auto clone = PropertyObjectConversionUtils::ClonePropertyObject(obj); + + ASSERT_TRUE(TestComparators::PropertyObjectEquals(obj, clone)); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_variant_converter.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_variant_converter.cpp new file mode 100644 index 0000000..6d28893 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_variant_converter.cpp @@ -0,0 +1,436 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using VariantConverterTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::tms; +using namespace opcua; + +static ListPtr CreateTestDimensions() +{ + auto list = List(); + + const auto rule1 = LinearDimensionRule(2.0, 3.0, 4); + const auto unit1 = Unit("V", 1, "voltage", "1"); + auto dimension1 = Dimension(rule1, unit1, "x"); + list.pushBack(dimension1); + + const auto rule2 = LogarithmicDimensionRule(1.0, 4.0, 2, 9); + auto dimension2 = Dimension(rule2); + list.pushBack(dimension2); + + return list; +} + +static DataDescriptorBuilderPtr CreateTestStructDescriptorBuilder() +{ + // struct Meta { + // string description; + // } + // + // struct CanMessage { + // int id; + // byte data; + // Meta meta; + // }; + + auto id = DataDescriptorBuilder().setSampleType(SampleType::Int32).setName("Id").build(); + auto data = DataDescriptorBuilder().setSampleType(SampleType::UInt8).setName("data").build(); + + auto desc = DataDescriptorBuilder() + .setSampleType(SampleType::String) + .setName("Description") + .setUnit(Unit("V", 1, "voltage", "Quantity")) + .setValueRange(Range(1, 10)) + .setRule(ExplicitDataRule()) + .setOrigin("2022-11-30T10:53:06") + .build(); + + auto meta = DataDescriptorBuilder() + .setSampleType(SampleType::Struct) + .setName("meta") + .setDimensions(CreateTestDimensions()) + .setStructFields(List(desc)) + .build(); + + auto can = DataDescriptorBuilder() + .setSampleType(SampleType::Struct) + .setName("CAN message") + .setStructFields(List(id, data, meta)); + + return can; +} + +static DataDescriptorPtr CreateTestStructDescriptor() +{ + return CreateTestStructDescriptorBuilder().build(); +} + +TEST_F(VariantConverterTest, Range) +{ + const RangePtr daqRange2 = Range(2, 20); + const RangePtr daqRange3 = Range(3, 30); + + const auto variant = VariantConverter::ToVariant(daqRange2); + const auto daqRangeOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqRangeOut.equals(daqRange2)); + ASSERT_FALSE(daqRangeOut.equals(daqRange3)); +} + +TEST_F(VariantConverterTest, Unit) +{ + const UnitPtr daqUnit2 = Unit("V", 2, "measured voltage", "voltage"); + const UnitPtr daqUnit3 = Unit("m", 3, "height of defender", "lenght"); + + const auto variant = VariantConverter::ToVariant(daqUnit2); + const auto daqUnitOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqUnitOut.equals(daqUnit2)); + ASSERT_FALSE(daqUnitOut.equals(daqUnit3)); +} + +TEST_F(VariantConverterTest, UnitMissingFields) +{ + auto daqUnit = UnitBuilder().setSymbol("s").build(); + const auto variant = VariantConverter::ToVariant(daqUnit); + const auto daqUnitOut = VariantConverter::ToDaqObject(variant); + ASSERT_TRUE(daqUnitOut.equals(daqUnit)); +} + +TEST_F(VariantConverterTest, Bool) +{ + auto variant = VariantConverter::ToVariant(True); + auto daqBoolOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(daqBoolOut, True); + + variant = VariantConverter::ToVariant(true); + daqBoolOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(daqBoolOut, true); + + variant = VariantConverter::ToVariant(False); + daqBoolOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(daqBoolOut, False); + + variant = VariantConverter::ToVariant(false); + daqBoolOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(daqBoolOut, false); +} + +TEST_F(VariantConverterTest, Int) +{ + auto variant = VariantConverter::ToVariant(55); + auto daqIntOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(daqIntOut, 55); +} + +TEST_F(VariantConverterTest, Float) +{ + auto variant = VariantConverter::ToVariant(55.55); + auto daqFloatOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(daqFloatOut, 55.55); +} + +TEST_F(VariantConverterTest, Number) +{ + const NumberPtr number1 = 10; + const NumberPtr number2 = 33.3; + const NumberPtr number3 = -27; + + NumberPtr numberOut; + OpcUaVariant variant; + + variant = VariantConverter::ToVariant(number1); + numberOut = VariantConverter::ToDaqObject(variant); + ASSERT_TRUE(numberOut.equals(number1)); + + variant = VariantConverter::ToVariant(number2); + numberOut = VariantConverter::ToDaqObject(variant); + ASSERT_TRUE(numberOut.equals(number2)); + + variant = VariantConverter::ToVariant(number3); + numberOut = VariantConverter::ToDaqObject(variant); + ASSERT_TRUE(numberOut.equals(number3)); + ASSERT_FALSE(numberOut.equals(number2)); +} + +TEST_F(VariantConverterTest, String) +{ + StringPtr hello = "Hello World!"; + + auto variant = VariantConverter::ToVariant(hello); + auto stringOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(stringOut, hello); +} + +TEST_F(VariantConverterTest, BaseObject) +{ + BaseObjectPtr object; + BaseObjectPtr objectOut; + OpcUaVariant variant; + + object = nullptr; + variant = VariantConverter::ToVariant(object); + objectOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(objectOut, object); + + object = true; + variant = VariantConverter::ToVariant(object); + objectOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(objectOut, object); + + object = -3; + variant = VariantConverter::ToVariant(object); + objectOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(objectOut, object); + + object = 12.5; + variant = VariantConverter::ToVariant(object); + objectOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(objectOut, object); + + object = "Hello World!"; + variant = VariantConverter::ToVariant(object); + objectOut = VariantConverter::ToDaqObject(variant); + ASSERT_EQ(objectOut, object); +} + +TEST_F(VariantConverterTest, LinearDataRule) +{ + const DataRulePtr daqDataRule = LinearDataRule(2.0, 3.0); + const DataRulePtr daqDataRuleWrong = LinearDataRule(1.0, 4.0); + + const auto variant = VariantConverter::ToVariant(daqDataRule); + const auto daqDataRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqDataRuleOut.equals(daqDataRule)); + ASSERT_FALSE(daqDataRuleOut.equals(daqDataRuleWrong)); +} + +TEST_F(VariantConverterTest, ConstantDataRule) +{ + const DataRulePtr daqDataRule = ConstantDataRule(); + + const auto variant = VariantConverter::ToVariant(daqDataRule); + const auto daqDataRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqDataRuleOut.equals(daqDataRule)); +} + +TEST_F(VariantConverterTest, ExplicitDataRule) +{ + const DataRulePtr daqDataRule = ExplicitDataRule(); + + const auto variant = VariantConverter::ToVariant(daqDataRule); + const auto daqDataRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqDataRuleOut.equals(daqDataRule)); +} + +TEST_F(VariantConverterTest, ExplicitDomainDataRule) +{ + const DataRulePtr daqDataRule = ExplicitDomainDataRule(5.123, 10); + + const auto variant = VariantConverter::ToVariant(daqDataRule); + const auto daqDataRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqDataRuleOut.equals(daqDataRule)); +} + +TEST_F(VariantConverterTest, CustomDataRule) +{ + auto params = Dict(); + params.set("count", 1); + params.set("Type", "apple"); + params.set("type1", "gala"); + params.set("type2", "fuji"); + params.set("weight", 1.123); + + auto daqDataRule = DataRuleBuilder().setType(DataRuleType::Other).setParameters(params).build(); + + const auto variant = VariantConverter::ToVariant(daqDataRule); + const auto daqDataRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqDataRuleOut.equals(daqDataRule)); +} + +TEST_F(VariantConverterTest, DataDescriptor) +{ + auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Float64) + .setName("Value 1") + .setUnit(Unit("V", 1, "voltage", "Quantity")) + .setValueRange(Range(1, 10)) + .setRule(ExplicitDataRule()) + .setDimensions(CreateTestDimensions()) + .setPostScaling(LinearScaling(10, 2)) + .build(); + + auto variant = VariantConverter::ToVariant(descriptor); + auto descriptorOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(descriptorOut.equals(descriptor)); +} + +TEST_F(VariantConverterTest, DataDescriptorEmpty) +{ + auto daqDescriptor = DataDescriptorBuilder().setSampleType(SampleType::Float64).build(); + auto variant = VariantConverter::ToVariant(daqDescriptor); + auto descriptorOut = VariantConverter::ToDaqObject(variant); + ASSERT_TRUE(descriptorOut.equals(daqDescriptor)); +} + +TEST_F(VariantConverterTest, StructDataDescriptor) +{ + auto descriptor = CreateTestStructDescriptor(); + auto descriptorWrong = DataDescriptorBuilder().setSampleType(SampleType::Float64).setName("Descriptor wrong").build(); + + auto variant = VariantConverter::ToVariant(descriptor); + auto descriptorOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(descriptorOut.equals(descriptor)); + ASSERT_FALSE(descriptorOut.equals(descriptorWrong)); +} + +TEST_F(VariantConverterTest, StructDescriptorEmpty) +{ + auto descriptor = DataDescriptorBuilder().setSampleType(SampleType::Float64).build(); + auto variant = VariantConverter::ToVariant(descriptor); + auto descriptorOut = VariantConverter::ToDaqObject(variant); + ASSERT_TRUE(descriptorOut.equals(descriptor)); +} + +TEST_F(VariantConverterTest, DataDescriptorMetadata) +{ + auto metadata = Dict(); + metadata.set("Name", "sine1"); + metadata.set("frequency", "50"); + + auto descriptor = CreateTestStructDescriptorBuilder().setName("Sine1").setMetadata(metadata).build(); + auto descriptorWrong = DataDescriptorBuilder().setSampleType(SampleType::Float64).setName("Wrong descriptor").build(); + + auto variant = VariantConverter::ToVariant(descriptor); + auto descriptorOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(descriptorOut.equals(descriptor)); + ASSERT_FALSE(descriptorOut.equals(descriptorWrong)); +} + +TEST_F(VariantConverterTest, LinearDimensionRule) +{ + const DimensionRulePtr daqRule = LinearDimensionRule(2.0, 3.0, 4); + const DimensionRulePtr daqRuleWrong = LinearDimensionRule(1.0, 4.0, 9); + + const auto variant = VariantConverter::ToVariant(daqRule); + const auto daqRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqRuleOut.equals(daqRule)); + ASSERT_FALSE(daqRuleOut.equals(daqRuleWrong)); +} + +TEST_F(VariantConverterTest, LogDimensionRule) +{ + const DimensionRulePtr daqRule = LogarithmicDimensionRule(2.0, 3.0, 10, 4); + const DimensionRulePtr daqRuleWrong = LogarithmicDimensionRule(1.0, 4.0, 2, 9); + + const auto variant = VariantConverter::ToVariant(daqRule); + const auto daqRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqRuleOut.equals(daqRule)); + ASSERT_FALSE(daqRuleOut.equals(daqRuleWrong)); +} + +TEST_F(VariantConverterTest, ListDimensionRule) +{ + ListPtr list{10, 20, 30}; + ListPtr listWrong{1, 2, 3}; + const DimensionRulePtr daqRule = ListDimensionRule(list); + const DimensionRulePtr daqRuleWrong = ListDimensionRule(listWrong); + + const auto variant = VariantConverter::ToVariant(daqRule); + const auto daqRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqRuleOut.equals(daqRule)); + ASSERT_FALSE(daqRuleOut.equals(daqRuleWrong)); +} + +TEST_F(VariantConverterTest, CustomDimensionRule) +{ + auto params = Dict(); + params.set("count", 1); + params.set("Type", "apple"); + params.set("type1", "gala"); + params.set("type2", "fuji"); + params.set("weight", 1.123); + + auto daqDimensionRule = DimensionRuleBuilder().setType(DimensionRuleType::Other).setParameters(params).build(); + + const auto variant = VariantConverter::ToVariant(daqDimensionRule); + const auto daqDimensionRuleOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqDimensionRuleOut.equals(daqDimensionRule)); +} + +TEST_F(VariantConverterTest, LinearScaling) +{ + const ScalingPtr daqScaling = LinearScaling(2.0, 3.0, SampleType::UInt8, ScaledSampleType::Float32); + const ScalingPtr daqScalingWrong = LinearScaling(1.0, 4.0, SampleType::Int16, ScaledSampleType::Float64); + + const auto variant = VariantConverter::ToVariant(daqScaling); + auto daqScalingOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(daqScalingOut.equals(daqScaling)); + ASSERT_FALSE(daqScalingOut.equals(daqScalingWrong)); +} + +TEST_F(VariantConverterTest, Dimension) +{ + const auto rule = LinearDimensionRule(2.0, 3.0, 4); + const auto unit = Unit("V", 1, "voltage", "1"); + auto dimension = Dimension(rule, unit, "x"); + + auto dimensionWrong = Dimension(rule, unit, "wrong"); + + const auto variant = VariantConverter::ToVariant(dimension); + auto dimensionOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(dimensionOut.equals(dimension)); + ASSERT_FALSE(dimensionOut.equals(dimensionWrong)); +} + +TEST_F(VariantConverterTest, Ratio) +{ + auto ratio = Ratio(1, 2); + auto ratioWrong = Ratio(1, 10); + + const auto variant = VariantConverter::ToVariant(ratio); + auto ratioOut = VariantConverter::ToDaqObject(variant); + + ASSERT_TRUE(ratioOut.equals(ratio)); + ASSERT_FALSE(ratioOut.equals(ratioWrong)); +} + +TEST_F(VariantConverterTest, FunctionBlockType) +{ + const FunctionBlockTypePtr fbType = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + + const auto variant = VariantConverter::ToVariant(fbType); + const auto fbTypeOut = VariantConverter::ToDaqObject(variant); + + ASSERT_EQ(fbTypeOut.getId(), fbType.getId()); + ASSERT_EQ(fbTypeOut.getName(), fbType.getName()); + ASSERT_EQ(fbTypeOut.getDescription(), fbType.getDescription()); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/test_variant_list_converter.cpp b/shared/libraries/opcuatms/opcuatms/tests/test_variant_list_converter.cpp new file mode 100644 index 0000000..296e492 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/test_variant_list_converter.cpp @@ -0,0 +1,308 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using VariantListConverterTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::tms; +using namespace opcua; + +TEST_F(VariantListConverterTest, Empty) +{ + auto list = List(); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Number) +{ + auto list = List(); + list.pushBack(10); + list.pushBack(-2); + list.pushBack(1.5); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Range) +{ + auto list = List(); + list.pushBack(Range(1, 10)); + list.pushBack(Range(2, 20)); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Ratio) +{ + auto list = List(); + list.pushBack(Ratio(10, 2)); + list.pushBack(Ratio(-2, 5)); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Unit) +{ + auto list = List(); + list.pushBack(Unit("Symbol", 1, "Name", "q")); + list.pushBack(Unit("V", 2)); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Boolean) +{ + auto list = List(); + list.pushBack(true); + list.pushBack(false); + list.pushBack(true); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Integer) +{ + auto list = List(); + list.pushBack(1000); + list.pushBack(-15); + list.pushBack(-22); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Float) +{ + auto list = List(); + list.pushBack(1000); + list.pushBack(3.14); + list.pushBack(-1.5); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, String) +{ + auto list = List(); + list.pushBack("Hello World!"); + list.pushBack(""); + list.pushBack("hakuna matata"); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, DISABLED_MixedBaseObject) +{ + // disabled: only homogeneous lists are allowed for now + + auto list = List(); + list.pushBack(nullptr); + list.pushBack(true); + list.pushBack(3); + list.pushBack(12.5); + list.pushBack("hakuna matata"); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, BaseObject) +{ + auto list = List(); + list.pushBack(1.5); + list.pushBack(2.5); + list.pushBack(3.5); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, DataDescriptor) +{ + auto id = DataDescriptorBuilder().setSampleType(SampleType::Int32).setName("Id").build(); + auto data = DataDescriptorBuilder().setSampleType(SampleType::UInt8).setName("data").build(); + auto can = DataDescriptorBuilder() + .setSampleType(SampleType::Struct) + .setName("CAN message") + .setStructFields(List(id, data)) + .build(); + + auto meta = DataDescriptorBuilder().setSampleType(SampleType::Float64).setName("meta").build(); + + auto list = List(); + list.pushBack(can); + list.pushBack(meta); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, ListDataDescriptor) +{ + auto list = List(); + list.pushBack(DataDescriptorBuilder().setSampleType(SampleType::Float64).build()); + + ASSERT_NO_THROW(VariantConverter::ToArrayVariant(list)); + EXPECT_THROW(VariantConverter::ToDaqList(OpcUaVariant()), ConversionFailedException); +} + +TEST_F(VariantListConverterTest, DataDescriptorMetadata) +{ + auto metadata = Dict(); + metadata.set("Name", "sine1"); + metadata.set("frequency", "50"); + auto descriptor1 = DataDescriptorBuilder().setSampleType(SampleType::Float64).setName("Sine1").setMetadata(metadata).build(); + + auto descriptor2 = DataDescriptorBuilder().setSampleType(SampleType::Float64).setName("Sine2").build(); + + auto list = List(); + list.pushBack(descriptor1); + list.pushBack(descriptor2); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, DataRule) +{ + auto list = List(); + list.pushBack(LinearDataRule(2.0, 3.0)); + list.pushBack(ConstantDataRule()); + list.pushBack(ExplicitDataRule()); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Dimension) +{ + const auto rule = LinearDimensionRule(2.0, 3.0, 4); + const auto unit = Unit("V", 1, "voltage", "1"); + auto dx = Dimension(rule, unit, "x"); + auto dy = Dimension(rule, unit, "y"); + + auto list = List(); + list.pushBack(dx); + list.pushBack(dy); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, Scaling) +{ + auto list = List(); + list.pushBack(LinearScaling(2.0, 3.0, SampleType::UInt8, ScaledSampleType::Float32)); + list.pushBack(LinearScaling(10.0, 2.0, SampleType::Float64, ScaledSampleType::Float64)); + + const auto variant = VariantConverter::ToArrayVariant(list); + const auto listOut = VariantConverter::ToDaqList(variant); + + Bool eq{false}; + listOut->equals(list, &eq); + ASSERT_TRUE(eq); +} + +TEST_F(VariantListConverterTest, DISABLED_NonExtensionObjectTest) +{ + // disabled: only homogeneous lists are allowed for now + + constexpr size_t listSize = 3; + + auto list = List(); + list.pushBack(Integer(10)); + list.pushBack(String("foo")); + list.pushBack(Floating(123.23)); + + const auto type = GetUaDataType(); + auto arr = (UA_Variant*) UA_Array_new(listSize, type); + + for (SizeT i = 0; i < listSize; ++i) + { + arr[i] = VariantConverter::ToArrayVariant(list).getDetachedValue(); + arr[i].arrayLength = list.getCount(); + } + + UA_Array_delete(arr, 3, type); +} diff --git a/shared/libraries/opcuatms/opcuatms/tests/testapp.cpp b/shared/libraries/opcuatms/opcuatms/tests/testapp.cpp new file mode 100644 index 0000000..f267ca1 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms/tests/testapp.cpp @@ -0,0 +1,13 @@ +#include +#include +#include + +int main(int argc, char** args) +{ + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new DaqMemCheckListener()); + + return RUN_ALL_TESTS(); +} diff --git a/shared/libraries/opcuatms/opcuatms_client/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_client/CMakeLists.txt new file mode 100644 index 0000000..190673c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcuatms_client VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_channel_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_channel_factory.h new file mode 100644 index 0000000..e79d7a7 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_channel_factory.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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline ChannelPtr TmsClientChannel( + const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + ChannelPtr obj(createWithImplementation(ctx, parent, localId, clientContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_channel_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_channel_impl.h new file mode 100644 index 0000000..0bb9820 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_channel_impl.h @@ -0,0 +1,33 @@ +/* + * 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_TMS + +class TmsClientChannelImpl : public TmsClientFunctionBlockBaseImpl> +{ + public: + explicit TmsClientChannelImpl( + const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component.h new file mode 100644 index 0000000..4de1231 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component.h @@ -0,0 +1,39 @@ +/* + * 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 + +/*! + * @brief Extends IComponent interface with functionality needed for openDAQ client components. + */ +DECLARE_OPENDAQ_INTERFACE(ITmsClientComponent, IBaseObject) +{ + /*! + * @brief Gets the global ID of the component as defined by openDAQ server. + * @param[out] globalId The global ID of the server component. + * + * OpenDAQ client device can be connected to multiple openDAQ servers. This means that the global ID which was unique on the server, + * is no longer unique on the client device. In that case a unique prefix is added to each client openDAQ device. + * This means that global ID of a client component can be differnet that the one of the same component on the server side. + * This method returs global ID as defined by openDAQ server. + */ + virtual ErrCode INTERFACE_FUNC getRemoteGlobalId(IString** globalId) = 0; +}; + +END_NAMESPACE_OPENDAQ diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_factory.h new file mode 100644 index 0000000..c122440 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_factory.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_TMS + +inline ComponentPtr TmsClientComponent(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + ComponentPtr obj(createWithImplementation(context, parent, localId, clientContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h new file mode 100644 index 0000000..b6e1575 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_component_impl.h @@ -0,0 +1,85 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +template +class TmsClientComponentBaseImpl; + +using TmsClientComponentImpl = TmsClientComponentBaseImpl>; + +template +class TmsClientComponentBaseImpl : public TmsClientPropertyObjectBaseImpl +{ +public: + + template, ChannelImpl> = 0> + TmsClientComponentBaseImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const std::map& propBrowseName = {}) + : TmsClientPropertyObjectBaseImpl(ctx, parent, localId, clientContext, nodeId, propBrowseName) + , remoteComponentId(nodeId.getIdentifier()) + { + initComponent(); + clientContext->readObjectAttributes(nodeId); + } + + template, ChannelImpl> = 0> + TmsClientComponentBaseImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const FunctionBlockTypePtr& type) + : TmsClientPropertyObjectBaseImpl(ctx, parent, localId, clientContext, nodeId, type) + { + initComponent(); + clientContext->readObjectAttributes(nodeId); + } + + // Component overrides + ErrCode INTERFACE_FUNC getActive(Bool* active) override; + ErrCode INTERFACE_FUNC setActive(Bool active) override; + ErrCode INTERFACE_FUNC getName(IString** name) override; + ErrCode INTERFACE_FUNC setName(IString* name) override; + ErrCode INTERFACE_FUNC getDescription(IString** description) override; + ErrCode INTERFACE_FUNC setDescription(IString* description) override; + ErrCode INTERFACE_FUNC getVisible(Bool* visible) override; + ErrCode INTERFACE_FUNC setVisible(Bool visible) override; + + // ITmsClientComponent + ErrCode INTERFACE_FUNC getRemoteGlobalId(IString** globalId) override; + +protected: + bool isChildComponent(const ComponentPtr& component); + PropertyObjectPtr findAndCreateComponentConfig(); + + std::string remoteComponentId; + +private: + LoggerComponentPtr getLoggerComponent(); + void initComponent(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_context.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_context.h new file mode 100644 index 0000000..fb6fe6e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_context.h @@ -0,0 +1,86 @@ +/* + * 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_TMS + +class TmsClientContext; +using TmsClientContextPtr = std::shared_ptr; + +class TmsClientContext +{ +public: + explicit TmsClientContext(const opcua::OpcUaClientPtr& client, const ContextPtr& context); + + const opcua::OpcUaClientPtr& getClient() const; + + void registerRootDevice(const DevicePtr& rootDevice); + DevicePtr getRootDevice(); + void registerObject(const opcua::OpcUaNodeId& nodeId, const BaseObjectPtr& object); + void unregisterObject(const opcua::OpcUaNodeId& nodeId); + BaseObjectPtr getObject(const opcua::OpcUaNodeId& nodeId) const; + opcua::OpcUaNodeId getNodeId(const BaseObjectPtr object) const; + CachedReferenceBrowserPtr getReferenceBrowser(); + AttributeReaderPtr getAttributeReader(); + void readObjectAttributes(const OpcUaNodeId& nodeId, bool forceRead = false); + size_t getMaxNodesPerBrowse(); + size_t getMaxNodesPerRead(); + void addEnumerationTypesToTypeManager(); + + template ::SmartPtr> + Ptr getObject(const opcua::OpcUaNodeId& nodeId) + { + auto obj = this->getObject(nodeId); + if (obj.assigned()) + return obj.asPtrOrNull(); + return Ptr(); + } + + template ::SmartPtr> + opcua::OpcUaNodeId getNodeId(const Ptr object) const + { + return this->getNodeId(object); + } + +protected: + opcua::OpcUaClientPtr client; + ContextPtr context; + LoggerComponentPtr loggerComponent; + CachedReferenceBrowserPtr referenceBrowser; + AttributeReaderPtr attributeReader; + mutable std::mutex mutex; + // Context should not hold objects because of cycling reference + std::unordered_map objects; + size_t maxNodesPerBrowse = 0; + size_t maxNodesPerRead = 0; + WeakRefPtr rootDevice; + bool enumerationTypesAdded = false; + + void initReferenceBrowser(); + void initAttributeReader(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_factory.h new file mode 100644 index 0000000..2c125f1 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_factory.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 +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline DevicePtr TmsClientDevice(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + DevicePtr obj(createWithImplementation(context, parent, localId, clientContext, nodeId, false)); + return obj; +} + +inline DevicePtr TmsClientRootDevice(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + DevicePtr obj(createWithImplementation(context, parent, localId, clientContext, nodeId, true)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h new file mode 100644 index 0000000..8af26ab --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_device_impl.h @@ -0,0 +1,80 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsClientDeviceImpl : public TmsClientComponentBaseImpl> +{ +public: + using Impl = MirroredDeviceBase; + using Super = TmsClientComponentBaseImpl>; + explicit TmsClientDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + bool isRootDevice); + + ErrCode INTERFACE_FUNC getDomain(IDeviceDomain** deviceDomain) override; + ErrCode INTERFACE_FUNC getAvailableOperationModes(IList** availableOpModes) override; + ErrCode INTERFACE_FUNC setOperationMode(OperationModeType modeType) override; + ErrCode INTERFACE_FUNC setOperationModeRecursive(OperationModeType modeType) override; + ErrCode INTERFACE_FUNC getOperationMode(OperationModeType* modeType) override; + +protected: + void findAndCreateSubdevices(); + DevicePtr onAddDevice(const StringPtr& connectionString, const PropertyObjectPtr& config) override; + DictPtr onAddDevices(const DictPtr& connectionArgs, + DictPtr errCodes, + DictPtr errorInfos) override; + void onRemoveDevice(const DevicePtr& device) override; + DeviceInfoPtr onGetInfo() override; + uint64_t onGetTicksSinceOrigin() override; + DictPtr onGetAvailableDeviceTypes() override; + PropertyObjectPtr onCreateDefaultAddDeviceConfig() override; + void findAndCreateFunctionBlocks(); + void findAndCreateSignals(); + void findAndCreateInputsOutputs(); + void findAndCreateCustomComponents(); + void findAndCreateSyncComponent(); + DictPtr onGetAvailableFunctionBlockTypes() override; + FunctionBlockPtr onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) override; + void onRemoveFunctionBlock(const FunctionBlockPtr& functionBlock) override; + ListPtr onGetLogFileInfos() override; + StringPtr onGetLog(const StringPtr& id, Int size, Int offset) override; + + void findAndCreateServerCapabilities(const DeviceInfoPtr& deviceInfo); + + void removed() override; + bool isAddedToLocalComponentTree() override; + StringPtr onGetRemoteId() const override; + +private: + void fetchTimeDomain(); + void fetchTicksSinceOrigin(); + + SizeT ticksSinceOrigin{}; + LoggerPtr logger; + LoggerComponentPtr loggerComponent; + std::unordered_map deviceInfoChangeableFields; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_folder_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_folder_factory.h new file mode 100644 index 0000000..ebb3966 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_folder_factory.h @@ -0,0 +1,34 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once +#include +#include +#include + +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + inline FolderPtr TmsClientFolder(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + FolderPtr obj(createWithImplementation>(context, parent, localId, clientContext, nodeId, false)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_folder_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_folder_impl.h new file mode 100644 index 0000000..50157d1 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_folder_impl.h @@ -0,0 +1,39 @@ +/* + * 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_TMS + +template > +class TmsClientFolderImpl : public TmsClientComponentBaseImpl +{ +public: + explicit TmsClientFolderImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + bool customFolderType); +private: + void findAndCreateFolders(std::map& orderedComponents, std::vector& unorderedComponents); + + LoggerComponentPtr loggerComponent; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_factory.h new file mode 100644 index 0000000..ec4f7c0 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_factory.h @@ -0,0 +1,34 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline FunctionBlockPtr TmsClientFunctionBlock( + const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + FunctionBlockPtr obj(createWithImplementation(ctx, parent, localId, clientContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_impl.h new file mode 100644 index 0000000..2437ea5 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_impl.h @@ -0,0 +1,52 @@ +/* + * 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_TMS + +template +class TmsClientFunctionBlockBaseImpl; + +using TmsClientFunctionBlockImpl = TmsClientFunctionBlockBaseImpl>; + +template +class TmsClientFunctionBlockBaseImpl : public TmsClientComponentBaseImpl +{ +public: + explicit TmsClientFunctionBlockBaseImpl( + const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); + + SignalPtr onGetStatusSignal() override; + +protected: + LoggerComponentPtr loggerComponent; + + void findAndCreateFunctionBlocks(); + void findAndCreateSignals(); + void findAndCreateInputPorts(); + void readFbType(); + CachedReferences getFunctionBlockReferences(); + CachedReferences getOutputSignalReferences(); + CachedReferences getInputPortBlockReferences(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_type_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_type_factory.h new file mode 100644 index 0000000..6aa163a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_type_factory.h @@ -0,0 +1,30 @@ +/* + * 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_TMS + +inline FunctionBlockTypePtr TmsClientFunctionBlockType(const ContextPtr& context, + const TmsClientContextPtr& tmsContext, + const OpcUaNodeId& nodeId) +{ + FunctionBlockTypePtr obj(createWithImplementation(context, tmsContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_type_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_type_impl.h new file mode 100644 index 0000000..41187a2 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_block_type_impl.h @@ -0,0 +1,42 @@ +/* + * 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_TMS + +class TmsClientFunctionBlockTypeImpl final : public TmsClientObjectImpl, public FunctionBlockTypeImpl +{ +public: + explicit TmsClientFunctionBlockTypeImpl(const ContextPtr& context, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); + // IFunctionBlockType + ErrCode INTERFACE_FUNC getId(IString** id) override; + ErrCode INTERFACE_FUNC getName(IString** name) override; + ErrCode INTERFACE_FUNC getDescription(IString** description) override; + ErrCode INTERFACE_FUNC createDefaultConfig(IPropertyObject** defaultConfig) override; + +private: + void readAttributes(); + + FunctionBlockTypePtr type; + PropertyObjectPtr defaultConfig; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_factory.h new file mode 100644 index 0000000..1d4cfdb --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_factory.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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline FunctionPtr TmsClientFunction(const daq::opcua::tms::TmsClientContextPtr& ctx, + const ContextPtr& daqContext, + const opcua::OpcUaNodeId& parentId, + const opcua::OpcUaNodeId& methodId) +{ + FunctionPtr obj(createWithImplementation(ctx, daqContext, parentId, methodId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_impl.h new file mode 100644 index 0000000..fb31f72 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_function_impl.h @@ -0,0 +1,47 @@ +/* + * 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 "tms_client_context.h" +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + + +class TmsClientFunctionImpl : public ImplementationOf +{ +public: + + TmsClientFunctionImpl(const TmsClientContextPtr& ctx, + const ContextPtr& daqContext, + const opcua::OpcUaNodeId& parentId, + const opcua::OpcUaNodeId& methodId); + + ErrCode INTERFACE_FUNC call(IBaseObject* args, IBaseObject** result) override; + ErrCode INTERFACE_FUNC getCoreType(CoreType* coreType) override; + +private: + TmsClientContextPtr ctx; + ContextPtr daqContext; + opcua::OpcUaNodeId parentId; + opcua::OpcUaNodeId methodId; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_input_port_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_input_port_factory.h new file mode 100644 index 0000000..f788c4c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_input_port_factory.h @@ -0,0 +1,33 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline InputPortPtr TmsClientInputPort(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& ctx, + const opcua::OpcUaNodeId& nodeId) +{ + InputPortPtr obj = createWithImplementation(context, parent, localId, ctx, nodeId); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_input_port_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_input_port_impl.h new file mode 100644 index 0000000..88141ef --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_input_port_impl.h @@ -0,0 +1,47 @@ +/* + * 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_TMS + +class TmsClientInputPortImpl : public TmsClientComponentBaseImpl> +{ +public: + explicit TmsClientInputPortImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& tmsCtx, + const opcua::OpcUaNodeId& nodeId); + + ErrCode INTERFACE_FUNC acceptsSignal(ISignal* signal, Bool* accepts) override; + ErrCode INTERFACE_FUNC connect(ISignal* signal) override; + ErrCode INTERFACE_FUNC connectSignalSchedulerNotification(ISignal* signal) override; + ErrCode INTERFACE_FUNC disconnect() override; + ErrCode INTERFACE_FUNC getSignal(ISignal** signal) override; + ErrCode INTERFACE_FUNC getConnection(IConnection** connection) override; + ErrCode INTERFACE_FUNC getRequiresSignal(Bool* value) override; + ErrCode INTERFACE_FUNC setRequiresSignal(Bool value) override; + + StringPtr onGetRemoteId() const override; // fixme move to protected + +protected: + SignalPtr onGetSignal(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_io_folder_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_io_folder_factory.h new file mode 100644 index 0000000..11344f4 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_io_folder_factory.h @@ -0,0 +1,33 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline FolderPtr TmsClientIoFolder(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + FolderPtr obj(createWithImplementation(context, parent, localId, clientContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_io_folder_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_io_folder_impl.h new file mode 100644 index 0000000..2f8a0f2 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_io_folder_impl.h @@ -0,0 +1,38 @@ +/* + * 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_TMS + +class TmsClientIoFolderImpl : public TmsClientFolderImpl> +{ +public: + explicit TmsClientIoFolderImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); + +protected: + LoggerComponentPtr loggerComponent; + void findAndCreateChannels(std::map& orderedComponents, std::vector& unorderedComponents); + void findAndCreateIoFolders(std::map& orderedComponents, std::vector& unorderedComponents); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_object_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_object_impl.h new file mode 100644 index 0000000..592f782 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_object_impl.h @@ -0,0 +1,86 @@ +/* + * 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_TMS + +class TmsClientObjectImpl +{ +public: + using ReferenceMap = std::unordered_map>; + +protected: + explicit TmsClientObjectImpl(const ContextPtr& daqContext, const TmsClientContextPtr& ctx, const opcua::OpcUaNodeId& nodeId); + virtual ~TmsClientObjectImpl(); + + void registerObject(const BaseObjectPtr& obj); + SignalPtr findSignal(const opcua::OpcUaNodeId& nodeId) const; + bool hasReference(const std::string& name); + opcua::OpcUaNodeId getNodeId(const std::string& nodeName); + void writeValue(const std::string& nodeName, const opcua::OpcUaVariant& value); + opcua::OpcUaVariant readValue(const std::string& nodeName); + virtual void subscriptionStatusChangeCallback(UA_StatusChangeNotification* notification); + uint32_t tryReadChildNumberInList(const std::string& nodeName); + uint32_t tryReadChildNumberInList(const opcua::OpcUaNodeId& nodeId); + CachedReferences getChildReferencesOfType(const opcua::OpcUaNodeId& nodeId, const opcua::OpcUaNodeId& typeId); + + opcua::MonitoredItem* monitoredItemsCreateEvent( + const opcua::EventMonitoredItemCreateRequest& item, + const opcua::EventNotificationCallbackType& eventNotificationCallback); + + opcua::MonitoredItem* monitoredItemsCreateDataChange( + const UA_MonitoredItemCreateRequest& item, + const opcua::DataChangeNotificationCallbackType& dataChangeNotificationCallback); + + template ::SmartPtr> + void writeValue(const std::string& nodeName, const CoreTypePtr& value) + { + writeValue(nodeName, VariantConverter::ToVariant(value)); + } + + template ::SmartPtr> + CoreTypePtr readValue(const std::string& nodeName) + { + const auto variant = readValue(nodeName); + return VariantConverter::ToDaqObject(variant); + } + + template ::SmartPtr> + ListPtr readList(const std::string& nodeName) + { + const auto variant = readValue(nodeName); + return VariantConverter::ToDaqList(variant); + } + + TmsClientContextPtr clientContext; + + opcua::OpcUaClientPtr client; + opcua::OpcUaNodeId nodeId; + ContextPtr daqContext; + +private: + opcua::Subscription* getSubscription(); + opcua::Subscription* subscription = nullptr; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_procedure_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_procedure_factory.h new file mode 100644 index 0000000..e315f75 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_procedure_factory.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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +inline ProcedurePtr TmsClientProcedure(const daq::opcua::tms::TmsClientContextPtr& ctx, + const ContextPtr& daqContext, + const opcua::OpcUaNodeId& parentId, + const opcua::OpcUaNodeId& methodId) +{ + ProcedurePtr obj(createWithImplementation(ctx, daqContext, parentId, methodId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_procedure_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_procedure_impl.h new file mode 100644 index 0000000..26d099b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_procedure_impl.h @@ -0,0 +1,45 @@ +/* + * 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 "tms_client_context.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsClientProcedureImpl : public ImplementationOf +{ +public: + TmsClientProcedureImpl(const TmsClientContextPtr& ctx, + const ContextPtr& daqContexts, + const opcua::OpcUaNodeId& parentId, + const opcua::OpcUaNodeId& methodId); + + ErrCode INTERFACE_FUNC dispatch(IBaseObject* args) override; + ErrCode INTERFACE_FUNC getCoreType(CoreType* coreType) override; + +private: + TmsClientContextPtr ctx; + ContextPtr daqContext; + opcua::OpcUaNodeId parentId; + opcua::OpcUaNodeId methodId; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_factory.h new file mode 100644 index 0000000..5c3d64a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_factory.h @@ -0,0 +1,42 @@ +/* + * 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_TMS + +OPENDAQ_DECLARE_CLASS_FACTORY_WITH_INTERFACE(INLINE_FACTORY, + TmsClientProperty, IProperty, + const daq::ContextPtr&, daqContext, + const daq::opcua::tms::TmsClientContextPtr&, ctx, + const opcua::OpcUaNodeId&, nodeId, + const daq::StringPtr&, propertyName) + +OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE(INLINE_FACTORY, + TmsClientProperty, IProperty, + const daq::ContextPtr&, daqContext, + const daq::opcua::tms::TmsClientContextPtr&, client, + const opcua::OpcUaNodeId&, nodeId, + const daq::StringPtr&, propertyName) + +inline PropertyPtr TmsClientProperty(const ContextPtr& daqContext, const TmsClientContextPtr& ctx, const OpcUaNodeId& nodeId, const StringPtr& propertyName = nullptr) +{ + PropertyPtr obj(TmsClientProperty_Create(daqContext, ctx, nodeId, propertyName)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_impl.h new file mode 100644 index 0000000..9d1bd9b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_impl.h @@ -0,0 +1,36 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsClientPropertyImpl : public TmsClientObjectImpl, public PropertyImpl +{ +public: + explicit TmsClientPropertyImpl(const ContextPtr& daqContext, const TmsClientContextPtr& ctx, const opcua::OpcUaNodeId& nodeId, const StringPtr& propertyName = nullptr); + +protected: + LoggerComponentPtr loggerComponent; + + void readBasicInfo(); + void configurePropertyFields(); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_object_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_object_factory.h new file mode 100644 index 0000000..a839c34 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_object_factory.h @@ -0,0 +1,50 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +OPENDAQ_DECLARE_CLASS_FACTORY_WITH_INTERFACE(INLINE_FACTORY, + TmsClientPropertyObject, + IPropertyObject, + const daq::ContextPtr&, + daqContext, + const daq::opcua::tms::TmsClientContextPtr&, + clientContext, + const opcua::OpcUaNodeId&, + nodeId) + +OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE(INLINE_FACTORY, + TmsClientPropertyObject, + IPropertyObject, + const daq::ContextPtr&, + daqContext, + const daq::opcua::tms::TmsClientContextPtr&, + ctx, + const opcua::OpcUaNodeId&, + nodeId) + +inline PropertyObjectPtr TmsClientPropertyObject(const ContextPtr& daqContext, const TmsClientContextPtr& ctx, const OpcUaNodeId& nodeId) +{ + auto obj(TmsClientPropertyObject_Create(daqContext, ctx, nodeId)); + return obj; +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_object_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_object_impl.h new file mode 100644 index 0000000..4d177fe --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_property_object_impl.h @@ -0,0 +1,165 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +namespace template_utils +{ + template + using enable_if_any = std::enable_if_t<(std::is_same_v || ...), int>; + + template + using enable_if_none = std::enable_if_t || ...), int>; +} + +template +class TmsClientPropertyObjectBaseImpl; + +using TmsClientPropertyObjectImpl = TmsClientPropertyObjectBaseImpl; + +template +class TmsClientPropertyObjectBaseImpl : public TmsClientObjectImpl, public Impl +{ +public: + + template = 0> + TmsClientPropertyObjectBaseImpl(const ContextPtr& daqContext, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const std::map& propBrowseName = {}) + : TmsClientObjectImpl(daqContext, clientContext, nodeId) + , Impl() + , propBrowseName(propBrowseName) + { + init(); + } + + template = 0> + TmsClientPropertyObjectBaseImpl(const ContextPtr& daqContext, + const StringPtr& protocolName, + const StringPtr& protocolId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const std::map& propBrowseName = {}) + : TmsClientObjectImpl(daqContext, clientContext, nodeId) + , Impl(protocolId, protocolName, ProtocolType::Streaming) + , propBrowseName(propBrowseName) + { + init(); + } + + template > = 0> + TmsClientPropertyObjectBaseImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const std::map& propBrowseName = {}) + : TmsClientObjectImpl(ctx, clientContext, nodeId) + , Impl(ctx, parent, localId) + , propBrowseName(propBrowseName) + { + init(); + } + + template, ChannelImpl, PropertyObjectImpl, ServerCapabilityConfigImpl, MirroredInputPortBase> = 0> + TmsClientPropertyObjectBaseImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const std::map& propBrowseName = {}) + : TmsClientObjectImpl(ctx, clientContext, nodeId) + , Impl(ctx, parent, localId, nullptr) + , propBrowseName(propBrowseName) + { + init(); + } + + template, ChannelImpl> = 0> + TmsClientPropertyObjectBaseImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + const FunctionBlockTypePtr& type, + const std::map& propBrowseName = {}) + : TmsClientObjectImpl(ctx, clientContext, nodeId) + , Impl(type, ctx, parent, localId, nullptr) + , propBrowseName(propBrowseName) + { + init(); + } + + void init(); + + ErrCode INTERFACE_FUNC setPropertyValue(IString* propertyName, IBaseObject* value) override; + ErrCode INTERFACE_FUNC setProtectedPropertyValue(IString* propertyName, IBaseObject* value) override; + ErrCode INTERFACE_FUNC getPropertyValue(IString* propertyName, IBaseObject** value) override; + ErrCode INTERFACE_FUNC getPropertySelectionValue(IString* propertyName, IBaseObject** value) override; + ErrCode INTERFACE_FUNC clearPropertyValue(IString* propertyName) override; + ErrCode INTERFACE_FUNC clearProtectedPropertyValue(IString* propertyName) override; + ErrCode INTERFACE_FUNC getProperty(IString* propertyName, IProperty** value) override; + ErrCode INTERFACE_FUNC addProperty(IProperty* property) override; + ErrCode INTERFACE_FUNC removeProperty(IString* propertyName) override; + ErrCode INTERFACE_FUNC getOnPropertyValueWrite(IString* propertyName, IEvent** event) override; + ErrCode INTERFACE_FUNC getOnPropertyValueRead(IString* propertyName, IEvent** event) override; + ErrCode INTERFACE_FUNC getOnAnyPropertyValueWrite(IEvent** event) override; + ErrCode INTERFACE_FUNC getOnAnyPropertyValueRead(IEvent** event) override; + ErrCode INTERFACE_FUNC getVisibleProperties(IList** properties) override; + ErrCode INTERFACE_FUNC hasProperty(IString* propertyName, Bool* hasProperty) override; + ErrCode INTERFACE_FUNC getAllProperties(IList** properties) override; + ErrCode INTERFACE_FUNC setPropertyOrder(IList* orderedPropertyNames) override; + ErrCode INTERFACE_FUNC beginUpdate() override; + ErrCode INTERFACE_FUNC endUpdate() override; + +protected: + std::unordered_map introspectionVariableIdMap; + std::unordered_map referenceVariableIdMap; + std::unordered_map objectTypeIdMap; + std::map propBrowseName; + opcua::OpcUaNodeId methodParentNodeId; + LoggerComponentPtr loggerComponent; + + ErrCode setOPCUAPropertyValueInternal(IString* propertyName, IBaseObject* value, bool protectedWrite); + void addProperties(const OpcUaNodeId& parentId, + std::map& orderedProperties, + std::vector& unorderedProperties); + void addMethodProperties(const opcua::OpcUaNodeId& parentNodeId, + std::map& orderedProperties, + std::vector& unorderedProperties, + std::unordered_map& functionPropValues); + PropertyPtr addVariableBlockProperty(const StringPtr& propName, const OpcUaNodeId& propNodeId); + void browseRawProperties(); + bool isIgnoredMethodProperty(const std::string& browseName); + PropertyObjectPtr cloneChildPropertyObject(const PropertyPtr& prop) override; + +private: + bool isBasePropertyObject(const PropertyObjectPtr& propObj); +}; + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_server_capability_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_server_capability_factory.h new file mode 100644 index 0000000..73f9409 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_server_capability_factory.h @@ -0,0 +1,34 @@ +/* + * 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_TMS + +inline ServerCapabilityPtr TmsClientServerCapability(const ContextPtr& daqContext, + const StringPtr& protocolId, + const StringPtr& protocolName, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + ServerCapabilityPtr obj( + createWithImplementation(daqContext, protocolId, protocolName, clientContext, nodeId) + ); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_server_capability_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_server_capability_impl.h new file mode 100644 index 0000000..8655ff4 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_server_capability_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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsClientServerCapabilityImpl : public TmsClientPropertyObjectBaseImpl +{ +public: + explicit TmsClientServerCapabilityImpl(const ContextPtr& daqContext, + const StringPtr& protocolId, + const StringPtr& protocolName, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_signal_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_signal_factory.h new file mode 100644 index 0000000..02f646e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_signal_factory.h @@ -0,0 +1,67 @@ +/* + * 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_TMS + +inline SignalPtr TmsClientSignal( + const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + SignalPtr obj(createWithImplementation(ctx, parent, localId, clientContext, nodeId)); + return obj; +} + +namespace details +{ + inline bool endsWith(std::string const& str, std::string const& suffix) + { + if (str.length() < suffix.length()) + { + return false; + } + return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0; + } +} + +inline SignalPtr FindOrCreateTmsClientSignal(const ContextPtr& ctx, + const ComponentPtr& parent, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + SignalPtr clientSignal = clientContext->getObject(nodeId); + if (!clientSignal.assigned()) + { + auto localId = clientContext->getAttributeReader()->getValue(nodeId, UA_ATTRIBUTEID_BROWSENAME).toString(); + clientSignal = TmsClientSignal(ctx, parent, localId, clientContext, nodeId); + + // TODO current client implementation limitation: The order of populating signals is important. + // The linked signal must be populated after the main signal; otherwise, the signal will have the wrong global ID. + if (!details::endsWith(clientSignal.getGlobalId(), nodeId.getIdentifier())) + ctx.getLogger() + .getOrAddComponent("OpcUaTmsClient") + .logMessage(SourceLocation(), "Wrong global ID of the signal on the client side (TODO)", LogLevel::Warn); + } + + return clientSignal; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_signal_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_signal_impl.h new file mode 100644 index 0000000..0a2f396 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_signal_impl.h @@ -0,0 +1,64 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// TmsClientSignalImpl + +class TmsClientSignalImpl final : public TmsClientComponentBaseImpl> +{ +public: + explicit TmsClientSignalImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); + + ErrCode INTERFACE_FUNC getPublic(Bool* active) override; + ErrCode INTERFACE_FUNC setPublic(Bool active) override; + + ErrCode INTERFACE_FUNC setDescriptor(IDataDescriptor* descriptor) override; + + ErrCode INTERFACE_FUNC setDomainSignal(ISignal* signal) override; + + ErrCode INTERFACE_FUNC getRelatedSignals(IList** signals) override; + ListPtr onGetRelatedSignals(); + ErrCode INTERFACE_FUNC setRelatedSignals(IList* signals) override; + ErrCode INTERFACE_FUNC addRelatedSignal(ISignal* signal) override; + ErrCode INTERFACE_FUNC removeRelatedSignal(ISignal* signal) override; + ErrCode INTERFACE_FUNC clearRelatedSignals() override; + ErrCode INTERFACE_FUNC getLastValue(IBaseObject** value) override; + + StringPtr onGetRemoteId() const override; + Bool onTriggerEvent(const EventPacketPtr& eventPacket) override; + +protected: + SignalPtr onGetDomainSignal() override; + DataDescriptorPtr onGetDescriptor() override; + bool clearDescriptorOnUnsubscribe() override; + + std::atomic isPublic = true; + +private: + std::unique_ptr descriptorNodeId; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_sync_component_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_sync_component_factory.h new file mode 100644 index 0000000..61e3599 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_sync_component_factory.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_TMS + +inline SyncComponentPtr TmsClientSyncComponent(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + ComponentPtr obj(createWithImplementation(context, parent, localId, clientContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS \ No newline at end of file diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_sync_component_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_sync_component_impl.h new file mode 100644 index 0000000..ffc78c3 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_sync_component_impl.h @@ -0,0 +1,59 @@ +/* + * 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_TMS + +class TmsClientSyncComponentImpl : public TmsClientComponentBaseImpl> +{ +public: + + using Impl = GenericSyncComponentImpl; + using Super = TmsClientComponentBaseImpl; + + TmsClientSyncComponentImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) + : Super(ctx, parent, localId, clientContext, nodeId) + { + } + + ErrCode INTERFACE_FUNC getSyncLocked(Bool* synchronizationLocked) override + { + try + { + const auto syncLockNodeId = clientContext->getReferenceBrowser()->getChildNodeId(nodeId, "SynchronizationLocked"); + OpcUaVariant opcUaVariant = client->readValue(*syncLockNodeId); + if (!opcUaVariant.isNull()) + { + BooleanPtr syncLockPtr = VariantConverter::ToDaqObject(opcUaVariant); + return syncLockPtr->getValue(synchronizationLocked); + } + } + catch (...) + { + LOG_W("Failed to get sync locked on OpcUA client sync component \"{}\"", this->globalId); + } + return OPENDAQ_SUCCESS; + } +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_tags_factory.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_tags_factory.h new file mode 100644 index 0000000..61b176d --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_tags_factory.h @@ -0,0 +1,28 @@ +/* + * 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_TMS + +inline TagsPtr TmsClientTags(const ContextPtr& ctx, const TmsClientContextPtr& clientContext, const OpcUaNodeId& nodeId) +{ + TagsPtr obj(createWithImplementation(ctx, clientContext, nodeId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_tags_impl.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_tags_impl.h new file mode 100644 index 0000000..f1dd9ad --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/objects/tms_client_tags_impl.h @@ -0,0 +1,45 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// TmsClientSignalImpl + +class TmsClientTagsImpl final : public TmsClientObjectImpl, public TagsImpl +{ +public: + explicit TmsClientTagsImpl(const ContextPtr& ctx, const TmsClientContextPtr& clientContext, const opcua::OpcUaNodeId& nodeId); + + + ErrCode INTERFACE_FUNC getList(IList** value) override; + ErrCode INTERFACE_FUNC add(IString* name) override; + ErrCode INTERFACE_FUNC replace(IList* tags) override; + ErrCode INTERFACE_FUNC remove(IString* name) override; + ErrCode INTERFACE_FUNC contains(IString* name, Bool* value) override; + ErrCode INTERFACE_FUNC query(IString* query, Bool* value) override; + +private: + void refreshTags(); + LoggerComponentPtr loggerComponent; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/tms_attribute_collector.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/tms_attribute_collector.h new file mode 100644 index 0000000..b99effb --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/tms_attribute_collector.h @@ -0,0 +1,69 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsAttributeCollector +{ +public: + TmsAttributeCollector(const CachedReferenceBrowserPtr& browser); + + tsl::ordered_set collectAttributes(const OpcUaNodeId& nodeId); + +private: + void collectDeviceAttributes(const OpcUaNodeId& nodeId); + void collectFunctionBlockAttributes(const OpcUaNodeId& nodeId); + void collectInputPortAttributes(const OpcUaNodeId& nodeId); + void collectSignalAttributes(const OpcUaNodeId& nodeId); + void collectComponentAttributes(const OpcUaNodeId& nodeId); + void collectPropertyObjectAttributes(const OpcUaNodeId& nodeId); + void collectPropertyAttributes(const OpcUaNodeId& nodeId); + void collectEvaluationPropertyAttributes(const OpcUaNodeId& nodeId); + void collectBaseObjectAttributes(const OpcUaNodeId& nodeId); + void collectMethodAttributes(const OpcUaNodeId& nodeId); + void collectVariableBlockAttributes(const OpcUaNodeId& nodeId); + + void collectIoNode(const OpcUaNodeId& nodeId); + void collectInputPortNode(const OpcUaNodeId& nodeId); + void collectFunctionBlockNode(const OpcUaNodeId& nodeId); + void collectSignalsNode(const OpcUaNodeId& nodeId); + void collectStreamingOptionsNode(const OpcUaNodeId& nodeId); + void collectMethodSetNode(const OpcUaNodeId& nodeId); + + bool isSubtypeOf(const OpcUaNodeId& typeId, const OpcUaNodeId& baseType); + bool typeEquals(const OpcUaNodeId& typeId, const OpcUaNodeId& baseType); + + CachedReferenceBrowserPtr browser; + tsl::ordered_set attributes; + + static const OpcUaNodeId NodeIdBaseObjectType; + static const OpcUaNodeId NodeIdBaseVariableType; + static const OpcUaNodeId NodeIdDeviceType; + static const OpcUaNodeId NodeIdFunctionBlockType; + static const OpcUaNodeId NodeIdComponentType; + static const OpcUaNodeId NodeIdSignalType; + static const OpcUaNodeId NodeIdInputPortType; + static const OpcUaNodeId NodeIdEvaluationVariableType; + static const OpcUaNodeId NodeIdVariableBlockType; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/tms_client.h b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/tms_client.h new file mode 100644 index 0000000..6a3257d --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/include/opcuatms_client/tms_client.h @@ -0,0 +1,52 @@ +/* + * 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 TmsClient final +{ +public: + TmsClient(const ContextPtr& context, + const ComponentPtr& parent, + const std::string& opcUaUrl); + + TmsClient(const ContextPtr& context, + const ComponentPtr& parent, + const OpcUaEndpoint& endpoint); + + daq::DevicePtr connect(); + + +protected: + void getRootDeviceNodeAttributes(OpcUaNodeId& nodeIdOut, std::string& browseNameOut); + void createAndConectClient(); + + tms::TmsClientContextPtr tmsClientContext; + ContextPtr context; + daq::opcua::OpcUaClientPtr client; + OpcUaEndpoint endpoint; + ComponentPtr parent; + LoggerComponentPtr loggerComponent; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms_client/src/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_client/src/CMakeLists.txt new file mode 100644 index 0000000..7ab112a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/CMakeLists.txt @@ -0,0 +1,147 @@ +set(LIB_NAME opcuatms_client) + +set(SRC_Cpp tms_client.cpp + tms_attribute_collector.cpp +) + +set(SRC_PublicHeaders +) + +set(SRC_PrivateHeaders tms_client.h + tms_attribute_collector.h +) + +# objects + +set(OBJECT_SRC_DIR "objects") + +set(SRC_Objects_Headers ${OBJECT_SRC_DIR}/tms_client_object_impl.h + ${OBJECT_SRC_DIR}/tms_client_context.h + + ${OBJECT_SRC_DIR}/tms_client_signal_impl.h + ${OBJECT_SRC_DIR}/tms_client_signal_factory.h + + ${OBJECT_SRC_DIR}/tms_client_function_block_impl.h + ${OBJECT_SRC_DIR}/tms_client_function_block_factory.h + + ${OBJECT_SRC_DIR}/tms_client_function_block_type_impl.h + ${OBJECT_SRC_DIR}/tms_client_function_block_type_factory.h + + ${OBJECT_SRC_DIR}/tms_client_channel_impl.h + ${OBJECT_SRC_DIR}/tms_client_channel_factory.h + + ${OBJECT_SRC_DIR}/tms_client_property_impl.h + ${OBJECT_SRC_DIR}/tms_client_property_factory.h + + ${OBJECT_SRC_DIR}/tms_client_input_port_impl.h + ${OBJECT_SRC_DIR}/tms_client_input_port_factory.h + + ${OBJECT_SRC_DIR}/tms_client_property_object_impl.h + ${OBJECT_SRC_DIR}/tms_client_property_object_factory.h + + ${OBJECT_SRC_DIR}/tms_client_device_impl.h + ${OBJECT_SRC_DIR}/tms_client_device_factory.h + + ${OBJECT_SRC_DIR}/tms_client_folder_impl.h + ${OBJECT_SRC_DIR}/tms_client_folder_factory.h + + ${OBJECT_SRC_DIR}/tms_client_io_folder_impl.h + ${OBJECT_SRC_DIR}/tms_client_io_folder_factory.h + + ${OBJECT_SRC_DIR}/tms_client_component.h + ${OBJECT_SRC_DIR}/tms_client_component_impl.h + ${OBJECT_SRC_DIR}/tms_client_component_factory.h + + ${OBJECT_SRC_DIR}/tms_client_server_capability_impl.h + ${OBJECT_SRC_DIR}/tms_client_server_capability_factory.h + + ${OBJECT_SRC_DIR}/tms_client_function_impl.h + ${OBJECT_SRC_DIR}/tms_client_function_factory.h + + ${OBJECT_SRC_DIR}/tms_client_procedure_impl.h + ${OBJECT_SRC_DIR}/tms_client_procedure_factory.h + + ${OBJECT_SRC_DIR}/tms_client_tags_impl.h + ${OBJECT_SRC_DIR}/tms_client_tags_factory.h + + ${OBJECT_SRC_DIR}/tms_client_sync_component_impl.h + ${OBJECT_SRC_DIR}/tms_client_sync_component_factory.h +) + +set(SRC_Objects ${OBJECT_SRC_DIR}/tms_client_object_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_context.cpp + ${OBJECT_SRC_DIR}/tms_client_signal_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_function_block_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_channel_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_input_port_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_property_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_property_object_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_device_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_folder_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_io_folder_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_component_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_server_capability_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_function_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_procedure_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_tags_impl.cpp + ${OBJECT_SRC_DIR}/tms_client_function_block_type_impl.cpp +) + +set(SRC_PublicHeaders ${SRC_PublicHeaders} ${SRC_Objects_Headers}) +set(SRC_Cpp ${SRC_Cpp} ${SRC_Objects}) + +source_group("objects\\client_object" "${OBJECT_SRC_DIR}/(tms_client_object.*|tms_client_context.*)") +source_group("objects\\signal" "${OBJECT_SRC_DIR}/tms_client_signal.*") +source_group("objects\\function_block" "${OBJECT_SRC_DIR}/tms_client_function_block.*") +source_group("objects\\channel" "${OBJECT_SRC_DIR}/tms_client_channel.*") +source_group("objects\\input_port" "${OBJECT_SRC_DIR}/tms_client_input_port.*") +source_group("objects\\property" "${OBJECT_SRC_DIR}/tms_client_property.*") +source_group("objects\\property_object" "${OBJECT_SRC_DIR}/tms_client_property_object.*") +source_group("objects\\device" "${OBJECT_SRC_DIR}/tms_client_device.*") +source_group("objects\\folder" "${OBJECT_SRC_DIR}/tms_client_folder.*") +source_group("objects\\component" "${OBJECT_SRC_DIR}/tms_client_component.*") +source_group("objects\\io_folder" "${OBJECT_SRC_DIR}/tms_client_io_folder.*") +source_group("objects\\server_capability" "${OBJECT_SRC_DIR}/tms_client_server_capability.*") +source_group("objects\\tags" "${OBJECT_SRC_DIR}/tms_client_tags.*") +source_group("objects\\function" "${OBJECT_SRC_DIR}/(tms_client_function_impl.*|tms_client_function_factory.h|tms_client_procedure.*)") +source_group("objects\\sync_component" "${OBJECT_SRC_DIR}tms_client_sync_component.*") + +# /objects + +prepend_include(${LIB_NAME} SRC_PrivateHeaders) +prepend_include(${LIB_NAME} SRC_PublicHeaders) + +add_library(${LIB_NAME} STATIC ${SRC_Cpp} + ${SRC_PublicHeaders} + ${SRC_PrivateHeaders} +) + +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +if(BUILD_64Bit OR BUILD_ARM) + set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) +else() + set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) +endif() + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +elseif (MINGW AND CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(${LIB_NAME} PRIVATE -Wa,-mbig-obj) +endif() + +target_link_libraries(${LIB_NAME} + PUBLIC + daq::opcuatms + daq::opcuaclient +) + +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/opcuatms/opcuatms_client/src/objects/tms_client_channel_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_channel_impl.cpp new file mode 100644 index 0000000..8c0a40b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_channel_impl.cpp @@ -0,0 +1,19 @@ +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsClientChannelImpl::TmsClientChannelImpl( + const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const daq::opcua::tms::TmsClientContextPtr& clientContext, + const OpcUaNodeId& nodeId +) + : TmsClientFunctionBlockBaseImpl(context, parent, localId, clientContext, nodeId) +{ + +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_component_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_component_impl.cpp new file mode 100644 index 0000000..eb4882c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_component_impl.cpp @@ -0,0 +1,233 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +template +ErrCode TmsClientComponentBaseImpl::getActive(Bool* active) +{ + try + { + *active = this->template readValue("Active"); + } + catch(...) + { + *active = true; + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to get active of component \"{}\". The default value was returned \"true\"", this->globalId); + } + return OPENDAQ_SUCCESS; +} + +template +ErrCode TmsClientComponentBaseImpl::setActive(Bool active) +{ + try + { + this->template writeValue("Active", active); + return OPENDAQ_SUCCESS; + } + catch(...) + { + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to set active of component \"{}\"", this->globalId); + } + return OPENDAQ_IGNORED; +} + +template +void TmsClientComponentBaseImpl::initComponent() +{ + try + { + this->tags = TmsClientTags(this->daqContext, this->clientContext, this->getNodeId("Tags")); + } + catch([[maybe_unused]] const std::exception& e) + { + const auto loggerComponent = getLoggerComponent(); + LOG_D("OpcUA Component {} failed to initialize: {}", this->globalId, e.what()); + } + catch(...) + { + const auto loggerComponent = getLoggerComponent(); + LOG_D("OpcUA Component {} failed to initialize", this->globalId); + } +} + +template +ErrCode TmsClientComponentBaseImpl::getName(IString** name) +{ + OPENDAQ_PARAM_NOT_NULL(name); + + StringPtr nameObj; + try + { + nameObj = this->client->readDisplayName(this->nodeId); + } + catch(...) + { + nameObj = this->localId; + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to get name of component \"{}\". The default value was returned \"{}\" (local id)", this->globalId, nameObj); + } + *name = nameObj.detach(); + return OPENDAQ_SUCCESS; +} + +template +ErrCode TmsClientComponentBaseImpl::setName(IString* name) +{ + OPENDAQ_PARAM_NOT_NULL(name); + + try + { + StringPtr nameObj = name; + this->client->writeDisplayName(this->nodeId, nameObj); + return OPENDAQ_SUCCESS; + } + catch(...) + { + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to set name of component \"{}\"", this->globalId); + } + + return OPENDAQ_IGNORED; +} + +template +ErrCode TmsClientComponentBaseImpl::getDescription(IString** description) +{ + OPENDAQ_PARAM_NOT_NULL(description); + + try + { + StringPtr descObj = this->client->readDescription(this->nodeId); + *description = descObj.detach(); + } + catch(...) + { + *description = StringPtr("").detach(); + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to get description of component \"{}\". The default value was returned \"\"", this->globalId); + } + return OPENDAQ_SUCCESS; +} + +template +ErrCode TmsClientComponentBaseImpl::setDescription(IString* description) +{ + OPENDAQ_PARAM_NOT_NULL(description); + + try + { + StringPtr descriptionObj = description; + this->client->writeDescription(this->nodeId, descriptionObj); + return OPENDAQ_SUCCESS; + } + catch(...) + { + auto loggerComponent = getLoggerComponent(); + LOG_D("Failed to set description of component \"{}\"", this->globalId); + } + + return OPENDAQ_IGNORED; +} + +template +ErrCode TmsClientComponentBaseImpl::getVisible(Bool* visible) +{ + try + { + *visible = this->template readValue("Visible"); + } + catch(...) + { + *visible = true; + const auto loggerComponent = getLoggerComponent(); + LOG_D("OpcUA Component {} failed to fetch \"Visible\" state. The default value was returned \"true\"", this->globalId); + } + + return OPENDAQ_SUCCESS; +} + +template +ErrCode TmsClientComponentBaseImpl::setVisible(Bool visible) +{ + try + { + this->template writeValue("Visible", visible); + return OPENDAQ_SUCCESS; + } + catch (...) + { + const auto loggerComponent = getLoggerComponent(); + LOG_D("OpcUA Component {} failed to set \"Active\" state.", this->globalId); + } + + return OPENDAQ_IGNORED; +} + +template +LoggerComponentPtr TmsClientComponentBaseImpl::getLoggerComponent() +{ + return this->daqContext.getLogger().getOrAddComponent("OpcUaClientComponent"); +} + +template +ErrCode TmsClientComponentBaseImpl::getRemoteGlobalId(IString** globalId) +{ + OPENDAQ_PARAM_NOT_NULL(globalId); + + *globalId = String(this->nodeId.getIdentifier()) .detach(); + return OPENDAQ_SUCCESS; +} + +template +bool TmsClientComponentBaseImpl::isChildComponent(const ComponentPtr& component) +{ + DevicePtr parentDevice = this->clientContext->getRootDevice(); + ComponentPtr currentComponent = component; + + do + { + if (currentComponent == parentDevice) + return true; + + currentComponent = currentComponent.getParent(); + } while (currentComponent.assigned()); + + return false; +} + +template +PropertyObjectPtr TmsClientComponentBaseImpl::findAndCreateComponentConfig() +{ + + std::string referenceName = "ComponentConfig"; + if (!this->hasReference(referenceName)) + return nullptr; + + auto refNodeId = this->getNodeId(referenceName); + return TmsClientPropertyObject(this->daqContext, this->clientContext, refNodeId); +} + +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; +template class TmsClientComponentBaseImpl>; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_context.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_context.cpp new file mode 100644 index 0000000..f0bb2f9 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_context.cpp @@ -0,0 +1,199 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq; +using namespace opcua; + +TmsClientContext::TmsClientContext(const opcua::OpcUaClientPtr& client, const ContextPtr& context) + : client(client) + , context(context) + , loggerComponent(context.getLogger().assigned() ? context.getLogger().getOrAddComponent("TmsClientContext") + : throw ArgumentNullException("Logger must not be null")) +{ + initReferenceBrowser(); + initAttributeReader(); +} + +const opcua::OpcUaClientPtr& TmsClientContext::getClient() const +{ + return client; +} + +void TmsClientContext::registerRootDevice(const DevicePtr& rootDevice) +{ + this->rootDevice = rootDevice; +} + +DevicePtr TmsClientContext::getRootDevice() +{ + return this->rootDevice.getRef(); +} + +void TmsClientContext::registerObject(const OpcUaNodeId& nodeId, const BaseObjectPtr& object) +{ + std::lock_guard guard(mutex); + objects[nodeId] = object.getObject(); +} + +void TmsClientContext::unregisterObject(const OpcUaNodeId& nodeId) +{ + std::lock_guard guard(mutex); + objects.extract(nodeId); +} + +BaseObjectPtr TmsClientContext::getObject(const opcua::OpcUaNodeId& nodeId) const +{ + std::lock_guard guard(mutex); + auto it = objects.find(nodeId); + if (it != objects.end()) + { + IBaseObject* obj = it->second; + return BaseObjectPtr(obj); + } + + return {}; +} +opcua::OpcUaNodeId TmsClientContext::getNodeId(const BaseObjectPtr object) const +{ + for (auto pair : objects) + { + if (object == pair.second) + return pair.first; + } + return opcua::OpcUaNodeId(); +} + +CachedReferenceBrowserPtr TmsClientContext::getReferenceBrowser() +{ + return referenceBrowser; +} + +AttributeReaderPtr TmsClientContext::getAttributeReader() +{ + return attributeReader; +} + +void TmsClientContext::readObjectAttributes(const OpcUaNodeId& nodeId, bool forceRead) +{ + if (!forceRead && attributeReader->hasAnyValue(nodeId)) + return; + + auto collector = TmsAttributeCollector(referenceBrowser); + auto attributes = collector.collectAttributes(nodeId); + + attributeReader->setAttibutes(attributes); + attributeReader->read(); +} + +size_t TmsClientContext::getMaxNodesPerBrowse() +{ + return maxNodesPerBrowse; +} + +size_t TmsClientContext::getMaxNodesPerRead() +{ + return maxNodesPerRead; +} + +void TmsClientContext::initReferenceBrowser() +{ + try + { + const auto maxNodesPerBrowseId = OpcUaNodeId(UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERBROWSE); + maxNodesPerBrowse = client->readValue(maxNodesPerBrowseId).toInteger(); + } + catch (const std::exception& e) + { + LOG_W("Failed to read maxNodesPerBrowse variable: {}", e.what()); + } + + referenceBrowser = std::make_shared(client, maxNodesPerBrowse); +} + +void TmsClientContext::initAttributeReader() +{ + try + { + const auto maxNodesPerReadId = OpcUaNodeId(UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD); + maxNodesPerRead = client->readValue(maxNodesPerReadId).toInteger(); + } + catch (const std::exception& e) + { + LOG_W("Failed to read maxNodesPerRead variable: {}", e.what()); + } + + attributeReader = std::make_shared(client, maxNodesPerRead); +} + +void TmsClientContext::addEnumerationTypesToTypeManager() +{ + if (enumerationTypesAdded) + return; + + if (!context.assigned() || !context.getTypeManager().assigned()) + return; // TypeManager required. Do nothing. + + auto typeManager = context.getTypeManager(); + + const auto DataTypeEnumerationNodeId = OpcUaNodeId(UA_NS0ID_ENUMERATION); + const auto& references = referenceBrowser->browse(DataTypeEnumerationNodeId); + StructPtr enumValuesStruct; + std::vector vecEnumerationsNodeIds; + + for (auto [browseName, ref] : references.byBrowseName) + vecEnumerationsNodeIds.push_back(ref->nodeId.nodeId); + + //Cache NodeIds + referenceBrowser->browseMultiple(vecEnumerationsNodeIds); + + auto listEnumValues = List(); + + for (auto [browseName, ref] : references.byBrowseName) + { + //If type already exists, skip + if(typeManager.hasType(browseName)) + continue; + + const auto& references1 = referenceBrowser->browse(ref->nodeId.nodeId); + for (auto [childBrowseName, ChildRef] : references1.byBrowseName) + { + const auto childNodeValue = client->readValue(ChildRef->nodeId.nodeId); + const auto childNodeObject = VariantConverter::ToDaqObject(childNodeValue, context); + + if (childBrowseName == "EnumStrings") + { + for (auto value : childNodeObject.asPtr()) + listEnumValues.pushBack(value); + } + else if (childBrowseName == "EnumValues") + { + for (const auto& value : childNodeObject.asPtr()) + { + if (enumValuesStruct = value.asPtrOrNull(true); enumValuesStruct.assigned()) + listEnumValues.pushBack(enumValuesStruct.get("DisplayName")); + } + } + } + + auto enumType = EnumerationType(browseName, listEnumValues); + + try + { + typeManager.addType(enumType); + } + catch (...) + { + LOG_I("Failed to add OPC UA type {} to type manager.", enumType.getName()); + } + + listEnumValues.clear(); + } + + enumerationTypesAdded = true; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp new file mode 100644 index 0000000..941de19 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_device_impl.cpp @@ -0,0 +1,825 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS +using namespace daq::opcua; + +namespace detail +{ + static std::unordered_set defaultComponents = {"Sig", "FB", "IO", "ServerCapabilities", "Synchronization"}; + + static std::unordered_map deviceInfoFieldMap = + { + {"AssetId", "assetId"}, + {"ComponentName", "name"}, + {"DeviceClass", "deviceClass"}, + {"DeviceManual", "deviceManual"}, + {"DeviceRevision", "deviceRevision"}, + {"HardwareRevision", "hardwareRevision"}, + {"Manufacturer", "manufacturer"}, + {"ManufacturerUri", "manufacturerUri"}, + {"Model", "model"}, + {"ProductCode", "productCode"}, + {"ProductInstanceUri", "productInstanceUri"}, + {"RevisionCounter", "revisionCounter"}, + {"SerialNumber", "serialNumber"}, + {"SoftwareRevision", "softwareRevision"}, + {"MacAddress", "macAddress"}, + {"ParentMacAddress", "parentMacAddress"}, + {"Platform", "platform"}, + {"Position", "position"}, + {"SystemType", "systemType"}, + {"SystemUUID", "systemUuid"}, + {"OpenDaqPackageVersion", "sdkVersion"}, + {"Location", "location"}, + {"UserName", "userName"}, + }; + + static std::unordered_map> deviceInfoSetterMap = + { + {"AssetId", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setAssetId(v.toString()); }}, + {"ComponentName", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setName(v.toString()); }}, + {"DeviceClass", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setDeviceClass(v.toString()); }}, + {"DeviceManual", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setDeviceManual(v.toString()); }}, + {"DeviceRevision", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setDeviceRevision(v.toString()); }}, + {"HardwareRevision", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setHardwareRevision(v.toString()); }}, + {"Manufacturer", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setManufacturer(v.toString()); }}, + {"ManufacturerUri", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setManufacturerUri(v.toString()); }}, + {"Model", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setModel(v.toString()); }}, + {"ProductCode", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setProductCode(v.toString()); }}, + {"ProductInstanceUri", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setProductInstanceUri(v.toString()); }}, + {"RevisionCounter", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setRevisionCounter(v.toInteger()); }}, + {"SerialNumber", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setSerialNumber(v.toString()); }}, + {"SoftwareRevision", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setSoftwareRevision(v.toString()); }}, + {"MacAddress", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setMacAddress(v.toString()); }}, + {"ParentMacAddress", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setParentMacAddress(v.toString()); }}, + {"Platform", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setPlatform(v.toString()); }}, + {"Position", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setPosition(v.toInteger()); }}, + {"SystemType", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setSystemType(v.toString()); }}, + {"SystemUUID", [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) { info.setSystemUuid(v.toString()); }}, + {"OpenDaqPackageVersion", + [](const DeviceInfoConfigPtr& info, const OpcUaVariant& v) + { info.asPtr().setProtectedPropertyValue("sdkVersion", v.toString()); }}, + }; + } + +TmsClientDeviceImpl::TmsClientDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + bool isRootDevice) + : TmsClientComponentBaseImpl(ctx, + parent, + localId, + clientContext, + nodeId, + {{"UserName", "userName"}, {"Location", "location"}}) + , logger(ctx.getLogger()) + , loggerComponent(this->logger.assigned() + ? this->logger.getOrAddComponent("TmsClientDevice") + : throw ArgumentNullException("Logger must not be null")) +{ + clientContext->readObjectAttributes(nodeId); + + if (isRootDevice) + clientContext->registerRootDevice(thisInterface()); + + findAndCreateSubdevices(); + findAndCreateFunctionBlocks(); + findAndCreateSignals(); + findAndCreateInputsOutputs(); + findAndCreateCustomComponents(); + findAndCreateSyncComponent(); + + // for the root device the client side local config object is used + if (!isRootDevice) + this->componentConfig = this->findAndCreateComponentConfig(); +} + +ErrCode TmsClientDeviceImpl::getDomain(IDeviceDomain** deviceDomain) +{ + if (this->isComponentRemoved) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_COMPONENT_REMOVED); + + fetchTimeDomain(); + return Super::getDomain(deviceDomain); +} + +ErrCode TmsClientDeviceImpl::getAvailableOperationModes(IList** availableOpModes) +{ + OPENDAQ_PARAM_NOT_NULL(availableOpModes); + + if (!this->hasReference("OperationModeOptions")) + { + LOG_D("OperationModes are not supported by the server") + return Super::getAvailableOperationModes(availableOpModes); + } + + const auto nodeId = getNodeId("OperationModeOptions"); + auto opModesNodeStrList = VariantConverter::ToDaqList(client->readValue(nodeId)); + auto convertedOpModes = List(); + for (const auto& opMode : opModesNodeStrList) + convertedOpModes.pushBack(static_cast(OperationModeTypeFromString(opMode))); + + *availableOpModes = convertedOpModes.detach(); + + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientDeviceImpl::setOperationMode(OperationModeType modeType) +{ + if (!this->hasReference("OperationMode")) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOT_SUPPORTED, "OperationModes are not supported by the server"); + + const auto nodeId = getNodeId("OperationMode"); + const auto modeTypeStr = OperationModeTypeToString(modeType); + + const auto variant = VariantConverter::ToVariant(String(modeTypeStr), nullptr, daqContext); + client->writeValue(nodeId, variant); + + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientDeviceImpl::setOperationModeRecursive(OperationModeType modeType) +{ + if (!this->hasReference("OperationMode")) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOT_SUPPORTED, "OperationModes are not supported by the server"); + + const auto nodeId = getNodeId("OperationMode"); + const auto modeTypeStr = "Recursive" + OperationModeTypeToString(modeType); + + const auto variant = VariantConverter::ToVariant(String(modeTypeStr), nullptr, daqContext); + client->writeValue(nodeId, variant); + + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientDeviceImpl::getOperationMode(OperationModeType* modeType) +{ + OPENDAQ_PARAM_NOT_NULL(modeType); + + if (!this->hasReference("OperationMode")) + { + LOG_D("OperationModes are not supported by the server") + return Super::getOperationMode(modeType); + } + + const auto nodeId = getNodeId("OperationMode"); + const auto variant = client->readValue(nodeId); + + *modeType = OperationModeTypeFromString(VariantConverter::ToDaqObject(variant, daqContext)); + + return OPENDAQ_SUCCESS; +} + +void TmsClientDeviceImpl::findAndCreateSubdevices() +{ + std::map orderedDevices; + std::vector unorderedDevices; + + const auto& references = getChildReferencesOfType(nodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQDEVICETYPE)); + + for (const auto& [browseName, ref] : references.byBrowseName) + { + try + { + auto subdeviceNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto clientSubdevice = TmsClientDevice(context, devices, browseName, clientContext, subdeviceNodeId); + + auto numberInList = this->tryReadChildNumberInList(subdeviceNodeId); + if (numberInList != std::numeric_limits::max() && !orderedDevices.count(numberInList)) + orderedDevices.emplace(numberInList, clientSubdevice); + else + unorderedDevices.emplace_back(clientSubdevice); + } + catch(...) + { + LOG_W("Failed to create subdevice \"{}\" in OpcUA client device \"{}\"", browseName, this->globalId); + } + } + + for (const auto& val : orderedDevices) + addSubDevice(val.second); + for (const auto& val : unorderedDevices) + addSubDevice(val); +} + +DevicePtr TmsClientDeviceImpl::onAddDevice(const StringPtr& /*connectionString*/, const PropertyObjectPtr& /*config*/) +{ + throw OpcUaClientCallNotAvailableException(); +} + +DictPtr TmsClientDeviceImpl::onAddDevices(const DictPtr& /*connectionArgs*/, + DictPtr /*errCodes*/, + DictPtr /*errorInfos*/) +{ + throw OpcUaClientCallNotAvailableException(); +} + +void TmsClientDeviceImpl::onRemoveDevice(const DevicePtr& /*device*/) +{ + throw OpcUaClientCallNotAvailableException(); +} + +static bool IsVersionHigher(const std::string &version, int major, int minor) +{ + int majorVersion = 0, minorVersion = 0; + std::stringstream ss(version); + ss >> majorVersion; + ss.ignore(); + ss >> minorVersion; + return majorVersion > major || (majorVersion == major && minorVersion >= minor); +} + +DeviceInfoPtr TmsClientDeviceImpl::onGetInfo() +{ + auto browseFilter = BrowseFilter(); + browseFilter.nodeClass = UA_NODECLASS_VARIABLE; + browseFilter.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + const auto& references = clientContext->getReferenceBrowser()->browseFiltered(nodeId, browseFilter); + + auto reader = AttributeReader(client, clientContext->getMaxNodesPerRead()); + + for (const auto& [browseName, ref] : references.byBrowseName) + { + reader.addAttribute({ref->nodeId.nodeId, UA_ATTRIBUTEID_VALUE}); + reader.addAttribute({ref->nodeId.nodeId, UA_ATTRIBUTEID_ACCESSLEVEL}); + } + reader.read(); + + bool serverSupportsEditableProperties = true; + if (references.byBrowseName.contains("OpenDaqPackageVersion")) + { + const auto refNodeId = OpcUaNodeId(references.byBrowseName.at("OpenDaqPackageVersion")->nodeId.nodeId); + const auto sdkVersion = reader.getValue(refNodeId, UA_ATTRIBUTEID_VALUE).toString(); + serverSupportsEditableProperties = sdkVersion.empty() || IsVersionHigher(sdkVersion, 3, 11); + } + + std::set ignoreProps = {"NumberInList", "Active", "Visible", "Tags"}; + auto changeableProperties = List(); + if (serverSupportsEditableProperties) + { + for (const auto& [browseName, ref] : references.byBrowseName) + { + const auto refNodeId = OpcUaNodeId(ref->nodeId.nodeId); + const auto value = reader.getValue(refNodeId, UA_ATTRIBUTEID_VALUE); + const auto accessLevel = reader.getValue(refNodeId, UA_ATTRIBUTEID_ACCESSLEVEL).toInteger(); + + if (!value.isScalar()) + continue; + if ((accessLevel & UA_ACCESSLEVELMASK_WRITE) == 0) + continue; + if (ignoreProps.count(browseName)) + continue; + + std::string propertyName = browseName; + if (detail::deviceInfoFieldMap.count(propertyName)) + propertyName = detail::deviceInfoFieldMap[propertyName]; + + deviceInfoChangeableFields.emplace(propertyName, refNodeId); + } + for (const auto& [name, _] : deviceInfoChangeableFields) + changeableProperties.pushBack(String(name)); + } + + if (this->objPtr.hasProperty("userName")) + changeableProperties.pushBack("userName"); + if (this->objPtr.hasProperty("location")) + changeableProperties.pushBack("location"); + + auto deviceInfo = DeviceInfoWithChanegableFields(changeableProperties); + deviceInfo.setName(this->client->readDisplayName(this->nodeId)); + + for (const auto& [browseName, ref] : references.byBrowseName) + { + const auto refNodeId = OpcUaNodeId(ref->nodeId.nodeId); + const auto value = reader.getValue(refNodeId, UA_ATTRIBUTEID_VALUE); + + if (!value.isScalar()) + continue; + if (ignoreProps.count(browseName)) + continue; + + try + { + std::string propertyName = browseName; + if (auto it = detail::deviceInfoFieldMap.find(propertyName); it != detail::deviceInfoFieldMap.end()) + propertyName = it->second; + + if (deviceInfo.hasProperty(propertyName)) + { + BaseObjectPtr daqValue; + if (value.isString()) + daqValue = String(value.toString()); + else if (value.isBool()) + daqValue = Bool(value.toBool()); + else if (value.isDouble()) + daqValue = Float(value.toDouble()); + else if (value.isInteger()) + daqValue = Int(value.toInteger()); + + if (daqValue.assigned()) + deviceInfo.asPtr(true).setProtectedPropertyValue(propertyName, daqValue); + } + else + { + PropertyBuilderPtr propertyBuilder; + if (value.isString()) + propertyBuilder = StringPropertyBuilder(propertyName, value.toString()); + else if (value.isBool()) + propertyBuilder = BoolPropertyBuilder(propertyName, value.toBool()); + else if (value.isDouble()) + propertyBuilder = FloatPropertyBuilder(propertyName, value.toDouble()); + else if (value.isInteger()) + propertyBuilder = IntPropertyBuilder(propertyName, value.toInteger()); + + if (propertyBuilder.assigned()) + { + const bool isReadOnly = deviceInfoChangeableFields.count(propertyName) == 0; + deviceInfo.addProperty(propertyBuilder.setReadOnly(isReadOnly).build()); + } + } + } + catch (const std::exception& e) + { + LOG_W("Failed to read device info attribute on OpcUa client device \"{}\": {}", this->globalId, e.what()); + } + } + + deviceInfo.getOnPropertyValueRead("name") += [this](PropertyObjectPtr&, PropertyValueEventArgsPtr& args) + { + args.setValue(this->client->readDisplayName(this->nodeId)); + }; + + for (const auto & entry : deviceInfoChangeableFields) + { + const auto& propName = entry.first; + const auto& nodeId = entry.second; + + try + { + deviceInfo.getOnPropertyValueWrite(propName) += [this, &nodeId](PropertyObjectPtr&, PropertyValueEventArgsPtr& args) + { + const auto variant = VariantConverter::ToVariant(args.getValue(), nullptr, daqContext); + client->writeValue(nodeId, variant); + }; + + deviceInfo.getOnPropertyValueRead(propName) += [this, &nodeId](PropertyObjectPtr&, PropertyValueEventArgsPtr& args) + { + const auto variant = client->readValue(nodeId); + const auto daqValue = VariantConverter::ToDaqObject(variant, daqContext); + args.setValue(daqValue); + }; + } + catch (...) + { + } + } + + findAndCreateServerCapabilities(deviceInfo); + return deviceInfo; +} + +void TmsClientDeviceImpl::fetchTimeDomain() +{ + auto timeDomainNodeId = getNodeId("Domain"); + auto variant = client->readValue(timeDomainNodeId); + + UA_DeviceDomainStructure* deviceDomain; + deviceDomain = (UA_DeviceDomainStructure*) variant.getValue().data; + if (!deviceDomain) + return; + + if (deviceDomain == nullptr) + return; + + auto numerator = deviceDomain->resolution.numerator; + auto denominator = deviceDomain->resolution.denominator; + if (denominator == 0) + denominator = 1; + const auto resolution = Ratio(numerator, denominator); + const auto origin = ConvertToDaqCoreString(deviceDomain->origin); + UnitPtr domainUnit; + if (deviceDomain->unit.unitId > 0) + domainUnit = Unit(ConvertToDaqCoreString(deviceDomain->unit.displayName.text), + deviceDomain->unit.unitId, + ConvertToDaqCoreString(deviceDomain->unit.description.text), + ConvertToDaqCoreString(deviceDomain->unit.quantity)); + else + domainUnit = Unit(""); + + setDeviceDomainNoCoreEvent(DeviceDomain(resolution, origin, domainUnit)); + ticksSinceOrigin = deviceDomain->ticksSinceOrigin; +} + +void TmsClientDeviceImpl::findAndCreateSyncComponent() +{ + this->removeComponentById("Synchronization"); + auto syncComponentNodeId = getNodeId("Synchronization"); + syncComponent = this->addExistingComponent(TmsClientSyncComponent(context, + this->thisPtr(), + "Synchronization", + clientContext, + syncComponentNodeId)); +} + +void TmsClientDeviceImpl::fetchTicksSinceOrigin() +{ + auto timeDomainNodeId = getNodeId("Domain"); + auto variant = client->readValue(timeDomainNodeId); + + UA_DeviceDomainStructure* deviceDomain; + deviceDomain = (UA_DeviceDomainStructure*) variant.getValue().data; + ticksSinceOrigin = deviceDomain->ticksSinceOrigin; +} + +uint64_t TmsClientDeviceImpl::onGetTicksSinceOrigin() +{ + fetchTicksSinceOrigin(); + return ticksSinceOrigin; +} + +DictPtr TmsClientDeviceImpl::onGetAvailableDeviceTypes() +{ + return Dict(); +} + +PropertyObjectPtr TmsClientDeviceImpl::onCreateDefaultAddDeviceConfig() +{ + return PropertyObject(); +} + +void TmsClientDeviceImpl::findAndCreateFunctionBlocks() +{ + std::map orderedFunctionBlocks; + std::vector unorderedFunctionBlocks; + + auto functionBlocksNodeId = getNodeId("FB"); + const auto& references = getChildReferencesOfType(functionBlocksNodeId, OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_FUNCTIONBLOCKTYPE)); + + for (const auto& [browseName, ref] : references.byBrowseName) + { + const auto functionBlockNodeId = OpcUaNodeId(ref->nodeId.nodeId); + + try + { + auto clientFunctionBlock = TmsClientFunctionBlock(context, this->functionBlocks, browseName, clientContext, functionBlockNodeId); + const auto numberInList = this->tryReadChildNumberInList(functionBlockNodeId); + if (numberInList != std::numeric_limits::max() && !orderedFunctionBlocks.count(numberInList)) + orderedFunctionBlocks.emplace(numberInList, clientFunctionBlock); + else + unorderedFunctionBlocks.emplace_back(clientFunctionBlock); + } + catch(...) + { + LOG_W("Failed to create function block \"{}\" to OpcUA client device \"{}\"", browseName, this->globalId); + } + } + + for (const auto& val : orderedFunctionBlocks) + this->addNestedFunctionBlock(val.second); + for (const auto& val : unorderedFunctionBlocks) + this->addNestedFunctionBlock(val); +} + +void TmsClientDeviceImpl::findAndCreateSignals() +{ + std::map orderedSignals; + std::vector unorderedSignals; + + const auto signalsNodeId = getNodeId("Sig"); + const auto& references = getChildReferencesOfType(signalsNodeId, OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_SIGNALTYPE)); + + for (const auto& [signalNodeId, ref] : references.byNodeId) + { + try + { + auto clientSignal = FindOrCreateTmsClientSignal(context, signals, clientContext, signalNodeId); + const auto numberInList = this->tryReadChildNumberInList(signalNodeId); + if (numberInList != std::numeric_limits::max() && !orderedSignals.count(numberInList)) + orderedSignals.emplace(numberInList, clientSignal); + else + unorderedSignals.emplace_back(clientSignal); + } + catch (...) + { + LOG_W("Failed to find signal to OpcUA client device \"{}\"", this->globalId); + } + } + + for (const auto& val : orderedSignals) + this->addSignal(val.second); + for (const auto& val : unorderedSignals) + this->addSignal(val); +} + +void TmsClientDeviceImpl::findAndCreateInputsOutputs() +{ + std::map orderedComponents; + std::vector unorderedComponents; + + this->ioFolder.clear(); + auto inputsOutputsNodeId = getNodeId("IO"); + const auto& channelreferences = getChildReferencesOfType(inputsOutputsNodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_CHANNELTYPE)); + + for (const auto& [browseName, ref] : channelreferences.byBrowseName) + { + try + { + const auto channelNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto tmsClientChannel = TmsClientChannel(context, this->ioFolder, browseName, clientContext, channelNodeId); + + auto numberInList = this->tryReadChildNumberInList(channelNodeId); + if (numberInList != std::numeric_limits::max() && !orderedComponents.count(numberInList)) + orderedComponents.insert(std::pair(numberInList, tmsClientChannel)); + else + unorderedComponents.emplace_back(tmsClientChannel); + } + catch (...) + { + LOG_W("Failed to find channel \"{}\" to OpcUA client device \"{}\"", browseName, this->globalId); + } + } + + const auto& folderReferences = getChildReferencesOfType(inputsOutputsNodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_IOCOMPONENTTYPE)); + + for (const auto& [browseName, ref] : folderReferences.byBrowseName) + { + try + { + const auto folderNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto tmsClientFolder = TmsClientIoFolder(context, this->ioFolder, browseName, clientContext, folderNodeId); + + auto numberInList = this->tryReadChildNumberInList(folderNodeId); + if (numberInList != std::numeric_limits::max()) + orderedComponents.insert(std::pair(numberInList, tmsClientFolder)); + else + unorderedComponents.emplace_back(tmsClientFolder); + } + catch (...) + { + LOG_W("Failed to find io folder \"{}\" to OpcUA client device \"{}\"", browseName, this->globalId); + } + } + + for (const auto& val : orderedComponents) + this->ioFolder.addItem(val.second); + for (const auto& val : unorderedComponents) + this->ioFolder.addItem(val); +} + +void TmsClientDeviceImpl::findAndCreateServerCapabilities(const DeviceInfoPtr& deviceInfo) +{ + std::map orderedCaps; + std::vector unorderedCaps; + + auto serverCapabilitiesNodeId = getNodeId("ServerCapabilities"); + + try + { + const auto& serverCapabilitiesReferences = + getChildReferencesOfType(serverCapabilitiesNodeId, OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_VARIABLEBLOCKTYPE)); + + for (const auto& [browseName, ref] : serverCapabilitiesReferences.byBrowseName) + { + const auto optionNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto clientServerCapability = TmsClientPropertyObject(daqContext, clientContext, optionNodeId); + + auto capabilityCopy = ServerCapability("", "", ProtocolType::Unknown); + for (const auto& prop : clientServerCapability.getAllProperties()) + { + const auto name = prop.getName(); + if (!capabilityCopy.hasProperty(name)) + capabilityCopy.addProperty(prop.asPtr().clone()); + + // AddressInfo is a special case, add it as a child object property of type IAddressInfo + if (name == "AddressInfo") + { + const auto addrInfoId = clientContext->getReferenceBrowser()->getChildNodeId(optionNodeId, "AddressInfo"); + const auto& addrInfoRefs = getChildReferencesOfType(addrInfoId, OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_VARIABLEBLOCKTYPE)); + + for (const auto& [addrInfoBrowseName, addrInfoRef] : addrInfoRefs.byBrowseName) + { + auto clientAddressInfo = TmsClientPropertyObject(daqContext, clientContext, OpcUaNodeId(addrInfoRef->nodeId.nodeId)); + auto addrInfoCopy = AddressInfo(); + for (const auto& addrProp : clientAddressInfo.getAllProperties()) + { + const auto addrName = addrProp.getName(); + if (!addrInfoCopy.hasProperty(addrName)) + addrInfoCopy.addProperty(addrProp.asPtr().clone()); + addrInfoCopy.asPtr().setProtectedPropertyValue(addrName, clientAddressInfo.getPropertyValue(addrName)); + } + + capabilityCopy.addAddressInfo(addrInfoCopy); + } + } + else + { + capabilityCopy.asPtr().setProtectedPropertyValue(name, clientServerCapability.getPropertyValue(name)); + } + } + + auto numberInList = this->tryReadChildNumberInList(optionNodeId); + if (numberInList != std::numeric_limits::max()) + orderedCaps.insert(std::pair(numberInList, capabilityCopy)); + else + unorderedCaps.emplace_back(capabilityCopy); + } + } + catch (const std::exception& e) + { + LOG_W("Failed to find 'ServerCapabilities' OpcUA node on OpcUA client device \"{}\": {}", this->globalId, e.what()); + } + + auto deviceInfoInternal = deviceInfo.asPtr(); + deviceInfoInternal.clearServerStreamingCapabilities(); + for (const auto& [_, val] : orderedCaps) + deviceInfoInternal.addServerCapability(val); + for (const auto& val : unorderedCaps) + deviceInfoInternal.addServerCapability(val); +} + +void TmsClientDeviceImpl::removed() +{ + if (this->clientContext->getRootDevice() == this->thisPtr()) + { + this->client->disconnect(false); + } + + Super::removed(); +} + +bool TmsClientDeviceImpl::isAddedToLocalComponentTree() +{ + return this->clientContext->getRootDevice() == this->thisPtr(); +} + +StringPtr TmsClientDeviceImpl::onGetRemoteId() const +{ + return String(remoteComponentId).detach(); +} + +void TmsClientDeviceImpl::findAndCreateCustomComponents() +{ + std::map orderedComponents; + std::vector unorderedComponents; + + auto componentId = OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQCOMPONENTTYPE); + const auto& folderReferences = getChildReferencesOfType(nodeId, componentId); + + for (const auto& [browseName, ref] : folderReferences.byBrowseName) + { + try + { + const auto folderNodeId = OpcUaNodeId(ref->nodeId.nodeId); + + if (detail::defaultComponents.count(browseName)) + continue; + + const auto& componentReferences = getChildReferencesOfType(folderNodeId, componentId); + + ComponentPtr child; + if (!componentReferences.byNodeId.empty()) + child = TmsClientFolder(context, this->thisPtr(), browseName, clientContext, folderNodeId); + else + child = TmsClientComponent(context, this->thisPtr(), browseName, clientContext, folderNodeId); + + auto numberInList = this->tryReadChildNumberInList(folderNodeId); + if (numberInList != std::numeric_limits::max() && !orderedComponents.count(numberInList)) + orderedComponents.insert(std::pair(numberInList, child)); + else + unorderedComponents.push_back(child); + } + catch (...) + { + LOG_W("Failed to find channel \"{}\" to OpcUA client device \"{}\"", browseName, this->globalId); + } + } + + for (const auto& val : orderedComponents) + this->components.push_back(val.second); + for (const auto& val : unorderedComponents) + this->components.push_back(val); +} + +DictPtr TmsClientDeviceImpl::onGetAvailableFunctionBlockTypes() +{ + auto browser = clientContext->getReferenceBrowser(); + auto types = Dict(); + + const auto fbFolderNodeId = browser->getChildNodeId(nodeId, "FB"); + + if (!browser->hasReference(fbFolderNodeId, "AvailableTypes")) + return types; + + const auto availableTypesId = browser->getChildNodeId(fbFolderNodeId, "AvailableTypes"); + + auto filter = BrowseFilter(); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + filter.referenceTypeId = OpcUaNodeId(UA_NS0ID_HASPROPERTY); + filter.nodeClass = UA_NODECLASS_VARIABLE; + + auto fbTypesReferences = browser->browseFiltered(availableTypesId, filter); + + for (const auto& [refNodeId, ref] : fbTypesReferences.byNodeId) + { + auto tmsFbType = TmsClientFunctionBlockType(daqContext, clientContext, refNodeId); + types.set(tmsFbType.getId(), tmsFbType); + } + + return types; +} + +FunctionBlockPtr TmsClientDeviceImpl::onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) +{ + const auto fbFolderNodeId = getNodeId("FB"); + const auto methodNodeId = clientContext->getReferenceBrowser()->getChildNodeId(fbFolderNodeId, "Add"); + + const auto typeIdVariant = OpcUaVariant(typeId.toStdString().c_str()); + const auto configVariant = PropertyObjectConversionUtils::ToDictVariant(config); + + auto request = OpcUaCallMethodRequest(); + request->objectId = fbFolderNodeId.copyAndGetDetachedValue(); + request->methodId = methodNodeId.copyAndGetDetachedValue(); + request->inputArgumentsSize = 2; + request->inputArguments = (UA_Variant*) UA_Array_new(request->inputArgumentsSize, &UA_TYPES[UA_TYPES_VARIANT]); + + request->inputArguments[0] = typeIdVariant.copyAndGetDetachedValue(); + request->inputArguments[1] = configVariant.copyAndGetDetachedValue(); + + auto response = this->client->callMethod(request); + + if (response->statusCode != UA_STATUSCODE_GOOD) + throw OpcUaException(response->statusCode, "Failed to add function block"); + + assert(response->outputArgumentsSize == 2); + const auto fbNodeId = OpcUaVariant(response->outputArguments[0]).toNodeId(); + const auto localId = OpcUaVariant(response->outputArguments[1]).toString(); + + auto clientFunctionBlock = TmsClientFunctionBlock(context, this->functionBlocks, localId, clientContext, fbNodeId); + addNestedFunctionBlock(clientFunctionBlock); + + return clientFunctionBlock; +} + +void TmsClientDeviceImpl::onRemoveFunctionBlock(const FunctionBlockPtr& functionBlock) +{ + const auto fbFolderNodeId = getNodeId("FB"); + const auto methodNodeId = clientContext->getReferenceBrowser()->getChildNodeId(fbFolderNodeId, "Remove"); + + const auto fbIdVariant = OpcUaVariant(functionBlock.getLocalId().toStdString().c_str()); + + auto request = OpcUaCallMethodRequest(); + request->objectId = fbFolderNodeId.copyAndGetDetachedValue(); + request->methodId = methodNodeId.copyAndGetDetachedValue(); + request->inputArgumentsSize = 1; + request->inputArguments = (UA_Variant*) UA_Array_new(request->inputArgumentsSize, &UA_TYPES[UA_TYPES_VARIANT]); + request->inputArguments[0] = fbIdVariant.copyAndGetDetachedValue(); + + auto response = this->client->callMethod(request); + + if (response->statusCode != UA_STATUSCODE_GOOD) + throw OpcUaException(response->statusCode, "Failed to remove function block"); + + removeNestedFunctionBlock(functionBlock); +} + +ListPtr TmsClientDeviceImpl::onGetLogFileInfos() +{ + throw OpcUaClientCallNotAvailableException("getLogFileInfo is not available for OpcUA client device"); +} + +StringPtr TmsClientDeviceImpl::onGetLog(const StringPtr& id, Int size, Int offset) +{ + throw OpcUaClientCallNotAvailableException("GetLog is not available for OpcUA client device"); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_folder_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_folder_impl.cpp new file mode 100644 index 0000000..30cb476 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_folder_impl.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +template +TmsClientFolderImpl::TmsClientFolderImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId, + bool customFolderType) + : TmsClientComponentBaseImpl(ctx, parent, localId, clientContext, nodeId) + , loggerComponent(this->daqContext.getLogger().assigned() ? this->daqContext.getLogger().getOrAddComponent("OpcUaClientFolder") + : throw ArgumentNullException("Logger must not be null")) +{ + if (!customFolderType) + { + std::map orderedComponents; + std::vector unorderedComponents; + findAndCreateFolders(orderedComponents, unorderedComponents); + auto thisPtr = this->template borrowPtr(); + for (const auto& val : orderedComponents) + thisPtr.addItem(val.second); + for (const auto& val : unorderedComponents) + thisPtr.addItem(val); + } +} + +template +void TmsClientFolderImpl::findAndCreateFolders(std::map& orderedComponents, std::vector& unorderedComponents) +{ + auto componentId = OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQCOMPONENTTYPE); + const auto& folderReferences = this->getChildReferencesOfType(this->nodeId, componentId); + + for (const auto& [browseName, ref] : folderReferences.byBrowseName) + { + try + { + const auto folderNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto thisPtr = this->template borrowPtr(); + + const auto& childComponentsReferences = this->getChildReferencesOfType(folderNodeId, componentId); + + ComponentPtr child; + if (!childComponentsReferences.byNodeId.empty()) + child = TmsClientFolder(this->context, thisPtr, browseName, this->clientContext, folderNodeId); + else + child = TmsClientComponent(this->context, thisPtr, browseName, this->clientContext, folderNodeId); + + auto numberInList = this->tryReadChildNumberInList(folderNodeId); + if (numberInList != std::numeric_limits::max() && !orderedComponents.count(numberInList)) + orderedComponents.insert(std::pair(numberInList, child)); + else + unorderedComponents.push_back(child); + } + catch (...) + { + LOG_W("Failed to find and create folder \"{}\" to OpcUA client folder \"{}\"", browseName, this->globalId); + throw; + } + } +} + +template class TmsClientFolderImpl>; +template class TmsClientFolderImpl>; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_block_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_block_impl.cpp new file mode 100644 index 0000000..a4f568c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_block_impl.cpp @@ -0,0 +1,204 @@ +#include +#include +#include +#include +#include +#include +#include + + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +template +TmsClientFunctionBlockBaseImpl::TmsClientFunctionBlockBaseImpl( + const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const OpcUaNodeId& nodeId +) + : TmsClientComponentBaseImpl(context, parent, localId, clientContext, nodeId, nullptr) + , loggerComponent(this->daqContext.getLogger().assigned() ? this->daqContext.getLogger().getOrAddComponent("OpcUaClientFunctionBlock") + : throw ArgumentNullException("Logger must not be null")) +{ + clientContext->readObjectAttributes(nodeId); + + readFbType(); + findAndCreateFunctionBlocks(); + findAndCreateSignals(); + findAndCreateInputPorts(); + this->componentConfig = this->findAndCreateComponentConfig(); +} + +template +CachedReferences TmsClientFunctionBlockBaseImpl::getFunctionBlockReferences() +{ + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(UA_NS0ID_HASCOMPONENT); + filter.typeDefinition = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_FUNCTIONBLOCKTYPE); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + return this->clientContext->getReferenceBrowser()->browseFiltered(this->nodeId, filter); +} + +template +void TmsClientFunctionBlockBaseImpl::findAndCreateFunctionBlocks() +{ + std::map orderedFunctionBlocks; + std::vector unorderedFunctionBlocks; + + const auto& references = getFunctionBlockReferences(); + for (const auto& [browseName, ref] : references.byBrowseName) + { + const auto functionBlockNodeId = OpcUaNodeId(ref->nodeId.nodeId); + + try + { + // TODO: If there is no access to the nodes within a function blocks an exeption + // is thrown which results that the application stops. However, this block should + // just be ignored. It is not an error at all. + auto clientFunctionBlock = TmsClientFunctionBlock(this->context, this->functionBlocks, browseName, this->clientContext, functionBlockNodeId); + + const auto numberInList = this->tryReadChildNumberInList(functionBlockNodeId); + if (numberInList != std::numeric_limits::max() && !orderedFunctionBlocks.count(numberInList)) + orderedFunctionBlocks.insert(std::pair(numberInList, clientFunctionBlock)); + else + unorderedFunctionBlocks.emplace_back(clientFunctionBlock); + } + catch(...) + { + LOG_W("Failed to create function block \"{}\" to OpcUA client device \"{}\"", browseName, this->globalId); + } + } + + for (const auto& val : orderedFunctionBlocks) + this->addNestedFunctionBlock(val.second); + for (const auto& val : unorderedFunctionBlocks) + this->addNestedFunctionBlock(val); +} + +template +void TmsClientFunctionBlockBaseImpl::findAndCreateSignals() +{ + std::map orderedSignals; + std::vector unorderedSignals; + + const auto& references = getOutputSignalReferences(); + + for (const auto& [signalNodeId, ref] : references.byNodeId) + { + try + { + auto clientSignal = FindOrCreateTmsClientSignal(this->context, this->signals, this->clientContext, signalNodeId); + const auto numberInList = this->tryReadChildNumberInList(signalNodeId); + if (numberInList != std::numeric_limits::max() && !orderedSignals.count(numberInList)) + orderedSignals.insert(std::pair(numberInList, clientSignal)); + else + unorderedSignals.emplace_back(clientSignal); + } + catch (...) + { + LOG_W("Failed to create signal to OpcUA client \"{}\"", this->globalId); + throw; + } + } + + for (const auto& val : orderedSignals) + this->addSignal(val.second); + for (const auto& val : unorderedSignals) + this->addSignal(val); +} + +template +CachedReferences TmsClientFunctionBlockBaseImpl::getOutputSignalReferences() +{ + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASVALUESIGNAL); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + auto signalsNodeId = this->getNodeId("Sig"); + + return this->clientContext->getReferenceBrowser()->browseFiltered(signalsNodeId, filter); +} + +template +CachedReferences TmsClientFunctionBlockBaseImpl::getInputPortBlockReferences() +{ + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASINPUTPORT); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + auto inputPortsNodeId = this->getNodeId("IP"); + + return this->clientContext->getReferenceBrowser()->browseFiltered(inputPortsNodeId, filter); +} + +template +void TmsClientFunctionBlockBaseImpl::findAndCreateInputPorts() +{ + std::map orderedInputPorts; + std::vector unorderedInputPorts; + + const auto& references = getInputPortBlockReferences(); + + for (const auto& [browseName, ref] : references.byBrowseName) + { + try + { + const auto inputPortNodeId = OpcUaNodeId(ref->nodeId.nodeId); + + auto clientInputPort = TmsClientInputPort(this->context, this->inputPorts, browseName, this->clientContext, inputPortNodeId); + + const auto numberInList = this->tryReadChildNumberInList(inputPortNodeId); + if (numberInList != std::numeric_limits::max() && !orderedInputPorts.count(numberInList)) + orderedInputPorts.insert(std::pair(numberInList, clientInputPort)); + else + unorderedInputPorts.emplace_back(clientInputPort); + } + catch(...) + { + LOG_W("Failed to find and create input port \"{}\" to OpcUA client \"{}\"", browseName, this->globalId); + throw; + } + } + + for (const auto& val : orderedInputPorts) + this->addInputPort(val.second); + for (const auto& val : unorderedInputPorts) + this->addInputPort(val); +} + +template +void TmsClientFunctionBlockBaseImpl::readFbType() +{ + auto infoNodeId = this->getNodeId("FunctionBlockInfo"); + auto variant = this->clientContext->getAttributeReader()->getValue(infoNodeId, UA_ATTRIBUTEID_VALUE); + this->type = VariantConverter::ToDaqObject(variant).detach(); +} + +template +SignalPtr TmsClientFunctionBlockBaseImpl::onGetStatusSignal() +{ + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASSTATUSSIGNAL); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + const auto& references = this->clientContext->getReferenceBrowser()->browseFiltered(this->nodeId, filter); + assert(references.byNodeId.size() <= 1); + + if (!references.byNodeId.empty()) + { + auto signalNodeId = references.byNodeId.begin().key(); + return this->findSignal(signalNodeId); + } + + return nullptr; +} + +// To force the compiler to generate the template classes that are used elsewhere +// If this is not done, then you will get linker errors if using it outside the static library +template class TmsClientFunctionBlockBaseImpl>; +template class TmsClientFunctionBlockBaseImpl>; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_block_type_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_block_type_impl.cpp new file mode 100644 index 0000000..a761484 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_block_type_impl.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +TmsClientFunctionBlockTypeImpl::TmsClientFunctionBlockTypeImpl(const ContextPtr& context, + const TmsClientContextPtr& tmsContext, + const opcua::OpcUaNodeId& nodeId) + : TmsClientObjectImpl(context, tmsContext, nodeId) + , FunctionBlockTypeImpl("", "", "", nullptr) +{ + readAttributes(); +} + +ErrCode TmsClientFunctionBlockTypeImpl::getId(IString** id) +{ + OPENDAQ_PARAM_NOT_NULL(id); + + *id = type.getId().detach(); + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientFunctionBlockTypeImpl::getName(IString** name) +{ + OPENDAQ_PARAM_NOT_NULL(name); + + *name = type.getName().detach(); + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientFunctionBlockTypeImpl::getDescription(IString** description) +{ + OPENDAQ_PARAM_NOT_NULL(description); + + *description = type.getDescription().detach(); + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientFunctionBlockTypeImpl::createDefaultConfig(IPropertyObject** defaultConfig) +{ + OPENDAQ_PARAM_NOT_NULL(defaultConfig); + + auto clone = PropertyObjectConversionUtils::ClonePropertyObject(this->defaultConfig); + *defaultConfig = clone.detach(); + return OPENDAQ_SUCCESS; +} + +void TmsClientFunctionBlockTypeImpl::readAttributes() +{ + const auto value = client->readValue(nodeId); + this->type = VariantConverter::ToDaqObject(value); + + const auto defaultConfigId = getNodeId("DefaultConfig"); + this->defaultConfig = TmsClientPropertyObject(daqContext, clientContext, defaultConfigId); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_impl.cpp new file mode 100644 index 0000000..a1a5eb2 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_function_impl.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + using namespace opcua; + +TmsClientFunctionImpl::TmsClientFunctionImpl(const TmsClientContextPtr& ctx, + const ContextPtr& daqContext, + const OpcUaNodeId& parentId, + const OpcUaNodeId& methodId) + : ctx(ctx) + , daqContext(daqContext) + , parentId(parentId) + , methodId(methodId) +{ +} + +ErrCode TmsClientFunctionImpl::call(IBaseObject* args, IBaseObject** result) +{ + StringPtr lastProccessDescription = ""; + ErrCode errCode = daqTry([&]() + { + auto argsPtr = BaseObjectPtr::Borrow(args); + OpcUaCallMethodRequest callRequest; + + if (!argsPtr.assigned()) + { + lastProccessDescription = "Creating call request with no args"; + callRequest = OpcUaCallMethodRequest(methodId, parentId, 0); + } + else if (auto argsList = argsPtr.asPtrOrNull(); argsList.assigned()) + { + lastProccessDescription = "Creating call request with list of arguments"; + OpcUaVariant varArgs = ListConversionUtils::ToVariantTypeArrayVariant(argsList, daqContext); + callRequest = OpcUaCallMethodRequest(methodId, parentId, argsList.getCount(), (UA_Variant*) varArgs->data); + } + else + { + lastProccessDescription = "Creating call request with one arguments"; + OpcUaVariant varArgs = VariantConverter::ToVariant(argsPtr, nullptr, daqContext); + callRequest = OpcUaCallMethodRequest(methodId, parentId, 1, &varArgs.getValue()); + } + + lastProccessDescription = "Calling function"; + OpcUaObject callResult = ctx->getClient()->callMethod(callRequest); + if (OPCUA_STATUSCODE_FAILED(callResult->statusCode) || (callResult->outputArgumentsSize != 1)) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_CALLFAILED); + + lastProccessDescription = "Getting call result"; + *result = VariantConverter::ToDaqObject(OpcUaVariant(callResult->outputArguments[0]), daqContext).detach(); + return OPENDAQ_SUCCESS; + }); + if (OPENDAQ_FAILED(errCode)) + { + daqClearErrorInfo(); + if (this->daqContext.getLogger().assigned()) + { + auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpcUaClientProcedure"); + LOG_W("Failed to call function on OpcUA client. Error in \"{}\"", lastProccessDescription); + } + } + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientFunctionImpl::getCoreType(CoreType* coreType) +{ + OPENDAQ_PARAM_NOT_NULL(coreType); + + *coreType = ctFunc; + return OPENDAQ_SUCCESS; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_input_port_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_input_port_impl.cpp new file mode 100644 index 0000000..4c7c006 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_input_port_impl.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + using namespace opcua; + +TmsClientInputPortImpl::TmsClientInputPortImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& tmsCtx, + const opcua::OpcUaNodeId& nodeId) + : TmsClientComponentBaseImpl(ctx, parent, localId, tmsCtx, nodeId) +{ +} + +ErrCode TmsClientInputPortImpl::getRequiresSignal(Bool* value) +{ + try + { + *value = readValue("RequiresSignal"); + } + catch(...) + { + LOG_W("Failed to get requires signals on OpcUA client input port \"{}\"", this->globalId); + } + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientInputPortImpl::setRequiresSignal(Bool value) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOTIMPLEMENTED); +} + +ErrCode TmsClientInputPortImpl::acceptsSignal(ISignal* signal, Bool* accepts) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); + + //const ErrCode errCode = daqTry([&]() + //{ + // OpcUaNodeId methodId(NAMESPACE_DAQBSP, UA_DAQBSPID_INPUTPORTTYPE_ACCEPTSSIGNAL); + + // auto signalNodeId = clientContext->getNodeId(signal); + // OpcUaVariant inputArg; + // inputArg.setScalar(*signalNodeId); + + // OpcUaCallMethodRequest callRequest(methodId, nodeId, 1, inputArg.get()); + // OpcUaObject result = client->callMethod(callRequest); + // if (OPCUA_STATUSCODE_FAILED(result->statusCode) || (result->outputArgumentsSize != 1)) + // return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_CALLFAILED); + + // *accepts = OpcUaVariant(result->outputArguments[0]).toBool(); + // + // return OPENDAQ_SUCCESS; + // + //}); + //OPENDAQ_RETURN_IF_FAILED(errCode); + //return errCode; +} + +ErrCode TmsClientInputPortImpl::connect(ISignal* signal) +{ + const ErrCode errCode = daqTry([&]() + { + if (!isChildComponent(signal)) + DAQ_THROW_EXCEPTION(NotFoundException); + + const SignalPtr signalPtr = signal; + const auto methodNodeId = getNodeId("Connect"); + + StringPtr remoteId; + signalPtr.asPtr()->getRemoteGlobalId(&remoteId); + const auto signalIdVariant = OpcUaVariant(remoteId.toStdString().c_str()); + + auto request = OpcUaCallMethodRequest(); + request->objectId = nodeId.copyAndGetDetachedValue(); + request->methodId = methodNodeId.copyAndGetDetachedValue(); + request->inputArgumentsSize = 1; + request->inputArguments = (UA_Variant*) UA_Array_new(request->inputArgumentsSize, &UA_TYPES[UA_TYPES_VARIANT]); + request->inputArguments[0] = signalIdVariant.copyAndGetDetachedValue(); + + auto response = client->callMethod(request); + + if (response->statusCode != UA_STATUSCODE_GOOD) + throw OpcUaGeneralException(); + }); + OPENDAQ_RETURN_IF_FAILED(errCode); + return errCode; +} + +ErrCode TmsClientInputPortImpl::connectSignalSchedulerNotification(ISignal* signal) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientInputPortImpl::disconnect() +{ + const ErrCode errCode = daqTry([&]() + { + const auto methodNodeId = getNodeId("Disconnect"); + + auto request = OpcUaCallMethodRequest(); + request->objectId = nodeId.copyAndGetDetachedValue(); + request->methodId = methodNodeId.copyAndGetDetachedValue(); + request->inputArgumentsSize = 0; + + auto response = client->callMethod(request); + + if (response->statusCode != UA_STATUSCODE_GOOD) + throw OpcUaGeneralException(); + }); + OPENDAQ_RETURN_IF_FAILED(errCode); + return errCode; +} + +ErrCode TmsClientInputPortImpl::getSignal(ISignal** signal) +{ + SignalPtr signalPtr; + const ErrCode errCode = wrapHandlerReturn(this, &TmsClientInputPortImpl::onGetSignal, signalPtr); + OPENDAQ_RETURN_IF_FAILED(errCode); + + *signal = signalPtr.detach(); + return errCode; +} + +SignalPtr TmsClientInputPortImpl::onGetSignal() +{ + auto browser = clientContext->getReferenceBrowser(); + + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_CONNECTEDTOSIGNAL); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + browser->invalidate(nodeId); + const auto& references = browser->browseFiltered(nodeId, filter); + assert(references.byNodeId.size() <= 1); + + if (!references.byNodeId.empty()) + { + auto connectedSignalNodeId = references.byNodeId.begin().key(); + return findSignal(connectedSignalNodeId); + } + + return nullptr; +} + +StringPtr TmsClientInputPortImpl::onGetRemoteId() const +{ + return String(remoteComponentId).detach(); +} + +ErrCode TmsClientInputPortImpl::getConnection(IConnection** connection) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOTIMPLEMENTED); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_io_folder_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_io_folder_impl.cpp new file mode 100644 index 0000000..9c74d1e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_io_folder_impl.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +TmsClientIoFolderImpl::TmsClientIoFolderImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) + : TmsClientFolderImpl(ctx, parent, localId, clientContext, nodeId, true) +{ + std::map orderedComponents; + std::vector unorderedComponents; + + findAndCreateIoFolders(orderedComponents, unorderedComponents); + findAndCreateChannels(orderedComponents, unorderedComponents); + + auto thisPtr = this->template borrowPtr(); + for (const auto& val : orderedComponents) + thisPtr.addItem(val.second); + for (const auto& val : unorderedComponents) + thisPtr.addItem(val); +} + +void TmsClientIoFolderImpl::findAndCreateChannels(std::map& orderedComponents, std::vector& unorderedComponents) +{ + const auto& references = getChildReferencesOfType(this->nodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_CHANNELTYPE)); + + for (const auto& [browseName, ref] : references.byBrowseName) + { + try + { + const auto channelNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto thisPtr = this->borrowPtr(); + auto tmsClientChannel = TmsClientChannel(this->context, thisPtr, browseName, this->clientContext, channelNodeId); + + auto numberInList = this->tryReadChildNumberInList(channelNodeId); + if (numberInList != std::numeric_limits::max() && !orderedComponents.count(numberInList)) + orderedComponents.insert(std::pair(numberInList, tmsClientChannel)); + else + unorderedComponents.emplace_back(tmsClientChannel); + } + catch (...) + { + LOG_W("Failed to find and create channel \"{}\" to OpcUA client io folder \"{}\"", browseName, this->globalId); + throw; + } + } +} + +void TmsClientIoFolderImpl::findAndCreateIoFolders(std::map& orderedComponents, std::vector& unorderedComponents) +{ + const auto& folderReferences = getChildReferencesOfType(this->nodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_IOCOMPONENTTYPE)); + + for (const auto& [browseName, ref] : folderReferences.byBrowseName) + { + try + { + const auto folderNodeId = OpcUaNodeId(ref->nodeId.nodeId); + auto thisPtr = this->template borrowPtr(); + auto tmsClientFolder = TmsClientIoFolder(this->context, thisPtr, browseName, this->clientContext, folderNodeId); + + auto numberInList = this->tryReadChildNumberInList(folderNodeId); + if (numberInList != std::numeric_limits::max() && !orderedComponents.count(numberInList)) + orderedComponents.insert(std::pair(numberInList, tmsClientFolder)); + else + unorderedComponents.emplace_back(tmsClientFolder); + } + catch (...) + { + LOG_W("Failed to find and create folder \"{}\" to OpcUA client io folder \"{}\"", browseName, this->globalId); + throw; + } + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS \ No newline at end of file diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_object_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_object_impl.cpp new file mode 100644 index 0000000..6177801 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_object_impl.cpp @@ -0,0 +1,108 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; +using namespace opcua::utils; + +TmsClientObjectImpl::TmsClientObjectImpl(const ContextPtr& daqContext, const TmsClientContextPtr& clientContext, const OpcUaNodeId& nodeId) + : clientContext(clientContext) + , client(clientContext->getClient()) + , nodeId(nodeId) + , daqContext(daqContext) +{ +} + +TmsClientObjectImpl::~TmsClientObjectImpl() +{ + clientContext->unregisterObject(nodeId); +} + +void TmsClientObjectImpl::registerObject(const BaseObjectPtr& obj) +{ + clientContext->registerObject(nodeId, obj); +} + +SignalPtr TmsClientObjectImpl::findSignal(const opcua::OpcUaNodeId& nodeId) const +{ + return clientContext->getObject(nodeId); +} + +bool TmsClientObjectImpl::hasReference(const std::string& name) +{ + return clientContext->getReferenceBrowser()->hasReference(nodeId, name); +} + +OpcUaNodeId TmsClientObjectImpl::getNodeId(const std::string& nodeName) +{ + return clientContext->getReferenceBrowser()->getChildNodeId(nodeId, nodeName); +} + +void TmsClientObjectImpl::writeValue(const std::string& nodeName, const OpcUaVariant& value) +{ + const auto nodeId = getNodeId(nodeName); + client->writeValue(nodeId, value); +} + +OpcUaVariant TmsClientObjectImpl::readValue(const std::string& nodeName) +{ + const auto nodeId = getNodeId(nodeName); + return client->readValue(nodeId); +} + +MonitoredItem* TmsClientObjectImpl::monitoredItemsCreateEvent(const EventMonitoredItemCreateRequest& item, + const EventNotificationCallbackType& eventNotificationCallback) +{ + return getSubscription()->monitoredItemsCreateEvent(UA_TIMESTAMPSTORETURN_BOTH, *item, eventNotificationCallback); +} + +MonitoredItem* TmsClientObjectImpl::monitoredItemsCreateDataChange(const UA_MonitoredItemCreateRequest& item, + const DataChangeNotificationCallbackType& dataChangeNotificationCallback) +{ + return getSubscription()->monitoredItemsCreateDataChange(UA_TIMESTAMPSTORETURN_BOTH, item, dataChangeNotificationCallback); +} + +Subscription* TmsClientObjectImpl::getSubscription() +{ + if (!subscription) + subscription = client->createSubscription(UA_CreateSubscriptionRequest_default(), std::bind(&TmsClientObjectImpl::subscriptionStatusChangeCallback, this, std::placeholders::_3)); + + return subscription; +} + +void TmsClientObjectImpl::subscriptionStatusChangeCallback(UA_StatusChangeNotification* notification) +{ + //TODO report on disconnect +} + +uint32_t TmsClientObjectImpl::tryReadChildNumberInList(const std::string& nodeName) +{ + const auto childId = this->getNodeId(nodeName); + return tryReadChildNumberInList(childId); +} + +uint32_t TmsClientObjectImpl::tryReadChildNumberInList(const opcua::OpcUaNodeId& nodeId) +{ + try + { + const auto numberInListId = clientContext->getReferenceBrowser()->getChildNodeId(nodeId, "NumberInList"); + const auto variant = clientContext->getAttributeReader()->getValue(numberInListId, UA_ATTRIBUTEID_VALUE); + return VariantConverter::ToDaqObject(variant); + } + catch (...) + { + } + + return std::numeric_limits::max(); +} + +CachedReferences TmsClientObjectImpl::getChildReferencesOfType(const opcua::OpcUaNodeId& nodeId, const opcua::OpcUaNodeId& typeId) +{ + BrowseFilter filter; + filter.typeDefinition = typeId; + return clientContext->getReferenceBrowser()->browseFiltered(nodeId, filter); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_procedure_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_procedure_impl.cpp new file mode 100644 index 0000000..5615a46 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_procedure_impl.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsClientProcedureImpl::TmsClientProcedureImpl(const TmsClientContextPtr& ctx, + const ContextPtr& daqContext, + const OpcUaNodeId& parentId, + const OpcUaNodeId& methodId) + : ctx(ctx) + , daqContext(daqContext) + , parentId(parentId) + , methodId(methodId) +{ +} + +ErrCode TmsClientProcedureImpl::dispatch(IBaseObject* args) +{ + StringPtr lastProccessDescription = ""; + ErrCode errCode = daqTry([&]() + { + auto argsPtr = BaseObjectPtr::Borrow(args); + OpcUaCallMethodRequest callRequest; + + if (!argsPtr.assigned()) + { + lastProccessDescription = "Creating call request with no args"; + callRequest = OpcUaCallMethodRequest(methodId, parentId, 0); + } + else if (auto argsList = argsPtr.asPtrOrNull(); argsList.assigned()) + { + lastProccessDescription = "Creating call request with list of arguments"; + OpcUaVariant varArgs = ListConversionUtils::ToVariantTypeArrayVariant(argsList, daqContext); + callRequest = OpcUaCallMethodRequest(methodId, parentId, argsList.getCount(), (UA_Variant*) varArgs->data); + } + else + { + lastProccessDescription = "Creating call request with one arguments"; + OpcUaVariant varArgs = VariantConverter::ToVariant(argsPtr, nullptr, daqContext); + callRequest = OpcUaCallMethodRequest(methodId, parentId, 1, &varArgs.getValue()); + } + + lastProccessDescription = "Calling procedure"; + OpcUaObject callResult = ctx->getClient()->callMethod(callRequest); + if (OPCUA_STATUSCODE_FAILED(callResult->statusCode) || (callResult->outputArgumentsSize != 0)) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_CALLFAILED); + + return OPENDAQ_SUCCESS; + }); + if (OPENDAQ_FAILED(errCode)) + { + daqClearErrorInfo(); + if (this->daqContext.getLogger().assigned()) + { + auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpcUaClientProcudure"); + LOG_W("Failed to call procedure on OpcUA client. Error: \"{}\"", lastProccessDescription); + } + } + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientProcedureImpl::getCoreType(CoreType* coreType) +{ + OPENDAQ_PARAM_NOT_NULL(coreType); + + *coreType = ctProc; + return OPENDAQ_SUCCESS; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_property_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_property_impl.cpp new file mode 100644 index 0000000..bb69199 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_property_impl.cpp @@ -0,0 +1,250 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +namespace details +{ + enum class PropertyField + { + CoercionExpression = 0, + ValidationExpression, + DefaultValue, + IsReadOnly, + IsVisible, + Unit, + MaxValue, + MinValue, + SuggestedValues, + SelectionValues + }; + + static std::unordered_map stringToPropertyFieldEnum{ + {"CoercionExpression", PropertyField::CoercionExpression}, + {"ValidationExpression", PropertyField::ValidationExpression}, + {"DefaultValue", PropertyField::DefaultValue}, + {"IsReadOnly", PropertyField::IsReadOnly}, + {"IsVisible", PropertyField::IsVisible}, + {"Unit", PropertyField::Unit}, + {"MaxValue", PropertyField::MaxValue}, + {"MinValue", PropertyField::MinValue}, + {"SuggestedValues", PropertyField::SuggestedValues}, + {"SelectionValues", PropertyField::SelectionValues}, + }; +} + +TmsClientPropertyImpl::TmsClientPropertyImpl(const ContextPtr& daqContext, const TmsClientContextPtr& ctx, const opcua::OpcUaNodeId& nodeId, const StringPtr& propertyName) + : TmsClientObjectImpl(daqContext, ctx, nodeId) +{ + if (!this->daqContext.getLogger().assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException, "Logger must not be null"); + + this->loggerComponent = this->daqContext.getLogger().getOrAddComponent("TmsClientPropertyImpl"); + this->name = propertyName; + + clientContext->readObjectAttributes(nodeId); + + readBasicInfo(); + configurePropertyFields(); +} + +void TmsClientPropertyImpl::readBasicInfo() +{ + auto reader = clientContext->getAttributeReader(); + if (!this->name.assigned()) + this->name = String(reader->getValue(nodeId, UA_ATTRIBUTEID_DISPLAYNAME).toString()); + this->description = String(reader->getValue(nodeId, UA_ATTRIBUTEID_DESCRIPTION).toString()); + + const auto dataType = reader->getValue(nodeId, UA_ATTRIBUTEID_DATATYPE).toNodeId(); + const auto enumerationTypeId = OpcUaNodeId(0, UA_NS0ID_ENUMERATION); + + if (clientContext->getReferenceBrowser()->isSubtypeOf(dataType, enumerationTypeId)) + { + this->valueType = ctEnumeration; + } + else + { + const auto variant = reader->getValue(nodeId, UA_ATTRIBUTEID_VALUE); + const auto object = VariantConverter::ToDaqObject(variant, daqContext); + this->valueType = object.getCoreType(); + } +} + +void TmsClientPropertyImpl::configurePropertyFields() +{ + const auto evaluationVariableTypeId = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_EVALUATIONVARIABLETYPE); + const auto& references = clientContext->getReferenceBrowser()->browse(nodeId); + const auto reader = clientContext->getAttributeReader(); + + for (auto [browseName, ref] : references.byBrowseName) + { + const auto childNodeId = OpcUaNodeId(ref->nodeId.nodeId); + + if (browseName == "CoercionExpression") + { + const auto eval = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + if (eval.assigned() && eval.getLength() > 0) + this->coercer = Coercer(eval); + } + else if (browseName == "ValidationExpression") + { + const auto eval = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + if (eval.assigned() && eval.getLength() > 0) + this->validator = Validator(eval); + } + else if (clientContext->getReferenceBrowser()->isSubtypeOf(ref->typeDefinition.nodeId, evaluationVariableTypeId)) + { + auto evalId = clientContext->getReferenceBrowser()->getChildNodeId(childNodeId, "EvaluationExpression"); + + StringPtr evalStr = VariantConverter::ToDaqObject(reader->getValue(evalId, UA_ATTRIBUTEID_VALUE)); + if (details::stringToPropertyFieldEnum.count(browseName)) + { + const auto propertyField = details::stringToPropertyFieldEnum[browseName]; + bool strHasValue = evalStr.assigned() && evalStr.getLength() > 0; + if (strHasValue) + { + switch (propertyField) + { + case details::PropertyField::DefaultValue: + + this->defaultValue = EvalValue(evalStr); + break; + + case details::PropertyField::IsReadOnly: + this->readOnly = EvalValue(evalStr).asPtr(); + break; + + case details::PropertyField::IsVisible: + this->visible = EvalValue(evalStr).asPtr(); + break; + + case details::PropertyField::Unit: + this->unit = EvalValue(evalStr).asPtr(); + break; + + case details::PropertyField::MaxValue: + this->maxValue = EvalValue(evalStr).asPtr(); + break; + + case details::PropertyField::MinValue: + this->minValue = EvalValue(evalStr).asPtr(); + break; + + case details::PropertyField::SuggestedValues: + this->suggestedValues = EvalValue(evalStr).asPtr(); + break; + + case details::PropertyField::SelectionValues: + this->selectionValues = EvalValue(evalStr); + break; + case details::PropertyField::CoercionExpression: + case details::PropertyField::ValidationExpression: + break; + } + } + else + { + switch (propertyField) + { + case details::PropertyField::DefaultValue: + { + // ToDo: This is a workarround for devices which are delivering not a default value, + // even if this is a mandatory property in the openDAQ Standard. + // However, the SDK creates too strong a requirement, which cannot be + // met by all the standards or devices to be embraced. + // In this case the actual value from the first connect is set to it. + // But, this creates a weak point: + // SDK stores only values of variables which are != to the device default value. + // The choosen default value could be not the true default value from the device. + // So, all in all we aligned on that in future the SDK will also support properties + // which have not a default value as the device for which the workaround is needed. + // But this is feature request and is covered with + // https://blueberrydaq.atlassian.net/browse/TBBAS-1216. + // But as long as the feature is not implemented this is a valid workarround to get + // devices working which are deliviering not a default value via the opc-ua interface. + // Afterwards, the workaround needs to be rolled back. + + auto value = reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE); + if(value.isNull()) + { + value = reader->getValue(nodeId, UA_ATTRIBUTEID_VALUE); + this->defaultValue = VariantConverter::ToDaqObject(value, daqContext); + LOG_W( + "Failed to read default value of property {} on OpcUa client. Default value is set to the value at connection time.", + this->name); + } + + //Special handling for enumerations as this data type is encoded as Int32 in OPCUA + const auto dataType = reader->getValue(nodeId, UA_ATTRIBUTEID_DATATYPE).toNodeId(); + const auto enumerationTypeId = OpcUaNodeId(0, UA_NS0ID_ENUMERATION); + + if (clientContext->getReferenceBrowser()->isSubtypeOf(dataType, enumerationTypeId)) + { + if (value->type != &UA_TYPES[UA_TYPES_INT32]) + DAQ_THROW_EXCEPTION(ConversionFailedException, "Enumeration node data type is not uint32_t"); + + const auto enumBrowseName = client->readBrowseName(dataType); + const auto enumType = GetUAEnumerationDataTypeByName(enumBrowseName); + OpcUaVariant variant{}; + UA_Variant_setScalarCopy(&variant.getValue(), value->data, enumType); + this->defaultValue = VariantConverter::ToDaqObject(variant, daqContext); + } + else + this->defaultValue = VariantConverter::ToDaqObject(value, daqContext); + + if (this->defaultValue.supportsInterface()) + this->defaultValue.freeze(); + + break; + } + case details::PropertyField::IsReadOnly: + this->readOnly = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + break; + case details::PropertyField::IsVisible: + this->visible = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + break; + case details::PropertyField::Unit: + this->unit = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + break; + case details::PropertyField::MaxValue: + this->maxValue = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + break; + case details::PropertyField::MinValue: + this->minValue = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + break; + case details::PropertyField::SuggestedValues: + { + this->suggestedValues = + VariantConverter::ToDaqList(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE), daqContext); + if (this->suggestedValues.supportsInterface()) + this->suggestedValues.freeze(); + break; + } + case details::PropertyField::SelectionValues: + { + this->selectionValues = + SelectionVariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + if (this->selectionValues.supportsInterface()) + this->selectionValues.freeze(); + break; + } + case details::PropertyField::CoercionExpression: + case details::PropertyField::ValidationExpression: + break; + } + } + } + } + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_property_object_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_property_object_impl.cpp new file mode 100644 index 0000000..6994eb6 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_property_object_impl.cpp @@ -0,0 +1,609 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +namespace detail +{ + std::unordered_set ignoredPropertyNames{"ServerCapabilities", + "OperationMode", + "OperationModeOptions", + "ComponentConfig"}; +} + +template +ErrCode TmsClientPropertyObjectBaseImpl::setOPCUAPropertyValueInternal(IString* propertyName, IBaseObject* value, bool protectedWrite) +{ + if (propertyName == nullptr) + { + LOG_W("Failed to set value for property with nullptr name on OpcUA client property object"); + return OPENDAQ_SUCCESS; + } + auto propertyNamePtr = StringPtr::Borrow(propertyName); + + if (this->isChildProperty(propertyNamePtr)) + { + PropertyPtr prop; + const ErrCode errCode = getProperty(propertyNamePtr, &prop); + OPENDAQ_RETURN_IF_FAILED(errCode); + + if (!prop.assigned()) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOTFOUND, fmt::format(R"(Child property "{}" not found)", propertyNamePtr)); + if (protectedWrite) + return prop.asPtr(true)->setValueProtected(value); + return prop->setValue(value); + } + + std::string lastProcessDescription = ""; + ErrCode errCode = daqTry([&] + { + if (const auto& it = introspectionVariableIdMap.find(propertyNamePtr); it != introspectionVariableIdMap.cend()) + { + PropertyPtr prop; + const ErrCode errCode = getProperty(propertyName, &prop); + OPENDAQ_RETURN_IF_FAILED(errCode); + + if (!protectedWrite && prop.getReadOnly()) + { + lastProcessDescription = "Checking existing property is read-only"; + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_ACCESSDENIED, fmt::format("Property \"{}\" is read-only", propertyNamePtr)); + } + + BaseObjectPtr valuePtr = value; + const auto ct = prop.getValueType(); + const auto valueCt = valuePtr.getCoreType(); + if (ct != valueCt) + valuePtr = valuePtr.convertTo(ct); + + lastProcessDescription = "Writing property value"; + const auto variant = VariantConverter::ToVariant(valuePtr, nullptr, daqContext); + client->writeValue(it->second, variant); + return OPENDAQ_SUCCESS; + } + + if (const auto& it = referenceVariableIdMap.find(propertyNamePtr); it != referenceVariableIdMap.cend()) + { + lastProcessDescription = "Setting property value"; + const auto refProp = this->objPtr.getProperty(propertyName).getReferencedProperty(); + const ErrCode errCode = setPropertyValue(refProp.getName(), value); + OPENDAQ_RETURN_IF_FAILED(errCode, fmt::format("Failed to set value for referenced property \"{}\"", propertyNamePtr)); + return errCode; + } + + if (const auto& it = objectTypeIdMap.find((propertyNamePtr)); it != objectTypeIdMap.cend()) + { + lastProcessDescription = "Object type properties cannot be set over OpcUA"; + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOTIMPLEMENTED, "Object type properties cannot be set over OpcUA"); + } + + lastProcessDescription = "Property not found"; + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOTFOUND, fmt::format("Property \"{}\" not found", propertyNamePtr)); + }); + + if (OPENDAQ_FAILED(errCode)) + LOG_W("Failed to set value for property \"{}\" on OpcUA client property object: {}", propertyNamePtr, lastProcessDescription); + + if (errCode == OPENDAQ_ERR_NOTFOUND || errCode == OPENDAQ_ERR_ACCESSDENIED) + return DAQ_EXTEND_ERROR_INFO(errCode, fmt::format("Failed to set value for property \"{}\" on OpcUA client property object", propertyNamePtr)); + else if (OPENDAQ_FAILED(errCode)) + daqClearErrorInfo(); + return OPENDAQ_SUCCESS; +} + +template +void TmsClientPropertyObjectBaseImpl::init() +{ + if (!this->daqContext.getLogger().assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException, "Logger must not be null"); + + this->loggerComponent = this->daqContext.getLogger().getOrAddComponent("TmsClientPropertyObject"); + clientContext->readObjectAttributes(nodeId); + browseRawProperties(); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::setPropertyValue(IString* propertyName, IBaseObject* value) +{ + return setOPCUAPropertyValueInternal(propertyName, value, false); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::setProtectedPropertyValue(IString* propertyName, IBaseObject* value) +{ + return setOPCUAPropertyValueInternal(propertyName, value, true); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::getPropertyValue(IString* propertyName, IBaseObject** value) +{ + if (propertyName == nullptr) + { + LOG_W("Failed to get value for property with nullptr name on OpcUA client property object"); + return OPENDAQ_SUCCESS; + } + auto propertyNamePtr = StringPtr::Borrow(propertyName); + + if (this->isChildProperty(propertyNamePtr)) + { + PropertyPtr prop; + ErrCode err = getProperty(propertyNamePtr, &prop); + OPENDAQ_RETURN_IF_FAILED(err); + + if (!prop.assigned()) + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_NOTFOUND, fmt::format(R"(Child property "{}" not found)", propertyNamePtr)); + return prop->getValue(value); + } + + ErrCode errCode = daqTry([&] + { + if (const auto& introIt = introspectionVariableIdMap.find(propertyNamePtr); introIt != introspectionVariableIdMap.cend()) + { + const auto variant = client->readValue(introIt->second); + const auto object = VariantConverter::ToDaqObject(variant, daqContext); + const ErrCode errCode = Impl::setProtectedPropertyValue(propertyName, object); + OPENDAQ_RETURN_IF_FAILED(errCode, fmt::format("Failed to get value for introspection property \"{}\"", propertyNamePtr)); + } + else if (referenceVariableIdMap.count(propertyNamePtr)) + { + const auto refProp = this->objPtr.getProperty(propertyName).getReferencedProperty(); + const ErrCode errCode = getPropertyValue(refProp.getName(), value); + OPENDAQ_RETURN_IF_FAILED(errCode, fmt::format("Failed to get value for referenced property \"{}\"", propertyNamePtr)); + return errCode; + } + return Impl::getPropertyValue(propertyName, value); + }); + if (OPENDAQ_FAILED(errCode)) + { + daqClearErrorInfo(); + LOG_W("Failed to get value for property \"{}\" on OpcUA client property object", propertyNamePtr); + } + return OPENDAQ_SUCCESS; +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::getPropertySelectionValue(IString* propertyName, IBaseObject** value) +{ + BaseObjectPtr object; + TmsClientPropertyObjectBaseImpl::getPropertyValue(propertyName, &object); + return Impl::getPropertySelectionValue(propertyName, value); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::clearPropertyValue(IString* propertyName) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_INVALID_OPERATION); +} + +template +ErrCode TmsClientPropertyObjectBaseImpl::clearProtectedPropertyValue(IString* propertyName) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_INVALID_OPERATION); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::getProperty(IString* propertyName, IProperty** value) +{ + return Impl::getProperty(propertyName, value); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::addProperty(IProperty* property) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_INVALID_OPERATION); +} + +template +ErrCode TmsClientPropertyObjectBaseImpl::removeProperty(IString* propertyName) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_INVALID_OPERATION); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::getOnPropertyValueWrite(IString* /*propertyName*/, IEvent** /*event*/) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +template +ErrCode TmsClientPropertyObjectBaseImpl::getOnPropertyValueRead(IString* /*propertyName*/, IEvent** /*event*/) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +template +ErrCode TmsClientPropertyObjectBaseImpl::getOnAnyPropertyValueWrite(IEvent** /*event*/) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +template +ErrCode TmsClientPropertyObjectBaseImpl::getOnAnyPropertyValueRead(IEvent** /*event*/) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::getVisibleProperties(IList** properties) +{ + return Impl::getVisibleProperties(properties); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::hasProperty(IString* propertyName, Bool* hasProperty) +{ + return Impl::hasProperty(propertyName, hasProperty); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::getAllProperties(IList** properties) +{ + return Impl::getAllProperties(properties); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::setPropertyOrder(IList* orderedPropertyNames) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_INVALID_OPERATION); +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::beginUpdate() +{ + if (!hasReference("BeginUpdate")) + return OPENDAQ_SUCCESS; + + const auto beginUpdateId = getNodeId("BeginUpdate"); + OpcUaCallMethodRequest request; + request->inputArgumentsSize = 0; + request->objectId = nodeId.copyAndGetDetachedValue(); + request->methodId = beginUpdateId.copyAndGetDetachedValue(); + client->callMethod(request); + return OPENDAQ_SUCCESS; +} + +template +ErrCode INTERFACE_FUNC TmsClientPropertyObjectBaseImpl::endUpdate() +{ + if (!hasReference("EndUpdate")) + return OPENDAQ_SUCCESS; + + const auto endUpdateId = getNodeId("EndUpdate"); + OpcUaCallMethodRequest request; + request->inputArgumentsSize = 0; + request->objectId = nodeId.copyAndGetDetachedValue(); + request->methodId = endUpdateId.copyAndGetDetachedValue(); + client->callMethod(request); + return OPENDAQ_SUCCESS; +} + +template +void TmsClientPropertyObjectBaseImpl::addProperties(const OpcUaNodeId& parentId, + std::map& orderedProperties, + std::vector& unorderedProperties) +{ + const auto introspectionVariableTypeId = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_INTROSPECTIONVARIABLETYPE); + const auto structureVariableTypeId = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_STRUCTUREVARIABLETYPE); + const auto referenceVariableTypeId = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_REFERENCEVARIABLETYPE); + const auto variableBlockTypeId = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_VARIABLEBLOCKTYPE); + + auto reader = clientContext->getAttributeReader(); + const auto& references = clientContext->getReferenceBrowser()->browse(parentId); + + for (auto& [childNodeId, ref] : references.byNodeId) + { + const auto typeId = OpcUaNodeId(ref->typeDefinition.nodeId); + auto propName = String(utils::ToStdString(ref->browseName.name)); + if (propBrowseName.count(propName)) + propName = propBrowseName[propName]; + if (detail::ignoredPropertyNames.count(propName)) + continue; + + Bool hasProp; + daq::checkErrorInfo(Impl::hasProperty(propName, &hasProp)); + PropertyPtr prop; + if (clientContext->getReferenceBrowser()->isSubtypeOf(typeId, referenceVariableTypeId)) + { + try + { + if (!hasProp) + { + StringPtr refPropEval = VariantConverter::ToDaqObject(reader->getValue(childNodeId, UA_ATTRIBUTEID_VALUE)); + prop = ReferenceProperty(propName, EvalValue(refPropEval)); + } + + referenceVariableIdMap.emplace(propName, childNodeId); + addProperties(childNodeId, orderedProperties, unorderedProperties); + } + catch(const std::exception& e) + { + LOG_W("Failed to add {} reference property on OpcUa client property object: {}", propName, e.what()); + continue; + } + } + else if (clientContext->getReferenceBrowser()->isSubtypeOf(typeId, introspectionVariableTypeId) || + clientContext->getReferenceBrowser()->isSubtypeOf(typeId, structureVariableTypeId)) + { + try + { + if (!hasProp) + prop = TmsClientProperty(daqContext, clientContext, ref->nodeId.nodeId, propName); + + introspectionVariableIdMap.emplace(propName, childNodeId); + } + catch(const std::exception& e) + { + LOG_W("Failed to add {} property on OpcUa client property object: {}", propName, e.what()); + continue; + } + } + else if (clientContext->getReferenceBrowser()->isSubtypeOf(typeId, variableBlockTypeId)) + { + try + { + if (!hasProp) + { + prop = addVariableBlockProperty(propName, childNodeId); + } + else if (this->objPtr.template supportsInterface()) + { + Impl::removeProperty(propName); + prop = addVariableBlockProperty(propName, childNodeId); + } + + objectTypeIdMap.emplace(propName, childNodeId); + } + catch (const std::exception& e) + { + LOG_W("Failed to add {} property on OpcUa client property object: {}", propName, e.what()); + continue; + } + } + + if (prop.assigned()) + { + auto numberInList = tryReadChildNumberInList(childNodeId); + if (numberInList != std::numeric_limits::max() && !orderedProperties.count(numberInList)) + orderedProperties.emplace(numberInList, prop); + else + unorderedProperties.push_back(prop); + } + } +} + +template +void TmsClientPropertyObjectBaseImpl::addMethodProperties(const OpcUaNodeId& parentNodeId, + std::map& orderedProperties, + std::vector& unorderedProperties, + std::unordered_map& functionPropValues) +{ + auto browser = clientContext->getReferenceBrowser(); + auto reader = clientContext->getAttributeReader(); + + const auto& references = browser->browse(parentNodeId); + const auto methodTypeId = OpcUaNodeId(0, UA_NS0ID_METHODNODE); + + for (auto& [childNodeId, ref] : references.byNodeId) + { + const auto typeId = OpcUaNodeId(ref->typeDefinition.nodeId); + const auto propName = String(utils::ToStdString(ref->browseName.name)); + + if (isIgnoredMethodProperty(propName)) + continue; + + Bool hasProp; + daq::checkErrorInfo(Impl::hasProperty(propName, &hasProp)); + + if (ref->nodeClass == UA_NODECLASS_METHOD) + { + if (!hasProp) + { + ListPtr inputArgs; + ListPtr outputArgs; + uint32_t numberInList = std::numeric_limits::max(); + + try + { + if (browser->hasReference(childNodeId, "InputArguments")) + { + const auto inputArgsId = browser->getChildNodeId(childNodeId, "InputArguments"); + inputArgs = VariantConverter::ToDaqList(reader->getValue(inputArgsId, UA_ATTRIBUTEID_VALUE)); + } + + if (browser->hasReference(childNodeId, "OutputArguments")) + { + const auto outputArgsId = browser->getChildNodeId(childNodeId, "OutputArguments"); + outputArgs = VariantConverter::ToDaqList(reader->getValue(outputArgsId, UA_ATTRIBUTEID_VALUE)); + } + + if (browser->hasReference(childNodeId, "NumberInList")) + { + const auto numberInListId = browser->getChildNodeId(childNodeId, "NumberInList"); + numberInList = VariantConverter::ToDaqObject(reader->getValue(numberInListId, UA_ATTRIBUTEID_VALUE)); + } + } + catch(const std::exception& e) + { + LOG_W("Failed to parse method properties on OpcUa client property object: {}", e.what()); + continue; + } + + BaseObjectPtr prop; + BaseObjectPtr func; + if (outputArgs.assigned() && outputArgs.getCount() == 1) + { + auto callableInfo = FunctionInfo(outputArgs[0].getType(), inputArgs); + prop = FunctionPropertyBuilder(propName, callableInfo).setReadOnly(true).build(); + func = TmsClientFunction(clientContext, daqContext, parentNodeId, childNodeId); + } + else + { + auto callableInfo = ProcedureInfo(inputArgs); + prop = FunctionPropertyBuilder(propName, callableInfo).setReadOnly(true).build(); + func = TmsClientProcedure(clientContext, daqContext, parentNodeId, childNodeId); + } + + functionPropValues.emplace(propName, func); + if (numberInList != std::numeric_limits::max() && !orderedProperties.count(numberInList)) + orderedProperties.emplace(numberInList, prop); + else + unorderedProperties.push_back(prop); + } + } + } +} + +template +PropertyPtr TmsClientPropertyObjectBaseImpl::addVariableBlockProperty(const StringPtr& propName, const OpcUaNodeId& propNodeId) +{ + auto reader = this->clientContext->getAttributeReader(); + + auto obj = TmsClientPropertyObject(daqContext, clientContext, propNodeId); + const auto description = reader->getValue(propNodeId, UA_ATTRIBUTEID_DESCRIPTION).toString(); + auto propBuilder = ObjectPropertyBuilder(propName, obj).setDescription(String(description)); + + const auto evaluationVariableTypeId = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_EVALUATIONVARIABLETYPE); + const auto variableBlockRefs = clientContext->getReferenceBrowser()->browse(propNodeId); + + for (auto& [browseName, variableBlockRef] : variableBlockRefs.byBrowseName) + { + const auto variableBlockNodeId = OpcUaNodeId(variableBlockRef->nodeId.nodeId); + + if (clientContext->getReferenceBrowser()->isSubtypeOf(variableBlockRef->typeDefinition.nodeId, evaluationVariableTypeId)) + { + auto evalId = clientContext->getReferenceBrowser()->getChildNodeId(variableBlockNodeId, "EvaluationExpression"); + + StringPtr evalStr = VariantConverter::ToDaqObject(reader->getValue(evalId, UA_ATTRIBUTEID_VALUE)); + + if (browseName == "IsReadOnly") + { + if (evalStr.assigned()) + propBuilder.setReadOnly(EvalValue(evalStr).asPtr()); + else + propBuilder.setReadOnly( + VariantConverter::ToDaqObject(reader->getValue(variableBlockNodeId, UA_ATTRIBUTEID_VALUE))); + } + else if (browseName == "IsVisible") + { + if (evalStr.assigned()) + propBuilder.setVisible(EvalValue(evalStr).asPtr()); + else + propBuilder.setVisible( + VariantConverter::ToDaqObject(reader->getValue(variableBlockNodeId, UA_ATTRIBUTEID_VALUE))); + } + } + } + + return propBuilder.build(); +} + +template +void TmsClientPropertyObjectBaseImpl::browseRawProperties() +{ + std::map orderedProperties; + std::vector unorderedProperties; + std::unordered_map functionPropValues; + + addProperties(nodeId, orderedProperties, unorderedProperties); + + // TODO: Make sure that this is a DeviceType node + if (hasReference("MethodSet")) + { + const auto methodNodeId = clientContext->getReferenceBrowser()->getChildNodeId(nodeId, "MethodSet"); + addMethodProperties(methodNodeId, orderedProperties, unorderedProperties, functionPropValues); + } + else + { + addMethodProperties(nodeId, orderedProperties, unorderedProperties, functionPropValues); + } + + auto addPropertyIgnoreDuplicates = [this](const daq::PropertyPtr& prop) + { + auto ec = Impl::addProperty(prop); + if (ec != OPENDAQ_ERR_ALREADYEXISTS) + return ec; + LOG_W("OPC UA exposes two properties with the same name \"{}\". The duplicate property will be ignored.", prop.getName()) + return OPENDAQ_SUCCESS; + }; + + for (const auto& val : orderedProperties) + daq::checkErrorInfo(addPropertyIgnoreDuplicates(val.second)); + for (const auto& val : unorderedProperties) + daq::checkErrorInfo(addPropertyIgnoreDuplicates(val)); + for (const auto& val : functionPropValues) + daq::checkErrorInfo(Impl::setProtectedPropertyValue(String(val.first), val.second)); + +} + +template +bool TmsClientPropertyObjectBaseImpl::isIgnoredMethodProperty(const std::string& browseName) +{ + return browseName == "BeginUpdate" || browseName == "EndUpdate" || browseName == "GetErrorInformation"; +} + +template +PropertyObjectPtr TmsClientPropertyObjectBaseImpl::cloneChildPropertyObject(const PropertyPtr& prop) +{ + const auto propPtrInternal = prop.asPtr(); + if (propPtrInternal.assigned() && propPtrInternal.getValueTypeUnresolved() == ctObject && prop.getDefaultValue().assigned()) + { + const auto propName = prop.getName(); + const auto defaultValueObj = prop.getDefaultValue().asPtrOrNull(); + if (!defaultValueObj.assigned()) + return nullptr; + + if (!isBasePropertyObject(defaultValueObj)) + { + return defaultValueObj.asPtr(true).clone(); + } + + if (const auto& objIt = objectTypeIdMap.find(propName); objIt != objectTypeIdMap.cend()) + { + return TmsClientPropertyObject(daqContext, clientContext, objIt->second); + } + + DAQ_THROW_EXCEPTION(NotFoundException, "Object property with name {} not found", propName); + } + + return nullptr; +} + +template +bool TmsClientPropertyObjectBaseImpl::isBasePropertyObject(const PropertyObjectPtr& propObj) +{ + return !propObj.supportsInterface() + && !propObj.supportsInterface() + && !propObj.supportsInterface(); +} + +template class TmsClientPropertyObjectBaseImpl; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl>; +template class TmsClientPropertyObjectBaseImpl; +template class TmsClientPropertyObjectBaseImpl>; + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_server_capability_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_server_capability_impl.cpp new file mode 100644 index 0000000..35b7753 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_server_capability_impl.cpp @@ -0,0 +1,18 @@ +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +TmsClientServerCapabilityImpl::TmsClientServerCapabilityImpl(const ContextPtr& daqContext, + const StringPtr& protocolName, + const StringPtr& protocolId, + const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) + : TmsClientPropertyObjectBaseImpl(daqContext, protocolId, protocolName, clientContext, nodeId) +{ + +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_signal_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_signal_impl.cpp new file mode 100644 index 0000000..a6a49bf --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_signal_impl.cpp @@ -0,0 +1,204 @@ +#include +#include +#include +#include +#include +#include +#include +#include + + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsClientSignalImpl::TmsClientSignalImpl( + const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const TmsClientContextPtr& clientContext, + const OpcUaNodeId& nodeId +) + : TmsClientComponentBaseImpl(ctx, parent, localId, clientContext, nodeId) +{ + if (hasReference("Value")) + { + const auto valueNodeId = clientContext->getReferenceBrowser()->getChildNodeId(nodeId, "Value"); + const auto dataDescriptorNodeId = clientContext->getReferenceBrowser()->getChildNodeId(valueNodeId, "DataDescriptor"); + descriptorNodeId = std::make_unique(dataDescriptorNodeId); + } + + registerObject(this->borrowPtr()); +} + +ErrCode TmsClientSignalImpl::getPublic(Bool* valPublic) +{ + *valPublic = isPublic; + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientSignalImpl::setPublic(Bool valPublic) +{ + isPublic = valPublic; + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientSignalImpl::setDescriptor(IDataDescriptor* /*descriptor*/) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +SignalPtr TmsClientSignalImpl::onGetDomainSignal() +{ + try + { + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASDOMAINSIGNAL); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + const auto& references = clientContext->getReferenceBrowser()->browseFiltered(nodeId, filter); + assert(references.byNodeId.size() <= 1); + + if (!references.byNodeId.empty()) + { + auto domainSignalNodeId = references.byNodeId.begin().key(); + return findSignal(domainSignalNodeId); + } + } + catch (...) + { + LOG_W("Failed to get domain signal on OpcUA client signal \"{}\"", this->globalId); + } + + return nullptr; +} + +DataDescriptorPtr TmsClientSignalImpl::onGetDescriptor() +{ + try + { + if (descriptorNodeId) + { + OpcUaVariant opcUaVariant = client->readValue(*descriptorNodeId); + if (!opcUaVariant.isNull()) + { + DataDescriptorPtr descriptorPtr = VariantConverter::ToDaqObject(opcUaVariant); + return descriptorPtr.addRefAndReturn(); + } + } + } + catch (...) + { + LOG_W("Failed to get descriptor on OpcUA client signal \"{}\"", this->globalId); + } + + return nullptr; +} + +bool TmsClientSignalImpl::clearDescriptorOnUnsubscribe() +{ + return true; +} + +ErrCode TmsClientSignalImpl::setDomainSignal(ISignal* signal) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientSignalImpl::getRelatedSignals(IList** signals) +{ + ListPtr signalsPtr; + const ErrCode errCode = wrapHandlerReturn(this, &TmsClientSignalImpl::onGetRelatedSignals, signalsPtr); + if (OPENDAQ_FAILED(errCode)) + { + daqClearErrorInfo(); + LOG_W("Failed to get related signals on OpcUA client signal \"{}\"", this->globalId); + } + *signals = signalsPtr.detach(); + return OPENDAQ_SUCCESS; +} + + +ListPtr TmsClientSignalImpl::onGetRelatedSignals() +{ + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_RELATESTOSIGNAL); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + const auto& references = clientContext->getReferenceBrowser()->browseFiltered(nodeId, filter); + + ListPtr resultList = List(); + + for (const auto& [signalNodeId, ref] : references.byNodeId) + { + auto signal = findSignal(signalNodeId); + resultList.pushBack(signal); + } + + return resultList.detach(); +} + +ErrCode TmsClientSignalImpl::setRelatedSignals(IList* signals) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientSignalImpl::addRelatedSignal(ISignal* signal) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientSignalImpl::removeRelatedSignal(ISignal* signal) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientSignalImpl::clearRelatedSignals() +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +Bool TmsClientSignalImpl::onTriggerEvent(const EventPacketPtr& eventPacket) +{ + return Self::onTriggerEvent(eventPacket); +} + + +StringPtr TmsClientSignalImpl::onGetRemoteId() const +{ + return String(remoteComponentId).detach(); +} + +ErrCode TmsClientSignalImpl::getLastValue(IBaseObject** value) +{ + *value = nullptr; + + auto readValueFunction = [this](IBaseObject** value, const std::string& nodeName) + { + try + { + const auto valueNodeId = clientContext->getReferenceBrowser()->getChildNodeId(nodeId, nodeName); + OpcUaVariant opcUaVariant = client->readValue(*valueNodeId); + if (!opcUaVariant.isNull()) + { + BaseObjectPtr valuePtr = VariantConverter::ToDaqObject(opcUaVariant); + *value = valuePtr.addRefAndReturn(); + } + } + catch (...) + { + LOG_W("Failed to get last value on OpcUA client signal \"{}\"", this->globalId); + } + return OPENDAQ_SUCCESS; + }; + + if (descriptorNodeId && *value != nullptr) + return OPENDAQ_SUCCESS; + + if (hasReference("AnalogValue")) + return readValueFunction(value, "AnalogValue"); + + return OPENDAQ_SUCCESS; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_tags_impl.cpp b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_tags_impl.cpp new file mode 100644 index 0000000..00f854a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/objects/tms_client_tags_impl.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +TmsClientTagsImpl::TmsClientTagsImpl(const ContextPtr& ctx, const TmsClientContextPtr& clientContext, const opcua::OpcUaNodeId& nodeId) + : TmsClientObjectImpl(ctx, clientContext, nodeId) + , TagsImpl() + , loggerComponent(ctx.getLogger().getOrAddComponent("OpenDAQOPCUAClientModule")) +{ +} + +ErrCode TmsClientTagsImpl::getList(IList** value) +{ + refreshTags(); + return TagsImpl::getList(value); +} + +ErrCode TmsClientTagsImpl::add(IString* name) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientTagsImpl::replace(IList* tags) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientTagsImpl::remove(IString* name) +{ + return DAQ_MAKE_ERROR_INFO(OPENDAQ_ERR_OPCUA_CLIENT_CALL_NOT_AVAILABLE); +} + +ErrCode TmsClientTagsImpl::contains(IString* name, Bool* value) +{ + refreshTags(); + return TagsImpl::contains(name, value); +} + +ErrCode TmsClientTagsImpl::query(IString* query, Bool* value) +{ + refreshTags(); + return TagsImpl::query(query, value); +} + +void TmsClientTagsImpl::refreshTags() +{ + try + { + const ListPtr tagValues = VariantConverter::ToDaqList(client->readValue(nodeId)); + this->tags.clear(); + for (const auto& tag : tagValues) + this->tags.insert(tag); + } + catch([[maybe_unused]] const std::exception& e) + { + LOG_D("OPC UA failed to fetch tags: {}", e.what()) + } + catch(...) + { + LOG_D("OPC UA failed to fetch tags.") + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/tms_attribute_collector.cpp b/shared/libraries/opcuatms/opcuatms_client/src/tms_attribute_collector.cpp new file mode 100644 index 0000000..8b670a7 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/tms_attribute_collector.cpp @@ -0,0 +1,294 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// Node ids + +const OpcUaNodeId TmsAttributeCollector::NodeIdBaseObjectType = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_DAQBASEOBJECTTYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdBaseVariableType = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_DAQBASEVARIABLETYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdDeviceType = OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQDEVICETYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdFunctionBlockType = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_BASEFUNCTIONBLOCKTYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdComponentType = OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQCOMPONENTTYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdSignalType = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_SIGNALTYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdInputPortType = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_INPUTPORTTYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdEvaluationVariableType = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_EVALUATIONVARIABLETYPE); +const OpcUaNodeId TmsAttributeCollector::NodeIdVariableBlockType = OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_VARIABLEBLOCKTYPE); + +// TmsAttributeCollector + +TmsAttributeCollector::TmsAttributeCollector(const CachedReferenceBrowserPtr& browser) + : browser(browser) +{ +} + + +tsl::ordered_set TmsAttributeCollector::collectAttributes(const OpcUaNodeId& nodeId) +{ + attributes.clear(); + + const auto typeDefinition = browser->getTypeDefinition(nodeId); + + if (typeDefinition.isNull()) + return attributes; + + if (typeEquals(typeDefinition, NodeIdDeviceType)) + collectDeviceAttributes(nodeId); + else if (isSubtypeOf(typeDefinition, NodeIdFunctionBlockType)) + collectFunctionBlockAttributes(nodeId); + else if (typeEquals(typeDefinition, NodeIdSignalType)) + collectSignalAttributes(nodeId); + else if (typeEquals(typeDefinition, NodeIdInputPortType)) + collectInputPortAttributes(nodeId); + else if (isSubtypeOf(typeDefinition, NodeIdComponentType)) + collectComponentAttributes(nodeId); + else if (isSubtypeOf(typeDefinition, NodeIdBaseObjectType)) + collectPropertyObjectAttributes(nodeId); + else if (isSubtypeOf(typeDefinition, NodeIdBaseVariableType)) + collectPropertyAttributes(nodeId); + + return attributes; +} + +void TmsAttributeCollector::collectDeviceAttributes(const OpcUaNodeId& nodeId) +{ + collectPropertyObjectAttributes(nodeId); + + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (typeEquals(ref->typeDefinition.nodeId, NodeIdDeviceType)) + collectDeviceAttributes(refNodeId); + else if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdComponentType)) + collectComponentAttributes(refNodeId); + } + + const auto ioNodeId = browser->getChildNodeId(nodeId, "IO"); + collectIoNode(ioNodeId); + + const auto fbNodeId = browser->getChildNodeId(nodeId, "FB"); + collectFunctionBlockNode(fbNodeId); + + const auto signalsNodeId = browser->getChildNodeId(nodeId, "Sig"); + collectSignalsNode(signalsNodeId); + + const auto inputPortsNodeId = browser->getChildNodeId(nodeId, "IP"); + collectInputPortNode(inputPortsNodeId); + + const auto streamingOptionsNodeId = browser->getChildNodeId(nodeId, "StreamingOptions"); + collectStreamingOptionsNode(streamingOptionsNodeId); + + const auto methodSetId = browser->getChildNodeId(nodeId, "MethodSet"); + collectMethodSetNode(methodSetId); + + const auto synchronizationNoded = browser->getChildNodeId(nodeId, "Synchronization"); + collectComponentAttributes(synchronizationNoded); +} + +void TmsAttributeCollector::collectFunctionBlockAttributes(const OpcUaNodeId& nodeId) +{ + collectPropertyObjectAttributes(nodeId); + + const auto fbInfoId = browser->getChildNodeId(nodeId, "FunctionBlockInfo"); + attributes.insert({fbInfoId, UA_ATTRIBUTEID_VALUE}); + + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdFunctionBlockType)) + collectFunctionBlockAttributes(refNodeId); + } + + const auto signalsNodeId = browser->getChildNodeId(nodeId, "Sig"); + collectSignalsNode(signalsNodeId); + + const auto inputPortsNodeId = browser->getChildNodeId(nodeId, "IP"); + collectInputPortNode(inputPortsNodeId); +} + +void TmsAttributeCollector::collectInputPortAttributes(const OpcUaNodeId& nodeId) +{ + collectBaseObjectAttributes(nodeId); +} + +void TmsAttributeCollector::collectSignalAttributes(const OpcUaNodeId& nodeId) +{ + collectBaseObjectAttributes(nodeId); + attributes.insert({nodeId, UA_ATTRIBUTEID_BROWSENAME}); +} + +void TmsAttributeCollector::collectComponentAttributes(const OpcUaNodeId& nodeId) +{ + collectPropertyObjectAttributes(nodeId); + + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdFunctionBlockType)) + collectFunctionBlockAttributes(refNodeId); + else if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdComponentType)) + collectComponentAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectPropertyObjectAttributes(const OpcUaNodeId& nodeId) +{ + collectBaseObjectAttributes(nodeId); + + attributes.insert({nodeId, UA_ATTRIBUTEID_DESCRIPTION}); + + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (ref->nodeClass == UA_NODECLASS_METHOD) + collectMethodAttributes(refNodeId); + else if (typeEquals(ref->typeDefinition.nodeId, NodeIdEvaluationVariableType)) + collectEvaluationPropertyAttributes(refNodeId); + else if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdBaseObjectType)) + collectPropertyObjectAttributes(refNodeId); + else if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdBaseVariableType)) + collectPropertyAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectPropertyAttributes(const OpcUaNodeId& nodeId) +{ + collectBaseObjectAttributes(nodeId); + + attributes.insert({nodeId, UA_ATTRIBUTEID_VALUE}); + attributes.insert({nodeId, UA_ATTRIBUTEID_DISPLAYNAME}); + attributes.insert({nodeId, UA_ATTRIBUTEID_DESCRIPTION}); + attributes.insert({nodeId, UA_ATTRIBUTEID_DATATYPE}); + + if (browser->hasReference(nodeId, "ValidationExpression")) + attributes.insert({browser->getChildNodeId(nodeId, "ValidationExpression"), UA_ATTRIBUTEID_VALUE}); + if (browser->hasReference(nodeId, "CoercionExpression")) + attributes.insert({browser->getChildNodeId(nodeId, "CoercionExpression"), UA_ATTRIBUTEID_VALUE}); + + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (typeEquals(ref->typeDefinition.nodeId, NodeIdEvaluationVariableType)) + collectEvaluationPropertyAttributes(refNodeId); + else if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdBaseVariableType)) + collectPropertyAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectEvaluationPropertyAttributes(const OpcUaNodeId& nodeId) +{ + attributes.insert({nodeId, UA_ATTRIBUTEID_VALUE}); + + const auto evalValueId = browser->getChildNodeId(nodeId, "EvaluationExpression"); + attributes.insert({evalValueId, UA_ATTRIBUTEID_VALUE}); +} + +void TmsAttributeCollector::collectBaseObjectAttributes(const OpcUaNodeId& nodeId) +{ + attributes.insert({nodeId, UA_ATTRIBUTEID_NODECLASS}); + + if (browser->hasReference(nodeId, "NumberInList")) + attributes.insert({browser->getChildNodeId(nodeId, "NumberInList"), UA_ATTRIBUTEID_VALUE}); +} + +void TmsAttributeCollector::collectMethodAttributes(const OpcUaNodeId& nodeId) +{ + if (browser->hasReference(nodeId, "InputArguments")) + attributes.insert({browser->getChildNodeId(nodeId, "InputArguments"), UA_ATTRIBUTEID_VALUE}); + if (browser->hasReference(nodeId, "OutputArguments")) + attributes.insert({browser->getChildNodeId(nodeId, "OutputArguments"), UA_ATTRIBUTEID_VALUE}); + if (browser->hasReference(nodeId, "NumberInList")) + attributes.insert({browser->getChildNodeId(nodeId, "NumberInList"), UA_ATTRIBUTEID_VALUE}); +} + +void TmsAttributeCollector::collectVariableBlockAttributes(const OpcUaNodeId& nodeId) +{ + collectPropertyObjectAttributes(nodeId); +} + +void TmsAttributeCollector::collectIoNode(const OpcUaNodeId& nodeId) +{ + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdFunctionBlockType)) + collectFunctionBlockAttributes(refNodeId); + else if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdComponentType)) + collectComponentAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectInputPortNode(const OpcUaNodeId& nodeId) +{ + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (typeEquals(ref->typeDefinition.nodeId, NodeIdInputPortType)) + collectInputPortAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectFunctionBlockNode(const OpcUaNodeId& nodeId) +{ + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdFunctionBlockType)) + collectFunctionBlockAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectSignalsNode(const OpcUaNodeId& nodeId) +{ + const auto& signalReferences = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : signalReferences.byNodeId) + { + if (isSubtypeOf(ref->typeDefinition.nodeId, NodeIdSignalType)) + collectSignalAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectStreamingOptionsNode(const OpcUaNodeId& nodeId) +{ + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (typeEquals(ref->typeDefinition.nodeId, NodeIdVariableBlockType)) + collectVariableBlockAttributes(refNodeId); + } +} + +void TmsAttributeCollector::collectMethodSetNode(const OpcUaNodeId& nodeId) +{ + const auto& references = browser->browse(nodeId); + + for (const auto& [refNodeId, ref] : references.byNodeId) + { + if (ref->nodeClass == UA_NODECLASS_METHOD) + collectMethodAttributes(refNodeId); + } +} + +bool TmsAttributeCollector::isSubtypeOf(const OpcUaNodeId& typeId, const OpcUaNodeId& baseType) +{ + return browser->isSubtypeOf(typeId, baseType); +} + +bool TmsAttributeCollector::typeEquals(const OpcUaNodeId& typeId, const OpcUaNodeId& baseType) +{ + return typeId == baseType; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_client/src/tms_client.cpp b/shared/libraries/opcuatms/opcuatms_client/src/tms_client.cpp new file mode 100644 index 0000000..9df603b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/src/tms_client.cpp @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace daq::opcua; +using namespace daq::opcua::tms; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +TmsClient::TmsClient(const ContextPtr& context, + const ComponentPtr& parent, + const std::string& opcUaUrl) + : TmsClient(context, parent, OpcUaEndpoint(opcUaUrl)) +{ +} + +TmsClient::TmsClient(const ContextPtr& context, + const ComponentPtr& parent, + const OpcUaEndpoint& endpoint) + : context(context) + , endpoint(endpoint) + , parent(parent) + , loggerComponent(context.getLogger().assigned() ? context.getLogger().getOrAddComponent("OpenDAQOPCUAClientModule") + : throw ArgumentNullException("Logger must not be null")) +{ +} + +daq::DevicePtr TmsClient::connect() +{ + const auto startTime = std::chrono::steady_clock::now(); + + createAndConectClient(); + client->runIterate(); + + // A first connect is needed to read from the server the available namespaces out from the server + auto namespaces = VariantConverter::ToDaqList(client->readValue(OpcUaNodeId(0,UA_NS0ID_SERVER_NAMESPACEARRAY))); + + client->stopIterate(); + client->disconnect(); + + // After a disconnect, we need to register the data types, but only these which are available on server side. + registerDaqTypes(endpoint, namespaces); + + createAndConectClient(); + client->runIterate(); + + tmsClientContext = std::make_shared(client, context); + tmsClientContext->addEnumerationTypesToTypeManager(); + + OpcUaNodeId rootDeviceNodeId; + std::string rootDeviceBrowseName; + getRootDeviceNodeAttributes(rootDeviceNodeId, rootDeviceBrowseName); + + auto device = TmsClientRootDevice(context, parent, rootDeviceBrowseName, tmsClientContext, rootDeviceNodeId); + + const auto deviceInfo = device.getInfo(); + deviceInfo.asPtr().setProtectedPropertyValue("connectionString", endpoint.getUrl()); + + const std::string packageVersion = deviceInfo.getSdkVersion(); + if (!packageVersion.empty() && packageVersion != OPENDAQ_PACKAGE_VERSION) + { + LOG_D("Connected to openDAQ OPC UA server with different version. Client version: {}, server version: {}", + OPENDAQ_PACKAGE_VERSION, + packageVersion); + } + + const auto endTime = std::chrono::steady_clock::now(); + const auto connectTime = std::chrono::duration(endTime - startTime); + LOG_D("Connected to openDAQ OPC UA server {}. Connect took {:.2f} s.", endpoint.getUrl(), connectTime.count()); + return device; +} + +void TmsClient::getRootDeviceNodeAttributes(OpcUaNodeId& nodeIdOut, std::string& browseNameOut) +{ + const OpcUaNodeId rootNodeId(NAMESPACE_DI, UA_DIID_DEVICESET); + + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(0, UA_NS0ID_HASCOMPONENT); + filter.typeDefinition = OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQDEVICETYPE); + + const auto& references = tmsClientContext->getReferenceBrowser()->browseFiltered(rootNodeId, filter); + + if (references.byNodeId.empty()) + DAQ_THROW_EXCEPTION(NotFoundException); + + nodeIdOut = OpcUaNodeId(references.byBrowseName.begin().value()->nodeId.nodeId); + browseNameOut = references.byBrowseName.begin().key(); +} + +void TmsClient::createAndConectClient() +{ + try + { + client = std::make_shared(endpoint); + client->connect(); + } + catch (const OpcUaException& e) + { + switch (e.getStatusCode()) + { + case UA_STATUSCODE_BADUSERACCESSDENIED: + case UA_STATUSCODE_BADIDENTITYTOKENINVALID: + DAQ_THROW_EXCEPTION(AuthenticationFailedException, e.what()); + default: + DAQ_THROW_EXCEPTION(NotFoundException, e.what()); + } + } +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms_client/tests/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_client/tests/CMakeLists.txt new file mode 100644 index 0000000..09d2ff2 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/tests/CMakeLists.txt @@ -0,0 +1,29 @@ +set(MODULE_NAME opcuatms_client) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_client_context.cpp +) + +add_executable(${TEST_APP} test_app.cpp + ${TEST_SOURCES} +) + +set_target_properties(${TEST_APP} PROPERTIES DEBUG_POSTFIX _debug) + +if (WIN32) + set(BCRYPT_LIB bcrypt.dll) +endif() + +target_link_libraries(${TEST_APP} PRIVATE ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::test_utils + ${BCRYPT_LIB} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}coverage ${TEST_APP} ${MODULE_NAME}coverage) +endif() diff --git a/shared/libraries/opcuatms/opcuatms_client/tests/test_app.cpp b/shared/libraries/opcuatms/opcuatms_client/tests/test_app.cpp new file mode 100644 index 0000000..a4462ae --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/tests/test_app.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int argc, char** args) +{ + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new DaqMemCheckListener()); + + return RUN_ALL_TESTS(); +} diff --git a/shared/libraries/opcuatms/opcuatms_client/tests/test_client_context.cpp b/shared/libraries/opcuatms/opcuatms_client/tests/test_client_context.cpp new file mode 100644 index 0000000..feafa12 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_client/tests/test_client_context.cpp @@ -0,0 +1,107 @@ +#include +#include +#include + +using namespace daq::opcua; +using namespace daq::opcua::tms; +using namespace daq; + +using ClientContextTest = testing::Test; + +static OpcUaClientPtr CreateClient() +{ + return std::make_shared("opc.tcp://localhost"); +} + +TEST_F(ClientContextTest, Create) +{ + auto client = CreateClient(); + auto context = NullContext(); + ASSERT_NO_THROW(TmsClientContext clientContext(client, context)); +} + +TEST_F(ClientContextTest, RegisterObject) +{ + auto client = CreateClient(); + auto context = NullContext(); + TmsClientContext clientContext(client, context); + + StringPtr str = "TestStrObject"; + ASSERT_NO_THROW(clientContext.registerObject(OpcUaNodeId(1, 1), str)); +} + +TEST_F(ClientContextTest, ContextGetObject) +{ + auto client = CreateClient(); + auto context = NullContext(); + TmsClientContext clientContext(client, context); + + BaseObjectPtr baseObj; + ASSERT_NO_THROW(baseObj = clientContext.getObject(OpcUaNodeId(1, 1))); + ASSERT_FALSE(baseObj.assigned()); + + StringPtr str = "TestStrObject"; + clientContext.registerObject(OpcUaNodeId(1, 1), str); + + ASSERT_NO_THROW(baseObj = clientContext.getObject(OpcUaNodeId(1, 1))); + ASSERT_TRUE(baseObj.assigned()); + ASSERT_EQ(baseObj, str); +} + +TEST_F(ClientContextTest, ContextGetObjectTemplate) +{ + auto client = CreateClient(); + auto context = NullContext(); + TmsClientContext clientContext(client, context); + + StringPtr strObj; + ASSERT_NO_THROW(strObj = clientContext.getObject(OpcUaNodeId(1, 1))); + ASSERT_FALSE(strObj.assigned()); + + StringPtr str = "TestStrObject"; + clientContext.registerObject(OpcUaNodeId(1, 1), str); + + ASSERT_NO_THROW(strObj = clientContext.getObject(OpcUaNodeId(1, 1))); + ASSERT_TRUE(strObj.assigned()); + ASSERT_EQ(strObj, str); + + FloatPtr floatPtr; + ASSERT_NO_THROW(floatPtr = clientContext.getObject(OpcUaNodeId(1, 1))); + ASSERT_FALSE(floatPtr.assigned()); +} + +TEST_F(ClientContextTest, UnregisterObject) +{ + auto client = CreateClient(); + auto context = NullContext(); + TmsClientContext clientContext(client, context); + + ASSERT_NO_THROW(clientContext.unregisterObject(OpcUaNodeId(1, 1))); + + StringPtr str = "TestStrObject"; + clientContext.registerObject(OpcUaNodeId(1, 1), str); + + ASSERT_NO_THROW(clientContext.unregisterObject(OpcUaNodeId(1, 1))); + + auto baseObj = clientContext.getObject(OpcUaNodeId(1, 1)); + ASSERT_FALSE(baseObj.assigned()); +} + +TEST_F(ClientContextTest, TestRefCount) +{ + auto getRefCount = [](const StringPtr& obj) + { + obj->addRef(); + return obj->releaseRef(); + }; + + auto client = CreateClient(); + auto context = NullContext(); + TmsClientContext clientContext(client, context); + + StringPtr str = "TestStrObject"; + ASSERT_EQ(getRefCount(str), 1); + + clientContext.registerObject(OpcUaNodeId(1, 1), str); + ASSERT_EQ(getRefCount(str), 1); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_server/CMakeLists.txt new file mode 100644 index 0000000..847fbec --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcuatms_server VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_analog_value.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_analog_value.h new file mode 100644 index 0000000..169433d --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_analog_value.h @@ -0,0 +1,48 @@ +/* + * 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_TMS + +// TmsServerAnalogValue + +class TmsServerAnalogValue; +using TmsServerAnalogValuePtr = std::shared_ptr; + +class TmsServerAnalogValue : public TmsServerVariable +{ +public: + using Super = TmsServerVariable; + + TmsServerAnalogValue(const SignalPtr& signal, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + std::string getBrowseName() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + opcua::OpcUaNodeId getDataTypeId() override; + void bindCallbacks() override; + +private: + static opcua::OpcUaNodeId sampleTypeToOpcUaDataType(SampleType sampleType); + + SignalPtr signal; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS + diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_channel.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_channel.h new file mode 100644 index 0000000..da88f5e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_channel.h @@ -0,0 +1,38 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerChannel; +using TmsServerChannelPtr = std::shared_ptr; + +class TmsServerChannel : public TmsServerFunctionBlock +{ +public: + using Super = TmsServerFunctionBlock; + + TmsServerChannel(const ChannelPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h new file mode 100644 index 0000000..5ff0472 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_component.h @@ -0,0 +1,277 @@ +/* + * 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_TMS + +template +class TmsServerComponent; +using TmsServerComponentPtr = std::shared_ptr>; + +template +class TmsServerComponent : public TmsServerObjectBaseImpl +{ +public: + using Super = TmsServerObjectBaseImpl; + + TmsServerComponent(const ComponentPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + bool createOptionalNode(const opcua::OpcUaNodeId& nodeId) override; + + std::string getBrowseName() override; + std::string getDisplayName() override; + std::string getDescription() override; + opcua::OpcUaNodeId getReferenceType() override; + void bindCallbacks() override; + void registerToTmsServerContext() override; + void addChildNodes() override; + void onCoreEvent(const CoreEventArgsPtr& args) override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + void configureNodeAttributes(opcua::OpcUaObject& attr) override; + + std::unique_ptr tmsPropertyObject; + std::unique_ptr tmsComponentConfig; + +private: + bool selfChange; +}; + +using namespace opcua; + +template +TmsServerComponent::TmsServerComponent(const ComponentPtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) + , selfChange(false) +{ + tmsPropertyObject = std::make_unique(this->object, this->server, this->daqContext, this->tmsContext, std::unordered_set{"Name", "Description"}); + if (auto componentPrivate = this->object.template asPtrOrNull(true); componentPrivate.assigned()) + { + auto componentConfig = componentPrivate.getComponentConfig(); + if (componentConfig.assigned()) + tmsComponentConfig = std::make_unique(componentConfig, this->server, this->daqContext, this->tmsContext, "ComponentConfig"); + } + + this->loggerComponent = this->daqContext.getLogger().getOrAddComponent("OPCUAServerComponent"); +} + +template +std::string TmsServerComponent::getBrowseName() +{ + return this->object.getLocalId(); +} + +template +std::string TmsServerComponent::getDisplayName() +{ + return this->object.getName(); +} + +template +std::string TmsServerComponent::getDescription() +{ + return this->object.getDescription(); +} + +template +OpcUaNodeId TmsServerComponent::getReferenceType() +{ + return OpcUaNodeId(0, UA_NS0ID_HASCOMPONENT); +} + +template +OpcUaNodeId TmsServerComponent::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQCOMPONENTTYPE); +} + +template +void TmsServerComponent::configureNodeAttributes(opcua::OpcUaObject& attr) +{ + TmsServerObject::configureNodeAttributes(attr); + std::unordered_set lockedAttrs; + for (const auto& str : this->object.getLockedAttributes()) + lockedAttrs.insert(str); + + if (!lockedAttrs.count("Name")) + attr->writeMask |= UA_WRITEMASK_DISPLAYNAME; + if (!lockedAttrs.count("Description")) + attr->writeMask |= UA_WRITEMASK_DESCRIPTION; +} + +template +bool TmsServerComponent::createOptionalNode(const OpcUaNodeId& nodeId) +{ + return false; +} + +template +void TmsServerComponent::bindCallbacks() +{ + this->addReadCallback("Tags", [this] + { + const TagsPtr tags = this->object.getTags(); + if (tags.assigned()) + return VariantConverter::ToArrayVariant(tags.getList()); + return VariantConverter::ToArrayVariant(List()); + }); + + this->addReadCallback("Active", [this] { return VariantConverter::ToVariant( this->object.getActive()); }); + if (!this->object.template supportsInterface() || !this->object.isFrozen()) + { + this->addWriteCallback("Active", [this] (const OpcUaVariant& variant) + { + this->object.setActive(VariantConverter::ToDaqObject(variant)); + return UA_STATUSCODE_GOOD; + }); + } + + this->addReadCallback("Visible", [this] { return VariantConverter::ToVariant( this->object.getVisible()); }); + + if (!this->object.template supportsInterface() || !this->object.isFrozen()) + { + this->addWriteCallback("Visible", [this] (const OpcUaVariant& variant) + { + this->object.setVisible(VariantConverter::ToDaqObject(variant)); + return UA_STATUSCODE_GOOD; + }); + } + + DisplayNameChangedCallback nameChangedCallback = + [this](const OpcUaNodeId& /*nodeId*/, const OpcUaObject& name, void* /*context*/) + { + if (selfChange) + return; + + try + { + selfChange = true; + this->object.setName(utils::ToStdString(name->text)); + } + catch ([[maybe_unused]] const std::exception& e) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Component {} failed to set component name: {}", this->object.getGlobalId(), e.what()); + } + catch (...) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Component {} failed to set component name.", this->object.getGlobalId()); + } + + selfChange = false; + }; + this->server->getEventManager()->onDisplayNameChanged(this->nodeId, nameChangedCallback); + + DisplayNameChangedCallback descriptionChangedCallback = + [this](const OpcUaNodeId& /*nodeId*/, const OpcUaObject& description, void* /*context*/) + { + if (selfChange) + return; + + try + { + selfChange = true; + this->object.setDescription(utils::ToStdString(description->text)); + } + catch ([[maybe_unused]] const std::exception& e) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Component {} failed to set component description: {}", this->object.getGlobalId(), e.what()); + } + catch (...) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Component {} failed to set component description.", this->object.getGlobalId()); + } + + selfChange = false; + }; + this->server->getEventManager()->onDescriptionChanged(this->nodeId, descriptionChangedCallback); +} + +template +void TmsServerComponent::registerToTmsServerContext() +{ + Super::registerToTmsServerContext(); + this->tmsContext->registerComponent(this->object, *this); +} + +template +void TmsServerComponent::addChildNodes() +{ + OpcUaNodeId newNodeId(0); + AddVariableNodeParams params(newNodeId, this->nodeId); + params.setBrowseName("Visible"); + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_BOOLEAN].typeId)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + + OpcUaObject attr = UA_VariableAttributes_default; + attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + params.attr = attr; + + this->server->addVariableNode(params); + + tmsPropertyObject->registerToExistingOpcUaNode(this->nodeId); + if (tmsComponentConfig) + tmsComponentConfig->registerOpcUaNode(this->nodeId); +} + +template +void TmsServerComponent::onCoreEvent(const CoreEventArgsPtr& args) +{ + Super::onCoreEvent(args); + + if (!selfChange && args.getEventId() == static_cast(CoreEventId::AttributeChanged)) + { + const StringPtr attrName = args.getParameters().get("AttributeName"); + + try + { + selfChange = true; + if (attrName == "Name") + this->server->setDisplayName(this->nodeId, args.getParameters().get("Name")); + else if (attrName == "Description") + this->server->setDescription(this->nodeId, args.getParameters().get("Description")); + } + catch ([[maybe_unused]] const std::exception& e) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Component {} failed to set node attribute \"{}\": {}", this->object.getGlobalId(), attrName, e.what()); + } + catch (...) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Component {} failed to set node attribute \"{}\".", this->object.getGlobalId(), attrName); + } + + selfChange = false; + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_device.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_device.h new file mode 100644 index 0000000..715ed61 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_device.h @@ -0,0 +1,76 @@ +/* + * 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_TMS + +class TmsServerDevice; +using TmsServerDevicePtr = std::shared_ptr; + +class TmsServerDevice : public TmsServerComponent +{ +public: + using Super = TmsServerComponent; + + TmsServerDevice(const DevicePtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + bool createOptionalNode(const opcua::OpcUaNodeId& nodeId) override; + + void bindCallbacks() override; + void addChildNodes() override; + + void createNonhierarchicalReferences() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + void populateDeviceInfo(); + void populateServerCapabilities(); + void addFunctionBlockFolderNodes(); + void createFunctionBlockTypesFolder(const OpcUaNodeId& parentId); + void createAddFunctionBlockNode(const OpcUaNodeId& parentId); + void createRemoveFunctionBlockNode(const OpcUaNodeId& parentId); + void onGetAvailableFunctionBlockTypes(const NodeEventManager::MethodArgs& args); + void onAddFunctionBlock(const NodeEventManager::MethodArgs& args); + void onRemoveFunctionBlock(const NodeEventManager::MethodArgs& args); + TmsServerFunctionBlockPtr addFunctionBlock(const StringPtr& fbTypeId, const OpcUaVariant& configVariant); + TmsServerFunctionBlockPtr addFunctionBlock(const StringPtr& fbTypeId, const PropertyObjectPtr& config); + void removeFunctionBlock(const StringPtr& localId); + + // TODO we need following list to keep this because of handlers. Move handlers (TmsServerObject) to context and use UA_NodeTypeLifecycle + // for deleting it + std::list signals; + std::list devices; + std::list functionBlocks; + std::list folders; + std::list components; + std::list serverCapabilities; + std::list functionBlockTypes; + std::list syncComponents; + std::list deviceInfoProperties; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_eval_value.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_eval_value.h new file mode 100644 index 0000000..c1520a9 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_eval_value.h @@ -0,0 +1,58 @@ +/* + * 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_TMS + +// TmsServerEvalValue + +class TmsServerEvalValue; +using TmsServerEvalValuePtr = std::shared_ptr; + +class TmsServerEvalValue : public TmsServerVariable +{ +public: + using Super = TmsServerVariable; + using ReadCallback = std::function; + using WriteCallback = std::function; + + TmsServerEvalValue(const EvalValuePtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + TmsServerEvalValue(const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + std::string getBrowseName() override; + void setReadCallback(ReadCallback readCallback); + void setWriteCallback(WriteCallback writeCallback); + void setIsSelectionType(bool isSelection); + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + void bindCallbacks() override; + void configureVariableNodeAttributes(opcua::OpcUaObject& attr) override; + +private: + opcua::OpcUaVariant readEvaluationExpression(); + opcua::OpcUaVariant readRoot(); + UA_StatusCode writeRoot(const opcua::OpcUaVariant& variant); + + ReadCallback readCallback; + WriteCallback writeCallback; + + bool isSelection = false; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_folder.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_folder.h new file mode 100644 index 0000000..0a795af --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_folder.h @@ -0,0 +1,44 @@ +/* + * 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_TMS + +class TmsServerFolder; +using TmsServerFolderPtr = std::shared_ptr; + +class TmsServerFolder : public TmsServerComponent +{ +public: + using Super = TmsServerComponent; + + TmsServerFolder(const FolderPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + void createNonhierarchicalReferences() override; + void addChildNodes() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + + std::list channels; + std::list folders; + std::list components; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_function_block.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_function_block.h new file mode 100644 index 0000000..2c976d4 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_function_block.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +template +class TmsServerFunctionBlock; + +using TmsServerFunctionBlockPtr = std::shared_ptr>; + +template +class TmsServerFunctionBlock : public TmsServerComponent +{ +public: + using Super = TmsServerComponent; + + TmsServerFunctionBlock(const FunctionBlockPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + bool createOptionalNode(const opcua::OpcUaNodeId& nodeId) override; + void bindCallbacks() override; + + void addChildNodes() override; + void createNonhierarchicalReferences() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + + // TODO we need following list to keep this becouse of handlers. Move handlers (TmsServerObject) to context and use UA_NodeTypeLifecycle + // for deleting it + std::list signals; + std::list inputPorts; + std::list functionBlocks; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_function_block_type.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_function_block_type.h new file mode 100644 index 0000000..963ca46 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_function_block_type.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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerFunctionBlockType; +using TmsServerFunctionBlockTypePtr = std::shared_ptr; + +class TmsServerFunctionBlockType : public TmsServerVariable +{ +public: + using Super = TmsServerVariable; + + TmsServerFunctionBlockType(const FunctionBlockTypePtr& object, + const OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext); + + std::string getBrowseName() override; + std::string getDisplayName() override; + std::string getDescription() override; + +protected: + OpcUaNodeId getTmsTypeId() override; + void addChildNodes() override; + void configureVariableNodeAttributes(OpcUaObject& attr) override; + +private: + void addIdNode(); + void addNameNode(); + void addDescriptionNode(); + void addDefaultConfigNode(); + + TmsServerPropertyObjectPtr tmsDefaultConfig; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_input_port.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_input_port.h new file mode 100644 index 0000000..ac1a9e3 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_input_port.h @@ -0,0 +1,46 @@ +/* + * 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_TMS + +class TmsServerInputPort; +using TmsServerInputPortPtr = std::shared_ptr; + +class TmsServerInputPort : public TmsServerComponent +{ +public: + using Super = TmsServerComponent; + + TmsServerInputPort(const InputPortPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + opcua::OpcUaNodeId getReferenceType() override; + void addChildNodes() override; + void bindCallbacks() override; + void createNonhierarchicalReferences() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + void createConnectMethodNode(); + void createDisconnectMethodNode(); + void onConnectSignal(NodeEventManager::MethodArgs args); + void onDisconenctSignal(NodeEventManager::MethodArgs args); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h new file mode 100644 index 0000000..f3a5707 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_object.h @@ -0,0 +1,184 @@ +/* + * 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_TMS + +template +struct RequestedNodeId +{ + opcua::OpcUaNodeId operator()(const CoreType& object) + { + return {}; + } +}; + +template +struct RequestedNodeId::Value>> +{ + opcua::OpcUaNodeId operator()(const CoreType& object) + { + return opcua::OpcUaNodeId(NAMESPACE_DAQBSP, object.getGlobalId().toStdString()); + } +}; + +class TmsServerObject; +using TmsServerObjectPtr = std::shared_ptr; + +class TmsServerContext; +using TmsServerContextPtr = std::shared_ptr; + +class TmsServerObject : public std::enable_shared_from_this +{ +public: + using ReadVariantCallback = std::function; + using WriteVariantCallback = std::function; + + TmsServerObject(const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + virtual ~TmsServerObject(); + + virtual std::string getBrowseName(); + virtual std::string getDisplayName(); + virtual std::string getDescription(); + opcua::NodeEventManagerPtr addEvent(const StringPtr& nodeName); + opcua::NodeEventManagerPtr addEvent(const opcua::OpcUaNodeId& nodeId); + opcua::OpcUaNodeId registerOpcUaNode( + const opcua::OpcUaNodeId& parentNodeId = opcua::OpcUaNodeId(UA_NS0ID_OBJECTSFOLDER)); + opcua::OpcUaNodeId registerToExistingOpcUaNode(const opcua::OpcUaNodeId& nodeId); + opcua::OpcUaNodeId getNodeId(); + void setNumberInList(uint32_t numberInList); + + void addHierarchicalReference(const opcua::OpcUaNodeId& parent); + virtual void createNonhierarchicalReferences(); + virtual void onCoreEvent(const CoreEventArgsPtr& eventArgs); + +protected: + virtual void validate(); + virtual opcua::OpcUaNodeId getRequestedNodeId(); + virtual opcua::OpcUaNodeId getReferenceType(); + virtual opcua::OpcUaNodeId getTmsTypeId() = 0; + virtual BaseObjectPtr getObject() = 0; + virtual opcua::OpcUaNodeId createNode(const opcua::OpcUaNodeId& parentNodeId); + virtual void addChildNodes(); + virtual void bindCallbacks(); + virtual void registerToTmsServerContext(); + virtual int64_t getCurrentClock(); + std::string readTypeBrowseName(); + virtual bool createOptionalNode(const opcua::OpcUaNodeId& nodeId); + virtual void configureNodeAttributes(opcua::OpcUaObject& attr); + void addReadCallback(const std::string& nodeName, ReadVariantCallback readFunc); + void addWriteCallback(const std::string& nodeName, WriteVariantCallback writeFunc); + void addReadCallback(const opcua::OpcUaNodeId& nodeId, ReadVariantCallback readFunc); + void addWriteCallback(const opcua::OpcUaNodeId& nodeId, WriteVariantCallback writeFunc); + void addReference(const opcua::OpcUaNodeId& targetNodeId, const opcua::OpcUaNodeId& referenceTypeId); + void deleteReferencesOfType(const opcua::OpcUaNodeId& referenceTypeId); + void bindReadWriteCallbacks(); + void browseReferences(); + bool hasChildNode(const std::string& nodeName) const; + opcua::OpcUaNodeId getChildNodeId(const std::string& nodeName); + opcua::OpcUaNodeId findSignalNodeId(const SignalPtr& signal) const; + + template + opcua::OpcUaNodeId findTmsObjectNodeId(const C& signal) const + { + auto nodeId = RequestedNodeId{}(signal); + return server->nodeExists(nodeId) ? nodeId : opcua::OpcUaNodeId(); + } + + template + void createChildNonhierarchicalReferences(const C& container) + { + for (const auto& item : container) + item->createNonhierarchicalReferences(); + } + + // TODO: NumberInList is configured only when object is registered. Order of nodes on objects of which child nodes + // reference other ones will not be correct! + template + std::shared_ptr registerTmsObjectOrAddReference(const opcua::OpcUaNodeId& parentNodeId, + const DAQ_T& daqObject, + uint32_t numberInList, + Params... params) + { + auto tmsObjectNodeId = findTmsObjectNodeId(daqObject); + if (!tmsObjectNodeId.isNull()) + { + void* nodeContext = {}; + opcua::CheckStatusCodeException(UA_Server_getNodeContext(this->server->getUaServer(), *tmsObjectNodeId, &nodeContext)); + auto tmsObject = std::dynamic_pointer_cast(static_cast(nodeContext)->shared_from_this()); + tmsObject->addHierarchicalReference(parentNodeId); + return tmsObject; + } + else + { + auto tmsObject = std::make_shared(daqObject, this->server, daqContext, tmsContext, std::forward(params)...); + tmsObject->registerOpcUaNode(parentNodeId); + if(numberInList != std::numeric_limits::max()) + tmsObject->setNumberInList(numberInList); + return tmsObject; + } + } + + opcua::OpcUaServerPtr server; + std::string typeBrowseName; + std::mutex valueMutex; + opcua::OpcUaNodeId nodeId; + ContextPtr daqContext; + uint32_t numberInList; + TmsServerContextPtr tmsContext; + std::unordered_map> references; + LoggerComponentPtr loggerComponent; + +private: + void bindCallbacksInternal(); + + std::unordered_map readCallbacks; + std::unordered_map writeCallbacks; + std::unordered_map eventManagers; +}; + +template +class TmsServerObjectBaseImpl : public TmsServerObject +{ +public: + TmsServerObjectBaseImpl(const BaseObjectPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : TmsServerObject(server, context, tmsContext) + , object(object) + { + } + +protected: + BaseObjectPtr getObject() override + { + return object; + } + + opcua::OpcUaNodeId getRequestedNodeId() override + { + return RequestedNodeId{}(object); + } + + CoreType object; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_property.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_property.h new file mode 100644 index 0000000..77bf617 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_property.h @@ -0,0 +1,94 @@ +/* + * 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_TMS + +class TmsServerProperty; +using TmsServerPropertyPtr = std::shared_ptr; + +class TmsServerProperty : public TmsServerVariable +{ +public: + using Super = TmsServerVariable; + + TmsServerProperty(const PropertyPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const std::string& browseName = ""); + TmsServerProperty(const PropertyPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const std::unordered_map& propOrder, + const std::string& browseName = ""); + TmsServerProperty(const PropertyPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const PropertyObjectPtr& parent, + const std::unordered_map& propOrder, + const std::string& browseName = ""); + + std::string getPropertyName(); + std::string getBrowseName() override; + void bindCallbacks() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + bool createOptionalNode(const opcua::OpcUaNodeId& nodeId) override; + void addChildNodes() override; + void configureVariableNodeAttributes(opcua::OpcUaObject& attr) override; + void validate() override; + opcua::OpcUaNodeId getDataTypeId() override; + +private: + void registerEvalValueNode(const std::string& nodeName, TmsServerEvalValue::ReadCallback readCallback, bool isSelection = false); + bool isSelectionType(); + bool isNumericType(); + bool isIntrospectionType(); + bool isReferenceType(); + bool isStructureType(); + + void hideReferenceTypeChildren(); + void hideNumericTypeChildren(); + void hideSelectionTypeChildren(); + void hideIntrospectionTypeChildren(); + void hideStructureTypeChildren(); + + void addReferenceTypeChildNodes(); + void addNumericTypeChildNodes(); + void addSelectionTypeChildNodes(); + void addIntrospectionTypeChildNodes(); + + std::unordered_map childObjects; + PropertyInternalPtr objectInternal; + WeakRefPtr parent; + + std::unordered_set HiddenNodes = {"FieldCoercionExpression", "FieldValidationExpression", ""}; + std::unordered_map childProperties; + std::unordered_map propOrder; + std::string browseName; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_property_object.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_property_object.h new file mode 100644 index 0000000..a0d6039 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_property_object.h @@ -0,0 +1,83 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerPropertyObject; +using TmsServerPropertyObjectPtr = std::shared_ptr; + +class TmsServerPropertyObject : public TmsServerObjectBaseImpl +{ +public: + using Super = TmsServerObjectBaseImpl; + + TmsServerPropertyObject(const PropertyObjectPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const std::unordered_set& ignoredProps = {}); + TmsServerPropertyObject(const PropertyObjectPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const StringPtr& name); + TmsServerPropertyObject(const PropertyObjectPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const StringPtr& name, + const PropertyPtr& objProp); + ~TmsServerPropertyObject(); + + std::string getBrowseName() override; + void addChildNodes() override; + void bindCallbacks() override; + bool createOptionalNode(const opcua::OpcUaNodeId& nodeId) override; + void setMethodParentNodeId(const opcua::OpcUaNodeId& methodParentNodeId); + void addProperty(const TmsServerPropertyPtr& childProperty); + std::unordered_set ignoredProps; + std::unordered_map propBrowseName; // property name -> browse name (if not use browse name as property name) + +protected: + void configureNodeAttributes(opcua::OpcUaObject& attr) override; + void triggerEvent(PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args); + opcua::OpcUaNodeId getTmsTypeId() override; + void addPropertyNode(const std::string& name, const opcua::OpcUaNodeId& parentId); + void bindPropertyCallbacks(const opcua::OpcUaNodeId& nodeId, const std::string& propName); + + std::unordered_map childProperties; + std::unordered_map childObjects; + std::unordered_map childEvalValues; + std::unordered_map> methodProps; + + void registerEvalValueNode(const std::string& nodeName, TmsServerEvalValue::ReadCallback readCallback); + void addMethodPropertyNode(const PropertyPtr& prop, uint32_t numberInList); + void bindMethodCallbacks(); + void addBeginUpdateNode(); + void addEndUpdateNode(); + + StringPtr name; + PropertyInternalPtr objProp; + opcua::OpcUaNodeId methodParentNodeId; + uint32_t numberinList = 0; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_signal.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_signal.h new file mode 100644 index 0000000..29fd1a2 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_signal.h @@ -0,0 +1,49 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerSignal; +using TmsServerSignalPtr = std::shared_ptr; + +class TmsServerSignal : public TmsServerComponent +{ +public: + using Super = TmsServerComponent; + + TmsServerSignal(const SignalPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + opcua::OpcUaNodeId getReferenceType() override; + void addChildNodes() override; + void onCoreEvent(const CoreEventArgsPtr& args) override; + + void createNonhierarchicalReferences() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + +private: + TmsServerValuePtr valueServer; + TmsServerAnalogValuePtr analogValueServer; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_sync_component.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_sync_component.h new file mode 100644 index 0000000..0b3b038 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_sync_component.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 +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerSyncComponent; +using TmsServerSyncComponentPtr = std::shared_ptr; + +class TmsServerSyncComponent : public TmsServerComponent +{ +public: + using Super = TmsServerComponent; + + TmsServerSyncComponent(const SyncComponentPtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + void addChildNodes() override; + void bindCallbacks() override; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + void triggerEvent(PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args); + + TmsServerSyncInterfacesPtr interfaces; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_sync_interface.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_sync_interface.h new file mode 100644 index 0000000..c002757 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_sync_interface.h @@ -0,0 +1,83 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerSyncInterface; +using TmsServerSyncInterfacePtr = std::shared_ptr; + +class TmsServerSyncInterface : public TmsServerPropertyObject +{ +public: + using Super = TmsServerPropertyObject; + using Super::Super; + +protected: + opcua::OpcUaNodeId getTmsTypeId() override + { + if (name == "InterfaceClockSync") + return OpcUaNodeId(NAMESPACE_DAQESP, UA_DAQESPID_INTERNALCLOCKSYNCINTERFACETYPE); + + if (name == "PtpSyncInterface") + return OpcUaNodeId(NAMESPACE_DAQESP, UA_DAQESPID_PTPSYNCINTERFACETYPE); + + return Super::getTmsTypeId(); + } +}; + +class TmsServerSyncInterfaces; +using TmsServerSyncInterfacesPtr = std::shared_ptr; + +class TmsServerSyncInterfaces : public TmsServerPropertyObject +{ +public: + using Super = TmsServerPropertyObject; + using Super::Super; + + bool createOptionalNode(const opcua::OpcUaNodeId& nodeId) override + { + const auto name = server->readBrowseNameString(nodeId); + + if (name == "") + return false; + + return Super::createOptionalNode(nodeId); + } + + void addChildNodes() override + { + uint32_t propNumber = 0; + + for (const auto& prop : object.getAllProperties()) + { + const auto propName = prop.getName(); + ignoredProps.emplace(propName); + PropertyObjectPtr obj = object.getPropertyValue(propName); + auto serverInfo = registerTmsObjectOrAddReference(nodeId, obj, propNumber++, propName, prop); + auto childNodeId = serverInfo->getNodeId(); + childObjects.insert({childNodeId, serverInfo}); + } + + Super::addChildNodes(); + } +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_value.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_value.h new file mode 100644 index 0000000..7102884 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_value.h @@ -0,0 +1,49 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +// TmsServerValue + +class TmsServerValue; +using TmsServerValuePtr = std::shared_ptr; + +class TmsServerValue : public TmsServerVariable +{ +public: + using Super = TmsServerVariable; + + TmsServerValue(const SignalPtr& signal, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + std::string getBrowseName() override; + + static opcua::OpcUaNodeId SampleTypeToOpcUaDataType(SampleType sampleType); + +protected: + opcua::OpcUaNodeId getTmsTypeId() override; + opcua::OpcUaNodeId getDataTypeId() override; + void addChildNodes() override; + void bindCallbacks() override; + +private: + SignalPtr signal; +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS + diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_variable.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_variable.h new file mode 100644 index 0000000..baa8968 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/objects/tms_server_variable.h @@ -0,0 +1,38 @@ +/* + * 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_TMS + +template +class TmsServerVariable : public TmsServerObjectBaseImpl +{ +public: + using Super = TmsServerObjectBaseImpl; + + TmsServerVariable(const CoreType& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext); + + opcua::OpcUaNodeId createNode(const opcua::OpcUaNodeId& parentNodeId) override; + + virtual opcua::OpcUaNodeId getDataTypeId(); + +protected: + virtual void configureVariableNodeAttributes(opcua::OpcUaObject& attr); +}; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h new file mode 100644 index 0000000..9435921 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server.h @@ -0,0 +1,50 @@ +/* + * 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 + +class TmsServer +{ +public: + TmsServer(const InstancePtr& instance); + TmsServer(const DevicePtr& device, const ContextPtr& context); + ~TmsServer(); + + void setOpcUaPort(uint16_t port); + void setOpcUaPath(const std::string& path); + void start(); + void stop(); + +protected: + DevicePtr device; + ContextPtr context; + std::unique_ptr tmsDevice; + std::shared_ptr tmsContext; + daq::opcua::OpcUaServerPtr server; + uint16_t opcUaPort = 4840; + std::string opcUaPath = "/"; + std::unordered_map registeredClientIds; +}; + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server_context.h b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server_context.h new file mode 100644 index 0000000..2275682 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/include/opcuatms_server/tms_server_context.h @@ -0,0 +1,45 @@ +/* + * Copyright 2022-2025 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +class TmsServerContext : public std::enable_shared_from_this +{ +public: + TmsServerContext(const ContextPtr& context, const DevicePtr& rootDevice); + ~TmsServerContext(); + void registerComponent(const ComponentPtr& component, TmsServerObject& obj); + DevicePtr getRootDevice(); + ComponentPtr findComponent(const std::string& globalId); + +private: + ContextPtr context; + DevicePtr rootDevice; + + std::unordered_map> idToObjMap; + void coreEventCallback(ComponentPtr& component, CoreEventArgsPtr& eventArgs); + std::string toRelativeGlobalId(const std::string& globalId); +}; + +using TmsServerContextPtr = std::shared_ptr; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt new file mode 100644 index 0000000..af433d7 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/CMakeLists.txt @@ -0,0 +1,105 @@ +set(LIB_NAME opcuatms_server) + +set(SRC_Cpp tms_server.cpp + tms_server_context.cpp +) + +set(SRC_PublicHeaders +) + +set(SRC_PrivateHeaders tms_server.h + tms_server_context.h +) + +# objects + +set(OBJECT_SRC_DIR "objects") + +set(SRC_Objects_Headers ${OBJECT_SRC_DIR}/tms_server_object.h + ${OBJECT_SRC_DIR}/tms_server_variable.h + ${OBJECT_SRC_DIR}/tms_server_signal.h + ${OBJECT_SRC_DIR}/tms_server_function_block.h + ${OBJECT_SRC_DIR}/tms_server_channel.h + ${OBJECT_SRC_DIR}/tms_server_input_port.h + ${OBJECT_SRC_DIR}/tms_server_property_object.h + ${OBJECT_SRC_DIR}/tms_server_device.h + ${OBJECT_SRC_DIR}/tms_server_folder.h + ${OBJECT_SRC_DIR}/tms_server_component.h + ${OBJECT_SRC_DIR}/tms_server_property.h + ${OBJECT_SRC_DIR}/tms_server_eval_value.h + ${OBJECT_SRC_DIR}/tms_server_value.h + ${OBJECT_SRC_DIR}/tms_server_analog_value.h + ${OBJECT_SRC_DIR}/tms_server_function_block_type.h + ${OBJECT_SRC_DIR}/tms_server_sync_component.h + ${OBJECT_SRC_DIR}/tms_server_sync_interface.h +) + +set(SRC_Objects ${OBJECT_SRC_DIR}/tms_server_object.cpp + ${OBJECT_SRC_DIR}/tms_server_variable.cpp + ${OBJECT_SRC_DIR}/tms_server_signal.cpp + ${OBJECT_SRC_DIR}/tms_server_function_block.cpp + ${OBJECT_SRC_DIR}/tms_server_channel.cpp + ${OBJECT_SRC_DIR}/tms_server_input_port.cpp + ${OBJECT_SRC_DIR}/tms_server_property_object.cpp + ${OBJECT_SRC_DIR}/tms_server_device.cpp + ${OBJECT_SRC_DIR}/tms_server_folder.cpp + ${OBJECT_SRC_DIR}/tms_server_component.cpp + ${OBJECT_SRC_DIR}/tms_server_property.cpp + ${OBJECT_SRC_DIR}/tms_server_eval_value.cpp + ${OBJECT_SRC_DIR}/tms_server_value.cpp + ${OBJECT_SRC_DIR}/tms_server_analog_value.cpp + ${OBJECT_SRC_DIR}/tms_server_function_block_type.cpp + ${OBJECT_SRC_DIR}/tms_server_sync_component.cpp +) + +set(SRC_PublicHeaders ${SRC_PublicHeaders} ${SRC_Objects_Headers}) +set(SRC_Cpp ${SRC_Cpp} ${SRC_Objects}) + +source_group("objects\\server_object" "${OBJECT_SRC_DIR}/tms_server_object.*") +source_group("objects\\server_variable" "${OBJECT_SRC_DIR}/tms_server_variable.*") +source_group("objects\\signal" "${OBJECT_SRC_DIR}/tms_server_signal.*") +source_group("objects\\function_block" "${OBJECT_SRC_DIR}/tms_server_function_block.*") +source_group("objects\\channel" "${OBJECT_SRC_DIR}/tms_server_channel.*") +source_group("objects\\input_port" "${OBJECT_SRC_DIR}/tms_server_input_port.*") +source_group("objects\\property_object" "${OBJECT_SRC_DIR}/tms_server_property_object.*") +source_group("objects\\device" "${OBJECT_SRC_DIR}/tms_server_device.*") +source_group("objects\\folder" "${OBJECT_SRC_DIR}/tms_server_folder.*") +source_group("objects\\component" "${OBJECT_SRC_DIR}/tms_server_component.*") +source_group("objects\\property" "${OBJECT_SRC_DIR}/tms_server_property.*") +source_group("objects\\eval_value" "${OBJECT_SRC_DIR}/tms_server_eval_value.*") +source_group("objects\\value" "${OBJECT_SRC_DIR}/tms_server_value.*") +source_group("objects\\analog_value" "${OBJECT_SRC_DIR}/tms_server_analog_value.*") +source_group("objects\\sync_component" "${OBJECT_SRC_DIR}/tms_server_sync_component.*") + +# /objects + +prepend_include(${LIB_NAME} SRC_PrivateHeaders) +prepend_include(${LIB_NAME} SRC_PublicHeaders) + + +add_library(${LIB_NAME} STATIC ${SRC_Cpp} + ${SRC_PublicHeaders} + ${SRC_PrivateHeaders} +) + +add_library(${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 daq::opcuatms + daq::opcuaserver +) + +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/opcuatms/opcuatms_server/src/objects/tms_server_analog_value.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_analog_value.cpp new file mode 100644 index 0000000..df5d76b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_analog_value.cpp @@ -0,0 +1,110 @@ +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerAnalogValue::TmsServerAnalogValue(const SignalPtr& signal, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(BaseObjectPtr(), server, context, tmsContext) + , signal(signal) +{ +} + +std::string TmsServerAnalogValue::getBrowseName() +{ + return "AnalogValue"; +} + +opcua::OpcUaNodeId TmsServerAnalogValue::getTmsTypeId() +{ + // Return the base data variable type definition + // The actual concrete data type is set via getDataTypeId() + return OpcUaNodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE); +} + +opcua::OpcUaNodeId TmsServerAnalogValue::getDataTypeId() +{ + try + { + const auto descriptor = signal.getDescriptor(); + if (descriptor.assigned()) + { + SampleType sampleType = descriptor.getSampleType(); + return sampleTypeToOpcUaDataType(sampleType); + } + } + catch (...) + { + // If descriptor is not available or any error occurs, return null + // which will use the default type from the type definition + } + + return {}; +} + +opcua::OpcUaNodeId TmsServerAnalogValue::sampleTypeToOpcUaDataType(SampleType sampleType) +{ + switch (sampleType) + { + case SampleType::Float32: + return OpcUaNodeId(0, UA_NS0ID_FLOAT); + case SampleType::Float64: + return OpcUaNodeId(0, UA_NS0ID_DOUBLE); + case SampleType::Int8: + return OpcUaNodeId(0, UA_NS0ID_SBYTE); + case SampleType::UInt8: + return OpcUaNodeId(0, UA_NS0ID_BYTE); + case SampleType::Int16: + return OpcUaNodeId(0, UA_NS0ID_INT16); + case SampleType::UInt16: + return OpcUaNodeId(0, UA_NS0ID_UINT16); + case SampleType::Int32: + return OpcUaNodeId(0, UA_NS0ID_INT32); + case SampleType::UInt32: + return OpcUaNodeId(0, UA_NS0ID_UINT32); + case SampleType::Int64: + return OpcUaNodeId(0, UA_NS0ID_INT64); + case SampleType::UInt64: + return OpcUaNodeId(0, UA_NS0ID_UINT64); + case SampleType::RangeInt64: + return OpcUaNodeId(0, UA_NS0ID_RANGE); + case SampleType::ComplexFloat32: + return OpcUaNodeId(0, UA_NS0ID_COMPLEXNUMBERTYPE); + case SampleType::ComplexFloat64: + return OpcUaNodeId(0, UA_NS0ID_DOUBLECOMPLEXNUMBERTYPE); + default: + return OpcUaNodeId(); + } +} + +void TmsServerAnalogValue::bindCallbacks() +{ + // The data type is already set correctly during node creation via getDataTypeId() + // in configureVariableNodeAttributes() + + addReadCallback(nodeId, [this]() + { + const auto descriptor = signal.getDescriptor(); + SampleType type = descriptor.assigned() ? descriptor.getSampleType() : SampleType::Undefined; + + if (type != SampleType::Float32 && type != SampleType::Float64 && type != SampleType::Int8 && + type != SampleType::Int16 && type != SampleType::Int32 && type != SampleType::Int64 && + type != SampleType::UInt8 && type != SampleType::UInt16 && type != SampleType::UInt32 && + type != SampleType::UInt64 && type != SampleType::RangeInt64 && type != SampleType::ComplexFloat32 && + type != SampleType::ComplexFloat64) + return OpcUaVariant(); + + ObjectPtr lastValue = signal.getLastValue(); + if (lastValue != nullptr) + return VariantConverter::ToVariant(lastValue, nullptr, daqContext); + + return OpcUaVariant(); + }); + + Super::bindCallbacks(); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS + diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_channel.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_channel.cpp new file mode 100644 index 0000000..50e73f5 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_channel.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerChannel::TmsServerChannel(const ChannelPtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +OpcUaNodeId TmsServerChannel::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_CHANNELTYPE); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_component.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_component.cpp new file mode 100644 index 0000000..e69de29 diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_device.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_device.cpp new file mode 100644 index 0000000..e7ad30a --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_device.cpp @@ -0,0 +1,600 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +namespace detail +{ + static OpcUaVariant createLocalizedTextVariant(ConstCharPtr text) + { + OpcUaVariant v; + OpcUaObject localizedText = UA_LOCALIZEDTEXT_ALLOC("", text); + v.setScalar(std::move(localizedText.getValue())); + return v; + } + + static std::unordered_map componentFieldToDeviceInfo = { + {"AssetId", "assetId"}, + {"ComponentName", "name"}, + {"DeviceClass", "deviceClass"}, + {"DeviceManual", "deviceManual"}, + {"DeviceRevision", "deviceRevision"}, + {"HardwareRevision", "hardwareRevision"}, + {"Manufacturer", "manufacturer"}, + {"ManufacturerUri", "manufacturerUri"}, + {"Model", "model"}, + {"ProductCode", "productCode"}, + {"ProductInstanceUri", "productInstanceUri"}, + {"RevisionCounter", "revisionCounter"}, + {"SerialNumber", "serialNumber"}, + {"SoftwareRevision", "softwareRevision"}, + {"MacAddress", "macAddress"}, + {"ParentMacAddress", "parentMacAddress"}, + {"Platform", "platform"}, + {"Position", "position"}, + {"SystemType", "systemType"}, + {"SystemUUID", "systemUuid"}, + {"OpenDaqPackageVersion", "sdkVersion"}, + }; + + static std::unordered_map> componentFieldToVariant = { + {"ComponentName", [](const DeviceInfoPtr& info) { return createLocalizedTextVariant(info.getName().getCharPtr()); }}, + {"Manufacturer", [](const DeviceInfoPtr& info) { return createLocalizedTextVariant(info.getManufacturer().getCharPtr()); }}, + {"Model", [](const DeviceInfoPtr& info) { return createLocalizedTextVariant(info.getModel().getCharPtr()); }}, + {"RevisionCounter", [](const DeviceInfoPtr& info) { return OpcUaVariant{static_cast(info.getRevisionCounter())}; }}, + {"Position", [](const DeviceInfoPtr& info) { return OpcUaVariant{static_cast(info.getPosition())}; }}, + }; +} + +TmsServerDevice::TmsServerDevice(const DevicePtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +OpcUaNodeId TmsServerDevice::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQDEVICETYPE); +} + +bool TmsServerDevice::createOptionalNode(const OpcUaNodeId& nodeId) +{ + const auto name = server->readBrowseNameString(nodeId); + + if (name == "AssetId" && object.getInfo().getAssetId() != "") + return true; + if (name == "ComponentName" && object.getInfo().getName() != "") + return true; + if (name == "DeviceClass" && object.getInfo().getDeviceClass() != "") + return true; + if (name == "ManufacturerUri" && object.getInfo().getManufacturerUri() != "") + return true; + if (name == "ProductCode" && object.getInfo().getProductCode() != "") + return true; + if (name == "ProductInstanceUri" && object.getInfo().getProductInstanceUri() != "") + return true; + if (name == "Synchronization" && object.getSyncComponent().assigned()) + return true; + + return Super::createOptionalNode(nodeId); +} + +void TmsServerDevice::bindCallbacks() +{ + this->addReadCallback("Domain", [this] + { + const auto deviceDomain = object.getDomain(); + if (!deviceDomain.assigned()) + return OpcUaVariant{}; + + try + { + OpcUaObject uaDeviceDomain; + uaDeviceDomain->resolution.numerator = deviceDomain.getTickResolution().getNumerator(); + uaDeviceDomain->resolution.denominator = deviceDomain.getTickResolution().getDenominator(); + uaDeviceDomain->origin = ConvertToOpcUaString(deviceDomain.getOrigin()).getDetachedValue(); + uaDeviceDomain->ticksSinceOrigin = object.getTicksSinceOrigin(); + auto unit = StructConverter::ToTmsType(deviceDomain.getUnit()); + uaDeviceDomain->unit = unit.getDetachedValue(); + + OpcUaVariant v; + v.setScalar(*uaDeviceDomain); + return v; + } + catch (...) + { + return OpcUaVariant{}; + } + }); + + Super::bindCallbacks(); +} + +void TmsServerDevice::populateDeviceInfo() +{ + auto deviceInfo = object.getInfo(); + + auto createNode = [this](std::string name, CoreType type) + { + OpcUaNodeId newNodeId(0); + AddVariableNodeParams params(newNodeId, nodeId); + params.setBrowseName(name); + switch (type) + { + case ctBool: + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_BOOLEAN].typeId)); + break; + case ctInt: + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_INT64].typeId)); + break; + case ctFloat: + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_DOUBLE].typeId)); + break; + case ctString: + params.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_STRING].typeId)); + break; + default: + throw; + } + + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + server->addVariableNode(params); + }; + + createNode("OpenDaqPackageVersion", ctString); + + std::unordered_set customInfoNamesSet; + for (auto propName : deviceInfo.getCustomInfoPropertyNames()) + { + try + { + auto prop = deviceInfo.getProperty(propName); + createNode(propName, prop.getValueType()); + customInfoNamesSet.insert(propName); + } + catch(...) + { + } + } + + OpcUaObject bd; + bd->nodeId = nodeId.copyAndGetDetachedValue(); + bd->resultMask = UA_BROWSERESULTMASK_ALL; + auto result = server->browse(bd); + + for (size_t i = 0; i < result->referencesSize; i++) + { + const auto& reference = result->references[i]; + std::string browseName = opcua::utils::ToStdString(reference.browseName.name); + + std::string propName; + if (const auto it = detail::componentFieldToDeviceInfo.find(browseName); it != detail::componentFieldToDeviceInfo.end()) + propName = it->second; + else if (customInfoNamesSet.count(browseName)) + propName = browseName; + else + continue; + + const auto & nodeId = reference.nodeId.nodeId; + const auto prop = deviceInfo.getProperty(propName); + + if (prop.getReadOnly()) + { + server->setAccessLevel(nodeId, UA_ACCESSLEVELMASK_READ); + OpcUaVariant value; + if (const auto it = detail::componentFieldToVariant.find(browseName); it != detail::componentFieldToVariant.end()) + { + value = it->second(deviceInfo); + } + else + { + const auto daqValue = deviceInfo.getPropertyValue(propName); + value = VariantConverter::ToVariant(daqValue, nullptr, daqContext); + } + server->writeValue(nodeId, *value); + continue; + } + + server->setAccessLevel(nodeId, UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE); + + if (const auto it = detail::componentFieldToVariant.find(browseName); it != detail::componentFieldToVariant.end()) + this->addReadCallback(nodeId, std::bind(it->second, deviceInfo)); + else + this->addReadCallback(nodeId, [this, name = propName] + { + const auto value = object.getInfo().getPropertyValue(name); + return VariantConverter::ToVariant(value, nullptr, daqContext); + }); + + this->addWriteCallback(nodeId, [this, name = propName](const OpcUaVariant& variant) + { + const auto value = VariantConverter::ToDaqObject(variant, daqContext); + this->object.getInfo().setPropertyValue(name, value); + return UA_STATUSCODE_GOOD; + }); + } + + std::map deviceInfoFieldsMap = + { + {"userName", "UserName"}, + {"location", "Location"} + }; + + for (const auto& [propName, browseName] : deviceInfoFieldsMap) + { + const auto& prop = deviceInfo.getProperty(propName); + const auto nodeId = getChildNodeId(browseName); + auto tmsProperty = std::make_shared(prop, server, daqContext, tmsContext, browseName); + tmsProperty->registerToExistingOpcUaNode(nodeId); + deviceInfoProperties.push_back(tmsProperty); + + this->addReadCallback(nodeId, [this, name = propName] + { + const auto value = object.getInfo().getPropertyValue(name); + return VariantConverter::ToVariant(value, nullptr, daqContext); + }); + + if (!prop.getReadOnly()) + { + this->addWriteCallback(nodeId, [this, name = propName](const OpcUaVariant& variant) + { + const auto value = VariantConverter::ToDaqObject(variant, daqContext); + this->object.getInfo().setPropertyValue(name, value); + return UA_STATUSCODE_GOOD; + }); + } + } + + { + const auto opModeOptions = ListProperty("OperationModeOptions", List(), false); + const auto tmsOpModeOptions = std::make_shared(opModeOptions, server, daqContext, tmsContext, "OperationModeOptions"); + tmsOpModeOptions->registerOpcUaNode(this->nodeId); + deviceInfoProperties.push_back(tmsOpModeOptions); + this->addReadCallback(tmsOpModeOptions->getNodeId(), [this] + { + auto opModes = List(); + for (const auto& opMode : object.getAvailableOperationModes()) + opModes.pushBack(OperationModeTypeToString(opMode)); + return VariantConverter::ToArrayVariant(opModes, nullptr, daqContext); + }); + + const auto opMode = StringProperty("OperationMode", "", false); + const auto tmsOpMode = std::make_shared(opMode, server, daqContext, tmsContext, "OperationMode"); + tmsOpMode->registerOpcUaNode(this->nodeId); + deviceInfoProperties.push_back(tmsOpMode); + + this->addReadCallback(tmsOpMode->getNodeId(), [this] + { + const auto opMode = OperationModeTypeToString(object.getOperationMode()); + return VariantConverter::ToVariant(opMode, nullptr, daqContext); + }); + + this->addWriteCallback(tmsOpMode->getNodeId(), [this](const OpcUaVariant& variant) + { + const auto strValue = VariantConverter::ToDaqObject(variant).asPtr().toStdString(); + if (strValue.find("Recursive") == 0) + { + auto opMode = OperationModeTypeFromString(strValue.substr(9)); + this->object.setOperationModeRecursive(opMode); + } + else + { + auto opMode = OperationModeTypeFromString(strValue); + this->object.setOperationMode(opMode); + } + return UA_STATUSCODE_GOOD; + }); + } +} + +void TmsServerDevice::populateServerCapabilities() +{ + const auto deviceInfo = object.getInfo(); + if (deviceInfo == nullptr) + return; + + const PropertyObjectPtr serverCapabilitiesObj = deviceInfo.getPropertyValue("serverCapabilities"); + + auto tmsServerCapability = registerTmsObjectOrAddReference( + nodeId, serverCapabilitiesObj.asPtr(), numberInList++, "ServerCapabilities"); + this->serverCapabilities.push_back(std::move(tmsServerCapability)); +} + +void TmsServerDevice::addFunctionBlockFolderNodes() +{ + auto fbNodeId = getChildNodeId("FB"); + + createFunctionBlockTypesFolder(fbNodeId); + createAddFunctionBlockNode(fbNodeId); + createRemoveFunctionBlockNode(fbNodeId); +} + +void TmsServerDevice::createFunctionBlockTypesFolder(const OpcUaNodeId& parentId) +{ + OpcUaNodeId nodeIdOut; + AddObjectNodeParams params(nodeIdOut, parentId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.typeDefinition = OpcUaNodeId(UA_NS0ID_FOLDERTYPE); + params.setBrowseName("AvailableTypes"); + + const auto typesFolderId = server->addObjectNode(params); + const auto fbTypes = this->object.getAvailableFunctionBlockTypes().getValueList(); + + for (const auto& fbType : fbTypes) + { + auto tmsFbType = std::make_shared(fbType, server, daqContext, tmsContext); + tmsFbType->registerOpcUaNode(typesFolderId); + functionBlockTypes.push_back(tmsFbType); + } +} + +void TmsServerDevice::createAddFunctionBlockNode(const OpcUaNodeId& parentId) +{ + OpcUaNodeId nodeIdOut; + AddMethodNodeParams params(nodeIdOut, parentId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.setBrowseName("Add"); + params.outputArgumentsSize = 2; + params.outputArguments = (UA_Argument*) UA_Array_new(params.outputArgumentsSize, &UA_TYPES[UA_TYPES_ARGUMENT]); + params.inputArgumentsSize = 2; + params.inputArguments = (UA_Argument*) UA_Array_new(params.inputArgumentsSize, &UA_TYPES[UA_TYPES_ARGUMENT]); + + params.outputArguments[0].name = UA_STRING_ALLOC("nodeId"); + params.outputArguments[0].dataType = UA_TYPES[UA_TYPES_NODEID].typeId; + params.outputArguments[0].valueRank = UA_VALUERANK_SCALAR; + + params.outputArguments[1].name = UA_STRING_ALLOC("localId"); + params.outputArguments[1].dataType = UA_TYPES[UA_TYPES_STRING].typeId; + params.outputArguments[1].valueRank = UA_VALUERANK_SCALAR; + + params.inputArguments[0].name = UA_STRING_ALLOC("typeId"); + params.inputArguments[0].dataType = UA_TYPES[UA_TYPES_STRING].typeId; + params.inputArguments[0].valueRank = UA_VALUERANK_SCALAR; + + params.inputArguments[1].name = UA_STRING_ALLOC("config"); + params.inputArguments[1].dataType = UA_TYPES_DAQBT[UA_TYPES_DAQBT_DAQKEYVALUEPAIR].typeId; + params.inputArguments[1].valueRank = UA_VALUERANK_ONE_DIMENSION; + + auto methodNodeId = server->addMethodNode(params); + + auto callback = [this](NodeEventManager::MethodArgs args) -> UA_StatusCode + { + try + { + this->onAddFunctionBlock(args); + return UA_STATUSCODE_GOOD; + } + catch (const OpcUaException& e) + { + return e.getStatusCode(); + } + catch (...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + }; + + addEvent(methodNodeId)->onMethodCall(callback); +} + +void TmsServerDevice::createRemoveFunctionBlockNode(const OpcUaNodeId& parentId) +{ + OpcUaNodeId nodeIdOut; + AddMethodNodeParams params(nodeIdOut, parentId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.setBrowseName("Remove"); + params.inputArgumentsSize = 1; + params.inputArguments = (UA_Argument*) UA_Array_new(params.inputArgumentsSize, &UA_TYPES[UA_TYPES_ARGUMENT]); + + params.inputArguments[0].name = UA_STRING_ALLOC("localId"); + params.inputArguments[0].dataType = UA_TYPES[UA_TYPES_STRING].typeId; + params.inputArguments[0].valueRank = UA_VALUERANK_SCALAR; + + auto methodNodeId = server->addMethodNode(params); + + auto callback = [this](NodeEventManager::MethodArgs args) -> UA_StatusCode + { + try + { + this->onRemoveFunctionBlock(args); + return UA_STATUSCODE_GOOD; + } + catch (const OpcUaException& e) + { + return e.getStatusCode(); + } + catch (...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + }; + + addEvent(methodNodeId)->onMethodCall(callback); +} + +void TmsServerDevice::onGetAvailableFunctionBlockTypes(const NodeEventManager::MethodArgs& args) +{ + assert(args.outputSize == 1); + + const auto fbTypes = object.getAvailableFunctionBlockTypes().getValueList(); + const auto variant = ListConversionUtils::ToArrayVariant(fbTypes); + args.output[0] = variant.copyAndGetDetachedValue(); +} + +void TmsServerDevice::onAddFunctionBlock(const NodeEventManager::MethodArgs& args) +{ + assert(args.inputSize == 2); + assert(args.outputSize == 2); + + const auto fbTypeId = OpcUaVariant(args.input[0]).toString(); + const auto configVariant = OpcUaVariant(args.input[1]); + + auto tmsFunctionBlock = addFunctionBlock(fbTypeId, configVariant); + + auto nodeIdOut = OpcUaVariant(tmsFunctionBlock->getNodeId()); + auto localIdOut = OpcUaVariant(tmsFunctionBlock->getBrowseName().c_str()); + args.output[0] = nodeIdOut.copyAndGetDetachedValue(); + args.output[1] = localIdOut.copyAndGetDetachedValue(); +} + +void TmsServerDevice::onRemoveFunctionBlock(const NodeEventManager::MethodArgs& args) +{ + assert(args.inputSize == 1); + + const auto fbLocalId = OpcUaVariant(args.input[0]).toString(); + removeFunctionBlock(fbLocalId); +} + + +TmsServerFunctionBlockPtr TmsServerDevice::addFunctionBlock(const StringPtr& fbTypeId, const OpcUaVariant& configVariant) +{ + const auto fbTypes = object.getAvailableFunctionBlockTypes(); + + if (!fbTypes.hasKey(fbTypeId)) + throw OpcUaException(UA_STATUSCODE_BADNOTFOUND, "Function block type not found"); + + auto config = fbTypes.get(fbTypeId).createDefaultConfig(); + PropertyObjectConversionUtils::ToPropertyObject(configVariant, config); + return addFunctionBlock(fbTypeId, config); +} + +TmsServerFunctionBlockPtr TmsServerDevice::addFunctionBlock(const StringPtr& fbTypeId, const PropertyObjectPtr& config) +{ + const auto fbFolderNodeId = getChildNodeId("FB"); + + auto functionBlock = object.addFunctionBlock(fbTypeId, config); + auto tmsFunctionBlock = registerTmsObjectOrAddReference>(fbFolderNodeId, functionBlock, functionBlocks.size()); + functionBlocks.push_back(tmsFunctionBlock); + tmsFunctionBlock->createNonhierarchicalReferences(); + return tmsFunctionBlock; +} + +void TmsServerDevice::removeFunctionBlock(const StringPtr& localId) +{ + for (auto it = functionBlocks.begin(); it != functionBlocks.end(); ++it) + { + auto fb = *it; + + if (fb->getBrowseName() == localId) + { + server->deleteNode(fb->getNodeId()); + functionBlocks.erase(it); + break; + } + } + + const auto objFunctionBlocks = this->object.getFunctionBlocks(search::LocalId(localId)); + for (const auto& fb : objFunctionBlocks) + { + this->object.removeFunctionBlock(fb); + } +} + +void TmsServerDevice::addChildNodes() +{ + populateDeviceInfo(); + populateServerCapabilities(); + auto methodSetNodeId = getChildNodeId("MethodSet"); + tmsPropertyObject->setMethodParentNodeId(methodSetNodeId); + + uint32_t numberInList = 0; + for (const auto& device : object.getDevices(search::Any())) + { + auto tmsDevice = registerTmsObjectOrAddReference(nodeId, device, numberInList++); + devices.push_back(std::move(tmsDevice)); + } + + auto functionBlockNodeId = getChildNodeId("FB"); + assert(!functionBlockNodeId.isNull()); + numberInList = 0; + for (const auto& functionBlock : object.getFunctionBlocks(search::Any())) + { + auto tmsFunctionBlock = registerTmsObjectOrAddReference>(functionBlockNodeId, functionBlock, numberInList++); + functionBlocks.push_back(std::move(tmsFunctionBlock)); + } + + auto signalsNodeId = getChildNodeId("Sig"); + assert(!signalsNodeId.isNull()); + numberInList = 0; + for (const auto& signal : object.getSignals(search::Any())) + { + if (signal.getPublic()) + { + auto tmsSignal = registerTmsObjectOrAddReference(signalsNodeId, signal, numberInList++); + signals.push_back(std::move(tmsSignal)); + } + } + + auto inputsOutputsNodeId = getChildNodeId("IO"); + assert(!inputsOutputsNodeId.isNull()); + + auto topFolder = object.getInputsOutputsFolder(); + auto inputsOutputsNode = std::make_unique(topFolder, server, daqContext, tmsContext); + inputsOutputsNode->registerToExistingOpcUaNode(inputsOutputsNodeId); + folders.push_back(std::move(inputsOutputsNode)); + + auto syncComponentNodeId = getChildNodeId("Synchronization"); + assert(!syncComponentNodeId.isNull()); + auto syncComponent = object.getSyncComponent(); + auto syncComponentNode = std::make_unique(syncComponent, server, daqContext, tmsContext); + syncComponentNode->registerToExistingOpcUaNode(syncComponentNodeId); + syncComponents.push_back(std::move(syncComponentNode)); + + tmsPropertyObject->ignoredProps.emplace("userName"); + tmsPropertyObject->ignoredProps.emplace("location"); + + // TODO add "Srv" as a default node + + numberInList = 0; + for (auto component : object.getItems(search::Any())) + { + auto id = component.getLocalId(); + if (id == "Dev" || id == "FB" || id == "IO" || id == "Sig" || id == "Synchronization" || id == "Srv") + continue; + + if (component.supportsInterface()) + { + auto folderNode = registerTmsObjectOrAddReference(nodeId, component, numberInList++); + folders.push_back(std::move(folderNode)); + } + else + { + auto componentNode = registerTmsObjectOrAddReference>(nodeId, component, numberInList++); + components.push_back(std::move(componentNode)); + } + } + + addFunctionBlockFolderNodes(); + Super::addChildNodes(); +} + + +void TmsServerDevice::createNonhierarchicalReferences() +{ + createChildNonhierarchicalReferences(signals); + createChildNonhierarchicalReferences(devices); + createChildNonhierarchicalReferences(functionBlocks); + createChildNonhierarchicalReferences(folders); + createChildNonhierarchicalReferences(components); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS + diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_eval_value.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_eval_value.cpp new file mode 100644 index 0000000..c8fbbef --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_eval_value.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +// TmsServerEvalValue + +TmsServerEvalValue::TmsServerEvalValue(const EvalValuePtr& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(nullptr, server, context, tmsContext) +{ + this->readCallback = [this, object]() { return object; }; + this->writeCallback = [](const BaseObjectPtr& object) { return UA_STATUSCODE_BADNOTWRITABLE; }; +} + +TmsServerEvalValue::TmsServerEvalValue(const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : TmsServerEvalValue(nullptr, server, context, tmsContext) +{ +} + +std::string TmsServerEvalValue::getBrowseName() +{ + return "EvalValue"; +} + +void TmsServerEvalValue::setReadCallback(ReadCallback readCallback) +{ + this->readCallback = std::move(readCallback); +} + +void TmsServerEvalValue::setWriteCallback(WriteCallback writeCallback) +{ + this->writeCallback = writeCallback; +} + +void TmsServerEvalValue::setIsSelectionType(bool isSelection) +{ + this->isSelection = isSelection; +} + +opcua::OpcUaNodeId TmsServerEvalValue::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_EVALUATIONVARIABLETYPE); +} + +void TmsServerEvalValue::configureVariableNodeAttributes(opcua::OpcUaObject& attr) +{ + attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; +} + +void TmsServerEvalValue::bindCallbacks() +{ + addReadCallback(nodeId, [this]() { return readRoot(); }); + addWriteCallback(nodeId, [this](const OpcUaVariant& variant) -> UA_StatusCode { return writeRoot(variant); }); + addReadCallback("EvaluationExpression", [this]() { return readEvaluationExpression(); }); +} + +opcua::OpcUaVariant TmsServerEvalValue::readEvaluationExpression() +{ + const auto object = this->readCallback(); + if (!object.assigned()) + return OpcUaVariant(); + + try + { + const auto eval = object.asPtrOrNull(); + if (eval.assigned()) + return VariantConverter::ToVariant(eval.getEval()); + } + catch (const ConversionFailedException&) + { + } + + return OpcUaVariant(); +} + +opcua::OpcUaVariant TmsServerEvalValue::readRoot() +{ + const auto object = this->readCallback(); + if (!object.assigned()) + return OpcUaVariant(); + + try + { + if (isSelection) + { + const auto eval = object.asPtrOrNull(); + if (eval.assigned()) + return SelectionVariantConverter::ToVariant(eval.getResult()); + + return SelectionVariantConverter::ToVariant(object); + } + + const auto eval = object.asPtrOrNull(); + if (eval.assigned()) + return VariantConverter::ToVariant(eval.getResult(), nullptr, daqContext); + + return VariantConverter::ToVariant(object, nullptr, daqContext); + } + catch (const ConversionFailedException&) + { + return OpcUaVariant(); + } +} + +UA_StatusCode TmsServerEvalValue::writeRoot(const OpcUaVariant& variant) +{ + try + { + const auto object = VariantConverter::ToDaqObject(variant, daqContext); + return writeCallback(object); + } + catch (const ConversionFailedException&) + { + return UA_STATUSCODE_BADINVALIDARGUMENT; + } + + return UA_STATUSCODE_GOOD; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_folder.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_folder.cpp new file mode 100644 index 0000000..45f3388 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_folder.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerFolder::TmsServerFolder(const FolderPtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +void TmsServerFolder::addChildNodes() +{ + uint32_t numberInList = 0; + for (auto item : object.getItems(search::Any())) + { + auto folder = item.asPtrOrNull(); + auto channel = item.asPtrOrNull(); + auto component = item.asPtrOrNull(); + + if (channel.assigned()) + { + auto tmsChannel = registerTmsObjectOrAddReference(this->nodeId, channel, numberInList++); + channels.push_back(std::move(tmsChannel)); + } + else if (folder.assigned()) // It is important to test for folder last as a channel also is a folder! + { + auto tmsFolder = registerTmsObjectOrAddReference(this->nodeId, folder, numberInList++); + folders.push_back(std::move(tmsFolder)); + } + else if (component.assigned()) // It is important to test for component after folder! + { + auto tmsComponent = registerTmsObjectOrAddReference>(this->nodeId, component, numberInList++); + components.push_back(std::move(tmsComponent)); + } + else + { + DAQ_THROW_EXCEPTION(daq::NotImplementedException, "Unhandled item: " + item.getGlobalId()); + } + } + + Super::addChildNodes(); +} + +OpcUaNodeId TmsServerFolder::getTmsTypeId() +{ + if (object.supportsInterface()) + return OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_IOCOMPONENTTYPE); + return OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQCOMPONENTTYPE); +} + +void TmsServerFolder::createNonhierarchicalReferences() +{ + createChildNonhierarchicalReferences(channels); + createChildNonhierarchicalReferences(folders); +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_function_block.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_function_block.cpp new file mode 100644 index 0000000..2e90250 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_function_block.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +template +TmsServerFunctionBlock::TmsServerFunctionBlock(const FunctionBlockPtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +template +OpcUaNodeId TmsServerFunctionBlock::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_FUNCTIONBLOCKTYPE); +} + +template +bool TmsServerFunctionBlock::createOptionalNode(const OpcUaNodeId& nodeId) +{ + auto browseName = this->server->readBrowseName(nodeId); + auto create = browseName->name == "Sig" || browseName->name == "IP"; + return create || Super::createOptionalNode(nodeId); +} + +template +void TmsServerFunctionBlock::bindCallbacks() +{ + this->addReadCallback("FunctionBlockInfo", [this]() { + auto info = this->object.getFunctionBlockType(); + if (info != nullptr) + return VariantConverter::ToVariant(info); + else + return OpcUaVariant(); + }); + + Super::bindCallbacks(); +} + +template +void TmsServerFunctionBlock::addChildNodes() +{ + auto signalsNodeId = this->getChildNodeId("Sig"); + assert(!signalsNodeId.isNull()); + + uint32_t numberInList = 0; + for (const auto& signal : this->object.getSignals(search::Any())) + { + if (signal.getPublic()) + { + auto tmsSignal = this->template registerTmsObjectOrAddReference(signalsNodeId, signal, numberInList++); + signals.push_back(std::move(tmsSignal)); + } + } + + auto inputPortsNodeId = this->getChildNodeId("IP"); + assert(!inputPortsNodeId.isNull()); + + numberInList = 0; + for (const auto& inputPort : this->object.getInputPorts(search::Any())) + { + auto tmsInputPort = this->template registerTmsObjectOrAddReference(inputPortsNodeId, inputPort, numberInList++); + inputPorts.push_back(std::move(tmsInputPort)); + } + + numberInList = 0; + for (const auto& fb : this->object.getFunctionBlocks(search::Any())) + { + auto tmsFunctionBlock = this->template registerTmsObjectOrAddReference>(this->nodeId, fb, numberInList++); + functionBlocks.push_back(std::move(tmsFunctionBlock)); + } + + Super::addChildNodes(); +} + +template +void TmsServerFunctionBlock::createNonhierarchicalReferences() +{ + this->createChildNonhierarchicalReferences(signals); + this->createChildNonhierarchicalReferences(inputPorts); + this->createChildNonhierarchicalReferences(functionBlocks); +} + +// To force the compiler to generate the template classes that are used elsewhere +// If this is not done, then you will get linker errors if using it outside the static library +template class TmsServerFunctionBlock; +template class TmsServerFunctionBlock; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_function_block_type.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_function_block_type.cpp new file mode 100644 index 0000000..b78f13b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_function_block_type.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include + +using namespace daq::opcua; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +TmsServerFunctionBlockType::TmsServerFunctionBlockType(const FunctionBlockTypePtr& object, + const OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +std::string TmsServerFunctionBlockType::getBrowseName() +{ + return object.getId(); +} + +std::string TmsServerFunctionBlockType::getDisplayName() +{ + return object.getId(); +} + +std::string TmsServerFunctionBlockType::getDescription() +{ + return object.getDescription(); +} + +OpcUaNodeId TmsServerFunctionBlockType::getTmsTypeId() +{ + return OpcUaNodeId(UA_NS0ID_BASEDATAVARIABLETYPE); +} + +void TmsServerFunctionBlockType::addChildNodes() +{ + Super::addChildNodes(); + addDefaultConfigNode(); +} + +void TmsServerFunctionBlockType::configureVariableNodeAttributes(OpcUaObject& attr) +{ + Super::configureVariableNodeAttributes(attr); + + attr->dataType = UA_TYPES_DAQBSP[UA_TYPES_DAQBSP_FUNCTIONBLOCKINFOSTRUCTURE].typeId; + attr->accessLevel = UA_ACCESSLEVELMASK_READ; + attr->writeMask = 0; + + const auto defaultValue = VariantConverter::ToVariant(object); + attr->value = defaultValue.copyAndGetDetachedValue(); +} + +void TmsServerFunctionBlockType::addDefaultConfigNode() +{ + auto defaultConfig = object.createDefaultConfig(); + + if (!defaultConfig.assigned()) + return; + + defaultConfig.freeze(); + + tmsDefaultConfig = std::make_shared(defaultConfig, server, daqContext, tmsContext, "DefaultConfig"); + tmsDefaultConfig->registerOpcUaNode(nodeId); +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_input_port.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_input_port.cpp new file mode 100644 index 0000000..5eb6342 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_input_port.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerInputPort::TmsServerInputPort(const InputPortPtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +OpcUaNodeId TmsServerInputPort::getReferenceType() +{ + return OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASINPUTPORT); +} + +void TmsServerInputPort::addChildNodes() +{ + Super::addChildNodes(); + + createConnectMethodNode(); + createDisconnectMethodNode(); +} + +OpcUaNodeId TmsServerInputPort::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_INPUTPORTTYPE); +} + +void TmsServerInputPort::createConnectMethodNode() +{ + OpcUaNodeId nodeIdOut; + AddMethodNodeParams params(nodeIdOut, nodeId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.setBrowseName("Connect"); + params.inputArgumentsSize = 1; + params.inputArguments = (UA_Argument*) UA_Array_new(params.inputArgumentsSize, &UA_TYPES[UA_TYPES_ARGUMENT]); + + params.inputArguments[0].name = UA_STRING_ALLOC("signalGlobalId"); + params.inputArguments[0].dataType = UA_TYPES[UA_TYPES_STRING].typeId; + params.inputArguments[0].valueRank = UA_VALUERANK_SCALAR; + + auto methodNodeId = server->addMethodNode(params); + + auto callback = [this](NodeEventManager::MethodArgs args) -> UA_StatusCode + { + try + { + onConnectSignal(args); + } + catch (const OpcUaException& e) + { + return e.getStatusCode(); + } + catch (...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + + return UA_STATUSCODE_GOOD; + }; + + addEvent(methodNodeId)->onMethodCall(callback); +} + +void TmsServerInputPort::createDisconnectMethodNode() +{ + OpcUaNodeId nodeIdOut; + AddMethodNodeParams params(nodeIdOut, nodeId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.setBrowseName("Disconnect"); + params.inputArgumentsSize = 0; + + auto methodNodeId = server->addMethodNode(params); + + auto callback = [this](NodeEventManager::MethodArgs args) -> UA_StatusCode + { + try + { + onDisconenctSignal(args); + } + catch (const OpcUaException& e) + { + return e.getStatusCode(); + } + catch (...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + + return UA_STATUSCODE_GOOD; + }; + + addEvent(methodNodeId)->onMethodCall(callback); +} + +void TmsServerInputPort::onConnectSignal(NodeEventManager::MethodArgs args) +{ + assert(args.inputSize == 1); + + const auto globalId = OpcUaVariant(args.input[0]).toString(); + ComponentPtr signalComponent = tmsContext->findComponent(globalId); + SignalPtr signal = signalComponent.assigned() ? signalComponent.asPtrOrNull() : nullptr; + + if (!signal.assigned()) + throw OpcUaException(UA_STATUSCODE_BADNOTFOUND, "Signal not found"); + + const auto refType = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_CONNECTEDTOSIGNAL); + const auto sourceId = nodeId; + const auto signalNodeId = findSignalNodeId(signal); + + deleteReferencesOfType(refType); + addReference(signalNodeId, refType); + browseReferences(); + + object.connect(signal); +} + +void TmsServerInputPort::onDisconenctSignal(NodeEventManager::MethodArgs args) +{ + assert(args.inputSize == 0); + + const auto refType = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_CONNECTEDTOSIGNAL); + + deleteReferencesOfType(refType); + + object.disconnect(); +} + +void TmsServerInputPort::bindCallbacks() +{ + addReadCallback("RequiresSignal", [this]() { return VariantConverter::ToVariant(object.getRequiresSignal()); }); + + Super::bindCallbacks(); +} + +void TmsServerInputPort::createNonhierarchicalReferences() +{ + auto connectedSignal = object.getSignal(); + if (connectedSignal.assigned()) + { + auto connectedSignalNodeId = findSignalNodeId(connectedSignal); + if (!connectedSignalNodeId.isNull()) + addReference(connectedSignalNodeId, OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_CONNECTEDTOSIGNAL)); + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp new file mode 100644 index 0000000..201084c --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_object.cpp @@ -0,0 +1,324 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; +namespace opcua_utils = opcua::utils; + +TmsServerObject::TmsServerObject(const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : server(server) + , daqContext(context) + , numberInList(0) + , tmsContext(tmsContext) + , loggerComponent(daqContext.getLogger().assigned() + ? daqContext.getLogger().getOrAddComponent("OPCUAServerComponent") + : throw ArgumentNullException("Logger must not be null")) +{ +} + +TmsServerObject::~TmsServerObject() +{ +} + +std::string TmsServerObject::getBrowseName() +{ + return typeBrowseName; +} + +OpcUaNodeId TmsServerObject::getRequestedNodeId() +{ + return {}; +} + +std::string TmsServerObject::getDisplayName() +{ + return ""; +} + +std::string TmsServerObject::getDescription() +{ + return ""; +} + +OpcUaNodeId TmsServerObject::getReferenceType() +{ + return OpcUaNodeId(UA_NS0ID_HASCOMPONENT); +} + +OpcUaNodeId TmsServerObject::registerOpcUaNode(const OpcUaNodeId& parentNodeId) +{ + validate(); + this->nodeId = createNode(parentNodeId); + return registerToExistingOpcUaNode(this->nodeId); +} + +OpcUaNodeId TmsServerObject::registerToExistingOpcUaNode(const OpcUaNodeId& nodeId) +{ + if (this->nodeId.isNull()) + validate(); + + this->nodeId = nodeId; + browseReferences(); + addChildNodes(); + browseReferences(); + bindCallbacksInternal(); + bindCallbacks(); + registerToTmsServerContext(); + bindReadWriteCallbacks(); + return this->nodeId; +} + +OpcUaNodeId TmsServerObject::getNodeId() +{ + return nodeId; +} + +void TmsServerObject::setNumberInList(uint32_t numberInList) +{ + this->numberInList = numberInList; +} + +void TmsServerObject::addHierarchicalReference(const OpcUaNodeId& parent) +{ + server->addReference(parent, getReferenceType(), getNodeId(), true); +} + +void TmsServerObject::createNonhierarchicalReferences() +{ +} + +void TmsServerObject::onCoreEvent(const CoreEventArgsPtr& /*eventArgs*/) +{ +} + +NodeEventManagerPtr TmsServerObject::addEvent(const StringPtr& nodeName) +{ + auto nodeId = getChildNodeId(nodeName); + return addEvent(nodeId); +} + +NodeEventManagerPtr TmsServerObject::addEvent(const OpcUaNodeId& nodeId) +{ + if (eventManagers.count(nodeId) > 0) + return eventManagers[nodeId]; + + auto eventManager = std::make_shared(nodeId, server); + eventManagers.insert({nodeId, eventManager}); + return eventManager; +} + +void TmsServerObject::validate() +{ +} + +OpcUaNodeId TmsServerObject::createNode(const OpcUaNodeId& parentNodeId) +{ + OpcUaNodeId newNodeId; + auto typeNodeId = getTmsTypeId(); + + std::string browseName; + auto blueberryComponent = getObject().asPtrOrNull(true); + if (blueberryComponent.assigned()) + { + browseName = blueberryComponent.getLocalId().toStdString(); + } + else + { + typeBrowseName = readTypeBrowseName(); + browseName = getBrowseName(); + if (browseName.empty()) + browseName = typeBrowseName; + } + + // Frist try to set via getRequestedNodeId, because NodeID will also be string + // if parent is numeric. However object needs to be GenericComponentPtr or child of it. + auto params = AddObjectNodeParams(getRequestedNodeId(), parentNodeId); + + // If object is not GenericComponentPtr or child the id should be set to string if possible. + // Possible means if parent has a string nodeId. + if (params.requestedNewNodeId.isNull()) + params = AddObjectNodeParams(browseName, parentNodeId); + + configureNodeAttributes(params.attr); + params.referenceTypeId = getReferenceType(); + params.setBrowseName(browseName); + params.typeDefinition = typeNodeId; + params.nodeContext = this; + params.addOptionalNodeCallback = [this](const OpcUaNodeId& nodeId) { return this->createOptionalNode(nodeId); }; + newNodeId = server->addObjectNode(params); + + return OpcUaNodeId(newNodeId); +} + +void TmsServerObject::addChildNodes() +{ +} + +bool TmsServerObject::hasChildNode(const std::string& nodeName) const +{ + return references.count(nodeName) != 0; +} + +OpcUaNodeId TmsServerObject::getChildNodeId(const std::string& nodeName) +{ + return references[nodeName]->nodeId.nodeId; +} + +OpcUaNodeId TmsServerObject::findSignalNodeId(const SignalPtr& signal) const +{ + return findTmsObjectNodeId(signal); +} + +void TmsServerObject::bindCallbacksInternal() +{ + if (hasChildNode("NumberInList")) + this->addReadCallback("NumberInList", [this]() { return VariantConverter::ToVariant(numberInList); }); +} + +void TmsServerObject::bindCallbacks() +{ +} + +void TmsServerObject::registerToTmsServerContext() +{ +} + +void TmsServerObject::bindReadWriteCallbacks() +{ + for (const auto& entry : readCallbacks) + { + const OpcUaNodeId nodeId = entry.first; + auto readCallback = entry.second; + + addEvent(nodeId)->onDataSourceRead([this, readCallback](NodeEventManager::DataSourceReadArgs args) -> UA_StatusCode { + std::lock_guard lock(this->valueMutex); + try + { + auto& dataVelue = args.value; + dataVelue->hasServerTimestamp = UA_TRUE; + dataVelue->sourceTimestamp = getCurrentClock(); + OpcUaVariant variant = readCallback(); + dataVelue->value = variant.getDetachedValue(); + return UA_STATUSCODE_GOOD; + } + catch (...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + }); + } + + for (const auto& entry : writeCallbacks) + { + const OpcUaNodeId nodeId = entry.first; + auto writeCallback = entry.second; + + addEvent(nodeId)->onDataSourceWrite([this, writeCallback](NodeEventManager::DataSourceWriteArgs args) -> UA_StatusCode { + std::lock_guard lock(this->valueMutex); + try + { + auto variant = OpcUaVariant(std::move(args.value->value)); + return writeCallback(variant); + } + catch (...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + }); + } +} + +int64_t TmsServerObject::getCurrentClock() +{ + // later we will probably have a getClockProvider() method or something similar + return -1; +} + +std::string TmsServerObject::readTypeBrowseName() +{ + auto typeNodeId = getTmsTypeId(); + OpcUaObject browseName; + UA_Server_readBrowseName(server->getUaServer(), *typeNodeId, browseName.get()); + return opcua_utils::ToStdString(browseName->name); +} + +bool TmsServerObject::createOptionalNode(const OpcUaNodeId& nodeId) +{ + return true; +} + +void TmsServerObject::configureNodeAttributes(OpcUaObject& attr) +{ + attr->eventNotifier = UA_EVENTNOTIFIER_SUBSCRIBE_TO_EVENT; + auto displayName = getDisplayName(); + if (!displayName.empty()) + attr->displayName = UA_LOCALIZEDTEXT_ALLOC("", displayName.c_str()); + auto description = getDescription(); + if (!description.empty()) + attr->description = UA_LOCALIZEDTEXT_ALLOC("", description.c_str()); +} + +void TmsServerObject::addReadCallback(const std::string& nodeName, ReadVariantCallback readFunc) +{ + auto nodeId = getChildNodeId(nodeName); + readCallbacks.insert({nodeId, std::move(readFunc)}); +} + +void TmsServerObject::addWriteCallback(const std::string& nodeName, WriteVariantCallback writeFunc) +{ + auto nodeId = getChildNodeId(nodeName); + writeCallbacks.insert({nodeId, std::move(writeFunc)}); +} + +void TmsServerObject::addReadCallback(const OpcUaNodeId& nodeId, ReadVariantCallback readFunc) +{ + readCallbacks.insert({nodeId, std::move(readFunc)}); +} + +void TmsServerObject::addWriteCallback(const OpcUaNodeId& nodeId, WriteVariantCallback writeFunc) +{ + writeCallbacks.insert({nodeId, std::move(writeFunc)}); +} + +void TmsServerObject::addReference(const OpcUaNodeId& targetNodeId, const OpcUaNodeId& referenceTypeId) +{ + server->addReference(nodeId, referenceTypeId, targetNodeId, true); +} + +void TmsServerObject::deleteReferencesOfType(const opcua::OpcUaNodeId& referenceTypeId) +{ + browseReferences(); + + for (const auto& [browseName, ref] : references) + { + if (OpcUaNodeId(ref->referenceTypeId) == referenceTypeId) + server->deleteReference(nodeId, referenceTypeId, ref->nodeId.nodeId, ref->isForward); + } + + browseReferences(); +} + +void TmsServerObject::browseReferences() +{ + references.clear(); + + OpcUaObject bd; + bd->nodeId = nodeId.copyAndGetDetachedValue(); + bd->resultMask = UA_BROWSERESULTMASK_ALL; + auto result = server->browse(bd); + + for (size_t i = 0; i < result->referencesSize; i++) + { + auto reference = result->references[i]; + std::string browseName = opcua_utils::ToStdString(reference.browseName.name); + references.insert({browseName, OpcUaObject(reference)}); + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_property.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_property.cpp new file mode 100644 index 0000000..d4b1024 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_property.cpp @@ -0,0 +1,370 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerProperty::TmsServerProperty(const PropertyPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const std::string& browseName) + : Super(object, server, context, tmsContext) + , browseName(browseName) +{ + objectInternal = object.asPtr(false); + + if (isReferenceType()) + hideReferenceTypeChildren(); + if (isNumericType()) + hideNumericTypeChildren(); + if (isIntrospectionType()) + hideIntrospectionTypeChildren(); + if (isSelectionType()) + hideSelectionTypeChildren(); + if (isStructureType()) + hideStructureTypeChildren(); +} + +TmsServerProperty::TmsServerProperty(const PropertyPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const std::unordered_map& propOrder, + const std::string& browseName) + : TmsServerProperty(object, server, context, tmsContext, browseName) +{ + this->propOrder = propOrder; + this->numberInList = propOrder.at(object.getName()); +} + +TmsServerProperty::TmsServerProperty(const PropertyPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const PropertyObjectPtr& parent, + const std::unordered_map& propOrder, + const std::string& browseName) + : TmsServerProperty(object, server, context, tmsContext, propOrder, browseName) +{ + this->parent = parent; +} + +std::string TmsServerProperty::getPropertyName() +{ + return this->object.getName(); +} + +std::string TmsServerProperty::getBrowseName() +{ + return this->browseName.empty() ? this->object.getName().toStdString() : this->browseName; +} + +void TmsServerProperty::bindCallbacks() +{ + if (!HiddenNodes.count("CoercionExpression")) + { + addReadCallback("CoercionExpression", [this] { return VariantConverter::ToVariant(object.getCoercer().getEval()); }); + } + + if (!HiddenNodes.count("ValidationExpression")) + { + addReadCallback("ValidationExpression", [this] { return VariantConverter::ToVariant(object.getValidator().getEval()); }); + } + + for (auto childProp : childProperties) + { + auto name = childProp.second->getBrowseName(); + auto parentObj = this->parent.getRef(); + if (!parentObj.getProperty(name).asPtr().getReferencedPropertyUnresolved().assigned()) + { + addReadCallback(name, [this, name] + { + const auto value = this->parent.getRef().getPropertyValue(name); + return VariantConverter::ToVariant(value, nullptr, daqContext); + }); + + if (!parentObj.supportsInterface() || !parentObj.isFrozen()) + { + addWriteCallback(name, [this, name](const OpcUaVariant& variant) + { + const auto value = VariantConverter::ToDaqObject(variant, daqContext); + this->parent.getRef().setPropertyValue(name, value); + return UA_STATUSCODE_GOOD; + }); + } + } + else + { + addReadCallback(name, [this, name] + { + const auto refProp = this->parent.getRef().getProperty(name).asPtr().getReferencedPropertyUnresolved(); + return VariantConverter::ToVariant(refProp.getEval(), nullptr, daqContext); + }); + } + + } +} + +opcua::OpcUaNodeId TmsServerProperty::getTmsTypeId() +{ + if (objectInternal.getSelectionValuesUnresolved().assigned()) + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_SELECTIONVARIABLETYPE); + + if (objectInternal.getReferencedPropertyUnresolved().assigned()) + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_REFERENCEVARIABLETYPE); + + const auto type = object.getValueType(); + + switch (type) + { + case CoreType::ctInt: + case CoreType::ctFloat: + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_NUMERICVARIABLETYPE); + case CoreType::ctStruct: + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_STRUCTUREVARIABLETYPE); + case CoreType::ctEnumeration: + default: + break; + } + + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_INTROSPECTIONVARIABLETYPE); +} + +bool TmsServerProperty::createOptionalNode(const opcua::OpcUaNodeId& nodeId) +{ + const auto name = server->readBrowseNameString(nodeId); + + return HiddenNodes.count(name) == 0; +} + +void TmsServerProperty::addChildNodes() +{ + if (isReferenceType()) + { + addReferenceTypeChildNodes(); + return; + } + + addIntrospectionTypeChildNodes(); + if (isNumericType()) + addNumericTypeChildNodes(); + else if (isSelectionType()) + addSelectionTypeChildNodes(); +} + +void TmsServerProperty::configureVariableNodeAttributes(opcua::OpcUaObject& attr) +{ + Super::configureVariableNodeAttributes(attr); + + attr->writeMask = attr->writeMask & ~UA_WRITEMASK_DISPLAYNAME; + attr->writeMask = attr->writeMask & ~UA_WRITEMASK_DESCRIPTION; + + if (object.getDescription().assigned()) + attr->description = UA_LOCALIZEDTEXT_ALLOC("", object.getDescription().getCharPtr()); +} + +opcua::OpcUaNodeId TmsServerProperty::getDataTypeId() +{ + const auto objInternal = object.asPtr(); + if (objInternal.getReferencedPropertyUnresolved().assigned()) + return OpcUaNodeId(0, UA_NS0ID_STRING); + + const auto type = objInternal.getValueTypeUnresolved(); + switch (type) + { + case CoreType::ctBool: + return OpcUaNodeId(0, UA_NS0ID_BOOLEAN); + case CoreType::ctInt: + return OpcUaNodeId(0, UA_NS0ID_INT64); + case CoreType::ctFloat: + return OpcUaNodeId(0, UA_NS0ID_DOUBLE); + case CoreType::ctString: + return OpcUaNodeId(0, UA_NS0ID_STRING); + case CoreType::ctEnumeration: + { + EnumerationPtr enumPtr = this->parent.getRef().getPropertyValue(object.getName()); + std::string enumTypeName = enumPtr.getEnumerationType().getName(); + const auto DataType = GetUAEnumerationDataTypeByName(enumTypeName); + if (DataType != nullptr) + return DataType->typeId; + else + break; + } + case CoreType::ctStruct: + { + StructPtr structPtr = this->parent.getRef().getPropertyValue(object.getName()); + std::string structTypeName = structPtr.getStructType().getName(); + const auto DataType = GetUAStructureDataTypeByName(structTypeName); + if(DataType != nullptr) + return DataType->typeId; + else + break; + } + default: + break; + } + + return {}; +} + +void TmsServerProperty::validate() +{ +} + +void TmsServerProperty::registerEvalValueNode(const std::string& nodeName, TmsServerEvalValue::ReadCallback readCallback, bool isSelection) +{ + auto nodeId = getChildNodeId(nodeName); + auto serverObject = std::make_shared(server, daqContext, tmsContext); + serverObject->setReadCallback(std::move(readCallback)); + serverObject->setIsSelectionType(isSelection); + auto childNodeId = serverObject->registerToExistingOpcUaNode(nodeId); + + childObjects.insert({childNodeId, serverObject}); +} + +bool TmsServerProperty::isSelectionType() +{ + return getTmsTypeId() == OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_SELECTIONVARIABLETYPE); +} + +bool TmsServerProperty::isNumericType() +{ + return getTmsTypeId() == OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_NUMERICVARIABLETYPE); +} + +bool TmsServerProperty::isIntrospectionType() +{ + return getTmsTypeId() == OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_INTROSPECTIONVARIABLETYPE); +} + +bool TmsServerProperty::isReferenceType() +{ + return getTmsTypeId() == OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_REFERENCEVARIABLETYPE); +} + +bool TmsServerProperty::isStructureType() +{ + return getTmsTypeId() == OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_STRUCTUREVARIABLETYPE); +} + +void TmsServerProperty::hideReferenceTypeChildren() +{ + // TODO: Adjust model to reference type variables not having the IsVisible field + HiddenNodes.insert("IsVisible"); +} + +void TmsServerProperty::hideNumericTypeChildren() +{ + hideIntrospectionTypeChildren(); + + if (const auto pos = HiddenNodes.find("DefaultValue"); pos != HiddenNodes.cend()) + HiddenNodes.erase(pos); + if (!objectInternal.getMaxValueUnresolved().assigned()) + HiddenNodes.insert("MaxValue"); + if (!objectInternal.getMinValueUnresolved().assigned()) + HiddenNodes.insert("MinValue"); + if (!objectInternal.getSuggestedValuesUnresolved().assigned()) + HiddenNodes.insert("SuggestedValues"); +} + +void TmsServerProperty::hideSelectionTypeChildren() +{ + hideIntrospectionTypeChildren(); + + if (const auto pos = HiddenNodes.find("DefaultValue"); pos != HiddenNodes.cend()) + HiddenNodes.erase(pos); +} + +void TmsServerProperty::hideIntrospectionTypeChildren() +{ + if (!objectInternal.getVisibleUnresolved().assigned()) + HiddenNodes.insert("IsVisible"); + if (!objectInternal.getReadOnlyUnresolved().assigned()) + HiddenNodes.insert("IsReadOnly"); + if (!objectInternal.getDefaultValueUnresolved().assigned()) + HiddenNodes.insert("DefaultValue"); + if (!objectInternal.getUnitUnresolved().assigned()) + HiddenNodes.insert("Unit"); + if (!object.getValidator().assigned()) + HiddenNodes.insert("ValidationExpression"); + if (!object.getCoercer().assigned()) + HiddenNodes.insert("CoercionExpression"); +} + +void TmsServerProperty::hideStructureTypeChildren() +{ + // TODO: Add support for these + HiddenNodes.insert("FieldCoercionExpression"); + HiddenNodes.insert("FieldValidationExpression"); +} + +void TmsServerProperty::addReferenceTypeChildNodes() +{ + const auto type = object.getValueType(); + if (type == CoreType::ctStruct) + { + StructPtr structPtr = this->parent.getRef().getPropertyValue(object.getName()); + std::string structTypeName = structPtr.getStructType().getName(); + if (!nativeStructConversionSupported(structTypeName) && GetUAStructureDataTypeByName(structTypeName) == nullptr) + return; + } + + if (type == CoreType::ctEnumeration) + { + EnumerationPtr enumPtr = this->parent.getRef().getPropertyValue(object.getName()); + std::string enumTypeName = enumPtr.getEnumerationType().getName(); + if(GetUAEnumerationDataTypeByName(enumTypeName) == nullptr) + return; + } + + const auto refNames = objectInternal.getReferencedPropertyUnresolved().getPropertyReferences(); + for (auto propName : refNames) + { + auto prop = parent.getRef().getProperty(propName); + if (prop.getValueType() != ctObject) + { + auto serverInfo = registerTmsObjectOrAddReference(nodeId, prop, std::numeric_limits::max(), parent.getRef(), propOrder); + auto childNodeId = serverInfo->getNodeId(); + childProperties.insert({childNodeId, serverInfo}); + } + } +} + +void TmsServerProperty::addNumericTypeChildNodes() +{ + if (!HiddenNodes.count("MinValue")) + registerEvalValueNode("MinValue", [this] { return this->objectInternal.getMinValueUnresolved(); }); + if (!HiddenNodes.count("MaxValue")) + registerEvalValueNode("MaxValue", [this] { return this->objectInternal.getMaxValueUnresolved(); }); + if (!HiddenNodes.count("SuggestedValues")) + registerEvalValueNode("SuggestedValues", [this] { return this->objectInternal.getSuggestedValuesUnresolved(); }); +} + +void TmsServerProperty::addSelectionTypeChildNodes() +{ + registerEvalValueNode("SelectionValues", [this] { return this->objectInternal.getSelectionValuesUnresolved(); }, true); +} + +void TmsServerProperty::addIntrospectionTypeChildNodes() +{ + if (!HiddenNodes.count("IsReadOnly")) + registerEvalValueNode("IsReadOnly", [this] { return this->objectInternal.getReadOnlyUnresolved(); }); + if (!HiddenNodes.count("IsVisible")) + registerEvalValueNode("IsVisible", [this] { return this->objectInternal.getVisibleUnresolved(); }); + if (!HiddenNodes.count("DefaultValue")) + registerEvalValueNode("DefaultValue", [this] { return this->objectInternal.getDefaultValueUnresolved(); }); + if (!HiddenNodes.count("Unit")) + registerEvalValueNode("Unit", [this] { return this->objectInternal.getUnitUnresolved(); }); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_property_object.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_property_object.cpp new file mode 100644 index 0000000..4f4f9af --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_property_object.cpp @@ -0,0 +1,430 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerPropertyObject::TmsServerPropertyObject(const PropertyObjectPtr& object, + const OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const std::unordered_set& ignoredProps) + : Super(object, server, context, tmsContext) + , ignoredProps(ignoredProps) +{ + loggerComponent = daqContext.getLogger().getOrAddComponent("OPCUAServerPropertyObject"); +} + +TmsServerPropertyObject::TmsServerPropertyObject(const PropertyObjectPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const StringPtr& name) + : TmsServerPropertyObject(object, server, context, tmsContext) +{ + this->name = name; +} + +TmsServerPropertyObject::TmsServerPropertyObject(const PropertyObjectPtr& object, + const opcua::OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext, + const StringPtr& name, + const PropertyPtr& objProp) + : TmsServerPropertyObject(object, server, context, tmsContext, name) +{ + this->objProp = objProp; +} + +TmsServerPropertyObject::~TmsServerPropertyObject() +{ +} + +std::string TmsServerPropertyObject::getBrowseName() +{ + if (name.assigned()) + return name; + + const auto className = object.getClassName(); + return className != "" ? className : "PropertyObject"; +} + +OpcUaNodeId TmsServerPropertyObject::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQBT, UA_DAQBTID_VARIABLEBLOCKTYPE); +} + +void TmsServerPropertyObject::addChildNodes() +{ + if (objProp.assigned()) + { + if (objProp.getVisibleUnresolved().assigned()) + registerEvalValueNode("IsVisible", [this] { return objProp.getVisibleUnresolved(); }); + if (objProp.getReadOnlyUnresolved().assigned()) + registerEvalValueNode("IsReadOnly", [this] { return objProp.getReadOnlyUnresolved(); }); + } + + std::unordered_map propOrder; + for (const auto& prop : object.getAllProperties()) + { + if (ignoredProps.count(prop.getName())) + continue; + + propOrder.insert(std::pair(prop.getName(), numberInList++)); + } + + for (const auto& prop : object.getAllProperties()) + { + const auto propName = prop.getName(); + if (ignoredProps.count(propName)) + continue; + + // NOTE: ctObject types cannot be placed below ReferenceVariableType properties + if (prop.getValueType() != ctObject || prop.getReferencedProperty().assigned()) + { + if (prop.getIsReferenced()) + continue; + if (prop.getValueType() == ctFunc || prop.getValueType() == ctProc) + { + const auto args = prop.getCallableInfo().getArguments(); + bool isCompatibleArg = true; + for (SizeT i = 0; (args.assigned() && i < args.getCount()); i++) + { + const auto type = args.getItemAt(i).getType(); + // The OPC UA server does not yet support list or dictionary type arguments + isCompatibleArg = (type != ctList && type != ctDict); + if (!isCompatibleArg) + break; + } + + if (isCompatibleArg) + addMethodPropertyNode(prop, propOrder[propName]); + continue; + } + + //Don't add property if struct is not part of UA data types list + if(prop.getValueType() == ctStruct) + { + StructPtr structPtr = prop.getDefaultValue().asPtrOrNull(); + if(structPtr.assigned()) + { + std::string structTypeName = structPtr.getStructType().getName(); + if (!nativeStructConversionSupported(structTypeName) && GetUAStructureDataTypeByName(structTypeName) == nullptr) + continue; + } + } + + //Don't add property if enumeration is not part of UA data types list + if (prop.getValueType() == ctEnumeration) + { + EnumerationPtr enumPtr = prop.getDefaultValue().asPtrOrNull(); + if(enumPtr.assigned()) + { + std::string enumTypeName = enumPtr.getEnumerationType().getName(); + if(GetUAEnumerationDataTypeByName(enumTypeName) == nullptr) + continue; + } + } + + std::string browseName = propName; + if (auto it = propBrowseName.find(propName); it != propBrowseName.end()) + browseName = it->second; + + OpcUaNodeId childNodeId; + std::shared_ptr serverInfo; + if (hasChildNode(browseName)) + { + const auto tempId = getChildNodeId(browseName); + serverInfo = std::make_shared(prop, server, daqContext, tmsContext, object, propOrder, browseName); + childNodeId = serverInfo->registerToExistingOpcUaNode(tempId); + } + else + { + serverInfo = registerTmsObjectOrAddReference(nodeId, prop, std::numeric_limits::max(), object, propOrder, browseName); + childNodeId = serverInfo->getNodeId(); + } + + childProperties.insert({childNodeId, serverInfo}); + } + else + { + PropertyObjectPtr obj = object.getPropertyValue(propName); + if (hasChildNode(propName)) + { + const auto childNodeId = getChildNodeId(propName); + auto childObj = std::make_shared(obj, server, daqContext, tmsContext, propName, prop); + childObj->registerToExistingOpcUaNode(childNodeId); + childObjects.insert({childNodeId, childObj}); + } + else + { + auto serverInfo = registerTmsObjectOrAddReference(nodeId, obj, propOrder[propName], propName, prop); + auto childNodeId = serverInfo->getNodeId(); + childObjects.insert({childNodeId, serverInfo}); + } + } + } + + addBeginUpdateNode(); + addEndUpdateNode(); +} + +void TmsServerPropertyObject::bindCallbacks() +{ + for (const auto& [id, prop] : childProperties) + bindPropertyCallbacks(id, prop->getPropertyName()); + + bindMethodCallbacks(); +} + +bool TmsServerPropertyObject::createOptionalNode(const opcua::OpcUaNodeId& nodeId) +{ + const auto name = server->readBrowseNameString(nodeId); + + if (name == "" || name == "" || name == "") + return false; + + if (!objProp.assigned() && (name == "IsReadOnly" || name == "IsVisible")) + return false; + + return true; +} + +void TmsServerPropertyObject::addProperty(const TmsServerPropertyPtr& childProperty) +{ + childProperties.insert({childProperty->getNodeId(), childProperty}); + childProperty->setNumberInList(numberInList++); +} + +void TmsServerPropertyObject::bindPropertyCallbacks(const opcua::OpcUaNodeId& nodeId, const std::string& propName) +{ + if (!this->object.getProperty(propName).asPtr().getReferencedPropertyUnresolved().assigned()) + { + addReadCallback(nodeId, [this, propName] + { + const auto value = this->object.getPropertyValue(propName); + return VariantConverter::ToVariant(value, nullptr, daqContext); + }); + + const auto freezable = this->object.asPtrOrNull(); + if (!freezable.assigned() || !this->object.isFrozen()) + { + addWriteCallback(nodeId, [this, propName](const OpcUaVariant& variant) + { + const auto value = VariantConverter::ToDaqObject(variant, daqContext); + this->object.asPtr().setProtectedPropertyValue(propName, value); + return UA_STATUSCODE_GOOD; + }); + } + } + else + { + addReadCallback(nodeId, [this, propName] + { + const auto refProp = this->object.getProperty(propName).asPtr().getReferencedPropertyUnresolved(); + return VariantConverter::ToVariant(refProp.getEval(), nullptr, daqContext); + }); + } +} + +void TmsServerPropertyObject::setMethodParentNodeId(const OpcUaNodeId& methodParentNodeId) +{ + this->methodParentNodeId = methodParentNodeId; +} + +void TmsServerPropertyObject::registerEvalValueNode(const std::string& nodeName, TmsServerEvalValue::ReadCallback readCallback) +{ + auto nodeId = getChildNodeId(nodeName); + auto serverObject = std::make_shared(server, daqContext, tmsContext); + serverObject->setReadCallback(std::move(readCallback)); + auto childNodeId = serverObject->registerToExistingOpcUaNode(nodeId); + + childEvalValues.insert({childNodeId, serverObject}); +} + +// TODO: Procedure/Function properties with list/dictionary types are not yet supported over OPC UA! +void TmsServerPropertyObject::addMethodPropertyNode(const PropertyPtr& prop, uint32_t numberInList) +{ + const auto name = prop.getName(); + OpcUaNodeId parentId = methodParentNodeId.isNull() ? nodeId : methodParentNodeId; + AddMethodNodeParams params(name, parentId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + + const auto callableInfo = prop.getCallableInfo(); + const auto returnType = callableInfo.getReturnType(); + + try + { + if (returnType != ctUndefined) + { + auto outputArg = VariantConverter::ToVariant(ArgumentInfo("", returnType)); + CheckStatusCodeException(UA_Array_copy(outputArg->data, 1, (void**) ¶ms.outputArguments, GetUaDataType())); + params.outputArgumentsSize = 1; + } + + const auto args = callableInfo.getArguments(); + if (args.assigned() && args.getCount() > 0) + { + auto inputArg = VariantConverter::ToArrayVariant(args); + CheckStatusCodeException(UA_Array_copy(inputArg->data, args.getCount(), (void**) ¶ms.inputArguments, GetUaDataType())); + params.inputArgumentsSize = args.getCount(); + } + + params.setBrowseName(name); + auto methodNodeId = server->addMethodNode(params); + + OpcUaNodeId numberInListRequestedNodeId(0); + AddVariableNodeParams numberInListParams(numberInListRequestedNodeId, methodNodeId); + numberInListParams.setBrowseName("NumberInList"); + numberInListParams.setDataType(OpcUaNodeId(UA_TYPES[UA_TYPES_UINT32].typeId)); + + numberInListParams.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + const auto numberInListNodeId = server->addVariableNode(numberInListParams); + server->writeValue(numberInListNodeId, OpcUaVariant(numberInList)); + + methodProps.insert({methodNodeId, {name, prop.getValueType()}}); + } + catch(const std::exception& e) + { + LOG_W("Failed to add method property node {}: {}", name, e.what()); + } + catch (...) + { + LOG_W("Failed to add method property node {}.", name); + } +} + +void TmsServerPropertyObject::bindMethodCallbacks() +{ + for (const auto& [nodeId, props] : methodProps) + { + auto methodName = props.first; + auto coreType = props.second; + + // TODO: Return more specific errors + addEvent(nodeId)->onMethodCall( + [this, methodName, coreType](NodeEventManager::MethodArgs args) -> UA_StatusCode + { + try + { + const BaseObjectPtr method = this->object.getPropertyValue(methodName); + if (args.inputSize > 0) + { + BaseObjectPtr daqArg; + if (args.inputSize > 1) + { + daqArg = List(); + for (size_t i = 0; i < args.inputSize; ++i) + { + auto variant = daq::opcua::OpcUaVariant(args.input[i]); + daqArg.asPtr().pushBack(VariantConverter::ToDaqObject(variant, daqContext)); + } + } + else + { + auto variant = daq::opcua::OpcUaVariant(args.input[0]); + daqArg = VariantConverter::ToDaqObject(variant, daqContext); + } + + if (coreType == ctFunc && args.outputSize > 0) + args.output[0] = VariantConverter::ToVariant(method.asPtr()(daqArg), nullptr, daqContext) + .getDetachedValue(); + else + method.asPtr()(daqArg); + + return UA_STATUSCODE_GOOD; + } + + if (coreType == ctFunc && args.outputSize > 0) + args.output[0] = + VariantConverter::ToVariant(method.asPtr()(), nullptr, daqContext).getDetachedValue(); + else + method.asPtr()(); + + + return UA_STATUSCODE_GOOD; + } + catch(...) + { + return UA_STATUSCODE_BADINTERNALERROR; + } + }); + } +} + +void TmsServerPropertyObject::addBeginUpdateNode() +{ + OpcUaNodeId nodeIdOut; + AddMethodNodeParams params(nodeIdOut, nodeId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.setBrowseName("BeginUpdate"); + auto methodNodeId = server->addMethodNode(params); + + auto callback = [this](NodeEventManager::MethodArgs args) + { + const auto status = this->object->beginUpdate(); + return status == OPENDAQ_SUCCESS ? UA_STATUSCODE_GOOD : UA_STATUSCODE_BADINTERNALERROR; + }; + + addEvent(methodNodeId)->onMethodCall(callback); +} + +void TmsServerPropertyObject::addEndUpdateNode() +{ + OpcUaNodeId nodeIdOut; + AddMethodNodeParams params(nodeIdOut, nodeId); + params.referenceTypeId = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)); + params.setBrowseName("EndUpdate"); + auto methodNodeId = server->addMethodNode(params); + + auto callback = [this](NodeEventManager::MethodArgs args) + { + const auto status = this->object->endUpdate(); + return status == OPENDAQ_SUCCESS ? UA_STATUSCODE_GOOD : UA_STATUSCODE_BADINTERNALERROR; + }; + + addEvent(methodNodeId)->onMethodCall(callback); +} + +void TmsServerPropertyObject::addPropertyNode(const std::string& name, const opcua::OpcUaNodeId& parentId) +{ + auto params = AddVariableNodeParams(UA_NODEID_NULL, parentId); + params.setBrowseName(name); + params.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + params.attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + server->addVariableNode(params); +} + +void TmsServerPropertyObject::configureNodeAttributes(opcua::OpcUaObject& attr) +{ + Super::configureNodeAttributes(attr); + + attr->writeMask |= UA_WRITEMASK_DISPLAYNAME; +} + +void TmsServerPropertyObject::triggerEvent(PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args) +{ + if(!this->server->getUaServer()) + return; + + EventAttributes attributes; + attributes.setTime(UA_DateTime_now()); + attributes.setMessage("Property value changed"); + this->server->triggerEvent(OpcUaNodeId(UA_NS0ID_BASEEVENTTYPE), nodeId, attributes); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_signal.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_signal.cpp new file mode 100644 index 0000000..d9df509 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_signal.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerSignal::TmsServerSignal(const SignalPtr& object, + const OpcUaServerPtr& server, + const ContextPtr& context, + const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +OpcUaNodeId TmsServerSignal::getReferenceType() +{ + // TODO UA_DAQBSPID_HASSTATUSSIGNAL + return OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASVALUESIGNAL); +} + +OpcUaNodeId TmsServerSignal::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_SIGNALTYPE); +} + +void TmsServerSignal::addChildNodes() +{ + // Create Value and AnalogValue nodes manually with correct data type + // Store them as members to keep them alive (callbacks depend on them) + valueServer = std::make_shared(object, server, daqContext, tmsContext); + valueServer->registerOpcUaNode(nodeId); + + analogValueServer = std::make_shared(object, server, daqContext, tmsContext); + analogValueServer->registerOpcUaNode(nodeId); + + Super::addChildNodes(); +} + +void TmsServerSignal::onCoreEvent(const CoreEventArgsPtr& args) +{ + Super::onCoreEvent(args); + + if (args.getEventId() == static_cast(CoreEventId::DataDescriptorChanged)) + { + try + { + const auto descriptor = object.getDescriptor(); + if (!descriptor.assigned() || !valueServer) + return; + + const auto currentDataType = server->readDataType(valueServer->getNodeId()); + const auto expectedDataType = TmsServerValue::SampleTypeToOpcUaDataType(descriptor.getSampleType()); + + if (currentDataType == expectedDataType) + return; + + if (valueServer) + { + auto valueNodeId = valueServer->getNodeId(); + if (!valueNodeId.isNull()) + server->deleteNode(valueNodeId); + } + + if (analogValueServer) + { + auto analogValueNodeId = analogValueServer->getNodeId(); + if (!analogValueNodeId.isNull()) + server->deleteNode(analogValueNodeId); + } + + valueServer = std::make_shared(object, server, daqContext, tmsContext); + valueServer->registerOpcUaNode(nodeId); + + analogValueServer = std::make_shared(object, server, daqContext, tmsContext); + analogValueServer->registerOpcUaNode(nodeId); + } + catch (...) + { + } + } +} + +void TmsServerSignal::createNonhierarchicalReferences() +{ + auto domainSignal = object.getDomainSignal(); + if (domainSignal.assigned()) + { + auto domainSignalNodeId = findSignalNodeId(domainSignal); + if (!domainSignalNodeId.isNull()) + { + try + { + addReference(domainSignalNodeId, OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASDOMAINSIGNAL)); + } + catch (const OpcUaException& ex) + { + if (ex.getStatusCode() != UA_STATUSCODE_BADDUPLICATEREFERENCENOTALLOWED) + throw; + } + } + } +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_sync_component.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_sync_component.cpp new file mode 100644 index 0000000..6595ad2 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_sync_component.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +TmsServerSyncComponent::TmsServerSyncComponent(const SyncComponentPtr& object, const OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +void TmsServerSyncComponent::addChildNodes() +{ + if (object.hasProperty("Interfaces")) + { + const auto prop = object.getProperty("Interfaces"); + const auto propName = prop.getName(); + const auto obj = object.getPropertyValue(propName); + + auto intefacesNodeId = getChildNodeId("Interfaces"); + interfaces = std::make_shared(obj, server, daqContext, tmsContext, propName, prop); + interfaces->setNumberInList(0); + interfaces->registerToExistingOpcUaNode(intefacesNodeId); + } + + tmsPropertyObject->ignoredProps.emplace("Interfaces"); + tmsPropertyObject->ignoredProps.emplace("InterfaceNames"); + tmsPropertyObject->ignoredProps.emplace("SynchronizationLocked"); + + Super::addChildNodes(); +} + +OpcUaNodeId TmsServerSyncComponent::getTmsTypeId() +{ + return OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_SYNCCOMPONENTTYPE); +} + +void TmsServerSyncComponent::triggerEvent(PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args) +{ + if(!this->server->getUaServer()) + return; + + EventAttributes attributes; + attributes.setTime(UA_DateTime_now()); + attributes.setMessage("Property value changed"); + this->server->triggerEvent(OpcUaNodeId(UA_NS0ID_BASEEVENTTYPE), nodeId, attributes); +} + +void TmsServerSyncComponent::bindCallbacks() +{ + // Bind read callback for SynchronizationLocked property + this->addReadCallback("SynchronizationLocked", + [this] { return VariantConverter::ToVariant( this->object.getSyncLocked()); }); + + Super::bindCallbacks(); +} + + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_value.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_value.cpp new file mode 100644 index 0000000..cd3a811 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_value.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include "opendaq/custom_log.h" + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace opcua; + +TmsServerValue::TmsServerValue(const SignalPtr& signal, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(BaseObjectPtr(), server, context, tmsContext) + , signal(signal) +{ +} + +std::string TmsServerValue::getBrowseName() +{ + return "Value"; +} + +opcua::OpcUaNodeId TmsServerValue::getTmsTypeId() +{ + // Return the base data variable type definition + // The actual concrete data type is set via getDataTypeId() + return OpcUaNodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE); +} + +opcua::OpcUaNodeId TmsServerValue::getDataTypeId() +{ + try + { + const auto descriptor = signal.getDescriptor(); + if (descriptor.assigned()) + { + SampleType sampleType = descriptor.getSampleType(); + return SampleTypeToOpcUaDataType(sampleType); + } + } + catch (...) + { + // If descriptor is not available or any error occurs, return null + // which will use the default type from the type definition + } + + return {}; +} + +opcua::OpcUaNodeId TmsServerValue::SampleTypeToOpcUaDataType(SampleType sampleType) +{ + switch (sampleType) + { + case SampleType::Float32: + return OpcUaNodeId(0, UA_NS0ID_FLOAT); + case SampleType::Float64: + return OpcUaNodeId(0, UA_NS0ID_DOUBLE); + case SampleType::Int8: + return OpcUaNodeId(0, UA_NS0ID_SBYTE); + case SampleType::UInt8: + return OpcUaNodeId(0, UA_NS0ID_BYTE); + case SampleType::Int16: + return OpcUaNodeId(0, UA_NS0ID_INT16); + case SampleType::UInt16: + return OpcUaNodeId(0, UA_NS0ID_UINT16); + case SampleType::Int32: + return OpcUaNodeId(0, UA_NS0ID_INT32); + case SampleType::UInt32: + return OpcUaNodeId(0, UA_NS0ID_UINT32); + case SampleType::Int64: + return OpcUaNodeId(0, UA_NS0ID_INT64); + case SampleType::UInt64: + return OpcUaNodeId(0, UA_NS0ID_UINT64); + case SampleType::RangeInt64: + return OpcUaNodeId(0, UA_NS0ID_RANGE); + case SampleType::ComplexFloat32: + return OpcUaNodeId(0, UA_NS0ID_COMPLEXNUMBERTYPE); + case SampleType::ComplexFloat64: + return OpcUaNodeId(0, UA_NS0ID_DOUBLECOMPLEXNUMBERTYPE); + case SampleType::String: + return OpcUaNodeId(0, UA_NS0ID_STRING); + case SampleType::Binary: + return OpcUaNodeId(0, UA_NS0ID_BYTESTRING); + default: + return OpcUaNodeId(); + } +} +void TmsServerValue::addChildNodes() +{ + try + { + auto params = AddVariableNodeParams("DataDescriptor", nodeId); + params.setBrowseName("DataDescriptor"); + params.setDataType(OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_DATADESCRIPTORSTRUCTURE)); + params.typeDefinition = OpcUaNodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE); + params.referenceTypeId = OpcUaNodeId(UA_NS0ID_HASPROPERTY); + + server->addVariableNode(params); + } + catch (const std::exception&) + { + const auto loggerComponent = this->daqContext.getLogger().getOrAddComponent("OpenDAQOPCUAServerModule"); + LOG_D("OPC UA Value {} failed create data descriptor node.", this->signal.getGlobalId()); + } + + Super::addChildNodes(); +} + +void TmsServerValue::bindCallbacks() +{ + addReadCallback(nodeId, [this]() + { + ObjectPtr lastValue = signal.getLastValue(); + if (lastValue != nullptr) + return VariantConverter::ToVariant(lastValue, nullptr, daqContext); + + return OpcUaVariant(); + }); + + addReadCallback("DataDescriptor", [this]() + { + DataDescriptorPtr descriptor = signal.getDescriptor(); + if (descriptor.assigned()) + return VariantConverter::ToVariant(descriptor, nullptr, daqContext); + else + return OpcUaVariant(); + }); + + Super::bindCallbacks(); +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS + diff --git a/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_variable.cpp b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_variable.cpp new file mode 100644 index 0000000..e0a1571 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/objects/tms_server_variable.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +using namespace daq::opcua; + +template +TmsServerVariable::TmsServerVariable(const CoreType& object, const opcua::OpcUaServerPtr& server, const ContextPtr& context, const TmsServerContextPtr& tmsContext) + : Super(object, server, context, tmsContext) +{ +} + +template +opcua::OpcUaNodeId TmsServerVariable::createNode(const opcua::OpcUaNodeId& parentNodeId) +{ + OpcUaNodeId newNodeId; + + this->typeBrowseName = this->readTypeBrowseName(); + std::string name = this->getBrowseName(); + + auto params = AddVariableNodeParams(name, parentNodeId); + configureVariableNodeAttributes(params.attr); + params.setBrowseName(name); + params.typeDefinition = this->getTmsTypeId(); + params.nodeContext = this; + params.addOptionalNodeCallback = [this](const OpcUaNodeId& nodeId) { return this->createOptionalNode(nodeId); }; + newNodeId = this->server->addVariableNode(params); + + return OpcUaNodeId(newNodeId); +} + +template +opcua::OpcUaNodeId TmsServerVariable::getDataTypeId() +{ + return {}; +} + +template +void TmsServerVariable::configureVariableNodeAttributes(opcua::OpcUaObject& attr) +{ + const auto dataType = this->getDataTypeId(); + if (!dataType.isNull()) + { + attr->dataType = dataType.getValue(); + if (attr->dataType.identifier.numeric == UA_NS0ID_INT64) + { + UA_Int64 var = 0; + UA_Variant_setScalarCopy(&attr->value, &var, &UA_TYPES[UA_TYPES_INT64]); + } + else if (attr->dataType.identifier.numeric == UA_NS0ID_DOUBLE) + { + UA_Double var = 0; + UA_Variant_setScalarCopy(&attr->value, &var, &UA_TYPES[UA_TYPES_DOUBLE]); + } + } + else + { + const auto tmsTypeId = this->getTmsTypeId(); + const auto dataTypeId = this->server->readDataType(tmsTypeId); + attr->dataType = *dataTypeId; + } + attr->accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + attr->writeMask |= UA_WRITEMASK_DISPLAYNAME | UA_WRITEMASK_DESCRIPTION; +} + +template class TmsServerVariable>; +template class TmsServerVariable; +template class TmsServerVariable; +template class TmsServerVariable; +template class TmsServerVariable; + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp new file mode 100644 index 0000000..64f99f8 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/tms_server.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace daq::opcua; +using namespace daq::opcua::tms; + +BEGIN_NAMESPACE_OPENDAQ_OPCUA + +TmsServer::~TmsServer() +{ + tmsContext = nullptr; + stop(); +} + +TmsServer::TmsServer(const InstancePtr& instance) + : TmsServer(instance.getRootDevice(), instance.getContext()) +{ +} + +TmsServer::TmsServer(const DevicePtr& device, const ContextPtr& context) + : device(device) + , context(context) +{ +} + +void TmsServer::setOpcUaPort(uint16_t port) +{ + this->opcUaPort = port; +} + +void TmsServer::setOpcUaPath(const std::string& path) +{ + this->opcUaPath = path; +} + +void TmsServer::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(); + if (info.hasServerCapability("OpenDAQOPCUAConfiguration")) + DAQ_THROW_EXCEPTION(InvalidStateException, fmt::format("Device \"{}\" already has an OpenDAQOPCUAConfiguration server capability.", info.getName())); + + server = std::make_shared(); + server->setPort(opcUaPort); + server->setAuthenticationProvider(context.getAuthenticationProvider()); + server->setClientConnectedHandler( + [this](const std::string& clientId) + { + const auto loggerComponent = context.getLogger().getOrAddComponent("TmsServer"); + 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("TmsServer"); + 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->prepare(); + + tmsContext = std::make_shared(context, device); + + 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); + + tmsDevice = std::make_unique(device, server, context, tmsContext); + tmsDevice->registerOpcUaNode(OpcUaNodeId(NAMESPACE_DI, UA_DIID_DEVICESET)); + tmsDevice->createNonhierarchicalReferences(); + + server->start(); +} + +void TmsServer::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(); + tmsDevice.reset(); +} + +END_NAMESPACE_OPENDAQ_OPCUA diff --git a/shared/libraries/opcuatms/opcuatms_server/src/tms_server_context.cpp b/shared/libraries/opcuatms/opcuatms_server/src/tms_server_context.cpp new file mode 100644 index 0000000..1b01fd9 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/src/tms_server_context.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS + +TmsServerContext::TmsServerContext(const ContextPtr& context, const DevicePtr& rootDevice) + : context(context) + , rootDevice(rootDevice) +{ + this->context.getOnCoreEvent() += event(this, &TmsServerContext::coreEventCallback); +} + +TmsServerContext::~TmsServerContext() +{ + this->context.getOnCoreEvent() -= event(this, &TmsServerContext::coreEventCallback); +} + +void TmsServerContext::registerComponent(const ComponentPtr& component, tms::TmsServerObject& obj) +{ + idToObjMap.insert(std::make_pair(component.getGlobalId(), obj.weak_from_this())); +} + +DevicePtr TmsServerContext::getRootDevice() +{ + return rootDevice; +} + +ComponentPtr TmsServerContext::findComponent(const std::string& globalId) +{ + std::string relativeGlobalId = toRelativeGlobalId(globalId); + return rootDevice.findComponent(relativeGlobalId); +} + +void TmsServerContext::coreEventCallback(ComponentPtr& component, CoreEventArgsPtr& eventArgs) +{ + if (!component.assigned()) + return; + + if (const auto it = idToObjMap.find(component.getGlobalId()); it != idToObjMap.end()) + if (const std::shared_ptr spt = it->second.lock()) + spt->onCoreEvent(eventArgs); +} + +std::string TmsServerContext::toRelativeGlobalId(const std::string& globalId) +{ + const std::string rootDeviceId = rootDevice.getGlobalId().toStdString(); + std::string relativeGlobalId = globalId; + + if (relativeGlobalId.rfind(rootDeviceId, 0) == 0) + relativeGlobalId = relativeGlobalId.substr(rootDeviceId.size()); + if (relativeGlobalId.rfind("/", 0) == 0) + relativeGlobalId = relativeGlobalId.substr(1); + + return relativeGlobalId; +} + +END_NAMESPACE_OPENDAQ_OPCUA_TMS diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/CMakeLists.txt b/shared/libraries/opcuatms/opcuatms_server/tests/CMakeLists.txt new file mode 100644 index 0000000..c882bb6 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/CMakeLists.txt @@ -0,0 +1,39 @@ +set(MODULE_NAME opcuatms_server) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_helpers.h + test_tms_device.cpp + test_tms_signal.cpp + test_tms_function_block.cpp + test_tms_channel.cpp + test_tms_input_port.cpp + test_tms_property_object.cpp + test_tms_server.cpp + tms_server_test.h + tms_server_test.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 ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::opcuatms_test_utils + daq::opcuaclient + daq::opendaq_mocks +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(opcuatmsservercoverage ${TEST_APP} opcuatmsservercoverage) +endif() diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_helpers.h b/shared/libraries/opcuatms/opcuatms_server/tests/test_helpers.h new file mode 100644 index 0000000..ef37ac0 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_helpers.h @@ -0,0 +1,134 @@ +/* + * 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 +#include + +namespace test_helpers +{ + inline daq::InstancePtr SetupInstance() + { + using namespace daq; + const auto logger = Logger(); + const auto moduleManager = ModuleManager("[[none]]"); + const auto authenticationProvider = AuthenticationProvider(); + 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://client_device"); + instance.addDevice("daqmock://phys_device"); + instance.addFunctionBlock("mock_fb_uid"); + + return instance; + } + + inline daq::opcua::OpcUaNodeId BrowseForChild(const daq::opcua::OpcUaClientPtr& client, + const daq::opcua::OpcUaNodeId& rootNodeId, + const std::string& browseName) + { + using namespace daq::opcua; + std::vector results; + + BrowseRequest request(rootNodeId, OpcUaNodeClass::Object); + OpcUaBrowser browser(request, client); + auto browseResult = browser.browse(); + + for (const UA_ReferenceDescription& reference : browseResult) + { + if (daq::opcua::utils::ToStdString(reference.browseName.name) == browseName) + return OpcUaNodeId(reference.nodeId.nodeId); + } + + return {}; + } + + inline std::vector BrowseForChildWithTypeId(const daq::opcua::OpcUaClientPtr& client, + const daq::opcua::OpcUaNodeId& rootNodeId, + const daq::opcua::OpcUaNodeId& typeNodeId) + { + using namespace daq::opcua; + std::vector results; + + BrowseRequest request(rootNodeId, OpcUaNodeClass::Object); + OpcUaBrowser browser(request, client); + auto browseResult = browser.browse(); + + for (const UA_ReferenceDescription& reference : browseResult) + { + if (OpcUaNodeId(reference.typeDefinition.nodeId) == typeNodeId) + results.push_back(OpcUaNodeId(reference.nodeId.nodeId)); + } + + return results; + } + + inline std::vector BrowseSubDevices(const daq::opcua::OpcUaClientPtr& client, + const daq::opcua::OpcUaNodeId& nodeId) + { + using namespace daq::opcua; + return BrowseForChildWithTypeId(client, nodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_DAQDEVICETYPE)); + } + + inline std::vector BrowseFunctionBlocks(const daq::opcua::OpcUaClientPtr& client, + const daq::opcua::OpcUaNodeId& nodeId) + { + using namespace daq::opcua; + return BrowseForChildWithTypeId(client, nodeId, OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_FUNCTIONBLOCKTYPE)); + } + + inline std::vector BrowseChannels(const daq::opcua::OpcUaClientPtr& client, + const daq::opcua::OpcUaNodeId& nodeId) + { + using namespace daq::opcua; + return BrowseForChildWithTypeId(client, nodeId, OpcUaNodeId(NAMESPACE_DAQDEVICE, UA_DAQDEVICEID_CHANNELTYPE)); + } + + inline std::vector BrowseSignals(const daq::opcua::OpcUaClientPtr& client, + const daq::opcua::OpcUaNodeId& nodeId) + { + using namespace daq::opcua; + return BrowseForChildWithTypeId(client, nodeId, OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_SIGNALTYPE)); + } + + inline daq::opcua::OpcUaNodeId GetMockPhysicalDevice(const daq::opcua::OpcUaClientPtr& client) + { + using namespace daq::opcua; + auto firstLvlDevices = BrowseSubDevices(client, OpcUaNodeId(NAMESPACE_DI, UA_DIID_DEVICESET)); + auto secondLvlDevices = BrowseSubDevices(client, firstLvlDevices[0]); + for (const auto& dev : secondLvlDevices) + { + if (client->readBrowseName(dev) == "mockdev") + return dev; + } + throw std::runtime_error("Mock device not found"); + } +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_channel.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_channel.cpp new file mode 100644 index 0000000..47e819f --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_channel.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_server_test.h" +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace daq::opcua::utils; + +class TmsChannelTest : public TmsServerObjectTest +{ +public: + ChannelPtr createChannel() + { + return MockChannel(ctx, nullptr, "mockch"); + } +}; + +TEST_F(TmsChannelTest, Create) +{ + ChannelPtr channel = createChannel(); + auto tmsChannel = TmsServerChannel(channel, this->getServer(), ctx, tmsCtx); +} + +TEST_F(TmsChannelTest, Register) +{ + ChannelPtr channel = createChannel(); + auto serverChannel = TmsServerChannel(channel, this->getServer(), ctx, tmsCtx); + auto nodeId = serverChannel.registerOpcUaNode(); + + ASSERT_TRUE(this->getClient()->nodeExists(nodeId)); +} + +TEST_F(TmsChannelTest, AttrFunctionBlockType) +{ + ChannelPtr channel = createChannel(); + auto serverChannel = TmsServerChannel(channel, this->getServer(), ctx, tmsCtx); + + auto nodeId = serverChannel.registerOpcUaNode(); + + auto variant = readChildNode(nodeId, "FunctionBlockInfo"); + ASSERT_TRUE(variant.isType()); + + UA_FunctionBlockInfoStructure type = variant.readScalar(); + + ASSERT_EQ(ToStdString(type.id), "mock_ch"); + ASSERT_EQ(ToStdString(type.name), "mock_ch"); + ASSERT_EQ(ToStdString(type.description), ""); +} + +TEST_F(TmsChannelTest, BrowseSignals) +{ + ChannelPtr channel = createChannel(); + auto serverChannel = TmsServerChannel(channel, this->getServer(), ctx, tmsCtx); + auto nodeId = serverChannel.registerOpcUaNode(); + + OpcUaServerNode serverNodeFB(*this->getServer(), nodeId); + auto signalServerNode = serverNodeFB.getChildNode(UA_QUALIFIEDNAME_ALLOC(NAMESPACE_DAQBSP, "Sig")); + auto signalReferences = signalServerNode->browse(OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASVALUESIGNAL)); + ASSERT_EQ(signalReferences.size(), 10u); +} + +TEST_F(TmsChannelTest, Property) +{ + // Build server channel + auto serverChannel = createChannel(); + + const auto sampleRateProp = + FloatPropertyBuilder("SampleRate", 100.0).setUnit(Unit("Hz")).setMinValue(1.0).setMaxValue(1000000.0).build(); + + serverChannel.addProperty(sampleRateProp); + + auto tmsServerChannel = TmsServerChannel(serverChannel, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsServerChannel.registerOpcUaNode(); + + auto sampleRateNodeId = this->getChildNodeId(nodeId, "SampleRate"); + ASSERT_FALSE(sampleRateNodeId.isNull()); + + auto srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 100.0); + + serverChannel.setPropertyValue("SampleRate", 14.0); + + srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 14.0); + + this->getServer()->writeValue(sampleRateNodeId, OpcUaVariant(22.2)); + + srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 22.2); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_device.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_device.cpp new file mode 100644 index 0000000..8d65798 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_device.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.h" +#include "tms_server_test.h" + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +using TmsDeviceTest = TmsServerObjectTest; + +TEST_F(TmsDeviceTest, Create) +{ + auto instance = test_helpers::SetupInstance(); + auto device = instance.getRootDevice(); + + auto tmsDevice = TmsServerDevice(device, this->getServer(), ctx, tmsCtx); +} + +TEST_F(TmsDeviceTest, Register) +{ + auto instance = test_helpers::SetupInstance(); + auto device = instance.getRootDevice(); + + auto tmsDevice = TmsServerDevice(device, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsDevice.registerOpcUaNode(); + + ASSERT_TRUE(this->getClient()->nodeExists(nodeId)); +} + +TEST_F(TmsDeviceTest, SubDevices) +{ + auto instance = test_helpers::SetupInstance(); + auto device = instance.getRootDevice(); + + auto tmsDevice = TmsServerDevice(device, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsDevice.registerOpcUaNode(); + + ASSERT_EQ(test_helpers::BrowseSubDevices(client, nodeId).size(), 2u); +} + +TEST_F(TmsDeviceTest, FunctionBlock) +{ + auto instance = test_helpers::SetupInstance(); + auto device = instance.getRootDevice(); + + auto tmsDevice = TmsServerDevice(device, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsDevice.registerOpcUaNode(); + + auto functionBlockNodeId = getChildNodeId(nodeId, "FB"); + ASSERT_EQ(test_helpers::BrowseFunctionBlocks(client, functionBlockNodeId).size(), 1u); +} + +TEST_F(TmsDeviceTest, Property) +{ + auto instance = test_helpers::SetupInstance(); + auto device = instance.getRootDevice(); + + const auto sampleRateProp = + FloatPropertyBuilder("SampleRate", 100.0).setUnit(Unit("Hz")).setMinValue(1.0).setMaxValue(1000000.0).build(); + + device.addProperty(sampleRateProp); + + auto tmsDevice = TmsServerDevice(device, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsDevice.registerOpcUaNode(); + + auto sampleRateNodeId = this->getChildNodeId(nodeId, "SampleRate"); + ASSERT_FALSE(sampleRateNodeId.isNull()); + + auto srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 100.0); + + device.setPropertyValue("SampleRate", 14.0); + + srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 14.0); + + this->getServer()->writeValue(sampleRateNodeId, OpcUaVariant(22.2)); + + srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 22.2); +} + +TEST_F(TmsDeviceTest, Components) +{ + auto instance = test_helpers::SetupInstance(); + auto device = instance.getRootDevice(); + + auto tmsDevice = TmsServerDevice(device, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsDevice.registerOpcUaNode(); + + auto devices = test_helpers::BrowseSubDevices(client, nodeId); + auto componentA = getChildNodeId(devices[1], "componentA"); + ASSERT_FALSE(componentA.isNull()); + auto componentA1 = getChildNodeId(componentA, "componentA1"); + ASSERT_FALSE(componentA1.isNull()); + auto componentB = getChildNodeId(devices[1], "componentB"); + ASSERT_FALSE(componentB.isNull()); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_function_block.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_function_block.cpp new file mode 100644 index 0000000..ddaf552 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_function_block.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_server_test.h" +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace daq::opcua::utils; + +class TmsFunctionBlockTest : public TmsServerObjectTest +{ +public: + + FunctionBlockPtr createFunctionBlock(const FunctionBlockTypePtr& type = FunctionBlockType("UID", "Name", "Desc")) + { + return MockFunctionBlock(type, ctx, nullptr, "mockfb"); + } +}; + +TEST_F(TmsFunctionBlockTest, Create) +{ + FunctionBlockPtr functionBlock = createFunctionBlock(); + auto tmsFunctionBlock = TmsServerFunctionBlock(functionBlock, this->getServer(), ctx, tmsCtx); +} + +TEST_F(TmsFunctionBlockTest, Register) +{ + FunctionBlockPtr functionBlock = createFunctionBlock(); + auto serverFunctionBlock = TmsServerFunctionBlock(functionBlock, this->getServer(), ctx, tmsCtx); + auto nodeId = serverFunctionBlock.registerOpcUaNode(); + + ASSERT_TRUE(this->getClient()->nodeExists(nodeId)); +} + +TEST_F(TmsFunctionBlockTest, AttrFunctionBlockType) +{ + // Build functionBlock info: + const FunctionBlockTypePtr type = FunctionBlockType("ID", "NAME", "DESCRIPTION"); + + // Build server functionBlock + auto serverFunctionBlock = createFunctionBlock(type); + + ASSERT_EQ(serverFunctionBlock.getFunctionBlockType(), type); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + auto variant = readChildNode(nodeId, "FunctionBlockInfo"); + ASSERT_TRUE(variant.isType()); + + UA_FunctionBlockInfoStructure tmsType = variant.readScalar(); + + ASSERT_EQ(ToStdString(tmsType.id), "ID"); + ASSERT_EQ(ToStdString(tmsType.name), "NAME"); + ASSERT_EQ(ToStdString(tmsType.description), "DESCRIPTION"); +} + +TEST_F(TmsFunctionBlockTest, BrowseSignals) +{ + FunctionBlockPtr functionBlock = createFunctionBlock(); + auto serverFunctionBlock = TmsServerFunctionBlock(functionBlock, this->getServer(), ctx, tmsCtx); + auto nodeId = serverFunctionBlock.registerOpcUaNode(); + + OpcUaServerNode serverNodeFB(*this->getServer(), nodeId); + auto signalServerNode = serverNodeFB.getChildNode(UA_QUALIFIEDNAME_ALLOC(NAMESPACE_DAQBSP, "Sig")); + auto signalReferences = signalServerNode->browse(OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASVALUESIGNAL)); + ASSERT_EQ(signalReferences.size(), 4u); +} + +// TODO: Enable once name and description are no longer props +TEST_F(TmsFunctionBlockTest, DISABLED_Property) +{ + // Build functionBlock info: + const FunctionBlockTypePtr type = FunctionBlockType("UID", "Name", "Desc"); + + // Build server functionBlock + auto serverFunctionBlock = createFunctionBlock(type); + + const auto sampleRateProp = + FloatPropertyBuilder("SampleRate", 100.0).setUnit(Unit("Hz")).setMinValue(1.0).setMaxValue(1000000.0).build(); + + serverFunctionBlock.addProperty(sampleRateProp); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + auto sampleRateNodeId = this->getChildNodeId(nodeId, "SampleRate"); + ASSERT_FALSE(sampleRateNodeId.isNull()); + + auto srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 100.0); + + serverFunctionBlock.setPropertyValue("SampleRate", 14.0); + + srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 14.0); + + this->getServer()->writeValue(sampleRateNodeId, OpcUaVariant(22.2)); + + srValue = this->getServer()->readValue(sampleRateNodeId); + ASSERT_TRUE(srValue.hasScalarType()); + ASSERT_DOUBLE_EQ(srValue.readScalar(), 22.2); +} + +TEST_F(TmsFunctionBlockTest, NestedFunctionBlocks) +{ + FunctionBlockPtr functionBlock = createFunctionBlock(); + + auto serverFunctionBlock = TmsServerFunctionBlock(functionBlock, this->getServer(), ctx, tmsCtx); + auto nodeId = serverFunctionBlock.registerOpcUaNode(); + + auto firstFB = functionBlock.getFunctionBlocks()[0]; + auto firstFBNodeId = OpcUaNodeId(nodeId.getNamespaceIndex(), firstFB.getGlobalId().toStdString()); + ASSERT_TRUE(getServer()->nodeExists(firstFBNodeId)); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_input_port.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_input_port.cpp new file mode 100644 index 0000000..d778f54 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_input_port.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include +#include +#include "tms_server_test.h" +#include +#include +#include +#include +#include +#include +#include "test_input_port_notifications.h" +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +class TmsInputPortTest : public TmsServerObjectTest +{ +public: + InputPortPtr createInputPort() + { + auto port = InputPort(ctx, nullptr, "Port"); + port.getTags().asPtr().add("Port"); + return port; + } +}; + +TEST_F(TmsInputPortTest, Create) +{ + InputPortPtr inputPort = createInputPort(); + auto tmsInputPort = TmsServerInputPort(inputPort, this->getServer(), ctx, tmsCtx); +} + +TEST_F(TmsInputPortTest, Register) +{ + InputPortPtr inputPort = createInputPort(); + auto serverInputPort = TmsServerInputPort(inputPort, this->getServer(), ctx, tmsCtx); + auto nodeId = serverInputPort.registerOpcUaNode(); + + ASSERT_TRUE(this->getClient()->nodeExists(nodeId)); +} + +TEST_F(TmsInputPortTest, ConnectedToReference) +{ + const auto logger = Logger(); + const auto scheduler = Scheduler(logger); + const auto authenticationProvider = AuthenticationProvider(); + const auto context = Context(scheduler, logger, nullptr, nullptr, authenticationProvider); + + SignalPtr signal = Signal(context, nullptr, "sig"); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, tmsCtx); + auto signalNodeId = serverSignal.registerOpcUaNode(); + + InputPortNotificationsPtr inputPortNotification = TestInputPortNotifications(); + + InputPortConfigPtr inputPort = InputPort(context, nullptr, "TestInputPort"); + inputPort.setListener(inputPortNotification); + inputPort.connect(signal); + + auto serverInputPort = TmsServerInputPort(inputPort, this->getServer(), ctx, tmsCtx); + auto inputPortNodeId = serverInputPort.registerOpcUaNode(); + + ASSERT_NO_THROW(serverInputPort.createNonhierarchicalReferences()); + + ASSERT_TRUE(this->getClient()->nodeExists(signalNodeId)); + ASSERT_TRUE(this->getClient()->nodeExists(inputPortNodeId)); + + OpcUaServerNode inputPortNode(*this->getServer(), inputPortNodeId); + auto connectedToNodes = inputPortNode.browse(OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_CONNECTEDTOSIGNAL)); + ASSERT_EQ(connectedToNodes.size(), 1u); + ASSERT_EQ(connectedToNodes[0]->getNodeId(), signalNodeId); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_property_object.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_property_object.cpp new file mode 100644 index 0000000..35ab66b --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_property_object.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_server_test.h" +#include +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +class TmsPropertyObjectTest : public TmsServerObjectTest +{ +public: + PropertyObjectPtr createPropertyObject() + { + GenericPropertyObjectPtr object = PropertyObject(); + + auto intProp = IntProperty("IntProp", 1); + object.addProperty(intProp); + + return object; + } +}; + +TEST_F(TmsPropertyObjectTest, Create) +{ + PropertyObjectPtr propertyObject = createPropertyObject(); + auto tmsPropertyObject = TmsServerPropertyObject(propertyObject, this->getServer(), ctx, tmsCtx); +} + +TEST_F(TmsPropertyObjectTest, BaseObjectList) +{ + GenericPropertyObjectPtr object = PropertyObject(); + + auto list = List(); + list.pushBack(1.0); + list.pushBack(2.0); + list.pushBack(3.0); + object.addProperty(ListProperty("ListProp", list)); + + auto tmsPropertyObject = TmsServerPropertyObject(object, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + OpcUaObject request; + request->nodesToReadSize = 1; + request->nodesToRead = (UA_ReadValueId*) UA_Array_new(1, &UA_TYPES[UA_TYPES_READVALUEID]); + request->nodesToRead[0].nodeId = nodeId.getDetachedValue(); + request->nodesToRead[0].attributeId = UA_ATTRIBUTEID_VALUE; + + OpcUaObject response = UA_Client_Service_read(client->getLockedUaClient(), *request); + const auto status = response->responseHeader.serviceResult; + ASSERT_TRUE(status == UA_STATUSCODE_GOOD); +} + +TEST_F(TmsPropertyObjectTest, Register) +{ + PropertyObjectPtr propertyObject = createPropertyObject(); + + auto tmsPropertyObject = TmsServerPropertyObject(propertyObject, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + ASSERT_TRUE(this->getClient()->nodeExists(nodeId)); +} + +TEST_F(TmsPropertyObjectTest, DISABLED_OnPropertyValueChangeEvent) +{ + PropertyObjectPtr propertyObject = createPropertyObject(); + + auto tmsPropertyObject = TmsServerPropertyObject(propertyObject, this->getServer(), ctx, tmsCtx); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + OpcUaObject request = UA_CreateSubscriptionRequest_default(); + + auto subscription = client->createSubscription(request); + + EventMonitoredItemCreateRequest monitoredItem(nodeId); + EventFilter filter(1); + filter.setSelectClause(0, SimpleAttributeOperand::CreateMessageValue()); + + monitoredItem.setEventFilter(std::move(filter)); + + std::promise waitForChangeEvent; + subscription->monitoredItemsCreateEvent( + UA_TIMESTAMPSTORETURN_BOTH, + *monitoredItem, + [&waitForChangeEvent]( + OpcUaClient* client, Subscription* subContext, MonitoredItem* monContext, size_t nEventFields, UA_Variant* eventFields) { + waitForChangeEvent.set_value(); + }); + + propertyObject.setPropertyValue("IntProp", 2); + + auto future = waitForChangeEvent.get_future(); + ASSERT_NE(future.wait_for(2s), std::future_status::timeout); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_server.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_server.cpp new file mode 100644 index 0000000..bd9a6a8 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_server.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include "test_helpers.h" +#include + +using TmsServerTest = testing::Test; + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua; +using namespace test_helpers; + + +TEST_F(TmsServerTest, Create) +{ + auto daqInstance = SetupInstance(); + TmsServer server(daqInstance); +} + +TEST_F(TmsServerTest, StartStop) +{ + auto daqInstance = SetupInstance(); + TmsServer server(daqInstance); + server.start(); + server.stop(); +} + +TEST_F(TmsServerTest, Connect) +{ + auto daqInstance = SetupInstance(); + TmsServer server(daqInstance); + server.start(); + + auto client = TmsObjectTest::CreateAndConnectTestClient(); + ASSERT_TRUE(client->isConnected()); +} + +TEST_F(TmsServerTest, DeviceTopology) +{ + auto daqInstance = SetupInstance(); + TmsServer server(daqInstance); + server.start(); + + auto client = TmsObjectTest::CreateAndConnectTestClient(); + + auto firstLvlDevices = BrowseSubDevices(client, OpcUaNodeId(NAMESPACE_DI, UA_DIID_DEVICESET)); + ASSERT_EQ(firstLvlDevices.size(), 1u); + + auto secondLvlDevices = BrowseSubDevices(client, firstLvlDevices[0]); + ASSERT_EQ(secondLvlDevices.size(), 2u); +} + +TEST_F(TmsServerTest, Channels) +{ + auto daqInstance = SetupInstance(); + TmsServer server(daqInstance); + server.start(); + + auto client = TmsObjectTest::CreateAndConnectTestClient(); + auto referenceBrowser = CachedReferenceBrowser(client); + auto mockPhysicalDevice = GetMockPhysicalDevice(client); + + auto inputsOutputsFolder = BrowseForChild(client, mockPhysicalDevice, "IO"); + ASSERT_FALSE(inputsOutputsFolder.isNull()); + + auto channels = BrowseChannels(client, inputsOutputsFolder); + ASSERT_EQ(channels.size(), 1u); + + auto signalsNode = BrowseForChild(client, channels[0], "Sig"); + ASSERT_FALSE(signalsNode.isNull()); + + auto signals = BrowseSignals(client, signalsNode); + ASSERT_EQ(signals.size(), 10u); + + auto byteStepSignal = BrowseForChild(client, signalsNode, "ByteStep"); + auto timeSignal = BrowseForChild(client, signalsNode, "Time"); + + auto filter = BrowseFilter(); + filter.referenceTypeId = OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASDOMAINSIGNAL); + filter.direction = UA_BROWSEDIRECTION_FORWARD; + + auto references = referenceBrowser.browseFiltered(byteStepSignal, filter); + ASSERT_EQ(references.byNodeId.size(), 1u); + ASSERT_EQ(references.byNodeId.begin().key(), timeSignal); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_signal.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_signal.cpp new file mode 100644 index 0000000..b5edd90 --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/test_tms_signal.cpp @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_server_test.h" + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +class TmsSignalTest : public TmsServerObjectTest +{ +public: + SignalConfigPtr createSignal(const ContextPtr& context, const StringPtr& localId = "sig") + { + SignalConfigPtr signal = Signal(context, nullptr, localId); + signal.setActive(false); + return signal; + } +}; + +TEST_F(TmsSignalTest, Create) +{ + SignalConfigPtr signal = Signal(ctx, nullptr, "sig"); + auto tmsSignal = TmsServerSignal(signal, this->getServer(), ctx, tmsCtx); +} + +TEST_F(TmsSignalTest, Register) +{ + SignalConfigPtr signal = Signal(ctx, nullptr, "sig"); + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, tmsCtx); + auto nodeId = serverSignal.registerOpcUaNode(); + + ASSERT_TRUE(this->getClient()->nodeExists(nodeId)); +} + +TEST_F(TmsSignalTest, DomainSignalReference) +{ + SignalConfigPtr signal = createSignal(ctx, "signal"); + SignalConfigPtr domainSignal = createSignal(ctx, "time signal"); + signal.setDomainSignal(domainSignal); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, tmsCtx); + auto signalNodeId = serverSignal.registerOpcUaNode(); + + auto serverDomainSignal = TmsServerSignal(domainSignal, this->getServer(), ctx, tmsCtx); + auto domainSignalNodeId = serverDomainSignal.registerOpcUaNode(); + + ASSERT_NO_THROW(serverSignal.createNonhierarchicalReferences()); + + ASSERT_TRUE(this->getClient()->nodeExists(signalNodeId)); + ASSERT_TRUE(this->getClient()->nodeExists(domainSignalNodeId)); + + OpcUaServerNode signalNode(*this->getServer(), signalNodeId); + auto hasDomainNodes = signalNode.browse(OpcUaNodeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASDOMAINSIGNAL)); + ASSERT_EQ(hasDomainNodes.size(), 1u); + ASSERT_EQ(hasDomainNodes[0]->getNodeId(), domainSignalNodeId); +} + +TEST_F(TmsSignalTest, ValueAndAnalogValueDataType) +{ + // Create signal with Float64 descriptor + auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Float64) + .setName("TestSignal") + .build(); + SignalConfigPtr signal = createSignal(ctx, "signal"); + signal.setDescriptor(descriptor); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, tmsCtx); + auto signalNodeId = serverSignal.registerOpcUaNode(); + + // Get Value and AnalogValue node IDs + OpcUaServerNode signalNode(*this->getServer(), signalNodeId); + auto valueNodeId = getChildNodeId(signalNodeId, "Value"); + auto analogValueNodeId = getChildNodeId(signalNodeId, "AnalogValue"); + + // Check that Value node has Float64 data type (not abstract NUMBER) + auto valueDataType = this->getClient()->readDataType(valueNodeId); + ASSERT_EQ(valueDataType, OpcUaNodeId(0, UA_NS0ID_DOUBLE)); + + // Check that AnalogValue node has Float64 data type (not abstract NUMBER) + auto analogValueDataType = this->getClient()->readDataType(analogValueNodeId); + ASSERT_EQ(analogValueDataType, OpcUaNodeId(0, UA_NS0ID_DOUBLE)); +} + +TEST_F(TmsSignalTest, ValueAndAnalogValueDataTypeInt32) +{ + // Create signal with Int32 descriptor + auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Int32) + .setName("TestSignal") + .build(); + SignalConfigPtr signal = createSignal(ctx, "signal"); + signal.setDescriptor(descriptor); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, tmsCtx); + auto signalNodeId = serverSignal.registerOpcUaNode(); + + // Get Value and AnalogValue node IDs + auto valueNodeId = getChildNodeId(signalNodeId, "Value"); + auto analogValueNodeId = getChildNodeId(signalNodeId, "AnalogValue"); + + // Check that Value node has Int32 data type (not abstract NUMBER) + auto valueDataType = this->getClient()->readDataType(valueNodeId); + ASSERT_EQ(valueDataType, OpcUaNodeId(0, UA_NS0ID_INT32)); + + // Check that AnalogValue node has Int32 data type (not abstract NUMBER) + auto analogValueDataType = this->getClient()->readDataType(analogValueNodeId); + ASSERT_EQ(analogValueDataType, OpcUaNodeId(0, UA_NS0ID_INT32)); +} \ No newline at end of file diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/testapp.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/testapp.cpp new file mode 100644 index 0000000..24298ce --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/testapp.cpp @@ -0,0 +1,23 @@ +#include +#include + +#include +#include +#include +#include + +int main(int argc, char** args) +{ + daq::daqInitializeCoreObjectsTesting(); + daqInitModuleManagerLibrary(); + daqInitOpenDaqLibrary(); + + 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/opcuatms/opcuatms_server/tests/tms_server_test.cpp b/shared/libraries/opcuatms/opcuatms_server/tests/tms_server_test.cpp new file mode 100644 index 0000000..59a92ec --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/tms_server_test.cpp @@ -0,0 +1,17 @@ +#include "tms_server_test.h" + +void TmsServerObjectTest::SetUp() +{ + TmsObjectTest::SetUp(); + + ctx = daq::NullContext(); + tmsCtx = std::make_shared(ctx, nullptr); +} + +void TmsServerObjectTest::TearDown() +{ + ctx = nullptr; + tmsCtx = nullptr; + + TmsObjectTest::TearDown(); +} diff --git a/shared/libraries/opcuatms/opcuatms_server/tests/tms_server_test.h b/shared/libraries/opcuatms/opcuatms_server/tests/tms_server_test.h new file mode 100644 index 0000000..fe4d66e --- /dev/null +++ b/shared/libraries/opcuatms/opcuatms_server/tests/tms_server_test.h @@ -0,0 +1,30 @@ +/* + * 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 + +class TmsServerObjectTest : public TmsObjectTest +{ +public: + void SetUp() override; + void TearDown() override; + + daq::ContextPtr ctx; + daq::opcua::tms::TmsServerContextPtr tmsCtx; +}; diff --git a/shared/libraries/opcuatms/tests/CMakeLists.txt b/shared/libraries/opcuatms/tests/CMakeLists.txt new file mode 100644 index 0000000..499d41e --- /dev/null +++ b/shared/libraries/opcuatms/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(opcuatms_integration_tests CXX) + +add_subdirectory(test_utils) + +option(OPENDAQ_ENABLE_OPCUA_INTEGRATION_TESTS "Enable OpcUa integration testing" OFF) + +if (OPENDAQ_ENABLE_OPCUA_INTEGRATION_TESTS) + add_subdirectory(opcuatms_integration) +endif() diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt b/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt new file mode 100644 index 0000000..0f69768 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/CMakeLists.txt @@ -0,0 +1,65 @@ +set(MODULE_NAME opcuatms_integration) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES main.cpp + tms_object_integration_test.h + tms_object_integration_test.cpp + test_tms_signal.cpp + test_tms_function_block.cpp + test_tms_channel.cpp + test_tms_input_port.cpp + test_tms_property.cpp + test_tms_property_object.cpp + test_tms_device.cpp + test_tms_integration.cpp + test_property_object_advanced.cpp + test_tms_amplifier.cpp + test_tms_component.cpp + test_tms_folder.cpp + test_tms_function_property.cpp + test_tms_fusion_device.cpp + test_tms_function_block_type.cpp +) + +set(SUPPORTS_ASAN 0) + +if(CXX_COMPILER_ID EQUAL Clang OR CXX_COMPILER_ID EQUAL AppleClang OR CXX_COMPILER_ID EQUAL GNU) + set(SUPPORTS_ASAN 1) + set(ASAN_COMPILE_FLAGS -fsanitize=address -fno-omit-frame-pointer) +endif() + +add_executable(${TEST_APP} ${TEST_SOURCES}) + +set_target_properties(${TEST_APP} PROPERTIES DEBUG_POSTFIX _debug) + +if (WIN32) + set(BCRYPT_LIB bcrypt.dll) +endif() + +target_link_libraries(${TEST_APP} PRIVATE daq::opcuatms_test_utils + daq::opcuatms_server + daq::opcuatms_client + daq::opendaq_mocks + ${BCRYPT_LIB} + Taskflow::Taskflow +) + +if(SUPPORTS_ASAN) + target_link_libraries(${TEST_APP} PRIVATE asan) + target_compile_options(${TEST_APP} PRIVATE -Wall -Werror ${CGOV_COMPILE_FLAGS} ${ASAN_COMPILE_FLAGS}) +endif() + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (MSVC) + target_compile_options(${TEST_APP} PRIVATE /wd4324 /bigobj) +elseif (MINGW AND CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(${TEST_APP} PRIVATE -Wa,-mbig-obj) +endif() + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}testcoverage ${TEST_APP} ${MODULE_NAME}testcoverage) +endif() diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/main.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/main.cpp new file mode 100644 index 0000000..24298ce --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/main.cpp @@ -0,0 +1,23 @@ +#include +#include + +#include +#include +#include +#include + +int main(int argc, char** args) +{ + daq::daqInitializeCoreObjectsTesting(); + daqInitModuleManagerLibrary(); + daqInitOpenDaqLibrary(); + + 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/opcuatms/tests/opcuatms_integration/test_property_object_advanced.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_property_object_advanced.cpp new file mode 100644 index 0000000..0bc51b8 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_property_object_advanced.cpp @@ -0,0 +1,872 @@ +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +struct RegisteredPropertyObject +{ + TmsServerPropertyObjectPtr serverProp; + PropertyObjectPtr clientProp; +}; + +// TODO: Add complex number type property test cases once implemented + +class TmsPropertyObjectAdvancedTest : public TmsObjectIntegrationTest +{ +public: + TypeManagerPtr manager; + + void SetUp() override + { + TmsObjectIntegrationTest::SetUp(); + + manager = TypeManager(); + + manager.addType(StructType("ListRuleDescriptionStructure", + List("Type", "Elements"), + List(SimpleType(ctString), SimpleType(ctList)))); + + manager.addType(StructType("CustomRuleDescriptionStructure", + List("Type", "Parameters"), + List(SimpleType(ctString), SimpleType(ctDict)))); + + manager.addType(StructType("AdditionalParametersType", + List("Parameters"), + List(SimpleType(ctList)))); + + manager.addType(StructType("KeyValuePair", + List("Key", "Value"), + List(SimpleType(ctString), SimpleType(ctUndefined)))); + + + auto functionProp = FunctionProperty( + "function", FunctionInfo(ctString, List(ArgumentInfo("Int", ctInt), ArgumentInfo("Float", ctFloat)))); + FunctionPtr funcCallback = Function( + [](ListPtr args) + { + int intVal = args[0]; + double floatVal = args[1]; + + if (floatVal > intVal) + return String("Float is greater."); + + return String("Int is greater or equal."); + }); + + auto procProp = + FunctionProperty("procedure", + ProcedureInfo(List( + ArgumentInfo("Ratio", ctRatio), + ArgumentInfo("String", ctString), + ArgumentInfo("Bool", ctBool)))); + ProcedurePtr procCallback = Procedure( + [&](ListPtr args) + { + testRatio = args[0]; + testString = args[1]; + testBool = args[2]; + }); + + const auto obj = PropertyObject(); + obj.addProperty(IntProperty("ObjNumber", 0)); + obj.addProperty(functionProp); + obj.setPropertyValue("function", funcCallback); + obj.addProperty(procProp); + obj.setPropertyValue("procedure", procCallback); + + auto obj1 = PropertyObject(); + obj1.addProperty(StringProperty("foo", "bar")); + + auto objClass = PropertyObjectClassBuilder("TestClass") + .addProperty(IntPropertyBuilder("Integer", 1) + .setDescription("MyInteger") + .setMaxValue(10) + .setMinValue(0) + .setSuggestedValues(List(1, 3, 5, 7, 10)) + .setUnit(EvalValue("Unit(%IntegerUnit:SelectedValue)")) + .build()) + .addProperty(FloatPropertyBuilder("Float", EvalValue("$Integer - 0.123")) + .setDescription("MyFloat") + .setMaxValue(EvalValue("$Integer + 1")) + .setMinValue(0) + .setSuggestedValues(EvalValue("[1.0, 3.0, 5.0, 7.0, 10.0] * if($Integer < 5, 1.0, 0.5)")) + .build()) + .addProperty(ListProperty("IntList", List(1, 2, 3, 4, 5))) + .addProperty(ListProperty("StringList", List("foo", "bar"))) + .addProperty(BoolPropertyBuilder("BoolReadOnly", False).setReadOnly(True).build()) + .addProperty(DictProperty("IntFloatDict", Dict({{0, 1.123}, {2, 2.345}, {4, 3.456}}))) + .addProperty(SelectionProperty("IntegerUnit", List("s", "ms"), 0, false)) + .addProperty(SelectionProperty("Range", EvalValue("[50.0, 10.0, 1.0, 0.1] * if($Integer < 5, 1.0, 1.123)"), 0)) + .addProperty(SparseSelectionProperty( + "StringSparseSelection", Dict({{0, "foo"}, {10, "bar"}}), 10, EvalValue("$Integer < 5"))) + .addProperty(ReferenceProperty("IntOrFloat", EvalValue("if($Float < 1, %Integer, %Float)"))) + .addProperty(ObjectProperty("Object", obj)) + .addProperty(StringPropertyBuilder("String", "foo").setReadOnly(EvalValue("$Float < 1.0")).build()) + .addProperty(ReferenceProperty("ObjectOrString", EvalValue("if($Integer < 5, %Object, %String)"))) + .addProperty(IntPropertyBuilder("ValidatedInt", 5).setValidator(Validator("Value < 10")).build()) + .addProperty(IntPropertyBuilder("CoercedInt", 10).setCoercer(Coercer("if(Value > 10, 10, Value)")).build()) + .addProperty(RatioProperty("Ratio", Ratio(1, 1000))) + .addProperty(ObjectPropertyBuilder("ObjectWithMetadata", obj1).setReadOnly(true).setVisible(false).build()) + .addProperty(StructProperty("UnitStruct", Unit("s", -1, "second", "time"))) + .addProperty(StructProperty("ArgumentStruct", ArgumentInfo("Arg", ctInt))) + .addProperty(StructProperty("RangeStruct", Range(1, 2))) + .addProperty(StructProperty("ComplexNumberStruct", ComplexNumber(1, 2))) + .addProperty(StructProperty("FunctionBlockTypeStruct", FunctionBlockType("Id", "Name", "Desc"))) + .addProperty(StructProperty("DeviceDomainStructure", + Struct("DeviceDomainStructure", + Dict({{"Resolution", Ratio(10, 20)}, + {"TicksSinceOrigin", 1000}, + {"Origin", "Origin"}, + {"Unit", + Unit("Symbol", -1, "Name", "Quantity")}}), + manager))) + .addProperty(StructProperty("ListRuleDescriptionStructure", + Struct("ListRuleDescriptionStructure", + Dict( + {{"Type", "List"}, {"Elements", List("foo", "bar")}}), + manager))) + .addProperty(StructProperty("CustomRuleDescriptionStructure", + Struct("CustomRuleDescriptionStructure", + Dict( + {{"Type", "List"}, + {"Parameters", + Dict({{"foo", "bar"}, {"foo1", "bar1"}})}}), + manager))) + .addProperty(StructProperty("AdditionalParametersType", + Struct("AdditionalParametersType", + Dict({{"Parameters", List( + Struct("KeyValuePair", + Dict( + {{"Key", "key1"}, + {"Value", "value1"}}), + manager), + Struct("KeyValuePair", + Dict( + {{"Key", "key1"}, + {"Value", "value1"}}), + manager))}}), + manager))) + .build(); + + manager.addType(objClass); + } + + void TearDown() override + { + TmsObjectIntegrationTest::TearDown(); + } + + RegisteredPropertyObject registerPropertyObject(const PropertyObjectPtr& prop) + { + const auto context = Context(nullptr, logger, manager, nullptr, nullptr); + const auto serverProp = + std::make_shared(prop, server, context, std::make_shared(context, nullptr)); + const auto nodeId = serverProp->registerOpcUaNode(); + const auto clientProp = TmsClientPropertyObject(Context(nullptr, logger, manager, nullptr, nullptr), clientContext, nodeId); + return {serverProp, clientProp}; + } + + StringPtr getLastMessage() + { + logger.flush(); + auto sink = getPrivateSink(); + while (sink.waitForMessage(2000)) + { + // Wait for actual last message + } + auto logMessage = sink.getLastMessage(); + return logMessage; + } + + RatioPtr testRatio; + StringPtr testString; + BoolPtr testBool; +}; + +TEST_F(TmsPropertyObjectAdvancedTest, SetUpServer) +{ + const auto obj = PropertyObject(manager, "TestClass"); + + const auto serverProp = std::make_shared(obj, server, ctx, serverContext); + const auto nodeId = serverProp->registerOpcUaNode(); +} + +TEST_F(TmsPropertyObjectAdvancedTest, SetUp) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); +} + +TEST_F(TmsPropertyObjectAdvancedTest, PropertyCount) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getAllProperties().getCount(), clientObj.getAllProperties().getCount()); + ASSERT_EQ(obj.getVisibleProperties().getCount(), clientObj.getVisibleProperties().getCount()); +} + +TEST_F(TmsPropertyObjectAdvancedTest, MinMax) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const auto serverIntProp = obj.getProperty("Integer"); + const auto clientIntProp = clientObj.getProperty("Integer"); + + const auto serverFloatProp = obj.getProperty("Float"); + const auto clientFloatProp = clientObj.getProperty("Float"); + + ASSERT_EQ(serverIntProp.getMinValue(), clientIntProp.getMinValue()); + ASSERT_EQ(serverIntProp.getMaxValue(), clientIntProp.getMaxValue()); + + ASSERT_EQ(serverFloatProp.getMinValue(), clientFloatProp.getMinValue()); + ASSERT_EQ(serverFloatProp.getMaxValue(), clientFloatProp.getMaxValue()); + + obj.setPropertyValue("Integer", 8); + ASSERT_EQ(serverFloatProp.getMaxValue(), 9); + ASSERT_EQ(clientFloatProp.getMaxValue(), 9); +} + +TEST_F(TmsPropertyObjectAdvancedTest, SuggestedValues) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const auto serverIntProp = obj.getProperty("Integer"); + const auto clientIntProp = clientObj.getProperty("Integer"); + + const auto serverFloatProp = obj.getProperty("Float"); + const auto clientFloatProp = clientObj.getProperty("Float"); + + ASSERT_EQ(serverIntProp.getSuggestedValues(), clientIntProp.getSuggestedValues()); + + ListPtr serverFloatSuggestedValues = serverFloatProp.getSuggestedValues(); + ListPtr clientFloatSuggestedValues = clientFloatProp.getSuggestedValues(); + + ASSERT_EQ(serverFloatSuggestedValues, clientFloatSuggestedValues); + + obj.setPropertyValue("Integer", 8); + + serverFloatSuggestedValues = serverFloatProp.getSuggestedValues(); + clientFloatSuggestedValues = clientFloatProp.getSuggestedValues(); + + ASSERT_EQ(serverFloatSuggestedValues, clientFloatSuggestedValues); +} + +TEST_F(TmsPropertyObjectAdvancedTest, Unit) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const auto serverIntProp = obj.getProperty("Integer"); + const auto clientIntProp = clientObj.getProperty("Integer"); + + UnitPtr serverUnit = serverIntProp.getUnit(); + UnitPtr clientUnit = clientIntProp.getUnit(); + + ASSERT_EQ(serverUnit, clientUnit); + + obj.setPropertyValue("IntegerUnit", 1); + + serverUnit = serverIntProp.getUnit(); + clientUnit = clientIntProp.getUnit(); + + ASSERT_EQ(serverUnit, clientUnit); +} + +TEST_F(TmsPropertyObjectAdvancedTest, Description) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const auto serverIntProp = obj.getProperty("Integer"); + const auto clientIntProp = clientObj.getProperty("Integer"); + + const StringPtr serverDesc = serverIntProp.getDescription(); + const StringPtr clientDesc = clientIntProp.getDescription(); + + ASSERT_EQ(serverDesc, clientDesc); +} + +TEST_F(TmsPropertyObjectAdvancedTest, Visible) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + //const auto serverIntUnitProp = obj.getProperty("IntegerUnit"); + //const auto clientIntUnitProp = clientObj.getProperty("IntegerUnit"); + + const auto serverSparseSelectionProp = obj.getProperty("StringSparseSelection"); + const auto clientSparseSelectionProp = clientObj.getProperty("StringSparseSelection"); + + //ASSERT_EQ(serverIntUnitProp.getVisible(), clientIntUnitProp.getVisible()); + ASSERT_EQ(serverSparseSelectionProp.getVisible(), clientSparseSelectionProp.getVisible()); + + obj.setPropertyValue("Integer", 9); + + ASSERT_EQ(serverSparseSelectionProp.getVisible(), false); + ASSERT_EQ(clientSparseSelectionProp.getVisible(), false); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ReadOnly) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const auto serverStringProp = obj.getProperty("String"); + const auto clientStringProp = clientObj.getProperty("String"); + + ASSERT_EQ(serverStringProp.getReadOnly(), clientStringProp.getReadOnly()); + + obj.setPropertyValue("Float", 3.0); + ASSERT_EQ(serverStringProp.getReadOnly(), false); + ASSERT_EQ(serverStringProp.getReadOnly(), false); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ReadOnlyPropValue) +{ + const auto obj = PropertyObject(manager, "TestClass"); + obj.asPtr().setProtectedPropertyValue("BoolReadOnly", True); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const bool value = clientObj.getPropertyValue("BoolReadOnly"); + ASSERT_TRUE(value); +} + +TEST_F(TmsPropertyObjectAdvancedTest, DefaultValues) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + for (auto serverProp : obj.getAllProperties()) + { + auto name = serverProp.getName(); + auto clientProp = clientObj.getProperty(name); + + if (serverProp.getValueType() != ctObject && serverProp.getValueType() != ctUndefined) + ASSERT_EQ(serverProp.getDefaultValue(), clientProp.getDefaultValue()); + } + + obj.setPropertyValue("Integer", 5); + ASSERT_DOUBLE_EQ(obj.getProperty("Float").getDefaultValue(), 4.877); + ASSERT_DOUBLE_EQ(clientObj.getProperty("Float").getDefaultValue(), 4.877); +} + +TEST_F(TmsPropertyObjectAdvancedTest, IntFloatGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getPropertyValue("Integer"), clientObj.getPropertyValue("Integer")); + ASSERT_DOUBLE_EQ(obj.getPropertyValue("Float"), clientObj.getPropertyValue("Float")); + + ASSERT_NO_THROW(clientObj.setPropertyValue("Integer", 5)); + ASSERT_NO_THROW(clientObj.setPropertyValue("Integer", -1)); + ASSERT_NO_THROW(clientObj.setPropertyValue("Integer", 11)); + + ASSERT_NO_THROW(clientObj.setPropertyValue("Float", 2.5)); + ASSERT_NO_THROW(clientObj.setPropertyValue("Float", 8.8)); + ASSERT_NO_THROW(clientObj.setPropertyValue("Float", -1.2)); + + ASSERT_EQ(obj.getPropertyValue("Integer"), clientObj.getPropertyValue("Integer")); + ASSERT_EQ(10, clientObj.getPropertyValue("Integer")); + ASSERT_DOUBLE_EQ(obj.getPropertyValue("Float"), clientObj.getPropertyValue("Float")); + ASSERT_DOUBLE_EQ(0, clientObj.getPropertyValue("Float")); +} + +TEST_F(TmsPropertyObjectAdvancedTest, StringGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getPropertyValue("String"), clientObj.getPropertyValue("String")); + + ASSERT_NO_THROW(clientObj.setPropertyValue("Float", 5.123)); + ASSERT_NO_THROW(clientObj.setPropertyValue("String", "bar")); + + ASSERT_EQ(obj.getPropertyValue("String"), "bar"); + ASSERT_EQ(clientObj.getPropertyValue("String"), "bar"); +} + +TEST_F(TmsPropertyObjectAdvancedTest, IntListGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ListPtr serverListObj = obj.getPropertyValue("IntList"); + ListPtr clientListObj = clientObj.getPropertyValue("IntList"); + + for (SizeT i = 0; i < serverListObj.getCount(); ++i) + ASSERT_EQ(serverListObj[i], clientListObj[i]); + + clientObj.setPropertyValue("IntList", List(3, 4, 5, 6, 7)); + + serverListObj = obj.getPropertyValue("IntList"); + clientListObj = clientObj.getPropertyValue("IntList"); + + for (SizeT i = 0; i < serverListObj.getCount(); ++i) + ASSERT_EQ(serverListObj[i], clientListObj[i]); +} + +TEST_F(TmsPropertyObjectAdvancedTest, RatioGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getPropertyValue("Ratio"), clientObj.getPropertyValue("Ratio")); + + ASSERT_NO_THROW(clientObj.setPropertyValue("Ratio", Ratio(1, 2000))); + + ASSERT_EQ(obj.getPropertyValue("Ratio"), Ratio(1, 2000)); + ASSERT_EQ(clientObj.getPropertyValue("Ratio"), Ratio(1, 2000)); +} + +TEST_F(TmsPropertyObjectAdvancedTest, StringListGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ListPtr serverListObj = obj.getPropertyValue("StringList"); + ListPtr clientListObj = clientObj.getPropertyValue("StringList"); + + ASSERT_EQ(serverListObj[0], clientListObj[0]); + ASSERT_EQ(serverListObj[1], clientListObj[1]); + + const auto list = List("bar", "foo"); + clientObj.setPropertyValue("StringList", list); + + serverListObj = obj.getPropertyValue("StringList"); + clientListObj = clientObj.getPropertyValue("StringList"); + + ASSERT_EQ(serverListObj[0], clientListObj[0]); + ASSERT_EQ(serverListObj[1], clientListObj[1]); +} + +TEST_F(TmsPropertyObjectAdvancedTest, IntFloatDictGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + DictPtr serverDict = obj.getPropertyValue("IntFloatDict"); + DictPtr clientDict = clientObj.getPropertyValue("IntFloatDict"); + + for (auto key : serverDict.getKeyList()) + ASSERT_EQ(serverDict.get(key), clientDict.get(key)); + + auto dict = Dict(); + dict.set(10, 5.678); + clientObj.setPropertyValue("IntFloatDict", dict); + + serverDict = obj.getPropertyValue("IntFloatDict"); + clientDict = clientObj.getPropertyValue("IntFloatDict"); + + for (auto key : serverDict.getKeyList()) + ASSERT_EQ(serverDict.get(key), clientDict.get(key)); +} + + +TEST_F(TmsPropertyObjectAdvancedTest, ObjectGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto nonClassObj = PropertyObject(); + nonClassObj.addProperty(IntProperty("test", 0)); + obj.addProperty(ObjectProperty("NonClassObj", nonClassObj)); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + PropertyObjectPtr serverChildObj = obj.getPropertyValue("Object"); + PropertyObjectPtr clientChildObj = clientObj.getPropertyValue("Object"); + ASSERT_EQ(serverChildObj.getPropertyValue("ObjNumber"), clientChildObj.getPropertyValue("ObjNumber")); + + auto testObj = PropertyObject(); + testObj.addProperty(IntProperty("test", 0)); + + ASSERT_NO_THROW(clientObj.setPropertyValue("Object", testObj)); + ASSERT_EQ(getLastMessage(), "Failed to set value for property \"Object\" on OpcUA client property object: Object type properties cannot be set over OpcUA"); + + ASSERT_NO_THROW(clientChildObj.setPropertyValue("ObjNumber", 1)); + + ASSERT_EQ(clientChildObj.getPropertyValue("ObjNumber"), 1); + PropertyObjectPtr serverNonClassObj = obj.getPropertyValue("NonClassObj"); + PropertyObjectPtr clientNonClassObj = clientObj.getPropertyValue("NonClassObj"); + ASSERT_EQ(serverNonClassObj.getPropertyValue("test"), clientNonClassObj.getPropertyValue("test")); + + clientNonClassObj.setPropertyValue("test", 1); + ASSERT_EQ(serverNonClassObj.getPropertyValue("test"), clientNonClassObj.getPropertyValue("test")); + + serverNonClassObj.setPropertyValue("test", 2); + ASSERT_EQ(serverNonClassObj.getPropertyValue("test"), clientNonClassObj.getPropertyValue("test")); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ReferencedGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto serverReferenceProp = obj.getProperty("IntOrFloat"); + auto clientReferenceProp = clientObj.getProperty("IntOrFloat"); + + auto defValue1 = serverReferenceProp.getDefaultValue(); + auto defValue2 = clientReferenceProp.getDefaultValue(); + ASSERT_EQ(serverReferenceProp.getDefaultValue(), clientReferenceProp.getDefaultValue()); + + ASSERT_EQ(obj.getPropertyValue("IntOrFloat"), 1); + ASSERT_EQ(clientObj.getPropertyValue("IntOrFloat"), 1); + + clientObj.setPropertyValue("Integer", 5); + clientObj.setPropertyValue("Float", 1.123); + + ASSERT_DOUBLE_EQ(serverReferenceProp.getDefaultValue(), 4.877); + ASSERT_DOUBLE_EQ(clientReferenceProp.getDefaultValue(), 4.877); + + clientObj.setPropertyValue("IntOrFloat", 2.345); + ASSERT_DOUBLE_EQ(obj.getPropertyValue("Float"), 2.345); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("Float"), 2.345); + + ASSERT_EQ(obj.getPropertyValue("IntOrFloat"), 2.345); + ASSERT_EQ(clientObj.getPropertyValue("IntOrFloat"), 2.345); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ReferencedGetSetObj) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto serverReferenceProp = obj.getProperty("ObjectOrString"); + auto clientReferenceProp = clientObj.getProperty("ObjectOrString"); + + PropertyObjectPtr serverDefaultValue = serverReferenceProp.getDefaultValue(); + PropertyObjectPtr clientDefaultValue = clientReferenceProp.getDefaultValue(); + + ASSERT_EQ(serverDefaultValue.getPropertyValue("ObjNumber"), clientDefaultValue.getPropertyValue("ObjNumber")); + + clientObj.setPropertyValue("Integer", 7); + + ASSERT_EQ(serverReferenceProp.getDefaultValue(), clientReferenceProp.getDefaultValue()); + + clientObj.setPropertyValue("ObjectOrString", "foobar"); + ASSERT_EQ(clientObj.getPropertyValue("String"), "foobar"); + ASSERT_EQ(obj.getPropertyValue("String"), "foobar"); +} + +TEST_F(TmsPropertyObjectAdvancedTest, SelectionListGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto serverIntegerUnitProp = obj.getProperty("IntegerUnit"); + auto clientIntegerUnitProp = clientObj.getProperty("IntegerUnit"); + + auto serverRangeProp = obj.getProperty("Range"); + auto clientRangeProp = clientObj.getProperty("Range"); + + ListPtr serverRangeSelectionValues = serverRangeProp.getSelectionValues(); + ListPtr clientRangeSelectionValues = clientRangeProp.getSelectionValues(); + + for (SizeT i = 0; i < serverRangeSelectionValues.getCount(); ++i) + ASSERT_DOUBLE_EQ(serverRangeSelectionValues[i], clientRangeSelectionValues[i]); + + ListPtr serverIntegerUnitSelectionValues = serverIntegerUnitProp.getSelectionValues(); + ListPtr clientIntegerUnitSelectionValues = clientIntegerUnitProp.getSelectionValues(); + + for (SizeT i = 0; i < serverIntegerUnitSelectionValues.getCount(); ++i) + ASSERT_EQ(serverIntegerUnitSelectionValues[i], clientIntegerUnitSelectionValues[i]); + + ASSERT_DOUBLE_EQ(clientObj.getPropertySelectionValue("Range"), obj.getPropertySelectionValue("Range")); + clientObj.setPropertyValue("Range", 1); + ASSERT_DOUBLE_EQ(clientObj.getPropertySelectionValue("Range"), obj.getPropertySelectionValue("Range")); + + ASSERT_EQ(clientObj.getPropertySelectionValue("IntegerUnit"), obj.getPropertySelectionValue("IntegerUnit")); + clientObj.setPropertyValue("IntegerUnit", 1); + ASSERT_EQ(clientObj.getPropertySelectionValue("IntegerUnit"), obj.getPropertySelectionValue("IntegerUnit")); +} + +TEST_F(TmsPropertyObjectAdvancedTest, SelectionDictGetSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto serverStringSparseSelectionProp = obj.getProperty("StringSparseSelection"); + auto clientStringSparseSelectionProp = clientObj.getProperty("StringSparseSelection"); + + ASSERT_EQ(serverStringSparseSelectionProp.getSelectionValues(), clientStringSparseSelectionProp.getSelectionValues()); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ValidationCoercion) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getPropertyValue("ValidatedInt"), clientObj.getPropertyValue("ValidatedInt")); + ASSERT_EQ(obj.getPropertyValue("CoercedInt"), clientObj.getPropertyValue("CoercedInt")); + + ASSERT_NO_THROW(clientObj.setPropertyValue("ValidatedInt", 11)); + ASSERT_EQ(getLastMessage(), "Failed to set value for property \"ValidatedInt\" on OpcUA client property object: Writing property value"); + ASSERT_EQ(obj.getPropertyValue("ValidatedInt"), 5); + ASSERT_EQ(clientObj.getPropertyValue("ValidatedInt"), 5); + + ASSERT_NO_THROW(clientObj.setPropertyValue("CoercedInt", 15)); + ASSERT_EQ(obj.getPropertyValue("CoercedInt"), 10); + ASSERT_EQ(clientObj.getPropertyValue("CoercedInt"), 10); +} + +TEST_F(TmsPropertyObjectAdvancedTest, NestedObjTest) +{ + const auto obj = PropertyObject(manager, "TestClass"); + + auto obj1 = PropertyObject(); + auto obj2 = PropertyObject(); + obj2.addProperty(IntProperty("foo", 10)); + obj1.addProperty(ObjectProperty("child", obj2)); + obj.addProperty(ObjectProperty("child", obj1)); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), 10); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ObjectPropWithMetadata) +{ + const auto obj = PropertyObject(manager, "TestClass"); + + const auto propObj = PropertyObject(); + propObj.addProperty(StringProperty("foo", "bar")); + const auto newObjProp = ObjectPropertyBuilder("LocalObjectWithMetadata", propObj).setReadOnly(true).setVisible(false).build(); + obj.addProperty(newObjProp); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + PropertyObjectPtr clientObjWithMetadata = clientObj.getPropertyValue("ObjectWithMetadata"); + ASSERT_NO_THROW(clientObjWithMetadata.setPropertyValue("foo", "notbar")); + ASSERT_EQ(clientObjWithMetadata.getPropertyValue("foo"), "notbar"); + + PropertyObjectPtr clientLocalObjWithMetadata = clientObj.getPropertyValue("LocalObjectWithMetadata"); + ASSERT_NO_THROW(clientLocalObjWithMetadata.setPropertyValue("foo", "notbar")); + ASSERT_EQ(clientLocalObjWithMetadata.getPropertyValue("foo"), "notbar"); + + PropertyPtr clientObjectWithMetadataProp = clientObj.getProperty("ObjectWithMetadata"); + PropertyPtr serverObjectWithMetadataProp = obj.getProperty("ObjectWithMetadata"); + ASSERT_EQ(clientObjectWithMetadataProp.getVisible(), serverObjectWithMetadataProp.getVisible()); + ASSERT_EQ(clientObjectWithMetadataProp.getReadOnly(), serverObjectWithMetadataProp.getReadOnly()); + + PropertyPtr clientLocalObjectWithMetadataProp = clientObj.getProperty("LocalObjectWithMetadata"); + PropertyPtr serverLocalObjectWithMetadataProp = obj.getProperty("LocalObjectWithMetadata"); + ASSERT_EQ(clientLocalObjectWithMetadataProp.getVisible(), serverLocalObjectWithMetadataProp.getVisible()); + ASSERT_EQ(clientLocalObjectWithMetadataProp.getReadOnly(), serverLocalObjectWithMetadataProp.getReadOnly()); +} + +TEST_F(TmsPropertyObjectAdvancedTest, FunctionCall) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + PropertyObjectPtr serverChildObj = obj.getPropertyValue("Object"); + PropertyObjectPtr clientChildObj = clientObj.getPropertyValue("Object"); + + ListPtr inputArgs = List(1, 1.123); + + FunctionPtr serverFunc = serverChildObj.getPropertyValue("function"); + FunctionPtr clientFunc = clientChildObj.getPropertyValue("function"); + + ASSERT_EQ(serverFunc(inputArgs), clientFunc(inputArgs)); + + inputArgs = List(-5, -7.23); + ASSERT_EQ(serverFunc(inputArgs), clientFunc(inputArgs)); +} + +TEST_F(TmsPropertyObjectAdvancedTest, ProcedureCall) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + PropertyObjectPtr clientChildObj = clientObj.getPropertyValue("Object"); + + ListPtr inputArgs = List(Ratio(1, 2), "foo", true); + + ProcedurePtr clientFunc = clientChildObj.getPropertyValue("procedure"); + ASSERT_NO_THROW(clientFunc(inputArgs)); + + ASSERT_EQ(testRatio, Ratio(1, 2)); + ASSERT_EQ(testString, "foo"); + ASSERT_EQ(testBool, true); + + testRatio = nullptr; + testString = nullptr; + testBool = nullptr; +} + +TEST_F(TmsPropertyObjectAdvancedTest, StructureGet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + //ASSERT_EQ(clientObj.getPropertyValue("UnitStruct"), obj.getPropertyValue("UnitStruct")); + //ASSERT_EQ(clientObj.getPropertyValue("ArgumentStruct"), obj.getPropertyValue("ArgumentStruct")); + ASSERT_EQ(clientObj.getPropertyValue("DeviceDomainStructure"), obj.getPropertyValue("DeviceDomainStructure")); + ASSERT_EQ(clientObj.getPropertyValue("ListRuleDescriptionStructure"), obj.getPropertyValue("ListRuleDescriptionStructure")); + ASSERT_EQ(clientObj.getPropertyValue("CustomRuleDescriptionStructure"), obj.getPropertyValue("CustomRuleDescriptionStructure")); + ASSERT_EQ(clientObj.getPropertyValue("AdditionalParametersType"), obj.getPropertyValue("AdditionalParametersType")); +} + +TEST_F(TmsPropertyObjectAdvancedTest, StructureSet) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + //clientObj.setPropertyValue("UnitStruct", Unit("V", 5, "volt", "voltage")); + //clientObj.setPropertyValue("ArgumentStruct", ArgumentInfo("test", ctFloat)); + clientObj.setPropertyValue("DeviceDomainStructure", + Struct("DeviceDomainStructure", + Dict({{"Resolution", Ratio(20, 30)}, + {"TicksSinceOrigin", 5000}, + {"Origin", "origin1"}, + {"Unit", Unit("symbol1", 2, "name1", "quantity1")}}), + manager)); + clientObj.setPropertyValue("ListRuleDescriptionStructure", + Struct("ListRuleDescriptionStructure", + Dict({{"Type", "List"}, {"Elements", List("foo1", "bar1")}}), + manager)); + clientObj.setPropertyValue("CustomRuleDescriptionStructure", + Struct("CustomRuleDescriptionStructure", + Dict({{"Type", "List"}, + {"Parameters", + Dict({{"foo", "bar"}, {"foo1", "bar1"}})}}), + manager)); + + const auto keyValuePairList = List( + Struct("KeyValuePair", Dict({{"Key", "key2"}, {"Value", "value2"}}), manager), + Struct("KeyValuePair", Dict({{"Key", "key2"}, {"Value", "value2"}}), manager)); + + clientObj.setPropertyValue("AdditionalParametersType", + Struct("AdditionalParametersType", + Dict({{"Parameters", keyValuePairList}}), + manager)); + + //ASSERT_EQ(clientObj.getPropertyValue("UnitStruct"), obj.getPropertyValue("UnitStruct")); + //ASSERT_EQ(clientObj.getPropertyValue("ArgumentStruct"), obj.getPropertyValue("ArgumentStruct")); + ASSERT_EQ(clientObj.getPropertyValue("DeviceDomainStructure"), obj.getPropertyValue("DeviceDomainStructure")); + ASSERT_EQ(clientObj.getPropertyValue("ListRuleDescriptionStructure"), obj.getPropertyValue("ListRuleDescriptionStructure")); + ASSERT_EQ(clientObj.getPropertyValue("CustomRuleDescriptionStructure"), obj.getPropertyValue("CustomRuleDescriptionStructure")); + ASSERT_EQ(clientObj.getPropertyValue("AdditionalParametersType"), obj.getPropertyValue("AdditionalParametersType")); +} + +TEST_F(TmsPropertyObjectAdvancedTest, PropertyOrder) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto serverProps = obj.getAllProperties(); + auto clientProps = clientObj.getAllProperties(); + + ASSERT_EQ(serverProps.getCount(), clientProps.getCount()); + + for (SizeT i = 0; i < serverProps.getCount(); ++i) + ASSERT_EQ(serverProps[i].getName(), clientProps[i].getName()); +} + +TEST_F(TmsPropertyObjectAdvancedTest, GainScalingStructure) +{ + const auto typeManager = TypeManager(); + const auto type = StructType("GainScalingStructure", List("Factor", "Offset"), List(SimpleType(ctFloat), SimpleType(ctFloat))); + typeManager.addType(type); + + const auto obj = PropertyObject(); + const auto structBuilder = StructBuilder("GainScalingStructure", typeManager).set("Factor", 0.5).set("Offset", 2.5); + obj.addProperty(StructProperty("Gain", structBuilder.build())); + + + const auto logger = Logger(); + const auto serverProp = + std::make_shared(obj, server, Context(nullptr, logger, manager, nullptr, nullptr), serverContext); + const auto nodeId = serverProp->registerOpcUaNode(); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + const auto structObj = obj.getPropertyValue("Gain"); + const auto newBuilder = StructBuilder(structObj); + clientObj.setPropertyValue("Gain", newBuilder.set("Factor", 2.0).set("Offset", 10.0).build()); +} + +TEST_F(TmsPropertyObjectAdvancedTest, BeginEndUpdate) +{ + bool eventTriggered = false; + + const auto obj = PropertyObject(manager, "TestClass"); + obj.addProperty(IntProperty("Prop1", 1, true)); + obj.addProperty(IntProperty("Prop2", 2, true)); + obj.addProperty(IntProperty("Prop3", 3, true)); + + obj.getOnEndUpdate() += [&eventTriggered](PropertyObjectPtr&, EndUpdateEventArgsPtr& args) + { + ASSERT_EQ(args.getEventName(), "EndUpdateEvent"); + + auto properties = args.getProperties(); + testing::ElementsAre("Prop1", "Prop2", "Prop3"); + + eventTriggered = true; + }; + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + clientObj.beginUpdate(); + clientObj.setPropertyValue("Prop1", 10); + clientObj.setPropertyValue("Prop2", 20); + clientObj.setPropertyValue("Prop3", 30); + clientObj.endUpdate(); + + ASSERT_TRUE(eventTriggered); +} + +TEST_F(TmsPropertyObjectAdvancedTest, NativeStructTypes) +{ + const auto obj = PropertyObject(manager, "TestClass"); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getPropertyValue("UnitStruct"), clientObj.getPropertyValue("UnitStruct")); + ASSERT_EQ(obj.getPropertyValue("ArgumentStruct"), clientObj.getPropertyValue("ArgumentStruct")); + ASSERT_EQ(obj.getPropertyValue("RangeStruct"), clientObj.getPropertyValue("RangeStruct")); + ASSERT_EQ(obj.getPropertyValue("ComplexNumberStruct"), clientObj.getPropertyValue("ComplexNumberStruct")); + ASSERT_EQ(obj.getPropertyValue("FunctionBlockTypeStruct"), clientObj.getPropertyValue("FunctionBlockTypeStruct")); + + clientObj.setPropertyValue("UnitStruct", Unit("new_symbol", 2, "new_name", "new_quantity")); + clientObj.setPropertyValue("ArgumentStruct", ArgumentInfo("new_name", CoreType::ctFloat)); + clientObj.setPropertyValue("RangeStruct", Range(100, 2000)); + clientObj.setPropertyValue("ComplexNumberStruct", ComplexNumber(100, 2000)); + clientObj.setPropertyValue("FunctionBlockTypeStruct", FunctionBlockType("new_id", "new_name", "new_desc")); + + ASSERT_EQ(obj.getPropertyValue("UnitStruct"), Unit("new_symbol", 2, "new_name", "new_quantity")); + ASSERT_EQ(obj.getPropertyValue("ArgumentStruct"), ArgumentInfo("new_name", CoreType::ctFloat)); + ASSERT_EQ(obj.getPropertyValue("RangeStruct"), Range(100, 2000)); + ASSERT_EQ(obj.getPropertyValue("ComplexNumberStruct"), ComplexNumber(100, 2000)); + ASSERT_EQ(obj.getPropertyValue("FunctionBlockTypeStruct"), FunctionBlockType("new_id", "new_name", "new_desc")); + + ASSERT_EQ(obj.getPropertyValue("UnitStruct"), clientObj.getPropertyValue("UnitStruct")); + ASSERT_EQ(obj.getPropertyValue("ArgumentStruct"), clientObj.getPropertyValue("ArgumentStruct")); + ASSERT_EQ(obj.getPropertyValue("RangeStruct"), clientObj.getPropertyValue("RangeStruct")); + ASSERT_EQ(obj.getPropertyValue("ComplexNumberStruct"), clientObj.getPropertyValue("ComplexNumberStruct")); + ASSERT_EQ(obj.getPropertyValue("FunctionBlockTypeStruct"), clientObj.getPropertyValue("FunctionBlockTypeStruct")); +} + diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_amplifier.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_amplifier.cpp new file mode 100644 index 0000000..e233f3b --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_amplifier.cpp @@ -0,0 +1,805 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +struct RegisteredPropertyObject +{ + TmsServerPropertyObjectPtr serverProp; + PropertyObjectPtr clientProp; +}; + +using namespace daq; + +class TMSAmplifierTest : public TmsObjectIntegrationTest +{ +protected: + + TypeManagerPtr objManager; + + void SetUp() override + { + TmsObjectIntegrationTest::SetUp(); + + // create class with name "STGAmplifier" + auto stgAmplClass = + PropertyObjectClassBuilder("StgAmp") + .addProperty(SelectionProperty( + "Measurement", List("Voltage", "Bridge", "Resistance", "Temperature", "Current", "Potentiometer"), 0)) + .addProperty(BoolProperty("DualCore", False)) + .addProperty(ReferenceProperty("Range", + EvalValue("switch($Measurement, 0, %VoltageRange, 1, %BridgeRange, 2, %ResistanceRange, 3, " + "%TemperatureRange, 4, %CurrentRange, 5, %PotentiometerRange)"))) + .addProperty(SelectionProperty("RangeUnit", List("Amplifier range", "Scaled range"), 0)) + .addProperty(SelectionProperty("LowPassFilter", + List("Anti-aliasing filter (IIR)", + "AAF (zero-phase distortion)", + "30 kHz", + "10 kHz", + "3 kHz", + "1 khZ", + "300 Hz", + "100 Hz", + "30 Hz", + "10 Hz"), + 0)) + .addProperty(ReferenceProperty( + "InputType", EvalValue("switch($Measurement, 0, %VoltageInputType, 3, %TemperatureInputType, 4, %CurrentInputType)"))) + .addProperty(ReferenceProperty( + "Excitation", + EvalValue("switch($Measurement, 0, %VoltageExcitation, 4, %CurrentExcitation, 5, %PotentiometerExcitation)"))) + .addProperty(ReferenceProperty("ExcitationUnit", EvalValue("switch($Measurement, 0, %VoltageExcitationUnit))"))) + .addProperty(SelectionProperty("Short", List("On", "Off"), 1)) + .addProperty(FloatProperty("PhysicalScale", 1.0, false)) + .addProperty(FloatProperty("PhysicalOffset", 0.0, false)) + .addProperty(StringProperty("PhysicalUnit", "V", false)) + .addProperty( + SelectionPropertyBuilder( + "VoltageRange", + EvalValue( + "[50.0, 10.0, 1.0, 0.1] * if($RangeUnit == 0, 1, $PhysicalScale) + if($RangeUnit == 0, 0, $PhysicalOffset)"), + 0) + .setUnit(EvalValue("if($RangeUnit == 0, Unit('V'), Unit($PhysicalUnit))")) + .build()) + .addProperty(SelectionProperty("VoltageInputType", List("Differential", "Single ended"), 1)) + .addProperty(FloatPropertyBuilder("VoltageExcitation", 6) + .setVisible(EvalValue("$VoltageInputType == 0")) + .setUnit(EvalValue("Unit(%VoltageExcitationUnit:SelectedValue)")) + .setSuggestedValues(EvalValue("If($VoltageExcitationUnit == 0, [20.0, 15.0, 10.0, 5.0, 2.5, 1.0, 0.0], " + "[60.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.0])")) + .build()) + .addProperty(SelectionProperty("VoltageExcitationUnit", List("V", "mA"), 0, EvalValue("$VoltageInputType == 0"))) + .addProperty( + SelectionPropertyBuilder( + "PotentiometerRange", + EvalValue( + "[20000.0, 2000.0, 200.0] * if($RangeUnit == 0, 1, $PhysicalScale) + if($RangeUnit == 0, 0, $PhysicalOffset)"), + 0) + .setUnit(EvalValue("if($RangeUnit == 0, Unit('V'), Unit($PhysicalUnit))")) + .build()) + .addProperty(FloatPropertyBuilder("PotentiometerExcitation", 6) + .setSuggestedValues(List(20.0, 15.0, 10.0, 5.0, 2.5, 1.0, 0.0)) + .setUnit(Unit("V")) + .build()) + .addProperty(SelectionPropertyBuilder("TemperatureRange", List("-200 .. 850"), 0) + .setUnit(EvalValue("if($RangeUnit == 0, Unit('C'), Unit($PhysicalUnit))")) + .build()) + .addProperty(SelectionProperty("TemperatureInputType", List("PT100", "PT200", "PT500", "PT1000", "PT2000"), 0)) + .addProperty(SelectionPropertyBuilder("ResistanceRange", + EvalValue("[100000.0, 10000.0, 1000.0, 100.0] * if($RangeUnit == 0, 1, " + "$PhysicalScale) + if($RangeUnit == 0, 0, $PhysicalOffset)"), + 0) + .setUnit(EvalValue("if($RangeUnit == 0, Unit('V'), Unit($PhysicalUnit))")) + .build()) + .addProperty(SelectionPropertyBuilder("BridgeRange", List(1000.0, 200.0, 20.0, 2.0), 0) + .setUnit(EvalValue("if($RangeUnit == 0, Unit('mV'), Unit($PhysicalUnit))")) + .build()) + .addProperty(SelectionProperty( + "BridgeMode", List("Full", "Half", "Quarter 3-wire", "Quarter 4-wire"), 0, EvalValue("$Measurement == 1"))) + .addProperty(ReferenceProperty( + "BridgeResistance", + EvalValue("if($BridgeMode == 0 || $BridgeMode == 1, %BridgeCustomResistance, $BridgeFixedResistance)"))) + .addProperty(FloatPropertyBuilder("BridgeCustomResistance", 120.0) + .setVisible(EvalValue("$Measurement == 1")) + .setUnit(Unit("Ohm")) + .setSuggestedValues(List(120.0, 350.0)) + .build()) + .addProperty(SelectionPropertyBuilder("BridgeFixedResistance", List(120.0, 350.0), 0) + .setVisible( EvalValue("$Measurement == 1")) + .setUnit(Unit("Ohm")) + .build()) + .addProperty(SelectionProperty("BridgeShunt", + List("Off", "Sns + 100kOhm", "Quarter 3-wire", "Quarter 4-wire"), + 0, + EvalValue("$Measurement == 1"))) + .addProperty(FloatPropertyBuilder("BridgeExcitation", 6) + .setSuggestedValues(EvalValue("If($BridgeExcitationUnit == 0, [20.0, 15.0, 10.0, 5.0, 2.5, 1.0, 0.0], " + "[60.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.0])")) + .setUnit(EvalValue("Unit(%BridgeExcitationUnit:SelectedValue)")) + .build()) + .addProperty(SelectionProperty("BridgeExcitationUnit", List("V", "mA"), 0)) + .addProperty( + SelectionProperty("Shunt", List("On", "Off"), 1, EvalValue("($Measurement == 1) && ($BridgeShunt == 1)"))) + .addProperty(FunctionProperty("Balance", ProcedureInfo(), EvalValue("$Measurement == 1"))) + .addProperty(FunctionProperty("Reset", ProcedureInfo(), EvalValue("$Measurement == 1"))) + .addProperty(FloatPropertyBuilder("SensorUnbalance", 0).setVisible(EvalValue("$Measurement == 1")).setUnit(Unit("mv/V")).build()) + .addProperty(SelectionPropertyBuilder("CurrentRange", EvalValue("[1000.0, 200.0, 20.0, 2.0] * $ResistorDivider"), 1) + .setUnit(EvalValue("if($RangeUnit == 0, Unit($ExternalShuntUnit), Unit($PhysicalUnit))")) + .build()) + .addProperty(SelectionProperty("CurrentInputType", List("Ext. direct shunt", "Ext. loop powered shunt"), 0)) + .addProperty(FloatPropertyBuilder("CurrentExcitation", 6) + .setVisible(EvalValue("$CurrentInputType == 1")) + .setSuggestedValues(List(20.0, 15.0, 10.0, 5.0, 2.5, 1.0, 0.0)) + .setUnit(Unit("V")) + .build()) + .addProperty(SelectionProperty("ExternalShunt", + List("Shunt 1", + "Shunt 2", + "Mini-1mR", + "Mini-500mR", + "Ato-1mR", + "Ato-500mR", + "Maxi-0.2mR", + "Maxi-500mR", + "Strip-0.2mR", + "Strip-500mR", + "Custom"), + 0, + EvalValue("$Measurement == 4"))) + .addProperty( + StringProperty("ExternalShuntUnit", EvalValue("if($ExternalShunt == 0 || $ExternalShunt == 1, 'mA', 'A')"), false)) + .addProperty(FloatProperty("ResistorDivider", + EvalValue("switch($ExternalShunt," + " 0, 1.0," + " 1, 0.5," + " 2, 50.0," + " 3, 0.1," + " 4, 50.0," + " 5, 0.1," + " 6, 250.0," + " 7, 0.1," + " 8, 250.0," + " 9, 0.1," + " 10, 0.0" + ")"), + false)) + .addProperty(FloatPropertyBuilder("Resistor", + EvalValue("switch($ExternalShunt," + " 0, 50.0," + " 1, 0.1," + " 2, 0.001," + " 3, 0.5," + " 4, 0.001," + " 5, 0.5," + " 6, 0.0002," + " 7, 0.5," + " 8, 0.0002," + " 9, 0.5," + " 10, 50.0" + ")")) + .setVisible( EvalValue("$Measurement == 4")) + .setReadOnly(EvalValue("$ExternalShunt != 10")) + .setUnit(Unit("Ohm")) + .build()) + .addProperty(FloatPropertyBuilder("Pmax", + EvalValue("switch($ExternalShunt," + " 0, 0.25," + " 1, 2.5," + " 2, 0.4," + " 3, 2.0," + " 4, 0.9," + " 5, 2.0," + " 6, 0.32," + " 7, 2.0," + " 8, 2.0," + " 9, 2.0," + " 10, 0.125" + ")")) + .setVisible(EvalValue("$Measurement == 4")) + .setReadOnly(EvalValue("$ExternalShunt != 10")) + .setUnit(Unit("W")) + .build()) + .addProperty(FloatPropertyBuilder("Imax", 0.0) + .setVisible(EvalValue("$Measurement == 4")) + .setReadOnly(true) + .setUnit(EvalValue("Unit($ExternalShuntUnit)")) + .build()) + .build(); + + objManager = TypeManager(); + objManager.addType(stgAmplClass); + + // create class with name "LvAmp" + auto lvAmplClass = + PropertyObjectClassBuilder("LvAmp") + .addProperty(StringPropertyBuilder("ModuleName", "XHS LV").setReadOnly(true).build()) + .addProperty(StringPropertyBuilder("SettingRevision", "1,9").setReadOnly(true).build()) + .addProperty(SelectionProperty("Measurement", List("Voltage", "Bridge", "Current"), 0)) + .addProperty( + ReferenceProperty("Range", EvalValue("switch($Measurement, 0, %VoltageRange, 1, %BridgeRange, 2, %CurrentRange)"))) + .addProperty( + SelectionPropertyBuilder("VoltageRange", List(100.0, 50.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1, 0.05), 0) + .setUnit(Unit("V")) + .build()) + .addProperty(SelectionPropertyBuilder("BridgeRange", + EvalValue("If($Excitation == 0, [0.0], [100.0, 50.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.5, " + "0.2, 0.1, 0.05] * 1000.0 / $Excitation)"), + 0) + .setUnit(Unit("mv/V")) + .build()) + .addProperty( + SelectionPropertyBuilder("CurrentRange", List(100.0, 50.0, 20.0, 10.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1, 0.05), 0) + .setUnit(Unit("V")) + .build()) + .addProperty(SelectionProperty("InputType", List("SingleEnded", "Differential"), 0)) + .addProperty(ReferenceProperty("Excitation", EvalValue("switch($InputType, 0, %SEExcitation, 1, %DiffExcitation)"))) + .addProperty(FloatPropertyBuilder("SEExcitation", 0.0) + .setUnit(Unit("V")) + .setSuggestedValues(List(0.0)) + .setMinValue(0.0) + .setMaxValue(0.0) + .setReadOnly(true) + .build()) + .addProperty(SelectionProperty("ExcitationType", List("Unipolar", "Bipolar"), 0, EvalValue("$InputType == 1"))) + .addProperty(ReferenceProperty("DiffExcitation", + EvalValue("switch($ExcitationType, 0, %UnipolarExcitation, 1, %BipolarExcitation)"))) + .addProperty(FloatPropertyBuilder("UnipolarExcitation", 0) + .setSuggestedValues(List(0.0, 1.0, 2.5, 5.0, 10.0, 12.0, 15.0, 24.0)) + .setMinValue(0.0) + .setMaxValue(24.0) + .setUnit(Unit("V")) + .build()) + .addProperty(FloatPropertyBuilder("BipolarExcitation", 2) + .setSuggestedValues(List(2.0, 2.5, 5.0, 10.0, 12.0, 15.0, 24.0, 30.0)) + .setMinValue(0.0) + .setMaxValue(30.0) + .setUnit(Unit("V")) + .build()) + .build(); + + objManager.addType(lvAmplClass); + } + + void TearDown() override + { + objManager.removeType("StgAmp"); + objManager.removeType("LvAmp"); + } + + RegisteredPropertyObject registerPropertyObject(const PropertyObjectPtr& prop) + { + const auto context = Context(nullptr, logger, TypeManager(), nullptr, nullptr); + const auto serverProp = + std::make_shared(prop, server, context, std::make_shared(context, nullptr)); + const auto nodeId = serverProp->registerOpcUaNode(); + const auto clientProp = TmsClientPropertyObject(Context(nullptr, logger, TypeManager(), nullptr, nullptr), clientContext, nodeId); + return {serverProp, clientProp}; + } +}; + +TEST_F(TMSAmplifierTest, SetUpServerLv) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + + const auto serverProp = std::make_shared(obj, server, ctx, serverContext); + const auto nodeId = serverProp->registerOpcUaNode(); +} + +TEST_F(TMSAmplifierTest, SetUpServerStg) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + + const auto serverProp = std::make_shared(obj, server, ctx, serverContext); + const auto nodeId = serverProp->registerOpcUaNode(); +} + +TEST_F(TMSAmplifierTest, SetUpLv) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, clientObj] = registerPropertyObject(obj); +} + +TEST_F(TMSAmplifierTest, SetUpStg) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, clientObj] = registerPropertyObject(obj); +} + +TEST_F(TMSAmplifierTest, TestVisibleProp) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + + auto boundPropInfo = lvAmpl.getProperty("ExcitationType"); + + lvAmpl.setPropertyValue("InputType", 0); + ASSERT_FALSE(lvAmpl.getProperty("ExcitationType").getVisible()); + ASSERT_FALSE(boundPropInfo.getVisible()); + + lvAmpl.setPropertyValue("InputType", 1); + ASSERT_TRUE(lvAmpl.getProperty("ExcitationType").getVisible()); + ASSERT_TRUE(boundPropInfo.getVisible()); +} + +TEST_F(TMSAmplifierTest, Measurement0) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + + lvAmpl.setPropertyValue("Measurement", 0); + + lvAmpl.setPropertyValue("Range", 0); + Float rangeValue = lvAmpl.getPropertySelectionValue("Range"); + auto rangeProp = lvAmpl.getProperty("Range"); + ListPtr rangeValues = rangeProp.getSelectionValues(); + + ASSERT_EQ(rangeValues[0], rangeValue); + + lvAmpl.setPropertyValue("Measurement", 1); + rangeValue = lvAmpl.getPropertySelectionValue("Range"); + rangeProp = lvAmpl.getProperty("Range"); + rangeValues = rangeProp.getSelectionValues(); + + ASSERT_EQ(rangeValues[0], rangeValue); +} + +TEST_F(TMSAmplifierTest, Measurement0PropertySEtter) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + + lvAmpl.getProperty("Measurement").setValue(0); + + lvAmpl.getProperty("Range").setValue(0); + Float rangeValue = lvAmpl.getPropertySelectionValue("Range"); + auto rangeProp = lvAmpl.getProperty("Range"); + ListPtr rangeValues = rangeProp.getSelectionValues(); + + ASSERT_EQ(rangeValues[0], rangeValue); + + lvAmpl.getProperty("Measurement").setValue(1); + rangeValue = lvAmpl.getPropertySelectionValue("Range"); + rangeProp = lvAmpl.getProperty("Range"); + rangeValues = rangeProp.getSelectionValues(); + + ASSERT_EQ(rangeValues[0], rangeValue); +} + +TEST_F(TMSAmplifierTest, Measurement1) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + + lvAmpl.setPropertyValue("Measurement", 1); + auto exc = lvAmpl.getPropertyValue("Excitation"); + auto it = lvAmpl.getPropertyValue("InputType"); + auto se = lvAmpl.getPropertyValue("SEExcitation"); + + auto a = lvAmpl.getPropertyValue("BridgeRange"); + auto c = lvAmpl.getPropertyValue("Range"); + + auto aProp = lvAmpl.getProperty("BridgeRange"); + ListPtr bridgeValues = aProp.getSelectionValues(); + + auto cProp = lvAmpl.getProperty("Range"); + ListPtr rangeValues = cProp.getSelectionValues(); +} + +TEST_F(TMSAmplifierTest, Measurement2) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + + lvAmpl.setPropertyValue("Measurement", 2); + + auto a = lvAmpl.getPropertyValue("BridgeRange"); + auto c = lvAmpl.getPropertyValue("Range"); + + auto aProp = lvAmpl.getProperty("BridgeRange"); + auto bridgeValues = aProp.getSelectionValues(); + + auto cProp = lvAmpl.getProperty("Range"); + auto rangeValues = cProp.getSelectionValues(); +} + +TEST_F(TMSAmplifierTest, ReadRefPropertyValues) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + auto rangeProp = stgAmpl.getProperty("Range"); + ListPtr rangeValues = rangeProp.getSelectionValues(); + ASSERT_EQ(rangeValues.getCount(), 4u); + ASSERT_EQ(rangeValues.getItemAt(0), 50.0); +} + +TEST_F(TMSAmplifierTest, SetAndGetRefProperty) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + stgAmpl.setPropertyValue("Measurement", 0); + + auto rangeProp = stgAmpl.getProperty("Range"); + ListPtr rangeValues = rangeProp.getSelectionValues(); + + ASSERT_EQ(rangeValues.getCount(), 4u); + ASSERT_EQ(rangeValues.getItemAt(0), 50.0); + + stgAmpl.setPropertyValue("Range", 1); + stgAmpl.setPropertyValue("Measurement", 1); + + rangeProp = stgAmpl.getProperty("Range"); + rangeValues = rangeProp.getSelectionValues(); + + ASSERT_EQ(rangeValues.getCount(), 4u); + ASSERT_EQ(rangeValues.getItemAt(0), 1000.0); + + stgAmpl.setPropertyValue("Range", 2); + stgAmpl.setPropertyValue("Measurement", 0); + + ASSERT_EQ(stgAmpl.getPropertyValue("Range"), 1); +} + +TEST_F(TMSAmplifierTest, CheckVoltage) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + stgAmpl.setPropertyValue("PhysicalUnit", "Pa"); + + // check if by default is in single ended input mode + ASSERT_EQ(stgAmpl.getPropertyValue("InputType"), 1); + + // switch to input type to differential mode + stgAmpl.setPropertyValue("InputType", 0); + auto props = stgAmpl.getVisibleProperties(); + + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 13u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "InputType"); + //ASSERT_EQ(props.getItemAt(6).getName(), "Excitation"); + //ASSERT_EQ(props.getItemAt(7).getName(), "ExcitationUnit"); + //ASSERT_EQ(props.getItemAt(8).getName(), "Short"); + + // check if by default excitation unit is V + ASSERT_EQ(stgAmpl.getPropertyValue("ExcitationUnit"), 0); + + ASSERT_EQ(stgAmpl.getProperty("Excitation").getUnit(), Unit("V")); + + // check if excitation values correct + ListPtr excitationValues = stgAmpl.getProperty("Excitation").getSuggestedValues(); + ASSERT_EQ(excitationValues.getCount(), 7u); + ASSERT_EQ(excitationValues.getItemAt(0), 20.0); + ASSERT_EQ(excitationValues.getItemAt(1), 15.0); + + // switch to excitation unit to mA + stgAmpl.setPropertyValue("ExcitationUnit", 1); + props = stgAmpl.getVisibleProperties(); + ASSERT_EQ(stgAmpl.getProperty("Excitation").getUnit(), Unit("mA")); + + // check if excitation values correct + excitationValues = stgAmpl.getProperty("Excitation").getSuggestedValues(); + ASSERT_EQ(excitationValues.getCount(), 7u); + ASSERT_EQ(excitationValues.getItemAt(0), 60.0); + ASSERT_EQ(excitationValues.getItemAt(1), 20.0); + + // switch to single ended input type + // TODO: Clear not supported + //stgAmpl.clearPropertyValue("InputType"); + stgAmpl.setPropertyValue("InputType", 1); + props = stgAmpl.getVisibleProperties(); + + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 11u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "InputType"); + //ASSERT_EQ(props.getItemAt(6).getName(), "Short"); + + // check voltage range + ASSERT_EQ(stgAmpl.getPropertyValue("RangeUnit"), 0); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("V")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), 50.0); + + stgAmpl.setPropertyValue("PhysicalScale", 2.0); + stgAmpl.setPropertyValue("PhysicalOffset", 10.0); + + // change to scaled range unit + stgAmpl.setPropertyValue("RangeUnit", 1); + props = stgAmpl.getVisibleProperties(); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("Pa")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), 110.0); +} + +TEST_F(TMSAmplifierTest, CheckPotentiometer) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + stgAmpl.setPropertyValue("PhysicalUnit", "Pa"); + + stgAmpl.setPropertyValue("Measurement", 5); + + auto props = stgAmpl.getVisibleProperties(); + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 11u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "Excitation"); + //ASSERT_EQ(props.getItemAt(6).getName(), "Short"); + + // check if by default excitation unit is V + ASSERT_EQ(stgAmpl.getProperty("Excitation").getUnit(), Unit("V")); + + // check if excitation values correct + ListPtr excitationValues = stgAmpl.getProperty("Excitation").getSuggestedValues(); + ASSERT_EQ(excitationValues.getCount(), 7u); + ASSERT_EQ(excitationValues.getItemAt(0), 20.0); + ASSERT_EQ(excitationValues.getItemAt(1), 15.0); + + // check potentiometer range + ASSERT_EQ(stgAmpl.getPropertyValue("RangeUnit"), 0); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("V")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), 20000.0); + + stgAmpl.setPropertyValue("PhysicalScale", 2.0); + stgAmpl.setPropertyValue("PhysicalOffset", 10.0); + + // change to scaled range unit + stgAmpl.setPropertyValue("RangeUnit", 1); + props = stgAmpl.getVisibleProperties(); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("Pa")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), 40010.0); +} + +TEST_F(TMSAmplifierTest, CheckResistance) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + stgAmpl.setPropertyValue("PhysicalUnit", "Pa"); + + stgAmpl.setPropertyValue("Measurement", 2); + + auto props = stgAmpl.getVisibleProperties(); + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 10u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "Short"); + + // check resistance range + ASSERT_EQ(stgAmpl.getPropertyValue("RangeUnit"), 0); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("V")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), 100000.0); + + stgAmpl.setPropertyValue("PhysicalScale", 2.0); + stgAmpl.setPropertyValue("PhysicalOffset", 10.0); + + // change to scaled range unit + stgAmpl.setPropertyValue("RangeUnit", 1); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("Pa")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), 200010.0); +} + +TEST_F(TMSAmplifierTest, CheckTemperature) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + stgAmpl.setPropertyValue("PhysicalUnit", "F"); + + stgAmpl.setPropertyValue("Measurement", 3); + + auto props = stgAmpl.getVisibleProperties(); + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 11u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "InputType"); + //ASSERT_EQ(props.getItemAt(6).getName(), "Short"); + + // check temperature range + ASSERT_EQ(stgAmpl.getPropertyValue("RangeUnit"), 0); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("C")); + ASSERT_EQ(stgAmpl.getProperty("Range").getSelectionValues().asPtr().getItemAt(0), "-200 .. 850"); + + stgAmpl.setPropertyValue("PhysicalScale", 2.0); + stgAmpl.setPropertyValue("PhysicalOffset", 10.0); + + // change to scaled range unit + stgAmpl.setPropertyValue("RangeUnit", 1); + props = stgAmpl.getVisibleProperties(); + ASSERT_EQ(stgAmpl.getProperty("Range").getUnit(), Unit("F")); + // ASSERT_EQ(props.getItemAt(2).getSelectionValues().getItemAt(0), 200010.0); +} + +TEST_F(TMSAmplifierTest, CheckCurrent) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + stgAmpl.setPropertyValue("PhysicalUnit", "Pa"); + + // switch to current mode + stgAmpl.setPropertyValue("Measurement", 4); + auto allProps = stgAmpl.getAllProperties(); + auto props = stgAmpl.getVisibleProperties(); + + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 15u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "InputType"); + //ASSERT_EQ(props.getItemAt(6).getName(), "Short"); + //ASSERT_EQ(props.getItemAt(7).getName(), "BridgeExcitation"); + //ASSERT_EQ(props.getItemAt(8).getName(), "BridgeExcitationUnit"); + //ASSERT_EQ(props.getItemAt(9).getName(), "ExternalShunt"); + //ASSERT_EQ(props.getItemAt(10).getName(), "Resistor"); + //ASSERT_EQ(props.getItemAt(11).getName(), "Pmax"); + //ASSERT_EQ(props.getItemAt(12).getName(), "Imax"); + + // check range + auto currRange = stgAmpl.getProperty("CurrentRange"); + auto rangeProp = stgAmpl.getProperty("Range"); + auto item = rangeProp.getSelectionValues().asPtr().getItemAt(0); + ASSERT_EQ(rangeProp.getSelectionValues().asPtr().getItemAt(0), 1000.0); + + ASSERT_EQ(rangeProp.getUnit(), Unit("mA")); + + auto iMaxProp = stgAmpl.getProperty("Imax"); + ASSERT_EQ(iMaxProp.getUnit(), Unit("mA")); + + stgAmpl.setPropertyValue("ExternalShunt", 1); + + // check range + ASSERT_EQ(rangeProp.getSelectionValues().asPtr().getItemAt(0), 500.0); + + // check shunt values + ASSERT_EQ(stgAmpl.getPropertyValue("Resistor"), 0.1); + ASSERT_EQ(stgAmpl.getPropertyValue("Pmax"), 2.5); + + stgAmpl.setPropertyValue("InputType", 1); + props = stgAmpl.getVisibleProperties(); + + // TODO: function properties metadata is not transferred from server + ASSERT_EQ(props.getCount(), 16u); + + // TODO: props are out of order + //ASSERT_EQ(props.getItemAt(0).getName(), "Measurement"); + //ASSERT_EQ(props.getItemAt(1).getName(), "DualCore"); + //ASSERT_EQ(props.getItemAt(2).getName(), "Range"); + //ASSERT_EQ(props.getItemAt(3).getName(), "RangeUnit"); + //ASSERT_EQ(props.getItemAt(4).getName(), "LowPassFilter"); + //ASSERT_EQ(props.getItemAt(5).getName(), "InputType"); + //ASSERT_EQ(props.getItemAt(6).getName(), "Excitation"); + //ASSERT_EQ(props.getItemAt(7).getName(), "Short"); + //ASSERT_EQ(props.getItemAt(8).getName(), "BridgeExcitation"); + //ASSERT_EQ(props.getItemAt(9).getName(), "BridgeExcitationUnit"); + //ASSERT_EQ(props.getItemAt(10).getName(), "ExternalShunt"); + //ASSERT_EQ(props.getItemAt(11).getName(), "Resistor"); + //ASSERT_EQ(props.getItemAt(12).getName(), "Pmax"); + //ASSERT_EQ(props.getItemAt(13).getName(), "Imax"); + + // set custom shunt + stgAmpl.setPropertyValue("ExternalShunt", 10); + + // check shunt values + ASSERT_EQ(stgAmpl.getPropertyValue("Resistor"), 50.0); + ASSERT_EQ(stgAmpl.getPropertyValue("Pmax"), 0.125); +} + + +TEST_F(TMSAmplifierTest, GetRefPropAfterChange) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + auto rangeProp = stgAmpl.getProperty("Range"); + + auto voltageRangeProp = stgAmpl.getProperty("VoltageRange"); + stgAmpl.setPropertyValue("Measurement", 0); + ASSERT_EQ(rangeProp.getReferencedProperty().getName(), voltageRangeProp.getName()); + + auto bridgeRangeProp = stgAmpl.getProperty("BridgeRange"); + stgAmpl.setPropertyValue("Measurement", 1); + ASSERT_EQ(rangeProp.getReferencedProperty().getName(), bridgeRangeProp.getName()); +} + +TEST_F(TMSAmplifierTest, GetRefPropSelectionValuesAfterChange) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + auto rangeProp = stgAmpl.getProperty("Range"); + + auto voltageRangeValues = stgAmpl.getProperty("VoltageRange").getSelectionValues(); + stgAmpl.setPropertyValue("Measurement", 0); + ASSERT_EQ(rangeProp.getSelectionValues(), voltageRangeValues); + + auto bridgeRangeValues = stgAmpl.getProperty("BridgeRange").getSelectionValues(); + stgAmpl.setPropertyValue("Measurement", 1); + ASSERT_EQ(rangeProp.getSelectionValues(), bridgeRangeValues); +} + +TEST_F(TMSAmplifierTest, PropertyOrder1) +{ + const auto obj = PropertyObject(objManager, "StgAmp"); + auto [serverObj, stgAmpl] = registerPropertyObject(obj); + + auto serverProps = obj.getAllProperties(); + auto clientProps = stgAmpl.getAllProperties(); + + ASSERT_EQ(serverProps.getCount(), clientProps.getCount()); + + for (SizeT i = 0; i < serverProps.getCount(); ++i) + ASSERT_EQ(serverProps[i].getName(), clientProps[i].getName()); +} + +TEST_F(TMSAmplifierTest, PropertyOrder2) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + + auto serverProps = obj.getAllProperties(); + auto clientProps = lvAmpl.getAllProperties(); + + ASSERT_EQ(serverProps.getCount(), clientProps.getCount()); + + for (SizeT i = 0; i < serverProps.getCount(); ++i) + ASSERT_EQ(serverProps[i].getName(), clientProps[i].getName()); +} + +TEST_F(TMSAmplifierTest, PropertyGetValue) +{ + const auto obj = PropertyObject(objManager, "LvAmp"); + auto [serverObj, lvAmpl] = registerPropertyObject(obj); + for (const auto& prop : lvAmpl.getAllProperties()) + ASSERT_EQ(lvAmpl.getPropertyValue(prop.getName()), prop.getValue()); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_channel.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_channel.cpp new file mode 100644 index 0000000..d03e1d9 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_channel.cpp @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +class TmsChannelTest : public TmsObjectIntegrationTest +{ +public: + + ChannelPtr createChannel(const ContextPtr& ctx) + { + return MockChannel(ctx, nullptr, "mockch"); + } +}; + +TEST_F(TmsChannelTest, Create) +{ + ChannelPtr channel = createChannel(ctx); + auto tmsChannel = TmsServerChannel(channel, this->getServer(), ctx, serverContext); +} + +TEST_F(TmsChannelTest, Register) +{ + auto ctx = NullContext(); + ChannelPtr channel = createChannel(ctx); + auto serverChannel = TmsServerChannel(channel, this->getServer(), ctx, serverContext); // Not possible either + auto nodeId = serverChannel.registerOpcUaNode(); + + ChannelPtr clientChannel = TmsClientChannel(ctx, nullptr, "Ch", clientContext, nodeId); + ASSERT_TRUE(clientChannel.assigned()); +} + +TEST_F(TmsChannelTest, BrowseName) +{ + auto ctx = NullContext(); + ChannelPtr serverChannel = createChannel(ctx); + + auto tmsServerChannel = TmsServerChannel(serverChannel, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerChannel.registerOpcUaNode(); + + ChannelPtr clientChannel = TmsClientChannel(ctx, nullptr, "Ch", clientContext, nodeId); + + auto browseName = client->readBrowseName(nodeId); + ASSERT_EQ(browseName, "mockch"); + + auto dsiplayName = client->readDisplayName(nodeId); + ASSERT_EQ(dsiplayName, "mockch"); +} + +TEST_F(TmsChannelTest, AttrFunctionBlockType) +{ + auto ctx = NullContext(); + ChannelPtr serverChannel = createChannel(ctx); + + auto tmsServerChannel = TmsServerChannel(serverChannel, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerChannel.registerOpcUaNode(); + + ChannelPtr clientChannel = TmsClientChannel(ctx, nullptr, "Ch", clientContext, nodeId); + + auto type = clientChannel.getFunctionBlockType(); + ASSERT_EQ(type.getId(), "mock_ch"); + ASSERT_EQ(type.getName(), "mock_ch"); + ASSERT_EQ(type.getDescription(), ""); +} + +TEST_F(TmsChannelTest, MethodGetInputPorts) +{ + auto ctx = NullContext(); + ChannelPtr serverChannel = createChannel(ctx); + + auto tmsServerChannel = TmsServerChannel(serverChannel, this->getServer(), ctx, serverContext); + auto channelNodeId = tmsServerChannel.registerOpcUaNode(); + + ChannelPtr clientChannel = TmsClientChannel(ctx, nullptr, "Ch", clientContext, channelNodeId); + + auto inputPorts = clientChannel.getInputPorts(); + ASSERT_TRUE(inputPorts != nullptr); + + //TODO: Implement more test when input ports actually can be returned + ASSERT_EQ(inputPorts.getCount(), 2u); // Mock function block creates 2 input ports + ASSERT_EQ(inputPorts[0].getLocalId(), "TestInputPort1"); + ASSERT_EQ(inputPorts[1].getLocalId(), "TestInputPort2"); +} + +TEST_F(TmsChannelTest, MethodGetSignals) +{ + auto ctx = NullContext(); + ChannelPtr serverChannel = createChannel(ctx); + + auto tmsServerChannel = TmsServerChannel(serverChannel, this->getServer(), ctx, serverContext); + auto channelNodeId = tmsServerChannel.registerOpcUaNode(); + + auto clientChannel = TmsClientChannel(ctx, nullptr, "Ch", clientContext, channelNodeId); + + ListPtr signals; + ASSERT_NO_THROW(signals = clientChannel.getSignals()); + ASSERT_TRUE(signals.assigned()); + + ASSERT_EQ(signals.getCount(), 10u); + ASSERT_EQ(signals[0].getDescriptor().getName(), "Signal1"); + ASSERT_EQ(signals[1].getDescriptor().getName(), "Signal2"); + ASSERT_EQ(signals[2].getDescriptor().getName(), "Signal3"); + ASSERT_EQ(signals[3].getDescriptor().getName(), "Signal4"); + ASSERT_EQ(signals[4].getDescriptor().getName(), "Time"); + ASSERT_EQ(signals[5].getDescriptor().getName(), "ChangingTime"); + ASSERT_EQ(signals[6].getDescriptor().getName(), "ByteStep"); + ASSERT_EQ(signals[7].getDescriptor().getName(), "IntStep"); + ASSERT_EQ(signals[8].getDescriptor().getName(), "Sine"); + ASSERT_EQ(signals[9].getDescriptor().getName(), "ChangingSignal"); +} + +TEST_F(TmsChannelTest, MethodGetStatusSignal) +{ + auto ctx = NullContext(); + ChannelPtr serverChannel = createChannel(ctx); + + auto tmsServerChannel = TmsServerChannel(serverChannel, this->getServer(), ctx, serverContext); + auto channelNodeId = tmsServerChannel.registerOpcUaNode(); + + SignalPtr serverSignal = Signal(ctx, nullptr, "sig"); + auto tmsServerSignal = TmsServerSignal(serverSignal, this->getServer(), ctx, serverContext); + auto signalNodeId = tmsServerSignal.registerOpcUaNode(); + + OpcUaNodeId referenceTypeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASSTATUSSIGNAL); + getServer()->addReference(channelNodeId, referenceTypeId, signalNodeId, true); + + ChannelPtr clientChannel = TmsClientChannel(ctx, nullptr, "Ch", clientContext, channelNodeId); + + //EXPECT_THROW(clientChannel.getStatusSignal(), daq::opcua::OpcUaClientCallNotAvailableException); + + //TODO: Implement more test when signals actually can be returned + //SignalPtr clientSignal = TmsClientSignal(client, signalNodeId); + //auto statusSignal = clientChannel.getStatusSignal(); + //ASSERT_EQ(statusSignal, clientSignal); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_component.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_component.cpp new file mode 100644 index 0000000..238e5bd --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_component.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include "tms_object_integration_test.h" +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +struct RegisteredComponent +{ + TmsServerComponentPtr serverObject; + ComponentPtr serverComponent; + ComponentPtr clientComponent; +}; + +class TmsComponentTest : public TmsObjectIntegrationTest +{ +public: + ComponentPtr createTestComponent() + { + auto component = Component(ctx, nullptr, "test"); + + component.addProperty(StringProperty("foo", "bar")); + auto obj = PropertyObject(); + obj.addProperty(IntProperty("Int", 0)); + component.addProperty(ObjectProperty("obj", obj)); + + component.getTags().asPtr().add("tag1"); + component.getTags().asPtr().add("tag2"); + + + return component; + } + + RegisteredComponent registerTestComponent(const ComponentPtr& customComponent = nullptr) + { + RegisteredComponent component{}; + + if (customComponent == nullptr) + component.serverComponent = createTestComponent(); + else + component.serverComponent = customComponent; + + component.serverComponent.asPtr().enableCoreEventTrigger(); + component.serverObject = std::make_shared>(component.serverComponent, this->getServer(), ctx, serverContext); + auto nodeId = component.serverObject->registerOpcUaNode(); + component.clientComponent = TmsClientComponent(NullContext(), nullptr, "test", clientContext, nodeId); + return component; + } +}; + +TEST_F(TmsComponentTest, Create) +{ + auto component = createTestComponent(); + auto serverComponent = TmsServerComponent(component, this->getServer(), ctx, serverContext); +} + +TEST_F(TmsComponentTest, Register) +{ + auto component = registerTestComponent(); +} + +TEST_F(TmsComponentTest, Active) +{ + auto component = registerTestComponent(); + + component.clientComponent.setActive(false); + ASSERT_EQ(component.serverComponent.getActive(), component.clientComponent.getActive()); + + component.clientComponent.setActive(true); + ASSERT_EQ(component.serverComponent.getActive(), component.clientComponent.getActive()); +} + +TEST_F(TmsComponentTest, Tags) +{ + auto component = registerTestComponent(); + + auto serverTags = component.serverComponent.getTags(); + auto clientTags = component.clientComponent.getTags(); + + ASSERT_TRUE(clientTags.query("tag1") && clientTags.query("tag2")); + ASSERT_TRUE(clientTags.contains("tag1") && clientTags.contains("tag2")); +} + + +TEST_F(TmsComponentTest, ModifyTags) +{ + auto component = registerTestComponent(); + + auto serverTags = component.serverComponent.getTags(); + auto clientTags = component.clientComponent.getTags(); + + ASSERT_TRUE(clientTags.query("tag1") && clientTags.query("tag2")); + ASSERT_TRUE(clientTags.contains("tag1") && clientTags.contains("tag2")); + + serverTags.asPtr().remove("tag2"); + + ASSERT_TRUE(clientTags.query("tag1") && !clientTags.query("tag2")); + ASSERT_TRUE(clientTags.contains("tag1") && !clientTags.contains("tag2")); + + serverTags.asPtr().add("tag3"); + + ASSERT_TRUE(clientTags.query("tag1") && clientTags.query("tag3")); + ASSERT_TRUE(clientTags.contains("tag1") && clientTags.contains("tag3")); +} + +TEST_F(TmsComponentTest, Properties) +{ + auto component = registerTestComponent(); + + PropertyObjectPtr serverObj = component.serverComponent.getPropertyValue("obj"); + PropertyObjectPtr clientObj = component.clientComponent.getPropertyValue("obj"); + ASSERT_EQ(serverObj.getPropertyValue("Int"), clientObj.getPropertyValue("Int")); + ASSERT_EQ(component.serverComponent.getPropertyValue("foo"), component.clientComponent.getPropertyValue("foo")); + + component.clientComponent.setPropertyValue("foo", "notbar"); + ASSERT_EQ(component.serverComponent.getPropertyValue("foo"), component.clientComponent.getPropertyValue("foo")); +} + +TEST_F(TmsComponentTest, NameAndDescription) +{ + const auto component = registerTestComponent(); + ASSERT_EQ(component.serverComponent.getName(), component.clientComponent.getName()); + ASSERT_EQ(component.serverComponent.getDescription(), component.clientComponent.getDescription()); + + component.serverComponent.setName("new_name"); + component.serverComponent.setDescription("new_description"); + + ASSERT_EQ(component.serverComponent.getName(), component.clientComponent.getName()); + ASSERT_EQ(component.serverComponent.getDescription(), component.clientComponent.getDescription()); + + component.clientComponent.setName("newer_name"); + component.clientComponent.setDescription("newer_description"); + + ASSERT_EQ(component.serverComponent.getName(), "newer_name"); + ASSERT_EQ(component.clientComponent.getName(), "newer_name"); + ASSERT_EQ(component.serverComponent.getDescription(), "newer_description"); + ASSERT_EQ(component.clientComponent.getDescription(), "newer_description"); +} + +TEST_F(TmsComponentTest, NameAndDescriptionLocked) +{ + const auto name = "locked"; + const auto customComponent = Component(NullContext(), nullptr, name); + customComponent.asPtr().lockAttributes(List("Name", "Description")); + const auto component = registerTestComponent(customComponent); + + ASSERT_NO_THROW(component.clientComponent.setName("new_name")); + ASSERT_NO_THROW(component.clientComponent.setDescription("new_description")); + + ASSERT_EQ(component.clientComponent.getName(), name); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_device.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_device.cpp new file mode 100644 index 0000000..71a3fc9 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_device.cpp @@ -0,0 +1,602 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +class TmsDeviceTest : public TmsObjectIntegrationTest +{ +public: + InstancePtr createDevice() + { + const auto moduleManager = ModuleManager("[[none]]"); + auto context = Context(nullptr, Logger(), nullptr, moduleManager, nullptr); + 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://client_device"); + const auto device = instance.addDevice("daqmock://phys_device"); + const auto infoInternal = device.getInfo().asPtr(); + infoInternal.addServerCapability(ServerCapability("protocol_1", "protocol 1", ProtocolType::Streaming)); + infoInternal.addServerCapability(ServerCapability("protocol_2", "protocol 2", ProtocolType::Configuration)); + + instance.addFunctionBlock("mock_fb_uid"); + + return instance; + } +}; + +TEST_F(TmsDeviceTest, CreateClientDevice) +{ + DevicePtr serverDevice = createDevice(); + + auto tmsPropertyObject = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + auto ctx = NullContext(); + ASSERT_NO_THROW(TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId)); +} + +TEST_F(TmsDeviceTest, SubDevices) +{ + DevicePtr serverDevice = createDevice(); + + auto tmsPropertyObject = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + auto ctx = NullContext(); + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + ASSERT_EQ(clientDevice.getDevices().getCount(), 2u); +} + +TEST_F(TmsDeviceTest, FunctionBlocks) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto tmsPropertyObject = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + ListPtr functionBlocks; + ASSERT_NO_THROW(functionBlocks = clientDevice.getFunctionBlocks()); + ASSERT_EQ(functionBlocks.getCount(), 1u); + + auto type = functionBlocks[0].getFunctionBlockType(); + ASSERT_EQ(type.getId(), "mock_fb_uid"); +} + +TEST_F(TmsDeviceTest, GetSignals) +{ + DevicePtr serverDevice = createDevice(); + + auto tmsPropertyObject = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + auto ctx = NullContext(); + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + + ListPtr signals; + ASSERT_NO_THROW(signals = clientDevice.getSignals()); + ASSERT_EQ(signals.getCount(), 0u); + + auto devices = clientDevice.getDevices(); + for (auto subDevice : devices) + { + auto name = subDevice.getName(); + ASSERT_NO_THROW(signals = subDevice.getSignals()); + if (name == "MockPhysicalDevice") + ASSERT_EQ(signals.getCount(), 1u); + else + ASSERT_EQ(signals.getCount(), 0u); + } + + ASSERT_NO_THROW(signals = clientDevice.getSignals(search::Recursive(search::Visible()))); + // one private signal in MockFunctionBlockImpl. and one in MockPhysicalDeviceImpl + ASSERT_EQ(signals.getCount(), serverDevice.getSignals(search::Recursive(search::Visible())).getCount() - 2); +} + +TEST_F(TmsDeviceTest, GetChannels) +{ + DevicePtr serverDevice = createDevice(); + + auto tmsPropertyObject = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsPropertyObject.registerOpcUaNode(); + + auto ctx = NullContext(); + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + ListPtr channels; + ASSERT_NO_THROW(channels = clientDevice.getChannels()); + ASSERT_EQ(channels.getCount(), serverDevice.getChannels().getCount()); + + ASSERT_NO_THROW(channels = clientDevice.getChannels(search::Recursive(search::Visible()))); + ASSERT_EQ(channels.getCount(), serverDevice.getChannels(search::Recursive(search::Visible())).getCount()); +} + +// TODO: Enable once name and description are no longer props +TEST_F(TmsDeviceTest, DISABLED_Property) +{ + DevicePtr serverDevice = createDevice(); + + const auto sampleRateProp = FloatPropertyBuilder("SampleRate", 100.0).setUnit(Unit("Hz")).setMinValue(1.0).setMaxValue(1000000.0).build(); + + serverDevice.addProperty(sampleRateProp); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto ctx = NullContext(); + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + + auto serverVisibleProps = serverDevice.getVisibleProperties(); + auto visibleProperties = clientDevice.getVisibleProperties(); + ASSERT_EQ(visibleProperties.getCount(), 5u); + ASSERT_EQ(visibleProperties[4].getName(), "SampleRate"); + + auto properties = clientDevice.getAllProperties(); + ASSERT_EQ(properties.getCount(), 5u); + ASSERT_EQ(properties[4].getName(), "SampleRate"); + + ASSERT_TRUE(clientDevice.hasProperty("SampleRate")); + ASSERT_EQ(clientDevice.getPropertyValue("SampleRate"), 100.0); + + clientDevice.setPropertyValue("SampleRate", 14.0); + ASSERT_EQ(clientDevice.getPropertyValue("SampleRate"), 14.0); + ASSERT_EQ(serverDevice.getPropertyValue("SampleRate"), 14.0); + + ASSERT_EQ(clientDevice.getPropertyValue("userName"), ""); + ASSERT_EQ(clientDevice.getPropertyValue("location"), ""); +} + +TEST_F(TmsDeviceTest, DeviceInfo) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto serverSubDevices = serverDevice.getDevices(); + ASSERT_EQ(serverSubDevices.getCount(), 2u); + auto serverSubDevice = serverSubDevices[1]; + auto serverDeviceInfo = serverSubDevice.getInfo(); + + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + + auto clientSubDevices = clientDevice.getDevices(); + ASSERT_EQ(clientSubDevices.getCount(), 2u); + auto clientSubDevice = clientSubDevices[1]; + auto clientDeviceInfo = clientSubDevice.getInfo(); + + //TODO: Test connectionString and Location when implemented + ASSERT_EQ(clientDeviceInfo.getName(), serverDeviceInfo.getName()); + ASSERT_EQ(clientDeviceInfo.getManufacturer(), serverDeviceInfo.getManufacturer()); + ASSERT_EQ(clientDeviceInfo.getManufacturerUri(), serverDeviceInfo.getManufacturerUri()); + ASSERT_EQ(clientDeviceInfo.getModel(), serverDeviceInfo.getModel()); + ASSERT_EQ(clientDeviceInfo.getProductCode(), serverDeviceInfo.getProductCode()); + ASSERT_EQ(clientDeviceInfo.getDeviceRevision(), serverDeviceInfo.getDeviceRevision()); + ASSERT_EQ(clientDeviceInfo.getHardwareRevision(), serverDeviceInfo.getHardwareRevision()); + ASSERT_EQ(clientDeviceInfo.getSoftwareRevision(), serverDeviceInfo.getSoftwareRevision()); + ASSERT_EQ(clientDeviceInfo.getDeviceManual(), serverDeviceInfo.getDeviceManual()); + ASSERT_EQ(clientDeviceInfo.getDeviceClass(), serverDeviceInfo.getDeviceClass()); + ASSERT_EQ(clientDeviceInfo.getSerialNumber(), serverDeviceInfo.getSerialNumber()); + ASSERT_EQ(clientDeviceInfo.getProductInstanceUri(), serverDeviceInfo.getProductInstanceUri()); + ASSERT_EQ(clientDeviceInfo.getMacAddress(), serverDeviceInfo.getMacAddress()); + ASSERT_EQ(clientDeviceInfo.getParentMacAddress(), serverDeviceInfo.getParentMacAddress()); + ASSERT_EQ(clientDeviceInfo.getPlatform(), serverDeviceInfo.getPlatform()); + ASSERT_EQ(clientDeviceInfo.getPosition(), serverDeviceInfo.getPosition()); + ASSERT_EQ(clientDeviceInfo.getSystemType(), serverDeviceInfo.getSystemType()); + ASSERT_EQ(clientDeviceInfo.getSystemUuid(), serverDeviceInfo.getSystemUuid()); + + + ASSERT_EQ(clientDeviceInfo.getName(), "MockPhysicalDevice"); + ASSERT_EQ(clientDeviceInfo.getManufacturer(), "manufacturer"); + ASSERT_EQ(clientDeviceInfo.getManufacturerUri(), "manufacturer_uri"); + ASSERT_EQ(clientDeviceInfo.getModel(), "model"); + ASSERT_EQ(clientDeviceInfo.getProductCode(), "product_code"); + ASSERT_EQ(clientDeviceInfo.getHardwareRevision(), "hardware_revision"); + ASSERT_EQ(clientDeviceInfo.getSoftwareRevision(), "software_revision"); + ASSERT_EQ(clientDeviceInfo.getDeviceManual(), "device_manual"); + ASSERT_EQ(clientDeviceInfo.getDeviceClass(), "device_class"); + ASSERT_EQ(clientDeviceInfo.getSerialNumber(), "serial_number"); + ASSERT_EQ(clientDeviceInfo.getProductInstanceUri(), "product_instance_uri"); + ASSERT_EQ(clientDeviceInfo.getRevisionCounter(), 123); + ASSERT_EQ(clientDeviceInfo.getPropertyValue("custom_string"), "custom_string"); + ASSERT_EQ(clientDeviceInfo.getPropertyValue("custom_float"), 1.123); + ASSERT_EQ(clientDeviceInfo.getPropertyValue("custom_int"), 1); +} + +TEST_F(TmsDeviceTest, DeviceInfoServerCapabilities) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto serverSubDevices = serverDevice.getDevices(); + ASSERT_EQ(serverSubDevices.getCount(), 2u); + auto serverSubDevice = serverSubDevices[1]; + auto serverDeviceInfo = serverSubDevice.getInfo(); + + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + + auto clientSubDevices = clientDevice.getDevices(); + ASSERT_EQ(clientSubDevices.getCount(), 2u); + auto clientSubDevice = clientSubDevices[1]; + auto clientDeviceInfo = clientSubDevice.getInfo(); + ASSERT_EQ(serverDeviceInfo.getServerCapabilities().getCount(), 2u); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities().getCount(), 2u); + auto name = clientDeviceInfo.getServerCapabilities()[1].getProtocolName(); + auto id = clientDeviceInfo.getServerCapabilities()[1].getProtocolId(); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[0].getProtocolId(), serverDeviceInfo.getServerCapabilities()[0].getProtocolId()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[1].getProtocolId(), serverDeviceInfo.getServerCapabilities()[1].getProtocolId()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[0].getProtocolName(), serverDeviceInfo.getServerCapabilities()[0].getProtocolName()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[1].getProtocolName(), serverDeviceInfo.getServerCapabilities()[1].getProtocolName()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[0].getProtocolType(), serverDeviceInfo.getServerCapabilities()[0].getProtocolType()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[1].getProtocolType(), serverDeviceInfo.getServerCapabilities()[1].getProtocolType()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[0].getConnectionType(), serverDeviceInfo.getServerCapabilities()[0].getConnectionType()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[1].getConnectionType(), serverDeviceInfo.getServerCapabilities()[1].getConnectionType()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[0].getCoreEventsEnabled(), serverDeviceInfo.getServerCapabilities()[0].getCoreEventsEnabled()); + ASSERT_EQ(clientDeviceInfo.getServerCapabilities()[1].getCoreEventsEnabled(), serverDeviceInfo.getServerCapabilities()[1].getCoreEventsEnabled()); +} + +TEST_F(TmsDeviceTest, DeviceGetTicksSinceOrigin) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + auto clientSubDevices = clientDevice.getDevices(); + auto clientSubDevice = clientSubDevices[1]; + + auto ticksSinceOrigin = clientSubDevice.getTicksSinceOrigin(); + ASSERT_EQ(ticksSinceOrigin, 789); +} + +TEST_F(TmsDeviceTest, DeviceDomain) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto serverSubDevices = serverDevice.getDevices(); + ASSERT_EQ(serverSubDevices.getCount(), 2u); + auto serverSubDevice = serverSubDevices[1]; + auto serverDeviceInfo = serverSubDevice.getInfo(); + + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + + auto clientSubDevices = clientDevice.getDevices(); + ASSERT_EQ(clientSubDevices.getCount(), 2u); + auto clientSubDevice = clientSubDevices[1]; + + auto deviceDomain = clientSubDevice.getDomain(); + + auto resolution = deviceDomain.getTickResolution(); + ASSERT_EQ(resolution.getNumerator(), 123); + ASSERT_EQ(resolution.getDenominator(), 456); + + auto ticksSinceOrigin = clientSubDevice.getTicksSinceOrigin(); + ASSERT_EQ(ticksSinceOrigin, 789); + + auto origin = deviceDomain.getOrigin(); + ASSERT_EQ(origin, "Origin"); + + auto unit = deviceDomain.getUnit(); + ASSERT_EQ(unit.getId(), 987); + ASSERT_EQ(unit.getSymbol(), "UnitSymbol"); + ASSERT_EQ(unit.getName(), "UnitName"); + ASSERT_EQ(unit.getQuantity(), "UnitQuantity"); + +} + +TEST_F(TmsDeviceTest, CustomComponents) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto serverSubDevice = serverDevice.getDevices()[1]; + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "Dev", clientContext, nodeId); + auto clientSubDevice = clientDevice.getDevices()[1]; + + ASSERT_EQ(serverSubDevice.getItems().getCount(), clientSubDevice.getItems().getCount()); + auto serverComponentA1 = serverSubDevice.getItem("componentA").asPtr().getItems()[0]; + auto clientComponentA1 = clientSubDevice.getItem("componentA").asPtr().getItems()[0]; + ASSERT_EQ(serverComponentA1.getName(), clientComponentA1.getName()); + + auto serverComponentB = serverSubDevice.getItem("componentB"); + auto clientComponentB = clientSubDevice.getItem("componentB"); + ASSERT_EQ(serverComponentB.getName(), clientComponentB.getName()); +} + +TEST_F(TmsDeviceTest, CustomComponentsProperties) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto serverSubDevice = serverDevice.getDevices()[1]; + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "Dev", clientContext, nodeId); + auto clientSubDevice = clientDevice.getDevices()[1]; + + ASSERT_EQ(serverSubDevice.getItems().getCount(), clientSubDevice.getItems().getCount()); + auto serverComponentA1 = serverSubDevice.getItem("componentA").asPtr().getItems()[0]; + auto clientComponentA1 = clientSubDevice.getItem("componentA").asPtr().getItems()[0]; + ASSERT_EQ(serverComponentA1.getPropertyValue("StringProp"), clientComponentA1.getPropertyValue("StringProp")); + + auto serverComponentB = serverSubDevice.getItem("componentB"); + auto clientComponentB = clientSubDevice.getItem("componentB"); + ASSERT_EQ(serverComponentB.getName(), clientComponentB.getName()); + ASSERT_EQ(clientComponentB.getPropertyValue("IntProp"), clientComponentB.getPropertyValue("IntProp")); +} + +TEST_F(TmsDeviceTest, ComponentMethods) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + + auto serverSubDevice = serverDevice.getDevices()[1]; + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "Dev", clientContext, nodeId); + auto clientSubDevice = clientDevice.getDevices()[1]; + + ASSERT_EQ(serverSubDevice.getName(), clientSubDevice.getName()); + + auto tags = serverSubDevice.getTags(); + auto clientTags = clientSubDevice.getTags(); + + ASSERT_TRUE(tags.query("phys_device")); + ASSERT_TRUE(clientTags.query("phys_device")); + + clientDevice.setActive(false); + ASSERT_EQ(serverDevice.getActive(), clientDevice.getActive()); + + clientDevice.setActive(true); + ASSERT_EQ(serverDevice.getActive(), clientDevice.getActive()); +} + +TEST_F(TmsDeviceTest, DeviceProcedureProperty) +{ + auto ctx = NullContext(); + DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = serverTmsDevice.registerOpcUaNode(); + auto clientDevice = TmsClientRootDevice(ctx, nullptr, "Dev", clientContext, nodeId); + auto clientSubDevice = clientDevice.getDevices()[1]; + + auto procProp = clientSubDevice.getProperty("stop"); + ASSERT_EQ(procProp.getValueType(), ctProc); + + ProcedurePtr proc = clientSubDevice.getPropertyValue("stop"); + ASSERT_NO_THROW(proc()); +} + +TEST_F(TmsDeviceTest, SignalOrder) +{ + auto serverDevice = DefaultDevice(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverDevice.getItem("Sig"); + for (int i = 0; i < 100; ++i) + folder.addItem(Signal(NullContext(), folder, "sig_" + std::to_string(i))); + + auto tmsServerDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerDevice.registerOpcUaNode(); + DevicePtr clientDevice = TmsClientRootDevice(NullContext(), nullptr, "Dev", clientContext, nodeId); + + const auto serverSignals = serverDevice.getSignals(); + const auto clientSignals = clientDevice.getSignals(); + + for (SizeT i = 0; i < serverSignals.getCount(); ++i) + ASSERT_EQ(serverSignals[i].getName(), clientSignals[i].getName()); +} + +TEST_F(TmsDeviceTest, DeviceOrder) +{ + auto serverDevice = DefaultDevice(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverDevice.getItem("Dev"); + for (int i = 0; i < 100; ++i) + folder.addItem(DefaultDevice(NullContext(), folder, "dev_" + std::to_string(i))); + + auto tmsServerDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerDevice.registerOpcUaNode(); + DevicePtr clientDevice = TmsClientRootDevice(NullContext(), nullptr, "Dev", clientContext, nodeId); + + const auto serverDevices = serverDevice.getDevices(); + const auto clientDevices = clientDevice.getDevices(); + + for (SizeT i = 0; i < serverDevices.getCount(); ++i) + ASSERT_EQ(serverDevices[i].getName(), clientDevices[i].getName()); +} + +TEST_F(TmsDeviceTest, FunctionBlockOrder) +{ + auto serverDevice = DefaultDevice(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverDevice.getItem("FB"); + for (int i = 0; i < 100; ++i) + folder.addItem(DefaultFunctionBlock(NullContext(), folder, "fb_" + std::to_string(i))); + + auto tmsServerDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerDevice.registerOpcUaNode(); + DevicePtr clientDevice = TmsClientRootDevice(NullContext(), nullptr, "Dev", clientContext, nodeId); + + const auto serverFbs = serverDevice.getFunctionBlocks(); + const auto clientFbs = clientDevice.getFunctionBlocks(); + + for (SizeT i = 0; i < serverFbs.getCount(); ++i) + ASSERT_EQ(serverFbs[i].getName(), clientFbs[i].getName()); +} + +TEST_F(TmsDeviceTest, IOFolderOrder) +{ + auto serverDevice = DefaultDevice(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverDevice.getItem("IO"); + for (int i = 0; i < 100; ++i) + { + folder.addItem(IoFolder(NullContext(), folder, "cmp_" + std::to_string(i))); + folder.addItem(DefaultChannel(NullContext(), folder, "ch_" + std::to_string(i))); + } + + auto tmsServerDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerDevice.registerOpcUaNode(); + DevicePtr clientDevice = TmsClientRootDevice(NullContext(), nullptr, "Dev", clientContext, nodeId); + + const auto serverIO = serverDevice.getInputsOutputsFolder().getItems(); + const auto clientIO = clientDevice.getInputsOutputsFolder().getItems(); + + for (SizeT i = 0; i < serverIO.getCount(); ++i) + ASSERT_EQ(serverIO[i].getName(), clientIO[i].getName()); +} + +TEST_F(TmsDeviceTest, CustomComponentOrder) +{ + auto serverDevice = DefaultDevice(NullContext(), nullptr, "mock"); + auto folder = serverDevice.asPtr(); + for (int i = 0; i < 100; ++i) + folder->addCustomComponent(Component(NullContext(), folder, "cmp_" + std::to_string(i))); + + auto tmsServerDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerDevice.registerOpcUaNode(); + DevicePtr clientDevice = TmsClientRootDevice(NullContext(), nullptr, "Dev", clientContext, nodeId); + + const auto serverCmps = serverDevice.getCustomComponents(); + const auto clientCmps = clientDevice.getCustomComponents(); + + for (SizeT i = 0; i < serverCmps.getCount(); ++i) + ASSERT_EQ(serverCmps[i].getName(), clientCmps[i].getName()); +} + +TEST_F(TmsDeviceTest, SdkPackageVersion) +{ + auto serverDevice = createDevice(); + auto tmsServerDevice = TmsServerDevice(serverDevice.getRootDevice(), this->getServer(), ctx, serverContext); + auto nodeId = tmsServerDevice.registerOpcUaNode(); + DevicePtr clientDevice = TmsClientRootDevice(NullContext(), nullptr, "Dev", clientContext, nodeId); + + ASSERT_EQ(clientDevice.getInfo().getSdkVersion(), OPENDAQ_PACKAGE_VERSION); +} + +TEST_F(TmsDeviceTest, DeviceInfoChanges) +{ + const auto ctx = NullContext(); + const DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + const auto nodeId = serverTmsDevice.registerOpcUaNode(); + const auto serverDeviceInfo = serverDevice.getDevices()[1].getInfo(); + + const auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + const auto clientSubDevice = clientDevice.getDevices()[1]; + const auto clientDeviceInfo = clientSubDevice.getInfo(); + + ASSERT_EQ(serverDeviceInfo.getName(), clientDeviceInfo.getName()); + ASSERT_EQ(serverDeviceInfo.getLocation(), clientDeviceInfo.getLocation()); + + clientSubDevice.setName("new_name"); + clientSubDevice.setPropertyValue("location", "new_location"); + + ASSERT_EQ("new_name", clientDeviceInfo.getName()); + ASSERT_EQ("new_location", clientDeviceInfo.getLocation()); + + ASSERT_EQ(serverDeviceInfo.getName(), clientDeviceInfo.getName()); + ASSERT_EQ(serverDeviceInfo.getLocation(), clientDeviceInfo.getLocation()); +} + +TEST_F(TmsDeviceTest, DeviceInfoChangeableField) +{ + const auto ctx = NullContext(); + const DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + const auto nodeId = serverTmsDevice.registerOpcUaNode(); + const auto serverDeviceInfo = serverDevice.getDevices()[1].getInfo(); + + const auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + const auto clientSubDevice = clientDevice.getDevices()[1]; + const auto clientDeviceInfo = clientSubDevice.getInfo(); + + ASSERT_EQ(serverDeviceInfo.getPropertyValue("TestChangeableField"), clientDeviceInfo.getPropertyValue("TestChangeableField")); + + serverDeviceInfo.setPropertyValue("TestChangeableField", "new_value"); + ASSERT_EQ("new_value", serverDeviceInfo.getPropertyValue("TestChangeableField")); + ASSERT_EQ("new_value", clientDeviceInfo.getPropertyValue("TestChangeableField")); + + clientDeviceInfo.setPropertyValue("TestChangeableField", "new_value_2"); + ASSERT_EQ("new_value_2", serverDeviceInfo.getPropertyValue("TestChangeableField")); + ASSERT_EQ("new_value_2", clientDeviceInfo.getPropertyValue("TestChangeableField")); +} + +TEST_F(TmsDeviceTest, DeviceInfoNotChangeableField) +{ + const auto ctx = NullContext(); + const DevicePtr serverDevice = createDevice(); + + auto serverTmsDevice = TmsServerDevice(serverDevice, this->getServer(), ctx, serverContext); + const auto nodeId = serverTmsDevice.registerOpcUaNode(); + const auto serverDeviceInfo = serverDevice.getDevices()[1].getInfo(); + + const auto clientDevice = TmsClientRootDevice(ctx, nullptr, "dev", clientContext, nodeId); + const auto clientSubDevice = clientDevice.getDevices()[1]; + const auto clientDeviceInfo = clientSubDevice.getInfo(); + + ASSERT_EQ("manufacturer", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("manufacturer", clientDeviceInfo.getManufacturer()); + + { + ASSERT_ANY_THROW(serverDeviceInfo.setPropertyValue("manufacturer", "server_manufacturer")); + ASSERT_EQ("manufacturer", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("manufacturer", clientDeviceInfo.getManufacturer()); + + serverDeviceInfo.asPtr(true).setProtectedPropertyValue("manufacturer", "server_manufacturer_2"); + ASSERT_EQ("server_manufacturer_2", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("manufacturer", clientDeviceInfo.getManufacturer()); + + ASSERT_ANY_THROW(serverDeviceInfo.asPtr(true).setManufacturer("server_manufacturer_3")); + ASSERT_EQ("server_manufacturer_2", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("manufacturer", clientDeviceInfo.getManufacturer()); + } + + { + ASSERT_ANY_THROW(clientDeviceInfo.setPropertyValue("manufacturer", "client_manufacturer")); + ASSERT_EQ("server_manufacturer_2", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("manufacturer", clientDeviceInfo.getManufacturer()); + + clientDeviceInfo.asPtr(true).setProtectedPropertyValue("manufacturer", "client_manufacturer_3"); + ASSERT_EQ("server_manufacturer_2", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("client_manufacturer_3", clientDeviceInfo.getManufacturer()); + + ASSERT_ANY_THROW(clientDeviceInfo.asPtr(true).setManufacturer("client_manufacturer_3")); + ASSERT_EQ("server_manufacturer_2", serverDeviceInfo.getManufacturer()); + ASSERT_EQ("client_manufacturer_3", clientDeviceInfo.getManufacturer()); + } +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_folder.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_folder.cpp new file mode 100644 index 0000000..d20920e --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_folder.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +struct RegisteredFolder +{ + TmsServerFolderPtr serverObject; + FolderPtr serverFolder; + FolderPtr clientFolder; +}; + +class TmsFolderTest : public TmsObjectIntegrationTest +{ +public: + FolderPtr createTestFolder() + { + auto folder1 = Folder(NullContext(), nullptr, "parent"); + auto folder2 = Folder(NullContext(), folder1, "child"); + folder1.addItem(folder2); + auto leafFolder = Folder(NullContext(), folder2, "folder"); + folder2.addItem(leafFolder); + + folder2.addProperty(StringProperty("foo", "bar")); + auto obj = PropertyObject(); + obj.addProperty(IntProperty("Int", 0)); + leafFolder.addProperty(ObjectProperty("obj", obj)); + + folder1.getTags().asPtr().add("tag1"); + folder2.getTags().asPtr().add("tag2"); + + return folder1; + } + + FolderPtr createTestIOFolder() + { + auto folder1 = IoFolder(NullContext(), nullptr, "parent"); + auto folder2 = IoFolder(NullContext(), folder1, "child"); + folder1.addItem(folder2); + auto channel = MockChannel(NullContext(), folder2, "channel"); + folder2.addItem(channel); + + return folder1; + } + + RegisteredFolder registerTestFolder(const FolderPtr& testFolder) + { + RegisteredFolder folder{}; + + folder.serverFolder = testFolder; + folder.serverObject = std::make_shared(folder.serverFolder, this->getServer(), ctx, serverContext); + auto nodeId = folder.serverObject->registerOpcUaNode(); + if (testFolder.supportsInterface()) + folder.clientFolder = TmsClientIoFolder(NullContext(), nullptr, "test", clientContext, nodeId); + else + folder.clientFolder = TmsClientFolder(NullContext(), nullptr, "test", clientContext, nodeId); + + return folder; + } +}; + +TEST_F(TmsFolderTest, Create) +{ + auto folder = createTestFolder(); + auto serverFolder = TmsServerFolder(folder, this->getServer(), ctx, serverContext); +} + +TEST_F(TmsFolderTest, Register) +{ + auto folder = registerTestFolder(createTestFolder()); +} + +TEST_F(TmsFolderTest, Active) +{ + auto folder = registerTestFolder(createTestFolder()); + + folder.clientFolder.setActive(false); + ASSERT_EQ(folder.serverFolder.getActive(), folder.clientFolder.getActive()); + + folder.clientFolder.setActive(true); + ASSERT_EQ(folder.serverFolder.getActive(), folder.clientFolder.getActive()); + + folder.clientFolder.getItems()[0].setActive(false); + ASSERT_EQ(folder.serverFolder.getItems()[0].getActive(), folder.clientFolder.getItems()[0].getActive()); + + + folder.clientFolder.getItems()[0].setActive(false); + ASSERT_EQ(folder.serverFolder.getItems()[0].getActive(), folder.clientFolder.getItems()[0].getActive()); + + + folder.clientFolder.getItems()[0].asPtr().getItems()[0].setActive(false); + ASSERT_EQ(folder.serverFolder.getItems()[0].asPtr().getItems()[0].getActive(), + folder.clientFolder.getItems()[0].asPtr().getItems()[0].getActive()); + + + folder.clientFolder.getItems()[0].asPtr().getItems()[0].setActive(false); + ASSERT_EQ(folder.serverFolder.getItems()[0].asPtr().getItems()[0].getActive(), + folder.clientFolder.getItems()[0].asPtr().getItems()[0].getActive()); +} + +TEST_F(TmsFolderTest, Tags) +{ + auto folder = registerTestFolder(createTestFolder()); + + auto serverTags = folder.serverFolder.getTags().getList(); + auto clientTags = folder.clientFolder.getTags().getList(); + ASSERT_EQ(serverTags.getCount(), clientTags.getCount()); + ASSERT_EQ(serverTags[0], clientTags[0]); + + auto serverTags1 = folder.serverFolder.getItems()[0].getTags().getList(); + auto clientTags2 = folder.clientFolder.getItems()[0].getTags().getList(); + ASSERT_EQ(serverTags1.getCount(), clientTags2.getCount()); + ASSERT_EQ(serverTags1[0], clientTags2[0]); +} + +TEST_F(TmsFolderTest, Properties) +{ + auto folder = registerTestFolder(createTestFolder()); + + PropertyObjectPtr serverObj = folder.serverFolder.getItems()[0].asPtr().getItems()[0].getPropertyValue("obj"); + PropertyObjectPtr clientObj = folder.clientFolder.getItems()[0].asPtr().getItems()[0].getPropertyValue("obj"); + ASSERT_EQ(serverObj.getPropertyValue("Int"), clientObj.getPropertyValue("Int")); + + ASSERT_EQ(folder.serverFolder.getItems()[0].getPropertyValue("foo"), folder.clientFolder.getItems()[0].getPropertyValue("foo")); + + folder.clientFolder.getItems()[0].setPropertyValue("foo", "notbar"); + ASSERT_EQ(folder.serverFolder.getItems()[0].getPropertyValue("foo"), folder.clientFolder.getItems()[0].getPropertyValue("foo")); +} + +TEST_F(TmsFolderTest, IOFolder) +{ + auto folder = registerTestFolder(createTestIOFolder()); + + ASSERT_TRUE(folder.serverFolder.asPtrOrNull().assigned()); + ASSERT_TRUE(folder.clientFolder.asPtrOrNull().assigned()); + + ASSERT_TRUE(folder.serverFolder.getItems()[0].asPtrOrNull().assigned()); + ASSERT_TRUE(folder.clientFolder.getItems()[0].asPtrOrNull().assigned()); + + ASSERT_TRUE(folder.serverFolder.getItems()[0].asPtrOrNull().getItems()[0].asPtrOrNull().assigned()); + ASSERT_TRUE(folder.clientFolder.getItems()[0].asPtrOrNull().getItems()[0].asPtrOrNull().assigned()); +} + +TEST_F(TmsFolderTest, IOFolderNodeOrder) +{ + auto folder = IoFolder(NullContext(), nullptr, "parent"); + for (int i = 0; i < 100; ++i) + { + folder.addItem(IoFolder(NullContext(), folder, "child" + std::to_string(i))); + } + + for (int i = 0; i < 10; ++i) + { + folder.addItem(MockChannel(NullContext(), folder, "channel" + std::to_string(i))); + } + + auto registered = registerTestFolder(folder); + + auto serverItems = folder.getItems(); + auto clientItems = registered.clientFolder.getItems(); + + for (SizeT i = 0; i < serverItems.getCount(); ++i) + ASSERT_EQ(serverItems[i].getName(), clientItems[i].getName()); +} + +TEST_F(TmsFolderTest, FolderNodeOrder) +{ + auto folder = Folder(NullContext(), nullptr, "parent"); + for (int i = 0; i < 100; ++i) + { + folder.addItem(Folder(NullContext(), folder, "child" + std::to_string(i))); + } + + auto registered = registerTestFolder(folder); + + auto serverItems = folder.getItems(); + auto clientItems = registered.clientFolder.getItems(); + + for (SizeT i = 0; i < serverItems.getCount(); ++i) + ASSERT_EQ(serverItems[i].getName(), clientItems[i].getName()); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block.cpp new file mode 100644 index 0000000..97ad4d0 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block.cpp @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include + +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +class TmsFunctionBlockTest : public TmsObjectIntegrationTest +{ +public: + FunctionBlockPtr createFunctionBlock(const FunctionBlockTypePtr& type = FunctionBlockType("UID", "Name", "Desc")) + { + const auto context = NullContext(); + return MockFunctionBlock(type, context, nullptr, "mockfb"); + } +}; + +TEST_F(TmsFunctionBlockTest, Create) +{ + FunctionBlockPtr functionBlock = createFunctionBlock(); + auto tmsFunctionBlock = TmsServerFunctionBlock(functionBlock, this->getServer(), ctx, serverContext); +} + +TEST_F(TmsFunctionBlockTest, Register) +{ + FunctionBlockPtr functionBlock = createFunctionBlock(); + auto serverFunctionBlock = TmsServerFunctionBlock(functionBlock, this->getServer(), ctx, serverContext); // Not possible either + auto nodeId = serverFunctionBlock.registerOpcUaNode(); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, nodeId); + ASSERT_TRUE(clientFunctionBlock.assigned()); +} + +TEST_F(TmsFunctionBlockTest, BrowseName) +{ + // Build functionBlock info: + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + + // Build server functionBlock + auto serverFunctionBlock = createFunctionBlock(type); + + ASSERT_EQ(serverFunctionBlock.getFunctionBlockType(), type); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, nodeId); + + auto browseName = client->readBrowseName(nodeId); + ASSERT_EQ(browseName, "mockfb"); + + auto displayName = client->readDisplayName(nodeId); + ASSERT_EQ(displayName, "mockfb"); +} + +TEST_F(TmsFunctionBlockTest, AttrFunctionBlockType) +{ + // Build functionBlock info: + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + + // Build server functionBlock + auto serverFunctionBlock = createFunctionBlock(type); + + ASSERT_EQ(serverFunctionBlock.getFunctionBlockType(), type); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, nodeId); + + auto clientType = clientFunctionBlock.getFunctionBlockType(); + ASSERT_EQ(clientType.getId(), "UNIQUE ID"); + ASSERT_EQ(clientType.getName(), "NAME"); + ASSERT_EQ(clientType.getDescription(), "DESCRIPTION"); +} + +TEST_F(TmsFunctionBlockTest, MethodGetInputPorts) +{ + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + auto serverFunctionBlock = createFunctionBlock(type); + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + + auto inputPorts = clientFunctionBlock.getInputPorts(); + ASSERT_TRUE(inputPorts != nullptr); + + ASSERT_EQ(inputPorts.getCount(), 2u); // Mock function block creates 2 input ports + ASSERT_EQ(inputPorts[0].getLocalId(), "TestInputPort1"); + ASSERT_EQ(inputPorts[1].getLocalId(), "TestInputPort2"); +} + +TEST_F(TmsFunctionBlockTest, MethodGetSignals) +{ + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + auto serverFunctionBlock = createFunctionBlock(type); + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + auto clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + + ListPtr serverSignals = serverFunctionBlock.getSignals(); + ListPtr signals; + ASSERT_NO_THROW(signals = clientFunctionBlock.getSignals()); + ASSERT_TRUE(signals.assigned()); + + ASSERT_EQ(signals.getCount(), 4u); + ASSERT_EQ(signals[0].getDescriptor().getName(), "Signal1"); + ASSERT_EQ(signals[1].getDescriptor().getName(), "Signal2"); + ASSERT_EQ(signals[2].getDescriptor().getName(), "Signal3"); + ASSERT_EQ(signals[3].getDescriptor().getName(), "Signal4"); +} + +TEST_F(TmsFunctionBlockTest, SignalCheckGlobalId) +{ + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + auto serverFunctionBlock = createFunctionBlock(type); + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + auto clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, serverFunctionBlock.getLocalId(), clientContext, functionBlockNodeId); + + ListPtr serverSignals = serverFunctionBlock.getSignals(); + ListPtr clientSignals = clientFunctionBlock.getSignals(); + + // one private signal in MockPhysicalDeviceImpl + ASSERT_EQ(clientSignals.getCount(), serverSignals.getCount() - 1); + + std::vector serverSignalsName; + for (const auto & signal : serverSignals) + serverSignalsName.push_back(signal.getGlobalId()); + + for (const auto & signal : clientSignals) + { + auto it = find(serverSignalsName.begin(), serverSignalsName.end(), signal.getGlobalId().toStdString()); + ASSERT_NE(it, serverSignalsName.end()); + } +} + +TEST_F(TmsFunctionBlockTest, MethodGetStatusSignal) +{ + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + auto serverFunctionBlock = createFunctionBlock(type); + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + SignalPtr serverSignal = Signal(NullContext(), nullptr, "sig"); + auto tmsServerSignal = TmsServerSignal(serverSignal, this->getServer(), ctx, serverContext); + auto signalNodeId = tmsServerSignal.registerOpcUaNode(); + + OpcUaNodeId referenceTypeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASSTATUSSIGNAL); + getServer()->addReference(functionBlockNodeId, referenceTypeId, signalNodeId, true); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, signalNodeId); + + auto statusSignal = clientFunctionBlock.getStatusSignal(); + ASSERT_EQ(statusSignal, clientSignal); +} + +TEST_F(TmsFunctionBlockTest, Property) +{ + const FunctionBlockTypePtr serverFunctionBlockType = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + auto serverFunctionBlock = createFunctionBlock(serverFunctionBlockType); + + const auto sampleRateProp = FloatPropertyBuilder("SampleRate", 100.0).setUnit(Unit("Hz")).setMinValue(1.0).setMaxValue(1000000.0).build(); + + serverFunctionBlock.addProperty(sampleRateProp); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + auto clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, nodeId); + + auto visibleProperties = clientFunctionBlock.getVisibleProperties(); + ASSERT_EQ(visibleProperties.getCount(), 3u); + ASSERT_EQ(visibleProperties[0].getName(), "TestConfigInt"); + ASSERT_EQ(visibleProperties[1].getName(), "TestConfigString"); + ASSERT_EQ(visibleProperties[2].getName(), "SampleRate"); + + auto properties = clientFunctionBlock.getAllProperties(); + ASSERT_EQ(properties.getCount(), 3u); + ASSERT_EQ(properties[0].getName(), "TestConfigInt"); + ASSERT_EQ(properties[1].getName(), "TestConfigString"); + ASSERT_EQ(properties[2].getName(), "SampleRate"); + + ASSERT_TRUE(clientFunctionBlock.hasProperty("SampleRate")); + ASSERT_EQ(clientFunctionBlock.getPropertyValue("SampleRate"), 100.0); + + clientFunctionBlock.setPropertyValue("SampleRate", 14.0); + ASSERT_EQ(clientFunctionBlock.getPropertyValue("SampleRate"), 14.0); + ASSERT_EQ(serverFunctionBlock.getPropertyValue("SampleRate"), 14.0); +} + +TEST_F(TmsFunctionBlockTest, NestedFunctionBlocks) +{ + auto serverFunctionBlock = createFunctionBlock(); + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + ASSERT_NO_THROW(clientFunctionBlock.getFunctionBlocks()); + auto nestedFunctionBlocks = clientFunctionBlock.getFunctionBlocks(); + ASSERT_EQ(nestedFunctionBlocks.getCount(), 1u); +} + +TEST_F(TmsFunctionBlockTest, ComponentMethods) +{ + auto serverFunctionBlock = createFunctionBlock(); + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + + ASSERT_EQ(serverFunctionBlock.getName(), clientFunctionBlock.getName()); + + auto tags = serverFunctionBlock.getTags(); + auto clientTags = clientFunctionBlock.getTags(); + + ASSERT_TRUE(tags.query("mock_fb")); + ASSERT_TRUE(clientTags.query("mock_fb")); + + clientFunctionBlock.setActive(false); + ASSERT_EQ(serverFunctionBlock.getActive(), clientFunctionBlock.getActive()); + + clientFunctionBlock.setActive(true); + ASSERT_EQ(serverFunctionBlock.getActive(), clientFunctionBlock.getActive()); +} + +TEST_F(TmsFunctionBlockTest, SignalOrder) +{ + auto serverFunctionBlock = DefaultFunctionBlock(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverFunctionBlock.getItem("Sig"); + for (int i = 0; i < 100; ++i) + folder.addItem(Signal(NullContext(), folder, "sig_" + std::to_string(i))); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + + const auto serverSignals = serverFunctionBlock.getSignals(); + const auto clientSignals = clientFunctionBlock.getSignals(); + + for (SizeT i = 0; i < serverSignals.getCount(); ++i) + ASSERT_EQ(serverSignals[i].getName(), clientSignals[i].getName()); +} + +TEST_F(TmsFunctionBlockTest, InputPortOrder) +{ + auto serverFunctionBlock = DefaultFunctionBlock(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverFunctionBlock.getItem("IP"); + for (int i = 0; i < 100; ++i) + folder.addItem(InputPort(NullContext(), folder, "ip_" + std::to_string(i))); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + + const auto serverInputPorts = serverFunctionBlock.getInputPorts(); + const auto clientInputPorts = clientFunctionBlock.getInputPorts(); + + for (SizeT i = 0; i < serverInputPorts.getCount(); ++i) + ASSERT_EQ(serverInputPorts[i].getName(), clientInputPorts[i].getName()); +} + +TEST_F(TmsFunctionBlockTest, FunctionBlockOrder) +{ + auto serverFunctionBlock = DefaultFunctionBlock(NullContext(), nullptr, "mock"); + FolderConfigPtr folder = serverFunctionBlock.getItem("FB"); + for (int i = 0; i < 40; ++i) + folder.addItem(DefaultFunctionBlock(NullContext(), folder, "fb_" + std::to_string(i))); + + auto tmsServerFunctionBlock = TmsServerFunctionBlock(serverFunctionBlock, this->getServer(), ctx, serverContext); + auto functionBlockNodeId = tmsServerFunctionBlock.registerOpcUaNode(); + FunctionBlockPtr clientFunctionBlock = TmsClientFunctionBlock(NullContext(), nullptr, "mockfb", clientContext, functionBlockNodeId); + + const auto serverFunctionBlocks = serverFunctionBlock.getFunctionBlocks(); + const auto clientFunctionBlocks = clientFunctionBlock.getFunctionBlocks(); + + for (SizeT i = 0; i < serverFunctionBlocks.getCount(); ++i) + ASSERT_EQ(serverFunctionBlocks[i].getName(), clientFunctionBlocks[i].getName()); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block_type.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block_type.cpp new file mode 100644 index 0000000..0c69209 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block_type.cpp @@ -0,0 +1,126 @@ +#include +#include "tms_object_integration_test.h" +#include +#include +#include +#include +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +class TmsFunctionBlockTypeTest : public TmsObjectIntegrationTest +{ +public: + FunctionBlockTypePtr createFunctionBlockType() + { + auto defaultConfig = PropertyObject(); + defaultConfig.addProperty(IntProperty("Port", 1000)); + defaultConfig.addProperty(StringProperty("Name", "vlado")); + defaultConfig.addProperty(ListProperty("Scaling", List(1.0, 2.0, 3.0))); + + return FunctionBlockType("RefFB", "Reference function block", "Description", defaultConfig); + } +}; + +TEST_F(TmsFunctionBlockTypeTest, Create) +{ + auto fbType = createFunctionBlockType(); + + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + ASSERT_TRUE(server->nodeExists(nodeId)); + + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + ASSERT_TRUE(clientFbType.assigned()); +} + +TEST_F(TmsFunctionBlockTypeTest, Values) +{ + auto fbType = createFunctionBlockType(); + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + ASSERT_EQ(fbType.getId(), clientFbType.getId()); + ASSERT_EQ(fbType.getName(), clientFbType.getName()); + ASSERT_EQ(fbType.getDescription(), clientFbType.getDescription()); + + auto serverConfig = fbType.createDefaultConfig(); + auto clientConfig = clientFbType.createDefaultConfig(); + ASSERT_TRUE(TestComparators::PropertyObjectEquals(serverConfig, clientConfig)); + + ASSERT_TRUE(TestComparators::FunctionBlockTypeEquals(fbType, clientFbType)); +} + +TEST_F(TmsFunctionBlockTypeTest, DefaultConfig) +{ + auto fbType = FunctionBlockType("Id", "", ""); + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + ASSERT_TRUE(TestComparators::FunctionBlockTypeEquals(fbType, clientFbType)); +} + +TEST_F(TmsFunctionBlockTypeTest, CloneConfig) +{ + auto fbType = createFunctionBlockType(); + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + auto clientConfig1 = clientFbType.createDefaultConfig(); + auto clientConfig2 = clientFbType.createDefaultConfig(); + clientConfig1.setPropertyValue("Name", "name1"); + clientConfig2.setPropertyValue("Name", "name2"); + + ASSERT_EQ(clientConfig1.getPropertyValue("Name"), "name1"); + ASSERT_EQ(clientConfig2.getPropertyValue("Name"), "name2"); +} + +TEST_F(TmsFunctionBlockTypeTest, ReadOnly) +{ + auto fbType = createFunctionBlockType(); + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + auto fbTypeVariant = VariantConverter::ToVariant(fbType); + ASSERT_THROW(client->writeValue(nodeId, fbTypeVariant), OpcUaException); + ASSERT_THROW(client->writeDisplayName(nodeId, "Display name"), OpcUaException); + ASSERT_THROW(client->writeDescription(nodeId, "Description"), OpcUaException); + + auto browser = CachedReferenceBrowser(client); + const auto defaultConfigId = browser.getChildNodeId(nodeId, "DefaultConfig"); + const auto nameId = browser.getChildNodeId(defaultConfigId, "Name"); + const auto portId = browser.getChildNodeId(defaultConfigId, "Port"); + const auto scalingId = browser.getChildNodeId(defaultConfigId, "Scaling"); + + ASSERT_THROW(client->writeValue(nameId, OpcUaVariant("value")), OpcUaException); + ASSERT_THROW(client->writeValue(portId, OpcUaVariant(1001)), OpcUaException); + ASSERT_THROW(client->writeValue(scalingId, OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsFunctionBlockTypeTest, DISABLED_NonDefaultValues) +{ + // This should work, but it doesnt, because of an error in client property object. + + auto defaultConfig = PropertyObject(); + defaultConfig.addProperty(IntProperty("Port", 0)); + defaultConfig.addProperty(StringProperty("Name", "")); + + defaultConfig.setPropertyValue("Port", 1000); + defaultConfig.setPropertyValue("Name", "vlado"); + + auto fbType = FunctionBlockType("RefFB", "Reference function block", "Description", defaultConfig); + + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + ASSERT_TRUE(TestComparators::FunctionBlockTypeEquals(fbType, clientFbType)); +} + diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_property.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_property.cpp new file mode 100644 index 0000000..fee0d76 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_property.cpp @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +struct RegisteredPropertyObject +{ + TmsServerPropertyObjectPtr serverProp; + PropertyObjectPtr clientProp; +}; + +class TmsFunctionTest: public TmsObjectIntegrationTest +{ +public: + RegisteredPropertyObject registerPropertyObject(const PropertyObjectPtr& obj) + { + const auto serverProp = std::make_shared(obj, server, ctx, serverContext); + const auto nodeId = serverProp->registerOpcUaNode(); + const auto clientProp = TmsClientPropertyObject(NullContext(logger), clientContext, nodeId); + return {serverProp, clientProp}; + } + + StringPtr getLastMessage() + { + logger.flush(); + auto sink = getPrivateSink(); + auto newMessage = sink.waitForMessage(2000); + if (newMessage == 0) + return StringPtr(""); + auto logMessage = sink.getLastMessage(); + return logMessage; + } +}; + +TEST_F(TmsFunctionTest, ProcedureNoArgs) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Proc", ProcedureInfo())); + bool called = false; + auto proc = Procedure([&called]() { called = true; }); + obj.setPropertyValue("Proc", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + ProcedurePtr clientProc = clientObj.getPropertyValue("Proc"); + ASSERT_NO_THROW(clientProc()); + ASSERT_EQ(called, true); +} + +TEST_F(TmsFunctionTest, FunctionNoArgs) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Func", FunctionInfo(ctBool))); + auto func = Function([]() { return true; }); + obj.setPropertyValue("Func", func); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + FunctionPtr clientFunc = clientObj.getPropertyValue("Func"); + ASSERT_TRUE(clientFunc()); +} + +TEST_F(TmsFunctionTest, ProcedureOneArg) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Proc", ProcedureInfo(List(ArgumentInfo("Int", ctInt))))); + Int callValue; + auto proc = Procedure([&callValue](const IntegerPtr& arg) { callValue = arg; }); + obj.setPropertyValue("Proc", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + ProcedurePtr clientProc = clientObj.getPropertyValue("Proc"); + + ASSERT_NO_THROW(clientProc(10)); + ASSERT_EQ(callValue, 10); + + ASSERT_NO_THROW(clientProc(100)); + ASSERT_EQ(callValue, 100); +} + +TEST_F(TmsFunctionTest, FunctionOneArg) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Func", FunctionInfo(ctInt, List(ArgumentInfo("Int", ctInt))))); + Int callValue; + auto func = Function([&callValue](const IntegerPtr& arg) { return arg; }); + obj.setPropertyValue("Func", func); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + FunctionPtr clientFunc = clientObj.getPropertyValue("Func"); + + ASSERT_EQ(clientFunc(10), 10); + ASSERT_EQ(clientFunc(100), 100); +} + +TEST_F(TmsFunctionTest, ProcedureMultipleArgs) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty( + "Proc", ProcedureInfo(List(ArgumentInfo("Int", ctInt), ArgumentInfo("Ratio", ctRatio), ArgumentInfo("String", ctString))))); + + Int intArg; + RatioPtr ratioArg; + StringPtr stringArg; + auto proc = Procedure([&intArg, &ratioArg, &stringArg](const ListPtr& args) + { + intArg = args[0]; + ratioArg = args[1]; + stringArg = args[2]; + }); + obj.setPropertyValue("Proc", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + ProcedurePtr clientProc = clientObj.getPropertyValue("Proc"); + + clientProc(10, Ratio(1, 20), "foo"); + + ASSERT_EQ(intArg, 10); + ASSERT_EQ(ratioArg, Ratio(1, 20)); + ASSERT_EQ(stringArg, "foo"); +} + +TEST_F(TmsFunctionTest, FunctionMultipleArgs) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty( + "Func", + FunctionInfo(ctBool, List(ArgumentInfo("Int", ctInt), ArgumentInfo("Ratio", ctRatio), ArgumentInfo("String", ctString))))); + + auto func = Function([](const ListPtr& args) + { + bool valid = args[0] == 10; + valid = valid && args[1] == Ratio(1, 20); + valid = valid && args[2] == "foo"; + return valid; + }); + obj.setPropertyValue("Func", func); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + FunctionPtr clientProc = clientObj.getPropertyValue("Func"); + + ASSERT_EQ(clientProc(10, Ratio(1, 20), "foo"), true); + ASSERT_EQ(clientProc(10, Ratio(1, 20), "bar"), false); +} + +TEST_F(TmsFunctionTest, AllArgTypes) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty( + "Proc", + ProcedureInfo(List(ArgumentInfo("Int", ctInt), + ArgumentInfo("Ratio", ctRatio), + ArgumentInfo("String", ctString), + ArgumentInfo("Bool", ctBool), + ArgumentInfo("Float", ctFloat))))); + + Int intArg; + RatioPtr ratioArg; + StringPtr stringArg; + Bool boolArg; + Float floatArg; + auto proc = Procedure( + [&intArg, &ratioArg, &stringArg, &boolArg, &floatArg](const ListPtr& args) + { + intArg = args[0]; + ratioArg = args[1]; + stringArg = args[2]; + boolArg = args[3]; + floatArg = args[4]; + }); + obj.setPropertyValue("Proc", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + ProcedurePtr clientProc = clientObj.getPropertyValue("Proc"); + + clientProc(10, Ratio(1, 20), "foo", false, 1.123); + + ASSERT_EQ(intArg, 10); + ASSERT_EQ(ratioArg, Ratio(1, 20)); + ASSERT_EQ(stringArg, "foo"); + ASSERT_EQ(boolArg, false); + ASSERT_EQ(floatArg, 1.123); +} + +TEST_F(TmsFunctionTest, InvalidArgTypes) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Proc", ProcedureInfo(List(ArgumentInfo("Int", ctInt))))); + Int callValue; + auto proc = Procedure([&callValue](const IntegerPtr& arg) { callValue = arg; }); + obj.setPropertyValue("Proc", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + ProcedurePtr clientProc = clientObj.getPropertyValue("Proc"); + + ASSERT_NO_THROW(clientProc("foo")); + ASSERT_EQ(getLastMessage(), "Failed to call procedure on OpcUA client. Error: \"Calling procedure\""); +} + +// NOTE: Should this throw an error? +TEST_F(TmsFunctionTest, InvalidReturnType) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Func", FunctionInfo(ctBool))); + auto func = Function([]() { return "str"; }); + obj.setPropertyValue("Func", func); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + FunctionPtr clientFunc = clientObj.getPropertyValue("Func"); + + ASSERT_EQ(clientFunc(), "str"); +} + +TEST_F(TmsFunctionTest, InvalidArgCount) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Proc", ProcedureInfo(List(ArgumentInfo("Int", ctInt))))); + Int callValue; + auto proc = Procedure([&callValue](const IntegerPtr& arg) { callValue = arg; }); + obj.setPropertyValue("Proc", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + ProcedurePtr clientProc = clientObj.getPropertyValue("Proc"); + + ASSERT_NO_THROW(clientProc()); + ASSERT_EQ(getLastMessage(), "Failed to call procedure on OpcUA client. Error: \"Calling procedure\""); + + ASSERT_NO_THROW(clientProc(1, 2)); + ASSERT_EQ(getLastMessage(), "Failed to call procedure on OpcUA client. Error: \"Calling procedure\""); +} + +TEST_F(TmsFunctionTest, ProcedureArgumentInfo) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty( + "Proc", + ProcedureInfo(List(ArgumentInfo("Int", ctInt), + ArgumentInfo("Ratio", ctRatio), + ArgumentInfo("String", ctString), + ArgumentInfo("Bool", ctBool), + ArgumentInfo("Float", ctFloat))))); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getProperty("Proc").getCallableInfo(), clientObj.getProperty("Proc").getCallableInfo()); +} + +TEST_F(TmsFunctionTest, FunctionArgumentInfo) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty( + "Func", + FunctionInfo(ctInt, List(ArgumentInfo("Int", ctInt), + ArgumentInfo("Ratio", ctRatio), + ArgumentInfo("String", ctString), + ArgumentInfo("Bool", ctBool), + ArgumentInfo("Float", ctFloat))))); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(obj.getProperty("Func").getCallableInfo(), clientObj.getProperty("Func").getCallableInfo()); +} + +TEST_F(TmsFunctionTest, UnsupportedArgumentInfo) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("proc1", ProcedureInfo(List(ArgumentInfo("List", ctList))))); + obj.addProperty(FunctionProperty("proc2", ProcedureInfo(List(ArgumentInfo("Dict", ctDict))))); + obj.addProperty(FunctionProperty("proc3", ProcedureInfo(List(ArgumentInfo("Proc", ctProc))))); + obj.addProperty(FunctionProperty("proc4", ProcedureInfo(List(ArgumentInfo("Object", ctObject))))); + obj.addProperty(FunctionProperty("proc5", ProcedureInfo(List(ArgumentInfo("binary_data", ctBinaryData))))); + obj.addProperty(FunctionProperty("proc6", ProcedureInfo(List(ArgumentInfo("Func", ctFunc))))); + obj.addProperty(FunctionProperty("proc7", ProcedureInfo(List(ArgumentInfo("complex_number", ctComplexNumber))))); + obj.addProperty(FunctionProperty("proc8", ProcedureInfo(List(ArgumentInfo("Undefined", ctUndefined))))); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(clientObj.getAllProperties().getCount(), 0u); +} + +TEST_F(TmsFunctionTest, UnsupportedReturnType) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("func1", FunctionInfo(ctList))); + obj.addProperty(FunctionProperty("func2", FunctionInfo(ctDict))); + obj.addProperty(FunctionProperty("func3", FunctionInfo(ctProc))); + obj.addProperty(FunctionProperty("func4", FunctionInfo(ctObject))); + obj.addProperty(FunctionProperty("func5", FunctionInfo(ctBinaryData))); + obj.addProperty(FunctionProperty("func6", FunctionInfo(ctFunc))); + obj.addProperty(FunctionProperty("func7", FunctionInfo(ctComplexNumber))); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_EQ(clientObj.getAllProperties().getCount(), 0u); +} + + +TEST_F(TmsFunctionTest, ServerThrow) +{ + auto obj = PropertyObject(); + obj.addProperty(FunctionProperty("Func", FunctionInfo(ctBool))); + auto func = Function([]() + { + throw GeneralErrorException{}; + return false; + }); + obj.setPropertyValue("Func", func); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + FunctionPtr clientFunc = clientObj.getPropertyValue("Func"); + ASSERT_NO_THROW(clientFunc()); + ASSERT_EQ(getLastMessage(), "Failed to call function on OpcUA client. Error in \"Calling function\""); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_fusion_device.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_fusion_device.cpp new file mode 100644 index 0000000..5329492 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_fusion_device.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +struct RegisteredPropertyObject +{ + TmsServerPropertyObjectPtr serverProp; + PropertyObjectPtr clientProp; +}; + +class TmsFusionDevice : public TmsObjectIntegrationTest +{ +protected: + TypeManagerPtr objManager; + + SignalPtr createSignal(const std::string& id) + { + SignalPtr signal = Signal(NullContext(), nullptr, id); + signal->setActive(false); + return signal; + } + + void SetUp() override + { + TmsObjectIntegrationTest::SetUp(); + objManager = TypeManager(); + + // Add Enumeration type to Type Manager + const auto enumExcitationType = EnumerationType( + "ExcitationTypeEnumeration", List("DoNotCare", "DCVoltage", "ACVoltage", "ACVoltageRectangle", "ACVoltageSinewave")); + objManager.addType(enumExcitationType); + + // create class with name "FusionAmp" + auto fusionAmpClass = + PropertyObjectClassBuilder("FusionAmp") + .addProperty(SelectionProperty("Measurement", List("Voltage", "FullBridge", "HalfBridge", "QuarterBridge"), 0)) + .addProperty(StructProperty("AdjustmentPoint", + Struct("AdjustmentPointScalingStructure", + Dict({{"Index", 1}, {"Factor", 2.1}, {"Offset", 3.0}}), + objManager))) + .addProperty(StructProperty( + "Scaler", Struct("GainScalingStructure", Dict({{"Factor", 2.1}, {"Offset", 3.0}}), objManager))) + .addProperty(EnumerationProperty("ExcitationType", Enumeration("ExcitationTypeEnumeration", "DCVoltage", objManager))) + .addProperty(StructProperty( + "FullBridge", + Struct("FullBridgeSensorStructure", + Dict( + {{"ExcitationVoltage", + Struct("ExcitationVoltageStructure", + Dict( + {{"ActualValue", 5.0}, + {"NominalValue", 5.0}, + {"NominalValueRange", Range(4.0, 6.0)}, + {"Type", Enumeration("ExcitationTypeEnumeration", "DCVoltage", objManager)}, + {"Frequency", 0}}), + objManager)}, + {"Resistance", 350.0}, + {"MaximumElectrical", 5.0}, + {"UsedWires", 6}}), + objManager))) + .addProperty(StructProperty("EUInformationWithQuantity", Unit("m/s", 1, "meter per second", "50"))) + .build(); + objManager.addType(fusionAmpClass); + } + + void TearDown() override + { + } + + RegisteredPropertyObject registerPropertyObject(const PropertyObjectPtr& prop) + { + const auto logger = Logger(); + const auto context = Context(nullptr, logger, objManager, nullptr, nullptr); + const auto serverProp = + std::make_shared(prop, server, context, std::make_shared(context, nullptr)); + const auto nodeId = serverProp->registerOpcUaNode(); + const auto clientProp = TmsClientPropertyObject(ctxClient, clientContext, nodeId); + return {serverProp, clientProp}; + } +}; + +TEST_F(TmsFusionDevice, SampleRateTest) +{ + SignalPtr daqServerSignal = createSignal("Id"); + daqServerSignal.addProperty(FloatProperty("SampleRate", 1.0, false)); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + // std::cin.get(); + + ASSERT_TRUE(clientSignal.getPublic()); + ASSERT_NO_THROW(clientSignal.getPropertyValue("SampleRate")); +} + +TEST_F(TmsFusionDevice, StructTest) +{ + const auto obj = PropertyObject(objManager, "FusionAmp"); + auto [serverObj, fusionAmp] = registerPropertyObject(obj); + + // Test struct with int and float values + const auto adjustmentPoint = StructBuilder(fusionAmp.getPropertyValue("AdjustmentPoint")); + adjustmentPoint.set("Index", 10); + adjustmentPoint.set("Factor", 3.1); + fusionAmp.setPropertyValue("AdjustmentPoint", adjustmentPoint.build()); + + const auto adjustmentPointManipulated = StructBuilder(fusionAmp.getPropertyValue("AdjustmentPoint")); + ASSERT_EQ(adjustmentPointManipulated.get("Index"), 10); + ASSERT_FLOAT_EQ(adjustmentPointManipulated.get("Factor"), (float) 3.1); + + // Test struct with double values + const auto scaler = StructBuilder(fusionAmp.getPropertyValue("Scaler")); + scaler.set("Factor", 3.62); + scaler.set("Offset", 3.1); + fusionAmp.setPropertyValue("Scaler", scaler.build()); + + const auto scalerManipulated = StructBuilder(fusionAmp.getPropertyValue("Scaler")); + ASSERT_DOUBLE_EQ(scalerManipulated.get("Factor"), (double) 3.62); + ASSERT_DOUBLE_EQ(scalerManipulated.get("Offset"), (double) 3.1); +} + +TEST_F(TmsFusionDevice, FullBridge) +{ + const auto obj = PropertyObject(objManager, "FusionAmp"); + auto [serverObj, fusionAmp] = registerPropertyObject(obj); + + const StructPtr fullBridge = fusionAmp.getPropertyValue("FullBridge"); + + const auto newExcitationVoltage = StructBuilder(fullBridge.get("ExcitationVoltage")) + .set("NominalValueRange", Range(1.2, 6.2)) + .set("ActualValue", 5.9) + .set("Type", Enumeration("ExcitationTypeEnumeration", "ACVoltage", objManager)) + .set("Frequency", 0) + .set("NominalValue", 5.0) + .build(); + + const auto newFullBridge = StructBuilder(fullBridge) + .set("ExcitationVoltage", newExcitationVoltage) + .set("UsedWires", 9) + .set("Resistance", 100.1) + .set("MaximumElectrical", 8.0) + .build(); + + fusionAmp.setPropertyValue("FullBridge", newFullBridge); + const auto serverFullBridge = obj.getPropertyValue("FullBridge"); + const auto clientFullBridge = fusionAmp.getPropertyValue("FullBridge"); + + ASSERT_EQ(serverFullBridge, clientFullBridge); + ASSERT_EQ(serverFullBridge, newFullBridge); +} + +TEST_F(TmsFusionDevice, EUInformationWithQuantity) +{ + const auto obj = PropertyObject(objManager, "FusionAmp"); + auto [serverObj, fusionAmp] = registerPropertyObject(obj); + + // Test unit property + const auto EUInformationStruct = Unit("s", 1, "seconds", "50"); + fusionAmp.setPropertyValue("EUInformationWithQuantity", EUInformationStruct); + + ASSERT_EQ(fusionAmp.getPropertyValue("EUInformationWithQuantity"), EUInformationStruct); +} + +TEST_F(TmsFusionDevice, EnumTest) +{ + // This test should be moved to coreobjects + const auto obj = PropertyObject(objManager, "FusionAmp"); + + // Test enum property + obj.setPropertyValue("ExcitationType", Enumeration("ExcitationTypeEnumeration", "ACVoltage", objManager)); + ASSERT_EQ(obj.getPropertyValue("ExcitationType"), Enumeration("ExcitationTypeEnumeration", "ACVoltage", objManager)); +} + +TEST_F(TmsFusionDevice, EnumPropertyTest) +{ + const auto obj = PropertyObject(objManager, "FusionAmp"); + auto [serverObj, fusionAmp] = registerPropertyObject(obj); + + // Test enum + fusionAmp.setPropertyValue("ExcitationType", Enumeration("ExcitationTypeEnumeration", "ACVoltage", objManager)); + ASSERT_EQ(obj.getPropertyValue("ExcitationType"), Enumeration("ExcitationTypeEnumeration", "ACVoltage", objManager)); + ASSERT_EQ(fusionAmp.getPropertyValue("ExcitationType"), Enumeration("ExcitationTypeEnumeration", "ACVoltage", objManager)); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_input_port.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_input_port.cpp new file mode 100644 index 0000000..83c2e01 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_input_port.cpp @@ -0,0 +1,170 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include +#include +#include +#include +#include +#include "test_input_port_notifications.h" + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +class TmsInputPortTest : public TmsObjectIntegrationTest +{ +public: + InputPortPtr createInputPort(std::string name, Bool requiresSignal) + { + auto ip = InputPort(NullContext(), nullptr, name); + ip.setRequiresSignal(requiresSignal); + ip.getTags().asPtr().add("Port"); + return ip; + } +}; + +TEST_F(TmsInputPortTest, Create) +{ + InputPortPtr inputPort = createInputPort("The Name", false); + auto tmsInputPort = TmsServerInputPort(inputPort, this->getServer(), ctx, serverContext); +} + +TEST_F(TmsInputPortTest, Register) +{ + const std::string name{"Some Name"}; + InputPortPtr daqServerInputPort = createInputPort(name, false); + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, "inputPort", clientContext, nodeId); + ASSERT_TRUE(clientInputPort.assigned()); +} + +TEST_F(TmsInputPortTest, BrowseName) +{ + const std::string name{"Some Name"}; + InputPortPtr daqServerInputPort = createInputPort(name, false); + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + auto browseName = client->readBrowseName(nodeId); + ASSERT_EQ(browseName, name); +} + +TEST_F(TmsInputPortTest, AttrName) +{ + const std::string name{"Test Name"}; + InputPortPtr daqServerInputPort = createInputPort(name, false); + ASSERT_FALSE(daqServerInputPort.getRequiresSignal()); + ASSERT_EQ(daqServerInputPort.getLocalId(), name); + + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + auto browseName = clientContext->getClient()->readBrowseName(nodeId); + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, browseName, clientContext, nodeId); + + ASSERT_EQ(daqServerInputPort.getLocalId(), clientInputPort.getLocalId()); + ASSERT_EQ(clientInputPort.getLocalId(), name); +} + +TEST_F(TmsInputPortTest, AttrRequiresSignalFalse) +{ + InputPortPtr daqServerInputPort = createInputPort("The Name", false); + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, "inputPort", clientContext, nodeId); + + ASSERT_FALSE(IsTrue(daqServerInputPort.getRequiresSignal())); + ASSERT_FALSE(IsTrue(clientInputPort.getRequiresSignal())); +} + +TEST_F(TmsInputPortTest, AttrRequiresSignalTrue) +{ + InputPortPtr daqServerInputPort = createInputPort("The Name", true); + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, "inputPort", clientContext, nodeId); + + ASSERT_TRUE(IsTrue(daqServerInputPort.getRequiresSignal())); + ASSERT_TRUE(IsTrue(clientInputPort.getRequiresSignal())); +} + +TEST_F(TmsInputPortTest, MethodAcceptsSignal) +{ + SignalPtr signal = Signal(NullContext(), nullptr, "sig"); + InputPortPtr daqServerInputPort = createInputPort("The Name", true); + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, "inputPort", clientContext, nodeId); + + //TODO: More testing when the server in fact really checks the signal if the signal is ok + EXPECT_THROW(clientInputPort.acceptsSignal(signal), daq::opcua::OpcUaClientCallNotAvailableException); +} + +TEST_F(TmsInputPortTest, ConnectedToReference) +{ + const auto logger = Logger(); + const auto scheduler = Scheduler(logger); + const auto context = Context(scheduler, logger, nullptr, nullptr, nullptr); + + SignalPtr signal = Signal(context, nullptr, "sig"); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + auto signalNodeId = serverSignal.registerOpcUaNode(); + + InputPortNotificationsPtr inputPortNotification = TestInputPortNotifications(); + + InputPortConfigPtr inputPort = InputPort(NullContext(), nullptr, "inputPort"); + inputPort.setListener(inputPortNotification); + inputPort.connect(signal); + + auto serverInputPort = TmsServerInputPort(inputPort, this->getServer(), ctx, serverContext); + auto inputPortNodeId = serverInputPort.registerOpcUaNode(); + + ASSERT_NO_THROW(serverInputPort.createNonhierarchicalReferences()); + + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, "inputPort", clientContext, inputPortNodeId); + SignalPtr clientSignal = TmsClientSignal(context, nullptr, "sig", clientContext, signalNodeId); + + auto connectedSignal = clientInputPort.getSignal(); + ASSERT_TRUE(connectedSignal.assigned()); + ASSERT_EQ(connectedSignal, clientSignal); +} + +TEST_F(TmsInputPortTest, ComponentMethods) +{ + const std::string name{"inputPort"}; + InputPortPtr daqServerInputPort = createInputPort(name, false); + auto serverInputPort = TmsServerInputPort(daqServerInputPort, this->getServer(), ctx, serverContext); + auto nodeId = serverInputPort.registerOpcUaNode(); + + InputPortPtr clientInputPort = TmsClientInputPort(NullContext(), nullptr, "inputPort", clientContext, nodeId); + + ASSERT_EQ(daqServerInputPort.getName(), clientInputPort.getName()); + + auto tags = daqServerInputPort.getTags(); + auto clientTags = clientInputPort.getTags(); + + ASSERT_TRUE(tags.query("Port")); + ASSERT_TRUE(clientTags.query("Port")); + + clientInputPort.setActive(false); + ASSERT_EQ(daqServerInputPort.getActive(), clientInputPort.getActive()); + + clientInputPort.setActive(true); + ASSERT_EQ(daqServerInputPort.getActive(), clientInputPort.getActive()); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp new file mode 100644 index 0000000..5e6c901 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_integration.cpp @@ -0,0 +1,620 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace daq; +using namespace daq::opcua; +using namespace std::chrono_literals; + +class TmsIntegrationTest : public testing::Test +{ +public: + const std::string OPC_URL = "opc.tcp://localhost/"; + + InstancePtr createDevice(const StringPtr& localId = "localInstance") + { + const auto moduleManager = ModuleManager("[[none]]"); + auto logger = Logger(); + auto context = Context(Scheduler(logger, 1), logger, TypeManager(), moduleManager, nullptr); + + const ModulePtr deviceModule(MockDeviceModule_Create(context)); + moduleManager.addModule(deviceModule); + + const ModulePtr fbModule(MockFunctionBlockModule_Create(context)); + moduleManager.addModule(fbModule); + + auto instance = InstanceCustom(context, localId); + instance.addDevice("daqmock://client_device"); + instance.addDevice("daqmock://phys_device"); + instance.addFunctionBlock("mock_fb_uid"); + + return instance; + } + + void SetUp() override + { + } + + void TearDown() override + { + } +}; + +TEST_F(TmsIntegrationTest, Connect) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice; + ASSERT_NO_THROW(clientDevice = tmsClient.connect()); + ASSERT_TRUE(clientDevice.assigned()); +} + +TEST_F(TmsIntegrationTest, Devices) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + auto devices = clientDevice.getDevices(); + ASSERT_EQ(devices.getCount(), 2u); +} + +TEST_F(TmsIntegrationTest, DeviceInfo) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + + auto devices = clientDevice.getDevices(); + DeviceInfoPtr deviceInfo; + ASSERT_NO_THROW(deviceInfo = devices[0].getInfo()); +} + +TEST_F(TmsIntegrationTest, FunctionBlocks) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + + auto functionBlocks = clientDevice.getFunctionBlocks(); + ASSERT_EQ(functionBlocks.getCount(), 1u); +} + +TEST_F(TmsIntegrationTest, FunctionBlockType) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + + auto functionBlocks = clientDevice.getFunctionBlocks(); + FunctionBlockTypePtr type; + ASSERT_NO_THROW(type = functionBlocks[0].getFunctionBlockType()); +} + +TEST_F(TmsIntegrationTest, GetSignals) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + + ListPtr signals; + ASSERT_NO_THROW(signals = clientDevice.getSignals(search::Recursive(search::Visible()))); + // one private signal in MockFunctionBlockImpl. and one in MockPhysicalDeviceImpl + ASSERT_EQ(signals.getCount(), device.getSignals(search::Recursive(search::Visible())).getCount() - 2); + + ASSERT_NO_THROW(signals = clientDevice.getSignals()); + ASSERT_EQ(signals.getCount(), 0u); +} + +TEST_F(TmsIntegrationTest, GetChannels) +{ + InstancePtr instance = createDevice(); + auto devices = instance.getDevices(); + ASSERT_EQ(devices.getCount(), 2u); + + auto device1 = devices.getItemAt(0); + ASSERT_EQ(device1.getChannels().getCount(), 0u); + + auto device2 = devices.getItemAt(1); + ASSERT_EQ(device2.getChannels().getCount(), 4u); + + ASSERT_EQ(instance.getChannels(search::Recursive(search::Visible())).getCount(), 4u); + ASSERT_EQ(instance.getChannels().getCount(), 0u); +} +TEST_F(TmsIntegrationTest, InputsOutputs) +{ + InstancePtr instance = createDevice(); + auto devices = instance.getDevices(); + ASSERT_EQ(devices.getCount(), 2u); + + auto device1 = devices.getItemAt(0); // device 1: No channels + ASSERT_EQ(device1.getChannels().getCount(), 0u); + auto device1IoFolder = device1.getInputsOutputsFolder(); // io + ASSERT_TRUE(device1IoFolder.assigned()); + ASSERT_EQ(device1IoFolder.getItems().getCount(), 0u); + + + auto device2 = devices.getItemAt(1); // device 2: 4 channels + ASSERT_EQ(device2.getChannels().getCount(), 4u); + + auto device2IoFolder = device2.getInputsOutputsFolder(); // io: 1 channnel, 2 folders + ASSERT_TRUE(device2IoFolder.assigned()); + auto ioItems = device2IoFolder.getItems(); + ASSERT_EQ(ioItems.getCount(), 3u); + ASSERT_EQ(ioItems[0].getName(), "mockch1"); // io/mockch1 + ASSERT_TRUE(ioItems[0].asPtrOrNull().assigned()); + ASSERT_TRUE(ioItems[0].asPtrOrNull().assigned()); + + ASSERT_EQ(ioItems[1].getName(), "mockfolderA"); // io/mockfolderA: 1 channel + ASSERT_TRUE(ioItems[1].asPtrOrNull().assigned()); + ASSERT_FALSE(ioItems[1].asPtrOrNull().assigned()); + + auto mockFolderAItems = ioItems[1].asPtr().getItems(); + ASSERT_EQ(mockFolderAItems.getCount(), 1u); + + ASSERT_EQ(mockFolderAItems[0].getName(), "mockchA1"); // io/mockfolderA/mockchA1 + ASSERT_TRUE(mockFolderAItems[0].asPtrOrNull().assigned()); + ASSERT_TRUE(mockFolderAItems[0].asPtrOrNull().assigned()); + + + ASSERT_EQ(ioItems[2].getName(), "mockfolderB"); // io/mockFolderB: 2 channels + ASSERT_TRUE(ioItems[2].asPtrOrNull().assigned()); + ASSERT_FALSE(ioItems[2].asPtrOrNull().assigned()); + + auto mockFolderBItems = ioItems[2].asPtr().getItems(); + ASSERT_EQ(mockFolderBItems.getCount(), 2u); + + ASSERT_EQ(mockFolderBItems[0].getName(), "mockchB1"); // io/mockfolderB/mockchB1 + ASSERT_TRUE(mockFolderBItems[0].asPtrOrNull().assigned()); + ASSERT_TRUE(mockFolderBItems[0].asPtrOrNull().assigned()); + + ASSERT_EQ(mockFolderBItems[1].getName(), "mockchB2"); // io/mockfolderB/mockchB1 + ASSERT_TRUE(mockFolderBItems[1].asPtrOrNull().assigned()); + ASSERT_TRUE(mockFolderBItems[1].asPtrOrNull().assigned()); +} + +TEST_F(TmsIntegrationTest, CustomComponents) +{ + InstancePtr instance = createDevice(); + auto devices = instance.getDevices(); + ASSERT_EQ(devices.getCount(), 2u); + + auto device2 = devices.getItemAt(1u); + ASSERT_EQ(device2.getCustomComponents().getCount(), 2u); + + FolderPtr componentA = device2.getCustomComponents()[0]; + ASSERT_EQ(componentA.getItems().getCount(), 1u); +} + +TEST_F(TmsIntegrationTest, GetDomainSignal) +{ + InstancePtr device = createDevice(); + + TmsServer tmsServer(device); + tmsServer.start(); + + DevicePtr clientDevice; + { + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); // The device should work after we delete the builder + clientDevice = tmsClient.connect(); + } + + ListPtr signals = clientDevice.getSignals(search::Recursive(search::Visible())); + + auto byteStepSignal = *std::find_if( + signals.begin(), signals.end(), [](const SignalPtr& signal) { return signal.getDescriptor().getName() == "ByteStep"; }); + + SignalPtr domainSignal; + ASSERT_NO_THROW(domainSignal = byteStepSignal.getDomainSignal()); + ASSERT_TRUE(domainSignal.assigned()); +} + +TEST_F(TmsIntegrationTest, GetAvailableFunctionBlockTypes) +{ + InstancePtr device = createDevice(); + TmsServer tmsServer(device); + tmsServer.start(); + + auto serverFbTypes = device.getAvailableFunctionBlockTypes(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + auto clientDevice = tmsClient.connect(); + + const auto clientFbTypes = clientDevice.getAvailableFunctionBlockTypes(); + + ASSERT_EQ(serverFbTypes.getCount(), 3u); + ASSERT_EQ(serverFbTypes.getCount(), clientFbTypes.getCount()); + ASSERT_TRUE(TestComparators::FunctionBlockTypeEquals(serverFbTypes.get("mock_fb_uid"), clientFbTypes.get("mock_fb_uid"))); +} + +TEST_F(TmsIntegrationTest, AddFunctionBlock) +{ + InstancePtr device = createDevice(); + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + auto clientDevice = tmsClient.connect(); + ASSERT_EQ("mock_fb_uid_1", clientDevice.getFunctionBlocks()[0].getLocalId()); + + auto fb1 = clientDevice.addFunctionBlock("mock_fb_uid"); + ASSERT_TRUE(fb1.assigned()); + ASSERT_EQ("mock_fb_uid_2", fb1.getLocalId()); + + auto fb2 = clientDevice.addFunctionBlock("mock_fb_uid"); + ASSERT_TRUE(fb2.assigned()); + ASSERT_EQ("mock_fb_uid_3", fb2.getLocalId()); + + ASSERT_EQ(3u, clientDevice.getFunctionBlocks().getCount()); +} + +TEST_F(TmsIntegrationTest, AddFunctionBlockWitchConfig) +{ + InstancePtr device = createDevice(); + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + auto clientDevice = tmsClient.connect(); + + const auto clientFbTypes = clientDevice.getAvailableFunctionBlockTypes(); + ASSERT_TRUE(clientFbTypes.hasKey("mock_fb_uid")); + + auto config = clientFbTypes.get("mock_fb_uid").createDefaultConfig(); + config.setPropertyValue("TestConfigString", "Hello Property!"); + + auto fb = clientDevice.addFunctionBlock("mock_fb_uid", config); + + ASSERT_EQ(2u, clientDevice.getFunctionBlocks().getCount()); + ASSERT_EQ(fb.getPropertyValue("TestConfigInt"), 0); + ASSERT_EQ(fb.getPropertyValue("TestConfigString"), "Hello Property!"); +} + + +TEST_F(TmsIntegrationTest, RemoveFunctionBlock) +{ + InstancePtr device = createDevice(); + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + auto clientDevice = tmsClient.connect(); + + auto fb1 = clientDevice.addFunctionBlock("mock_fb_uid"); + auto fb2 = clientDevice.addFunctionBlock("mock_fb_uid"); + ASSERT_EQ(3u, clientDevice.getFunctionBlocks().getCount()); + + clientDevice.removeFunctionBlock(fb1); + ASSERT_EQ(2u, clientDevice.getFunctionBlocks().getCount()); + + clientDevice.removeFunctionBlock(fb2); + ASSERT_EQ(1u, clientDevice.getFunctionBlocks().getCount()); +} + +TEST_F(TmsIntegrationTest, InputPortConnect) +{ + InstancePtr device = createDevice(); + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + auto clientDevice = tmsClient.connect(); + + auto inputPort = clientDevice.getChannelsRecursive().getItemAt(0).getInputPorts().getItemAt(0); + auto signal1 = clientDevice.getSignalsRecursive().getItemAt(0); + auto signal2 = clientDevice.getSignalsRecursive().getItemAt(1); + + SignalPtr portSignal = inputPort.getSignal(); + ASSERT_FALSE(portSignal.assigned()); + + ASSERT_NO_THROW(inputPort.disconnect()); + + inputPort.connect(signal1); + portSignal = inputPort.getSignal(); + ASSERT_TRUE(portSignal.assigned()); + ASSERT_EQ(portSignal, signal1); + + inputPort.connect(signal2); + portSignal = inputPort.getSignal(); + ASSERT_TRUE(portSignal.assigned()); + ASSERT_EQ(portSignal, signal2); + + inputPort.disconnect(); + portSignal = inputPort.getSignal(); + ASSERT_FALSE(portSignal.assigned()); +} + +TEST_F(TmsIntegrationTest, InputPortMultipleServers) +{ + auto StartServerDevice = [&](const InstancePtr& device, uint16_t port) + { + auto tmsServer = std::make_shared(device); + tmsServer->setOpcUaPort(port); + tmsServer->start(); + return tmsServer; + }; + + InstancePtr device1 = createDevice(); + InstancePtr device2 = createDevice("localInstance2"); + auto server1 = StartServerDevice(device1, 4001); + auto server2 = StartServerDevice(device2, 4002); + + InstancePtr instance = Instance(); + auto clientDevice1 = instance.addDevice("daq.opcua://127.0.0.1:4001"); + auto clientDevice2 = instance.addDevice("daq.opcua://127.0.0.1:4002"); + + InputPortPtr inputPort1, inputPort2; + SignalPtr signal1, signal2; + ASSERT_NO_THROW(inputPort1 = clientDevice1.getChannelsRecursive().getItemAt(0).getInputPorts().getItemAt(0)); + ASSERT_NO_THROW(inputPort2 = clientDevice2.getChannelsRecursive().getItemAt(0).getInputPorts().getItemAt(0)); + ASSERT_NO_THROW(signal1 = clientDevice1.getSignalsRecursive().getItemAt(0)); + ASSERT_NO_THROW(signal2 = clientDevice2.getSignalsRecursive().getItemAt(0)); + + SignalPtr portSignal; + + ASSERT_NO_THROW(inputPort1.connect(signal1)); + portSignal = inputPort1.getSignal(); + ASSERT_EQ(portSignal, signal1); + + ASSERT_NO_THROW(inputPort2.connect(signal2)); + portSignal = inputPort2.getSignal(); + ASSERT_EQ(portSignal, signal2); + + ASSERT_THROW(inputPort1.connect(signal2), NotFoundException); + ASSERT_THROW(inputPort2.connect(signal1), NotFoundException); +} + +TEST_F(TmsIntegrationTest, BeginEndUpdateDevice) +{ + InstancePtr device = createDevice(); + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + auto clientDevice = tmsClient.connect(); + + ASSERT_NO_THROW(clientDevice.beginUpdate()); + ASSERT_NO_THROW(clientDevice.endUpdate()); +} + + +TEST_F(TmsIntegrationTest, SyncComponentNoSubdevices) +{ + auto inst = Instance(); + auto serverTypeManager = inst.getContext().getTypeManager(); + auto serverSync = inst.getSyncComponent(); + SyncComponentPrivatePtr syncComponentPrivate = serverSync.asPtr(true); + + syncComponentPrivate.addInterface(PropertyObject(serverTypeManager, "PtpSyncInterface")); + syncComponentPrivate.addInterface(PropertyObject(serverTypeManager, "InterfaceClockSync")); + + serverSync.setSelectedSource(1); + + TmsServer tmsServer(inst); + tmsServer.start(); + + TmsClient tmsClient(inst.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + auto clientSync = clientDevice.getSyncComponent(); + + ASSERT_EQ(serverSync.getSelectedSource(), clientSync.getSelectedSource()); + ASSERT_EQ(serverSync.getSyncLocked(), clientSync.getSyncLocked()); + + auto serverInterfaces = serverSync.getInterfaces(); + auto clientInterfaces = clientSync.getInterfaces(); + + ASSERT_EQ(serverInterfaces.getCount(), clientInterfaces.getCount()); + ASSERT_EQ(serverInterfaces.getKeyList(), clientInterfaces.getKeyList()); +} + +TEST_F(TmsIntegrationTest, SyncComponent) +{ + InstancePtr device = createDevice(); + auto serverTypeManager = device.getContext().getTypeManager(); + auto serverSubDevice = device.getDevices()[1]; + auto serverSync = serverSubDevice.getSyncComponent(); + SyncComponentPrivatePtr syncComponentPrivate = serverSync.asPtr(true); + + syncComponentPrivate.addInterface(PropertyObject(serverTypeManager, "PtpSyncInterface")); + syncComponentPrivate.addInterface(PropertyObject(serverTypeManager, "InterfaceClockSync")); + + serverSync.setSelectedSource(1); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + auto clientSubDevice = clientDevice.getDevices()[1]; + auto clientSync = clientSubDevice.getSyncComponent(); + + ASSERT_EQ(serverSync.getSelectedSource(), clientSync.getSelectedSource()); + ASSERT_EQ(serverSync.getSyncLocked(), clientSync.getSyncLocked()); + + auto serverInterfaces = serverSync.getInterfaces(); + auto clientInterfaces = clientSync.getInterfaces(); + + ASSERT_EQ(serverInterfaces.getCount(), clientInterfaces.getCount()); + ASSERT_EQ(serverInterfaces.getKeyList(), clientInterfaces.getKeyList()); +} + +TEST_F(TmsIntegrationTest, SyncComponentCustomInterfaceValues) +{ + InstancePtr device = createDevice(); + auto serverTypeManager = device.getContext().getTypeManager(); + auto serverSubDevice = device.getDevices()[1]; + auto serverSync = serverSubDevice.getSyncComponent(); + SyncComponentPrivatePtr syncComponentPrivate = serverSync.asPtr(true); + + auto ptpSyncInterface = PropertyObject(serverTypeManager, "PtpSyncInterface"); + ptpSyncInterface.setPropertyValue("Mode", 2); + + PropertyObjectPtr status = ptpSyncInterface.getPropertyValue("Status"); + status.setPropertyValue("State", 2); + status.setPropertyValue("Grandmaster", "1234"); + + PropertyObjectPtr parameters = ptpSyncInterface.getPropertyValue("Parameters"); + + StructPtr configuration = parameters.getPropertyValue("PtpConfigurationStructure"); + + auto newConfiguration = StructBuilder(configuration) + .set("ClockType", Enumeration("PtpClockTypeEnumeration", "OrdinaryBoundary", serverTypeManager)) + .set("TransportProtocol", Enumeration("PtpProtocolEnumeration", "UDP6_SCOPE", serverTypeManager)) + .set("StepFlag", Enumeration("PtpStepFlagEnumeration", "Two", serverTypeManager)) + .set("DomainNumber", 123) + .set("LeapSeconds", 123) + .set("DelayMechanism", Enumeration("PtpDelayMechanismEnumeration", "E2E", serverTypeManager)) + .set("Priority1", 123) + .set("Priority2", 123) + .set("Profiles", Enumeration("PtpProfileEnumeration", "802_1AS", serverTypeManager)) + .build(); + + parameters.setPropertyValue("PtpConfigurationStructure", newConfiguration); + + PropertyObjectPtr ports = parameters.getPropertyValue("Ports"); + ports.addProperty(BoolProperty("Port1", true)); + + syncComponentPrivate.addInterface(ptpSyncInterface); + + syncComponentPrivate.setSyncLocked(true); + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + auto clientSubDevice = clientDevice.getDevices()[1]; + auto clientSync = clientSubDevice.getSyncComponent(); + + auto serveSyncInterface = serverSync.getInterfaces(); + auto clientSyncInterface = clientSync.getInterfaces(); + + ASSERT_EQ(serverSync.getSelectedSource(), clientSync.getSelectedSource()); + ASSERT_EQ(serverSync.getSyncLocked(), clientSync.getSyncLocked()); + + auto serverInterfaces = serverSync.getInterfaces(); + auto clientInterfaces = clientSync.getInterfaces(); + ASSERT_EQ(serverInterfaces.getCount(), clientInterfaces.getCount()); + ASSERT_EQ(serverInterfaces.getKeyList(), clientInterfaces.getKeyList()); + + auto clientPtpSyncInterface = clientInterfaces.get("PtpSyncInterface"); + ASSERT_EQ(clientPtpSyncInterface.getPropertyValue("Mode"), 2); + + PropertyObjectPtr clientStatus = clientPtpSyncInterface.getPropertyValue("Status"); + ASSERT_EQ(clientStatus.getPropertyValue("State"), 2); + ASSERT_EQ(clientStatus.getPropertyValue("Grandmaster"), "1234"); + + PropertyObjectPtr clientParameters = clientPtpSyncInterface.getPropertyValue("Parameters"); + ASSERT_EQ(clientParameters.getPropertyValue("PtpConfigurationStructure"), newConfiguration); + + PropertyObjectPtr clientPorts = clientParameters.getPropertyValue("Ports"); + ASSERT_EQ(clientPorts.getPropertyValue("Port1"), true); +} + +TEST_F(TmsIntegrationTest, SyncComponentCustomInterface) +{ + InstancePtr device = createDevice(); + auto serverTypeManager = device.getContext().getTypeManager(); + auto serverSubDevice = device.getDevices()[1]; + auto serverSync = serverSubDevice.getSyncComponent(); + SyncComponentPrivatePtr syncComponentPrivate = serverSync.asPtr(true); + + { + auto customSyncInterface = PropertyObjectClassBuilder(serverTypeManager, "CustomInterface") + .setParentName("SyncInterfaceBase") + .addProperty(SelectionProperty("CustomState", List("Cool", "Awesome"), 1)) + .build(); + serverTypeManager->addType(customSyncInterface); + syncComponentPrivate.addInterface(PropertyObject(serverTypeManager, "CustomInterface")); + } + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + auto clientSubDevice = clientDevice.getDevices()[1]; + auto clientSync = clientSubDevice.getSyncComponent(); + + auto serverInterfaces = serverSync.getInterfaces(); + auto clientInterfaces = clientSync.getInterfaces(); + + ASSERT_EQ(serverInterfaces.getCount(), clientInterfaces.getCount()); + ASSERT_EQ(serverInterfaces.getKeyList(), clientInterfaces.getKeyList()); + + auto customInterface = clientInterfaces.get("CustomInterface"); + ASSERT_TRUE(customInterface.hasProperty("CustomState")); + ASSERT_EQ(customInterface.getProperty("CustomState").getSelectionValues(), List("Cool", "Awesome")); + ASSERT_EQ(customInterface.getPropertyValue("CustomState"), 1); + ASSERT_EQ(customInterface.getPropertySelectionValue("CustomState"), "Awesome"); +} + +TEST_F(TmsIntegrationTest, SyncComponentCustomModeOptions) +{ + InstancePtr device = createDevice(); + auto serverTypeManager = device.getContext().getTypeManager(); + auto serverSubDevice = device.getDevices()[1]; + auto serverSync = serverSubDevice.getSyncComponent(); + SyncComponentPrivatePtr syncComponentPrivate = serverSync.asPtr(true); + auto modeOptions = Dict({{2, "Auto"}, {3, "Off"}}); + + { + auto interfaceClockSync = PropertyObject(serverTypeManager, "InterfaceClockSync"); + interfaceClockSync.setPropertyValue("ModeOptions", modeOptions); + syncComponentPrivate.addInterface(interfaceClockSync); + } + + TmsServer tmsServer(device); + tmsServer.start(); + + TmsClient tmsClient(device.getContext(), nullptr, OPC_URL); + DevicePtr clientDevice = tmsClient.connect(); + auto clientSubDevice = clientDevice.getDevices()[1]; + auto clientSync = clientSubDevice.getSyncComponent(); + + auto serverInterfaces = serverSync.getInterfaces(); + auto clientInterfaces = clientSync.getInterfaces(); + + ASSERT_EQ(serverInterfaces.getCount(), clientInterfaces.getCount()); + ASSERT_EQ(serverInterfaces.getKeyList(), clientInterfaces.getKeyList()); + + auto interfaceClockSync = clientInterfaces.get("InterfaceClockSync"); + auto modeProperty = interfaceClockSync.getProperty("Mode"); + ASSERT_EQ(modeProperty.getSelectionValues(), modeOptions); + ASSERT_EQ(interfaceClockSync.getPropertySelectionValue("Mode"), "Off"); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_property.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_property.cpp new file mode 100644 index 0000000..39abcc2 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_property.cpp @@ -0,0 +1,232 @@ +#include +#include "tms_object_integration_test.h" +#include +#include +#include +#include +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +struct RegisteredProperty +{ + TmsServerPropertyPtr serverProp; + PropertyPtr clientProp; +}; + +class TmsPropertyTest : public TmsObjectIntegrationTest +{ +public: + + PropertyBuilderPtr createIntPropertyBuilder() + { + return IntPropertyBuilder("Age", 10).setDescription("Hello world.").setMinValue(-100).setMaxValue(100); + } + + PropertyPtr createIntProperty() + { + return createIntPropertyBuilder().build(); + } + + PropertyBuilderPtr createEnumPropertyBuilder() + { + auto values = List(); + values.pushBack("banana"); + values.pushBack("apple"); + values.pushBack("blueberry"); + + return SelectionPropertyBuilder("Fruits", values, 0).setDescription("Hello world."); + } + + PropertyPtr createEnumProperty() + { + return createEnumPropertyBuilder().build(); + } + + PropertyBuilderPtr createDictEnumPropertyBuilder() + { + auto values = Dict(); + values.set(0, "banana"); + values.set(2, "apple"); + values.set(5, "blueberry"); + + return SparseSelectionPropertyBuilder("FruitsDict", values, 0).setDescription("Hello world."); + } + + PropertyPtr createDictEnumProperty() + { + return createDictEnumPropertyBuilder().build(); + } + + RegisteredProperty registerProperty(const PropertyPtr& prop) + { + auto serverProp = std::make_shared(prop, server, ctx, serverContext); + auto nodeId = serverProp->registerOpcUaNode(); + auto clientProp = TmsClientProperty(NullContext(), clientContext, nodeId); + return {serverProp, clientProp}; + } +}; + +TEST_F(TmsPropertyTest, Create) +{ + auto prop = createIntProperty(); + + auto serverProp = TmsServerProperty(prop, server, ctx, serverContext); + auto nodeId = serverProp.registerOpcUaNode(); + ASSERT_TRUE(server->nodeExists(nodeId)); + + auto clientProp = TmsClientProperty(NullContext(), clientContext, nodeId); + ASSERT_TRUE(clientProp.assigned()); +} + +TEST_F(TmsPropertyTest, Name) +{ + auto prop = createIntProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getName(), prop.getName()); + ASSERT_THROW(client->writeDisplayName(serverProp->getNodeId(), "Name"), OpcUaException); +} + +TEST_F(TmsPropertyTest, Description) +{ + auto prop = createIntProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getDescription(), prop.getDescription()); + ASSERT_THROW(client->writeDescription(serverProp->getNodeId(), "New description."), OpcUaException); +} + +TEST_F(TmsPropertyTest, Unit) +{ + auto prop = createIntPropertyBuilder().setUnit(Unit("V", 1, "voltage")).build(); + + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_TRUE(BaseObjectPtr::Equals(clientProp.getUnit(), prop.getUnit())); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "Unit", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, UnitEval) +{ + auto prop = createIntPropertyBuilder().setUnit(EvalValue("Unit('V')")).build(); + + auto [serverProp, clientProp] = registerProperty(prop); + ASSERT_TRUE(BaseObjectPtr::Equals(clientProp.getUnit(), prop.getUnit())); +} + +TEST_F(TmsPropertyTest, IsEnumEnum) +{ + auto prop = createEnumProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + ASSERT_TRUE(clientProp.getSelectionValues().assigned()); +} + +TEST_F(TmsPropertyTest, IsEnumInt) +{ + auto prop = createIntProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + ASSERT_FALSE(clientProp.getSelectionValues().assigned()); +} + +TEST_F(TmsPropertyTest, MinValue) +{ + auto prop = createIntProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getMinValue(), prop.getMinValue()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "MinValue", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, MinValueEnum) +{ + auto prop = createEnumProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_FALSE(clientProp.getMinValue().assigned()); +} + +TEST_F(TmsPropertyTest, MaxValue) +{ + auto prop = createIntProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getMaxValue(), prop.getMaxValue()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "MaxValue", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, MaxValueEnum) +{ + auto prop = createEnumProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_FALSE(clientProp.getMaxValue().assigned()); +} + +TEST_F(TmsPropertyTest, DefaultValue) +{ + auto prop = createEnumProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getDefaultValue(), prop.getDefaultValue()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "DefaultValue", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, IsVisible) +{ + auto prop = createIntPropertyBuilder().setVisible(true).build(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getVisible(), prop.getVisible()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "IsVisible", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, IsVisibleEval) +{ + auto prop = createIntPropertyBuilder().setVisible(EvalValue("3 > 0")).build(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getVisible(), prop.getVisible()); +} + +TEST_F(TmsPropertyTest, IsReadOnly) +{ + auto prop = createIntPropertyBuilder().setReadOnly(true).build(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getReadOnly(), prop.getReadOnly()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "IsReadOnly", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, IsReadOnlyEval) +{ + auto prop = createIntPropertyBuilder().setReadOnly(EvalValue("3 < 0")).build(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getReadOnly(), prop.getReadOnly()); +} + +TEST_F(TmsPropertyTest, EnumValues) +{ + auto prop = createEnumProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + auto sel1 = clientProp.getSelectionValues(); + auto sel2 = prop.getSelectionValues(); + + ASSERT_EQ(clientProp.getSelectionValues(), prop.getSelectionValues()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "EnumValues", OpcUaVariant()), OpcUaException); +} + +TEST_F(TmsPropertyTest, DictEnumValues) +{ + auto prop = createDictEnumProperty(); + auto [serverProp, clientProp] = registerProperty(prop); + + ASSERT_EQ(clientProp.getSelectionValues(), prop.getSelectionValues()); + ASSERT_THROW(writeChildNode(serverProp->getNodeId(), "EnumValues", OpcUaVariant()), OpcUaException); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_property_object.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_property_object.cpp new file mode 100644 index 0000000..f316843 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_property_object.cpp @@ -0,0 +1,679 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" +#include +#include +#include + +#include +#include +#include +#include + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; +using namespace std::chrono_literals; + +struct RegisteredPropertyObject +{ + TmsServerPropertyObjectPtr serverProp; + PropertyObjectPtr clientProp; +}; + +class TmsPropertyObjectTest : public TmsObjectIntegrationTest +{ +public: + + TypeManagerPtr manager; + + void SetUp() override + { + TmsObjectIntegrationTest::SetUp(); + manager = TypeManager(); + auto personClass = PropertyObjectClassBuilder("PersonObject").build(); + manager.addType(personClass); + } + + void TearDown() override + { + TmsObjectIntegrationTest::TearDown(); + } + + PropertyObjectPtr createPropertyObject() + { + GenericPropertyObjectPtr object = PropertyObject(manager, "PersonObject"); + + auto roles = List(); + roles.pushBack("Software"); + roles.pushBack("Hardware"); + + object.addProperty(SelectionProperty("Role", roles, 0)); + object.addProperty(IntProperty("Height", 180)); + object.addProperty(IntProperty("Age", 0)); + object.setPropertyValue("Age", 999); + + // Is not registered + //object.addProperty(PropertyObjectClassPtr::CreateStringPropInfo("Name")); + //object.setPropertyValue("Name", "Glados"); + + return object; + } + + StringPtr getLastMessage() + { + logger.flush(); + auto sink = getPrivateSink(); + auto newMessage = sink.waitForMessage(2000); + if (newMessage == 0) + return StringPtr(""); + auto logMessage = sink.getLastMessage(); + return logMessage; + } + + RegisteredPropertyObject registerPropertyObject(const PropertyObjectPtr& prop) + { + auto serverProp = std::make_shared(prop, server, ctx, serverContext); + auto nodeId = serverProp->registerOpcUaNode(); + auto clientProp = TmsClientPropertyObject(NullContext(logger), clientContext, nodeId); + return {serverProp, clientProp}; + } +}; + +TEST_F(TmsPropertyObjectTest, Create) +{ + auto prop = createPropertyObject(); + + auto serverProp = TmsServerPropertyObject(prop, server, ctx, serverContext); + auto nodeId = serverProp.registerOpcUaNode(); + ASSERT_TRUE(server->nodeExists(nodeId)); + + auto clientProp = TmsClientPropertyObject(NullContext(), clientContext, nodeId); + ASSERT_TRUE(clientProp.assigned()); +} + +// TODO: Class names cannot be transferred as of now +TEST_F(TmsPropertyObjectTest, DISABLED_ClassName) +{ + auto prop = createPropertyObject(); + + auto propClass = PropertyObjectClassBuilder("Chell").setParentName("PersonObject").build(); + manager.addType(propClass); + + auto [serverProp, clientProp] = registerPropertyObject(prop); + + ASSERT_EQ(clientProp.getClassName(), prop.getClassName()); + + manager.removeType("Chell"); +} + +TEST_F(TmsPropertyObjectTest, PropertyValue) +{ + auto prop = createPropertyObject(); + auto [serverProp, clientProp] = registerPropertyObject(prop); + + ASSERT_EQ(clientProp.getPropertyValue("Height"), prop.getPropertyValue("Height")); + clientProp.setPropertyValue("Height", 100); + ASSERT_EQ(clientProp.getPropertyValue("Height"), 100); + ASSERT_EQ(prop.getPropertyValue("Height"), 100); + + ASSERT_ANY_THROW(clientProp.setPropertyValue("Missing", 100)); + ASSERT_EQ(getLastMessage(), "Failed to set value for property \"Missing\" on OpcUA client property object: Property not found"); +} + +TEST_F(TmsPropertyObjectTest, PropertyValueRole) +{ + auto prop = createPropertyObject(); + auto [serverProp, clientProp] = registerPropertyObject(prop); + + ASSERT_EQ(clientProp.getPropertyValue("Role"), prop.getPropertyValue("Role")); + clientProp.setPropertyValue("Role", 1); + ASSERT_EQ(clientProp.getPropertyValue("Role"), 1); + ASSERT_EQ(prop.getPropertyValue("Role"), 1); + + ASSERT_NO_THROW(clientProp.setPropertyValue("Role", 2)); + ASSERT_EQ(getLastMessage(), "Failed to set value for property \"Role\" on OpcUA client property object: Writing property value"); +} + +TEST_F(TmsPropertyObjectTest, getPropertySelectionValue) +{ + auto prop = createPropertyObject(); + auto [serverProp, clientProp] = registerPropertyObject(prop); + const auto targetRole = "Hardware"; + + ASSERT_EQ(clientProp.getPropertySelectionValue("Role"), prop.getPropertySelectionValue("Role")); + clientProp.setPropertyValue("Role", 1); + ASSERT_EQ(prop.getPropertySelectionValue("Role"), targetRole); + ASSERT_EQ(clientProp.getPropertySelectionValue("Role"), targetRole); +} + +TEST_F(TmsPropertyObjectTest, GetProperty) +{ + auto prop = createPropertyObject(); + auto [serverProp, clientProp] = registerPropertyObject(prop); + + auto height = clientProp.getProperty("Height"); + ASSERT_TRUE(height.assigned()); +} + +TEST_F(TmsPropertyObjectTest, EnumVisibleProperties) +{ + auto prop = createPropertyObject(); + auto [serverProp, clientProp] = registerPropertyObject(prop); + + auto infos = prop.getVisibleProperties(); + auto clientInfos = clientProp.getVisibleProperties(); + ASSERT_EQ(infos.getCount(), clientInfos.getCount()); +} + +// TODO: hasPropertyValue should only return true of local value is set +TEST_F(TmsPropertyObjectTest, HasProperty) +{ + auto prop = createPropertyObject(); + auto [serverProp, clientProp] = registerPropertyObject(prop); + + ASSERT_TRUE(clientProp.hasProperty("Age")); + ASSERT_TRUE(clientProp.hasProperty("Role")); + ASSERT_TRUE(clientProp.hasProperty("Height")); + ASSERT_FALSE(clientProp.hasProperty("Missing")); +} + +// TODO: Dictionary properties are not yet available on OpcUa TMS +TEST_F(TmsPropertyObjectTest, TestDictProperty) +{ + auto dict = Dict(); + dict.set("BananaCount", 10); + dict.set("AppleCount", 5); + dict.set("BlueberryCount", 999); + + auto prop = DictProperty("DictProp", dict); + auto propObj = PropertyObject(); + propObj.addProperty(prop); + + auto [serverProp, clientProp] = registerPropertyObject(propObj); + auto clientDict = clientProp.getPropertyValue("DictProp"); + ASSERT_EQ(dict, clientDict); +} + +// TODO: List properties are not yet available on OpcUa TMS +TEST_F(TmsPropertyObjectTest, TestListProperty) +{ + auto list = List("Banana", "Apple", "Blueberry"); + + auto prop = ListProperty("ListProp", list); + auto propObj = PropertyObject(); + propObj.addProperty(prop); + + auto [serverProp, clientProp] = registerPropertyObject(propObj); + auto clientDict = clientProp.getPropertyValue("ListProp"); + ASSERT_EQ(list, clientDict); +} + +TEST_F(TmsPropertyObjectTest, TestPropertyOrder) +{ + auto obj = PropertyObject(); + for (SizeT i = 0; i < 200; ++i) + obj.addProperty(BoolProperty("Bool" + std::to_string(i), true)); + for (SizeT i = 0; i < 200; ++i) + obj.addProperty(StringProperty("String" + std::to_string(i), "test")); + for (SizeT i = 0; i < 200; ++i) + { + obj.addProperty(FunctionProperty("Func" + std::to_string(i), ProcedureInfo())); + ProcedurePtr test = Procedure([](){}); + obj.setPropertyValue("Func" + std::to_string(i), test); + } + + auto [serverObj, clientObj] = registerPropertyObject(obj); + auto serverProps = obj.getAllProperties(); + auto clientProps = clientObj.getAllProperties(); + + ASSERT_EQ(serverProps.getCount(), clientProps.getCount()); + + for (SizeT i = 0; i < serverProps.getCount(); ++i) + ASSERT_EQ(serverProps[i].getName(), clientProps[i].getName()); + +} + +TEST_F(TmsPropertyObjectTest, TestReadOnlyWrite) +{ + auto obj = PropertyObject(); + obj.addProperty(IntPropertyBuilder("ReadOnly", 0).setReadOnly(true).build()); + auto [serverObj, clientObj] = registerPropertyObject(obj); + auto serverProps = obj.getAllProperties(); + auto clientProps = clientObj.getAllProperties(); + + ASSERT_EQ(clientObj.getPropertyValue("ReadOnly"), 0); + clientObj.asPtr().setProtectedPropertyValue("ReadOnly", 100); + ASSERT_EQ(clientObj.getPropertyValue("ReadOnly"), 100); +} + +TEST_F(TmsPropertyObjectTest, TestReadOnlyWriteFail) +{ + auto obj = PropertyObject(); + obj.addProperty(IntPropertyBuilder("ReadOnly", 0).setReadOnly(true).build()); + auto [serverObj, clientObj] = registerPropertyObject(obj); + auto serverProps = obj.getAllProperties(); + auto clientProps = clientObj.getAllProperties(); + + ASSERT_EQ(clientObj.getPropertyValue("ReadOnly"), 0); + ASSERT_THROW(clientObj.setPropertyValue("ReadOnly", 100), AccessDeniedException); +} + +TEST_F(TmsPropertyObjectTest, DotAccessClient) +{ + PropertyObjectPtr obj = PropertyObject(); + PropertyObjectPtr child1 = PropertyObject(); + PropertyObjectPtr child2 = PropertyObject(); + + child2.addProperty(StringProperty("foo", "bar")); + child1.addProperty(ObjectProperty("child", child2)); + obj.addProperty(ObjectProperty("child", child1)); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto prop = clientObj.getProperty("child.child.foo"); + + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), "bar"); + ASSERT_EQ(prop.getValue(), "bar"); + + ASSERT_NO_THROW(clientObj.setPropertyValue("child.child.foo", "test")); + ASSERT_EQ(obj.getPropertyValue("child.child.foo"), "test"); + ASSERT_EQ(prop.getValue(), "test"); + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), "test"); + + ASSERT_NO_THROW(prop.setValue("bar")); + ASSERT_EQ(obj.getPropertyValue("child.child.foo"), "bar"); + ASSERT_EQ(prop.getValue(), "bar"); + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), "bar"); +} + +TEST_F(TmsPropertyObjectTest, DotAccessClientSelection) +{ + PropertyObjectPtr obj = PropertyObject(); + PropertyObjectPtr child1 = PropertyObject(); + PropertyObjectPtr child2 = PropertyObject(); + + child2.addProperty(SelectionProperty("fruit", List("apple", "orange", "banana"), 0)); + child1.addProperty(ObjectProperty("child", child2)); + obj.addProperty(ObjectProperty("child", child1)); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto prop = clientObj.getProperty("child.child.fruit"); + + ASSERT_EQ(clientObj.getPropertyValue("child.child.fruit"), 0); + ASSERT_EQ(prop.getValue(), 0); + ASSERT_EQ(clientObj.getPropertySelectionValue("child.child.fruit"), "apple"); + ASSERT_EQ(obj.getPropertySelectionValue("child.child.fruit"), "apple"); + + ASSERT_NO_THROW(clientObj.setPropertyValue("child.child.fruit", 1)); + ASSERT_EQ(clientObj.getPropertyValue("child.child.fruit"), 1); + ASSERT_EQ(prop.getValue(), 1); + ASSERT_EQ(clientObj.getPropertySelectionValue("child.child.fruit"), "orange"); + ASSERT_EQ(obj.getPropertySelectionValue("child.child.fruit"), "orange"); + + ASSERT_NO_THROW(prop.setValue(2)); + ASSERT_EQ(clientObj.getPropertyValue("child.child.fruit"), 2); + ASSERT_EQ(prop.getValue(), 2); + ASSERT_EQ(clientObj.getPropertySelectionValue("child.child.fruit"), "banana"); + ASSERT_EQ(obj.getPropertySelectionValue("child.child.fruit"), "banana"); +} + +TEST_F(TmsPropertyObjectTest, DotAccessClientServerChange) +{ + PropertyObjectPtr obj = PropertyObject(); + PropertyObjectPtr child1 = PropertyObject(); + PropertyObjectPtr child2 = PropertyObject(); + + child2.addProperty(StringProperty("foo", "bar")); + child1.addProperty(ObjectProperty("child", child2)); + obj.addProperty(ObjectProperty("child", child1)); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + auto prop = clientObj.getProperty("child.child.foo"); + auto serverProp = obj.getProperty("child.child.foo"); + + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), "bar"); + ASSERT_EQ(prop.getValue(), "bar"); + + ASSERT_NO_THROW(obj.setPropertyValue("child.child.foo", "test")); + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), "test"); + ASSERT_EQ(prop.getValue(), "test"); + + ASSERT_NO_THROW(serverProp.setValue("bar")); + ASSERT_EQ(clientObj.getPropertyValue("child.child.foo"), "bar"); + ASSERT_EQ(prop.getValue(), "bar"); +} + +TEST_F(TmsPropertyObjectTest, ProcedurePropWithListArg) +{ + PropertyObjectPtr obj = PropertyObject(); + auto argInfo = ListArgumentInfo("Int", ctInt); + + auto prop = FunctionProperty("ProcedureProp", ProcedureInfo(List(argInfo))); + obj.addProperty(prop); + auto proc = Procedure( + [](const ListPtr& list) + { + for (const auto& val : list) + ASSERT_EQ(val.getCoreType(), ctInt); + }); + + obj.setPropertyValue("ProcedureProp", proc); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_FALSE(clientObj.hasProperty("ProcedureProp")); + + // TODO: Procedure/Function properties with list/dictionary types are not yet supported over OPC UA! + //proc = clientObj.getPropertyValue("ProcedureProp"); + + //ASSERT_EQ(clientObj.getProperty("ProcedureProp").getCallableInfo().getArguments()[0], argInfo); + + //auto listArg = List(Integer(1), Integer(2)); + //proc(listArg); +} +TEST_F(TmsPropertyObjectTest, ProcedurePropWithDictArg) +{ + PropertyObjectPtr obj = PropertyObject(); + auto argInfo = DictArgumentInfo("Int", ctInt, ctString); + + auto prop = FunctionProperty("ProcedureProp", ProcedureInfo(List(argInfo))); + obj.addProperty(prop); + auto proc = Procedure( + [](const DictPtr& dict) + { + for (const auto& [key, val] : dict) + { + ASSERT_EQ(key.getCoreType(), ctInt); + ASSERT_EQ(val.getCoreType(), ctString); + } + }); + + obj.setPropertyValue("ProcedureProp", proc); + + auto [serverObj, clientObj] = registerPropertyObject(obj); + + ASSERT_FALSE(clientObj.hasProperty("ProcedureProp")); + + // TODO: Procedure/Function properties with list/dictionary types are not yet supported over OPC UA! + //proc = clientObj.getPropertyValue("ProcedureProp"); + + //ASSERT_EQ(clientObj.getProperty("ProcedureProp").getCallableInfo().getArguments()[0], argInfo); + + //auto dictArg = Dict({{0, "foo"}, {1, "bar"}}); + //proc(dictArg); +} + +TEST_F(TmsPropertyObjectTest, StringSuggestedValues) +{ + PropertyObjectPtr obj = PropertyObject(); + obj.addProperty(StringPropertyBuilder("StringSuggestedValues", "Orange").setSuggestedValues(List("Apple", "Orange", "Mango")).build()); + auto [serverObj, clientObj] = registerPropertyObject(obj); + + // TODO: String suggested values are not yet supported over OPC UA! + //ASSERT_EQ(clientObj.getProperty("StringSuggestedValues").getSuggestedValues(), List("Apple", "Orange", "Mango")); + ASSERT_EQ(clientObj.getProperty("StringSuggestedValues").getSuggestedValues(), nullptr); + + ASSERT_NO_THROW(clientObj.setPropertyValue("StringSuggestedValues", "Tomato")); +} + +class TmsNestedPropertyObjectTest : public TmsObjectIntegrationTest +{ +public: + + void SetUp() override + { + TmsObjectIntegrationTest::SetUp(); + serverObj = test_utils::createMockNestedPropertyObject(); + auto registeredObj = registerPropertyObject(serverObj); + serverTMSObj = registeredObj.serverProp; + clientObj = registeredObj.clientProp; + } + + void TearDown() override + { + TmsObjectIntegrationTest::TearDown(); + } + + RegisteredPropertyObject registerPropertyObject(const PropertyObjectPtr& prop) + { + auto serverProp = std::make_shared(prop, server, ctx, serverContext); + auto nodeId = serverProp->registerOpcUaNode(); + auto clientProp = TmsClientPropertyObject(NullContext(logger), clientContext, nodeId); + return {serverProp, clientProp}; + } + + PropertyObjectPtr serverObj; + TmsServerPropertyObjectPtr serverTMSObj; + PropertyObjectPtr clientObj; +}; + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientGet) +{ + const PropertyObjectPtr child1 = clientObj.getPropertyValue("child1"); + const PropertyObjectPtr child2 = clientObj.getPropertyValue("child2"); + const PropertyObjectPtr child1_1 = child1.getPropertyValue("child1_1"); + const PropertyObjectPtr child1_2 = child1.getPropertyValue("child1_2"); + const PropertyObjectPtr child1_2_1 = child1_2.getPropertyValue("child1_2_1"); + const PropertyObjectPtr child2_1 = child2.getPropertyValue("child2_1"); + + ASSERT_EQ(child1_2_1.getPropertyValue("String"), "String"); + ASSERT_DOUBLE_EQ(child1_1.getPropertyValue("Float"), 1.1); + ASSERT_EQ(child1_2.getPropertyValue("Int"), 1); + ASSERT_EQ(child2_1.getPropertyValue("Ratio"), Ratio(1,2)); +} + + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientGetDotAccess) +{ + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 2)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientGetSelectionValueDotAccess) +{ + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.Selection"), 0); + ASSERT_EQ(clientObj.getPropertySelectionValue("child1.child1_2.child1_2_1.Selection"), "a"); + clientObj.setPropertyValue("child1.child1_2.child1_2_1.Selection", 1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.Selection"), 1); + ASSERT_EQ(clientObj.getPropertySelectionValue("child1.child1_2.child1_2_1.Selection"), "b"); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientSet) +{ + const PropertyObjectPtr child1 = clientObj.getPropertyValue("child1"); + const PropertyObjectPtr child2 = clientObj.getPropertyValue("child2"); + const PropertyObjectPtr child1_1 = child1.getPropertyValue("child1_1"); + const PropertyObjectPtr child1_2 = child1.getPropertyValue("child1_2"); + const PropertyObjectPtr child1_2_1 = child1_2.getPropertyValue("child1_2_1"); + const PropertyObjectPtr child2_1 = child2.getPropertyValue("child2_1"); + + child1_2_1.setPropertyValue("String", "new_string"); + child1_1.setPropertyValue("Float", 2.1); + child1_2.setPropertyValue("Int", 2); + child2_1.setPropertyValue("Ratio", Ratio(1, 5)); + + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "new_string"); + ASSERT_DOUBLE_EQ(serverObj.getPropertyValue("child1.child1_1.Float"), 2.1); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.Int"), 2); + ASSERT_EQ(serverObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); + + ASSERT_EQ(child1_2_1.getPropertyValue("String"), "new_string"); + ASSERT_DOUBLE_EQ(child1_1.getPropertyValue("Float"), 2.1); + ASSERT_EQ(child1_2.getPropertyValue("Int"), 2); + ASSERT_EQ(child2_1.getPropertyValue("Ratio"), Ratio(1, 5)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientSetDotAccess) +{ + clientObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + clientObj.setPropertyValue("child1.child1_1.Float", 2.1); + clientObj.setPropertyValue("child1.child1_2.Int", 2); + clientObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "new_string"); + ASSERT_DOUBLE_EQ(serverObj.getPropertyValue("child1.child1_1.Float"), 2.1); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.Int"), 2); + ASSERT_EQ(serverObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "new_string"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 2.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 2); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientProtectedSet) +{ + ASSERT_THROW(clientObj.setPropertyValue("child1.child1_2.child1_2_1.ReadOnlyString", "new_string"), AccessDeniedException); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.ReadOnlyString"), "String"); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.ReadOnlyString"), "String"); + + ASSERT_NO_THROW(clientObj.asPtr().setProtectedPropertyValue("child1.child1_2.child1_2_1.ReadOnlyString", "new_string")); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.ReadOnlyString"), "new_string"); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.ReadOnlyString"), "new_string"); +} + +// TODO: Enable once clearing property values is supported via OPC UA +TEST_F(TmsNestedPropertyObjectTest, DISABLED_TestNestedObjectClientClear) +{ + clientObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + clientObj.setPropertyValue("child1.child1_1.Float", 2.1); + clientObj.setPropertyValue("child1.child1_2.Int", 2); + clientObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + clientObj.clearPropertyValue("ObjectProperty"); + + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(serverObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(serverObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 2)); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 2)); + + clientObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + clientObj.setPropertyValue("child1.child1_1.Float", 2.1); + clientObj.setPropertyValue("child1.child1_2.Int", 2); + clientObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + clientObj.clearPropertyValue("child1"); + + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(serverObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(serverObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectServerSet) +{ + serverObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + serverObj.setPropertyValue("child1.child1_1.Float", 2.1); + serverObj.setPropertyValue("child1.child1_2.Int", 2); + serverObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "new_string"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 2.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 2); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectServerClearIndividual) +{ + serverObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + serverObj.setPropertyValue("child1.child1_1.Float", 2.1); + serverObj.setPropertyValue("child1.child1_2.Int", 2); + serverObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "new_string"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 2.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 2); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); + + serverObj.clearPropertyValue("child1.child1_2.child1_2_1.String"); + serverObj.clearPropertyValue("child1.child1_1.Float"); + serverObj.clearPropertyValue("child1.child1_2.Int"); + serverObj.clearPropertyValue("child2.child2_1.Ratio"); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 2)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectServerClearObject) +{ + serverObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + serverObj.setPropertyValue("child1.child1_1.Float", 2.1); + serverObj.setPropertyValue("child1.child1_2.Int", 2); + serverObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "new_string"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 2.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 2); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); + + for (const auto& prop : serverObj.getAllProperties()) + serverObj.asPtr().clearProtectedPropertyValue(prop.getName()); + + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(serverObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(serverObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(serverObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 2)); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 2)); + + serverObj.setPropertyValue("child1.child1_2.child1_2_1.String", "new_string"); + serverObj.setPropertyValue("child1.child1_1.Float", 2.1); + serverObj.setPropertyValue("child1.child1_2.Int", 2); + serverObj.setPropertyValue("child2.child2_1.Ratio", Ratio(1, 5)); + + serverObj.asPtr().clearProtectedPropertyValue("child1"); + + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.child1_2_1.String"), "String"); + ASSERT_DOUBLE_EQ(clientObj.getPropertyValue("child1.child1_1.Float"), 1.1); + ASSERT_EQ(clientObj.getPropertyValue("child1.child1_2.Int"), 1); + ASSERT_EQ(clientObj.getPropertyValue("child2.child2_1.Ratio"), Ratio(1, 5)); +} + +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientFunctionCall) +{ + const PropertyObjectPtr child = clientObj.getPropertyValue("child1.child1_2.child1_2_1"); + FunctionPtr func1 = clientObj.getPropertyValue("child1.child1_2.child1_2_1.Function"); + FunctionPtr func2 = child.getPropertyValue("Function"); + + ASSERT_EQ(func1(1), 1); + ASSERT_EQ(func2(5), 5); +} + +// NOTE: OPC UA does not propagate error codes. +TEST_F(TmsNestedPropertyObjectTest, TestNestedObjectClientProcedureCall) +{ + const PropertyObjectPtr child = clientObj.getPropertyValue("child1.child1_2.child1_2_1"); + ProcedurePtr proc1 = clientObj.getPropertyValue("child1.child1_2.child1_2_1.Procedure"); + ProcedurePtr proc2 = child.getPropertyValue("Procedure"); + + ASSERT_NO_THROW(proc1(5)); + ASSERT_NO_THROW(proc1(0)); // Outputs warning + ASSERT_NO_THROW(proc2(5)); + ASSERT_NO_THROW(proc2(0)); // Outputs warning +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_signal.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_signal.cpp new file mode 100644 index 0000000..f9834e6 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_signal.cpp @@ -0,0 +1,700 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tms_object_integration_test.h" + + +using namespace daq; +using namespace opcua::tms; +using namespace opcua; + +class TmsSignalTest : public TmsObjectIntegrationTest +{ +public: + SignalPtr createSignal(const std::string& id) + { + SignalPtr signal = Signal(NullContext(), nullptr, id); + signal->setActive(false); + return signal; + } + + SignalPtr createFullSignal(const std::string& signalName) + { + // Build signal descriptor: + auto serverMetadata = Dict(); + for (size_t i = 0; i < 2; i++) + { + serverMetadata.set("Metadata" + std::to_string(i), "Value " + std::to_string(i)); + } + + // Build value descriptor: + auto serverDataDescriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Float32) + .setName("Value Name") + .setDimensions(List()) + .setRule(ConstantDataRule()) + .setUnit(Unit("Symbol", 1, "Name", "Quantity")) // Optional + .setOrigin("Origin") // Optional + .setValueRange(Range(0.0, 100.0)) // Optional + .setMetadata(serverMetadata) + .build(); + + // Build server signal + auto serverSignal = SignalWithDescriptor(NullContext(), serverDataDescriptor, nullptr, "sig"); + serverSignal->setActive(true); + serverSignal.getTags().asPtr().add("tag1"); + serverSignal.getTags().asPtr().add("tag2"); + + return serverSignal; + } + + void checkLastValueComplex(const SignalPtr& signal, const double& realValue, const double& imaginaryValue) + { + ComplexNumberPtr ptr = signal.getLastValue().asPtr(); + auto real = ptr.getReal(); + auto imaginary = ptr.getImaginary(); + + ASSERT_DOUBLE_EQ(real, realValue); + ASSERT_DOUBLE_EQ(imaginary, imaginaryValue); + } + + void checkLastValueRange(const SignalPtr& signal, const int64_t& lowValue, const int64_t& highValue) + { + RangePtr ptr = signal.getLastValue().asPtr(); + auto low = ptr.getLowValue().getIntValue(); + auto high = ptr.getHighValue().getIntValue(); + + ASSERT_EQ(low, lowValue); + ASSERT_EQ(high, highValue); + } + + void checkLastValueListOfInt64(const SignalPtr& signal) + { + auto lv = signal.getLastValue(); + ListPtr ptr; + ASSERT_NO_THROW(ptr = lv.asPtr()); + ASSERT_EQ(ptr.getItemAt(0), 4); + ASSERT_EQ(ptr.getItemAt(1), 44); + } + + template + void testGetLastValue(const SampleType& sampleType, const T& value) + { + auto daqServerSignal = Signal(NullContext(), nullptr, "Id"); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + daqServerSignal.setDescriptor(DataDescriptorBuilder().setSampleType(sampleType).build()); + + auto dataPacket = DataPacket(daqServerSignal.getDescriptor(), 5); + auto data = static_cast(dataPacket.getData()); + data[4] = value; + + daqServerSignal.sendPacket(dataPacket); + + auto clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + if (sampleType == SampleType::Float32 || sampleType == SampleType::Float64) + { + ASSERT_DOUBLE_EQ(clientSignal.getLastValue(), static_cast(value)); + ASSERT_DOUBLE_EQ(daqServerSignal.getLastValue(), static_cast(value)); + } + else + { + ASSERT_EQ(clientSignal.getLastValue(), static_cast(value)); + ASSERT_EQ(daqServerSignal.getLastValue(), static_cast(value)); + } + } + + template + void testGetLastValueComplex(const SampleType& sampleType, const T& realValue, const T& imaginaryValue) + { + auto daqServerSignal = Signal(NullContext(), nullptr, "Id"); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + daqServerSignal.setDescriptor(DataDescriptorBuilder().setSampleType(sampleType).build()); + + auto dataPacket = DataPacket(daqServerSignal.getDescriptor(), 5); + auto data = static_cast(dataPacket.getData()); + data[8] = realValue; + data[9] = imaginaryValue; + + daqServerSignal.sendPacket(dataPacket); + + auto clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + checkLastValueComplex(clientSignal, realValue, imaginaryValue); + checkLastValueComplex(daqServerSignal, realValue, imaginaryValue); + } +}; + +TEST_F(TmsSignalTest, Create) +{ + SignalPtr signal = Signal(NullContext(), nullptr, "sig"); + auto tmsSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); +} + +TEST_F(TmsSignalTest, Register) +{ + SignalPtr signal = createSignal("Id"); + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + ASSERT_TRUE(clientSignal.assigned()); +} + +TEST_F(TmsSignalTest, AttrUniqueId) +{ + SignalPtr daqServerSignal = createSignal("Id"); + daqServerSignal.setActive(true); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "Id", clientContext, nodeId); + + // UniqueId is set by core, so only test is that it is transferred correctly: + ASSERT_EQ(daqServerSignal.getLocalId(), clientSignal.getLocalId()); +} + +TEST_F(TmsSignalTest, AttrActive) +{ + SignalPtr daqServerSignal = createSignal("Id"); + daqServerSignal.setActive(true); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + ASSERT_TRUE(daqServerSignal.getActive()); + ASSERT_TRUE(clientSignal.getActive()); + + daqServerSignal.setActive(false); + + ASSERT_FALSE(daqServerSignal.getActive()); + ASSERT_FALSE(clientSignal.getActive()); + + clientSignal.setActive(true); + + ASSERT_TRUE(daqServerSignal.getActive()); + ASSERT_TRUE(clientSignal.getActive()); +} + +TEST_F(TmsSignalTest, AttrPublic) +{ + SignalPtr daqServerSignal = createSignal("Id"); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + ASSERT_TRUE(clientSignal.getPublic()); + + // client side change is reflected on client side (public is not transferred): + clientSignal.setPublic(false); + ASSERT_FALSE(clientSignal.getPublic()); + + clientSignal.setPublic(true); + ASSERT_TRUE(clientSignal.getPublic()); +} + +TEST_F(TmsSignalTest, GetLastValueFloat32) +{ + float value = 4.1f; + testGetLastValue(SampleType::Float32, value); +} + +TEST_F(TmsSignalTest, GetLastValueFloat64) +{ + double value = 4.1; + testGetLastValue(SampleType::Float64, value); +} + +TEST_F(TmsSignalTest, GetLastValueInt8) +{ + int8_t value = 4; + testGetLastValue(SampleType::Int8, value); +} + +TEST_F(TmsSignalTest, GetLastValueInt16) +{ + int16_t value = 4; + testGetLastValue(SampleType::Int16, value); +} + +TEST_F(TmsSignalTest, GetLastValueInt32) +{ + int32_t value = 4; + testGetLastValue(SampleType::Int32, value); +} +TEST_F(TmsSignalTest, GetLastValueInt64) +{ + int64_t value = 4; + testGetLastValue(SampleType::Int64, value); +} + +TEST_F(TmsSignalTest, GetLastValueUInt8) +{ + uint8_t value = 4u; + testGetLastValue(SampleType::UInt8, value); +} + +TEST_F(TmsSignalTest, GetLastValueUInt16) +{ + uint16_t value = 4u; + testGetLastValue(SampleType::UInt16, value); +} + +TEST_F(TmsSignalTest, GetLastValueUInt32) +{ + uint32_t value = 4u; + testGetLastValue(SampleType::UInt32, value); +} +TEST_F(TmsSignalTest, GetLastValueUInt64) +{ + uint64_t value = 4u; + testGetLastValue(SampleType::UInt64, value); +} + +TEST_F(TmsSignalTest, GetLastValueRange) +{ + auto daqServerSignal = Signal(NullContext(), nullptr, "Id"); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + daqServerSignal.setDescriptor(DataDescriptorBuilder().setSampleType(SampleType::RangeInt64).build()); + + auto dataPacket = DataPacket(daqServerSignal.getDescriptor(), 5); + auto data = static_cast(dataPacket.getData()); + data[8] = 8; + data[9] = 9; + + daqServerSignal.sendPacket(dataPacket); + + auto clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + checkLastValueRange(clientSignal, 8, 9); + checkLastValueRange(daqServerSignal, 8, 9); +} + +TEST_F(TmsSignalTest, GetLastValueListOfInt64) +{ + auto daqServerSignal = Signal(NullContext(), nullptr, "Id"); + + auto numbers = List(); + numbers.pushBack(1); + numbers.pushBack(2); + + auto dimensions = List(); + dimensions.pushBack(Dimension(ListDimensionRule(numbers))); + + auto descriptor = DataDescriptorBuilder().setName("test").setSampleType(SampleType::Int64).setDimensions(dimensions).build(); + + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + daqServerSignal.setDescriptor(DataDescriptorBuilder().setSampleType(SampleType::RangeInt64).build()); + + auto dataPacket = DataPacket(descriptor, 5); + auto data = static_cast(dataPacket.getData()); + data[8] = 4; + data[9] = 44; + + daqServerSignal.sendPacket(dataPacket); + + auto clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + checkLastValueListOfInt64(clientSignal); + checkLastValueListOfInt64(daqServerSignal); +} + +TEST_F(TmsSignalTest, GetLastValueComplexFloat32) +{ + float real = 8.1f; + float imaginary = 9.1f; + testGetLastValueComplex(SampleType::ComplexFloat32, real, imaginary); +} + +TEST_F(TmsSignalTest, GetLastValueComplexFloat64) +{ + double real = 8.1; + double imaginary = 9.1; + testGetLastValueComplex(SampleType::ComplexFloat64, real, imaginary); +} + +TEST_F(TmsSignalTest, AttrDescriptor) +{ + auto serverMetadata = Dict(); + for (size_t i = 0; i < 2; i++) + { + serverMetadata.set("Metadata" + std::to_string(i), "Value " + std::to_string(i)); + } + + auto serverDataDescriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Float32) + .setDimensions(List()) + .setRule(ConstantDataRule()) + .setUnit(Unit("Symbol", 1, "Name", "Quantity")) // Optional + .setOrigin("Origin") // Optional + .setValueRange(Range(0.0, 100.0)) // Optional + .setName("Signal Name") + .setMetadata(serverMetadata) + .build(); + + // Build server signal + auto serverSignal = Signal(NullContext(), nullptr, "sig"); + serverSignal.setName("My signal"); + serverSignal.setDescription("My signal description"); + serverSignal->setActive(true); + ASSERT_FALSE(serverSignal.getDescriptor().assigned()); + serverSignal.setDescriptor(serverDataDescriptor); + ASSERT_EQ(serverSignal.getDescriptor(), serverDataDescriptor); + + auto tmsServerSignal = TmsServerSignal(serverSignal, this->getServer(), ctx, serverContext); + auto nodeId = tmsServerSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + ASSERT_EQ(clientSignal.getActive(), serverSignal.getActive()); + // TODO: TMS signal should be implemented similar as fb, i.e. it needs to include property object + // ASSERT_EQ(clientSignal.getName(), "My signal"); + // ASSERT_EQ(clientSignal.getDescription(), "My signal description"); + auto clientDataDescriptor = clientSignal.getDescriptor(); + ASSERT_EQ(clientDataDescriptor.getName(), "Signal Name"); + + auto clientMetadata = clientDataDescriptor.getMetadata(); + auto keyList = clientMetadata.getKeyList(); + auto valueList = clientMetadata.getValueList(); + ASSERT_EQ(keyList.getCount(), 2u); + ASSERT_EQ(valueList.getCount(), 2u); + for (int i = 0; i < 2; i++) + { + auto key = keyList.getItemAt(i); + auto value = valueList.getItemAt(i); + ASSERT_EQ("Metadata" + std::to_string(i), key); + ASSERT_EQ("Value " + std::to_string(i), value); + } + + ASSERT_EQ(clientDataDescriptor.getDimensions().getCount(), 0u); + ASSERT_EQ(clientDataDescriptor.getOrigin(), "Origin"); + ASSERT_EQ(clientDataDescriptor.getSampleType(), SampleType::Float32); + + auto clientRule = clientDataDescriptor.getRule(); + ASSERT_EQ(clientRule.getType(), DataRuleType::Constant); + auto clientRuleParameters = clientRule.getParameters(); + ASSERT_EQ(clientRuleParameters.getCount(), 0u); + + auto clientUnit = clientDataDescriptor.getUnit(); + ASSERT_EQ(clientUnit.getQuantity(), "Quantity"); + ASSERT_EQ(clientUnit.getName(), "Name"); + ASSERT_EQ(clientUnit.getSymbol(), "Symbol"); + + auto clientValueRange = clientDataDescriptor.getValueRange(); + ASSERT_EQ(clientValueRange.getLowValue(), 0.0); + ASSERT_EQ(clientValueRange.getHighValue(), 100.0); + + auto browseName = client->readBrowseName(nodeId); + ASSERT_EQ(browseName, "sig"); + + auto displayName = client->readDisplayName(nodeId); + ASSERT_EQ(displayName, "My signal"); +} + +TEST_F(TmsSignalTest, AttrDomainSignal) +{ + SignalPtr daqServerDomainSignal = createSignal("sig1"); + auto serverDomainSignal = TmsServerSignal(daqServerDomainSignal, this->getServer(), ctx, serverContext); + auto domainNodeId = serverDomainSignal.registerOpcUaNode(); + + SignalPtr daqServerSignal = createSignal("sig2"); + auto serverSignal = TmsServerSignal(daqServerSignal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + OpcUaNodeId referenceTypeId(NAMESPACE_DAQBSP, UA_DAQBSPID_HASDOMAINSIGNAL); + getServer()->addReference(nodeId, referenceTypeId, domainNodeId); + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + [[maybe_unused]] SignalPtr clientDomainSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, domainNodeId); + + SignalPtr domainSignal; + EXPECT_NO_THROW(domainSignal = clientSignal.getDomainSignal()); + ASSERT_EQ(clientDomainSignal, domainSignal); +} + +TEST_F(TmsSignalTest, AttrRelatedSignals) +{ + SignalPtr daqServerSignal1 = createSignal("id1"); + auto serverSignal1 = TmsServerSignal(daqServerSignal1, this->getServer(), ctx, serverContext); + auto nodeId1 = serverSignal1.registerOpcUaNode(); + + SignalPtr daqServerSignal2 = createSignal("id2"); + auto serverSignal2 = TmsServerSignal(daqServerSignal2, this->getServer(), ctx, serverContext); + auto nodeId2 = serverSignal2.registerOpcUaNode(); + + SignalPtr daqServerSignal3 = createSignal("id3"); + auto serverSignal3 = TmsServerSignal(daqServerSignal3, this->getServer(), ctx, serverContext); + auto nodeId3 = serverSignal3.registerOpcUaNode(); + + OpcUaNodeId referenceTypeId(NAMESPACE_DAQBSP, UA_DAQBSPID_RELATESTOSIGNAL); + getServer()->addReference(nodeId2, referenceTypeId, nodeId1); + getServer()->addReference(nodeId3, referenceTypeId, nodeId1); + getServer()->addReference(nodeId3, referenceTypeId, nodeId2); + + SignalPtr clientSignal1 = TmsClientSignal(NullContext(), nullptr, "sig1", clientContext, nodeId1); + SignalPtr clientSignal2 = TmsClientSignal(NullContext(), nullptr, "sig2", clientContext, nodeId2); + SignalPtr clientSignal3 = TmsClientSignal(NullContext(), nullptr, "sig3", clientContext, nodeId3); + + auto relatedSignals1 = clientSignal1.getRelatedSignals(); + auto relatedSignals2 = clientSignal2.getRelatedSignals(); + auto relatedSignals3 = clientSignal3.getRelatedSignals(); + + ASSERT_TRUE(relatedSignals1 != nullptr); + ASSERT_TRUE(relatedSignals2 != nullptr); + ASSERT_TRUE(relatedSignals3 != nullptr); + + ASSERT_EQ(relatedSignals1.getCount(), 0u); + + ASSERT_EQ(relatedSignals2.getCount(), 1u); + ASSERT_EQ(relatedSignals2[0].getLocalId(), "sig1"); + + ASSERT_EQ(relatedSignals3.getCount(), 2u); + // Cannot depend on order... + ASSERT_TRUE((relatedSignals3[0] == clientSignal1 || relatedSignals3[0] == clientSignal2) && relatedSignals3[0] != clientSignal3); + ASSERT_TRUE((relatedSignals3[1] == clientSignal1 || relatedSignals3[1] == clientSignal2) && relatedSignals3[1] != clientSignal3); + ASSERT_TRUE(relatedSignals3[0] != relatedSignals3[1]); +} + +TEST_F(TmsSignalTest, MethodGetConnections) +{ + SignalPtr daqServerSignal1 = createSignal("Id"); + auto serverSignal1 = TmsServerSignal(daqServerSignal1, this->getServer(), ctx, serverContext); + auto nodeId1 = serverSignal1.registerOpcUaNode(); + + SignalPtr clientSignal1 = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId1); + ASSERT_EQ(clientSignal1.getConnections().getCount(), 0u); + + // TODO: Implement more test when related signals actually can be returned + // ASSERT_EQ(relatedSignals1.getCount(), 2u); + // ASSERT_EQ(relatedSignals2.getCount(), 2u); + // ASSERT_EQ(relatedSignals3.getCount(), 2u); +} + +TEST_F(TmsSignalTest, ComponentMethods) +{ + auto signal = createFullSignal("sig"); + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + ASSERT_EQ(signal.getName(), clientSignal.getName()); + + // TODO: Support changing read-only properties over OPC UA + ASSERT_NO_THROW(clientSignal.setName("new_name")); + ASSERT_EQ(signal.getName(), clientSignal.getName()); + + ASSERT_NO_THROW(clientSignal.setDescription("new_description")); + ASSERT_EQ(signal.getDescription(), clientSignal.getDescription()); + + auto tags = signal.getTags(); + auto clientTags = clientSignal.getTags(); + ASSERT_TRUE(clientTags.query("tag1") && clientTags.query("tag2")); +} + +TEST_F(TmsSignalTest, ExplicitDomainRuleDescriptor) +{ + const auto descriptor = DataDescriptorBuilder().setSampleType(SampleType::Int64).setRule(ExplicitDomainDataRule(5.1, 20)).build(); + const auto signal = SignalWithDescriptor(NullContext(), descriptor, nullptr, "sig"); + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + ASSERT_EQ(signal.getDescriptor().getRule(), clientSignal.getDescriptor().getRule()); +} + +TEST_F(TmsSignalTest, Visible) +{ + const auto descriptor = DataDescriptorBuilder().setSampleType(SampleType::Int64).setRule(ExplicitDomainDataRule()).build(); + const auto signal = Signal(NullContext(), nullptr, "sig"); + signal.asPtr().unlockAllAttributes(); + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + const auto nodeId = serverSignal.registerOpcUaNode(); + const SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + + ASSERT_EQ(signal.getVisible(), clientSignal.getVisible()); + ASSERT_NO_THROW(clientSignal.setVisible(false)); + ASSERT_EQ(signal.getVisible(), false); + ASSERT_EQ(clientSignal.getVisible(), false); + + signal.asPtr().lockAllAttributes(); + + ASSERT_EQ(signal.getVisible(), clientSignal.getVisible()); + ASSERT_NO_THROW(clientSignal.setVisible(true)); + ASSERT_EQ(signal.getVisible(), false); + ASSERT_EQ(clientSignal.getVisible(), false); +} + +TEST_F(TmsSignalTest, GetNoValue) +{ + auto domainDescriptor = DataDescriptorBuilder() + .setName("domain stub") + .setSampleType(SampleType::UInt64) + .setOrigin("2024-01-08T00:02:03+00:00") + .setTickResolution(Ratio(1, 1000000)) + .setRule(LinearDataRule(1000, 0)) + .setUnit(Unit("s", -1, "seconds", "time")) + .build(); + const auto domainSignal = SignalWithDescriptor(NullContext(), domainDescriptor, nullptr, "domainSig"); + + auto dataDescriptor = DataDescriptorBuilder().setName("stub").setSampleType(SampleType::Float64).build(); + const auto signal = SignalWithDescriptor(NullContext(), dataDescriptor, nullptr, "sig"); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + ASSERT_TRUE(clientSignal.assigned()); + ASSERT_FALSE(clientSignal.getLastValue().assigned()); +} + +template +static void sendValueToSignal(const SignalConfigPtr& signal, const T& value) +{ + if (!signal.assigned()) + return; + + DataPacketPtr packet; + + if (signal.getDomainSignal().assigned()) + { + auto domainPacket = DataPacket(signal.getDomainSignal().getDescriptor(), 1); + packet = DataPacketWithDomain(domainPacket, signal.getDescriptor(), 1); + } + else + { + packet = DataPacket(signal.getDescriptor(), 1); + } + + auto dst = static_cast(packet.getData()); + *dst = value; + signal.sendPacket(packet); +} + +TEST_F(TmsSignalTest, GetValue) +{ + auto domainDescriptor = DataDescriptorBuilder() + .setName("domain stub") + .setSampleType(SampleType::UInt64) + .setOrigin("2024-01-08T00:02:03+00:00") + .setTickResolution(Ratio(1, 1000000)) + .setRule(LinearDataRule(1000, 0)) + .setUnit(Unit("s", -1, "seconds", "time")) + .build(); + const auto domainSignal = SignalWithDescriptor(NullContext(), domainDescriptor, nullptr, "domainSig"); + + auto dataDescriptor = DataDescriptorBuilder().setName("stub").setSampleType(SampleType::Float64).build(); + const auto signal = SignalWithDescriptor(NullContext(), dataDescriptor, nullptr, "sig"); + + auto serverSignal = TmsServerSignal(signal, this->getServer(), ctx, serverContext); + auto nodeId = serverSignal.registerOpcUaNode(); + + SignalPtr clientSignal = TmsClientSignal(NullContext(), nullptr, "sig", clientContext, nodeId); + ASSERT_TRUE(clientSignal.assigned()); + + sendValueToSignal(signal, 1.0); + sendValueToSignal(signal, 10.0); + + ASSERT_EQ(clientSignal.getLastValue(), 10.0); +} + +TEST_F(TmsSignalTest, ValueAndAnalogValueDataTypeChange) +{ + // Create signal with Float64 descriptor + auto descriptor1 = DataDescriptorBuilder() + .setSampleType(SampleType::Float64) + .setName("TestSignal") + .build(); + SignalConfigPtr serverSignal = Signal(ctx, nullptr, "sig"); + serverSignal.setDescriptor(descriptor1); + serverSignal.setActive(true); + + // IMPORTANT: Keep TmsServerSignal alive for the duration of the test + // It must be a shared_ptr so that weak_ptr in TmsServerContext can lock it + auto tmsServerSignal = std::make_shared(serverSignal, this->getServer(), ctx, serverContext); + auto signalNodeId = tmsServerSignal->registerOpcUaNode(); + + // Get Value and AnalogValue node IDs using browser + CachedReferenceBrowser browser(client); + auto valueNodeId = browser.getChildNodeId(signalNodeId, "Value"); + auto analogValueNodeId = browser.getChildNodeId(signalNodeId, "AnalogValue"); + + // Check initial data types are Float64 + auto valueDataType1 = client->readDataType(valueNodeId); + ASSERT_EQ(valueDataType1, OpcUaNodeId(0, UA_NS0ID_DOUBLE)); + + auto analogValueDataType1 = client->readDataType(analogValueNodeId); + ASSERT_EQ(analogValueDataType1, OpcUaNodeId(0, UA_NS0ID_DOUBLE)); + + // Change descriptor to Int32 + auto descriptor2 = DataDescriptorBuilder() + .setSampleType(SampleType::Int32) + .setName("TestSignal") + .build(); + serverSignal.setDescriptor(descriptor2); + + // Manually trigger the DataDescriptorChanged event on TmsServerSignal + // Without Instance, core events are not generated automatically + auto eventArgs = CoreEventArgsDataDescriptorChanged(descriptor2); + tmsServerSignal->onCoreEvent(eventArgs); + + // Create a new browser to get fresh references after node recreation + // Old browser may have cached references to deleted nodes + CachedReferenceBrowser newBrowser(client); + + // Get new node IDs after recreation (old ones may be invalid) + auto newValueNodeId = newBrowser.getChildNodeId(signalNodeId, "Value"); + auto newAnalogValueNodeId = newBrowser.getChildNodeId(signalNodeId, "AnalogValue"); + + // Verify that nodes exist + ASSERT_TRUE(client->nodeExists(newValueNodeId)) + << "New Value node should exist after recreation"; + ASSERT_TRUE(client->nodeExists(newAnalogValueNodeId)) + << "New AnalogValue node should exist after recreation"; + + // Check that data types have changed to Int32 + auto valueDataType2 = client->readDataType(newValueNodeId); + ASSERT_EQ(valueDataType2, OpcUaNodeId(0, UA_NS0ID_INT32)) + << "Value node data type should be Int32 after descriptor change. Got: " << valueDataType2.toString(); + + auto analogValueDataType2 = client->readDataType(newAnalogValueNodeId); + ASSERT_EQ(analogValueDataType2, OpcUaNodeId(0, UA_NS0ID_INT32)) + << "AnalogValue node data type should be Int32 after descriptor change. Got: " << analogValueDataType2.toString(); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp b/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp new file mode 100644 index 0000000..d147f7b --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.cpp @@ -0,0 +1,48 @@ +#include "tms_object_integration_test.h" +#include + +using namespace daq; +using namespace daq::opcua; +using namespace daq::opcua::tms; + +static LoggerPtr CreateLoggerWithDebugSink(const LoggerSinkPtr& sink) +{ + sink.setLevel(LogLevel::Warn); + auto sinks = DefaultSinks(nullptr); + sinks.pushBack(sink); + return LoggerWithSinks(sinks); +} + +void TmsObjectIntegrationTest::SetUp() +{ + TmsObjectTest::SetUp(); + debugSink = LastMessageLoggerSink(); + logger = CreateLoggerWithDebugSink(debugSink); + + ctx = daq::NullContext(logger); + ctxClient = daq::NullContext(logger); + clientContext = std::make_shared(client, ctxClient); + serverContext = std::make_shared(ctx, nullptr); + + clientContext->addEnumerationTypesToTypeManager(); +} + +LastMessageLoggerSinkPrivatePtr TmsObjectIntegrationTest::getPrivateSink() +{ + if(!debugSink.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException, "Sink must not be null"); + auto sinkPtr = debugSink.asPtrOrNull(); + if (sinkPtr == nullptr) + DAQ_THROW_EXCEPTION(InvalidTypeException, "Wrong sink. GetLastMessage supports only by LastMessageLoggerSink"); + return sinkPtr; +} + +void TmsObjectIntegrationTest::TearDown() +{ + clientContext.reset(); + serverContext = nullptr; + ctx = nullptr; + ctxClient = nullptr; + + TmsObjectTest::TearDown(); +} diff --git a/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.h b/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.h new file mode 100644 index 0000000..a85d8c9 --- /dev/null +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/tms_object_integration_test.h @@ -0,0 +1,40 @@ +/* + * 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 + +class TmsObjectIntegrationTest : public TmsObjectTest +{ +public: + void SetUp() override; + void TearDown() override; + daq::LastMessageLoggerSinkPrivatePtr getPrivateSink(); + +protected: + daq::opcua::tms::TmsClientContextPtr clientContext; + daq::opcua::tms::TmsServerContextPtr serverContext; + + daq::LoggerPtr logger; + daq::ContextPtr ctx; + daq::ContextPtr ctxClient; +private: + daq::LoggerSinkPtr debugSink; +}; diff --git a/shared/libraries/opcuatms/tests/test_utils/CMakeLists.txt b/shared/libraries/opcuatms/tests/test_utils/CMakeLists.txt new file mode 100644 index 0000000..e64509b --- /dev/null +++ b/shared/libraries/opcuatms/tests/test_utils/CMakeLists.txt @@ -0,0 +1,21 @@ +set(MODULE_NAME opcuatms_test_utils) + +set(MODULE_SOURCES tms_object_test.h + tms_object_test.cpp + test_input_port_notifications.h + test_input_port_notifications.cpp + +) + +add_library(${MODULE_NAME} STATIC ${MODULE_SOURCES}) +add_library(${SDK_TARGET_NAMESPACE}::${MODULE_NAME} ALIAS ${MODULE_NAME}) + +target_include_directories(${MODULE_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(${MODULE_NAME} PUBLIC daq::test_utils + daq::opcuaclient + daq::opcuaserver + daq::opendaq + daq::opcua_daq_types +) diff --git a/shared/libraries/opcuatms/tests/test_utils/test_input_port_notifications.cpp b/shared/libraries/opcuatms/tests/test_utils/test_input_port_notifications.cpp new file mode 100644 index 0000000..db7e6d7 --- /dev/null +++ b/shared/libraries/opcuatms/tests/test_utils/test_input_port_notifications.cpp @@ -0,0 +1,29 @@ +#include "test_input_port_notifications.h" + +using namespace daq; + +TestInputPortNotificationsImpl::TestInputPortNotificationsImpl() + : ImplementationOfWeak() +{ +} + +ErrCode TestInputPortNotificationsImpl::acceptsSignal(IInputPort* port, ISignal* signal, Bool* accept) +{ + *accept = true; + return OPENDAQ_SUCCESS; +} + +ErrCode TestInputPortNotificationsImpl::connected(IInputPort* port) +{ + return OPENDAQ_SUCCESS; +} + +ErrCode TestInputPortNotificationsImpl::disconnected(IInputPort* port) +{ + return OPENDAQ_SUCCESS; +} + +daq::ErrCode TestInputPortNotificationsImpl::packetReceived(daq::IInputPort* port) +{ + return OPENDAQ_SUCCESS; +} diff --git a/shared/libraries/opcuatms/tests/test_utils/test_input_port_notifications.h b/shared/libraries/opcuatms/tests/test_utils/test_input_port_notifications.h new file mode 100644 index 0000000..7b551fd --- /dev/null +++ b/shared/libraries/opcuatms/tests/test_utils/test_input_port_notifications.h @@ -0,0 +1,40 @@ +/* + * 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 + +class TestInputPortNotificationsImpl : public daq::ImplementationOfWeak +{ +public: + TestInputPortNotificationsImpl(); + + daq::ErrCode INTERFACE_FUNC acceptsSignal(daq::IInputPort* port, daq::ISignal* signal, daq::Bool* accept) override; + daq::ErrCode INTERFACE_FUNC connected(daq::IInputPort* port) override; + daq::ErrCode INTERFACE_FUNC disconnected(daq::IInputPort* port) override; + daq::ErrCode INTERFACE_FUNC packetReceived(daq::IInputPort* port) override; +}; + +OPENDAQ_DECLARE_CLASS_FACTORY_WITH_INTERFACE(INLINE_FACTORY, TestInputPortNotifications, daq::IInputPortNotifications) +OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE(INLINE_FACTORY, TestInputPortNotifications, daq::IInputPortNotifications); + +inline daq::InputPortNotificationsPtr TestInputPortNotifications() +{ + daq::InputPortNotificationsPtr inputPortNotification = TestInputPortNotifications_Create(); + return inputPortNotification; +} diff --git a/shared/libraries/opcuatms/tests/test_utils/tms_object_test.cpp b/shared/libraries/opcuatms/tests/test_utils/tms_object_test.cpp new file mode 100644 index 0000000..b339166 --- /dev/null +++ b/shared/libraries/opcuatms/tests/test_utils/tms_object_test.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace daq::opcua; + +TmsObjectTest::TmsObjectTest() +{ +} + +void TmsObjectTest::SetUp() +{ + testing::Test::SetUp(); + + server = CreateAndStartTestServer(); + client = CreateAndConnectTestClient(); +} + +void TmsObjectTest::TearDown() +{ + client.reset(); + server.reset(); + testing::Test::TearDown(); +} + +OpcUaServerPtr TmsObjectTest::getServer() +{ + return server; +} + +OpcUaClientPtr TmsObjectTest::getClient() +{ + return client; +} + +void TmsObjectTest::writeChildNode(const OpcUaNodeId& parent, const std::string& browseName, const OpcUaVariant& variant) +{ + getClient()->writeValue(getChildNodeId(parent, browseName), variant); +} + +OpcUaVariant TmsObjectTest::readChildNode(const OpcUaNodeId& parent, const std::string& browseName) +{ + return getClient()->readValue(getChildNodeId(parent, browseName)); +} + +OpcUaNodeId TmsObjectTest::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); +} + +void TmsObjectTest::waitForInput() +{ + std::cout << "Type q to quit..." << std::endl; + std::string input; + std::cin >> input; +} + +daq::opcua::OpcUaServerPtr TmsObjectTest::CreateAndStartTestServer() +{ + auto server = std::make_shared(); + server->setPort(4840); + server->start(); + return server; +} + +daq::opcua::OpcUaClientPtr TmsObjectTest::CreateAndConnectTestClient() +{ + OpcUaEndpoint endpoint("opc.tcp://127.0.0.1:4840"); + endpoint.registerCustomTypes(UA_TYPES_DI_COUNT, UA_TYPES_DI); + endpoint.registerCustomTypes(UA_TYPES_DAQBT_COUNT, UA_TYPES_DAQBT); + endpoint.registerCustomTypes(UA_TYPES_DAQBSP_COUNT, UA_TYPES_DAQBSP); + endpoint.registerCustomTypes(UA_TYPES_DAQDEVICE_COUNT, UA_TYPES_DAQDEVICE); + endpoint.registerCustomTypes(UA_TYPES_DAQESP_COUNT, UA_TYPES_DAQESP); + endpoint.registerCustomTypes(UA_TYPES_DAQHBK_COUNT, UA_TYPES_DAQHBK); + + auto client = std::make_shared(endpoint); + client->connect(); + client->runIterate(); + return client; +} diff --git a/shared/libraries/opcuatms/tests/test_utils/tms_object_test.h b/shared/libraries/opcuatms/tests/test_utils/tms_object_test.h new file mode 100644 index 0000000..1790706 --- /dev/null +++ b/shared/libraries/opcuatms/tests/test_utils/tms_object_test.h @@ -0,0 +1,45 @@ +/* + * 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 + +class TmsObjectTest : public testing::Test +{ +public: + TmsObjectTest(); + + virtual void SetUp() override; + virtual void TearDown() override; + + daq::opcua::OpcUaServerPtr getServer(); + 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); + void waitForInput(); + + static daq::opcua::OpcUaServerPtr CreateAndStartTestServer(); + static daq::opcua::OpcUaClientPtr CreateAndConnectTestClient(); + +protected: + daq::opcua::OpcUaServerPtr server; + daq::opcua::OpcUaClientPtr client; +};