Files
Marc Schier 0374e19121 Add MachineTool, WMTP, GMS and CranesHoists companions (#4765)
# Description

_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._

Add provider-backed MachineTool, bundled WMTP, GMS and CranesHoists
companions using the existing Machinery/MachineVision fluent, DI and
typed-client approach. **The PR is open/non-draft by user choice.
Current head `ecc4a7d6ca3e8004cf28ab54da605da302d026bf` is mergeable and
has 108 successful checks, six intentionally skipped checks, zero
failures and zero pending checks. All five primary workflows completed
successfully at that exact head.**

| Family | Released specification/model | Packaging |
| --- | --- | --- |
| MachineTool | OPC 40501-1, 1.02.0 | Model, Server, Client and MCP;
normal root versions |
| WMTP | OPC 40505, 1.0.0 | Bundled in MachineTool; distinct CLR/wire
namespace and independent WMTP-only hosting |
| GMS | OPC 40210 reference 1.0.1, normative model 1.0.0 | Model,
Server, Client and MCP; normal root versions |
| CranesHoists | OPC 40020-1, 1.00 | Model, Server, Client and MCP; all
four plus Debug variants forced preview |

There are **12 new packable projects**. No separate WMTP
package/profile/guide, root version bump, tag, release or package
publication is included. Mandatory instance structure is the default;
selected optional features require explicit providers. The
implementation reuses Machinery results/jobs/files/ProcessValues and
Robotics topology, including retained MachineTool Production, defined
GMS jobs/parts, modern ISA95/Machinery jobs, shared-owner hosting and
isolated instances alongside MachineVision.

Runtime and integration fixes preserve raw quality/timestamps, coherent
acquisition, namespace ownership, accepted-configuration state,
borrowed-resource lifetime, session/node/file cleanup, terminal-value
publication, event propagation and endpoint-aware capability
publication. Firmware/licence transfer means provider acceptance, not
installation/activation. Cancellation does not silently abort another
caller's operation. The later upstream MCP expansion is integrated
without losing either family set or its default-off privileged/live
execution policy.

### Conformance and scope

Canonical model inputs are pinned to UA-Nodeset revision
`96a08945425d0295edb3607d2a38f6841ef44046`.

**GMS upstream exception:** [Annex
A.1](https://reference.opcfoundation.org/specs/OPC-40210/annex-a) makes
the released NodeSet normative, but §12.3.2.7 requires undefined
`GMS_ProductionStatisticsType`. Inherited statistics and defined
jobs/parts are implemented without inventing a standard type/NodeId; the
complete GMS Production facet is withheld.

The requirement ledger records **102 positively evidenced CU rows, 96
directly negative-tested rows and six explicit negative-evidence
limitations**. All 648 test-class/name/TRX links were independently
resolved to passing cases. The six limitations concern Stacklight,
retained legacy PowerOnDuration, Machinery PowerOnDuration, WorkingUnit,
Obligation and MaintenanceMode. They are not claims that every
invalid-input or hardware-control behavior was tested. The undefined GMS
requirement is outside the 102 defined CUs. No external certification
claim is made.

Retained baseline complete-assembly line coverage, with generated code
retained and no source exclusions:

| Family | Model | Server | Client |
| --- | ---: | ---: | ---: |
| MachineTool/WMTP | 83.80% | 91.98% | 84.74% |
| GMS | 95.51% | 90.63% | 88.38% |
| CranesHoists | 92.76% | 94.45% | 92.24% |

These nine measurements predate the later focused review fixes and
merges; they are not relabelled as new `ecc4a7d6c` percentages. The
current head's separate CI code-coverage check succeeded. No new
exclusions or threshold relaxations were introduced.

## Related Issues

- Fixes
[#4764](https://github.com/OPCFoundation/UA-.NETStandard/issues/4764),
the implementation/design and acceptance tracker.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-10-15 15:15:46 +02:00

515 lines
29 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using NUnit.Framework;
using Opc.Ua.Machinery.Result;
using Opc.Ua.MachineTool;
namespace Opc.Ua.GMS.Tests
{
/// <summary>
/// Exercises generated factories and inherited mandatory/default-optional structures at public model seams.
/// </summary>
[TestFixture]
[Category("GMS")]
public sealed class ModelGenerationTests
{
/// <summary>
/// GMS uses the existing dependency types rather than regenerating their declarations in its assembly.
/// </summary>
[Test]
public void GeneratedInheritanceUsesDependencyAssemblies()
{
Assert.Multiple(() =>
{
Assert.That(typeof(GMSState).BaseType, Is.EqualTo(typeof(MachineToolState)));
Assert.That(typeof(GMSIdentificationState).BaseType,
Is.EqualTo(typeof(MachineToolIdentificationState)));
Assert.That(typeof(GMSEquipmentState).BaseType, Is.EqualTo(typeof(EquipmentState)));
Assert.That(typeof(GMSMonitoringState).BaseType, Is.EqualTo(typeof(MonitoringState)));
Assert.That(typeof(GMSJobState).BaseType, Is.EqualTo(typeof(ProductionJobState)));
Assert.That(typeof(GMSPartState).BaseType, Is.EqualTo(typeof(ProductionPartState)));
Assert.That(typeof(SensorState).BaseType, Is.EqualTo(typeof(ToolState)));
Assert.That(typeof(MultiSensorState).BaseType, Is.EqualTo(typeof(MultiToolState)));
Assert.That(typeof(GMSResultManagementState).BaseType, Is.EqualTo(typeof(ResultManagementState)));
Assert.That(typeof(GMSResultState).BaseType, Is.EqualTo(typeof(ResultState)));
Assert.That(typeof(IntermediateResultEventState).BaseType, Is.EqualTo(typeof(ResultReadyEventState)));
Assert.That(typeof(GMSState).Assembly, Is.Not.SameAs(typeof(MachineToolState).Assembly));
Assert.That(typeof(GMSResultState).Assembly, Is.Not.SameAs(typeof(ResultState).Assembly));
Assert.That(Namespaces.GMS, Is.EqualTo("http://opcfoundation.org/UA/GMS/"));
Assert.That(GmsModel.NamespaceUri, Is.EqualTo(Namespaces.GMS));
Assert.That(GmsModel.Version, Is.EqualTo("1.0.0"));
Assert.That(GmsModel.ReferenceEdition, Is.EqualTo("1.0.1"));
Assert.That(GmsModel.SourceCommit, Is.EqualTo("96a08945425d0295edb3607d2a38f6841ef44046"));
Assert.That(GmsModel.NormalizedNodeSetSha256,
Is.EqualTo("9E510C01A2671B732E5EB27561B06A5BC040BB219412884D0A3AFF3C55B88EE0"));
});
}
/// <summary>
/// Every published object type has a typed, reflection-free factory at its canonical numeric identity.
/// </summary>
[TestCase(1001u, typeof(IntermediateResultEventState))]
[TestCase(1002u, typeof(GMSState))]
[TestCase(1003u, typeof(GMSEquipmentState))]
[TestCase(1004u, typeof(GMSMonitoringState))]
[TestCase(1005u, typeof(CorrectionState))]
[TestCase(1006u, typeof(ToolMonitoringState))]
[TestCase(1007u, typeof(SensorState))]
[TestCase(1008u, typeof(GMSResultManagementState))]
[TestCase(1009u, typeof(CharacteristicState))]
[TestCase(1010u, typeof(SensorWarningAlarmState))]
[TestCase(1011u, typeof(GMSIdentificationState))]
[TestCase(1012u, typeof(MultiSensorState))]
[TestCase(1014u, typeof(LoadingMonitoringState))]
[TestCase(1015u, typeof(SensorExchangeRackState))]
[TestCase(1016u, typeof(GMSJobState))]
[TestCase(1017u, typeof(GMSPartState))]
[TestCase(1018u, typeof(RotaryTableState))]
[TestCase(1019u, typeof(CalibrationPrognosisState))]
[TestCase(1022u, typeof(TipExchangeRackState))]
public void ObjectFactoriesRetainCanonicalTypes(uint identifier, Type expectedType)
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
var factory = new NodeStateFactory();
factory.AddGmsTypeSystem();
var typeId = NodeId.Create(identifier, GmsModel.NamespaceUri, context.NamespaceUris);
var browseName = new QualifiedName("GmsComponent", 1);
NodeState? created = factory.CreateInstance(
context, null, NodeClass.Object, browseName, ReferenceTypeIds.HasComponent, typeId);
Assert.That(created, Is.TypeOf(expectedType));
Assert.That(((BaseObjectState)created!).TypeDefinitionId, Is.EqualTo(typeId));
}
/// <summary>
/// The three GMS variable types retain their typed factory and underlying wire data types.
/// </summary>
[TestCase(2002u, typeof(AdditionalSensorState), 26u, "http://opcfoundation.org/UA/")]
[TestCase(2003u, typeof(CatalogState), 26u, "http://opcfoundation.org/UA/")]
[TestCase(2004u, typeof(GMSResultState), 3008u, "http://opcfoundation.org/UA/Machinery/Result/")]
public void VariableFactoriesRetainCanonicalTypes(
uint identifier,
Type expectedType,
uint dataTypeId,
string dataTypeNamespace)
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
var factory = new NodeStateFactory();
factory.AddGmsTypeSystem();
var typeId = NodeId.Create(identifier, GmsModel.NamespaceUri, context.NamespaceUris);
NodeState? created = factory.CreateInstance(
context, null, NodeClass.Variable, new QualifiedName("Value", 1),
ReferenceTypeIds.HasComponent, typeId);
Assert.That(created, Is.TypeOf(expectedType));
var variable = (BaseVariableState)created!;
Assert.Multiple(() =>
{
Assert.That(variable.TypeDefinitionId, Is.EqualTo(typeId));
Assert.That(variable.DataType,
Is.EqualTo(NodeId.Create(dataTypeId, dataTypeNamespace, context.NamespaceUris)));
});
}
/// <summary>
/// The default factory retains required inherited production, monitoring, identification and result structure.
/// </summary>
[TestCase(false)]
[TestCase(true)]
public void GmsFactoryRetainsMandatoryInheritedStructure(bool reverseNamespaces)
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: reverseNamespaces);
GMSState machine = context.CreateInstanceOfGMSType();
var identification = ModelTestContext.RequireChild<GMSIdentificationState>(
context, machine, "Identification", Di.Namespaces.OpcUaDi);
var equipment = ModelTestContext.RequireChild<GMSEquipmentState>(
context, machine, "Equipment", MachineTool.Namespaces.MachineTool);
var monitoring = ModelTestContext.RequireChild<GMSMonitoringState>(
context, machine, "Monitoring", MachineTool.Namespaces.MachineTool);
var operation = ModelTestContext.RequireChild<MachineOperationMonitoringState>(
context, monitoring, "MachineTool", MachineTool.Namespaces.MachineTool);
var mode = ModelTestContext.RequireChild<BaseVariableState>(
context, operation, "OperationMode", MachineTool.Namespaces.MachineTool);
var production = ModelTestContext.RequireChild<ProductionState>(
context, machine, "Production", MachineTool.Namespaces.MachineTool);
var active = ModelTestContext.RequireChild<ProductionActiveProgramState>(
context, production, "ActiveProgram", MachineTool.Namespaces.MachineTool);
var state = ModelTestContext.RequireChild<ProductionProgramStateMachineState>(
context, active, "State", MachineTool.Namespaces.MachineTool);
var current = ModelTestContext.RequireChild<FiniteStateVariableState>(
context, state, "CurrentState", Opc.Ua.Namespaces.OpcUa);
var result = ModelTestContext.RequireChild<GMSResultManagementState>(
context, machine, "ResultManagement", GmsModel.NamespaceUri);
ModelTestContext.RequireChild<NotificationState>(
context, machine, "Notification", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<PropertyState<LocalizedText>>(
context, identification, "Manufacturer", Di.Namespaces.OpcUaDi);
ModelTestContext.RequireChild<PropertyState<string>>(
context, identification, "ProductInstanceUri", Di.Namespaces.OpcUaDi);
ModelTestContext.RequireChild<PropertyState<string>>(
context, identification, "SerialNumber", Di.Namespaces.OpcUaDi);
ModelTestContext.RequireChild<BaseVariableState>(
context, active, "Name", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<PropertyState<ushort>>(
context, active, "NumberInList", Opc.Ua.Namespaces.OpcUa);
ModelTestContext.RequireChild<PropertyState<NodeId>>(context, current, "Id", Opc.Ua.Namespaces.OpcUa);
ModelTestContext.RequireChild<PropertyState<uint>>(context, current, "Number", Opc.Ua.Namespaces.OpcUa);
Assert.Multiple(() =>
{
Assert.That(machine.TypeDefinitionId,
Is.EqualTo(NodeId.Create(1002, GmsModel.NamespaceUri, context.NamespaceUris)));
Assert.That(identification.ReferenceTypeId, Is.EqualTo(ReferenceTypeIds.HasAddIn));
Assert.That(mode.DataType, Is.EqualTo(ExpandedNodeId.ToNodeId(
MachineTool.DataTypeIds.MachineOperationMode, context.NamespaceUris)));
Assert.That(result.TypeDefinitionId,
Is.EqualTo(NodeId.Create(1008, GmsModel.NamespaceUri, context.NamespaceUris)));
Assert.That(result.ReferenceTypeId, Is.EqualTo(ReferenceTypeIds.HasComponent));
Assert.That(result.ResultTransfer, Is.Null);
Assert.That(result.GetLatestResult, Is.Null);
Assert.That(result.CorrectionsFolder, Is.Null);
Assert.That(identification.Workspace, Is.Null);
Assert.That(equipment.Tools, Is.Null);
Assert.That(equipment.Accessories, Is.Null);
Assert.That(monitoring.LoadingMonitoring, Is.Null);
Assert.That(monitoring.ToolMonitoring, Is.Null);
});
}
/// <summary>
/// Sensor factories specialize real tools and preserve mandatory tool-life and classification structures.
/// </summary>
[Test]
public void SensorFactoryPreservesToolInheritanceAndOptionalQualification()
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
SensorState sensor = context.CreateInstanceOfSensorType();
var life = ModelTestContext.RequireChild<BaseObjectState>(
context, sensor, "ToolLife", MachineTool.Namespaces.MachineTool);
var classification = ModelTestContext.RequireChild<BaseVariableState>(
context, sensor, "Class", GmsModel.NamespaceUri);
ModelTestContext.RequireChild<BaseDataVariableState<uint>>(
context, sensor, "ControlIdentifier1", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<BaseVariableState>(
context, sensor, "Locked", MachineTool.Namespaces.MachineTool);
Assert.Multiple(() =>
{
Assert.That(classification.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.UInteger));
Assert.That(life.FindChild(context, new QualifiedName(
"Qualified", context.NamespaceUris.GetIndexOrAppend(GmsModel.NamespaceUri))), Is.Null);
Assert.That(sensor.Alignment, Is.Null);
Assert.That(sensor.IsQualifiedStatus, Is.Null);
});
sensor.AddAlignment(context).AddIsQualifiedStatus(context).AddAxes(context);
sensor.Alignment!.Value = ToolAlignmentState.Indexed;
sensor.IsQualifiedStatus!.Value = ToolIsQualifiedStatus.Imprecise;
sensor.Axes!.Value = ["A", "B"];
Assert.Multiple(() =>
{
Assert.That(sensor.Alignment.DataType,
Is.EqualTo(NodeId.Create(3004, GmsModel.NamespaceUri, context.NamespaceUris)));
Assert.That(sensor.IsQualifiedStatus.DataType,
Is.EqualTo(NodeId.Create(3002, GmsModel.NamespaceUri, context.NamespaceUris)));
Assert.That(sensor.Axes.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
});
}
/// <summary>
/// A MultiSensor's inherited placeholder creates the specialized sensor with required nested tool members.
/// </summary>
[Test]
public void MultiSensorPlaceholderCreatesSensorRatherThanBaseTool()
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
MultiSensorState multi = context.CreateInstanceOfMultiSensorType();
var browseName = new QualifiedName("ProbeHead", 1);
SensorState sensor = multi.AddTool_Placeholder(context, browseName);
Assert.Multiple(() =>
{
Assert.That(multi.FindChild(context, browseName), Is.SameAs(sensor));
Assert.That(sensor.Parent, Is.SameAs(multi));
Assert.That(sensor.TypeDefinitionId,
Is.EqualTo(NodeId.Create(1007, GmsModel.NamespaceUri, context.NamespaceUris)));
Assert.That(sensor.Class, Is.Not.Null);
Assert.That(sensor.ToolLife, Is.Not.Null);
Assert.That(sensor.ControlIdentifier1, Is.Not.Null);
});
}
/// <summary>
/// The Qualified declaration retains published defaults; selecting the CU is a runtime responsibility.
/// </summary>
[Test]
public void QualifiedDeclarationPreservesCanonicalDefaultsAndRequiredProperties()
{
SystemContext context = ModelTestContext.CreateSystemContext();
Dictionary<NodeId, NodeState> nodes = ModelTestContext.IndexNodes(
context, new NodeStateCollection().AddGmsTypeSystem(context));
var qualified = (ToolLifeState<double>)nodes[NodeId.Create(
6341, GmsModel.NamespaceUri, context.NamespaceUris)];
Assert.Multiple(() =>
{
Assert.That(qualified.Indication!.Value, Is.EqualTo(ToolLifeIndication.Time));
Assert.That(qualified.IsCountingUp!.Value, Is.True,
"The XML and Table 17 say true; the QualifiedToolLife CU instead requires false at runtime.");
Assert.That(qualified.EngineeringUnits, Is.Not.Null);
Assert.That(qualified.TypeDefinitionId, Is.EqualTo(ExpandedNodeId.ToNodeId(
MachineTool.VariableTypeIds.ToolLifeType, context.NamespaceUris)));
Assert.That(qualified.ModellingRuleId, Is.EqualTo(Opc.Ua.ObjectIds.ModellingRule_Optional));
Assert.That(qualified.Indication.BrowseName.NamespaceIndex,
Is.EqualTo(context.NamespaceUris.GetIndex(MachineTool.Namespaces.MachineTool)));
});
}
/// <summary>
/// GMS monitoring adds optional typed loading and tool monitoring without replacing required operation data.
/// </summary>
[Test]
public void OptionalMonitoringIncludesInheritedMandatoryMembers()
{
SystemContext context = ModelTestContext.CreateSystemContext();
GMSMonitoringState monitoring = context.CreateInstanceOfGMSMonitoringType();
monitoring.AddLoadingMonitoring(context).AddToolMonitoring(context);
var load = ModelTestContext.RequireChild<LoadingMonitoringState>(
context, monitoring, "LoadingMonitoring", GmsModel.NamespaceUri);
var tool = ModelTestContext.RequireChild<ToolMonitoringState>(
context, monitoring, "ToolMonitoring", GmsModel.NamespaceUri);
var status = ModelTestContext.RequireChild<BaseVariableState>(
context, load, "LoadStatus", GmsModel.NamespaceUri);
ModelTestContext.RequireChild<BaseVariableState>(
context, load, "Name", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<BaseVariableState>(
context, tool, "Name", MachineTool.Namespaces.MachineTool);
Assert.Multiple(() =>
{
Assert.That(status.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.UInteger));
Assert.That(load.IsInLoadingPosition, Is.Null);
Assert.That(tool.ActiveTool, Is.Null);
});
tool.AddActiveTool(context);
Assert.That(tool.ActiveTool!.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
}
/// <summary>
/// GMS jobs and parts retain the older MachineTool production model while adding measurement metadata.
/// </summary>
[Test]
public void ProductionJobsAndPartsRetainInheritedMandatoryStateAndStatistics()
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
GMSJobState job = context.CreateInstanceOfGMSJobType();
GMSPartState part = context.CreateInstanceOfGMSPartType();
ModelTestContext.RequireChild<PropertyState<string>>(
context, job, "Identifier", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<BaseVariableState>(
context, job, "RunsCompleted", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<BaseVariableState>(
context, job, "RunsPlanned", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<OrderedListState>(
context, job, "ProductionPrograms", MachineTool.Namespaces.MachineTool);
var state = ModelTestContext.RequireChild<ProductionStateMachineState>(
context, job, "State", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<FiniteStateVariableState>(
context, state, "CurrentState", Opc.Ua.Namespaces.OpcUa);
ModelTestContext.RequireChild<BaseVariableState>(
context, part, "Name", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<BaseVariableState>(
context, part, "PartQuality", MachineTool.Namespaces.MachineTool);
ModelTestContext.RequireChild<BaseVariableState>(
context, part, "ProcessIrregularity", MachineTool.Namespaces.MachineTool);
Assert.Multiple(() =>
{
Assert.That(job.MeasurementReason, Is.Null);
Assert.That(part.Operator, Is.Null);
Assert.That(part.ProcessParameter, Is.Null);
});
job.AddMeasurementReason(context).AddRemainingTime(context);
job.MeasurementReason!.Value = MeasurementReasonEnum.AuditMeasurement;
part.AddOperator(context).AddProcessParameter(context);
Assert.Multiple(() =>
{
Assert.That(job.MeasurementReason.DataType,
Is.EqualTo(NodeId.Create(3009, GmsModel.NamespaceUri, context.NamespaceUris)));
Assert.That(job.RemainingTime!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.Duration));
Assert.That(part.Operator!.CatalogName, Is.Not.Null);
Assert.That(part.Operator.CatalogEntry, Is.Not.Null);
Assert.That(part.ProcessParameter!.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
});
}
/// <summary>
/// The real inherited statistics type remains available without inventing the undefined GMS statistics type.
/// </summary>
[Test]
public void GmsRetainsOptionalMachineToolProductionStatistics()
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
GMSState machine = context.CreateInstanceOfGMSType();
ProductionState production = machine.Production!;
Assert.That(production.Statistics, Is.Null);
production.AddStatistics(context);
var statistics = ModelTestContext.RequireChild<ProductionStatisticsState>(
context, production, "Statistics", MachineTool.Namespaces.MachineTool);
Assert.That(statistics.PartsProducedInLifetime, Is.Null);
statistics.AddPartsProducedInLifetime(context);
statistics.PartsProducedInLifetime!.Value = 42;
Assert.Multiple(() =>
{
Assert.That(statistics.TypeDefinitionId,
Is.EqualTo(NodeId.Create(20, MachineTool.Namespaces.MachineTool, context.NamespaceUris)));
Assert.That(statistics.PartsProducedInLifetime.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.UInt32));
Assert.That(statistics.PartsProducedInLifetime.Value, Is.EqualTo(42));
Assert.That(statistics.PartsProducedInLifetime.BrowseName.NamespaceIndex,
Is.EqualTo(context.NamespaceUris.GetIndex(MachineTool.Namespaces.MachineTool)));
});
}
/// <summary>
/// Accessories and prognoses preserve their DI/Machinery identification and required calibration data.
/// </summary>
[Test]
public void AccessoryAndCalibrationFactoriesPreserveMandatoryInheritedMembers()
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
RotaryTableState rotary = context.CreateInstanceOfRotaryTableType();
SensorExchangeRackState sensorRack = context.CreateInstanceOfSensorExchangeRackType();
TipExchangeRackState tipRack = context.CreateInstanceOfTipExchangeRackType();
foreach (BaseObjectState accessory in new BaseObjectState[] { rotary, sensorRack, tipRack })
{
var identification = ModelTestContext.RequireChild<Machinery.MachineryItemIdentificationState>(
context, accessory, "Identification", Di.Namespaces.OpcUaDi);
ModelTestContext.RequireChild<PropertyState<LocalizedText>>(
context, identification, "Manufacturer", Di.Namespaces.OpcUaDi);
Assert.That(identification.ReferenceTypeId, Is.EqualTo(ReferenceTypeIds.HasAddIn));
}
CalibrationPrognosisState calibration = context.CreateInstanceOfCalibrationPrognosisType();
Assert.Multiple(() =>
{
Assert.That(rotary.IsIntegrated, Is.Not.Null);
Assert.That(rotary.NumberOfAxes!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.Byte));
Assert.That(sensorRack.IsAvailable, Is.Not.Null);
Assert.That(tipRack.IsAvailable, Is.Not.Null);
Assert.That(calibration.Calibrated!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.Boolean));
Assert.That(calibration.DateOfCalibration!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.UtcTime));
Assert.That(calibration.PredictedTime!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.UtcTime));
Assert.That(calibration.CalibrationCertificate, Is.Null);
});
calibration.AddCalibrationCertificate(context).AddCalibrationInterval(context);
Assert.Multiple(() =>
{
Assert.That(calibration.CalibrationCertificate!.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
Assert.That(calibration.CalibrationInterval!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.Duration));
});
}
/// <summary>
/// Specialized result/event/alarm factories keep Machinery Result inheritance and GMS-specific members.
/// </summary>
[Test]
public void ResultEventAndAlarmFactoriesPreserveTheirTypedExtensions()
{
SystemContext context = ModelTestContext.CreateSystemContext(reverseNamespaces: true);
GMSResultState result = context.CreateInstanceOfGMSResultType();
IntermediateResultEventState intermediate = context.CreateInstanceOfIntermediateResultEventType();
SensorWarningAlarmState alarm = context.CreateInstanceOfSensorWarningAlarmType();
Assert.Multiple(() =>
{
Assert.That(result.ResultMetaData, Is.Not.Null);
Assert.That(result.UsedTools, Is.Null);
Assert.That(result.ValueRank, Is.EqualTo(ValueRanks.Scalar));
Assert.That(intermediate.Result, Is.Not.Null);
Assert.That(intermediate.EventId, Is.Not.Null);
Assert.That(alarm.ErrorCode!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.String));
Assert.That(alarm.ActiveState, Is.Not.Null);
Assert.That(alarm.InputNode, Is.Not.Null);
});
result.AddUsedTools(context);
result.UsedTools!.Value = ["sensor-17", "sensor-18"];
var browseName = new QualifiedName("Diameter", 1);
CharacteristicState characteristic = intermediate.AddCharacteristics_Placeholder(context, browseName);
characteristic.AddResultValue(context).AddNominal(context).AddUpperToleranceLimit(context);
Assert.Multiple(() =>
{
Assert.That(intermediate.FindChild(context, browseName), Is.SameAs(characteristic));
Assert.That(result.UsedTools.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
Assert.That(characteristic.ResultValue!.EngineeringUnits, Is.Not.Null);
Assert.That(characteristic.UpperToleranceLimit!.DataType, Is.EqualTo(Opc.Ua.DataTypeIds.Double));
});
}
/// <summary>
/// Every released GMS enumeration registers exact symbols and rejects unknown values and names.
/// </summary>
[Test]
public void EnumFactoriesPreserveReleasedValuesAndRejectInvalidSymbols()
{
ServiceMessageContext context = ModelTestContext.CreateMessageContext();
XElement root = XDocument.Load(Path.Combine(
TestContext.CurrentContext.TestDirectory, "Model", "opc.ua.gms.nodeset2.xml")).Root!;
XNamespace ns = "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd";
XElement[] enums =
[
.. root.Elements(ns + "UADataType").Where(node =>
node.Element(ns + "Definition")!.Elements(ns + "Field")
.Any(field => field.Attribute("Value") != null))
];
Assert.That(enums, Has.Length.EqualTo(4));
foreach (XElement definition in enums)
{
uint identifier = uint.Parse(
((string)definition.Attribute("NodeId")!)[7..], CultureInfo.InvariantCulture);
Assert.That(context.Factory.TryGetEnumeratedType(
new ExpandedNodeId(identifier, GmsModel.NamespaceUri), out IEnumeratedType? type), Is.True);
foreach (XElement field in definition.Element(ns + "Definition")!.Elements(ns + "Field"))
{
int value = (int)field.Attribute("Value")!;
string name = (string)field.Attribute("Name")!;
Assert.That(type!.TryGetSymbol(value, out string? symbol), Is.True);
Assert.That(symbol, Is.EqualTo(name));
Assert.That(type.TryGetValue(name, out int actual), Is.True);
Assert.That(actual, Is.EqualTo(value));
}
Assert.Multiple(() =>
{
Assert.That(type!.TryGetSymbol(-1, out _), Is.False);
Assert.That(type.TryGetSymbol(int.MaxValue, out _), Is.False);
Assert.That(type.TryGetValue("NotAReleasedValue", out _), Is.False);
});
}
}
}
}