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

951 lines
45 KiB
Markdown

# Machine Tools and Wireless Peripherals
The `Opc.Ua.MachineTool*` libraries implement released
[OPC 40501-1 MachineTool](https://reference.opcfoundation.org/specs/OPC-40501-1)
and [OPC 40505 Wireless Machine Tool Peripherals](https://reference.opcfoundation.org/specs/OPC-40505)
(WMTP). They follow the same model, fluent server-builder and typed-client
approach as [Machinery](Machinery.md) and [Machine Vision](Vision.md).
WMTP is bundled in the MachineTool model, server, client and MCP packages and
in this guide. It retains its own wire and CLR namespaces. A wireless-only
server does not need a `MachineToolType` instance.
These libraries expose equipment observations and explicitly bound standard
requests. They are not CNC executors, radio/pairing drivers, firmware installers
or equipment safety systems.
## Contents
- [Library layout](#library-layout)
- [Models and dependencies](#models-and-dependencies)
- [Versions and namespaces](#versions-and-namespaces)
- [Package dependencies versus loaded models](#package-dependencies-versus-loaded-models)
- [Quick start - server](#quick-start---server)
- [Register a mandatory-only machine](#register-a-mandatory-only-machine)
- [Publish an acquired observation](#publish-an-acquired-observation)
- [Quick start - client](#quick-start---client)
- [Discover and read typed instances](#discover-and-read-typed-instances)
- [Use a container-managed connection](#use-a-container-managed-connection)
- [Server hosting and ownership](#server-hosting-and-ownership)
- [Standalone hosting](#standalone-hosting)
- [Compose into an existing owner](#compose-into-an-existing-owner)
- [Custom owners and generated subtypes](#custom-owners-and-generated-subtypes)
- [MachineTool builder reference](#machinetool-builder-reference)
- [Mandatory structure and monitoring](#mandatory-structure-and-monitoring)
- [Tools, working units and life](#tools-working-units-and-life)
- [Retained Production](#retained-production)
- [Machinery and ISA-95 job management](#machinery-and-isa-95-job-management)
- [FileSystem and WorkMasters](#filesystem-and-workmasters)
- [Prognoses and notifications](#prognoses-and-notifications)
- [Wireless peripheral server](#wireless-peripheral-server)
- [Build an independent peripheral](#build-an-independent-peripheral)
- [Measurement kinds and acquired values](#measurement-kinds-and-acquired-values)
- [Trigger and delta configuration](#trigger-and-delta-configuration)
- [Work and service records](#work-and-service-records)
- [Calibration, device time and byte transfer](#calibration-device-time-and-byte-transfer)
- [Typed client workflows](#typed-client-workflows)
- [Machine monitoring, production and services](#machine-monitoring-production-and-services)
- [Bounded data and event observation](#bounded-data-and-event-observation)
- [Wireless measurements and configuration](#wireless-measurements-and-configuration)
- [Wireless record queries](#wireless-record-queries)
- [Lifetime, cancellation and failure handling](#lifetime-cancellation-and-failure-handling)
- [Server transactions and dynamic removal](#server-transactions-and-dynamic-removal)
- [Client and provider ownership](#client-and-provider-ownership)
- [MCP integration](#mcp-integration)
- [Select or embed the bounded profile](#select-or-embed-the-bounded-profile)
- [Passive and explicitly mutating tools](#passive-and-explicitly-mutating-tools)
- [Authorization, budgets and cancellation](#authorization-budgets-and-cancellation)
- [Conformance and support limits](#conformance-and-support-limits)
- [Supported surfaces](#supported-surfaces)
- [Runtime claims and deployment requirements](#runtime-claims-and-deployment-requirements)
- [Model provenance](#model-provenance)
- [Samples and related documentation](#samples-and-related-documentation)
## Library layout
The names below are assembly/project names. The corresponding NuGet package ID
is `OPCFoundation.NetStandard.` followed by that name; for example,
`OPCFoundation.NetStandard.Opc.Ua.MachineTool.Client`.
| Library | Responsibility |
| --- | --- |
| `Opc.Ua.MachineTool` | Both generated models, node states, identifiers, loaders, client proxies and shared provider/snapshot contracts |
| `Opc.Ua.MachineTool.Server` | Fluent machine/peripheral builders, acquisition and publication, provider-backed methods and hosting extensions |
| `Opc.Ua.MachineTool.Client` | Subtype-aware discovery, bounded reads/observations, generated service navigation and WMTP workflows; no server-runtime dependency |
| `Opc.Ua.Mcp.MachineTool` | The combined MachineTool/WMTP MCP catalog over host-owned named sessions |
The first three libraries target .NET Framework 4.8 and .NET 8, 9 and 10.
The MCP library targets .NET 8, 9 and 10. They follow the repository's normal
root package-version policy, including `.Debug` variants; they are not
independently forced to preview. The released specification version is distinct
from the NuGet version. NativeAOT application examples use the same APIs.
## Models and dependencies
### Versions and namespaces
| Model | Version | Publication date | Wire namespace | CLR namespace |
| --- | --- | --- | --- | --- |
| MachineTool, OPC 40501-1 | `1.02.0` | 2024-11-01 | `http://opcfoundation.org/UA/MachineTool/` | `Opc.Ua.MachineTool` |
| Wireless Machine Tool Peripherals, OPC 40505 | `1.0.0` | 2025-11-01 | `http://opcfoundation.org/UA/WMTP/` | `Opc.Ua.MachineTool.WMTP` |
Use namespace URIs, generated identifiers and proxies rather than hard-coded
namespace indexes. `MachineToolModels.GetNamespaceUris(parts)` gives the
selected non-core closure in dependency order.
`AddMachineToolTypeSystem` loads that closure into an existing node collection
or registers its encodeable factories; the caller commits a factory builder.
Model loading alone does not instantiate a machine or grant any capability.
### Package dependencies versus loaded models
The single model package references the union of both dependency graphs, but
runtime model selection remains independent:
| `MachineToolParts` | Runtime closure beyond UA core |
| --- | --- |
| `MachineTool` (default) | DI, IA, Machinery building blocks, ISA-95 Job Control V2, Machinery Jobs and MachineTool |
| `Wmtp` | DI, IA, Machinery building blocks, IRDI, PA-DIM, Machinery ProcessValues and WMTP |
| `All` | Both closures, deduplicated |
The owner needs Machinery `BuildingBlocks`, plus `Jobs` for MachineTool or
`ProcessValues` for WMTP. Standalone registration selects those parts.
Referencing the WMTP-containing assembly does not cause a MachineTool-only
endpoint to expose WMTP, nor does a WMTP-only endpoint require ISA-95 jobs.
The retained MachineTool `Production` declarations are still present in 1.02.0,
even where deprecated in favor of Machinery Jobs. They are implemented rather
than removed: older clients and [GMS](GMS.md) still depend on their wire identity.
## Quick start - server
### Register a mandatory-only machine
The following host-configuration helper creates one machine on the standard
Machinery owner. `provider` is this machine's application-owned acquisition
adapter; `retainRuntime` stores the returned handle in the application's
acquisition coordinator. It should not dispose the handle at callback exit.
```csharp
using System;
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.MachineTool;
using Opc.Ua.MachineTool.Server;
using Opc.Ua.Server.Hosting;
static IOpcUaServerBuilder AddMillServer(
IServiceCollection services,
IMachineToolProvider provider,
Action<MachineToolRuntime<MachineToolState>> retainRuntime)
{
return services.AddOpcUa()
.AddServer(options =>
{
options.ApplicationName = "MachineToolServer";
options.ApplicationUri = "urn:example:machinetool-server";
options.EndpointUrls.Add("opc.tcp://localhost:62558/MachineToolServer");
})
.AddMachineTool()
.ConfigureMachineTool(async (context, ct) =>
{
MachineToolRuntime<MachineToolState> machine = await context
.AddMachineTool("Mill")
.WithProvider(provider)
.WithIdentification(identity =>
{
identity.Manufacturer = new LocalizedText("Example manufacturer");
identity.SerialNumber = "Mill-001";
identity.ProductInstanceUri = "urn:example:mill:001";
})
.BuildAsync(ct);
retainRuntime(machine);
});
}
```
Register this while building the application host, then await normal host
startup and shutdown. Configure application certificates, trusted peers,
endpoint policies and user authentication through the existing
[server hosting](DependencyInjection.md#server-feature) and
[certificate infrastructure](CertificateManager.md).
No trust bypass or embedded credential is needed by the builder.
`BuildAsync` acquires and validates the provider snapshot before publishing the
instance. Missing identification, missing providers, invalid selected
capabilities or conflicting identities fail setup; they do not leave an
advertised half-machine. The initial default snapshot explicitly marks
unavailable acquisitions with bad quality.
### Publish an acquired observation
`IMachineToolProvider.ReadAsync` returns an immutable `MachineToolSnapshot`.
The application decides when to acquire and refresh; a client read is not an
instruction to run a program.
For a mandatory-only simulation, create an
`InMemoryMachineToolProvider(new MachineToolSnapshot())` and later publish
an explicitly supplied observation:
```csharp
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.MachineTool;
using Opc.Ua.MachineTool.Server;
static async ValueTask PublishMillObservationAsync(
InMemoryMachineToolProvider provider,
MachineToolRuntime<MachineToolState> machine,
MachineOperationMode observedMode,
DateTimeUtc acquiredAt,
CancellationToken ct)
{
await provider.SetSnapshotAsync(new MachineToolSnapshot
{
Operation = new MachineToolOperation
{
Mode = observedMode,
Sample = new MachineToolSampleInfo(acquiredAt, StatusCodes.Good)
}
}, ct);
await machine.RefreshAsync(ct);
}
```
This example supplies only overall operation; the active program remains
explicitly unavailable. When optional inventories are enabled, provide their
complete coherent observations too. `SetSnapshotAsync` assigns the next
revision in this in-memory adapter. A production adapter must supply
monotonically increasing revisions itself and must not mutate a returned graph.
## Quick start - client
### Discover and read typed instances
The typed facade takes an already connected `ISession`. It registers the
selected model factories, resolves actual subtypes and keeps optional absence
separate from a present bad/uncertain `DataValue`.
```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.MachineTool;
using Opc.Ua.MachineTool.Client;
static async ValueTask ReadMachinesAsync(
ISession session, Opc.Ua.ITelemetryContext telemetry, CancellationToken ct)
{
await using MachineToolClient client = session.CreateMachineToolClient(telemetry);
ArrayOf<MachineToolDeviceEntry> devices = await client.DiscoverAsync(cancellationToken: ct);
for (int index = 0; index < devices.Count; index++)
{
MachineToolDeviceEntry device = devices[index];
if (device.Kind != MachineToolDeviceKind.MachineTool)
{
continue;
}
MachineToolReadSnapshot snapshot = await client.ReadSnapshotAsync(device.NodeId, ct);
Console.WriteLine($"{device.NodeId}: {snapshot.OperationMode.StatusCode}");
if (snapshot.OperationMode.WrappedValue.TryGetValue(out int mode))
{
Console.WriteLine($"Observed mode {mode}, acquired {snapshot.OperationMode.SourceTimestamp}");
}
}
}
```
The caller still owns `session`. Disposing the facade cancels and drains its
observations; it does **not** close the session. Use the returned kind to
distinguish machines from wireless peripherals, rather than treating every
discovered root as a `MachineToolType`.
### Use a container-managed connection
Add `AddMachineToolClient()` to an existing `AddOpcUa().AddClient(...)`
registration, then resolve `MachineToolClientFactory`. The equivalent
`Func<CancellationToken, Task<MachineToolClient>>` is also registered.
The injected managed-session accessor caches a shared connection for the
container. Factory-created facades borrow it, just like directly constructed
facades. Disposing one facade, or cancellation immediately after acquisition,
must not disconnect other consumers. The container disposes the shared
session at shutdown; applications that create a session independently retain
its ownership. See [client hosting](DependencyInjection.md#client-feature).
## Server hosting and ownership
### Standalone hosting
`AddMachineTool(options => ...)` selects one `MachineryNodeManager`.
`MachineToolServerOptions.InstanceNamespaceUri` defaults to
`urn:opcua-netstandard:machinetool:instances` and must be an absolute application
URI, not a standard model namespace.
`ConfigureMachineTool` awaits a
`Func<IMachineryBuildContext, CancellationToken, ValueTask>` after the model
closure has loaded. Create multiple machines or peripherals in that callback,
with distinct names, providers and stores. Registration and rollback belong
to the shared owner; a handle belongs to one instance.
### Compose into an existing owner
Do not call `AddMachineTool()` when another registration already owns the same
DI/Machinery address space. Load the additional model through the existing
pipeline and target that owner explicitly:
```csharp
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Machinery.Server;
using Opc.Ua.MachineTool;
using Opc.Ua.Server.Hosting;
static IOpcUaServerBuilder AddMachineToolModelsToCell(IOpcUaServerBuilder server)
{
return server
.AddMachinery(options =>
{
options.Parts = MachineryParts.BuildingBlocks |
MachineryParts.Jobs | MachineryParts.ProcessValues;
options.InstanceNamespaceUri = "urn:example:cell:instances";
})
.AddMachineToolToMachinery(options => options.Parts = MachineToolParts.All);
}
```
Here `AddMachinery` establishes the cell's owner; omit that call if it is already
registered. Use `ConfigureMachineToolFor<MachineryNodeManager>(...)` to build
on it. Shared composition uses the existing owner's application namespace,
not a second namespace-owner registration.
GMS, cranes and Machine Vision can use the same owner when its required model
closure and Machinery parts are present. The
[CompanionSuite sample](../samples/CompanionSuite/) demonstrates that composition
without registering duplicate `DeviceSet` or `Machines` owners.
### Custom owners and generated subtypes
`ConfigureMachineToolFor<TNodeManager>` targets an existing `DiNodeManager`
subtype that supports the Machinery construction pipeline and node-ID
allocation contracts. Loading model types and implementing the compatible
owner are separate from running the configurator; see
[Machinery composition](Machinery.md#coexisting-with-a-server-that-already-owns-di-or-isa-95).
`context.AddMachineTool<TState>(name, createState)` accepts a generated
MachineTool subtype. The factory must return an unpublished node with the
requested identity. `WithMachinery(...)` configures that same typed Machinery
builder; it must not independently build, register or dispose it. This is how
GMS keeps its native root, result management and derived child types instead
of being represented by a copied base machine.
## MachineTool builder reference
### Mandatory structure and monitoring
`WithFeatures(MachineToolFeatures.None)` is the default. Mandatory inherited
identification, monitoring and retained active-program structure are not removed
just because optional features are off.
| Selection | Provider data and behavior |
| --- | --- |
| `Channels` | Ordinary and combined channels, execution/mode observations and override values; combined channels refer to real ordinary channels |
| `SoftwareIdentification` | Installed software identities, separate from the overall machine revision |
| `AdditionalIdentification` | Requires component name, model, construction year/month and device class on the nameplate |
| `WorkingUnits` | Spindles, lasers and EDM generators with their reported modes and values |
| `Stacklights` | Actual segmented IA stacklights, not a computed substitute for machine state |
| `LegacyPowerOnDuration` | Retained deprecated integral-hours counter |
| `OperationCounters` | DI/Machinery duration counter using the runtime's declared millisecond input |
| `PartsProducedInLifetime` | Explicit lifetime production count, not a count inferred from reads |
| `MachineryState` | Nested Machinery item/operation-mode observations |
| `Obligation`, `MaintenanceMode` | Responsibility and maintenance substate; selections and parent states must be compatible |
`MachineToolSampleInfo` carries acquisition time and quality. Feed overrides,
mode values, counters and inventory references are validated before commit.
An unsupported or missing value is not silently converted into a successful
zero. Optional flags are capability selections, not permission to fabricate data.
`WithMonitoringRequirements` describes the deployment's monitoring needs for
conformance evaluation. It does not increase server quotas or turn on optional
features.
### Tools, working units and life
`WithEquipment(MachineToolEquipmentOptions)` configures `Tools`, `DynamicTools`
and `ToolLife`, with finite tool, element, text and per-tool life-entry limits.
Tools and multi-tools retain their stable provider keys, identifiers, location,
lock state and independent life measurements. Life values do not implicitly
drive a physical tool change.
Dynamic tools use NodeVersion/model-change reporting and the shared final-value
publication barrier. Removed equipment is not disposed while its final required
observation is still waiting to enter a Publish response. Typed factories can
create real generated companion subtypes and return their owned resources.
The options added by `WithEquipment`, `WithProduction` and `WithPrognoses`
contribute feature flags. A later `WithFeatures(...)` replaces the combined
selection, so configure it deliberately rather than unintentionally clearing
an earlier module.
### Retained Production
`WithProduction(MachineToolProductionOptions)` binds the retained Production
model, including jobs, ordered programs, part sets, individual parts, statistics
and interruption conditions. The provider supplies production phases and
transition sequence numbers; `Initializing`, `Running`, `Interrupted`, `Ended`
and `Aborted` are observations, not new execution methods.
| Selection | Effect |
| --- | --- |
| `Production` | Retained jobs/programs and state/transition information |
| `DynamicProductionPlan` | Insertion/removal using stable keys, publication barriers and resource cleanup |
| `ProductionParts` | Part sets, parts and coherent quality counts |
| `ProductionStatistics` | Retained UInt32 lifetime statistics |
| `ProductionInterruptions` | Known interruption reasons as retained standard conditions |
The default bounds include 64 jobs, 128 programs per job, 32 part sets per job,
1,024 parts per set and 4,096 aggregate elements. Select tighter limits in
`MachineToolProductionOptions` when appropriate; there is no silent truncation.
The runtime validates references, count consistency and permitted transitions
before replacing the accepted snapshot.
### Machinery and ISA-95 job management
The modern `JobManagement` AddIn is separate from retained Production.
It lives under `MachineryBuildingBlocks` and uses the shared ISA-95 Job Control
V2 binder. Complete MachineTool job management requires **both**
`IIsa95JobOrderReceiverV2` and `IIsa95JobResponseProviderV2`, plus a per-instance
`IIsa95JobOrderCatalog`. A receiver-only configuration is rejected.
```csharp
using Opc.Ua.MachineTool;
using Opc.Ua.MachineTool.Server;
static MachineToolBuilder<MachineToolState> BindJobSource(
MachineToolBuilder<MachineToolState> machine,
MachineToolJobSource source)
{
return machine
.WithProvider(source)
.WithJobManagement(new MachineToolJobManagementOptions
{
Receiver = source,
ResponseProvider = source,
Catalog = source,
PredefinedParameters = true
})
.WithProduction(new MachineToolProductionOptions
{
Features = MachineToolFeatures.Production |
MachineToolFeatures.DynamicProductionPlan
});
}
```
Construct the source with explicit `MachineToolJobSourceOptions` and job/program
descriptors. `MachineToolJobSource` is a bounded, single-host, non-durable
reference adapter linking accepted ISA-95 state with retained observations.
It never executes CNC hardware. Keep it application-owned and dispose it after
its runtimes have drained.
Standard verbs remain `Store`, `StoreAndStart`, `Start`, `Stop`, `Abort`,
`Pause`, `Resume`, `Clear`, `Cancel`, `Update` and `RevokeStart`. Their UInt64
`ReturnStatus` is an ISA-95 bitmap: success is **1**, not UA `Good` (zero).
An accepted order is not evidence of physical execution. Providers describing
different equipment must not be combined into one instance's control,
response and monitoring views.
### FileSystem and WorkMasters
`WithFileSystem(IFileSystemProvider, includeWorkMasters, FileDirectoryBindingOptions)`
uses the existing [file-system binder](../src/Opc.Ua.Server/FileSystem/FileDirectoryBinder.cs).
It does not add another
file node manager. When selected, `WorkMasters` must already exist as a directory
in the provider and is exposed with the MachineTool-qualified browse name.
An absent directory or inadequate materialization limits fail setup.
The provider owns storage and access decisions. Operation flags and
`MaxEntries`/`MaxDepth` bound the shared directory binding. The runtime owns
materialized nodes and session-bound file handles, not the application-supplied
provider. Remote paths are scoped UA paths, not permission to read host-local
files. Reading a program file does not execute it.
### Prognoses and notifications
`WithPrognoses(MachineToolPrognosisOptions)` selects explicit
`SupportedKinds` and the `Prognoses`/`DynamicPrognoses` features. The provider
supplies predictions and actual affected-entity references; the library does not
infer equipment forecasts. Limits cover prognosis count, referenced entities
and text.
`WithNotifications(MachineToolNotificationOptions)` requires a separate
`IMachineToolNotificationProvider`. It binds errors, alerts and messages through
the existing alarm/event machinery, with bounded identities, message batches
and refresh time. `RefreshNotificationsAsync` is separate from the ordinary
equipment snapshot refresh.
The notification model carries retained conditions, enable/acknowledge/comment
behavior and original event identities. Passive observation does not acknowledge
or confirm conditions. Use [Alarms and Conditions](AlarmsAndConditions.md) for
the standard condition lifecycle rather than inventing machine-specific verbs.
## Wireless peripheral server
### Build an independent peripheral
Select `MachineToolParts.Wmtp` for a wireless-only endpoint, or `All` to host
machines and peripherals together. Both use `ConfigureMachineTool`; inside the
callback use `AddWirelessPeripheral`, not `AddMachineTool`.
```csharp
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Machinery.Server;
using Opc.Ua.MachineTool.Server;
using Opc.Ua.MachineTool.WMTP;
static ValueTask<WmtpPeripheral> BuildTemperaturePeripheralAsync(
IMachineryBuildContext context,
IWmtpDeviceProvider device,
IWmtpMeasurementProvider measurements,
IWmtpAuthorization authorization,
EUInformation temperatureUnits,
CancellationToken ct)
{
return context.AddWirelessPeripheral("TemperatureProbe")
.WithProvider(device)
.WithMeasurementProvider(measurements)
.WithAuthorization(authorization)
.WithIdentification(identity =>
{
identity.Manufacturer = new LocalizedText("Example manufacturer");
identity.SerialNumber = "Probe-001";
})
.AddMeasurement(new WmtpMeasurementDefinition
{
Name = "Temperature",
Kind = WmtpMeasurementKind.Temperature,
DataType = BuiltInType.Double,
EngineeringUnits = temperatureUnits,
EURange = new Opc.Ua.Range { Low = -20, High = 120 },
InstrumentRange = new Opc.Ua.Range { Low = -20, High = 120 },
ValuePrecision = 0.1,
Definition = "Acquired probe temperature",
TriggerMilliseconds = 1000,
Delta = Variant.From(0d)
})
.BuildAsync(ct);
}
```
The nameplate is the existing component identification inside
`DeviceInformation`. The explicit device adapter can also implement measurement,
calibration, time or transfer interfaces; separate `With...Provider` calls
override those interfaces for this peripheral.
No unkeyed device provider is chosen implicitly from DI. A shared authorization
policy may be resolved from the construction context, but device selection
remains explicit. Selected methods require both their provider and authorization
policy before any peripheral is published.
### Measurement kinds and acquired values
`WmtpMeasurementKind` covers all eight released kinds:
| Kind | Kind | Kind | Kind |
| --- | --- | --- | --- |
| Temperature | RotationSpeed | Humidity | Acceleration |
| Force | Torque | Position | Dimension |
Each measurement is a generated `WMTPMeasurementType` composed with Machinery
ProcessValues/PA-DIM, not a second signal implementation. Its definition supplies
the exact numeric `BuiltInType`, units, engineering/instrument ranges, precision,
sample category, trigger and delta. Distinct sensor names permit repeated kinds;
the builder permits at most 256 sensors per peripheral.
`IWmtpMeasurementProvider.ReadAsync(sensor, ct)` returns
`WmtpMeasurementSample`, including the original `DataValue`, device index and
measurement period. A bad/null sample is unavailable data, not a new zero.
`StaleAfter` controls when a usable acquisition becomes
`UncertainLastUsableValue`; refreshing receipt time does not refresh the
equipment's acquisition timestamp.
| `WmtpMeasurementFeatures` | Additional behavior |
| --- | --- |
| `LimitCounters` | Status and duration above high/high-high with standard references |
| `InternalUpdateInterval` | The sensor's own sampling interval |
| `WorkCycleInformation` | Acquisition timestamp, index and measurement period |
| `Uncertainty` | Absolute and relative uncertainty |
| `LimitAlarm` | The shared ProcessValues limit alarm |
### Trigger and delta configuration
The mandatory `SetTriggerSettings` and `SetDeltaCondition` methods are
provider-backed. Trigger is a whole-millisecond value representable by UInt32.
Delta retains its numeric input type; incompatible, nonfinite, negative or
unrepresentable values are refused rather than rounded into another type.
**Trigger and delta must not both be zero.** Each method passes both proposed
settings to `IWmtpMeasurementProvider.ConfigureAsync`, so acceptance is atomic.
Before acceptance, refusal or cancellation leaves the prior settings unchanged.
After the provider accepts, a later caller cancellation does not discard that
accepted configuration. A client whose request was interrupted must inspect
the current settings instead of assuming rollback or retrying automatically.
### Work and service records
`WithWorkRecords(store, features)` and `WithServiceRecords(store, features)`
select independent datasets. The same store instance cannot back both.
`IWmtpRecordStore` exposes count, complete-copy read and atomic delete; reads
never consume records.
The basic dataset supplies abort, count and complete-report operations.
`WmtpRecordFeatures` explicitly selects time counts, index/time reports,
first/last reports and all/index/time deletion. Index and acquisition-time
ranges are inclusive. First/last refer to storage order.
`CombinedReportAll` means the complete dataset, not an implicit page.
If its encoded response exceeds the configured report budget, it fails.
Use a supported explicit range; do not silently truncate the result.
`InMemoryWmtpRecordStore` is a bounded reference store, not distributed durable
storage. Work and service records remain separate even when they describe the
same sensor.
Operation coordination is peripheral-scoped. An authorized `AbortOperation`
may affect the peripheral's active work, so authorization receives both the
calling session and active operation's session. A local cancellation or timeout
never implicitly sends this method.
### Calibration, device time and byte transfer
| `WmtpFeatures` | Required adapter or observation |
| --- | --- |
| `Calibration` | `IWmtpCalibrationProvider`; observed calibration mode plus explicit mode request |
| `DeviceTime` | `IWmtpDeviceTimeProvider`; UTC time with `TimeZoneDataType` offset/DST information |
| `Firmware`, `License` | `IWmtpTransferProvider`; bounded byte acceptance, plus selected licence metadata |
| `Battery` | Original battery ProcessValue sample |
| `ServiceInformation` | Service dates and device-reported lifecycle status |
| `AdvancedServiceInformation` | Basic service information and the specified lifetime/operation counters |
| `WorkRecords`, `ServiceRecords` | The corresponding explicit record stores |
`WithByteLimits(maximumTransferBytes, maximumReportBytes)` bounds the server
payloads; transport/message quotas may impose a lower limit. The maximum
configurable value is 64 MiB for each, with a minimum report budget of 256 bytes.
Setting device time does not change the server's `TimeProvider`. Calibration-mode
acceptance does not prove a physical calibration completed. Firmware/licence
transfer success means **the provider accepted the bytes**; it does not assert
installation, restart, signature verification or licence activation.
Those responsibilities belong to the application adapter and equipment.
## Typed client workflows
### Machine monitoring, production and services
| API | Result or role |
| --- | --- |
| `DiscoverAsync` | Actual machine/peripheral roots, including vendor subtypes |
| `ReadSnapshotAsync` | Monitoring values with raw quality/time and optional absence |
| `ReadEquipmentAsync` | Tools/multi-tools, identities, locations, locks and life |
| `ReadProductionAsync` | Retained jobs, programs, parts, statistics and transitions |
| `ReadPrognosesAsync` | Predictions and their real target relationships |
| `GetMachineProxy`, `GetPeripheralProxy` | Generated namespace-correct advanced proxies |
| `GetJobManagementAsync` | Optional generated Machinery Jobs proxy |
| `GetFileSystemAsync`, `GetWorkMastersAsync` | Optional standard directory proxies |
| `GetWirelessPeripheral` | Typed WMTP workflow on the same session |
Missing optional service AddIns return null where the accessor documents it.
Missing mandatory structure, denied access and service failures remain errors.
Discovering a namespace or root is not proof of every optional capability.
Use the generated job-role proxies below the returned Jobs object and the
existing [ISA-95](ISA95.md) and [file client](FileSystemClient.md) workflows.
Do not substitute a job-response receiver for the published response provider,
or assume a same-named directory is the qualified WorkMasters node.
### Bounded data and event observation
`ObserveDataAsync` observes a machine's monitoring/active-program values.
`ObserveValuesAsync` accepts 1-256 distinct non-null native variable NodeIds,
including WMTP signal nodes. Both create their own subscription and preserve
source timestamps and status.
```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.MachineTool;
using Opc.Ua.MachineTool.Client;
static ValueTask<MachineToolDataObservation> ObserveMachineAsync(
MachineToolClient client, NodeId machineId, CancellationToken ct)
{
return client.ObserveDataAsync(machineId, new MachineToolObservationOptions
{
Window = TimeSpan.FromSeconds(5),
MaximumNotifications = 20
}, ct);
}
```
The observation window includes setup and cannot exceed 60 seconds.
The maximum retained notification count is 100 and decoded payload is bounded.
Results indicate count-limit completion and loss; an empty timed window is not
proof of a particular machine state.
`ObserveEventsAsync` is passive. `ObserveConditionsAsync` explicitly requests
standard retained-condition refresh and reports refresh completion. Neither
implicitly acknowledges or confirms a condition. Generated event decoders and
the shared event collector preserve concrete subtype identity and field order.
See [subscriptions](Subscriptions.md) for long-lived application subscriptions.
### Wireless measurements and configuration
`GetWirelessPeripheral(peripheralId)` returns a `WmtpPeripheralClient`.
`ReadSnapshotAsync` reads device metadata and `ReadMeasurementsAsync` reads
the bounded native inventory. A measurement's `NodeId` is its configuration
object; its **`ValueNodeId`** is the PA-DIM signal to observe.
The generated measurement proxy preserves the released method signatures:
```csharp
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.MachineTool.Client;
using Opc.Ua.MachineTool.WMTP;
static async ValueTask ConfigureWirelessSensorAsync(
WmtpPeripheralClient peripheral, NodeId measurementId, CancellationToken ct)
{
WMTPMeasurementTypeClient sensor = peripheral.GetMeasurementProxy(measurementId);
await sensor.SetDeltaConditionAsync(Variant.From(2d), ct);
await sensor.SetTriggerSettingsAsync(0, ct);
}
```
This is two explicit requests, not a client-side transaction. Each preserves
the both-not-zero invariant independently; if the second fails, the first
accepted change remains. Choose a delta type compatible with the actual sensor.
`SetCalibrationAsync`, `SetDeviceTimeAsync` and `TransferAsync` use generated
configuration navigation. Transfers take bounded `ByteString` data, not a path
or URL. They do not add installation or licensing semantics.
### Wireless record queries
The wrapper normalizes the two released work/service proxy shapes while keeping
the same standard operations:
```csharp
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.MachineTool.Client;
using Opc.Ua.MachineTool.WMTP;
static async ValueTask<ArrayOf<WMTPOutputDataType>> ReadWorkRecordsAsync(
WmtpPeripheralClient peripheral, CancellationToken ct)
{
WmtpRecordClient records = await peripheral.GetWorkRecordsAsync(ct);
return await records.ReadAsync(new WmtpRecordQuery
{
Selection = WmtpRecordSelection.Index,
FromIndex = 1,
ToIndex = 20
}, maximumRecords: 20, cancellationToken: ct);
}
```
The endpoint must select index reports for this example. Use
`GetServiceRecordsAsync` for the other dataset.
`CountAsync` supports all/time selections; `ReadAsync` supports
all/index/time/first/last; `DeleteAsync` supports all/index/time.
An absent optional method is not an empty successful result.
`ReadAsync` rejects reports above the caller's bound; the default is 4,096.
`AbortAsync` is always an explicit authorized request.
## Lifetime, cancellation and failure handling
### Server transactions and dynamic removal
MachineTool refresh separates acquisition/validation, coherent commit and
publication. Rejected acquisition does not overwrite the previously accepted
snapshot. A publication failure after acceptance is different:
`RetryPublicationAsync` retries the retained values/events without reacquiring
or repeating equipment effects.
Dynamic production, tools and prognoses use the shared
[final-value publication contract](NodeManagers.md#final-value-publication-before-node-removal).
It waits for the specific terminal queue entry to enter a real Publish
response, not merely an equal older value or a queued notification. It does not
claim network delivery or client acknowledgement. Failed/cancelled publication
does not authorize removing the still-required subtree.
Callbacks must honor cancellation and avoid reentering their own runtime's
acquisition/disposal path. Stop acquisition before shutting down the owning
host. Disposal drains this instance's registrations and owned resources rather
than touching another machine's provider or subscriptions.
### Client and provider ownership
| Resource | Owner |
| --- | --- |
| Application-supplied MachineTool provider, job source, notification provider or file provider | Application |
| WMTP providers/stores supplied normally | Application |
| Resource passed to `WmtpPeripheralBuilder.WithOwnedResource` | Peripheral only after successful construction; do not transfer a shared resource |
| Runtime-created nodes, subscriptions, metadata/file handles and registration resources | Their existing runtime/operation owner |
| Session supplied to `MachineToolClient`, including one acquired by its factory | Application or DI container |
WMTP failed construction does not take ownership of caller resources.
Intentionally shared device adapters must remain application-owned and
coordinate their own equipment access.
Local cancellation ends the caller's wait and triggers call-owned cleanup.
It does not prove a submitted remote write was rolled back, does not send a
wireless Abort implicitly, and does not close a borrowed connection.
Keep independent session shutdown in the code that actually owns the session.
## MCP integration
### Select or embed the bounded profile
The host profile is `machinetool`; there is no separate `wmtp` profile:
```powershell
opcua-mcp --profile machinetool
```
For embedding:
```csharp
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Mcp;
static IMcpServerBuilder AddMachineToolMcp(IServiceCollection services)
{
services.AddOpcUaMcpCore().AddOpcUaMcpMachineTool();
return services.AddMcpServer()
.WithStdioServerTransport()
.WithOpcUaMcpFilters()
.WithOpcUaMachineToolTools(McpToolProfile.MachineTool);
}
```
The profile-set overload supports composed catalogs and deduplicates common
connection tools. Calls borrow the host's `OpcUaSessionManager` sessions;
provide `sessionName` when more than one connected session is available.
### Passive and explicitly mutating tools
| Group | Tools |
| --- | --- |
| Machine inventory/reads | `machinetool_list_devices`, `machinetool_read_machine`, `machinetool_read_equipment`, `machinetool_read_production`, `machinetool_read_prognoses` |
| Observation | `machinetool_observe_values`, `machinetool_observe_machine`, `machinetool_observe_events` |
| Jobs/files | `machinetool_query_jobs`, `machinetool_control_job`, `machinetool_list_files`, `machinetool_read_file`, `machinetool_change_file` |
| Wireless reads | `wmtp_read_peripheral`, `wmtp_count_records`, `wmtp_report_records` |
| Wireless mutations | `wmtp_configure_measurement`, `wmtp_configure_peripheral`, `wmtp_delete_records`, `wmtp_abort_operation`, `wmtp_transfer` |
Mutations require explicit `confirm=true` and remain subject to the remote
server's authorization and provider refusal. Reads do not start jobs, change
settings, consume records or acknowledge conditions. Native job return bitmaps,
UA service status and firmware/licence acceptance are not interchangeable.
### Authorization, budgets and cancellation
Profile selection controls the catalog, not authority.
The host's privileged/live capabilities are independently default-off;
enabling a capability also requires appropriate caller grants and network rules.
Confirmation flags do not replace those controls or OPC UA permissions.
See [MCP execution policy](McpServer.md#privileged-and-live-execution-policy).
Wireless deadlines are 1-60 seconds; complete replies default to 128 KiB and
cap at 1 MiB. Wireless inventories cap at 256 measurements, reports at 4,096
records, and inline transfer data at 1 MiB decoded. Native client/server limits
may be tighter. Machine job/file tools have their own schema limits; file
content is bounded separately from base64/JSON expansion.
Oversize complete results fail rather than silently truncate. Observations
report their bound/loss information and release only their own resources.
MCP cancellation must reach the server, for example through the standard
`notifications/cancelled` notification; abandoning a local client wait is not
necessarily a remote cancellation.
The [MCP package reference](../tools/Opc.Ua.Mcp.MachineTool/NugetREADME.md)
documents exact selectors, typed numeric unions, job JSON, scoped remote paths
and byte limits. Use the live schemas rather than inventing additional
wireless or CNC verbs.
## Conformance and support limits
### Supported surfaces
| Feature | Support |
| --- | --- |
| MachineTool 1.02.0 and bundled WMTP 1.0.0 | Released models; independent runtime selection |
| Monitoring, channels, working units, tools/life, prognoses and conditions | Explicit provider-backed observations |
| Retained Production, states/parts/statistics, Machinery/ISA-95 jobs and remote files | Implemented through native types and shared services |
| All eight WMTP measurement kinds and checked trigger/delta settings | Original type, quality and acquisition time retained |
| Battery/service metadata, calibration mode and UTC/time-zone requests | Selected observations/providers required |
| Independent work/service record operations | Complete-copy reads, explicit range/delete/abort |
| Firmware/licence bytes | Authorized provider acceptance only |
| Standalone/shared hosting, isolated instances, typed clients, MCP and NativeAOT | Same model/runtime APIs; deployment-specific configuration still required |
| Separate WMTP package/profile/guide; CNC, Bluetooth/pairing, flash/licensing or safety drivers | Not supplied |
| External OPC Foundation certification of a deployment | Not claimed |
### Runtime claims and deployment requirements
Model exposure and operational conformance are different. Contributors report
capabilities of committed live instances and selected providers; namespace
presence alone cannot establish a monitoring, job, file or record facet.
Security-dependent claims also require actual endpoint policies, certificates
and user-token support. Configuring a monitoring requirement does not make a
server quota or equipment update rate true.
Facet identities follow the released
[MachineTool profiles](https://reference.opcfoundation.org/specs/OPC-40501-1/14)
and [WMTP profiles](https://reference.opcfoundation.org/specs/OPC-40505/9).
See [Profiles](Profiles.md) for repository-wide scope and certification limits.
The executable evidence is maintained in the
[MachineTool tests](../tests/Opc.Ua.MachineTool.Tests/), including live-session,
publication, provider-refusal and lifetime regressions; support tables are not
a certification result for a particular device.
## Model provenance
Both models are pinned to OPC Foundation UA-Nodeset revision
[`96a08945425d0295edb3607d2a38f6841ef44046`](https://github.com/OPCFoundation/UA-Nodeset/tree/96a08945425d0295edb3607d2a38f6841ef44046).
Original filenames, including WMTP's `Nodeset2` spelling, are preserved:
| Local model | Purpose |
| --- | --- |
| [`Opc.Ua.MachineTool.NodeSet2.xml`](../src/Opc.Ua.MachineTool/Model/MachineTool/Opc.Ua.MachineTool.NodeSet2.xml) | Normative MachineTool wire identities |
| [`Opc.Ua.WMTP.Nodeset2.xml`](../src/Opc.Ua.MachineTool/Model/WMTP/Opc.Ua.WMTP.Nodeset2.xml) | Normative WMTP wire identities |
| `*.Derived.NodeIds.csv` beside each model | Locally derived generator drift guards, not replacement numeric identifiers |
| [`MachineToolModels`](../src/Opc.Ua.MachineTool/MachineToolModels.cs) | Pinned source commit, namespace closure and LF-normalized NodeSet SHA-256 values |
The generated state/proxy types consume dependency payloads from their existing
model assemblies. No second copy of DI, Machinery, ISA-95 or PA-DIM is generated
inside MachineTool. See [model dependencies](ModelDependencies.md) for the
cross-assembly mechanism and [Machinery provenance](Machinery.md#model-sources-identifier-tables-and-repairs)
for identifier-table conventions.
## Samples and related documentation
- [MachineTool/WMTP sample pair](../samples/MachineTool/README.md): machine-only,
wireless-only and mixed modes; mandatory/advanced selections; bounded,
encrypted loopback examples and explicit accepted/refused requests.
- [CompanionSuite](../samples/CompanionSuite/): all families on one DI/Machinery
owner with isolated instances.
- [Machinery](Machinery.md), [Device Integration](DeviceIntegration.md),
[ISA-95](ISA95.md), [GMS](GMS.md) and [Machine Vision](Vision.md).
- [Node manager lifetimes](NodeManagers.md), [state machines](StateMachines.md),
[alarms and conditions](AlarmsAndConditions.md), [file services](FileSystemClient.md)
and [model-change tracking](ModelChangeTracking.md).
- [MCP server](McpServer.md) and the
[MachineTool MCP package](../tools/Opc.Ua.Mcp.MachineTool/NugetREADME.md).
- [Developer guide](DeveloperGuide.md) for framework-specific builds and tests.