Hello,
My software team is having an issue with the OPC UA server, in which our property nodes show a DataType of null. We are using a PredefinedNodes.uanodes file generated with the model compiler from the following XML:
<?xml version="1.0" encoding="utf-8"?> <ModelDesign xmlns:uax="https://opcfoundation.org/UA/2008/02/Types.xsd" xmlns:ua="https://opcfoundation.org/UA/" xmlns:COMPANY="https://company.com/UA/company/" TargetNamespace="https://company.com/UA/company/" TargetVersion="0.9.0" TargetXmlNamespace="https://company.com/UA/company/" xmlns="https://opcfoundation.org/UA/ModelDesign.xsd"> <Namespaces> <Namespace Name="company" Prefix="company" XmlNamespace="https://company.com/UA/company/2020/09/Types.xsd" XmlPrefix="company">https://company.com/UA/company/</Namespace> <Namespace Name="OpcUa" Prefix="Opc.Ua" InternalPrefix="Opc.Ua.Server" XmlNamespace="https://opcfoundation.org/UA/2008/02/Types.xsd" XmlPrefix="OpcUa" Version="1.03" PublicationDate="2013-12-02T00:00:00Z">https://opcfoundation.org/UA/</Namespace> </Namespaces> <ObjectType SymbolicName="COMPANY:CustomType1" BaseType="ua:BaseObjectType" SupportsEvents="true"> <Description>Type for service</Description> <Children> <Property SymbolicName="COMPANY:ThingDescription" ModellingRule="Mandatory" DataType="ua:String" ValueRank="Scalar"> <Description>Description of thing</Description> </Property> </Children> </ObjectType> <ObjectType SymbolicName="COMPANY:CustomType2" BaseType="ua:BaseObjectType" SupportsEvents="true"> <Description>Type for some other thing</Description> <Children> <Property SymbolicName="COMPANY:Property1" ModellingRule="Mandatory" DataType="ua:String" ValueRank="Scalar"> <Description>First property</Description> </Property> <Property SymbolicName="COMPANY:Property2" ModellingRule="Mandatory" DataType="ua:String" ValueRank="Scalar"> <Description>Second property</Description> </Property> </Children> </ObjectType> <Object SymbolicName="COMPANY:ThingSet" TypeDefinition="ua:BaseObjectType"> <Description>Contains all instances of things</Description> <Children> <Object SymbolicName="COMPANY:MYSERVICE.TYPE" TypeDefinition="COMPANY:CustomType1"> <BrowseName>MYSERVICE.TYPE</BrowseName> <Description>A special service, for me</Description> <Children> <Property SymbolicName="COMPANY:ThingDescription" DataType="ua:String" AccessLevel="Read"> <Description>Description of thing</Description> </Property> <Object SymbolicName="COMPANY:SetOfMoreThings" TypeDefinition="ua:BaseObjectType"> <Description>Contains a set of things that are of CustomType2</Description> <Children> <Object SymbolicName="COMPANY:MYCATEGORY" TypeDefinition="ua:BaseObjectType"> <Description>All things categorized under MYCATEGORY</Description> <Children> <Object SymbolicName="COMPANY:INSTANCE_NAME" TypeDefinition="COMPANY:CustomType2"> <BrowseName>CustomType2 Instance</BrowseName> <Description>Instance of CustomType2</Description> <Children> <Property SymbolicName="COMPANY:Property1" DataType="ua:String" AccessLevel="Read"> <Description>First property</Description> </Property> <Property SymbolicName="COMPANY:Property2" DataType="ua:String" AccessLevel="Read"> <Description>Second property</Description> </Property> </Children> </Object> </Children> </Object> </Children> </Object> </Children> </Object> </Children> <References> <Reference IsInverse="true"> <ReferenceType>ua:Organizes</ReferenceType> <TargetId>ua:ObjectsFolder</TargetId> </Reference> </References> </Object> </ModelDesign>
"Property1" and "Property2" above should have a string DataType, but it's null instead. I'm seeing the issue while using the ReferenceServer sample, modified to consume the .uanodes file. Does something look wrong in the XML?
I've done a lot of debugging in the OPC UA implementation source code, and I have a lot more to say about the behavior, but I want to keep things simple right now and focus on whether the XML is valid before I go deeper with it.
Ok, great. I'd like to move onto the next piece, then. Does our override of AddBehaviourToPredefinedNode look alright? Specifically the switch statement (this is the part that, when I debug into it, I start seeing some weird things that explain the null DataType...all of which I'll elaborate on after we investigate this override for possible issues).
protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context, NodeState predefinedNode) { if (predefinedNode is BaseInstanceState passiveNode) { NodeId typeId = passiveNode.TypeDefinitionId; if (!IsNodeIdInNamespace(typeId) || typeId.IdType != IdType.Numeric) { return predefinedNode; } switch ((uint)typeId.Identifier) { case ObjectTypes.CustomType2: return ReplaceNodeWithType(passiveNode, context, (p) => new Custom2State(p)); case ObjectTypes.CustomType1: return ReplaceNodeWithType(passiveNode, context, (p) => new Custom1State(p)); } } return predefinedNode; } protected BaseInstanceState ReplaceNodeWithType<T>(BaseInstanceState passiveNode, ISystemContext context, Func<NodeState, T> create) where T : BaseInstanceState { if (passiveNode is T) { return passiveNode; } T activeNode = create(passiveNode.Parent); activeNode.Create(context, passiveNode); if (passiveNode.Parent != null) { passiveNode.Parent.ReplaceChild(context, activeNode); } return activeNode; }
This is the generated Custom2State class...I don't see it explicitly assigning a DataType anywhere, although the PropertyState members are of type string:
[System.CodeDom.Compiler.GeneratedCodeAttribute("Opc.Ua.ModelCompiler", "1.0.0.0")]
public partial class Custom2State : BaseObjectState
{
public Custom2State(NodeState parent) : base(parent)
{
}protected override NodeId GetDefaultTypeDefinitionId(NamespaceTable namespaceUris)
{
return Opc.Ua.NodeId.Create(company.ObjectTypes.CustomType2, company.Namespaces.company, namespaceUris);
}protected override void Initialize(ISystemContext context)
{
Initialize(context, InitializationString);
InitializeOptionalChildren(context);
}protected override void Initialize(ISystemContext context, NodeState source)
{
InitializeOptionalChildren(context);
base.Initialize(context, source);
}protected override void InitializeOptionalChildren(ISystemContext context)
{
base.InitializeOptionalChildren(context);
}private const string InitializationString = "bigstring";
public PropertyState<string> Property1
{
get
{
return m_property1;
}set
{
if (!Object.ReferenceEquals(m_property1, value))
{
ChangeMasks |= NodeStateChangeMasks.Children;
}m_property1 = value;
}
}/// <remarks />
public PropertyState<string> Property2
{
get
{
return m_property2;
}set
{
if (!Object.ReferenceEquals(m_property2, value))
{
ChangeMasks |= NodeStateChangeMasks.Children;
}m_property2 = value;
}
}public override void GetChildren(
ISystemContext context,
IList<BaseInstanceState> children)
{
if (m_property1 != null)
{
children.Add(m_property1);
}if (m_property2 != null)
{
children.Add(m_property2);
}base.GetChildren(context, children);
}protected override BaseInstanceState FindChild(
ISystemContext context,
QualifiedName browseName,
bool createOrReplace,
BaseInstanceState replacement)
{
if (QualifiedName.IsNull(browseName))
{
return null;
}BaseInstanceState instance = null;
switch (browseName.Name)
{
case company.BrowseNames.Property1:
{
if (createOrReplace)
{
if (Property1 == null)
{
if (replacement == null)
{
Property1 = new PropertyState<string>(this);
}
else
{
Property1 = (PropertyState<string>)replacement;
}
}
}instance = Property1;
break;
}case company.BrowseNames.Property2:
{
if (createOrReplace)
{
if (Property2 == null)
{
if (replacement == null)
{
Property2 = new PropertyState<string>(this);
}
else
{
Property2 = (PropertyState<string>)replacement;
}
}
}instance = Property2;
break;
}
}
}private PropertyState<string> m_property1;
private PropertyState<string> m_property2;
}
Thank you for all of your replies so far :).
We haven't had any issue writing values. We can see that the value is set properly. It's the DataType property of the Custom2State object that gets set to null during AddBehaviourToPredefinedNodes. Here is what I've uncovered when debugging the OPC UA server source code:
In AddBehaviourToPredefinedNode, the CustomType1 node is processed first, and it is replaced with a Custom1State object. When this Custom1State object is initialized, all of its children (which include the CustomType2 instance and its properties) are initialized as well. Eventually, the Initialize method of the NodeState class is called on the CustomType2 instance, which at this point in execution is a BaseInstanceState object. That last bit becomes very important. Here is the code, for context (NodeState.cs, Initialize method):
for (int ii = 0; ii < children.Count; ii++)
{
BaseInstanceState sourceChild = children[ii];
BaseInstanceState child = CreateChild(context, sourceChild.BrowseName);if (child == null)
{
child = (BaseInstanceState)sourceChild.MemberwiseClone();
AddChild(child);
}child.Initialize(context, sourceChild);
}
When its children (Property1 and Property2) are created, CreateChild calls the FindChild method of NodeState (rather than the Custom2State override, since AddBehaviourToPredefinedNode has not been called on the current object yet). This FindChild method will return null:
protected virtual BaseInstanceState FindChild(
ISystemContext context,
QualifiedName browseName,
bool createOrReplace,
BaseInstanceState replacement)
{
if (QualifiedName.IsNull(browseName))
{
return null;
}if (m_children != null)
{
for (int ii = 0; ii < m_children.Count; ii++)
{
BaseInstanceState child = m_children[ii];if (browseName == child.BrowseName)
{
if (createOrReplace && replacement != null)
{
m_children[ii] = child = replacement;
}return child;
}
}
}if (createOrReplace)
{
if (replacement != null)
{
AddChild(replacement);
}
}return null;
}
So because the result of CreateChild is null, the child is created from a MemberwiseClone of the sourceChild object. The child is given a type of BaseInstanceState, and it does not have a DataType.
Later on, AddBehaviourToPredefinedNode is called for the Custom2State object (the CustomType2 instance) and it attempts to correct what was done previously. But it doesn't succeed at this. What happens is that NodeState::Initialize is called on the object that has now been replaced with a Custom2State object, and when its children are created, the sourceChild variable is a BaseInstanceState, and then when the new child's Initialize method (BaseVariableState::Initialize) is called, it skips the DataType initialization because the sourceChild is not also a BaseVariableState:
protected override void Initialize(ISystemContext context, NodeState source)
{
BaseVariableState instance = source as BaseVariableState;if (instance != null)
{
m_value = ExtractValueFromVariant(context, instance.m_value, false);
m_timestamp = instance.m_timestamp;
m_statusCode = instance.m_statusCode;
m_dataType = instance.m_dataType;
m_valueRank = instance.m_valueRank;
m_arrayDimensions = null;
m_accessLevel = instance.m_accessLevel;
m_userAccessLevel = instance.m_userAccessLevel;
m_minimumSamplingInterval = instance.m_minimumSamplingInterval;
m_historizing = instance.m_historizing;if (instance.m_arrayDimensions != null)
{
m_arrayDimensions = new ReadOnlyList<uint>(instance.m_arrayDimensions, true);
}m_value = ExtractValueFromVariant(context, m_value, false);
}base.Initialize(context, source);
}
I know that's extremely convoluted, but the short version is that due to the order in which nested objects are initialized/replaced, the children of Custom2State are the wrong type, and this prevents DataType from being set. Does it look to you like this is a bug? If the XML, our AddBehaviourToPredefinedNodes override, and the generated Custom2State class look correct, it seems like a bug is likely.

05/30/2017

The InitializationString contains the default Attribute values.
I suggest you start with a sample that works:
https://github.com/OPCFoundati.....Manager.cs
I ran it and it sets the DataType properly.
That said, that example uses code generated with an old version of the tool.
If you can show that the works out of the box but stops working if the files are rebuilt with:
https://github.com/OPCFoundati.....Design.bat
Then we are dealing with a bug in the code generator.
I'm having some trouble running that script, but I've looked at the Boiler example before and noted that 1) it worked and 2) it has nested custom types. The difference between the Boiler example and my own, however, is that the BoilerNodeManager does not override AddBehaviourToPredefinedNodes. So while it may have a non-null DataType on the nested custom node, it's not actually hitting the code path that *would* cause a null DataType.
I didn't see any examples that both 1) have nested custom types and 2) override AddBehaviourToPredefinedNodes.
I'm still willing to try building Boiler with the newer model compiler, for the sake of covering our bases. The script doesn't run out-of-the-box because it's missing the model compiler EXE. I've built the model compiler (v104) separately and copied the EXE to that folder, but then it started complaining about a missing DLL. So I copied all of the DLLs from the model compiler's output folder and tried again...but then it didn't like the specific version of System.ServiceModel.Primitives, and that's where I gave up. Is there some process I'm missing here?

05/30/2017

BoilerNodeManager does not override AddBehaviourToPredefinedNodes.
I don't understand why you say this: https://github.com/OPCFoundati.....Manager.cs
protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context, NodeState predefinedNode)
{BaseObjectState passiveNode = predefinedNode as BaseObjectState;
if (passiveNode == null)
{
return predefinedNode;....
}
1 Guest(s)