From 0431529fb4b4414cc05227fa6692dfb4962ecfeb Mon Sep 17 00:00:00 2001 From: tomaz-cvetko Date: Thu, 24 Sep 2026 14:42:24 +0200 Subject: [PATCH 1/2] Implementation --- .../tms_client_function_block_type_impl.h | 29 +++++++++ .../tms_client_function_block_impl.cpp | 21 ++++++- .../tms_client_function_block_type_impl.cpp | 57 ++++++++++++++++++ .../objects/tms_server_function_block_type.h | 12 ++++ .../src/objects/tms_server_function_block.cpp | 7 +++ .../tms_server_function_block_type.cpp | 33 ++++++++++ .../test_tms_function_block.cpp | 40 +++++++++++++ .../test_tms_function_block_type.cpp | 60 +++++++++++++++++++ 8 files changed, 258 insertions(+), 1 deletion(-) 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 index 41187a27..20ec9e2f 100644 --- 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 @@ -20,6 +20,31 @@ BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS +/*! + * @brief The function block type options carried next to the FunctionBlockInfoStructure. + * + * The companion specification's structure only holds id, name and description, so the options + * travel as read-only properties of the node. A server that predates them exposes no such nodes, + * in which case every field keeps its default. + */ +struct FunctionBlockTypeOptions +{ + Bool alwaysEmptyInput = False; + Bool singleton = False; + StringPtr commonSettingsTypeId; +}; + +/*! + * @brief Reads the function block type options from the properties of the given node. + * @param clientContext The client context used to browse for the option nodes. + * @param nodeId The node the options are attached to. + * + * Missing nodes are not an error: the corresponding option keeps its default, which is how a + * connection to a server that does not publish the options behaves. + */ +FunctionBlockTypeOptions ReadFunctionBlockTypeOptions(const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId); + class TmsClientFunctionBlockTypeImpl final : public TmsClientObjectImpl, public FunctionBlockTypeImpl { public: @@ -31,12 +56,16 @@ class TmsClientFunctionBlockTypeImpl final : public TmsClientObjectImpl, public ErrCode INTERFACE_FUNC getName(IString** name) override; ErrCode INTERFACE_FUNC getDescription(IString** description) override; ErrCode INTERFACE_FUNC createDefaultConfig(IPropertyObject** defaultConfig) override; + ErrCode INTERFACE_FUNC getAlwaysEmptyInput(Bool* alwaysEmpty) override; + ErrCode INTERFACE_FUNC getSingleton(Bool* singleton) override; + ErrCode INTERFACE_FUNC getCommonSettingsTypeId(IString** typeId) override; private: void readAttributes(); FunctionBlockTypePtr type; PropertyObjectPtr defaultConfig; + FunctionBlockTypeOptions options; }; 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 index a4f568cc..a9ef9569 100644 --- 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 @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include @@ -174,7 +176,24 @@ void TmsClientFunctionBlockBaseImpl::readFbType() { auto infoNodeId = this->getNodeId("FunctionBlockInfo"); auto variant = this->clientContext->getAttributeReader()->getValue(infoNodeId, UA_ATTRIBUTEID_VALUE); - this->type = VariantConverter::ToDaqObject(variant).detach(); + const auto baseType = VariantConverter::ToDaqObject(variant); + + // The options live next to the structure rather than inside it, so the type has to be rebuilt + // once they are read. Against a server that does not publish them the options keep their + // defaults and the type matches what the structure alone used to produce. + const auto options = ReadFunctionBlockTypeOptions(this->clientContext, infoNodeId); + + FunctionBlockTypePtr fbType = FunctionBlockTypeBuilder() + .setId(baseType.getId()) + .setName(baseType.getName()) + .setDescription(baseType.getDescription()) + .setDefaultConfig(baseType.createDefaultConfig()) + .setAlwaysEmptyInput(options.alwaysEmptyInput) + .setSingleton(options.singleton) + .setCommonSettingsTypeId(options.commonSettingsTypeId) + .build(); + + this->type = fbType.detach(); } template 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 index a7614849..b510e98e 100644 --- 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 @@ -2,9 +2,41 @@ #include #include #include +#include BEGIN_NAMESPACE_OPENDAQ_OPCUA_TMS +FunctionBlockTypeOptions ReadFunctionBlockTypeOptions(const TmsClientContextPtr& clientContext, + const opcua::OpcUaNodeId& nodeId) +{ + FunctionBlockTypeOptions options; + + const auto browser = clientContext->getReferenceBrowser(); + const auto client = clientContext->getClient(); + + const auto readOption = [&browser, &client, &nodeId](const std::string& name, OpcUaVariant& value) + { + if (!browser->hasReference(nodeId, name)) + return false; + + value = client->readValue(browser->getChildNodeId(nodeId, name)); + return !value.isNull(); + }; + + OpcUaVariant value; + + if (readOption("AlwaysEmptyInput", value) && value.isBool()) + options.alwaysEmptyInput = value.readScalar() ? True : False; + + if (readOption("Singleton", value) && value.isBool()) + options.singleton = value.readScalar() ? True : False; + + if (readOption("CommonSettingsTypeId", value) && value.isString()) + options.commonSettingsTypeId = ConvertToDaqCoreString(value.readScalar()); + + return options; +} + TmsClientFunctionBlockTypeImpl::TmsClientFunctionBlockTypeImpl(const ContextPtr& context, const TmsClientContextPtr& tmsContext, const opcua::OpcUaNodeId& nodeId) @@ -47,10 +79,35 @@ ErrCode TmsClientFunctionBlockTypeImpl::createDefaultConfig(IPropertyObject** de return OPENDAQ_SUCCESS; } +ErrCode TmsClientFunctionBlockTypeImpl::getAlwaysEmptyInput(Bool* alwaysEmpty) +{ + OPENDAQ_PARAM_NOT_NULL(alwaysEmpty); + + *alwaysEmpty = options.alwaysEmptyInput; + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientFunctionBlockTypeImpl::getSingleton(Bool* singleton) +{ + OPENDAQ_PARAM_NOT_NULL(singleton); + + *singleton = options.singleton; + return OPENDAQ_SUCCESS; +} + +ErrCode TmsClientFunctionBlockTypeImpl::getCommonSettingsTypeId(IString** typeId) +{ + OPENDAQ_PARAM_NOT_NULL(typeId); + + *typeId = options.commonSettingsTypeId.addRefAndReturn(); + return OPENDAQ_SUCCESS; +} + void TmsClientFunctionBlockTypeImpl::readAttributes() { const auto value = client->readValue(nodeId); this->type = VariantConverter::ToDaqObject(value); + this->options = ReadFunctionBlockTypeOptions(clientContext, nodeId); const auto defaultConfigId = getNodeId("DefaultConfig"); this->defaultConfig = TmsClientPropertyObject(daqContext, clientContext, defaultConfigId); 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 index b2152beb..fe1beffb 100644 --- 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 @@ -40,6 +40,18 @@ class TmsServerFunctionBlockType : public TmsServerVariable #include +#include #include #include #include @@ -81,6 +82,12 @@ void TmsServerFunctionBlock::addChildNodes() functionBlocks.push_back(std::move(tmsFunctionBlock)); } + // The options are not carried by FunctionBlockInfoStructure, so they are exposed as + // properties of the FunctionBlockInfo node instead. + const auto infoNodeId = this->getChildNodeId("FunctionBlockInfo"); + if (!infoNodeId.isNull()) + TmsServerFunctionBlockType::AddOptionNodes(this->server, infoNodeId, this->object.getFunctionBlockType(), this); + Super::addChildNodes(); } 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 index ec21682d..58d0ba66 100644 --- 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 @@ -1,6 +1,7 @@ #include #include #include +#include #include using namespace daq::opcua; @@ -39,6 +40,38 @@ void TmsServerFunctionBlockType::addChildNodes() { Super::addChildNodes(); addDefaultConfigNode(); + AddOptionNodes(server, nodeId, object, this); +} + +void TmsServerFunctionBlockType::AddOptionNodes(const OpcUaServerPtr& server, + const OpcUaNodeId& parentNodeId, + const FunctionBlockTypePtr& type, + void* nodeContext) +{ + if (!type.assigned()) + return; + + const auto addNode = [&server, &parentNodeId, nodeContext](const std::string& name, + const UA_DataType& dataType, + const OpcUaVariant& value) + { + AddVariableNodeParams params(OpcUaNodeId(0), parentNodeId); + params.setBrowseName(name); + params.nodeContext = nodeContext; + params.setDataType(OpcUaNodeId(dataType.typeId)); + params.typeDefinition = OpcUaNodeId(UA_NODEID_NUMERIC(0, UA_NS0ID_PROPERTYTYPE)); + params.attr->accessLevel = UA_ACCESSLEVELMASK_READ; + params.attr->writeMask = 0; + params.attr->value = value.copyAndGetDetachedValue(); + server->addVariableNode(params); + }; + + addNode("AlwaysEmptyInput", UA_TYPES[UA_TYPES_BOOLEAN], OpcUaVariant(IsTrue(type.getAlwaysEmptyInput()))); + addNode("Singleton", UA_TYPES[UA_TYPES_BOOLEAN], OpcUaVariant(IsTrue(type.getSingleton()))); + + const StringPtr commonSettingsTypeId = type.getCommonSettingsTypeId(); + if (commonSettingsTypeId.assigned()) + addNode("CommonSettingsTypeId", UA_TYPES[UA_TYPES_STRING], OpcUaVariant(commonSettingsTypeId.getCharPtr())); } void TmsServerFunctionBlockType::configureVariableNodeAttributes(OpcUaObject& attr) 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 index 994ec940..2e405002 100644 --- a/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block.cpp +++ b/shared/libraries/opcuatms/tests/opcuatms_integration/test_tms_function_block.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -103,6 +104,45 @@ TEST_F(TmsFunctionBlockTest, AttrFunctionBlockType) ASSERT_EQ(clientType.getDescription(), "DESCRIPTION"); } +TEST_F(TmsFunctionBlockTest, AttrFunctionBlockTypeOptions) +{ + const FunctionBlockTypePtr type = FunctionBlockTypeBuilder() + .setId("UNIQUE ID") + .setName("NAME") + .setDescription("DESCRIPTION") + .setAlwaysEmptyInput(True) + .setSingleton(True) + .setCommonSettingsTypeId("SettingsId") + .build(); + + auto serverFunctionBlock = createFunctionBlock(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_TRUE(clientType.getAlwaysEmptyInput()); + ASSERT_TRUE(clientType.getSingleton()); + ASSERT_EQ(clientType.getCommonSettingsTypeId(), "SettingsId"); +} + +TEST_F(TmsFunctionBlockTest, AttrFunctionBlockTypeOptionDefaults) +{ + const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); + + auto serverFunctionBlock = createFunctionBlock(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_FALSE(clientType.getAlwaysEmptyInput()); + ASSERT_FALSE(clientType.getSingleton()); + ASSERT_FALSE(clientType.getCommonSettingsTypeId().assigned()); +} + TEST_F(TmsFunctionBlockTest, MethodGetInputPorts) { const FunctionBlockTypePtr type = FunctionBlockType("UNIQUE ID", "NAME", "DESCRIPTION"); 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 index c45846cc..fd512374 100644 --- 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 @@ -1,6 +1,7 @@ #include #include "tms_object_integration_test.h" #include +#include #include #include #include @@ -114,6 +115,65 @@ TEST_F(TmsFunctionBlockTypeTest, ReadOnly) ASSERT_THROW(client->writeValue(scalingId, OpcUaVariant()), OpcUaException); } +TEST_F(TmsFunctionBlockTypeTest, Options) +{ + const FunctionBlockTypePtr fbType = FunctionBlockTypeBuilder() + .setId("RefFB") + .setName("Reference function block") + .setDescription("Description") + .setAlwaysEmptyInput(True) + .setSingleton(True) + .setCommonSettingsTypeId("SettingsId") + .build(); + + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + ASSERT_TRUE(clientFbType.getAlwaysEmptyInput()); + ASSERT_TRUE(clientFbType.getSingleton()); + ASSERT_EQ(clientFbType.getCommonSettingsTypeId(), "SettingsId"); + + ASSERT_TRUE(TestComparators::FunctionBlockTypeEquals(fbType, clientFbType)); +} + +TEST_F(TmsFunctionBlockTypeTest, OptionDefaults) +{ + auto fbType = createFunctionBlockType(); + + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + auto clientFbType = TmsClientFunctionBlockType(ctx, clientContext, nodeId); + + ASSERT_FALSE(clientFbType.getAlwaysEmptyInput()); + ASSERT_FALSE(clientFbType.getSingleton()); + ASSERT_FALSE(clientFbType.getCommonSettingsTypeId().assigned()); + + // An unassigned id publishes no node at all, which is what lets a client tell it apart from an + // empty string and what a server predating the options looks like. + auto browser = CachedReferenceBrowser(client); + ASSERT_FALSE(browser.hasReference(nodeId, "CommonSettingsTypeId")); +} + +TEST_F(TmsFunctionBlockTypeTest, OptionsReadOnly) +{ + const FunctionBlockTypePtr fbType = FunctionBlockTypeBuilder() + .setId("RefFB") + .setSingleton(True) + .setCommonSettingsTypeId("SettingsId") + .build(); + + auto serverFbType = std::make_shared(fbType, server, ctx, serverContext); + auto nodeId = serverFbType->registerOpcUaNode(); + + auto browser = CachedReferenceBrowser(client); + const auto singletonId = browser.getChildNodeId(nodeId, "Singleton"); + const auto commonSettingsId = browser.getChildNodeId(nodeId, "CommonSettingsTypeId"); + + ASSERT_THROW(client->writeValue(singletonId, OpcUaVariant(false)), OpcUaException); + ASSERT_THROW(client->writeValue(commonSettingsId, OpcUaVariant("other")), OpcUaException); +} + TEST_F(TmsFunctionBlockTypeTest, DISABLED_NonDefaultValues) { // This should work, but it doesnt, because of an error in client property object. From 2e5c11b3cf057817dce55f7326831880d19ef65c Mon Sep 17 00:00:00 2001 From: tomaz-cvetko Date: Thu, 24 Sep 2026 14:44:03 +0200 Subject: [PATCH 2/2] Update changelog --- changelog/changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog/changelog b/changelog/changelog index 3c21568e..6e722198 100644 --- a/changelog/changelog +++ b/changelog/changelog @@ -1,4 +1,6 @@ ## Features +- [#39](https://github.com/openDAQ/OpcUaModules/pull/39) Carry the function block type options `AlwaysEmptyInput`, `Singleton` and `CommonSettingsTypeId` over OPC UA. + - [#22](https://github.com/openDAQ/OpcUaModules/pull/22) Ignore calls to `updateOperationMode` on OPC UA client side. - [#20](https://github.com/openDAQ/OpcUaModules/pull/20) Support advertising servers via OpcUa with discovery toggling