Files
UA-.NETStandard/docs/CranesHoists.md
5899354441 Clarify documentation, migration guidance, and SDK selection (#4823)
# Description

Remove stock phrasing and correct misleading or inconsistent technical
documentation. The initial two passes covered 367 Markdown files,
followed by an interactive review of 169 editorial flags. The U5
interpretation originally deferred in that review has since received the
upstream Mantis answer recorded by #4843; remaining server/spec
follow-ups stay explicit in the CTT log.

- Correct package responsibilities, migration steps, API names,
ownership, protocol descriptions and links. Keep bundled migration
guides synchronized. Preserve historical conclusions as historical
records with separate re-check notes.
- Provide at least two rendered TOC levels throughout `docs/`. Convert
appropriate architecture figures to Mermaid without changing code
examples, transcripts or historical quotations.
- Address the October 19 feedback with concrete setup/migration
instructions, corrected certificate paths and PubSub names, shorter
repeated caveats, consistent XML comments, and a full review of the
Concepts guide. Retain #4839's API corrections.
- Integrate #4841's runtime/performance guidance and review
incoming/recent prose. Preserve dated measurements rather than claiming
new benchmark runs; clarify allocation ratios, startup-only processor
settings, JIT warm-up and the benchmark commit's scope. Missing raw
measurement artifacts and repeat ranges remain tracked by #4842.
- Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log
conflict using the updated Mantis states and OPC answers, retaining
historical framing and re-checks for C28/C32/C43.
- Address the latest naming/index feedback in `adb5bf693`: rename the
guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the
provider and DataType-declaration contracts into the conversion guide
and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and
`samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di,
FluentApi, ISA95, Lds and PubSub; update solution entries, project
references, Docker/CI paths, test fixture paths and documentation links
with exact casing.

**WoT guide scope.** This branch retains the detailed conversion guide
that #4758 had replaced with an anchor stub. Conversion limits, verified
document sets, preservation rules and provider ownership now live
together in `WotNodeSetConversion.md`; binding/runtime guidance remains
in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger
prose was shortened, the oversized argument-table cell moved into its
section, and existing anchors preserved.

**SDK policy is a separate, explicit user-directed change.**
`global.json` sets a stable minimum of `10.0.400`, `rollForward:
latestMajor`, and `allowPrerelease: false`. Newer major SDKs are
permitted; CI's explicit SDK pins are unchanged. README, GettingStarted,
DeveloperGuide and Recycling instructions reflect the possible analyzer
differences. The image-contract regression fix accepts compatible stable
SDK images instead of requiring exact equality with the floor.

The latest four rename/index threads are resolved after publication.
Three older threads remain open: universal obsolete-removal timing,
unconditional nonce-strength wording, and a blanket Vision NativeAOT
claim. Existing implementation/evidence constraints are explained in
their inline replies. The two-level TOCs remain as requested.

## Validation

- `git diff --check` and staged diff checks: passed. The untouched
upstream AutoID NodeSet retains its provenance bytes, including two
existing license-header trailing spaces.
- `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled
migration documents and manifests synchronized.
- Documentation/path validation: **101** documents retain two rendered
TOC levels (**2,228** entries), with idempotent formatting. **52** new
relative links and their anchors passed exact-case checks. No tracked
references to the old guide/sample paths remain. Both relocated
provider-contract sections are preserved in the destination.
- `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects
and the existing FluentApi/Lds/PubSub projects. Updated solution and
pump ProjectReference paths also passed exact-case existence checks.
- Latest focused commands, run from the repository root:

```powershell
# Passed: 61 channel send-order and reconnect tests.
dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal

# Passed: 5 pump datasheet tests; builds the renamed pump sample.
dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal

# Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models.
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal

# Passed: build, zero warnings/errors.
dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal

# Passed: dependent sample-test project build, zero warnings/errors.
dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal
```

Earlier focused integration checks passed 2,620 tests across AutoID,
event queues/scheduling, source generation, MCP/package profiles and
WoT. Both reviewed Mermaid diagrams rendered locally without network
access. These checks are not a full solution run.

The full solution has **not** been rerun for this review. Earlier
`dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did
not pass: three AOT projects rejected VSTest and 20 Vision tests failed
during graphics initialization with `OutOfMemoryException`. Remaining
long-running tests were stopped at the user's request. A focused Vision
rerun passed 558 tests and reproduced those 20 failures. The earlier
`dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was
blocked by the former SDK pin and was not retried after the policy
change. The user approved proceeding with those limitations documented;
current remote CI is not claimed green.

## Related Issues

- Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and
diagnostic-tool documentation refresh. #4841 supplies documentation and
earlier results, not all newly requested measurements.
- #4758 explains the conversion guide's earlier consolidation; the
current provider contracts are now consolidated into
`WotNodeSetConversion.md`.
- #4839 supplies the reviewed API corrections; #4843 supplies the
updated CTT issue record.
- The original prose cleanup and SDK policy were user-requested without
a source issue. No new runtime architecture is introduced by these
documentation edits.

## 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.
- [ ] 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.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Roman Ettlinger <romanett98@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:10:55 +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:

flowchart TD
    devices["DeviceSet"] --> system["CraneMotionDeviceSystem"]
    system --> controllers["Controllers<br/>Software and task controls"]
    system --> safety["SafetyStates"]
    system --> motions["MotionDevices"] --> motion["CraneMotionDevice"]
    motion --> axes["Axes"] --> axis["CraneAxis"]
    motion --> power["PowerTrains<br/>Motors referenced by the axes"]
    system --> protection["ProtectiveFunctions<br/>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.