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

37 KiB

Cranes and Hoists

The Opc.Ua.CranesHoists* libraries implement the released OPC 40020-1 Cranes and Hoists 1.00 model using the existing Robotics, Machinery and Device Integration infrastructure. The generated crane types retain their actual Robotics inheritance and native topology; the runtime is not a parallel collection of generic objects.

All CranesHoists model, server, client and MCP packages remain preview-only, including Debug variants. A released information model does not imply production certification of these packages or a particular crane.

This is provider-backed monitoring and explicit standard limit configuration, not physical motion control, a safety controller or a way to bypass protection. An accepted request is distinct from a provider-reported active hardware limit.

Contents

Library layout

These are assembly/project names. NuGet package IDs prepend OPCFoundation.NetStandard., for example OPCFoundation.NetStandard.Opc.Ua.CranesHoists.Client.

Library Responsibility
Opc.Ua.CranesHoists Generated model states, identifiers, loaders, proxies and shared signal/provider/snapshot contracts
Opc.Ua.CranesHoists.Server Fluent crane construction, coherent acquisition, limit authorization/acceptance and shared-owner hosting
Opc.Ua.CranesHoists.Client Bounded discovery, native topology/quality reads, observations and standard configuration requests
Opc.Ua.Mcp.CranesHoists Six bounded MCP workflows using host-owned named sessions

Model/server/client target .NET Framework 4.8 and .NET 8, 9 and 10. MCP targets .NET 8, 9 and 10. All four package families and their .Debug variants are forced preview; this does not change the repository's root version or Robotics' independent package policy. The typed client has no server-runtime dependency. NativeAOT sample applications use the same public APIs.

Models and dependencies

Version and namespaces

Item Value
Specification/model OPC 40020-1, 1.00
Publication date 2023-09-01
Wire namespace http://opcfoundation.org/UA/CranesHoists/
Generated CLR namespace Opc.Ua.CranesHoists
Dependencies UA core, DI, Machinery and Robotics, reusing their existing model assemblies
Metadata CranesHoistsModel.NamespaceUri, Version, SourceCommit, NormalizedNodeSetSha256

Use generated identifiers and namespace-qualified proxies; a namespace index is assigned by the actual server, not fixed by the companion specification. The model's published dependency versions remain recorded in the NodeSet. They are not a request to generate duplicate copies of DI, Machinery or Robotics beside the repository's current compatible assemblies.

Native Robotics topology

The native root is CraneMotionDeviceSystemState, derived from the Robotics system type. Motion devices, axes and motors similarly retain the generated CraneMotionDeviceState, CraneAxisState and CraneMotorState types. Controllers, task controls, software and safety states reuse Robotics.

Conceptually, one system contains:

DeviceSet
  CraneMotionDeviceSystem
    Controllers -> software and task controls
    SafetyStates
    MotionDevices
      CraneMotionDevice
        Axes -> CraneAxis
        PowerTrains -> motors referenced by the axes
    ProtectiveFunctions -> ProtectsAxis references

Machinery identification and optional Components/Notifications bindings attach to those same nodes. The Machines discovery path organizes the registered system; it does not relocate or register the DeviceSet root a second time. Physical composition and protection references remain distinct from ownership of nodes in the Robotics folders.

Quick start - server

Register one mandatory-only crane

This host-configuration helper receives the application-owned provider and retains the runtime for subsequent refresh:

using System;
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.CranesHoists;
using Opc.Ua.CranesHoists.Server;
using Opc.Ua.Server.Hosting;

static IOpcUaServerBuilder AddCraneServer(
    IServiceCollection services,
    ICraneDataProvider provider,
    Action<CraneSystemHandle> retainRuntime)
{
    return services.AddOpcUa()
        .AddServer(options =>
        {
            options.ApplicationName = "CranesHoistsServer";
            options.ApplicationUri = "urn:example:cranes-server";
            options.EndpointUrls.Add("opc.tcp://localhost:62557/CranesHoistsServer");
        })
        .AddCranesHoists(options =>
            options.InstanceNamespaceUri = "urn:example:cranes:instances")
        .ConfigureCranesHoists(async (context, ct) =>
        {
            CraneSystemHandle crane = await context.AddCraneSystem("Crane1")
                .WithProvider(provider)
                .WithIdentification(identity =>
                {
                    identity.Manufacturer = new LocalizedText("Example manufacturer");
                    identity.SerialNumber = "Crane1";
                    identity.ProductInstanceUri = "urn:example:crane:1";
                })
                .AddController("Controller", controller =>
                {
                    controller.AddSoftware("ControllerSoftware");
                    controller.AddTaskControl("Main");
                })
                .AddSafetyState("Safety")
                .AddDevice("Hoist", CraneMotionDeviceCategoryEnum.HOIST, device =>
                    device.WithIdentification(identity =>
                    {
                        identity.Manufacturer = new LocalizedText("Example manufacturer");
                        identity.SerialNumber = "Hoist1";
                        identity.ProductInstanceUri = "urn:example:hoist:1";
                    }).AddAxis("Lift", axis => axis.AddMotor("LiftMotor")))
                .BuildAsync(ct);
            retainRuntime(crane);
        });
}

This is the minimum useful native topology: a system identity/provider, controller with software/task control, safety state, identified device, axis and motor. Setup validates selected providers and acquires the required samples before exposing the tree.

Register it during host construction and await normal host startup/shutdown. Use the existing server hosting and certificate framework for endpoint policies, trust and identities. A model builder is not an authorization or safety policy. Do not dispose the handle at the end of the configuration callback.

Supply explicit samples

The provider uses the component IDs chosen by the builder. The previous mandatory-only example requires these telemetry addresses:

Component ID Signals
Crane1 OperationalMode, SystemOn
Hoist SpeedOverride
LiftMotor MotorTemperature
Safety EmergencyStop, ProtectiveStop, SafetyOperationalMode

An explicit unavailable-data simulation can supply those addresses without inventing successful equipment or safety observations:

using Opc.Ua;
using Opc.Ua.CranesHoists;
using Opc.Ua.CranesHoists.Server;

static SimulatedCraneProvider CreateUnavailableCraneProvider()
{
    var unavailable = new DataValue(Variant.Null, StatusCodes.BadWaitingForInitialData);
    return new SimulatedCraneProvider(
    [
        new CraneSample(new("Crane1", CraneSignal.OperationalMode), unavailable),
        new CraneSample(new("Crane1", CraneSignal.SystemOn), unavailable),
        new CraneSample(new("Hoist", CraneSignal.SpeedOverride), unavailable),
        new CraneSample(new("LiftMotor", CraneSignal.MotorTemperature), unavailable),
        new CraneSample(new("Safety", CraneSignal.EmergencyStop), unavailable),
        new CraneSample(new("Safety", CraneSignal.ProtectiveStop), unavailable),
        new CraneSample(new("Safety", CraneSignal.SafetyOperationalMode), unavailable)
    ]);
}

For real equipment, implement ICraneDataProvider and return the complete selected acquisition with its true wire types, quality and source timestamps. For a simulation, SetSamplesAsync replaces the supplied report; call CraneSystemHandle.RefreshAsync to acquire and publish it. SimulatedCraneProvider never generates movement or active limit changes.

Actual-position capability is not selected in the minimal example. The native position variable remains explicitly unavailable with BadNoData, rather than pretending the axis is at zero.

Quick start - client

Discover and read a system

The typed client borrows a connected session and follows actual crane subtypes:

using System;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.CranesHoists;
using Opc.Ua.CranesHoists.Client;

static async ValueTask ReadCranesAsync(
    ISession session, ITelemetryContext telemetry, CancellationToken ct)
{
    CranesHoistsClient client = session.CranesHoists(telemetry);
    ArrayOf<CraneEntry> cranes = await client.DiscoverAsync(ct);
    for (int index = 0; index < cranes.Count; index++)
    {
        CraneEntry crane = cranes[index];
        CraneSystemSnapshot snapshot = await client.ReadSystemAsync(crane.NodeId, ct);
        Console.WriteLine($"{crane.BrowseName}: {snapshot.Elements.Count} crane elements");
        foreach (CraneSignalValue signal in snapshot.System.Signals)
        {
            if (!signal.NodeId.IsNull)
            {
                Console.WriteLine($"{signal.Signal}: {signal.Value.StatusCode}, " +
                    $"acquired {signal.Value.SourceTimestamp}");
            }
        }
    }
}

The default discovery entry point is DI DeviceSet. The overload taking a root NodeId can use Machines or another application organizer. A missing model returns an empty discovery result, but SupportsModel=true by itself does not prove there is a complete crane instance.

CranesHoistsClient is not a disposable session owner. Its bounded observations own their own resources, and the application/container continues to own the underlying session.

Use the client factory

AddCranesHoistsClient(...) extends an existing AddOpcUa().AddClient(...) registration. Resolve CranesHoistsClientFactory and await CreateAsync(ct) to use the container-managed connection. The factory and direct constructor both borrow that connection.

CranesHoistsClientOptions configures topology/browse bounds. Direct session.CranesHoists(telemetry, options) uses the same workflows. Closing an independently created session belongs to its creator, not to a crane facade. See client hosting.

Server hosting and composition

Standalone owner

AddCranesHoists() creates one Machinery owner with BuildingBlocks. Its options are the existing MachineryServerOptions, including the application-owned instance namespace. ConfigureCranesHoists receives a CranesHoistsBuildContext containing both Robotics construction and Machinery binding contexts.

The hosting callback awaits construction and seals the Robotics context after configuration. Multiple crane systems use independent IDs/providers and the same validated construction path.

Existing Machinery owner

When the application already has a Machinery owner, add only the crane models:

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

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

Omit the AddMachinery call if that owner already exists. Configure with ConfigureCranesHoistsFor<MachineryNodeManager>(...). Other families can contribute their model closure to the same owner; they must not register a second DI/Machinery root. The CompanionSuite sample demonstrates cranes with MachineTool, WMTP, GMS and Machine Vision.

Existing Robotics or custom owner

For an existing Robotics owner, use AddCranesHoistsToRobotics() and ConfigureCranesHoistsFor<RoboticsNodeManager>(...). The hosting extension uses the Robotics instance namespace for the shared Machinery bindings. Do not call standalone AddCranesHoists() beside it.

Direct construction takes new CranesHoistsBuildContext(robotics, machinery, timeProvider). The contexts must use the same manager and instance namespace; otherwise construction is rejected. The model closure must already be loaded and Machinery BuildingBlocks enabled. Direct callers retain responsibility for the normal Robotics context sealing/lifetime.

The root is registered once through Robotics. Machinery participates using BindMachine and registration participants, preserving the existing identification and DeviceSet parent/reference. See Machinery external-root bindings for the shared mechanism, rather than adopting the root again with a second AddMachine(...).BuildAsync().

Builder reference

System, controller and safety structure

CraneSystemBuilder is single-use. Component IDs are system-local, must be unique and contain 1-128 non-whitespace characters; a system is bounded to 128 component identities.

System builder call Effect
WithProvider Explicit per-system acquisition adapter; borrowed by default
WithIdentification Mandatory Machinery nameplate on the existing generated root
AddController Existing Robotics controller builder, including software and task controls
AddSafetyState Inherited safety-state observations supplied by the provider
WithExternalControl Reports external-control request state; does not acquire control
WithGlobalSpeedLimit Selects the standard global limit-enable request; requires limit provider/authorizer
WithFunctionalGroups Operational/Configuration references to existing ParameterSet variables
WithComponents Machinery Components discovery references
WithEvents Shared Machinery Notifications publisher

Mandatory controllers and safety states are not automatically fabricated. Reporting their values is not an implementation of the physical safety functions they describe.

Motion devices and physical composition

AddDevice(id, category, configure) creates a native crane device using CraneMotionDeviceCategoryEnum. Each device needs identification and at least one axis.

WithWorkingPeriod selects the provider-reported designed working period. WithComponents selects the Components discovery AddIn. WithPhysicalComponent(childId) references another device in this same system without moving it out of the Robotics MotionDevices folder.

Physical composition rejects unknown/self references, duplicate physical parents and cycles. These relationships are not a second node ownership graph. Use real native relationships instead of reconstructing a physical hierarchy from display names.

Axes, motors, loads and brakes

Builder Call Meaning
Axis WithPositionCapability(available, profile) Static availability/profile; an available position requires a real provider sample
Axis WithAxisFunctions Discrete axis-function telemetry and the nine standard EnumValues
Axis WithMovingState Provider-reported IsMoving
Axis WithRatedSpeed(value, units) Positive finite nominal speed and units
Axis WithSpeedLimits Directional request properties and separate active-limit telemetry
Axis WithActualLoad(configure) Mass telemetry, with optional native Robotics load geometry
Axis AddMotor Native motor in the device's standard power-train topology
Axis AddBackupBrake Native backup brake with acquired BrakeReleased
Motor WithOverheated, WithServiceBrake Selected overtemperature/service-brake observations
Motor WithIdentification Optional shared component nameplate

Every axis needs at least one motor. Optional load configuration uses the generated LoadState and ISystemContext; center-of-mass/inertia retain their native structured data types rather than an untyped JSON substitute. The provider must supply the telemetry implied by every selected surface.

Enabling WithSpeedLimits without WithLimitRequests on the containing system is a configuration error, not a read-only fallback. Additional brake or motor nodes do not introduce actuation methods.

Protective functions

AddProtectiveFunction(id, protectedAxes, configure) creates the native protection object and forward ProtectsAxis relationships. Axis references must be distinct and belong to this system.

WithDescription(mode, name, rampDown) configures descriptive properties. WithDirections(minusSlowdown, minusStop, plusSlowdown, plusStop) declares directional capabilities. WithStateMonitoring() selects coherent Enabled and Active observations; these are not commands.

The library does not disable a protection, perform a safety stop or infer that a safety device is healthy from its mere presence. Descriptions/capability flags and provider-reported activity remain separate.

Provider contracts and coherent publication

Acquisition identity and sample quality

ICraneDataProvider.ReadAsync(ct) returns a complete ArrayOf<CraneSample> for one crane. Every sample has a CraneSignalAddress(componentId, signal) and a full DataValue. CraneSystemHandle.TelemetrySignals exposes the exact required addresses after construction.

Supply each configured telemetry address once. Missing, duplicate or incompatible samples fail validation. A bad/null sample explicitly represents unavailable acquisition and preserves the distinction from a missing signal. Do not replace stale/bad safety or position observations with default Good Booleans or zero positions.

Provider graphs are per-instance. Reusing a component ID in another crane is valid, but sharing its mutable provider or request history implicitly is not. Real equipment, high availability and distributed state belong to the application's provider design.

Static capability versus telemetry

CraneSignal is an SDK selector, not a new wire enumeration. The definition supplies static capabilities such as motion category, position availability and protection direction support. Those fields are not included in provider telemetry.

Accepted limit request properties are also not telemetry. Return active SpeedLimitDirMinus/SpeedLimitDirPlus observations from the data provider, not the requested setpoints. The runtime and client keep both identities.

The standard wire types remain important: limit setpoints are Double, enable requests are Boolean, and enumerated/structured readings retain their declared types. A convenient generic variant does not permit an arbitrary type conversion.

Refresh and validation failures

CraneSystemHandle.RefreshAsync(ct) acquires a report, validates it completely and commits the coherent set before publication. A rejected report leaves the previous accepted signals unchanged. Equipment callbacks do not run while the publication lock is held.

ReadSignal(address) returns the last complete acquired DataValue. GetSignal(address) and GetElement(componentId) expose actual native nodes for advanced integration; they do not transfer node ownership. PublishEventAsync uses the selected Machinery event publisher.

SimulatedCraneProvider.SetSamplesAsync atomically replaces only the supplied simulation report. Its accepted-request history is separate, bounded and never used to rewrite active hardware telemetry. It is a single-host reference adapter, not a physical crane implementation.

Explicit limit requests

Select providers and authorization

Selected limit requests require both:

Contract Responsibility
ICraneWriteAuthorizer.AuthorizeAsync Decide whether the actual OPC UA caller may submit the specific request
ICraneLimitProvider.RequestAsync Explicitly accept/refuse the request; honor cancellation and equipment policy

Bind them with CraneSystemBuilder.WithLimitRequests(provider, authorizer). They are not inferred merely because an acquisition provider exists. SimulatedCraneProvider can record accepted requests only when explicitly configured to accept them, and an independent authorizer is still required.

The standard writable selection is deliberately narrow:

Signal Target Input
SpeedLimitDirMinusSetpoint, SpeedLimitDirPlusSetpoint Axis Finite Double, inclusive range 0-100
SpeedLimitEnabledDirMinus, SpeedLimitEnabledDirPlus Axis Boolean
SpeedLimitGlobalEnabled System Boolean

The server's role/access checks and authorizer remain authoritative even when a client reports Writable=true. No arbitrary signal becomes writable because it is present in a snapshot.

Requested values versus active limits

A successful request means provider acceptance, not a new active limit. The accepted requested value is reported separately from the data provider's active SpeedLimitDirMinus/SpeedLimitDirPlus. The provider can continue to report a different active value after acceptance.

Neither SystemOn, ExternalControlRequest nor a protective-function state is a control-takeover or safety-bypass command. If cancellation or disconnection occurs after submission, the client may not know the request's outcome. Inspect request and active values before deciding what to do; do not retry an uncertain write automatically.

Typed client workflows

Snapshots, metadata and relationships

API Result
DiscoverAsync() Crane roots from DeviceSet
DiscoverAsync(root, ct) Crane roots below an explicit organizer
ReadSystemAsync Bounded system plus actual device/axis/motor/brake/protection snapshots
ReadElementAsync One native element, its signals, identification and relationships
System(nodeId), Axis(nodeId) Generated CraneMotionDeviceSystemTypeClient / CraneAxisTypeClient proxies
ConfigureLimitAsync One standard request-property write, with original UA status

CraneEntry.TypeDefinition preserves a vendor subtype rather than replacing it with a standard base. CraneSignalValue.NodeId.IsNull identifies optional absence; a non-null signal with bad quality is still present. Writable reflects current-user access, not permission to ignore provider policy.

Snapshots retain actual axis-function EnumValues and forward physical-component or protection relationships. Inventory reads stop at nested crane-system boundaries instead of merging two instances into one report. Use generated proxies for advanced namespace-correct access rather than manually assembling raw Call/Write messages.

Bounded values and events

using System;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.CranesHoists;
using Opc.Ua.CranesHoists.Client;

static ValueTask<CraneObservation> ObserveCraneSignalsAsync(
    CranesHoistsClient client, ArrayOf<NodeId> signalIds, CancellationToken ct)
{
    return client.ObserveValuesAsync(signalIds, new CraneObservationOptions
    {
        Window = TimeSpan.FromSeconds(5),
        MaximumNotifications = 20,
        MaximumPayloadBytes = 256 * 1024
    }, ct);
}

Use real signal NodeIds returned by snapshots, not their containing object IDs. The native observation API accepts a bounded distinct selection. ObserveEventsAsync observes an explicitly event-enabled system or notifier; it does not perform condition refresh, acknowledgement or confirmation.

CraneObservation returns raw value notifications, event identity/fields, qualified field paths, LimitReached and DroppedNotifications. The window includes setup and is at most 60 seconds, with at most 100 retained notifications and 1 MiB decoded payload. Default window/count are five seconds and 50. A window ending without events is not proof of a safety state.

Topology discovery is separately bounded by CranesHoistsClientOptions: 256 visited objects, 32 references per requested browse page and 64 pages per browse by default. Exceeding the bound fails, rather than returning a silently truncated topology.

Configuration through the typed client

Only explicitly selected standard limit requests are accepted by the helper:

using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.CranesHoists;
using Opc.Ua.CranesHoists.Client;

static async ValueTask<CraneElementSnapshot> RequestAndInspectLimitAsync(
    CranesHoistsClient client, NodeId axisId, CancellationToken ct)
{
    await client.ConfigureLimitAsync(
        axisId, CraneSignal.SpeedLimitDirPlusSetpoint, Variant.From(40d), ct);
    return await client.ReadElementAsync(axisId, ct);
}

This submits one authorized configuration request, then reads the axis. Inspect the distinct request and active-limit signals in the returned snapshot; the method does not assert that the active value became 40. The runtime enforces the range and target type even if the caller uses the generated proxy or a generic OPC UA Write.

Lifetime and cancellation

Runtime, provider and node ownership

Robotics owns registration of the native tree. Machinery enlists identification, discovery and resource bindings in that registration rather than adopting the root a second time. Setup failures roll back the owner's attempted tree and its resources.

WithProvider(provider) leaves provider ownership with the application. Use ownsProvider: true only for an independently owned provider transferred to this runtime; do not transfer an adapter shared with another crane. The transfer applies when runtime construction starts, not merely when the builder method is called.

CraneSystemHandle.DisposeAsync stops admission, cancels/drains callbacks and releases runtime-owned resources. RemoveAsync additionally asks the existing owner to remove the native tree. Deleting the root or stopping its owner also drains the bound runtime. These lifecycle operations are not physical stop commands and do not replace equipment safety logic.

Session and observation ownership

The client and its factory always borrow the session. One-shot observation owns the subscription/items it creates and removes them on completion, error or cancellation; it does not delete an application's unrelated subscription. There is no persistent observation handle to close through a separate private protocol.

Providers must honor cancellation and complete owned work so shutdown can drain. A local cancellation after a submitted limit request does not establish whether the equipment accepted it. The host/application closes its own session independently; a crane read or bounded observation does not do so.

MCP integration

Select or embed the crane profile

The host profile is craneshoists:

opcua-mcp --profile craneshoists

For embedding:

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

static IMcpServerBuilder AddCraneMcp(IServiceCollection services)
{
    services.AddOpcUaMcpCore().AddOpcUaMcpCranesHoists();
    return services.AddMcpServer()
        .WithStdioServerTransport()
        .WithOpcUaMcpFilters()
        .WithOpcUaCranesHoistsTools(McpToolProfile.CranesHoists);
}

The profile-set overload supports composition and deduplicates shared connection tools. The package remains preview-only and depends on the typed client rather than the server runtime. Each call uses an existing host-managed named session; select sessionName explicitly when connection choice is ambiguous.

Catalog and data representation

Tool Workflow
craneshoists_list_systems Subtype-aware discovery from DeviceSet or a selected organizer
craneshoists_read_system Bounded native topology, observations and selected capabilities
craneshoists_read_element One element, nameplate, axis metadata and relationships
craneshoists_observe_values One bounded observation of distinct signal nodes
craneshoists_observe_events One bounded event observation without refresh/acknowledgement
craneshoists_configure_limit One explicitly consented standard limit request

Present signals include their actual NodeId, current-user access and native UA JSON reading.DataValue. That representation preserves numeric/structured types, status and source/server timestamps. present=false denotes optional absence, not a failed read. Service and access failures are MCP errors with the original UA status.

craneshoists_configure_limit requires confirmConfiguration=true. For an axis NodeId returned by discovery, signal="SpeedLimitDirPlusSetpoint" and a finite numeric value submit that request only. Its receipt distinguishes requestAccepted=true from activeValueVerified=false. It never silently verifies or changes a physical limit on the caller's behalf.

Default-off authority and request bounds

Selecting craneshoists or full selects a catalog, not authority. Privileged/live host capabilities remain independently default-off; enabled capabilities require the configured caller grants and network rules. Per-call confirmation does not bypass those policies, OPC UA access checks or the crane's ICraneWriteAuthorizer. See MCP execution policy.

Discovery defaults to 50 systems and caps at 100. Value observations select 1-64 distinct nodes; time/count ceilings are 60 seconds and 100 notifications. Decoded and serialized reply budgets are separate, with a 1 MiB ceiling. Oversize complete replies fail rather than claiming partial success.

Observation reports bound completion and loss, keeps event IDs/qualified field order and releases its own subscription. Cancellation must reach the server through the standard MCP cancellation notification or the embedding CancellationToken; abandoning a local wait is not necessarily a remote cancel. No automatic retry of an uncertain configuration write is performed.

The crane MCP reference contains exact selectors, JSON types, bounds and embedding contracts.

Conformance and support limits

Supported surfaces

Feature Support
Released OPC 40020-1 1.00 model and provider-backed runtime Implemented through native generated types
Controllers, motion devices, axes, motors, brakes, loads and protective functions Shared Robotics topology with explicit optional selection
Raw values, quality/timestamps, geometry and equipment references Retained in runtime and typed-client snapshots
Standard speed-limit setpoint/enable requests Explicit authorization and provider acceptance; not active-state confirmation
Standalone/shared Machinery-Robotics hosting and isolated instances Same construction/runtime path
Typed clients, bounded MCP, samples and NativeAOT Supported through the same APIs
Model/server/client/MCP packages and Debug variants Preview only
Physical motion, safety-system implementation, control takeover or safety bypass Not supplied
External OPC Foundation certification of a deployment Not claimed

Runtime profile contribution

The runtime contributes the released http://opcfoundation.org/UA-Profile/CranesHoists/Server/Basic profile for successfully committed live systems. Conformance units reflect actual native system/device/axis/motor types and selected brake, protection, enum and ProtectsAxis surfaces. Disposed systems withdraw their contribution.

The source of the URI is the released OPC 40020-1 profiles section, not an invented package-specific profile. Namespace presence and a read of static capabilities do not prove authorization, equipment behavior, safety integrity or certification. See Profiles for repository-wide qualifications.

The CranesHoists tests exercise native types and relationships, mandatory/optional validation, atomic rejected reports, requested-versus-active limits, real TCP clients and isolated ownership. They do not implement or certify a safety function.

Model provenance

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

Source Role
Opc.Ua.CranesHoists.NodeSet2.xml Original released NodeSet and authoritative numeric identifiers
Opc.Ua.CranesHoists.NodeIds.csv Locally derived generator drift table, not a separate upstream publication
CranesHoistsModel Version, source revision and LF-normalized NodeSet SHA-256
Opc.Ua.CranesHoists.csproj Explicit namespace/model URI and existing Robotics/Machinery/DI references

The generator consumes dependency payloads from those assemblies rather than duplicating their model inputs. Numeric wire identities come from the NodeSet; the CSV guards symbolic drift. See ModelDependencies and Machinery's model-source conventions.