Skip to content
Merged
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
95 changes: 72 additions & 23 deletions NodeSetToAML.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3160,35 +3160,82 @@ private void AddStructureFieldDefinition( AttributeFamilyType attribute, UANode


// Now fill the data
AddModifyAttribute( structureFieldAttribute.Attribute,
"Name", "String", new Variant( field.Name ) );
AddModifyAttribute( structureFieldAttribute.Attribute,
"ValueRank", "Int32", new Variant( field.ValueRank ) );
AddModifyAttribute( structureFieldAttribute.Attribute,
"IsOptional", "Boolean", new Variant( field.IsOptional) );

SetArrayDimensions( structureFieldAttribute.Attribute, field.ArrayDimensions );
RemoveUnwantedAttribute(structureFieldAttribute.Attribute["ArrayDimensions"], "StructureFieldDefinition");
if ( string.IsNullOrEmpty(field.ArrayDimensions))
RemoveUnwantedAttribute(structureFieldAttribute, "Name");

if ( field.ValueRank == ValueRanks.Scalar ||
field.ValueRank >= ValueRanks.OneDimension )
{
AddModifyAttribute(structureFieldAttribute.Attribute,
"ValueRank", "Int32", new Variant(field.ValueRank));
}
else
{
RemoveUnwantedAttribute(structureFieldAttribute.Attribute["ArrayDimensions"], "UInt32");
RemoveUnwantedAttribute( structureFieldAttribute, "ValueRank" );
}

AddModifyAttribute( structureFieldAttribute.Attribute,
"AllowSubtypes", "Boolean", new Variant( field.AllowSubTypes ) );
AddModifyAttribute( structureFieldAttribute.Attribute,
"MaxStringLength", "UInt32", new Variant( field.MaxStringLength ) );
if ( field.IsOptional )
{
AddModifyAttribute(structureFieldAttribute.Attribute,
"IsOptional", "Boolean", new Variant(true));
}
else
{
RemoveUnwantedAttribute(structureFieldAttribute, "IsOptional");
}

if( field.Description != null && field.Description.Length > 0 )
if ( field.ValueRank >= ValueRanks.OneDimension &&
!string.IsNullOrEmpty( field.ArrayDimensions ) )
{
SetArrayDimensions(structureFieldAttribute.Attribute, field.ArrayDimensions);
RemoveUnwantedAttribute(structureFieldAttribute.Attribute["ArrayDimensions"],
"StructureFieldDefinition");
}
else
{
RemoveUnwantedAttribute(structureFieldAttribute, "ArrayDimensions");
}


// Max String Length is only for strings and bytestrings
// This seems to be a point for discussion.
// Do we put max string length in if it zero?
if ( field.MaxStringLength > 0 &&
m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.String ) ||
m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.ByteString ) )
Comment on lines +3203 to +3204
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parentheses around the OR condition. The logical AND operator has higher precedence than OR, causing incorrect evaluation. The condition should be: if ( field.MaxStringLength > 0 && (m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.String ) || m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.ByteString )) )

Suggested change
m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.String ) ||
m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.ByteString ) )
(m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.String ) ||
m_modelManager.IsTypeOf( field.DecodedDataType, Opc.Ua.DataTypeIds.ByteString )) )

Copilot uses AI. Check for mistakes.
{
LocalizedText localizedText = new LocalizedText(
field.Description[0].Locale, field.Description[ 0 ].Value );
AddModifyAttribute( structureFieldAttribute.Attribute,
"Description", "LocalizedText", new Variant( localizedText ) );
"MaxStringLength", "UInt32", new Variant( field.MaxStringLength ) );
}
else if ( structureFieldAttribute.Attribute[ "MaxStringLength" ] != null )
{
RemoveUnwantedAttribute( structureFieldAttribute, "MaxStringLength" );
}

if (field.Description != null && field.Description.Length > 0)
{
List<Variant> localizedTextList = new List<Variant>(field.Description.Length);
foreach(NodeSet.LocalizedText description in field.Description)
{
localizedTextList.Add(
new Variant(
new LocalizedText(description.Locale, description.Value)));
}
Variant localizedTextArray = new Variant(localizedTextList);

LocalizedText localizedText = new LocalizedText(
field.Description[0].Locale, field.Description[0].Value);
AddModifyAttribute(structureFieldAttribute.Attribute,
"Description", "LocalizedText", localizedTextArray,
bListOf: true);
RemoveUnwantedAttribute(structureFieldAttribute.Attribute["Description"],
"StructureFieldDefinition");
}
else if (structureFieldAttribute.Attribute["Description"] != null)
{
RemoveUnwantedAttribute(structureFieldAttribute, "Description");
}

RemoveUnwantedAttribute(structureFieldAttribute.Attribute["Description"],
"StructureFieldDefinition");


// Remove the NodeId from the structure Field
AttributeType nodeIdAttribute = structureFieldAttribute.Attribute[ "DataType" ];
Expand All @@ -3200,8 +3247,10 @@ private void AddStructureFieldDefinition( AttributeFamilyType attribute, UANode
RemoveUnwantedNodeIdAttribute(structureFieldAttribute);
RemoveNodeIdsFromDefinition(structureFieldAttribute);


fieldDefinitionAttribute.Attribute.Insert( structureFieldAttribute );
if (structureFieldAttribute.Attribute.Count > 0)
{
fieldDefinitionAttribute.Attribute.Insert(structureFieldAttribute);
}
Comment on lines +3250 to +3253
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The condition checking if attributes exist before insertion could be more explicit. Consider adding a comment explaining why empty attribute collections should be skipped, as this logic relates directly to the PR's goal of removing unnecessary information.

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down
33 changes: 13 additions & 20 deletions SystemTest/NodeSetFiles/Modified.Opc.Ua.NodeSet2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9723,26 +9723,19 @@
<Definition Name="CartesianCoordinates" />
</UADataType>
<UADataType NodeId="i=18810" BrowseName="3DCartesianCoordinates" SymbolicName="ThreeDCartesianCoordinates">
<DisplayName>3DCartesianCoordinates</DisplayName>
<Category>Base Info Spatial Data</Category>
<Documentation>https://reference.opcfoundation.org/v105/Core/docs/Part5/12.26</Documentation>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=18809</Reference>
</References>
<Definition Name="3DCartesianCoordinates" SymbolicName="ThreeDCartesianCoordinates">
<Field Name="X" DataType="i=11" >
<Description>No Locale</Description>
</Field>
<Field Name="Y" DataType="i=11" >
<Description Locale="en">With Locale</Description>
</Field>
<Field Name="Z" DataType="i=11" >
<Description>Multiple Descriptions</Description>
<Description Locale="en">Multiple Descriptions with Locale</Description>
</Field>
</Definition>
</UADataType>
<UADataType NodeId="i=18811" BrowseName="Orientation" IsAbstract="true">
<DisplayName>3DCartesianCoordinates</DisplayName>
<Category>Base Info Spatial Data</Category>
<Documentation>https://reference.opcfoundation.org/v105/Core/docs/Part5/12.26</Documentation>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false" BrowseName="CartesianCoordinates">i=18809</Reference>
</References>
<Definition Name="3DCartesianCoordinates" SymbolicName="ThreeDCartesianCoordinates">
<Field Name="X" DataType="i=11" />
<Field Name="Y" DataType="i=11" />
<Field Name="Z" DataType="i=11" />
</Definition>
</UADataType>
<UADataType NodeId="i=18811" BrowseName="Orientation" IsAbstract="true">
<DisplayName>Orientation</DisplayName>
<Category>Base Info Spatial Data</Category>
<Documentation>https://reference.opcfoundation.org/v105/Core/docs/Part5/12.27</Documentation>
Expand Down
27 changes: 25 additions & 2 deletions SystemTest/NodeSetFiles/TestAml.xml
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,37 @@
<Definition Name="1:PublisherQosDataType">
<Field Name="QosCategory" DataType="String" IsOptional="true" MaxStringLength="123">
<Description Locale="en">Quality of Service Category</Description>
<Description Locale="fr">Catégorie de qualité de service</Description>
<Description>Kategorie „Dienstqualität“</Description>
</Field>
<Field Name="DatagramQos" DataType="TransmitQosDataType" ValueRank="2" ArrayDimensions="2,3" AllowSubTypes="true">
<Description>Transmit Quality of Service</Description>
</Field>
<Field Name="NoDescription" DataType="String" IsOptional="false" MaxStringLength="321"/>
<Field Name="NoDescription" DataType="String" IsOptional="false" ValueRank="-2" MaxStringLength="321"/>
<Field Name="PracticallyEmpty" DataType="i=12" ValueRank="-2"/>
</Definition>
</UADataType>

<UADataType NodeId="ns=1;i=3007" BrowseName="3DCartesianCoordinates" SymbolicName="ThreeDCartesianCoordinates">
<DisplayName>3DCartesianCoordinates</DisplayName>
<Category>Base Info Spatial Data</Category>
<Documentation>https://reference.opcfoundation.org/v105/Core/docs/Part5/12.26</Documentation>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=18809</Reference>
</References>
<Definition Name="3DCartesianCoordinates" SymbolicName="ThreeDCartesianCoordinates">
<Field Name="X" DataType="i=11" >
<Description>No Locale</Description>
</Field>
<Field Name="Y" DataType="i=11" >
<Description Locale="en">With Locale</Description>
</Field>
<Field Name="Z" DataType="i=11" >
<Description>Multiple Descriptions</Description>
<Description Locale="en">Multiple Descriptions with Locale</Description>
</Field>
</Definition>
</UADataType>

<UAObject SymbolicName="http___opcfoundation_org_UA_FX_AML_TESTING" NodeId="ns=1;i=5000" BrowseName="1:http://opcfoundation.org/UA/FX/AML/TESTING" ParentNodeId="i=11715">
<DisplayName>http://opcfoundation.org/UA/FX/AML/TESTING</DisplayName>
Expand Down Expand Up @@ -3563,7 +3586,7 @@
<!-- Next Numbers
ObjectType 1009
VariableType 2006
DataType 3007
DataType 3008
Object 5025
Variable 6238
-->
Expand Down
118 changes: 58 additions & 60 deletions SystemTest/TestStructureFieldDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using Aml.Engine.CAEX;
using Aml.Engine.CAEX.Extensions;
using System.Linq;
using System;
using Opc.Ua;

namespace SystemTest
{
Expand All @@ -26,7 +23,7 @@ public void TestUnwantedAttributes(TestHelper.Uris uriId, uint nodeId)

foreach( AttributeType attribute in objectToTest.Attribute )
{
if ( attribute.Name != "NodeId")
if ( attribute.Name != "NodeId" && attribute.Name != "PracticallyEmpty" )
{
AttributeType structureAttribute = GetAttribute(attribute, "StructureFieldDefinition");
foreach(AttributeType definitionAttribute in structureAttribute.Attribute)
Expand All @@ -50,81 +47,82 @@ public void TestUnwantedAttributes(TestHelper.Uris uriId, uint nodeId)
}

[TestMethod, Timeout(TestHelper.UnitTestTimeout)]
[DataRow("QosCategory","Name", "QosCategory")]
[DataRow("QosCategory", "Description", "Quality of Service Category")]
[DataRow("QosCategory", "ValueRank", "-1")]
[DataRow("QosCategory", "ArrayDimensions", null)]
[DataRow("QosCategory", "MaxStringLength", "123")]
[DataRow("QosCategory", "IsOptional", "true")]
[DataRow("QosCategory", "AllowSubtypes", "false")]

[DataRow("DatagramQos", "Name", "DatagramQos")]
[DataRow("DatagramQos", "Description", "Transmit Quality of Service")]
[DataRow("DatagramQos", "ValueRank", "2")]
[DataRow("DatagramQos", "MaxStringLength", "0")]
[DataRow("DatagramQos", "IsOptional", "false")]
[DataRow("DatagramQos", "AllowSubtypes", "true")]

[DataRow("NoDescription", "Name", "NoDescription")]
[DataRow("NoDescription", "Description", null)]
[DataRow("NoDescription", "ValueRank", "-1")]
[DataRow("NoDescription", "ArrayDimensions", null)]
[DataRow("NoDescription", "MaxStringLength", "321")]
[DataRow("NoDescription", "IsOptional", "false")]
[DataRow("NoDescription", "AllowSubtypes", "false")]
[DataRow("QosCategory","Name", null, "", "")]
[DataRow("QosCategory", "Description", "Quality of Service Category", "0", "en")]
[DataRow("QosCategory", "Description", "Catégorie de qualité de service", "1", "fr")]
[DataRow("QosCategory", "Description", "Kategorie „Dienstqualität“", "2", "")]
[DataRow("QosCategory", "ValueRank", "-1", "", "")]
[DataRow("QosCategory", "ArrayDimensions", null, "", "")]
[DataRow("QosCategory", "MaxStringLength", "123", "", "")]
[DataRow("QosCategory", "IsOptional", "true", "", "")]

[DataRow("DatagramQos", "Name", null, "", "")]
[DataRow("DatagramQos", "Description", "Transmit Quality of Service", "0", "")]
[DataRow("DatagramQos", "ArrayDimensions", "2", "0", "")]
[DataRow("DatagramQos", "ArrayDimensions", "3", "1", "")]
[DataRow("DatagramQos", "ValueRank", "2", "", "")]
[DataRow("DatagramQos", "IsOptional", null, "", "")]

[DataRow("NoDescription", "Name", null, "", "")]
[DataRow("NoDescription", "Description", null, "", "")]
[DataRow("NoDescription", "ValueRank", null, "", "")]
[DataRow("NoDescription", "ArrayDimensions", null, "", "")]
[DataRow("NoDescription", "MaxStringLength", "321", "", "")]
[DataRow("NoDescription", "IsOptional", null, "", "")]

public void TestAttributeValues(string variableName,
string attributeName,
string expectedValue)
{
AttributeValues(variableName, attributeName, expectedValue);
}

[TestMethod, Timeout(TestHelper.UnitTestTimeout)]
public void TestDescriptionLocale()
{
AttributeValues("QosCategory", "Description", "Quality of Service Category", "en");
}

[TestMethod, Timeout(TestHelper.UnitTestTimeout)]
public void TestArrayDimensions()
{
AttributeType structured = GetStructured(TestHelper.Uris.Test,
PublisherQosDataType, "DatagramQos");
AttributeType attribute = GetAttribute(structured, "ArrayDimensions");
AttributeType first = GetAttribute(attribute, "0");
Assert.AreEqual("2", first.Value, "Unexpected value for ArrayDimensions[0].");
AttributeType second = GetAttribute(attribute, "1");
Assert.AreEqual("3", second.Value, "Unexpected value for ArrayDimensions[1].");
}

public void AttributeValues(string variableName,
string attributeName,
string expectedValue,
string localeId = "")
string arrayIndex,
string localeId)
{
AttributeFamilyType objectToTest = GetTestAttribute(TestHelper.Uris.Test,
PublisherQosDataType);

AttributeType variableAttribute = GetAttribute(objectToTest.Attribute, variableName);
AttributeType structured = GetAttribute(variableAttribute, "StructureFieldDefinition");
AttributeType attribute = GetAttribute(structured.Attribute, attributeName);
Assert.AreEqual(expectedValue, attribute.Value,
$"Unexpected value for {variableName}.{attributeName} in {structured.Name}.");

if (!string.IsNullOrEmpty(localeId))
if (string.IsNullOrEmpty(expectedValue))
{
// attributeName should not exist
Assert.IsNull(structured.Attribute[attributeName],
$"Attribute {attributeName} exists in {variableName} when it should not.");
}
else
{
AttributeType locale = GetAttribute(attribute.Attribute, localeId);
Assert.AreEqual(expectedValue, locale.Value,
$"Unexpected locale value for {variableName}.{attributeName} in {structured.Name}.");
AttributeType attribute = GetAttribute(structured.Attribute, attributeName);

if (!string.IsNullOrEmpty(arrayIndex))
{
attribute = GetAttribute(attribute, arrayIndex);
}

Assert.AreEqual(expectedValue, attribute.Value,
$"Unexpected value for {variableName}.{attributeName} in {structured.Name}.");

if (!string.IsNullOrEmpty(localeId))
{
AttributeType locale = GetAttribute(attribute.Attribute, localeId);
Assert.AreEqual(expectedValue, locale.Value,
$"Unexpected locale value for {variableName}.{attributeName} in {structured.Name}.");
}
}
}


[TestMethod, Timeout(TestHelper.UnitTestTimeout)]
public void TestUnwantedStructureAttribute()
{
AttributeFamilyType objectToTest = GetTestAttribute(TestHelper.Uris.Test, PublisherQosDataType);
AttributeType emptyAttribute = GetAttribute(objectToTest.Attribute, "PracticallyEmpty");
Assert.IsNull(emptyAttribute.Attribute["StructureFieldDefinition"],
"Unexpected StructureFieldDefinition found in PracticallyEmpty");
}


#endregion

#region Helpers
#region Helpers

private CAEXDocument GetDocument()
{
Expand Down