Null DataType in PropertyState of predefined nodes generated via model compiler | OPC UA Implementation: Stacks, Tools, and Samples | Forum

Avatar
Search
Forum Scope


Match



Forum Options



Minimum search word length is 3 characters - maximum search word length is 84 characters
Lost password?
sp_Feed sp_PrintTopic sp_TopicIcon
Null DataType in PropertyState of predefined nodes generated via model compiler
Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
1
04/05/2021 - 12:48
sp_Permalink sp_Print

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.

Avatar
Randy Armstrong
Admin
Forum Posts: 628
Member Since:
05/30/2017
sp_UserOfflineSmall Offline
2
04/06/2021 - 01:49
sp_Permalink sp_Print

The XML is valid.

The autogenerated code should have the correct value for the DataType Attribute, however, you have no default value specified so the Value will be null until it is set.

Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
3
04/06/2021 - 08:09
sp_Permalink sp_Print

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; }
Avatar
Randy Armstrong
Admin
Forum Posts: 628
Member Since:
05/30/2017
sp_UserOfflineSmall Offline
4
04/06/2021 - 12:37
sp_Permalink sp_Print

The code looks correct but you need to look at the CustomType2State class definition.

What  datatypes are assigned to the Property1 and Property2 members?

Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
5
04/06/2021 - 13:36
sp_Permalink sp_Print

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;
}

Avatar
Randy Armstrong
Admin
Forum Posts: 628
Member Since:
05/30/2017
sp_UserOfflineSmall Offline
6
04/06/2021 - 16:57
sp_Permalink sp_Print

OK the DataType is string as it should be.

You can write string values.

The default value is null.

Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
7
04/07/2021 - 09:48
sp_Permalink sp_Print

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.

Avatar
Randy Armstrong
Admin
Forum Posts: 628
Member Since:
05/30/2017
sp_UserOfflineSmall Offline
8
04/07/2021 - 14:50
sp_Permalink sp_Print

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.

Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
9
04/08/2021 - 08:36
sp_Permalink sp_Print

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?

Avatar
Randy Armstrong
Admin
Forum Posts: 628
Member Since:
05/30/2017
sp_UserOfflineSmall Offline
10
04/08/2021 - 12:41
sp_Permalink sp_Print sp_EditHistory

You need to use NuGet Restore instead of copying DLLs.

I pushed an update to the ModelCompiler.

Please try again.

Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
11
04/08/2021 - 16:30
sp_Permalink sp_Print

I'm having no problems browsing the Boiler nodes using a .uanodes built from the latest ModelCompiler code.

Avatar
Randy Armstrong
Admin
Forum Posts: 628
Member Since:
05/30/2017
sp_UserOfflineSmall Offline
12
04/08/2021 - 22:01
sp_Permalink sp_Print

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;

....
}

Avatar
Paul Fake
Member
Members
Forum Posts: 7
Member Since:
04/05/2021
sp_UserOfflineSmall Offline
13
04/09/2021 - 07:34
sp_Permalink
Awaiting Moderation

Forum Timezone: America/Phoenix
Most Users Ever Online: 202
Currently Online:
Guest(s) 7
Currently Browsing this Page:
1 Guest(s)
Top Posters:
Forum Stats:
Groups: 2
Forums: 9
Topics: 854
Posts: 2596