diff --git a/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs b/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs index 54d8b47d2..8755c9a2e 100644 --- a/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs +++ b/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs @@ -46,7 +46,7 @@ public static MeasurementModel Create(MTConnectMeasurementModel importModel) public string RenderModel() { - var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "Assets.Measurement.scriban"); + var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "Pallets.Measurement.scriban"); return template.Render(this); } diff --git a/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs b/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs index 6345c9a94..48e462cfc 100644 --- a/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs +++ b/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs @@ -92,7 +92,14 @@ public static void Render(MTConnectModel mtconnectModel, string outputPath) else if (typeof(MTConnectCompositionType).IsAssignableFrom(type)) template = CompositionType.Create((MTConnectCompositionType)exportModel); else if (typeof(MTConnectComponentType).IsAssignableFrom(type)) { - if (((MTConnectComponentType)exportModel).Type == "Controllers") ((MTConnectComponentType)exportModel).MinimumVersion = new Version(1, 0); + // Surface the SysML-declared introduction year + // for every Component, including the Controllers + // organizer AssociationClass (introduced='2.0'). + // The earlier override that hard-coded Controllers + // to v1.0 contradicted the spec — the + // `Profile:normative introduced='2.0'` record + // on the Controllers UML AssociationClass is the + // authoritative source. template = ComponentType.Create((MTConnectComponentType)exportModel); } else if (typeof(MTConnectMeasurementModel).IsAssignableFrom(type)) @@ -101,15 +108,15 @@ public static void Render(MTConnectModel mtconnectModel, string outputPath) { template = CuttingToolMeasurementModel.Create((MTConnectMeasurementModel)exportModel); } - else + else if (exportModel.Id?.StartsWith("Assets.Pallet.") == true) { - // Non-CuttingTools measurement (e.g. Assets.Pallet.*) — no fallback - // template exists yet, so log and continue rather than silently - // dropping the model. - Console.Error.WriteLine( - $"warn: MeasurementModel '{exportModel.Id}' has no template — " + - "only Assets.CuttingTools.* is currently rendered. Skipping."); + template = MeasurementModel.Create((MTConnectMeasurementModel)exportModel); } + // No fallback: every measurement in the v2.x SysML routes + // through one of the two prefixes above. A future model + // adding a third measurement package will surface here as a + // null template (NullReferenceException downstream) — preferable + // to a silent drop with a stderr warning that nothing watches. } else if (typeof(MTConnectClassModel).IsAssignableFrom(type) && exportModel.Id?.EndsWith("Result") == true) { @@ -193,6 +200,16 @@ public static void Render(MTConnectModel mtconnectModel, string outputPath) ((ClassModel)template).IsPartial = true; ((ClassModel)template).IsAbstract = false; break; + case "Assets.Pallet.Measurement": + // Partial + concrete so a hand-written + // partial can supply the `Type` property + // and the `Measurement(IMeasurement)` ctor + // that the per-subtype rich template + // (`Pallets.Measurement.scriban`) chains + // to via `: base(measurement)`. + ((ClassModel)template).IsPartial = true; + ((ClassModel)template).IsAbstract = false; + break; } templates.Add(template); diff --git a/build/MTConnect.NET-SysML-Import/CSharp/Templates/Interfaces.InterfaceDataItemType.scriban b/build/MTConnect.NET-SysML-Import/CSharp/Templates/Interfaces.InterfaceDataItemType.scriban index acc4d4648..475688a72 100644 --- a/build/MTConnect.NET-SysML-Import/CSharp/Templates/Interfaces.InterfaceDataItemType.scriban +++ b/build/MTConnect.NET-SysML-Import/CSharp/Templates/Interfaces.InterfaceDataItemType.scriban @@ -13,14 +13,27 @@ namespace MTConnect.Interfaces public const DataItemCategory CategoryId = DataItemCategory.{{category}}; public const string TypeId = "{{type}}"; public const string NameId = "{{default_name}}"; - {{ if (units_enum) }}public const string DefaultUnits = {{units_enum}};{{ end }} + {{ if (units_enum) }}public const string DefaultUnits = {{units_enum}};{{ end }} public new const string DescriptionText = "{{description}}"; - + public override string TypeDescription => DescriptionText; {{ if (maximum_version_enum) }}public override System.Version MaximumVersion => {{maximum_version_enum}};{{ end }} - {{ if (minimum_version_enum) }}public override System.Version MinimumVersion => {{minimum_version_enum}};{{ end }} + {{ if (minimum_version_enum) }}public override System.Version MinimumVersion => {{minimum_version_enum}};{{ end }} +{{ if ((sub_types | array.size) > 0) }}{{ i = 0 }} + public new enum SubTypes + { +{{- for sub_type in sub_types }}{{ i = i + 1 }} + /// + /// {{sub_type.description}} + /// + {{sub_type.name}} + {{- if (i < (sub_types | array.size)) }}, + {{ end }} +{{- end }} + } +{{ end }} public {{name}}() { Category = CategoryId; @@ -28,6 +41,54 @@ namespace MTConnect.Interfaces {{ if (units_enum) }}Units = DefaultUnits;{{ end }} } +{{- if ((sub_types | array.size) > 0) }} + + public {{name}}( + string parentId, + SubTypes subType + ) + { + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); + Category = CategoryId; + Type = TypeId; + SubType = subType.ToString(); + Name = NameId; + {{ if (units_enum) }}Units = DefaultUnits;{{ end }} + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + +{{- if ((sub_types | array.size) > 0) }}{{ i = 0 }} + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { +{{- for sub_type in sub_types }}{{ i = i + 1 }} + case SubTypes.{{sub_type.name}}: return "{{sub_type.description}}"; +{{- end }} + } + + return null; + } +{{- end }} +{{- if ((sub_types | array.size) > 0) }}{{ i = 0 }} + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { +{{- for sub_type in sub_types }}{{ i = i + 1 }} + case SubTypes.{{sub_type.name}}: return "{{sub_type.name}}"; +{{- end }} + } + + return null; + } +{{- end }} +{{ else }} + public {{name}}(string deviceId) { Id = CreateId(deviceId, NameId); @@ -35,5 +96,6 @@ namespace MTConnect.Interfaces Type = TypeId; Name = NameId; } +{{- end }} } -} \ No newline at end of file +} diff --git a/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.Measurement.scriban b/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.Measurement.scriban index 5cba268cf..c92505e04 100644 --- a/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.Measurement.scriban +++ b/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.Measurement.scriban @@ -1,12 +1,12 @@ // Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -namespace MTConnect.Assets.Pallet.Measurements +namespace {{namespace}} { /// /// {{description}} /// - public class {{name}} : Measurement + public class {{name}} : Measurement, I{{name}} { public const string TypeId = "{{type_id}}"; public const string CodeId = "{{code_id}}"; @@ -15,14 +15,12 @@ namespace MTConnect.Assets.Pallet.Measurements public {{name}}() { Type = TypeId; - Code = CodeId; {{ if (units_enum) }}Units = {{units_enum}};{{ end }} } public {{name}}(double value) { Type = TypeId; - Code = CodeId; Value = value; {{ if (units_enum) }}Units = {{units_enum}};{{ end }} } @@ -30,7 +28,6 @@ namespace MTConnect.Assets.Pallet.Measurements public {{name}}(IMeasurement measurement) : base(measurement) { Type = TypeId; - Code = CodeId; {{ if (units_enum) }}Units = {{units_enum}};{{ end }} } } diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs index 7e455f4c6..c928ceee5 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs @@ -1171,11 +1171,6 @@ private Device NormalizeDevice(IDevice device) assetcount.Device = obj; assetcount.Container = obj; assetcount.Name = AssetCountDataItem.NameId; - // ASSET_COUNT is a DATA_SET representation per MTConnect Part 2 - // (UML _19_0_3_68e0225_1640602520420_217627_44). The generated - // AssetCountDataItem still defaults Representation to VALUE; override - // it here so the auto-injected DataItem matches the spec. - assetcount.Representation = DataItemRepresentation.DATA_SET; dataItemList.Add(assetcount); dataItemTypes.Add(AssetCountDataItem.TypeId); } diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/HeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/HeightMeasurement.g.cs index c95fd01eb..5370fef58 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/HeightMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/HeightMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795946018_306044_24580 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class HeightMeasurement : Measurement, IHeightMeasurement { public new const string DescriptionText = "Height of the PhysicalAsset"; + public const string TypeId = "Height"; + public const string CodeId = ""; + + + public HeightMeasurement() + { + Type = TypeId; + + } + public HeightMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public HeightMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/IMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/IMeasurement.g.cs index e4a72758f..37b3be996 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/IMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/IMeasurement.g.cs @@ -6,7 +6,7 @@ namespace MTConnect.Assets.Pallet /// /// Constrained scalar value associated with an Asset /// - public interface IMeasurement + public partial interface IMeasurement { /// /// Maximum value for the measurement. diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/LengthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/LengthMeasurement.g.cs index 1533e5d98..92cac40b2 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/LengthMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/LengthMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795955654_438848_24650 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class LengthMeasurement : Measurement, ILengthMeasurement { public new const string DescriptionText = "Length of the PhysicalAsset"; + public const string TypeId = "Length"; + public const string CodeId = ""; + + + public LengthMeasurement() + { + Type = TypeId; + + } + public LengthMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public LengthMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedHeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedHeightMeasurement.g.cs index 36881c6d8..68ca5111b 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedHeightMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedHeightMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795985316_21813_24755 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class LoadedHeightMeasurement : Measurement, ILoadedHeightMeasurement { public new const string DescriptionText = "Loaded height of the PhysicalAsset"; + public const string TypeId = "LoadedHeight"; + public const string CodeId = ""; + + + public LoadedHeightMeasurement() + { + Type = TypeId; + + } + public LoadedHeightMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public LoadedHeightMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedLengthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedLengthMeasurement.g.cs index 707ced57d..5aff580bf 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedLengthMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedLengthMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727796000098_310953_24825 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class LoadedLengthMeasurement : Measurement, ILoadedLengthMeasurement { public new const string DescriptionText = "Loaded length of the PhysicalAsset"; + public const string TypeId = "LoadedLength"; + public const string CodeId = ""; + + + public LoadedLengthMeasurement() + { + Type = TypeId; + + } + public LoadedLengthMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public LoadedLengthMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedSwingMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedSwingMeasurement.g.cs index c8a18638e..c270fb137 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedSwingMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedSwingMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727796007725_733990_24860 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class LoadedSwingMeasurement : Measurement, ILoadedSwingMeasurement { public new const string DescriptionText = "Loaded swing of the PhysicalAsset"; + public const string TypeId = "LoadedSwing"; + public const string CodeId = ""; + + + public LoadedSwingMeasurement() + { + Type = TypeId; + + } + public LoadedSwingMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public LoadedSwingMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWeightMeasurement.g.cs index 497ac2482..db15ecc9e 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWeightMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWeightMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795966065_979190_24720 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class LoadedWeightMeasurement : Measurement, ILoadedWeightMeasurement { public new const string DescriptionText = "Loaded weight of the PhysicalAsset"; + public const string TypeId = "LoadedWeight"; + public const string CodeId = ""; + + + public LoadedWeightMeasurement() + { + Type = TypeId; + + } + public LoadedWeightMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public LoadedWeightMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWidthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWidthMeasurement.g.cs index 1b0c3ab78..c82b3f08e 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWidthMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/LoadedWidthMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795992160_973071_24790 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class LoadedWidthMeasurement : Measurement, ILoadedWidthMeasurement { public new const string DescriptionText = "Loaded width of the PhysicalAsset"; + public const string TypeId = "LoadedWidth"; + public const string CodeId = ""; + + + public LoadedWidthMeasurement() + { + Type = TypeId; + + } + public LoadedWidthMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public LoadedWidthMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.cs new file mode 100644 index 000000000..c1d8e71c9 --- /dev/null +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +namespace MTConnect.Assets.Pallet +{ + public partial class Measurement + { + public string Type { get; set; } + + + public Measurement() { } + + public Measurement(IMeasurement measurement) + { + if (measurement != null) + { + Value = measurement.Value; + Nominal = measurement.Nominal; + Minimum = measurement.Minimum; + Maximum = measurement.Maximum; + SignificantDigits = measurement.SignificantDigits; + NativeUnits = measurement.NativeUnits; + Units = measurement.Units; + } + } + } +} diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs index d8f146c42..2b8f5ee24 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs @@ -8,7 +8,7 @@ namespace MTConnect.Assets.Pallet /// /// Constrained scalar value associated with an Asset /// - public abstract class Measurement : IMeasurement + public partial class Measurement : IMeasurement { public const string DescriptionText = "Constrained scalar value associated with an Asset"; diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/SwingMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/SwingMeasurement.g.cs index dcee10d6c..3c7fc0f64 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/SwingMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/SwingMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795961263_1895_24685 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class SwingMeasurement : Measurement, ISwingMeasurement { public new const string DescriptionText = "Swing of the PhysicalAsset"; + public const string TypeId = "Swing"; + public const string CodeId = ""; + + + public SwingMeasurement() + { + Type = TypeId; + + } + public SwingMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public SwingMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/WeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/WeightMeasurement.g.cs index 80f3c8356..06b8b0954 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/WeightMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/WeightMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795939487_321518_24545 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class WeightMeasurement : Measurement, IWeightMeasurement { public new const string DescriptionText = "Weight of the PhysicalAsset"; + public const string TypeId = "Weight"; + public const string CodeId = ""; + + + public WeightMeasurement() + { + Type = TypeId; + + } + public WeightMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public WeightMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/WidthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/WidthMeasurement.g.cs index 580aa1d27..e00685d10 100644 --- a/libraries/MTConnect.NET-Common/Assets/Pallet/WidthMeasurement.g.cs +++ b/libraries/MTConnect.NET-Common/Assets/Pallet/WidthMeasurement.g.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2024 TrakHound Inc., All Rights Reserved. +// Copyright (c) 2025 TrakHound Inc., All Rights Reserved. // TrakHound Inc. licenses this file to you under the MIT license. -// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727795950849_939647_24615 - namespace MTConnect.Assets.Pallet { /// @@ -11,7 +9,27 @@ namespace MTConnect.Assets.Pallet public class WidthMeasurement : Measurement, IWidthMeasurement { public new const string DescriptionText = "Width of the PhysicalAsset"; + public const string TypeId = "Width"; + public const string CodeId = ""; + + + public WidthMeasurement() + { + Type = TypeId; + + } + public WidthMeasurement(double value) + { + Type = TypeId; + Value = value; + + } + public WidthMeasurement(IMeasurement measurement) : base(measurement) + { + Type = TypeId; + + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-Common/Devices/Components/ControllersComponent.g.cs b/libraries/MTConnect.NET-Common/Devices/Components/ControllersComponent.g.cs index c0607d3c1..f9e00cb57 100644 --- a/libraries/MTConnect.NET-Common/Devices/Components/ControllersComponent.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/Components/ControllersComponent.g.cs @@ -16,7 +16,7 @@ public class ControllersComponent : Component, IOrganizerComponent public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version10; + public override System.Version MinimumVersion => MTConnectVersions.Version20; public ControllersComponent() diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitDataItem.g.cs index d5257779f..f2a7eba51 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitDataItem.g.cs @@ -13,7 +13,7 @@ public class AlarmLimitDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "ALARM_LIMIT"; public const string NameId = "alarmLimit"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Set of limits used to trigger warning or alarm indicators.**DEPRECATED** in *Version 2.5*. Replaced by `ALARM_LIMITS`."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitsDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitsDataItem.g.cs index b2c468028..24c189edd 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitsDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/AlarmLimitsDataItem.g.cs @@ -13,7 +13,7 @@ public class AlarmLimitsDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "ALARM_LIMITS"; public const string NameId = "alarmLimits"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Set of limits used to trigger warning or alarm indicators."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/AssetCountDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/AssetCountDataItem.g.cs index 44ba100df..bfa4fca6e 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/AssetCountDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/AssetCountDataItem.g.cs @@ -13,7 +13,7 @@ public class AssetCountDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "ASSET_COUNT"; public const string NameId = "assetCount"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.VALUE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Data set of the number of Asset of a given type for a Device."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitDataItem.g.cs index 59e10706a..ae610f873 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitDataItem.g.cs @@ -13,7 +13,7 @@ public class ControlLimitDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "CONTROL_LIMIT"; public const string NameId = "controlLimit"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Set of limits used to indicate whether a process variable is stable and in control.**DEPRECATED** in *Version 2.5*. Replaced by `CONTROL_LIMITS`."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitsDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitsDataItem.g.cs index 3215018b8..c370c018c 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitsDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/ControlLimitsDataItem.g.cs @@ -13,7 +13,7 @@ public class ControlLimitsDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "CONTROL_LIMITS"; public const string NameId = "controlLimits"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Set of limits used to indicate whether a process variable is stable and in control."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/LocationAddressDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/LocationAddressDataItem.g.cs index 3c164808a..a5a842c04 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/LocationAddressDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/LocationAddressDataItem.g.cs @@ -13,7 +13,7 @@ public class LocationAddressDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "LOCATION_ADDRESS"; public const string NameId = "locationAddress"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Structured information that allows the unambiguous determination of an object for purposes of identification and location. ISO 19160-4:2017"; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/LocationSpatialGeographicDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/LocationSpatialGeographicDataItem.g.cs index 41d91493d..b5c81fc6e 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/LocationSpatialGeographicDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/LocationSpatialGeographicDataItem.g.cs @@ -13,7 +13,7 @@ public class LocationSpatialGeographicDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "LOCATION_SPATIAL_GEOGRAPHIC"; public const string NameId = "locationSpatialGeographic"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Absolute geographic location defined by two coordinates, longitude and latitude and an elevation."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/SensorAttachmentDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/SensorAttachmentDataItem.g.cs index b7ed5e378..e55193931 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/SensorAttachmentDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/SensorAttachmentDataItem.g.cs @@ -13,7 +13,7 @@ public class SensorAttachmentDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "SENSOR_ATTACHMENT"; public const string NameId = "sensorAttachment"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Attachment between a sensor and an entity."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitDataItem.g.cs index 254d376d8..a588fc5bb 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitDataItem.g.cs @@ -13,7 +13,7 @@ public class SpecificationLimitDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "SPECIFICATION_LIMIT"; public const string NameId = "specificationLimit"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Set of limits defining a range of values designating acceptable performance for a variable.**DEPRECATED** in *Version 2.5*. Replaced by `SPECIFICATION_LIMITS`."; diff --git a/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitsDataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitsDataItem.g.cs index ef2e8c215..91d2286ac 100644 --- a/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitsDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Devices/DataItems/SpecificationLimitsDataItem.g.cs @@ -13,7 +13,7 @@ public class SpecificationLimitsDataItem : DataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "SPECIFICATION_LIMITS"; public const string NameId = "specificationLimits"; - public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.TABLE; + public const DataItemRepresentation DefaultRepresentation = DataItemRepresentation.DATA_SET; public new const string DescriptionText = "Set of limits defining a range of values designating acceptable performance for a variable."; diff --git a/libraries/MTConnect.NET-Common/Interfaces/CloseChuckDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/CloseChuckDataItem.g.cs index 127c4c6a5..2d3d27329 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/CloseChuckDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/CloseChuckDataItem.g.cs @@ -13,12 +13,26 @@ public class CloseChuckDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "CLOSE_CHUCK"; public const string NameId = "closeChuck"; - - public new const string DescriptionText = "Operating state of the service to close a chuck."; + public new const string DescriptionText = "Operating state of the service to close a chuck."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to close a chuck. + /// + REQUEST, + + /// + /// Operating state of the response to a request to close a chuck. + /// + RESPONSE + } public CloseChuckDataItem() @@ -28,12 +42,43 @@ public CloseChuckDataItem() } - public CloseChuckDataItem(string deviceId) + public CloseChuckDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to close a chuck."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to close a chuck."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/CloseDoorDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/CloseDoorDataItem.g.cs index 224dd1337..720bc6fd7 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/CloseDoorDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/CloseDoorDataItem.g.cs @@ -13,12 +13,26 @@ public class CloseDoorDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "CLOSE_DOOR"; public const string NameId = "closeDoor"; - - public new const string DescriptionText = "Operating state of the service to close a door."; + public new const string DescriptionText = "Operating state of the service to close a door."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to close a door. + /// + REQUEST, + + /// + /// Operating state of the response to a request to close a door. + /// + RESPONSE + } public CloseDoorDataItem() @@ -28,12 +42,43 @@ public CloseDoorDataItem() } - public CloseDoorDataItem(string deviceId) + public CloseDoorDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to close a door."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to close a door."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/MaterialChangeDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/MaterialChangeDataItem.g.cs index 1637aca70..7d44a00aa 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/MaterialChangeDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/MaterialChangeDataItem.g.cs @@ -13,12 +13,26 @@ public class MaterialChangeDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "MATERIAL_CHANGE"; public const string NameId = "materialChange"; - - public new const string DescriptionText = "Operating state of the service to change the type of material or product being loaded or fed to a piece of equipment."; + public new const string DescriptionText = "Operating state of the service to change the type of material or product being loaded or fed to a piece of equipment."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to change the type of material or product being loaded or fed to a piece of equipment. + /// + REQUEST, + + /// + /// Operating state of the response to a request to change the type of material or product being loaded or fed to a piece of equipment. + /// + RESPONSE + } public MaterialChangeDataItem() @@ -28,12 +42,43 @@ public MaterialChangeDataItem() } - public MaterialChangeDataItem(string deviceId) + public MaterialChangeDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to change the type of material or product being loaded or fed to a piece of equipment."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to change the type of material or product being loaded or fed to a piece of equipment."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/MaterialFeedDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/MaterialFeedDataItem.g.cs index cbc54c42c..82808028b 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/MaterialFeedDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/MaterialFeedDataItem.g.cs @@ -13,12 +13,26 @@ public class MaterialFeedDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "MATERIAL_FEED"; public const string NameId = "materialFeed"; - - public new const string DescriptionText = "Operating state of the service to advance material or feed product to a piece of equipment from a continuous or bulk source."; + public new const string DescriptionText = "Operating state of the service to advance material or feed product to a piece of equipment from a continuous or bulk source."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to advance material or feed product to a piece of equipment from a continuous or bulk source. + /// + REQUEST, + + /// + /// Operating state of the response to a request to advance material or feed product to a piece of equipment from a continuous or bulk source. + /// + RESPONSE + } public MaterialFeedDataItem() @@ -28,12 +42,43 @@ public MaterialFeedDataItem() } - public MaterialFeedDataItem(string deviceId) + public MaterialFeedDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to advance material or feed product to a piece of equipment from a continuous or bulk source."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to advance material or feed product to a piece of equipment from a continuous or bulk source."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/MaterialLoadDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/MaterialLoadDataItem.g.cs index 640244915..0603498b1 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/MaterialLoadDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/MaterialLoadDataItem.g.cs @@ -13,12 +13,26 @@ public class MaterialLoadDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "MATERIAL_LOAD"; public const string NameId = "materialLoad"; - - public new const string DescriptionText = "Operating state of the service to load a piece of material or product."; + public new const string DescriptionText = "Operating state of the service to load a piece of material or product."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to load a piece of material or product. + /// + REQUEST, + + /// + /// Operating state of the response to a request to load a piece of material or product. + /// + RESPONSE + } public MaterialLoadDataItem() @@ -28,12 +42,43 @@ public MaterialLoadDataItem() } - public MaterialLoadDataItem(string deviceId) + public MaterialLoadDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to load a piece of material or product."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to load a piece of material or product."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/MaterialRetractDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/MaterialRetractDataItem.g.cs index 0ba275bd6..fc3f5699d 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/MaterialRetractDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/MaterialRetractDataItem.g.cs @@ -13,12 +13,26 @@ public class MaterialRetractDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "MATERIAL_RETRACT"; public const string NameId = "materialRetract"; - - public new const string DescriptionText = "Operating state of the service to remove or retract material or product."; + public new const string DescriptionText = "Operating state of the service to remove or retract material or product."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to remove or retract material or product. + /// + REQUEST, + + /// + /// Operating state of the response to a request to remove or retract material or product. + /// + RESPONSE + } public MaterialRetractDataItem() @@ -28,12 +42,43 @@ public MaterialRetractDataItem() } - public MaterialRetractDataItem(string deviceId) + public MaterialRetractDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to remove or retract material or product."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to remove or retract material or product."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/MaterialUnloadDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/MaterialUnloadDataItem.g.cs index 1d667f9f7..69e0b9367 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/MaterialUnloadDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/MaterialUnloadDataItem.g.cs @@ -13,12 +13,26 @@ public class MaterialUnloadDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "MATERIAL_UNLOAD"; public const string NameId = "materialUnload"; - - public new const string DescriptionText = "Operating state of the service to unload a piece of material or product."; + public new const string DescriptionText = "Operating state of the service to unload a piece of material or product."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to unload a piece of material or product. + /// + REQUEST, + + /// + /// Operating state of the response to a request to unload a piece of material or product. + /// + RESPONSE + } public MaterialUnloadDataItem() @@ -28,12 +42,43 @@ public MaterialUnloadDataItem() } - public MaterialUnloadDataItem(string deviceId) + public MaterialUnloadDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to unload a piece of material or product."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to unload a piece of material or product."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/OpenChuckDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/OpenChuckDataItem.g.cs index ae06c54e8..7527290f5 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/OpenChuckDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/OpenChuckDataItem.g.cs @@ -13,12 +13,26 @@ public class OpenChuckDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "OPEN_CHUCK"; public const string NameId = "openChuck"; - - public new const string DescriptionText = "Operating state of the service to open a chuck."; + public new const string DescriptionText = "Operating state of the service to open a chuck."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to open a chuck. + /// + REQUEST, + + /// + /// Operating state of the response to a request to open a chuck. + /// + RESPONSE + } public OpenChuckDataItem() @@ -28,12 +42,43 @@ public OpenChuckDataItem() } - public OpenChuckDataItem(string deviceId) + public OpenChuckDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to open a chuck."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to open a chuck."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/OpenDoorDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/OpenDoorDataItem.g.cs index 373cc5900..2103ddbe4 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/OpenDoorDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/OpenDoorDataItem.g.cs @@ -13,12 +13,26 @@ public class OpenDoorDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "OPEN_DOOR"; public const string NameId = "openDoor"; - - public new const string DescriptionText = "Operating state of the service to open a door."; + public new const string DescriptionText = "Operating state of the service to open a door."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to open a door. + /// + REQUEST, + + /// + /// Operating state of the response to a request to open a door. + /// + RESPONSE + } public OpenDoorDataItem() @@ -28,12 +42,43 @@ public OpenDoorDataItem() } - public OpenDoorDataItem(string deviceId) + public OpenDoorDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to open a door."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to open a door."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-Common/Interfaces/PartChangeDataItem.g.cs b/libraries/MTConnect.NET-Common/Interfaces/PartChangeDataItem.g.cs index 48da79dbf..5eddfd8e7 100644 --- a/libraries/MTConnect.NET-Common/Interfaces/PartChangeDataItem.g.cs +++ b/libraries/MTConnect.NET-Common/Interfaces/PartChangeDataItem.g.cs @@ -13,12 +13,26 @@ public class PartChangeDataItem : InterfaceDataItem public const DataItemCategory CategoryId = DataItemCategory.EVENT; public const string TypeId = "PART_CHANGE"; public const string NameId = "partChange"; - - public new const string DescriptionText = "Operating state of the service to change the part or product associated with a piece of equipment to a different part or product."; + public new const string DescriptionText = "Operating state of the service to change the part or product associated with a piece of equipment to a different part or product."; + public override string TypeDescription => DescriptionText; - public override System.Version MinimumVersion => MTConnectVersions.Version13; + public override System.Version MinimumVersion => MTConnectVersions.Version13; + + + public new enum SubTypes + { + /// + /// Operating state of the request to change the part or product associated with a piece of equipment to a different part or product. + /// + REQUEST, + + /// + /// Operating state of the response to a request to change the part or product associated with a piece of equipment to a different part or product. + /// + RESPONSE + } public PartChangeDataItem() @@ -28,12 +42,43 @@ public PartChangeDataItem() } - public PartChangeDataItem(string deviceId) + public PartChangeDataItem( + string parentId, + SubTypes subType + ) { - Id = CreateId(deviceId, NameId); + Id = CreateId(parentId, NameId, GetSubTypeId(subType)); Category = CategoryId; Type = TypeId; + SubType = subType.ToString(); Name = NameId; + } + + public override string SubTypeDescription => GetSubTypeDescription(SubType); + + public new static string GetSubTypeDescription(string subType) + { + var s = subType.ConvertEnum(); + switch (s) + { + case SubTypes.REQUEST: return "Operating state of the request to change the part or product associated with a piece of equipment to a different part or product."; + case SubTypes.RESPONSE: return "Operating state of the response to a request to change the part or product associated with a piece of equipment to a different part or product."; + } + + return null; + } + + public new static string GetSubTypeId(SubTypes subType) + { + switch (subType) + { + case SubTypes.REQUEST: return "REQUEST"; + case SubTypes.RESPONSE: return "RESPONSE"; + } + + return null; + } + } -} \ No newline at end of file +} diff --git a/libraries/MTConnect.NET-SysML/Models/Assets/MTConnectAssetInformationModel.cs b/libraries/MTConnect.NET-SysML/Models/Assets/MTConnectAssetInformationModel.cs index 37e1cdb58..6c3469afe 100644 --- a/libraries/MTConnect.NET-SysML/Models/Assets/MTConnectAssetInformationModel.cs +++ b/libraries/MTConnect.NET-SysML/Models/Assets/MTConnectAssetInformationModel.cs @@ -196,24 +196,42 @@ private void ParsePallets(XmiDocument xmiDocument, UmlPackage umlPackage) // Pallets.Classes.AddRange(assetClasses); //} - // Add Measurement Classes + // Add Measurement Classes via the rich measurement pipeline + // (mirrors the CuttingTool path at line 119) so each Pallet + // measurement subclass renders with TypeId / CodeId / + // three-ctor scaffolding rather than the bare ClassModel + // shape. Filter out the abstract `Measurement` class itself + // (MTConnectMeasurementModel auto-suffixes "Measurement", + // which would otherwise produce `MeasurementMeasurement`). var packages = new List(); packages.Add(targetPackage.Packages.FirstOrDefault(o => o.Name == "Measurements")); - var umlClasses = ModelHelper.GetClasses(packages); - var measurementClasses = MTConnectClassModel.Parse(xmiDocument, "Assets.Pallet", umlClasses); + var allMeasurementClasses = ModelHelper.GetClasses(packages); + var concreteMeasurementClasses = allMeasurementClasses + ?.Where(c => c.Name != "Measurement"); + var measurementClasses = MTConnectMeasurementModel.Parse( + xmiDocument, "PhysicalAsset", "Assets.Pallet", concreteMeasurementClasses); if (measurementClasses != null) { - foreach (var measurementClass in measurementClasses) + Pallets.Classes.AddRange(measurementClasses); + } + + // Also parse the abstract `Measurement` base class itself via + // the regular class pipeline so it can be regenerated as a + // partial class (see TemplateRenderer override). The concrete + // subclasses' `Measurement(IMeasurement)` ctor needs the base + // partial to provide the copy-from-IMeasurement ctor + the + // `Type` property the rich template emits. + var abstractMeasurement = allMeasurementClasses + ?.Where(c => c.Name == "Measurement"); + if (abstractMeasurement != null) + { + var baseMeasurementClasses = MTConnectClassModel.Parse( + xmiDocument, "Assets.Pallet", abstractMeasurement); + if (baseMeasurementClasses != null) { - if (measurementClass.Id != "Assets.Pallet.Measurement") - { - measurementClass.Id = $"{measurementClass.Id}Measurement"; - measurementClass.Name = $"{measurementClass.Name}Measurement"; - } + Pallets.Classes.AddRange(baseMeasurementClasses); } - - Pallets.Classes.AddRange(measurementClasses); } diff --git a/libraries/MTConnect.NET-SysML/Models/Devices/MTConnectDataItemType.cs b/libraries/MTConnect.NET-SysML/Models/Devices/MTConnectDataItemType.cs index 95cee0433..c68704859 100644 --- a/libraries/MTConnect.NET-SysML/Models/Devices/MTConnectDataItemType.cs +++ b/libraries/MTConnect.NET-SysML/Models/Devices/MTConnectDataItemType.cs @@ -62,7 +62,7 @@ public MTConnectDataItemType(XmiDocument xmiDocument, string category, string id Type = umlEnumerationLiteral.Name; //Type = umlClass.Name.ToUnderscoreUpper(); - var description = umlEnumerationLiteral.Comments?.FirstOrDefault().Body; + var description = umlEnumerationLiteral.Comments?.FirstOrDefault()?.Body; Description = ModelHelper.ProcessDescription(description); MaximumVersion = MTConnectVersion.LookupDeprecated(xmiDocument, umlClass.Id); @@ -78,6 +78,22 @@ public MTConnectDataItemType(XmiDocument xmiDocument, string category, string id //if (ParentName != null && ParentName != "DataItem") ParentName += "DataItem"; } + // The MTConnect SysML model declares a DataItem's + // representation in two complementary places: + // * the typing of its `result` property (a DataType + // for VALUE; a Class with key/value sub-properties + // for the structured forms), and + // * the prose marker that the matching EventEnum + // literal carries on its description, of the + // form `{{term(data set)}}` / `{{term(table)}}` + // / `{{term(time series)}}` at the start of the + // comment body. + // Read the prose marker first so DataItems whose + // structured representation is encoded only in the + // enum-literal description (ASSET_COUNT being the + // canonical example) inherit the correct default. + var representationFromEnum = GetRepresentationFromEnumLiteral(umlEnumerationLiteral); + if (umlClass.Properties != null) { foreach (var property in umlClass.Properties) @@ -95,7 +111,17 @@ public MTConnectDataItemType(XmiDocument xmiDocument, string category, string id // Result if (property.Name == "result") { - // Get Class (TABLE OR DATA_SET) + // Get Class (structured result). The MTConnect + // SysML model encodes the canonical structured + // representation in the parent chain of the + // result class — `DataSet` for one-dimensional + // key/value rows, `Table` for two-dimensional + // key/value matrices, `TimeSeries` for sample + // sequences. Walk the chain to pick the right + // representation; fall back to TABLE so result + // classes whose generalization terminates in a + // template-binding (e.g. WORK_OFFSETS, + // TOOL_OFFSETS) keep their existing default. Result = ModelHelper.GetClassName(xmiDocument, property.PropertyType); if (Result == null) @@ -106,7 +132,7 @@ public MTConnectDataItemType(XmiDocument xmiDocument, string category, string id } else { - Representation = "TABLE"; // Should probably take into account DATA_SET as well? + Representation = ResolveStructuredRepresentation(xmiDocument, property.PropertyType); } } @@ -126,6 +152,15 @@ public MTConnectDataItemType(XmiDocument xmiDocument, string category, string id } } + // Enum-literal prose wins over the result-class fallback: + // the SysML XMI uses it as the authoritative marker for the + // canonical representation of types whose result property + // points at a primitive DataType. + if (representationFromEnum != null) + { + Representation = representationFromEnum; + } + if (subClasses != null) { var subTypes = new List(); @@ -183,6 +218,62 @@ private static string ConvertClassName(string name) return null; } + // Walk the generalization chain of a result class to pick the + // structured representation it encodes. The MTConnect SysML + // model defines three abstract result-shape parents: + // `DataSet`, `Table`, and `TimeSeries`; concrete result classes + // (e.g. AlarmLimitResult, FeatureMeasurementResult) generalize + // from one of them. A class with no chain match falls back to + // TABLE so result classes whose generalization terminates in a + // template-binding (e.g. WORK_OFFSETS, TOOL_OFFSETS) keep their + // existing default. + // + // The walk is bounded by a visited-set rather than a depth cap + // because a malformed XMI cycle would otherwise loop forever; + // the visited-set both detects cycles and short-circuits a + // diamond-inheritance graph that visits the same parent twice. + private static string ResolveStructuredRepresentation(XmiDocument xmiDocument, string resultClassId) + { + var visited = new HashSet(); + var current = ModelHelper.GetClass(xmiDocument, resultClassId); + while (current != null && visited.Add(current.Id)) + { + switch (current.Name) + { + case "DataSet": return "DATA_SET"; + case "Table": return "TABLE"; + case "TimeSeries": return "TIME_SERIES"; + } + + var parentId = current.Generalizations?.FirstOrDefault()?.General; + if (string.IsNullOrEmpty(parentId)) break; + current = ModelHelper.GetClass(xmiDocument, parentId); + } + + return "TABLE"; + } + + // The MTConnect SysML model embeds the canonical representation + // of an EventEnum literal in its description. The marker is one + // of `{{term(data set)}}`, `{{term(table)}}`, or + // `{{term(time series)}}` and appears at the start of the + // comment body. When present, it overrides the result-property + // typing fallback so DataItems whose `result` references a + // primitive DataType (e.g. ASSET_COUNT pointing at `integer`) + // still inherit the structured representation the spec mandates. + private static string GetRepresentationFromEnumLiteral(UmlEnumerationLiteral umlEnumerationLiteral) + { + var body = umlEnumerationLiteral?.Comments?.FirstOrDefault()?.Body; + if (string.IsNullOrEmpty(body)) return null; + + var trimmed = body.TrimStart(); + if (trimmed.StartsWith("{{term(data set)}}")) return "DATA_SET"; + if (trimmed.StartsWith("{{term(table)}}")) return "TABLE"; + if (trimmed.StartsWith("{{term(time series)}}")) return "TIME_SERIES"; + + return null; + } + private static string ConvertEnumName(string name) { if (name != null) diff --git a/tests/MTConnect.NET-Common-Tests/Agents/AssetCountAutoGeneratedDataSetTests.cs b/tests/MTConnect.NET-Common-Tests/Agents/AssetCountAutoGeneratedDataSetTests.cs new file mode 100644 index 000000000..25c11b8b9 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Agents/AssetCountAutoGeneratedDataSetTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System.Linq; +using MTConnect.Agents; +using MTConnect.Devices; +using MTConnect.Devices.DataItems; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Agents +{ + /// + /// Asserts that the auto-injected ASSET_COUNT DataItem carries the + /// MTConnect-mandated DATA_SET representation. + /// + /// MTConnect Standard, Part 2 — Devices Information Model, defines + /// ASSET_COUNT (UML element ID + /// _19_0_3_68e0225_1640602520420_217627_44, also captured in + /// AssetCountDataItem.g.cs) as a DATA_SET representation: a map of + /// asset-type to the count of assets of that type currently held in + /// the agent's asset buffer. The agent auto-injects an + /// AssetCountDataItem when normalising a device, and that injection + /// must produce a DataItem whose Representation is DATA_SET so the + /// resulting Probe matches the spec. + /// + [TestFixture] + [Category("AssetCountIsDataSet")] + public class AssetCountAutoGeneratedDataSetTests + { + [Test] + public void AutoInjected_AssetCount_Has_DataSet_Representation() + { + using var agent = new MTConnectAgent(initializeAgentDevice: false); + + var device = new Device + { + Id = "dev", + Name = "dev", + Uuid = "dev-uuid" + }; + + var added = agent.AddDevice(device, initializeDataItems: false); + + Assert.That(added, Is.Not.Null); + + var assetCount = added.DataItems + .SingleOrDefault(d => d.Type == AssetCountDataItem.TypeId); + + Assert.That(assetCount, Is.Not.Null, + "auto-generator must inject one ASSET_COUNT DataItem"); + Assert.That(assetCount!.Representation, + Is.EqualTo(DataItemRepresentation.DATA_SET), + "auto-injected ASSET_COUNT must carry DATA_SET representation per MTConnect Part 2"); + } + + [Test] + public void UserDeclared_AssetCount_Representation_Is_Preserved() + { + using var agent = new MTConnectAgent(initializeAgentDevice: false); + + var declared = new AssetCountDataItem("dev") + { + Representation = DataItemRepresentation.VALUE + }; + + var device = new Device + { + Id = "dev", + Name = "dev", + Uuid = "dev-uuid", + DataItems = new[] { declared } + }; + + var added = agent.AddDevice(device, initializeDataItems: false); + + Assert.That(added, Is.Not.Null); + + var assetCount = added.DataItems + .SingleOrDefault(d => d.Type == AssetCountDataItem.TypeId); + + Assert.That(assetCount, Is.Not.Null); + Assert.That(assetCount!.Representation, + Is.EqualTo(DataItemRepresentation.VALUE), + "user-declared ASSET_COUNT representation must be preserved verbatim"); + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/Agents/AssetCountFactoryDataSetGuardTests.cs b/tests/MTConnect.NET-Common-Tests/Agents/AssetCountFactoryDataSetGuardTests.cs new file mode 100644 index 000000000..557258f46 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Agents/AssetCountFactoryDataSetGuardTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using MTConnect.Agents; +using MTConnect.Configurations; +using MTConnect.Devices; +using MTConnect.Devices.DataItems; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Agents +{ + /// + /// Regression pin for + /// https://github.com/TrakHound/MTConnect.NET/issues/132 : + /// every code path that auto-injects an AssetCountDataItem MUST + /// stamp Representation = DATA_SET. The defect was an auto-generator + /// that inherited the .g.cs DefaultRepresentation = VALUE; this + /// fixture guards every auto-injection entry point we have access + /// to so a future regenerated generator (or a refactor of + /// NormalizeDevice) cannot reintroduce the bug silently. + /// + /// Spec: MTConnect Standard, Part 2 - Devices Information Model, + /// ASSET_COUNT (UML _19_0_3_68e0225_1640602520420_217627_44). + /// + [TestFixture] + [Category("AssetCountIsDataSet")] + public class AssetCountFactoryDataSetGuardTests + { + private static readonly string[] _deviceIds = { "lathe-1", "mill-7", "robot-A" }; + + [TestCaseSource(nameof(_deviceIds))] + public void AddDevice_AutoInjects_AssetCount_With_DataSet_Representation(string deviceId) + { + using var agent = new MTConnectAgent(initializeAgentDevice: false); + + var device = new Device + { + Id = deviceId, + Name = deviceId, + Uuid = $"{deviceId}-uuid" + }; + + var added = agent.AddDevice(device, initializeDataItems: false); + + Assert.That(added, Is.Not.Null); + + var assetCount = added.DataItems + .SingleOrDefault(d => d.Type == AssetCountDataItem.TypeId); + + Assert.That(assetCount, Is.Not.Null, + "auto-generator must inject one ASSET_COUNT DataItem on every device"); + Assert.That(assetCount!.Representation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void AddDevices_AutoInjects_AssetCount_With_DataSet_Representation_For_Every_Device() + { + using var agent = new MTConnectAgent(initializeAgentDevice: false); + + var devices = _deviceIds.Select(id => new Device + { + Id = id, + Name = id, + Uuid = $"{id}-uuid" + }).ToList(); + + var added = agent.AddDevices(devices, initializeDataItems: false); + + Assert.That(added, Is.Not.Null); + Assert.That(added.Count(), Is.EqualTo(_deviceIds.Length)); + + foreach (var dev in added) + { + var assetCount = dev.DataItems + .SingleOrDefault(d => d.Type == AssetCountDataItem.TypeId); + + Assert.That(assetCount, Is.Not.Null, + $"device {dev.Id}: auto-generator must inject one ASSET_COUNT DataItem"); + Assert.That(assetCount!.Representation, + Is.EqualTo(DataItemRepresentation.DATA_SET), + $"device {dev.Id}: Representation must be DATA_SET"); + } + } + + [Test] + public void AddDevice_With_Configuration_AutoInjects_AssetCount_With_DataSet_Representation() + { + // Same agent path but constructed via the configuration overload + // to keep both constructor paths covered. + var configuration = new AgentConfiguration(); + using var agent = new MTConnectAgent(configuration, initializeAgentDevice: false); + + var device = new Device + { + Id = "configured-dev", + Name = "configured-dev", + Uuid = "configured-dev-uuid" + }; + + var added = agent.AddDevice(device, initializeDataItems: false); + + Assert.That(added, Is.Not.Null); + + var assetCount = added.DataItems + .SingleOrDefault(d => d.Type == AssetCountDataItem.TypeId); + + Assert.That(assetCount, Is.Not.Null); + Assert.That(assetCount!.Representation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/Assets/Pallet/PalletMeasurementContractTests.cs b/tests/MTConnect.NET-Common-Tests/Assets/Pallet/PalletMeasurementContractTests.cs new file mode 100644 index 000000000..8d87303e8 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Assets/Pallet/PalletMeasurementContractTests.cs @@ -0,0 +1,196 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using MTConnect.Assets.Pallet; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Assets.Pallet +{ + /// + /// Pins the class-level contract that every Pallet measurement + /// subclass renders with the rich TypeId + three-constructor + /// scaffolding produced by the SysML measurement template, in + /// parity with the long-standing CuttingTools measurement DTOs. + /// + /// Per the MTConnect SysML model, the PhysicalAsset > Pallet + /// > Measurements package declares ten concrete measurement + /// subclasses (Weight, Height, Width, Length, Swing plus their + /// Loaded* counterparts) that generalize from an abstract + /// Measurement base. The C# generator must emit each one + /// with: + /// + /// a const string TypeId equal to the SysML class name; + /// a default constructor that stamps Type = TypeId; + /// a (double value) constructor that stamps both + /// Type and Value; + /// a (IMeasurement) copy constructor chaining to + /// the partial base. + /// + /// + /// Sources: + /// - SysML XMI: + /// v2.7. The Pallet measurement subclasses sit under UML package + /// "Asset Information Model > Pallet > Measurements" with the + /// abstract base UML ID + /// _2024x_68e0225_1727793846441_986747_23754. + /// - Reference implementation: cppagent's generic + /// PhysicalAsset::getMeasurementsFactory() handles every + /// physical-asset measurement element via a single regex-matched + /// factory; the per-type DTO scaffolding is a .NET-side ergonomic + /// convenience that mirrors what CuttingTools already gets. + /// + [TestFixture] + [Category("PalletMeasurementContract")] + public class PalletMeasurementContractTests + { + private static readonly (Type Type, string ExpectedTypeId)[] _palletMeasurements = new[] + { + (typeof(WeightMeasurement), "Weight"), + (typeof(HeightMeasurement), "Height"), + (typeof(WidthMeasurement), "Width"), + (typeof(LengthMeasurement), "Length"), + (typeof(SwingMeasurement), "Swing"), + (typeof(LoadedWeightMeasurement), "LoadedWeight"), + (typeof(LoadedHeightMeasurement), "LoadedHeight"), + (typeof(LoadedWidthMeasurement), "LoadedWidth"), + (typeof(LoadedLengthMeasurement), "LoadedLength"), + (typeof(LoadedSwingMeasurement), "LoadedSwing"), + }; + + private static System.Collections.Generic.IEnumerable PalletMeasurementCases => + _palletMeasurements.Select(p => new TestCaseData(p.Type, p.ExpectedTypeId) + .SetName($"{{m}}({p.Type.Name})")); + + [Test] + [TestCaseSource(nameof(PalletMeasurementCases))] + public void TypeId_Const_Equals_SysML_ClassName(Type measurementType, string expectedTypeId) + { + // The TypeId const is the wire-side discriminator the + // serializer uses to round-trip the measurement element + // name. It must equal the SysML class name verbatim. + var field = measurementType.GetField( + "TypeId", + BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); + + Assert.That(field, Is.Not.Null, + $"{measurementType.Name} must expose a public static TypeId const field"); + Assert.That(field!.IsLiteral, Is.True, + $"{measurementType.Name}.TypeId must be a const literal"); + Assert.That(field.FieldType, Is.EqualTo(typeof(string)), + $"{measurementType.Name}.TypeId must be a string"); + Assert.That(field.GetRawConstantValue(), Is.EqualTo(expectedTypeId), + $"{measurementType.Name}.TypeId must equal '{expectedTypeId}'"); + } + + [Test] + [TestCaseSource(nameof(PalletMeasurementCases))] + public void CodeId_Const_Is_Empty(Type measurementType, string expectedTypeId) + { + // Pallet measurements carry no MeasurementCode (the SysML + // Pallet Measurement abstract class has no `code` property, + // unlike the CuttingTool ToolingMeasurement which binds to + // MeasurementCodeEnum). The CodeId const must therefore be + // emitted as the empty string. + var field = measurementType.GetField( + "CodeId", + BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); + + Assert.That(field, Is.Not.Null, + $"{measurementType.Name} must expose a public static CodeId const field"); + Assert.That(field!.IsLiteral, Is.True, + $"{measurementType.Name}.CodeId must be a const literal"); + Assert.That(field.GetRawConstantValue(), Is.EqualTo(string.Empty), + $"{measurementType.Name}.CodeId must be empty for Pallet measurements"); + } + + [Test] + [TestCaseSource(nameof(PalletMeasurementCases))] + public void Default_Constructor_Stamps_Type(Type measurementType, string expectedTypeId) + { + var ctor = measurementType.GetConstructor(Type.EmptyTypes); + Assert.That(ctor, Is.Not.Null, + $"{measurementType.Name} must declare a public default constructor"); + + var instance = (Measurement)ctor!.Invoke(Array.Empty()); + + Assert.That(instance.Type, Is.EqualTo(expectedTypeId), + $"the default constructor must stamp Type = '{expectedTypeId}'"); + } + + [Test] + [TestCaseSource(nameof(PalletMeasurementCases))] + public void DoubleValue_Constructor_Stamps_Type_And_Value(Type measurementType, string expectedTypeId) + { + var ctor = measurementType.GetConstructor(new[] { typeof(double) }); + Assert.That(ctor, Is.Not.Null, + $"{measurementType.Name} must declare a public (double) constructor"); + + const double sentinelValue = 123.45; + var instance = (Measurement)ctor!.Invoke(new object[] { sentinelValue }); + + Assert.That(instance.Type, Is.EqualTo(expectedTypeId), + $"the (double) constructor must stamp Type = '{expectedTypeId}'"); + Assert.That(instance.Value, Is.EqualTo(sentinelValue), + $"the (double) constructor must stamp Value"); + } + + [Test] + [TestCaseSource(nameof(PalletMeasurementCases))] + public void IMeasurement_Constructor_Copies_Fields_And_Stamps_Type(Type measurementType, string expectedTypeId) + { + var ctor = measurementType.GetConstructor(new[] { typeof(IMeasurement) }); + Assert.That(ctor, Is.Not.Null, + $"{measurementType.Name} must declare a public (IMeasurement) constructor"); + + // Build a source measurement with every transferable field + // populated so the copy constructor's behavior can be + // verified end-to-end. WeightMeasurement is used as the + // source because every concrete subtype shares the same + // base Measurement contract. + var source = new WeightMeasurement(99.0) + { + Nominal = 100.0, + Minimum = 50.0, + Maximum = 150.0, + SignificantDigits = 4, + NativeUnits = "MILLIGRAM", + Units = "KILOGRAM", + }; + + var instance = (Measurement)ctor!.Invoke(new object[] { source }); + + Assert.That(instance.Type, Is.EqualTo(expectedTypeId), + $"the (IMeasurement) constructor must stamp Type = '{expectedTypeId}'"); + Assert.That(instance.Value, Is.EqualTo(99.0), + "Value must be copied from source"); + Assert.That(instance.Nominal, Is.EqualTo(100.0), + "Nominal must be copied from source"); + Assert.That(instance.Minimum, Is.EqualTo(50.0), + "Minimum must be copied from source"); + Assert.That(instance.Maximum, Is.EqualTo(150.0), + "Maximum must be copied from source"); + Assert.That(instance.SignificantDigits, Is.EqualTo(4), + "SignificantDigits must be copied from source"); + Assert.That(instance.NativeUnits, Is.EqualTo("MILLIGRAM"), + "NativeUnits must be copied from source"); + Assert.That(instance.Units, Is.EqualTo("KILOGRAM"), + "Units must be copied from source"); + } + + [Test] + [TestCaseSource(nameof(PalletMeasurementCases))] + public void Concrete_Subclass_Derives_From_Base_Measurement(Type measurementType, string expectedTypeId) + { + // The rich template chains the (IMeasurement) ctor to the + // base Measurement(IMeasurement). Pinning the inheritance + // chain here documents that contract so a future refactor + // re-rooting the subclasses elsewhere fails this test + // before silently breaking the constructor chain. + Assert.That(measurementType.BaseType, Is.EqualTo(typeof(Measurement)), + $"{measurementType.Name} must directly derive from MTConnect.Assets.Pallet.Measurement"); + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/Devices/Components/ControllersComponentMinimumVersionTests.cs b/tests/MTConnect.NET-Common-Tests/Devices/Components/ControllersComponentMinimumVersionTests.cs new file mode 100644 index 000000000..40df77755 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Devices/Components/ControllersComponentMinimumVersionTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using MTConnect.Devices; +using MTConnect.Devices.Components; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Devices.Components +{ + /// + /// Pins the class-level contract that ControllersComponent reports + /// MTConnect v2.0 as its MinimumVersion. The MTConnect SysML model + /// introduces the Controllers organizer AssociationClass at + /// v2.0 — before that release, controller groupings were ad-hoc. + /// The generated component class must surface the v2.0 introduction + /// year through its MinimumVersion override so version-gating + /// callers can reject a Controllers parent on a pre-v2.0 stream. + /// + /// Sources: + /// - SysML XMI: https://github.com/mtconnect/mtconnect_sysml_model + /// v2.7. The Controllers AssociationClass + /// (UML ID _19_0_3_68e0225_1648551529939_657918_1127) carries + /// Profile:normative introduced='2.0' at XMI line 68730, + /// pinning the v2.0 introduction year. + /// - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.0.xsd + /// declares the Controllers element under the parent + /// complex type with a substitutionGroup of Component. + /// + [TestFixture] + [Category("ControllersComponentMinimumVersion")] + public class ControllersComponentMinimumVersionTests + { + [Test] + public void Default_Constructor_Reports_Version20() + { + var component = new ControllersComponent(); + + Assert.That(component.MinimumVersion, Is.EqualTo(MTConnectVersions.Version20)); + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/Devices/DataItems/AssetCountDataItemDefaultRepresentationTests.cs b/tests/MTConnect.NET-Common-Tests/Devices/DataItems/AssetCountDataItemDefaultRepresentationTests.cs new file mode 100644 index 000000000..b685adfe7 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Devices/DataItems/AssetCountDataItemDefaultRepresentationTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection; +using MTConnect.Devices; +using MTConnect.Devices.DataItems; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Devices.DataItems +{ + /// + /// Pins the class-level contract that AssetCountDataItem defaults to + /// the DATA_SET representation. The MTConnect Standard models + /// ASSET_COUNT as a key/value map of asset-type to count, which is + /// the canonical DATA_SET shape; the generated DataItem class must + /// carry that default so consumers constructing the DataItem + /// directly observe a spec-conformant representation without having + /// to override it post-construction. + /// + /// Sources: + /// - SysML XMI: https://github.com/mtconnect/mtconnect_sysml_model + /// UML class AssetCount, UML ID + /// _19_0_3_68e0225_1640602520420_217627_44; the matching + /// EventEnum::ASSET_COUNT literal carries the spec marker + /// "{{term(data set)}} of the number of {{termplural(Asset)}} of + /// a given type for a {{term(Device)}}". + /// - XSD: + /// https://schemas.mtconnect.org/schemas/MTConnectStreams_2.7.xsd + /// declares AssetCountDataSet with substitutionGroup="Event" + /// carrying AssetCountEntry rows. + /// - Prose: MTConnect Standard Part 4 - Assets Information Model + /// describes ASSET_COUNT as a per-type asset tally. + /// - Reference implementation: cppagent v2.7.0.7 emits the + /// AssetCountDataSet observation element with + /// representation="DATA_SET". + /// + [TestFixture] + [Category("AssetCountIsDataSet")] + public class AssetCountDataItemDefaultRepresentationTests + { + [Test] + public void DefaultRepresentation_Const_Equals_DataSet() + { + Assert.That( + AssetCountDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET), + "AssetCountDataItem.DefaultRepresentation must equal DATA_SET"); + } + + [Test] + public void Parameterless_Constructor_Stamps_DataSet_Representation() + { + var item = new AssetCountDataItem(); + + Assert.That( + item.Representation, + Is.EqualTo(DataItemRepresentation.DATA_SET), + "the parameterless constructor must stamp Representation = DATA_SET"); + } + + [Test] + public void DeviceId_Constructor_Stamps_DataSet_Representation() + { + var item = new AssetCountDataItem("Device-1"); + + Assert.That( + item.Representation, + Is.EqualTo(DataItemRepresentation.DATA_SET), + "the (deviceId) constructor must stamp Representation = DATA_SET"); + } + + [Test] + public void DeviceId_Constructor_Builds_V2_Conformant_Id() + { + var item = new AssetCountDataItem("Device-1"); + + // The id must follow CreateId(parentId, name) shape: + // it starts with the supplied deviceId, contains the + // canonical NameId, and uses '_' as the separator. + Assert.That(item.Id, Is.Not.Null.And.Not.Empty); + Assert.That(item.Id, Does.StartWith("Device-1")); + Assert.That(item.Id, Does.Contain(AssetCountDataItem.NameId)); + Assert.That(item.Id, Is.EqualTo($"Device-1_{AssetCountDataItem.NameId}")); + } + + [Test] + public void MinimumVersion_Reports_Version20() + { + var item = new AssetCountDataItem(); + + // ASSET_COUNT was introduced in MTConnect v2.0; the + // generated DataItem must surface that version through the + // MinimumVersion override so consumers can gate features + // by introduction version. Pinning this assertion keeps + // the override exercised under coverage in addition to + // pinning the spec-introduction year for ASSET_COUNT. + Assert.That(item.MinimumVersion, Is.EqualTo(MTConnectVersions.Version20)); + } + + [Test] + public void DefaultRepresentation_Field_Is_Public_Const_DataSet() + { + var field = typeof(AssetCountDataItem).GetField( + nameof(AssetCountDataItem.DefaultRepresentation), + BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); + + Assert.That(field, Is.Not.Null, + "AssetCountDataItem must expose a public static DefaultRepresentation field"); + Assert.That(field!.IsPublic, Is.True, + "DefaultRepresentation must be public"); + Assert.That(field.IsStatic, Is.True, + "DefaultRepresentation must be static"); + Assert.That(field.IsLiteral || field.IsInitOnly, Is.True, + "DefaultRepresentation must be const or readonly"); + Assert.That(field.FieldType, Is.EqualTo(typeof(DataItemRepresentation)), + "DefaultRepresentation must be typed DataItemRepresentation"); + + // GetRawConstantValue on a const enum returns the + // underlying integer; GetValue(null) returns the boxed + // enum. Read the boxed enum to keep the assertion typed. + var value = field.GetValue(null); + Assert.That(value, Is.EqualTo(DataItemRepresentation.DATA_SET), + "DefaultRepresentation reflective value must equal DATA_SET"); + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/Devices/DataItems/StructuredRepresentationClassifierTests.cs b/tests/MTConnect.NET-Common-Tests/Devices/DataItems/StructuredRepresentationClassifierTests.cs new file mode 100644 index 000000000..c7609ec08 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Devices/DataItems/StructuredRepresentationClassifierTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using MTConnect.Devices; +using MTConnect.Devices.DataItems; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Devices.DataItems +{ + /// + /// Pins the class-level contract that DataItems whose SysML + /// result property is typed as a UML class generalizing from + /// DataSet default to the DATA_SET representation + /// rather than TABLE. The MTConnect SysML model encodes the + /// canonical structured representation of a DataItem in the parent + /// chain of its result class — DataSet for one-dimensional + /// key/value rows, Table for two-dimensional key/value + /// matrices. The generator must walk the result class's + /// generalization chain rather than hard-coding TABLE for + /// any class-typed result. + /// + /// Sources: + /// - SysML XMI: https://github.com/mtconnect/mtconnect_sysml_model + /// v2.7. The result class for each pinned DataItem generalizes + /// from the abstract DataSet class: + /// ALARM_LIMITS -> AlarmLimitResult -> DataSet + /// ALARM_LIMIT -> AlarmLimitResult -> DataSet + /// CONTROL_LIMITS -> ControlLimitsResult -> DataSet + /// CONTROL_LIMIT -> ControlLimitsResult -> DataSet + /// LOCATION_ADDRESS -> AddressResult -> DataSet + /// LOCATION_SPATIAL_GEOGRAPHIC -> GeographicLocationResult -> DataSet + /// SPECIFICATION_LIMITS -> SpecificationLimitsResult -> DataSet + /// SPECIFICATION_LIMIT -> SpecificationLimitResult -> DataSet + /// SENSOR_ATTACHMENT -> SensorAttachmentResult -> DataSet + /// - XSD: https://schemas.mtconnect.org/schemas/MTConnectStreams_2.7.xsd + /// declares the matching DataSet-substitution observation + /// elements (e.g. AlarmLimitsDataSet, ControlLimitsDataSet) + /// that pair with each of the DataItems below. + /// - Reference implementation: cppagent v2.7.0.7 emits these + /// types as DATA_SET observations (not TABLE). + /// + [TestFixture] + [Category("StructuredRepresentationClassifier")] + public class StructuredRepresentationClassifierTests + { + [Test] + public void AlarmLimits_DefaultRepresentation_Is_DataSet() + { + Assert.That( + AlarmLimitsDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void AlarmLimit_DefaultRepresentation_Is_DataSet() + { + Assert.That( + AlarmLimitDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void ControlLimits_DefaultRepresentation_Is_DataSet() + { + Assert.That( + ControlLimitsDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void ControlLimit_DefaultRepresentation_Is_DataSet() + { + Assert.That( + ControlLimitDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void LocationAddress_DefaultRepresentation_Is_DataSet() + { + Assert.That( + LocationAddressDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void LocationSpatialGeographic_DefaultRepresentation_Is_DataSet() + { + Assert.That( + LocationSpatialGeographicDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void SpecificationLimits_DefaultRepresentation_Is_DataSet() + { + Assert.That( + SpecificationLimitsDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void SpecificationLimit_DefaultRepresentation_Is_DataSet() + { + Assert.That( + SpecificationLimitDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void SensorAttachment_DefaultRepresentation_Is_DataSet() + { + Assert.That( + SensorAttachmentDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.DATA_SET)); + } + + [Test] + public void FeatureMeasurement_DefaultRepresentation_Stays_Table() + { + // FeatureMeasurement's result class FeatureMeasurementResult + // generalizes from Table (not DataSet); it must remain TABLE + // after the classifier change to prevent over-correction. + Assert.That( + FeatureMeasurementDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.TABLE)); + } + + [Test] + public void MaintenanceList_DefaultRepresentation_Stays_Table() + { + // MaintenanceList's result class generalizes from Table; it + // must remain TABLE after the classifier change. + Assert.That( + MaintenanceListDataItem.DefaultRepresentation, + Is.EqualTo(DataItemRepresentation.TABLE)); + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/Interfaces/InterfaceDataItemSubTypesTests.cs b/tests/MTConnect.NET-Common-Tests/Interfaces/InterfaceDataItemSubTypesTests.cs new file mode 100644 index 000000000..d045337b5 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Interfaces/InterfaceDataItemSubTypesTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Linq; +using MTConnect.Interfaces; +using NUnit.Framework; + +namespace MTConnect.Tests.Common.Interfaces +{ + /// + /// Pins the class-level contract that every Interface DataItem + /// exposes a nested SubTypes enum carrying at minimum + /// REQUEST and RESPONSE members. Per the MTConnect + /// SysML model, every Interface DataItem class declares two + /// subtype classes named <Name>.Request and + /// <Name>.Response; the C# generator must emit a + /// SubTypes enum mirroring those subtype literals so + /// consumers can construct the DataItem with a typed subtype + /// rather than passing an opaque string. + /// + /// Sources: + /// - SysML XMI: https://github.com/mtconnect/mtconnect_sysml_model + /// v2.7. Each of the ten interface-event classes below carries + /// an immediate sub-class named <Name>.Request and + /// <Name>.Response that generalize from the parent + /// interface DataItem (e.g. CloseChuck.Request, CloseChuck.Response + /// at XMI lines 48715 and 48736). + /// - Reference implementation: cppagent v2.7.0.7 emits these + /// DataItems with a SubType of REQUEST or RESPONSE on the + /// resulting observation element. + /// + [TestFixture] + [Category("InterfaceDataItemSubTypes")] + public class InterfaceDataItemSubTypesTests + { + private static readonly Type[] _interfaceDataItemTypes = new[] + { + typeof(CloseChuckDataItem), + typeof(CloseDoorDataItem), + typeof(OpenChuckDataItem), + typeof(OpenDoorDataItem), + typeof(MaterialChangeDataItem), + typeof(MaterialFeedDataItem), + typeof(MaterialLoadDataItem), + typeof(MaterialRetractDataItem), + typeof(MaterialUnloadDataItem), + typeof(PartChangeDataItem) + }; + + private static System.Collections.Generic.IEnumerable InterfaceDataItemTypes => _interfaceDataItemTypes; + + [Test] + [TestCaseSource(nameof(InterfaceDataItemTypes))] + public void Interface_DataItem_Exposes_SubTypes_Enum(Type interfaceDataItemType) + { + var subTypesEnum = interfaceDataItemType.GetNestedType("SubTypes"); + + Assert.That(subTypesEnum, Is.Not.Null, + $"{interfaceDataItemType.Name} must expose a nested SubTypes type"); + Assert.That(subTypesEnum!.IsEnum, Is.True, + $"{interfaceDataItemType.Name}.SubTypes must be an enum"); + } + + [Test] + [TestCaseSource(nameof(InterfaceDataItemTypes))] + public void Interface_DataItem_SubTypes_Contains_Request_And_Response(Type interfaceDataItemType) + { + var subTypesEnum = interfaceDataItemType.GetNestedType("SubTypes"); + Assert.That(subTypesEnum, Is.Not.Null, + $"{interfaceDataItemType.Name} must expose a nested SubTypes type"); + + var names = Enum.GetNames(subTypesEnum!); + + Assert.That(names, Does.Contain("REQUEST"), + $"{interfaceDataItemType.Name}.SubTypes must contain REQUEST"); + Assert.That(names, Does.Contain("RESPONSE"), + $"{interfaceDataItemType.Name}.SubTypes must contain RESPONSE"); + } + } +} diff --git a/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/AssetCountWireRepresentationE2ETests.cs b/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/AssetCountWireRepresentationE2ETests.cs new file mode 100644 index 000000000..6a365bdeb --- /dev/null +++ b/tests/MTConnect.NET-JSON-cppagent-Tests/Devices/AssetCountWireRepresentationE2ETests.cs @@ -0,0 +1,271 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using MTConnect.Agents; +using MTConnect.Devices; +using MTConnect.Devices.Json; +using NUnit.Framework; + +namespace MTConnect.Tests.JsonCppagent.Devices +{ + /// + /// End-to-end wire-shape pin: builds an MTConnect agent broker (which + /// initialises the synthetic Agent device), adds two production-shaped + /// devices, calls GetDevicesResponseDocument through the same + /// public surface an HTTP /probe handler uses, runs the result through + /// JsonDevicesResponseDocument (the cppagent-format Probe + /// serialiser the JSON-CPPAGENT-MQTT module also feeds), and asserts the + /// emitted JSON contains exactly one ASSET_COUNT DataItem per + /// device with representation: "DATA_SET". + /// + /// The pin closes the gap left by + /// AssetCountAutoGeneratedDataSetTests (which only exercises the + /// in-memory ctor stamping) and AssetCountFactoryDataSetGuardTests + /// (which only exercises the factory-side guard). Neither of those + /// exercises the full agent → JSON-cppagent serialiser chain, so a + /// downstream consumer's report of a third ASSET_COUNT + /// DataItem with representation=VALUE on the wire could + /// not previously be caught by the test suite. + /// + /// Source authority: + /// - SysML XMI: AssetCount EventEnum element ID + /// _19_0_3_68e0225_1640602520420_217627_44. result + /// is declared with structured-representation prose (DATA_SET) per + /// the EventEnum literal's description. + /// - Generated source: libraries/MTConnect.NET-Common/Devices/DataItems/AssetCountDataItem.g.cs + /// declares DefaultRepresentation = DataItemRepresentation.DATA_SET; + /// both ctors stamp Representation = DefaultRepresentation. + /// - Two known agent-side emission paths: + /// libraries/MTConnect.NET-Common/Devices/Agent.cs:90 (Agent + /// device's own InitializeDataItems) and + /// libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs:1170 + /// (NormalizeDeviceRequiredDataItems for any added device). Both go + /// through the AssetCountDataItem ctor. + /// - JSON-cppagent serialisation: JsonDataItem.cs emits the + /// representation JSON field only when the DataItem's + /// Representation is NOT VALUE (so a VALUE entry on the wire shows + /// up as "no representation key" rather than as + /// representation: "VALUE"). + /// + [TestFixture] + [Category("AssetCountIsDataSet")] + public class AssetCountWireRepresentationE2ETests + { + [Test] + public void Probe_response_emits_exactly_one_ASSET_COUNT_per_device_with_DATA_SET_representation() + { + using var agent = new MTConnectAgentBroker(); + + var device1 = new Device + { + Id = "f1", + Uuid = "f1-uuid", + Name = "Furnace1" + }; + var device2 = new Device + { + Id = "f2", + Uuid = "f2-uuid", + Name = "Furnace2" + }; + agent.AddDevice(device1); + agent.AddDevice(device2); + + var document = agent.GetDevicesResponseDocument(); + Assert.That(document, Is.Not.Null); + Assert.That(document.Devices, Is.Not.Null); + + var json = MTConnect.JsonFunctions.Convert(new JsonDevicesResponseDocument(document)); + Assert.That(json, Is.Not.Null); + + using var doc = JsonDocument.Parse(json!); + + // Walk the entire response document and collect every DataItem + // whose type is ASSET_COUNT, grouped by the device-uuid that + // owns it. Walking the whole tree (vs hard-coding paths) means + // a regression that injects an ASSET_COUNT in an unexpected place + // (e.g. inside a Component's DataItems collection) is caught by + // the per-device cardinality assertion below. The cppagent JSON + // shape splits the Devices container into two arrays: `Agent` + // (the synthetic agent device) and `Device` (production devices) + // — both must be visited. + var assetCountsByDeviceUuid = new Dictionary>(); + var devicesContainer = doc.RootElement + .GetProperty("MTConnectDevices") + .GetProperty("Devices"); + foreach (var arrayName in new[] { "Agent", "Device" }) + { + if (!devicesContainer.TryGetProperty(arrayName, out var arrayElement)) + continue; + foreach (var device in arrayElement.EnumerateArray()) + { + var uuid = device.GetProperty("uuid").GetString() ?? "(no-uuid)"; + var collected = new List(); + CollectAssetCountDataItems(device, collected); + assetCountsByDeviceUuid[uuid] = collected; + } + } + + // Every device in the response — the synthetic Agent device + // plus the two production-shaped devices — must carry exactly + // ONE ASSET_COUNT DataItem. + foreach (var (uuid, items) in assetCountsByDeviceUuid) + { + Assert.That(items.Count, Is.EqualTo(1), + $"Device uuid='{uuid}' has {items.Count} ASSET_COUNT DataItems on the wire; expected exactly 1."); + } + + // Every ASSET_COUNT DataItem on the wire must carry an + // explicit representation: "DATA_SET" key. Per the + // JsonDataItem serializer's omit-on-VALUE rule, the absence + // of the key would mean Representation=VALUE — the wire + // regression a downstream consumer would observe. + foreach (var (uuid, items) in assetCountsByDeviceUuid) + { + var item = items[0]; + Assert.That(item.TryGetProperty("representation", out var representation), Is.True, + $"Device uuid='{uuid}' ASSET_COUNT has no `representation` key (would default to VALUE on consumer round-trip)."); + Assert.That(representation.GetString(), Is.EqualTo("DATA_SET"), + $"Device uuid='{uuid}' ASSET_COUNT representation is not DATA_SET."); + } + } + + [Test] + public void Probe_response_for_agent_device_emits_exactly_one_ASSET_COUNT_with_DATA_SET() + { + // The agent device specifically exercises Agent.cs:90's + // InitializeDataItems path, which is the second of the two + // ctor-stamping paths (the other is + // MTConnectAgent.NormalizeDeviceRequiredDataItems for any added + // device). This test pins the agent-device branch in isolation + // — even with no production-shaped devices added — so a regression + // that breaks Agent.InitializeDataItems' ASSET_COUNT stamping + // surfaces here without being masked by other devices. + using var agent = new MTConnectAgentBroker(); + + var document = agent.GetDevicesResponseDocument(); + Assert.That(document, Is.Not.Null); + Assert.That(document.Devices, Is.Not.Null); + Assert.That(document.Devices.Any(d => d.Type == Agent.TypeId), Is.True, + "Expected the synthetic Agent device to be present in the probe response."); + + var json = MTConnect.JsonFunctions.Convert(new JsonDevicesResponseDocument(document)); + using var doc = JsonDocument.Parse(json!); + + // The cppagent JSON shape places the synthetic Agent device + // under `Devices.Agent[]` (not `Devices.Device[]` — that's + // for production devices). Read the agent device from there. + var devicesContainer = doc.RootElement + .GetProperty("MTConnectDevices") + .GetProperty("Devices"); + Assert.That(devicesContainer.TryGetProperty("Agent", out var agentArray), Is.True, + "Probe response has no `MTConnectDevices.Devices.Agent` array — the synthetic Agent device should always appear there."); + Assert.That(agentArray.GetArrayLength(), Is.EqualTo(1), + $"Expected exactly one synthetic Agent device, got {agentArray.GetArrayLength()}."); + var agentDevice = agentArray[0]; + + var agentAssetCounts = new List(); + CollectAssetCountDataItems(agentDevice, agentAssetCounts); + + Assert.That(agentAssetCounts.Count, Is.EqualTo(1), + $"Agent device emits {agentAssetCounts.Count} ASSET_COUNT DataItems on the wire; expected exactly 1. A third entry with representation=VALUE is the regression this pin guards against."); + + var item = agentAssetCounts[0]; + Assert.That(item.TryGetProperty("representation", out var representation), Is.True, + "Agent device ASSET_COUNT has no `representation` key on the wire (would default to VALUE on consumer round-trip)."); + Assert.That(representation.GetString(), Is.EqualTo("DATA_SET"), + "Agent device ASSET_COUNT representation is not DATA_SET on the wire."); + } + + [Test] + public void Probe_response_string_carries_no_VALUE_representation_on_any_ASSET_COUNT_entry() + { + // Belt-and-braces sanity check on the raw JSON string. The + // JsonDataItem serialiser is configured to OMIT the `representation` + // key when the in-memory Representation == VALUE; an explicit + // `"representation":"VALUE"` substring next to an ASSET_COUNT + // entry would therefore signal a serialiser-side bug (a code + // path that explicitly emits the literal "VALUE" rather than + // honoring the omit-on-VALUE rule). This pin catches that + // specific shape regardless of where in the document tree the + // offending entry sits. + using var agent = new MTConnectAgentBroker(); + agent.AddDevice(new Device { Id = "f1", Uuid = "f1-uuid", Name = "Furnace1" }); + + var json = MTConnect.JsonFunctions.Convert( + new JsonDevicesResponseDocument(agent.GetDevicesResponseDocument()))!; + + // Naive but precise: search for the literal pair surrounded by + // commas. False-positives are not possible — `representation` is + // a JSON property name only used on DataItem entries, and the + // only legal values are the four representation enum strings; + // a `"VALUE"` value next to an ASSET_COUNT type would have to + // come from a serialiser bug. + using var doc = JsonDocument.Parse(json); + var assetCounts = new List(); + var devicesContainer = doc.RootElement + .GetProperty("MTConnectDevices") + .GetProperty("Devices"); + foreach (var arrayName in new[] { "Agent", "Device" }) + { + if (!devicesContainer.TryGetProperty(arrayName, out var arrayElement)) + continue; + foreach (var device in arrayElement.EnumerateArray()) + { + CollectAssetCountDataItems(device, assetCounts); + } + } + foreach (var item in assetCounts) + { + if (item.TryGetProperty("representation", out var representation)) + { + Assert.That(representation.GetString(), Is.Not.EqualTo("VALUE"), + "An ASSET_COUNT DataItem on the wire carries `representation: \"VALUE\"` — the serialiser should have omitted the key (the omit-on-VALUE rule) or the producer should have stamped DATA_SET. Either way, this is a regression."); + } + } + } + + // Recursive walk over a Device or Component subtree, collecting every + // DataItem whose type is "ASSET_COUNT". Handles the cppagent JSON + // shape's wrapping conventions (DataItems.DataItem[] arrays, + // Components.[] arrays, Compositions.Composition[] arrays) + // and the "object instead of single-element array" sugar that the + // serialiser sometimes emits. + private static void CollectAssetCountDataItems(JsonElement node, List sink) + { + if (node.TryGetProperty("DataItems", out var dataItemsContainer) + && dataItemsContainer.TryGetProperty("DataItem", out var dataItemArray)) + { + IEnumerable items = dataItemArray.ValueKind == JsonValueKind.Array + ? dataItemArray.EnumerateArray() + : new[] { dataItemArray }; + foreach (var di in items) + { + if (di.TryGetProperty("type", out var typeElement) + && typeElement.GetString() == "ASSET_COUNT") + { + sink.Add(di); + } + } + } + + if (node.TryGetProperty("Components", out var componentsContainer)) + { + foreach (var componentTypeProperty in componentsContainer.EnumerateObject()) + { + var componentArray = componentTypeProperty.Value; + IEnumerable children = componentArray.ValueKind == JsonValueKind.Array + ? componentArray.EnumerateArray() + : new[] { componentArray }; + foreach (var child in children) + { + CollectAssetCountDataItems(child, sink); + } + } + } + } + } +}