convertersystems / opc-ua-client

Visualize and control your enterprise using OPC Unified Architecture (OPC UA) and Visual Studio.
MIT License
394 stars 113 forks source link

Derived types that do not subtype Structure (i=22) #177

Open quinmars opened 3 years ago

quinmars commented 3 years ago

This is the last point on my list of experiences during writing the UaTypeGenerator. Actually, this is not just one point but two very similar issues, where the first is more important, but the second one could help to solve the first one. The OPC UA type system supports to subtype non structural types, e.g., Double (i=11), DateTime (i=13) or ByteString (i=15). C# itself does not have a notion like this, but I think, it is comparable to C's typedefs. Most notably those subtypes do not have an encoding id on their own.

Let me give you two examples:

<UADataType NodeId="i=290" BrowseName="Duration">
  <DisplayName>Duration</DisplayName>
  <References>
    <Reference ReferenceType="HasSubtype" IsForward="false">i=11</Reference>
  </References>
</UADataType>

the Duration is part of the standard schema and the following is part of a Siemens TIA nodeset:

<UADataType NodeId="ns=3;i=3006" BrowseName="3:TIME">
    <DisplayName>TIME</DisplayName>
    <Description Locale="en-US">TIME is interpreted as milliseconds T#-24d20h31m23s648ms to T#+24d20h31m23s647ms. The representation contains information for days (d) hours (h) minutes (m) seconds (s) and milliseconds (ms).</Description>
    <References>
        <Reference ReferenceType="HasSubtype" IsForward="false">i=6</Reference>
    </References>
</UADataType>

This brings up some questions.

Why annotate this

It would be good, if there is a way for the type generator to get that information by means of reflection, i.e., without knowing the NodeSet2.xml file. At the moment I have hardcoded two data types IDs, that are commonly used. Theoretically I could add all standard type definitions, but this does not solve the situation where one companion specification uses an derived type of another.

What type should be used

While both types are similar to dotnet's TimeSpan, I would not use it for the first case (Duration), because a double has a different value range than a TimeSpan. In particular a double can denote values that cannot be represented by a TimeSpan. Hence I would not like to use TimeSpan here. So here one could use a plain double or a thin wrapper struct around a double. This wrapper could be autogenerated. A wrapper has the benefit, that it denotes the intention of the value in the types system. On the other hand I'm not a big fan of "typedefs" and quite thankful that they do not exist in C#. Personally I would use a simple double there. For the Siemens' TIME this looks completely different. There TimeSpan would be an appropriate data type. On the other hand an integer value that gives you the duration in milliseconds would also be ok.

Conclusion

I intentionally do not present a solution here, but I will post a possible solution later that is up to discussion.

awcullen commented 3 years ago

It seems that UAExpert decides to treat a variable with data type "TIME" as an Int32 for purposes of reading/writing.

image

image

Would 'using' be helpful with this?

using TIME = System.Int32;
quinmars commented 3 years ago

Sorry, for the long delay of the follow-up post. My initial idea was to add the parent-child mapping as an attribute to a converter class or struct. But as I mention in my initial post this is not a really important case (supporting conversion). And it would only work for the structural access and not for the direct access, because there we do not have the data type ID. On the other hand it was the only idea I had, how we can put those information into attributes. In the meantime, I found another way. We could simply have an dictionary that maps the (expanded) node ids of the derived data types to the (expanded) node id of the parent. We would probably need some attribute, to mark those dictionaries.

[ParentDataTypeIds]
public IReadOnlyDictionary<ExpandedNodeId,ExpandedNodeId> SomeName { get; } = ...

The TypeLibrary can than expose a method to collect and merge all available dictonaries. One question is if we would include there all parents or only non-structural types. Because for structural types one can already query the inheritance by the net type system.