Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog/changelog
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <opcuatms_client/objects/tms_client_signal_factory.h>
#include <opcuatms_client/objects/tms_client_function_block_factory.h>
#include <opcuatms_client/objects/tms_client_input_port_factory.h>
#include <opcuatms_client/objects/tms_client_function_block_type_impl.h>
#include <opendaq/component_type_builder_factory.h>
#include <open62541/daqbsp_nodeids.h>


Expand Down Expand Up @@ -174,7 +176,24 @@ void TmsClientFunctionBlockBaseImpl<Impl>::readFbType()
{
auto infoNodeId = this->getNodeId("FunctionBlockInfo");
auto variant = this->clientContext->getAttributeReader()->getValue(infoNodeId, UA_ATTRIBUTEID_VALUE);
this->type = VariantConverter<IFunctionBlockType>::ToDaqObject(variant).detach();
const auto baseType = VariantConverter<IFunctionBlockType>::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 <typename Impl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,41 @@
#include <opcuatms/converters/struct_converter.h>
#include <opcuatms_client/objects/tms_client_property_object_factory.h>
#include <opcuatms/converters/property_object_conversion_utils.h>
#include <opcuatms/core_types_utils.h>

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<UA_Boolean>() ? True : False;

if (readOption("Singleton", value) && value.isBool())
options.singleton = value.readScalar<UA_Boolean>() ? True : False;

if (readOption("CommonSettingsTypeId", value) && value.isString())
options.commonSettingsTypeId = ConvertToDaqCoreString(value.readScalar<UA_String>());

return options;
}

TmsClientFunctionBlockTypeImpl::TmsClientFunctionBlockTypeImpl(const ContextPtr& context,
const TmsClientContextPtr& tmsContext,
const opcua::OpcUaNodeId& nodeId)
Expand Down Expand Up @@ -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<IFunctionBlockType>::ToDaqObject(value);
this->options = ReadFunctionBlockTypeOptions(clientContext, nodeId);

const auto defaultConfigId = getNodeId("DefaultConfig");
this->defaultConfig = TmsClientPropertyObject(daqContext, clientContext, defaultConfigId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ class TmsServerFunctionBlockType : public TmsServerVariable<FunctionBlockTypePtr
std::string getDescription() override;
bool checkPermission(const Permission permission, const UA_NodeId* const nodeId, const OpcUaSession* const sessionContext) override;

/*!
* @brief Adds the function block type option nodes under the given parent node.
*
* The options are not part of the FunctionBlockInfoStructure defined by the companion specification,
* so they are exposed as separate read-only properties. `CommonSettingsTypeId` is only added when the
* type has one assigned; a client that does not find a node falls back to the option's default.
*/
static void AddOptionNodes(const OpcUaServerPtr& server,
const OpcUaNodeId& parentNodeId,
const FunctionBlockTypePtr& type,
void* nodeContext);

protected:
OpcUaNodeId getTmsTypeId() override;
void addChildNodes() override;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <opendaq/channel_ptr.h>
#include <opcuatms_server/objects/tms_server_function_block.h>
#include <opcuatms_server/objects/tms_server_function_block_type.h>
#include <opcuatms/converters/variant_converter.h>
#include <open62541/statuscodes.h>
#include <open62541/daqbsp_nodeids.h>
Expand Down Expand Up @@ -81,6 +82,12 @@ void TmsServerFunctionBlock<T>::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();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <opcuatms_server/objects/tms_server_function_block_type.h>
#include <opcuatms/converters/variant_converter.h>
#include <opcuatms_server/objects/tms_server_property_object.h>
#include <opcuaserver/opcuaaddnodeparams.h>
#include <open62541/daqbsp_nodeids.h>

using namespace daq::opcua;
Expand Down Expand Up @@ -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<UA_VariableAttributes>& attr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <opendaq/function_block_type_ptr.h>
#include <opendaq/function_block_ptr.h>
#include <opendaq/function_block_type_factory.h>
#include <opendaq/component_type_builder_factory.h>
#include <opendaq/signal_factory.h>
#include <opendaq/input_port_factory.h>
#include <gtest/gtest.h>
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <gtest/gtest.h>
#include "tms_object_integration_test.h"
#include <opendaq/function_block_type_factory.h>
#include <opendaq/component_type_builder_factory.h>
#include <opcuatms_server/objects/tms_server_function_block_type.h>
#include <opcuatms_client/objects/tms_client_function_block_type_factory.h>
#include <coreobjects/property_factory.h>
Expand Down Expand Up @@ -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<TmsServerFunctionBlockType>(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<TmsServerFunctionBlockType>(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<TmsServerFunctionBlockType>(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.
Expand Down
Loading