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;....
}
This is embarrassing, but I've been looking at a different BoilerNodeManager this whole time. There appear to be two of them in the source code. I've been running the one in the Quickstarts solution.
Sorry about that. I'm going to try using the other one now and I'll report back with what I find.
I was able to get the error to go away by changing the XML. It took a lot of experimentation, but this is what works:
<?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.....ce>
<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/&l.....ce>
</Namespaces>
<ObjectType SymbolicName="COMPANY:CarMakerType" BaseType="ua:BaseObjectType" SupportsEvents="true">
<Description>The maker of the cars</Description>
<Children>
<Property SymbolicName="COMPANY:CarMakerDescription" ModellingRule="Mandatory" DataType="ua:String" ValueRank="Scalar">
<Description>Description of car maker</Description>
</Property>
<Object SymbolicName="COMPANY:CarTypes" TypeDefinition="COMPANY:CarTypeSet">
<Description>Contains all cars made by the maker organized by category</Description>
<Children>
<Object SymbolicName="COMPANY:CarCategory">
<BrowseName>Car category</BrowseName>
<Description>Contains all cars of the selected category</Description>
<Children>
<Object SymbolicName="COMPANY:Car">
<Description>Car model type</Description>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
</ObjectType>
<ObjectType SymbolicName="COMPANY:CarTypeSet" TypeDefinition="ua:BaseOjectType">
<Description>Contains all cars made by the maker organized by category</Description>
<Children>
<Object SymbolicName="COMPANY:CarCategory" TypeDefinition="COMPANY:CarCategoryType">
<BrowseName>Car category</BrowseName>
<Description>Contains all cars of the selected category</Description>
<Children>
<Object SymbolicName="COMPANY:Car">
<Description>Car model type</Description>
</Object>
</Children>
</Object>
</Children>
</ObjectType>
<ObjectType SymbolicName="COMPANY:CarCategoryType" BaseType="ua:BaseObjectType">
<Description>Contains all cars of the selected category</Description>
<Children>
<Object SymbolicName="COMPANY:Car" TypeDefinition="COMPANY:CarType" SupportsEvents="true">
<Description>Car model type</Description>
</Object>
</Children>
</ObjectType>
<ObjectType SymbolicName="COMPANY:CarType" BaseType="ua:BaseObjectType" SupportsEvents="true">
<Description>Car model type</Description>
<Children>
<Property SymbolicName="COMPANY:EngineSize" ModellingRule="Mandatory" DataType="ua:String" ValueRank="Scalar">
<Description>Engine size</Description>
</Property>
<Property SymbolicName="COMPANY:SafetyFeatures" ModellingRule="Mandatory" DataType="ua:String" ValueRank="Scalar">
<Description>List of safety features</Description>
</Property>
</Children>
</ObjectType>
<Object SymbolicName="COMPANY:CarMakerSet" TypeDefinition="ua:BaseObjectType">
<Description>Contains all instances of car makers</Description>
<Children>
<Object SymbolicName="COMPANY:HONDA" TypeDefinition="COMPANY:CarMakerType">
<BrowseName>HONDA</BrowseName>
<Description>A car maker</Description>
<Children>
<Property SymbolicName="COMPANY:CarMakerDescription" DataType="ua:String" AccessLevel="Read">
<Description>Description of car maker</Description>
</Property>
<Object SymbolicName="COMPANY:CarTypes">
<Description>Contains all cars made by the maker organized by category</Description>
<Children>
<Object SymbolicName="COMPANY:CarCategory">
<DisplayName>SEDAN</DisplayName>
<Description>Contains all cars of the selected category</Description>
<Children>
<Object SymbolicName="COMPANY:Car">
<DisplayName>Accord</DisplayName>
<Description>Accord</Description>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
<References>
<Reference IsInverse="true">
<ReferenceType>ua:Organizes</ReferenceType>
<TargetId>ua:ObjectsFolder</TargetId>
</Reference>
</References>
</Object></ModelDesign>
The problem was that in the original XML, I did not have the correct hierarchy in the type definitions. It seems like it's necessary to mimic the instance hierarchy in the type definitions. (I know that the original XML that I shared is pretty different in its content...both of these are simplifications and obfuscations of our actual metadata, and I thought that cars would be easier to discuss than generic "custom types.")
Now I have a new problem. The above XML seems to work perfectly fine; I can browse the nodes and they appear as I would expect them to, and I see no errors about a null DataType. But, in my object instances I actually want to support multiple car types and categories. For example, I currently have an Accord, and I'd like to also include a Civic. Or, I have a SEDAN category, and I'd like to also have an SUV category. I can't find a way to add these extra objects to the instance hierarchy. I've tried this:
<Object SymbolicName="COMPANY:CarMakerSet" TypeDefinition="ua:BaseObjectType">
<Description>Contains all instances of car makers</Description>
<Children>
<Object SymbolicName="COMPANY:HONDA" TypeDefinition="COMPANY:CarMakerType">
<BrowseName>HONDA</BrowseName>
<Description>A car maker</Description>
<Children>
<Property SymbolicName="COMPANY:CarMakerDescription" DataType="ua:String" AccessLevel="Read">
<Description>Description of car maker</Description>
</Property>
<Object SymbolicName="COMPANY:CarTypes">
<Description>Contains all cars made by the maker organized by category</Description>
<Children>
<Object SymbolicName="COMPANY:CarCategory">
<DisplayName>SEDAN</DisplayName>
<Description>Contains all cars of the selected category</Description>
<Children>
<Object SymbolicName="COMPANY:Car">
<DisplayName>Accord</DisplayName>
<Description>Accord</Description>
</Object>
<Object SymbolicName="COMPANY:Car">
<DisplayName>Civic</DisplayName>
<Description>Civic</Description>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
<References>
<Reference IsInverse="true">
<ReferenceType>ua:Organizes</ReferenceType>
<TargetId>ua:ObjectsFolder</TargetId>
</Reference>
</References>
</Object>
Which gives me a model compiler error because I am re-using a symbolic name in the same hierarchy. Ok, so I thought maybe it's associating the Car object with the CarType type based on its position in the hierarchy, and I actually need to give it a unique symbolic name and explicitly set the type, like this:
<Object SymbolicName="COMPANY:CarMakerSet" TypeDefinition="ua:BaseObjectType">
<Description>Contains all instances of car makers</Description>
<Children>
<Object SymbolicName="COMPANY:HONDA" TypeDefinition="COMPANY:CarMakerType">
<BrowseName>HONDA</BrowseName>
<Description>A car maker</Description>
<Children>
<Property SymbolicName="COMPANY:CarMakerDescription" DataType="ua:String" AccessLevel="Read">
<Description>Description of car maker</Description>
</Property>
<Object SymbolicName="COMPANY:CarTypes">
<Description>Contains all cars made by the maker organized by category</Description>
<Children>
<Object SymbolicName="COMPANY:CarCategory">
<DisplayName>SEDAN</DisplayName>
<Description>Contains all cars of the selected category</Description>
<Children>
<Object SymbolicName="COMPANY:ACCORD" TypeDefinition="COMPANY:CarType">
<DisplayName>Accord</DisplayName>
<Description>Accord</Description>
</Object>
<Object SymbolicName="COMPANY:CIVIC" TypeDefinition="COMPANY:CarType">
<DisplayName>Civic</DisplayName>
<Description>Civic</Description>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
</Object>
</Children>
<References>
<Reference IsInverse="true">
<ReferenceType>ua:Organizes</ReferenceType>
<TargetId>ua:ObjectsFolder</TargetId>
</Reference>
</References>
</Object>
That compiled fine, but the nodes are all wrong. When I browse, what I see under the SEDAN category is actually three entries: Car (I guess it gets this one from the type hierarchy?), ACCORD, and CIVIC. In this case, both ACCORD and CIVIC will give me the null DataType error when I try to browse the properties of either.
How should the XML look if I want to include a variable number of object instances of the same type inside a type hierarchy?
1 Guest(s)