Files
UA-.NETStandard/docs/GMS.md
Marc Schier 2836da7ec8 Fix sessionless ResultTransfer ownership bypass and other issues. (#4834)
# Description

Correct ownership, endpoint validation, policy propagation, subscription
delivery, file handling and generated-model behavior across the stack,
tools and samples. Add focused regression tests and update the
corresponding guides and sample documentation.

### Previously included corrections

- Require an active, matching session to generate or access transient
ResultTransfer file handles. Cover both sessionless generation and reads
against a handle owned by an active session, using live session owners
in the Machinery tests.
- Keep physical filesystem operations within their configured mount and
apply the same confinement checks throughout file operations.
- Validate LDS semaphore paths consistently during server registration
and registration-store maintenance.

### Individual issues addressed

| # | Component | Issue and correction |
|---|---|---|
| 1 | AI jobs and transfers | Jobs and transfers did not consistently
retain their creator's identity and session. Apply ownership checks to
descendants, file handles, subscriptions and execution, and recheck
deployment permissions before dispatch. |
| 2 | Password-management client | Password-bearing calls could use
signing-only channels or follow a replacement connection. Capture an
encrypted transport generation and keep dispatch bound to it; preserve
ordinary non-password edits. |
| 3 | GDS and certificate-push clients | Connections omitted hostname
verification. Validate the original configured DNS name or IP address
before session activation. |
| 4 | Certificate push | Private-key uploads could use signing-only
calls. Require an encrypted, generation-bound dispatch while preserving
certificate-only updates and restoring the caller's identity afterward.
|
| 5 | MQTT client | The .NET Framework custom-trust callback could
discard TLS hostname, certificate-availability and usage errors. Retain
those checks while supporting configured issuer trust. |
| 6 | DTLS handshake | Certificate trust was not tied to the configured
peer address. Match the certificate's DNS or IP identity to the intended
endpoint. |
| 7 | Replicated address spaces | Serialized nodes lost per-node and
descendant permission metadata. Preserve role permissions, user-role
permissions and access restrictions, including updates to existing
replica nodes. |
| 8 | MCP PubSub imports | Imports without groups skipped checks on the
effective broker destination. Check the actual endpoint before resolving
credentials or activating the connection. |
| 9 | Reference-server accounts | A disabled username account could
authenticate again with its previous roles. Reject disabled accounts
during fresh authentication. |
| 10 | Console reference client | The sample disabled hostname checks
when connecting to an otherwise trusted peer. Retain the original
requested host and validate it before activation. |
| 11 | Capture replay and export | Replay and raw export could accept
non-capture files or include key material. Require complete PCAP/PCAPNG
containers and exclude key sidecars and decryption-secret blocks. |
| 12 | PubSub adapter fallback | The fallback client configuration
automatically accepted peer certificates. Apply normal trust and
hostname validation before activation or reads. |
| 13 | Kafka transport | Explicit TLS enablement was checked but did not
reach both final Confluent configurations. Use the same effective
protocol for validation, producers and consumers. |
| 14 | PubSub actions | Configuration rebuilds could widen a
connection-local action handler's scope. Preserve its original
connection association and reject mismatched dispatch. |
| 15 | PubSub readers | Reader-level message-protection settings were
accepted but ignored. Apply the effective group and reader settings
before accepting and publishing received values. |
| 16 | PubSub key groups | Different `SecurityGroupId` values could
share one connection-wide key context. Reject mixed groups on a
connection and require separate connections for distinct groups. |
| 17 | MCP session registrations | Session-name replacement or reuse
could retain another registration's credential bindings. Bind access to
the registration, caller and authenticated endpoint, and preserve that
identity when borrowing sessions. |
| 18 | Viewer HTTP host | Requests reached the viewer's OPC UA session
without consistent Host, Origin and port checks. Validate the request
destination and browser origin before using the session, while retaining
supported local MCP clients. |
| 19 | Vision file operations | File operations did not consistently
apply protected-file checks, and remote handles lacked caller and
connection context. Apply the shared checks and use host-side opaque
handles bound to caller, target and generation. |
| 20 | DTLS record integrity | The integrity-only MAC used a truncated
sequence number. Authenticate the full reconstructed sequence number and
epoch so records remain distinct across rollover. |
| 21 | AMB client sample | Connection setup omitted hostname validation.
Preserve the originally requested host and verify the peer identity
before activation. |
| 22 | Robot sample writes | The R2 `SpeedOverride` callback could
target R1 state. Bind each callback to its own robot and apply that
robot's write policy. |
| 23 | AI result properties | Optional result children inherited
remotely writable defaults. Preserve their declaration restrictions and
keep server-produced results read-only. |
| 24 | WSS peer-trust updates | Existing channels did not consistently
respond to peer-trust changes. Revalidate complete chains for forward
and reverse WSS channels and close only those that no longer validate. |
| 25 | GDS revocation checks | Caller-provided certificates could cause
issuer retrieval from embedded URLs. Resolve validation from configured
local trust, issuers and CRLs without fetching caller-provided
locations. |
| 26 | GMS observations | Published correction observations retained
writable model defaults. Reject remote writes while preserving
publisher-side updates. |
| 27 | Production-dataset monitoring | Subscription admission and
delivery could use the reporter's context instead of each subscriber's
permissions. Evaluate the subscriber at admission and on every delivery,
including cancellation and disposal paths. |
| 28 | WoT MQTT destinations | Hostless MQTT forms could imply a
localhost destination before loopback checks. Reject them before
credential lookup or connection attempts and validate the effective
endpoint. |
| 29 | XRegistry opaque reads | Content-ID reads and notifications did
not consistently honor encrypted-read requirements or the currently
accepted transport. Recheck the bound endpoint and reject transport
downgrades before rebinding, including when protected values are already
queued. |
| 30 | PubSub capture dissection | Key loading did not consistently
check the configured capture-root boundary. Check each actual key path
immediately before opening it. |
| 31 | PubSub publication queues | Imported WriterGroup queues were not
fully checked against publish-topic grants. Check the destination
actually used by each group. |
| 32 | MCP PubSub mutations | A caller-supplied mode could select
subscribe grants for an operation that publishes. Derive the required
direction and parent context from the actual mutation. |
| 33 | Generated write masks | Model import and generation could widen
an explicit `UserWriteMask`. Preserve it through import, copy, merge,
inheritance and dependency metadata. |
| 34 | Generated method flags | An explicit `UserExecutable=false` could
be lost. Preserve its specified state and resulting method behavior
through the same model paths. |
| 35 | Generated role identifiers | Unresolved role identifiers could
cause permission entries to be omitted. Preserve namespace-qualified
role IDs and report a diagnostic when a policy cannot be represented. |
| 36 | Aggregation-server defaults | The `localhost` default did not
reliably constrain listener binding. Use numeric loopback for the
default endpoint and command-line fallback. |
| 37 | Certificate-validation cache | Cached results could skip current
leaf or issuer validity checks. Recheck validity windows on cache hits
and keep hostname validation independent of cached chain trust. |
| 38 | Software-update folders | Revision names and marker contents
containing dot segments could select paths outside the software root.
Normalize and check every derived path against that root. |
| 39 | USDA arrays | Array elements were not encoded for their string or
asset context. Escape them as data and preserve valid composition and
round-trip behavior. |
| 40 | Adapter hot reload | A changed dataset definition with the same
name could keep old fields, subscriptions or cached values active.
Compare complete immutable definitions and retire old state before
replacement. |
| 41 | Adapter index ranges | Subscription publication could ignore
`IndexRange` and return the complete value. Preserve the requested
projection and its status and timestamps; reject unsupported range
providers. |
| 42 | PubSub metadata and targets | Metadata from one connection could
change another connection's target mapping. Scope metadata to its
connection and provenance, and resolve targets using actual typed field
IDs rather than field names or unintended positional fallback. |
| 43 | Scales product locks | Product locks were not applied across
every remote mutation path. Apply them consistently to value writes,
filling calculations and related recipe/controller operations while
retaining permitted updates. |
| 44 | Event permission caching | Cached event decisions could outlive
identity, role or node-policy changes. Revalidate the current
subscriber, event type and source permissions instead of reusing a stale
decision. |
| 45 | WoT Modbus destinations | Hostless Modbus forms could imply
localhost before loopback checks. Reject them before connecting and
validate the effective endpoint. |
| 46 | MCP local PKI tools | Peer-trust mutations did not consistently
require the caller's `CertificateAdministration` grant. Check the
capability before trust and untrust operations. |
| 47 | Robot-control HTTP bridge | The bridge lacked consistent Host,
Origin and port validation and caller-specific session grants. Apply
those checks before borrowing the OPC UA session and retain supported
local clients. |
| 48 | Subscription transfers | Transferred subscriptions could retain
cached notifications after the destination's permissions changed.
Require authorization continuity between source and destination
identities and roles, invalidate stale retransmission data when
continuity fails, and preserve retransmission behavior for compatible
transfers. |

### Compatibility notes

- Replication uses the versioned NSRP v1 policy envelope. Upgrade peers
together and re-encode or reseed legacy topology from the authoritative
model; missing permission metadata is not inferred from old records.
- Integrity-only DTLS peers must be upgraded together for the full
sequence/epoch MAC. AEAD behavior is unchanged.
- Distinct PubSub key groups require separate connections.
- Custom sessions that cannot capture the actual encrypted transport,
and subscription providers that cannot honor a requested range, are
rejected rather than silently changing the requested behavior.
- Subscription transfers retain retransmission data only when the
destination authorization remains compatible with the source snapshot;
otherwise the old sequence numbers and cached data are invalidated.

## Related Issues

No separate tracking issue is linked. The individual corrections are
listed above.

## 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.
- [ ] I have added all necessary documentation.
- [x] 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.
- [ ] I have addressed **all** PR feedback received.
2026-10-21 05:37:29 +02:00

42 KiB

Geometric Measurement Systems

The Opc.Ua.GMS* libraries implement OPC 40210 Geometric Measurement Systems. They combine the released GMS model with the existing MachineTool runtime, Machinery building blocks and result services. A system remains a native GMSType instance, not a base MachineTool object with an unrelated result tree.

The reference document is 1.0.1; its released normative NodeSet is 1.0.0. GMS version 1 is a monitoring and result model. It does not define new measurement-start, physical-calibration, GMS job-start or correction-actuation commands.

Contents

Library layout

The names in this table are assembly/project names. NuGet package IDs add the OPCFoundation.NetStandard. prefix, for example OPCFoundation.NetStandard.Opc.Ua.GMS.Server.

Library Responsibility
Opc.Ua.GMS Released generated states, identifiers, loaders, typed proxies and shared observation/result contracts
Opc.Ua.GMS.Server Native GMS builder, equipment/production binding, result publisher, bounded reference stores and hosting
Opc.Ua.GMS.Client Discovery, bounded native reads, results/downloads and observations over an existing session
Opc.Ua.Mcp.GMS Six bounded GMS MCP workflows over the host's named sessions

Model/server/client target .NET Framework 4.8 and .NET 8, 9 and 10. MCP targets .NET 8, 9 and 10. GMS packages, including .Debug variants, follow the normal root version policy; they are not independently forced to preview. The model is server/client-independent, and the typed client does not depend on the server runtime.

Models and dependencies

Reference edition and wire namespace

Item Value
Reference document OPC 40210, edition 1.0.1
Released NodeSet model version 1.0.0, published 2023-07-01
Wire namespace http://opcfoundation.org/UA/GMS/
Generated CLR namespace Opc.Ua.GMS
Shared contract namespace Opc.Ua.GMS.Contracts
Model metadata GmsModel.ReferenceEdition, Version, NamespaceUri, SourceCommit

GmsModel.GetNamespaceUris() returns the dependency closure in order. AddGmsTypeSystem loads/registers GMS and its existing dependencies rather than generating duplicate copies of their types. Generated proxies resolve namespace-qualified identities; clients must not assume the GMS namespace has a fixed numeric index.

Reuse of MachineTool and Machinery

GMS derives from MachineTool and consumes DI, IA, Machinery, MachineTool's ISA-95/Machinery Jobs dependency closure, and Machinery Result. Although the MachineTool assembly also contains WMTP, GMS does not select WMTP, PA-DIM or ProcessValues merely by referencing that assembly.

The root, identification, tools, jobs/parts and result management keep their GMS-derived types. Shared machinery supplies registration, model-change tracking, state observations, conditions, metadata handles and temporary-file transfer. The server reuses these services rather than implementing another subscription, result-transfer or session-lifetime protocol.

The NodeSet's required-model versions are publication dependencies, not a second set of generated assemblies to install beside the repository's current compatible models. In particular, retained MachineTool Production remains available for GMS despite its partial deprecation in newer MachineTool editions.

Quick start - server

Build a mandatory-only system

Every complete system needs an explicit equipment provider, result store and nameplate. The following configuration helper receives those application-owned services and gives the resulting runtime to an acquisition coordinator:

using System;
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.GMS.Contracts;
using Opc.Ua.GMS.Server;
using Opc.Ua.Server.Hosting;

static IOpcUaServerBuilder AddInspectionServer(
    IServiceCollection services,
    IGmsProvider observations,
    IGmsResultStore results,
    Action<GmsHandle> retainRuntime)
{
    return services.AddOpcUa()
        .AddServer(options =>
        {
            options.ApplicationName = "GmsServer";
            options.ApplicationUri = "urn:example:gms-server";
            options.EndpointUrls.Add("opc.tcp://localhost:62559/GmsServer");
        })
        .AddGms()
        .ConfigureGms(async (context, ct) =>
        {
            GmsHandle system = await context.AddGms("InspectionCell")
                .WithProvider(observations)
                .WithIdentification(identity =>
                {
                    identity.Manufacturer = new LocalizedText("Example manufacturer");
                    identity.SerialNumber = "CMM-001";
                    identity.ProductInstanceUri = "urn:example:inspection:CMM-001";
                })
                .WithResults(results)
                .BuildAsync(ct);
            retainRuntime(system);
        });
}

Invoke it during host construction, then await the normal application/server startup and shutdown. Configure certificates, trust, endpoints and user authentication through server hosting and CertificateManager.

For a simulation, InMemoryGmsProvider(new GmsSystemSnapshot()) starts with explicitly unavailable observations. InMemoryGmsResultStore takes the existing IServiceMessageContext, such as context.Context.AsMessageContext() inside configuration. It is a bounded reference store, not durable production storage.

Do not put the returned GmsHandle in an await using scoped to the configuration callback: that would shut it down as startup completes. retainRuntime should only retain it for application-managed refresh.

Acquire and refresh observations

IGmsProvider.ReadAsync returns an immutable, coherent GmsSystemSnapshot. Its MachineTool member carries the inherited revision and observations; native sensor/calibration/job/part observations extend that same acquisition.

using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.GMS.Contracts;
using Opc.Ua.GMS.Server;

static async ValueTask PublishGmsObservationAsync(
    InMemoryGmsProvider provider,
    GmsHandle system,
    GmsSystemSnapshot acquisition,
    CancellationToken ct)
{
    await provider.SetSnapshotAsync(acquisition, ct);
    await system.RefreshAsync(ct);
}

The in-memory adapter assigns a strictly increasing inherited revision. A real provider must do so itself and keep returned graphs immutable. Updating the adapter does not itself refresh OPC UA nodes; the runtime's refresh performs validation and coherent publication. Neither call starts a physical measurement.

Quick start - client

Discover systems and read results

The facade borrows an existing connected session and discovers actual GMS subtypes, not arbitrary objects in a namespace:

using System;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.GMS.Client;
using Opc.Ua.GMS.Contracts;
using Opc.Ua.Machinery.Result;

static async ValueTask ReadInspectionResultsAsync(
    ISession session, ITelemetryContext telemetry, CancellationToken ct)
{
    await using GmsClient client = session.CreateGmsClient(telemetry);
    ArrayOf<GmsDeviceEntry> systems = await client.DiscoverAsync(ct);
    for (int index = 0; index < systems.Count; index++)
    {
        GmsDeviceEntry system = systems[index];
        GmsSystemReadSnapshot snapshot = await client.ReadSnapshotAsync(system.NodeId, ct);
        GmsResultClient results = client.GetResults(system.NodeId);
        ArrayOf<string> ids = await results.QueryAsync(16, cancellationToken: ct);
        Console.WriteLine($"{system.NodeId}: {ids.Count} result identifiers");
        if (ids.Count != 0)
        {
            ResultDataType result = await results.GetByIdAsync(ids[0], ct);
            Console.WriteLine(result.ResultMetaData!.ResultId);
        }
    }
}

QueryAsync returns up to the requested result count; it is not a statement that the entire store contains no more results. Every acquired metadata handle is released by the typed operation. Use ReadPublishedResultsAsync when raw result-variable DataValue quality and acquisition timestamps are needed, rather than treating the returned ResultDataType envelope as a DataValue.

Factory and session ownership

AddGmsClient() extends an existing AddOpcUa().AddClient(...) registration and registers IGmsClientFactory. CreateAsync uses the configured container-cached ManagedSession and borrows it.

Direct constructors, session.CreateGmsClient(...), GmsClientFactory and GmsResultClientFactory do not transfer session ownership. Disposing one GMS facade drains only its own work and leaves other consumers connected. Cancellation after session acquisition or a failed facade constructor also leaves the shared session with its owner.

The DI container closes its cached session at shutdown. An application that creates an independent session must close/dispose it separately. See client hosting and the MachineTool lifetime rules.

Server hosting and composition

Standalone and existing Machinery owners

AddGms registers one Machinery owner with MachineryParts.BuildingBlocks | MachineryParts.Result. GmsServerOptions.InstanceNamespaceUri chooses the standalone application namespace. Optional system features are still off until the per-instance builder selects them.

When the owner already exists, use AddGmsToMachinery, not a second AddGms:

using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Machinery.Server;
using Opc.Ua.Server.Hosting;

static IOpcUaServerBuilder AddGmsModelsToCell(IOpcUaServerBuilder server)
{
    return server.AddMachinery(options =>
    {
        options.Parts = MachineryParts.BuildingBlocks | MachineryParts.Result;
        options.InstanceNamespaceUri = "urn:example:cell:instances";
    }).AddGmsToMachinery();
}

The example establishes the owner first; omit AddMachinery if the application already registered it. The existing owner must include both required parts. ConfigureGmsFor<MachineryNodeManager>(...) uses that owner's context. Multiple systems require independent per-instance providers and stores.

MachineTool, WMTP, cranes and Machine Vision can share the owner when all their required model/part selections are present. See the CompanionSuite sample for the combined setup.

Custom owners and one native root

ConfigureGmsFor<TNodeManager> targets a compatible existing DiNodeManager subtype with the shared Machinery construction services. GmsModelProvider plugs into its existing model-provider pipeline. The model loader does not create another DeviceSet or MachineTool owner.

The native builder uses AddMachineTool<GMSState> with GMS-owned factories. WithMachinery(...) adds bindings to the same unpublished typed root; that callback must not call BuildAsync itself. Registration, rollback and root-bound resources follow the existing owner's transaction.

For advanced result-only binding on an already staged GMS root, WithGmsResults(store, out binding, options) adopts its declared GMSResultManagementState. binding.CreatePublisher(machineHandle) is tied to that exact registered root. It does not create another base AddIn and cannot be attached to another machine. The staged root still needs its non-result GMS requirements; this binding alone is not a full GMS system.

System builder and provider contracts

Mandatory defaults and optional features

GmsFeatures.None retains mandatory identification, equipment, monitoring, production and result-management structure. It does not silently install optional sensors, files or command adapters.

GmsBuilder.WithOptions(GmsSystemOptions) controls native optional surfaces:

Feature Required data or relationship
Identification Workspace and device-subclass observations
Sensors Native sensor/multisensor inventory, including qualification countdowns
DynamicSensors Requires Sensors; permits inventory changes through shared lifetime handling
Loading Part-loading state and optional loading position
ToolMonitoring Requires Sensors; references actual active sensor keys
Accessories Rotary tables, sensor exchange racks and tip exchange racks
AdditionalSensors Environmental/additional analog measurements with class and units
Calibration Calibration state, dates, interval/certificate and forecast observations
Production GMS jobs/parts on retained MachineTool Production
DynamicProduction Requires Production; permits changing production plans
SensorWarnings Requires AdditionalSensors, not primary measurement sensors

Selected data must be complete and type-correct before publication. Invalid feature combinations fail with BadConfigurationError. Enabling All does not supply the missing equipment observations.

WithMachineToolFeatures selects inherited monitoring independently. WithProduction, WithPrognoses and WithNotifications reuse the corresponding MachineTool options, while the GMS builder retains its own native job, part and sensor factories.

Sensors, multisensors and qualification

GmsSystemSnapshot.Sensors describes native single sensors and nested multisensors. GmsSensor.Tool supplies the inherited tool identity/life/location data; native fields retain their declared types, including class, capabilities, alignment, axes, probe information, measuring/working range and resolution.

GmsQualifiedLife.RemainingHours is a Double countdown in hours under the native Qualified life entry. The binding preserves Qualified/Time and IsCountingUp=false. Negative remaining hours after expiry are meaningful and are not clamped to zero. Acquisition quality, geometric qualification status and countdown expiry are different facts.

Dynamic sensor changes use stable keys and the existing MachineTool tool module. ToolMonitoring.ActiveSensorKeys resolves the accepted sensor inventory, not arbitrary identifiers from another instance. The runtime rejects incompatible references before committing the acquisition.

Workspace, loading and accessories

Workspace and device-subclass values belong to GmsIdentificationObservation. Part loading is a GmsLoadingObservation; active-tool monitoring is a GmsToolMonitoringObservation. Accessory kinds use the real released rotary table and exchange-rack types, with their own identification where required.

These are observations, not automatic part loading, tool changing or calibration procedures. Static optional-node shapes are configured before publication. A provider cannot add a new optional property to a fixed published slot merely by changing an untyped payload.

Additional sensors and warnings

GmsAdditionalSensor preserves its identifier, class, engineering units and actual DataValue. These sensors measure environmental or auxiliary conditions not directly involved in the geometrical measurement itself.

GmsSensorWarning binds a retained SensorWarningAlarmType condition to an additional sensor using SensorKey, or an explicit compatible InputNodeId. Its limits, active state, severity, message and acquisition metadata remain provider observations. The warning's input is not a primary probe's qualification countdown.

ErrorCode is also a provider-owned observation: its runtime instance is read-only before registration, regardless of AllowedOperations. Ordinary Value writes return BadNotWritable; providers update it through accepted snapshots and RefreshAsync, without granting a second cache-write path.

Use the separate inherited MachineTool notification provider for unrelated operator alerts. Standard condition methods remain subject to their allowed operations and the server's authorization. Reading or observing a warning does not acknowledge or confirm it.

Calibration observations

GmsCalibrationObservation includes the acquired calibrated state, date of calibration, forecast time and optional interval, preparation time and certificate reference. GmsHandle.CalibrationHistory is a bounded application-side history of accepted observations, not a new historical-access or calibration-execution service.

The client preserves these values and returns null when the optional calibration surface is absent. It does not fetch a certificate URI automatically, derive a calibration result from elapsed host time or command the measuring equipment.

Native production and inherited behavior

GMS jobs and parts extend the retained MachineTool Production model. The provider supplies matching keys across inherited job/program/part-set/part observations and native GmsJobObservation/GmsPartObservation. Native fields include batch identity, duration/remaining time, measurement reason, part descriptions and catalog-backed production metadata.

Catalog observations preserve their declared Number scalar/array representation, enumeration metadata and ValueAsText; they are not normalized through an untyped string or floating-point representation. Rejected acquisition does not commit new job/part indexes.

Inherited production statistics remain implemented. This is distinct from claiming the complete GMS Production facet, whose undefined requirement is documented below. No GMS-specific job execution verbs are introduced.

Results, characteristics and corrections

Per-instance stores and result binding

IGmsResultStore belongs to the model contract assembly and is injectable. It supplies newest/by-ID reads, a bounded newest-first ID list, atomic writes and per-ID acknowledgement results. It must honor cancellation and return immutable snapshots. The same store object cannot back two active GMS bindings.

InMemoryGmsResultStore rejects count/byte overflow instead of silently evicting results. Applications choose retention explicitly with RemoveAsync. Durability, distributed concurrency and equipment/database transactions belong to an application implementation of IGmsResultStore.

GmsResultBindingOptions controls the result subtree separately from system features:

Option Meaning
PublishedResults Fixed ring of native result-variable slots; default 4, zero selects method-only access
IncludeUsedTools Exposes GMSResultType.UsedTools
EnableIntermediateEvents Allows concrete intermediate-result events
EnableFileTransfer Selects the shared temporary-file download path
Corrections Null omits the optional folder; an empty array exposes an explicitly empty folder
Limits Result, metadata, file, stored-byte, characteristic and correction budgets

Selected UsedTools properties are read-only provider provenance on every published result slot, including before its first result is acquired. Ordinary Value writes return BadNotWritable; GmsResultPublisher.PublishAsync remains the update path and retains acquisition quality and timestamps. Unselected properties remain absent, and zero published slots still supports method-only result access.

Default distinction: GmsBuilder.WithResults(store) disables file transfer for the mandatory-only system. A separately constructed new GmsResultBindingOptions() defaults EnableFileTransfer to true. Set that property explicitly when supplying an options object.

Publish an acquired result

GmsResult wraps an acquired DataValue carrying the unchanged Machinery ResultDataType. The envelope must contain ResultMetaData with a nonempty ResultId; optional structured fields need their encoding-mask bits.

using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.GMS.Contracts;
using Opc.Ua.GMS.Server;
using Opc.Ua.Machinery.Result;

static ValueTask PublishInspectionAsync(
    GmsHandle system,
    ResultDataType equipmentResult,
    StatusCode acquisitionQuality,
    DateTimeUtc acquiredAt,
    CancellationToken ct)
{
    var sample = new DataValue(
        Variant.FromStructure(equipmentResult), acquisitionQuality, acquiredAt, acquiredAt);
    return system.Results.PublishAsync(new GmsResult(sample), ct);
}

The provider supplies the real envelope, quality and acquisition time; the server does not invent a successful measurement. The result boundary copies structured metadata, samples and buffers. Stable result-variable slots retain their projection and acquisition metadata when the ring advances, even if the backing store later removes an older entry.

Final results emit a concrete subtype of Machinery's abstract ResultReadyEventType. The concrete type is registered in the application namespace while retaining the standard base relationship.

Intermediate characteristics and tolerances

An intermediate result requires at least one GmsCharacteristic. Its geometric validity and tolerance evaluation are independent of OPC UA quality:

using Opc.Ua;
using Opc.Ua.GMS;
using Opc.Ua.GMS.Contracts;
using Opc.Ua.Machinery.Result;

static GmsResult CreateIntermediateInspection(
    in DataValue resultEnvelope,
    GmsAnalogValue measurement,
    GmsTolerance lower,
    GmsTolerance upper,
    ResultEvaluationEnum evaluation,
    bool geometricallyValid)
{
    return new GmsResult(resultEnvelope,
        characteristics:
        [
            new GmsCharacteristic("Bore")
            {
                Identifier = "BoreDiameter",
                ResultValue = measurement,
                LowerTolerance = lower,
                UpperTolerance = upper,
                Evaluation = evaluation,
                IsValid = geometricallyValid
            }
        ],
        programName: "Inspection",
        isIntermediate: true);
}

resultEnvelope must contain the same required ResultDataType metadata as a final result. GmsResult marks intermediate metadata as IsPartial. Enable intermediate events in the binding before publishing this result.

GmsAnalogValue carries a Double sample and units; a bad/null value represents unavailable acquisition. Every configured GmsTolerance carries its mandatory ToleranceLimitEnum form. Nominal values, importance class and formula are separate optional fields. Neither tolerance evaluation nor IsValid is inferred from a Good/Bad UA status.

Intermediate events derive from IntermediateResultEventType, not the final-result event type. Generated decoding preserves the explicitly selected characteristic placeholders and their qualified names.

Calculated corrections are observations

Each GmsCorrection requires an identifier, a characteristic identifier and at least one relative or absolute GmsAnalogValue. Configure the correction objects through GmsResultBindingOptions.Corrections, then update their observations using system.Results.PublishCorrectionAsync(correction, ct).

Correction values, limits, identifiers and characteristic references are read-only to remote clients on the published instance. Updates use the server-side publisher; ordinary Value writes cannot falsify an observation or bypass its fixed-reference checks.

The configured identity, characteristic reference and optional-node shape remain fixed. Changing them requires rebuilding the instance, not silently retargeting a correction node. Publishing or reading a correction never applies it to a machine.

Queries, metadata handles and files

GMS binds the real GMSResultManagementState through the existing Machinery Result implementation:

  • GetLatestResult, GetResultById and GetResultIdListFiltered return session-bound metadata handles that pin their result.
  • ReleaseResultHandle releases that caller's handle. Session termination reclaims its handles without invalidating another session's results.
  • Queries use the standard ContentFilter and ordered RelativePath list, with the Machinery evaluator's NULL and ordering rules.
  • Result files use GenerateFileForRead and the existing TemporaryFileTransferClient/UaFileStream workflow, not a private protocol.
  • Result acknowledgement returns domain statuses; it does not delete results or start a measurement.

The shared result-transfer path is read-only. Its required write-side methods return the standard refusal statuses instead of pretending to accept uploads. Metadata/file cleanup, cancellation and retained-root deletion use the shared Machinery result lifetime.

Typed client workflows

System reads and optional absence

GmsClient API Workflow
DiscoverAsync Native GMSType and vendor-subtype discovery
ReadSnapshotAsync Identification and inherited/native system values
ReadEquipmentAsync Sensors/multisensors, qualification, loading, tool monitoring and accessories/additional sensors
ReadCalibrationAsync Optional calibration observation; absent surface is null
ReadProductionAsync Native job/part metadata and retained production
GetProxy Generated GMSTypeClient
GetResults Bounded GmsResultClient on the same session

Reads preserve NodeIds, optional presence, numeric types, quality and acquisition timestamps. A null ArrayOf<T> is checked with IsNull, not a CLR nullable wrapper. An absent optional DataValue is distinct from a present bad-quality sample. Missing mandatory structure and service/access errors are not converted to successful empty snapshots.

Separate client service calls are not an atomic equipment-history transaction. Use the returned acquisition metadata and application correlation when a cross-read snapshot is required.

Result access and downloads

GmsResultClient API Ownership or outcome
GetLatestAsync, GetByIdAsync Detached result envelope; call-owned metadata handle released
QueryAsync Bounded standard filter/order query; query handle released
ReadPublishedResultsAsync Original result-slot DataValues and optional UsedTools
ReadCorrectionsAsync Native correction values/units, not commands
DownloadAsync Bounded ByteString or streamed download into a caller-owned destination
AcknowledgeAsync One unchanged domain result per identifier; no implicit retry
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.GMS.Client;

static ValueTask DownloadInspectionAsync(
    GmsResultClient results,
    string resultId,
    Stream destination,
    CancellationToken ct)
{
    return results.DownloadAsync(resultId, destination, ct);
}

The destination stays open; after failure it may contain a partial prefix. The client releases its remote file and metadata resources on success, failure and cancellation. The ByteString overload shares that implementation. GmsResultDomainException represents a nonzero result-method domain error, separately from ServiceResultException and cancellation. When both an operation and its cleanup fail, both errors remain visible.

Bounded observations and generated proxies

ObserveDataAsync, ObserveEventsAsync and ObserveConditionsAsync reuse the MachineTool collector and MachineToolObservationOptions. Windows include setup, are at most 60 seconds, and retain at most 100 notifications within a finite decoded-byte budget. Condition refresh is explicit; observation does not acknowledge/confirm warnings or execute jobs.

GmsResultClient.ObserveIntermediateResultsAsync composes an existing IStreamingSubscription. Select the real, instance-qualified characteristic placeholders through CreateIntermediateResultFilter; do not assume their namespace is the GMS model namespace. The readiness-callback overload signals only after actual monitored-item creation succeeds. The enumerator removes its own monitored item, while the caller retains the streaming subscription.

GetProxy and GmsResultClient.Root expose generated proxies for advanced navigation. Reuse them rather than manually composing unqualified browse paths or low-level Call arrays. Event fields retain their actual values but do not gain invented per-field acquisition timestamps; use result variables for raw sample quality/time.

Lifetime, cancellation and bounds

Coherent refresh and retained publication

The GMS binding prepares native projections within MachineTool's acquire/validate/commit/publish transaction. Rejected observations do not replace accepted sensor, job or part indexes. Observation and CalibrationHistory reflect accepted state.

If notification publication fails after commit, RetryPublicationAsync retries retained publication without reacquiring or repeating equipment effects. It is not an automatic retry of a remote acknowledgement or a physical operation.

Dynamic sensor/production removal observes the final-value publication barrier: the exact required terminal entry must enter a real Publish response before its subtree can be removed. Queuing, equality with an older sample, network delivery and client acknowledgement are different boundaries.

Resource ownership and cleanup

Resource Owner
IGmsProvider, IGmsResultStore, inherited notification/provider services Application; use per-instance selections
GMS root and result binding One MachineTool/Machinery registration
Runtime-created observation and result-transfer resources The corresponding runtime or call
Session used by any GMS facade/factory Application or DI container
Stream passed as a download destination Caller
Streaming subscription passed to intermediate observation Caller; enumerator owns only its item

GmsHandle.DisposeAsync drains the native binding and shared runtime. Deleting the bound root or stopping its manager also releases the registration's resources. Providers must honor cancellation and avoid reentering their own refresh/disposal path.

GmsClient.DisposeAsync cancels and joins its admitted operations and inherited observations without closing a borrowed session. Cancellation is not proof that a submitted acknowledgement was undone; inspect the result and never retry an uncertain mutation implicitly.

Configuration limits

Options Representative defaults
GmsSystemOptions 128 sensors, 128 accessory/additional-sensor slots, 32 calibration-history entries, 128 warning identities, 1,024 array entries
GmsClientOptions 128 native elements, 16,384 examined references, 1 MiB raw sample payload
GmsResultLimits 64 results, 256 KiB metadata, 16 MiB per file, 64 MiB total stored bytes, 64 characteristics/used tools/corrections

Bounds are validated before service work or instance publication. Provider-side, client-side and transport quotas are independent; the most restrictive applicable bound wins. Oversize complete payloads fail rather than silently omit measurements. A result query's explicit maximum is a query selection limit, not unlimited export.

MCP integration

Select or embed the GMS profile

opcua-mcp --profile gms

For embedding, select only the GMS catalog and shared connection tools:

using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Mcp;

static IMcpServerBuilder AddGmsMcp(IServiceCollection services)
{
    services.AddOpcUaMcpCore().AddOpcUaMcpGms();
    return services.AddMcpServer()
        .WithStdioServerTransport()
        .WithOpcUaMcpFilters()
        .WithOpcUaGmsTools(McpToolProfile.Gms);
}

The profile-set overload composes catalogs and deduplicates common connection tools. It borrows host-managed named sessions. Omit sessionName only when exactly one session is connected.

Catalog and explicit acknowledgement

Tool Workflow
gms_list_systems Actual GMS/vendor-subtype inventory
gms_read_system System, Equipment, Calibration, Production or Capabilities
gms_read_results Latest, ById, bounded Query, raw Published slots or Corrections
gms_download_result Bounded inline result bytes; no host-local path
gms_observe Data, Events, explicit retained Conditions, or IntermediateResults
gms_acknowledge_results Standard result acknowledgement with confirmAcknowledgement=true

Passive workflows do not acknowledge results/conditions, fetch calibration certificate URLs, execute measurements/jobs or apply corrections. Acknowledgement is the separate standard state change, with explicit consent and authoritative server permission; per-result domain failures remain visible.

For gms_read_results, native query options belong inside the query object:

{
  "systemNodeId": "ns=12;s=InspectionCell",
  "operation": "Query",
  "query": {"filterField": "JobId", "filterValue": "J-1", "orderBy": "ResultId"},
  "maximumResults": 20,
  "sessionName": "metrology"
}

Use the NodeId actually returned by discovery; the namespace index above is illustrative. This remains a standard Machinery filter, not a new GMS query protocol.

Host policy and bounded replies

Catalog selection is not an authorization grant. Privileged/live host capabilities are independently default-off and require explicit caller grants/network policy when enabled. A confirmation argument does not create OPC UA authority. See MCP execution policy.

The GMS tools cap inventory/results/notifications at 100, observation windows at 60 seconds, downloads at 524,288 bytes and complete UTF-8 replies at 1 MiB (default 128 KiB). Native read limits may be tighter. The MCP query workflow requests one extra identifier to detect a result set that exceeds the requested complete-reply bound.

Unknown/duplicate query properties and incorrectly typed selector values fail before session use. Incomplete or lossy retained-condition refresh is an MCP error, not an apparently complete empty set. Calls release their own subscription, metadata and file resources; cancellation must reach the server through the transport, such as the standard MCP notifications/cancelled notification. No persistent handle or implicit retry is introduced.

See the GMS MCP reference for exact schemas, characteristic selection, bounds and cancellation behavior.

Conformance and support limits

Supported surfaces

Feature Support
OPC 40210 reference 1.0.1 / released model 1.0.0 Implemented
Sensors/multisensors, qualification, workspace, loading, tools and accessories Explicit provider-backed observations
Calibration and additional environmental sensors Monitoring; no physical calibration
Native GMS jobs/parts and inherited production statistics Implemented
Results, characteristics/tolerances, corrections, queries, downloads and events Native types with shared result services; corrections do not actuate
SensorWarningAlarmType Additional environmental-sensor conditions
Standalone/shared hosting, typed clients, bounded MCP and NativeAOT Supported through the same runtime APIs
Complete GMS Production facet Withheld because of the undefined requirement below
Invented measurement-start, calibration-execution or GMS job/correction-control verbs Not supplied
External OPC Foundation certification of a deployment Not claimed

The undefined Production statistics requirement

OPC 40210 section 12.3.2.7 requires GMS_ProductionStatisticsType for the Production conformance unit. The released specification does not define that type or its NodeId, and Annex A.1's normative NodeSet contains no such definition.

The implementation does not fabricate a type, reuse a different type under that name or silently claim the complete Production facet. Defined native GMS jobs/parts and inherited MachineTool statistics remain supported. This is a specific upstream conformance exception, not a statement that all production monitoring is absent.

Instance capabilities versus certification

GmsHandle.Capabilities describes current native conformance units, dependency-qualified server profiles and the precise upstream exception. Contributions follow successfully committed live instances and selected features, not assembly references or namespace presence.

The profile definitions are in OPC 40210 section 12. Endpoint security, certificates, user roles, quotas and actual equipment behavior remain deployment responsibilities. Profiles records repository-wide scope; it is not an external certification claim.

The GMS tests cover native model/proxy identity, equipment and production, result filtering/transfers, quality preservation, warning sources, rejected acquisitions and real-session ownership/cleanup. The test fixtures and runtime capability checks are distinct from certification of a vendor device.

Model provenance

The normative model is pinned to UA-Nodeset revision 96a08945425d0295edb3607d2a38f6841ef44046.

Source Role
Model/opc.ua.gms.nodeset2.xml Original lowercase publication filename and normative wire identities
Opc.Ua.GMS.Derived.NodeIds.csv Locally derived symbolic drift guard for the generator
GmsModel Reference/model versions, namespace closure, source commit and LF-normalized NodeSet SHA-256
Opc.Ua.GMS.csproj Explicit generator namespace, model URI and existing dependency assemblies

The NodeSet supplies numeric identifiers; a derived identifier table is not a replacement specification. Shared MachineTool, Machinery and DI definitions are consumed through their existing assembly payloads. See ModelDependencies and Machinery's identifier-table explanation.