# Description This completes the AutoID contribution proposed in #4824: the model package for [OPC 30010 OPC UA for AutoID Devices](https://reference.opcfoundation.org/AutoID/v101/docs/) (1.01) plus the server, the client, an MCP profile and a sample pair, in one PR. It follows the newest companion pattern (GMS #4765, MachineTool, CranesHoists, MachineVision). ## Commits 1. `fix(sourcegen)`: a structure property whose DataType a subtype refines is redeclared with its own field (the NodeSet needs it; unchanged from the first slice). 2. `feat(autoid)` model package `Opc.Ua.AutoID` (unchanged from the first slice). Adds only `AutoIdReaderKind`. 3. `feat(autoid)` server package `Opc.Ua.AutoID.Server`. 4. `feat(autoid)` client package `Opc.Ua.AutoID.Client`. 5. `feat(autoid)` MCP profile `Opc.Ua.Mcp.AutoID`. 6. `feat(autoid)` samples `samples/AutoID`. 7. `build(autoid)` solution, package list and package validation tests. 8. `fix(autoid)` x3, `docs(autoid)` and `test(autoid)`: review fixes, net48 compatibility, documentation, the native AOT test. 9. `fix(autoid)` x3 and `feat(autoid-client)`: `GetLocation` location types, the `Client` of access events, the default scan cycle of the simulated readers, and NodeIds and methods in the device snapshot (details below). 10. `feat(autoid)`, `test(autoid)`, `docs(autoid)` (`afdd7858b`, `22bc3b26e`, `ed25fe4ae`, on top of the master merge `1c7948f65`): AutoID composes into an existing Device Integration owner like Pumps and Scales (review feedback; see "Composition with other DI-based companions"). 11. `1458af9e9` (marcschier): the `AutoIdReadOptions` remarks state that `StatusOnly` limits the values read and that each call still browses (Copilot review). 12. `fix(autoid)` `3ef2f1507`: `GetDeviceLocation` answers with the same status codes as `GetLocation`; `feat(autoid-mcp)` `f6405152f`: `autoid_read_device` shows the new snapshot NodeIds and methods (details below). ## Server: `Opc.Ua.AutoID.Server` - Reader devices (RFID, OCR, optical, optical verifier, RTLS) on the DI device model, created by a fluent builder (`AutoIdRuntime.Reader(name, provider)`) and hosted with `AddAutoId` / `ConfigureAutoId` on `IServiceCollection`. - Hardware is behind provider interfaces: `IAutoIdReaderProvider`, `IAutoIdScanProvider` (Scan), `IAutoIdAsyncScanProvider` (ScanStart/ScanStop), `IAutoIdRfidProvider` (ReadTag, WriteTag, WriteTagID, KillTag, LockTag, SetTagPassword, each opt-in by flag), `IAutoIdRtlsProvider`. Providers push scans, presence, log entries and access events through `IAutoIdReaderSink`. In-memory providers (an EPC Gen2 tag model with banks, passwords, locks and kill) are included for tests and samples. - Optional children (methods, `LastScanData`, `DeviceLocation`, `AntennaNames`, location methods) exist only when the provider supports them. Facet profiles and conformance units are advertised only for what is implemented (`IConformanceContributor`); the Full facet is withheld. - `GetLocation` (OPC 30010 6.6.3.2) returns the location in the requested `LocationType`. An unsupported type is `Bad_InvalidArgument`, an unknown object `Bad_NotFound`, and a supported type the object's location is not available in `Bad_InvalidState` (6.6.3.2: "not available or not allowed e.g. due to special configuration"); a provider result of another type never reaches the client. `InMemoryRtlsProvider` takes an optional location converter, for example from the configured local coordinate system (9.3.4) into WGS84. - `GetDeviceLocation` (6.1.3.7) uses the same mapping: an undefined location type is `Bad_InvalidArgument`, a device location that is not available in the requested type `Bad_InvalidState` (it answered `Bad_NotSupported` before). 6.1.3.7 lists only "Standard OPC UA status codes defined for the Call Service in OPC 10000-4"; there `Bad_NotSupported` means "The Method is not supported for the Object instance", which is wrong for a published method, and OPC 10000-4 5.12.2.4 allows reusing an existing status code with the same canonical meaning, so the method follows 6.6.3.2. - Access events (7.10, 7.11): `Client` is "the name of the client application which was the originator of the command", so it is the application name of the session's client description (else its application URI), not the user identity. The results of a `ScanStart` scan name the client that started it; scans the device reports on its own name the device. - The simulated readers default to one scan cycle per second (`CycleInterval`, configurable per provider; the length of a cycle is device specific in OPC 30010), so an unbounded `ScanStart` raises at most one scan event per second. - Behaviour: one scan at a time with a generation counter; ScanStart/ScanStop serialised; a scan started by a session stops when that session closes; the maximum scan duration, tag read length and result count are limited by `AutoIdServerOptions`; device status changes and events are raised outside locks; registration is rolled back on failure; readers, providers and the runtime are disposed in order and in-flight operations are drained. ## Composition with other DI-based companions A server has one Device Integration owner, and AutoID now uses the existing harmonized composition instead of requiring its own node manager: - `AutoIdMachineryModelProvider` adds the AutoID model to the Machinery owner's model-provider pipeline (dependency-ordered, `Order` 200) and keeps the DI nodes the owner loaded, so DI and `DeviceSet` exist once. - `AddAutoIdToMachinery` (after `AddMachinery`) registers it; `ConfigureAutoIdFor<TNodeManager>` creates readers on any `DiNodeManager` owner, successive delegates share one `AutoIdRuntime`; `owner.CreateAutoIdRuntime()` is the path without DI hosting. The owner disposes the runtime on shutdown. - Readers are registered in the owner below its `DeviceSet`, NodeIds in the owner's instance namespace (`AutoIdRuntime.InstanceNamespaceIndex`), not in the `Machines` folder; the AutoID conformance units and facets are published by the owner next to the DI and Machinery ones. - `AddAutoId` remains for the "AutoID is the only DI-based companion" case and is built from the same provider. Next to another DI owner it is still rejected; the message names the composition API. (Bypassing that check gives two `DeviceSet` copies; the server routes `DeviceSet` to the first owner and readers in the second are not browsable, checked locally.) - `Opc.Ua.AutoID.Server` now references `Opc.Ua.Machinery.Server` (a stable package), as Vision does; a stand-alone server still loads only DI and AutoID. ## Client: `Opc.Ua.AutoID.Client` Reader discovery (a failed browse propagates), typed calls for every published method, a device snapshot with documented `AutoIdReadOptions`, and an async event stream (`ObserveScan/Access/Presence/LogEntries`) on `IStreamingSubscription`. Cancellation is honoured throughout. `AddAutoIdClient` registers it for DI. Because most children of a device are optional (6.1, 6.5.3, 6.6.3), the snapshot also carries the NodeIds of `DeviceStatus`, `LastScanData`, `LastScanTimestamp` and `DeviceLocation` and the browse names of the published methods (`Methods`, `HasMethod`), taken from the same browse whatever the read options, so a client can monitor the device and knows what it may call without browsing again. ## MCP profile: `Opc.Ua.Mcp.AutoID` `McpToolProfile.AutoId` with nine tools: list readers, read device, scan, scan start/stop, observe scan events, read tag, get supported location types, get location. `autoid_read_device` also returns `deviceStatusId`, `lastScanDataId`, `lastScanTimestampId` and `deviceLocationId` (NodeId strings, null when the device does not publish the variable) and `methods` (the browse names of the published methods), built explicitly without reflection. Registered in the host builder, the profile switches of the other profiles, `Test-McpGrade.ps1`, `McpREADME.md` and `.mcp/server.json`. ## Samples `samples/AutoID/AutoIdServer` (a dock-door reader with a simulated pallet) and `AutoIdClient` with a README and `Test-AutoIdSamplePair.ps1`. The default port is not a common one; tests use port 0. ## Other changes - `Opc.Ua.Di.Server` gets `InternalsVisibleTo` for `Opc.Ua.AutoID.Server` (`RegisterRootNotifierAsync`). - `UA.slnx`, `.azurepipelines/expected-packages.txt`, `PackageVersionPolicyTests`, `NuGetPackageSetValidationTests`. - Docs: `docs/AutoID.md`, `docs/McpServer.md`, `docs/Profiles.md`, `docs/Samples.md`, `docs/DeveloperGuide.md`, `docs/README.md`, `docs/WhatsNewIn2.0.md`, `README.md`, NugetREADMEs. The "model package only" statements are removed. - All packages follow the root version (no preview override). ## Specification notes and questions for the maintainers - The model is 1.01, published 2020-06-18 (the NodeSet header). OPC 30010 6.1.3.16 prescribes the `AutoIdModelVersion` string `"1.00"`, which is what the NodeSet carries and what the server publishes. - **Question:** the NodeSet's optical verifier `Scan` method returns `WGS84Coordinate`, which looks like a model error. It is not published, and providers that implement `IAutoIdScanProvider` for a verifier are rejected. Is that the behaviour you want? - The stack answers a call to an unpublished optional method with `BadNotImplemented`. ## Design choices reviewers may want changed - `Opc.Ua.Di.Server` gets `InternalsVisibleTo` for `Opc.Ua.AutoID.Server`, so the server can call `RegisterRootNotifierAsync`. - `AutoIdReaderKind` (and `AutoIdReaderKinds.TypeIdOf`) lives in the model package `Opc.Ua.AutoID`, not in the server package, so the client and the MCP tools share it. - The MCP profile is read-only/safe: no tools that write, kill or lock tags. ## Tests `tests/Opc.Ua.AutoID.Tests` has 306 tests (116 of the model slice, 190 new): reader lifecycle, scan semantics, tag access handlers, limits, conformance, in-memory providers, hosting, and over-the-wire client/server tests with result codes, events, concurrency and dispose. `tests/Opc.Ua.Tools.Tests` has 13 AutoID MCP tests plus the updated profile-count tests. New behaviour tests fail without the code (for example a provider's own cancellation, a synchronous scan while a scan stop is in progress; for the last round: `GetLocation` of a supported type the object is not located in answered `Bad_NotFound`, access events named the user "Anonymous" and attributed `ScanStart` results to the device, and an unbounded `ScanStart` on a default simulated reader reported 20 times in 400 ms). Composition: `AutoIdCompositionTests` (stock Machinery owner, providers out of order, DI loaded once, instance ownership, rollback, cancellation, runtime disposal, an application-owned DI owner, a second DI owner rejected) and `AutoIdMachineryCompositionEndToEndTests` (Machinery + Pumps + AutoID in one hosted server, read back by the AutoID, Machinery and Pumps clients, including a scan and the published profiles and conformance units). Mutating the change (no lifetime registration, the DI instance namespace for browse names, the old ownership message) fails three of them. The sample pair script passes. `tests/Opc.Ua.Aot.Tests.Mcp` has an AutoID profile test (`AutoIdMcpAotTests`); the project was published with Native AOT on osx-arm64 and all 64 tests passed. Publishing it also found two `JsonArray.Add<T>` uses in the MCP tools (IL2026/IL3050), now fixed. ## Verification (local, macOS arm64, SDK pinned in `global.json`) The branch contains the master merges `fc556be7e`, `1c7948f65` and `af7feb886`; the three composition commits sit between the last two (the latest merge touches no AutoID, DI or Machinery file). `1458af9e9` (marcschier) and the two commits of the last round follow `af7feb886`. - **Last round (`3ef2f1507`, `f6405152f`).** Builds with `-m:2 -nodeReuse:false -p:UseSharedCompilation=false`: `Opc.Ua.AutoID.Tests` on all target frameworks and with `-p:CustomTestTarget=net48`, `Opc.Ua.Mcp.AutoID` on net8.0/net9.0/net10.0, `Opc.Ua.Tools.Tests` on net10.0: 0 warnings, 0 errors. `Opc.Ua.AutoID.Tests` 306/306 on net10.0; `Opc.Ua.Tools.Tests` AutoID tests 60/60, and the profile, catalogue, composition and package tests that reference AutoID (2241) pass. The changed tests fail without the code: `GetDeviceLocation` answered `Bad_NotSupported` for both cases, and `autoid_read_device` had no `deviceStatusId`. `tests/Opc.Ua.Aot.Tests.Mcp` published with Native AOT on osx-arm64 (no IL2026/IL3050 warning): 64/64 passed. `dotnet format` whitespace, style and analyzers `--verify-no-changes` on the changed files: no finding on a changed line (line endings aside, which the LF checkout on macOS reports for every file). - **Build (composition round).** `Opc.Ua.AutoID.Server` on all target frameworks, `Opc.Ua.AutoID.Tests` on net10.0 and with `-p:CustomTestTarget=net48`, and the AutoID server sample: 0 warnings, 0 errors. MCP tools and JSON unchanged, so the native AOT test was not republished. - **Build (earlier rounds).** The AutoID packages and `Opc.Ua.AutoID.Tests` on net10.0 and, with `-p:CustomTestTarget=net48`, on net48/net8.0/net9.0/net10.0; `Opc.Ua.Tools.Tests` and `Opc.Ua.SourceGeneration.Core.Tests` on net10.0 (`-m:2 -nodeReuse:false -p:UseSharedCompilation=false`): 0 warnings, 0 errors. - **Tests.** `Opc.Ua.AutoID.Tests` 306/306 on net10.0 (composition round; earlier round 288/288); `Opc.Ua.Tools.Tests` (AutoID, host builder, profile and package tests) 2438/2438 on net10.0; `Opc.Ua.SourceGeneration.Core.Tests` 4358 passed, 8 skipped, after the master merge touched `NodeStateGenerator.cs`. net48 was built, not run (no Mono on macOS); the fork CI runs it. The MCP tools and their JSON are unchanged in the last round, so the native AOT test was not republished. - **Coverage.** Composition round: the local coverlet collector recorded zero hits for every module in this environment (also for unchanged code), so no new local number; the new paths are exercised by the 18 composition tests listed above and the fork CI uploads coverage. Earlier round: cobertura from the AutoID tests: `Opc.Ua.AutoID.Server` 92.0 % lines / 86.8 % branches, `Opc.Ua.AutoID.Client` 89.9 % / 80 %; 110 of the 112 coverable changed lines of the last round are covered (the two missed lines skip a browse result without NodeId or name). The generated code of the model package is excluded as in the other model packages. - **Format.** `dotnet format` whitespace, style and analyzers (`--severity info`) `--verify-no-changes` on the changed files: clean. Fork CI (full matrix including macOS): [run 39162077465](https://github.com/biancode/UA-.NETStandard/actions/runs/39162077465) on `f6405152f`: green. (Previous round: run 39130154771 on `ed25fe4ae`, 90/90 jobs green; the upstream checks on the merge `af7feb886` green, 108 passed, 5 skipped.) CodeQL: [run 39162079826](https://github.com/biancode/UA-.NETStandard/actions/runs/39162079826) on `f6405152f`: green, no alert in a file this PR touches. (Previous round: run 39130156598 on `ed25fe4ae`, green.) ## Related Issues - Part of #4824 (tracking issue: AutoID companion SDK). ## Checklist - [x] 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. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (The solution builds on all TFMs; the AutoID test projects ran on net10.0 locally, net48 runs in the fork CI.) - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
24 KiB
AutoID Devices
The AutoID packages implement OPC 30010 OPC UA for AutoID Devices (release 1.01, 2020-06-18): RFID readers, OCR readers, optical (barcode) readers and verifiers, and real-time locating systems (RTLS), with their scan results, tag access methods and diagnostics events.
| Package | What it adds |
|---|---|
Opc.Ua.AutoID |
The information model: node states, DataTypes, identifiers, encodeables and client proxies |
Opc.Ua.AutoID.Server |
Reader hosting on the Device Integration node manager, a provider contract for the reader hardware, the OPC 30010 methods and events, simulated readers |
Opc.Ua.AutoID.Client |
Reader discovery, typed scan and tag calls, device snapshots and an asynchronous stream of events |
Opc.Ua.Mcp.AutoID |
Nine bounded MCP tools; no tool changes a tag |
The AutoID sample runs a simulated RFID reader end to end.
Contents
- Model and dependencies
- What the model package contains
- Loading the model
- Scan data and scan results
- Hosting readers
- Providers
- Behavior of the methods
- Events
- Conformance
- Client
- MCP tools
- Sample
- Specification notes
- Provenance
Model and dependencies
| Item | Value |
|---|---|
| Package | OPCFoundation.NetStandard.Opc.Ua.AutoID |
| Model URI | http://opcfoundation.org/UA/AutoID/ |
| Model version | 1.01, published 2020-06-18 |
| Required models | OPC UA 1.03, Device Integration 1.01 (lower bounds) |
| Generated CLR namespace | Opc.Ua.AutoID |
| Source | UA-Nodeset latest/AutoID |
AutoIdDeviceType derives from the DI DeviceType, so the package references
Opc.Ua.Di and loads the DI model the stack ships, which is newer than the
required 1.01.
What the model package contains
- The device types
AutoIdDeviceType(abstract),RfidReaderDeviceType,OcrReaderDeviceType,OpticalReaderDeviceType,OpticalVerifierDeviceTypeandRtlsDeviceType, with their methods (Scan,ScanStart,ScanStop,ReadTag,WriteTag,WriteTagID,KillTag,LockTag,SetTagPassword,GetLocation, ...) as typed method states. - The event types
AutoIdScanEventTypewith one subtype per reader kind,AutoIdDiagnosticsEventType,AutoIdAccessEventType,RfidAccessEventType,AutoIdLogEntryEventTypeandAutoIdPresenceEventType. - The DataTypes: the
ScanDataandLocationunions, the abstractScanResultwithRfidScanResult,OcrScanResult,OpticalScanResult,OpticalVerifierScanResultandRtlsLocationResult, the access resultsAccessResultandRfidAccessResult,ScanSettings,RfidSighting,ScanDataEpc, the coordinate structures and six enumerations. AutoIdModel(model identity,GetNamespaceUris()) and theAddAutoIdTypeSystemregistration overloads.
The scan event subtypes and RfidAccessEventType narrow the DataType of their
ScanResult or AccessResult property to a structure subtype. The generated
states expose the property with that type, for example
RfidScanEventState.ScanResult is a
PropertyState<ArrayOf<RfidScanResult>>.
Loading the model
using Opc.Ua;
using Opc.Ua.AutoID;
// Encodeables (DI and AutoID); the caller commits the builder.
messageContext.Factory.Builder.AddAutoIdTypeSystem().Commit();
// Type nodes for a node manager's predefined nodes.
var predefinedNodes = new NodeStateCollection();
predefinedNodes.AddAutoIdTypeSystem(systemContext);
// Typed node states for instances of the AutoID types.
nodeStateFactory.AddAutoIdTypeSystem();
AddAutoIdTypeSystem loads DI before AutoID, registers missing namespaces and
skips roots that are already present, so it can be combined with other models
that also load DI.
Scan data and scan results
ScanData is a union: SwitchField selects the ByteString, String,
Epc or Custom arm. The scan and access results have optional fields;
set the corresponding bits in EncodingMask to send them.
var result = new RfidScanResult
{
CodeType = "EPC",
ScanData = new ScanData
{
SwitchField = ScanDataFields.Epc,
Epc = new ScanDataEpc { PC = 0x3000, UId = ByteString.From(epc) }
},
Timestamp = DateTimeUtc.Now,
Sighting = [new RfidSighting { Antenna = 1, Strength = -52 }]
};
The NodeSet defines binary and XML encodings; JSON uses the DataType NodeIds.
Hosting readers
A server has one Device Integration DeviceSet, and exactly one node manager
owns the DI namespace. Readers are therefore hosted in one of two ways:
AddAutoIdregisters a node manager of its own that loads the DI and AutoID models and creates one device per reader belowDeviceSet. Use it when AutoID is the only DI-based companion of the server.AddAutoIdToMachineryloads the AutoID model into the Machinery node manager ofAddMachinery, which then owns DI for every companion of the server; the readers are created in that manager, below the sameDeviceSetas the machines, pumps or scales. See Coexisting with a server that already owns DI.
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.AutoID.Server;
services.AddOpcUa()
.AddServer(options => { /* application name, endpoints, PKI */ })
.AddAutoId(options => options.MaximumScanDuration = TimeSpan.FromSeconds(10))
.ConfigureAutoId(async context =>
{
AutoIdReader reader = await context.Runtime
.Reader(new QualifiedName("DockDoor1", context.Manager.InstanceNamespaceIndex), hardware)
.WithDeviceInfo("Dock door antenna portal")
.WithDeviceLocationName("Dock 1, gate A")
.WithAntennaNames([new AntennaNameIdPair { AntennaId = 1, AntennaName = "Left post" }])
.WithIdentification(nameplate => nameplate.SerialNumber = "R100-0001")
.BuildAsync(context.CancellationToken);
});
Reader(...) returns a builder; nothing is registered before BuildAsync.
The builder materialises exactly the components the provider supports (a reader
without a Scan provider has no Scan method), registers the device, makes it
an event notifier and attaches the provider. A failure at any step is rolled
back, so the browse name can be used again, and concurrent builds of one name
admit exactly one. Disposing the runtime (the node manager does so on shutdown)
stops every reader's scans, detaches its provider and, for a provider the reader
owns (OwnsProvider()), disposes it.
AutoIdServerOptions bounds what a client can ask for:
| Option | Default | Effect |
|---|---|---|
MaximumScanDuration |
30 s | A synchronous Scan asking for more is Bad_InvalidArgument; a provider that does not return in time is cancelled and the call fails with Bad_Timeout |
MaximumTagReadLength |
65536 | A longer ReadTag is Bad_InvalidArgument |
MaximumScanResults |
1000 | More results fail the scan with Bad_ResponseTooLarge instead of being truncated |
PublishAccessEvents |
true |
Whether commands that reach a tag raise access events |
AdditionalNamespaceUris |
none | Extra namespaces to register |
The readers keep their scan state (Idle_0, Scanning_2, Busy_3, Error_1)
in DeviceStatus. AutoIdReader.SetFaultedAsync(true) reports Error_1: new
scans then return DEVICE_NOT_READY_17 and an active asynchronous scan stops.
SetDeviceLocation moves the DeviceLocation of a mobile reader.
Coexisting with a server that already owns DI
AddAutoId claims the Device Integration address space like AddOpcUaDi,
AddMachinery, AddPumps or AddScales, so registering it together with
another DI owner, in either order, throws InvalidOperationException while the
services are registered; the message names the current owner and the
composition API. The check is what keeps the address space consistent: two DI
owners would each load their own copy of DeviceSet, the server would route
DeviceSet to the first one, and readers created in the second one would not
be browsable.
To host readers next to machines, call AddAutoIdToMachinery after
AddMachinery and create the readers with ConfigureAutoIdFor<TNodeManager>.
Other Machinery-based companions compose into the same owner:
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.AutoID.Server;
using Opc.Ua.Machinery.Server;
services.AddOpcUa()
.AddServer(options => { /* application name, endpoints, PKI */ })
.AddMachinery(options =>
{
options.Parts = MachineryParts.BuildingBlocks;
options.InstanceNamespaceUri = "urn:acme:plant:instances";
})
.AddPumpsToMachinery() // optional, any *ToMachinery companion
.AddAutoIdToMachinery(options => options.MaximumScanDuration = TimeSpan.FromSeconds(10))
.ConfigureMachinery(async (machinery, ct) =>
{
await machinery.AddMachine(new QualifiedName("Press1", machinery.InstanceNamespaceIndex))
.WithIdentification(id =>
{
id.Manufacturer = new LocalizedText("Acme");
id.SerialNumber = "PR-1";
id.ProductInstanceUri = "urn:acme:press:1";
})
.BuildAsync(ct);
})
.ConfigureAutoIdFor<MachineryNodeManager>(async (context, ct) =>
{
AutoIdReader reader = await context.Runtime
.Reader(new QualifiedName("PressInfeed", context.Runtime.InstanceNamespaceIndex), hardware)
.WithDeviceInfo("Infeed portal of Press1")
.BuildAsync(ct);
});
| Item | In the composed server |
|---|---|
| Node managers | The MachineryNodeManager alone; no AutoIdNodeManager is created |
Device Integration model, DeviceSet |
Loaded once by the Machinery owner's core model provider |
| AutoID model | Added to the owner by AutoIdMachineryModelProvider, with AdditionalNamespaceUris |
| Readers | Below the owner's DeviceSet (Organizes), NodeIds in MachineryServerOptions.InstanceNamespaceUri; take browse names from context.Runtime.InstanceNamespaceIndex. WithParent places a reader below another node of the owner instead, for example a component of a machine |
Machines folder |
Readers are devices, not machines, and are not added to it |
| Methods, events, providers | As in the stand-alone server; the readers are event notifiers of the owner |
| Conformance | The runtime contributes the AutoID units and facets to the owner, which publishes them with the DI and Machinery ones |
| Lifetime | The owner disposes the runtime on shutdown: scans stop, providers detach, owned providers are disposed |
Successive ConfigureAutoIdFor<MachineryNodeManager> delegates run in
registration order in the Machinery configuration pipeline, before its builder
is sealed, and share one AutoIdRuntime; context.Manager is the Machinery
owner. ConfigureAutoIdFor<AutoIdNodeManager> works with AddAutoId and
receives the stand-alone runtime, so configuration code can be shared by both
hosting paths. Opc.Ua.AutoID.Server references Opc.Ua.Machinery.Server for
this; a stand-alone server still loads only the DI and AutoID models.
Direct composition and lifetime
Without dependency injection, pass new AutoIdMachineryModelProvider() together
with new MachineryModelProvider(MachineryParts.BuildingBlocks) to the
MachineryNodeManager constructor; providers are dependency-ordered even if
supplied in another order. Once the address space is created:
AutoIdRuntime autoId = owner.CreateAutoIdRuntime();
AutoIdReader reader = await autoId.CreateReaderAsync(
new QualifiedName("PressInfeed", autoId.InstanceNamespaceIndex), hardware, ct);
The first CreateAutoIdRuntime(options) call selects the options; later calls
return the same runtime, and on an AutoIdNodeManager it returns its Runtime.
The stand-alone AutoIdNodeManager is built from the same pieces: a Device
Integration owner that loads DI and then AutoIdMachineryModelProvider.
Any other Device Integration owner composes AutoID the same way, as with
Pumps or Scales: an application-owned DiNodeManager subclass loads the model in
its LoadPredefinedNodesAsync with nodes.AddAutoIdTypeSystem(context), which
keeps DI nodes that are already present, calls CreateAutoIdRuntime() (or is
configured through ConfigureAutoIdFor<TOwner>), and disposes the runtime
through DisposeMachineryResourcesAsync() from its asynchronous disposal.
AddAutoIdToMachinery is the registration shortcut for the stock owner's
model-provider pipeline; the plain AddOpcUaDi owner loads only DI and has no
such pipeline, so a server built on it uses the subclass route.
Providers
A reader is the OPC UA face of a hardware provider. The base contract
IAutoIdReaderProvider (kind, attach, detach) is enough for a device that scans
on its own (OPC 30010 6.1.1 option 3): the provider reports through the
IAutoIdReaderSink it receives when attached. Capabilities are added by
implementing more interfaces:
| Interface | Methods it enables |
|---|---|
IAutoIdScanProvider |
Scan |
IAutoIdAsyncScanProvider |
ScanStart, ScanStop |
IAutoIdRfidProvider (SupportedOperations) |
ReadTag, WriteTag, WriteTagID, KillTag, LockTag, SetTagPassword, each only when listed |
IAutoIdRtlsProvider |
GetLocation, GetSupportedLocationTypes |
The sink pushes ReportScanAsync (scan event and LastScanData),
CompleteScanAsync (a termination condition ended the asynchronous scan),
ReportPresenceAsync, ReportLogEntryAsync and ReportAccessAsync. Provider
calls are made without any runtime lock held, scans never overlap on one reader,
and the sink throws ObjectDisposedException after the reader was disposed.
Four simulated providers ship in the package and back the tests and the sample:
InMemoryRfidReaderProvider (an EPC Gen2 reader with memory banks, passwords,
locks and kill), InMemoryScanReaderProvider (OCR and optical),
InMemoryOpticalVerifierProvider and InMemoryRtlsProvider. Their scan cycle
defaults to one second (CycleInterval), so an unbounded ScanStart raises at
most one scan event per second; pass a cycleInterval to the constructor for
another pace. InMemoryRtlsProvider stores each object's location in the type it
was registered with. A locationConverter translates it into the other supported
types, for example from the local coordinate system the system was configured
with (OPC 30010 9.3.4) into WGS84:
// A site whose local origin is at 48 N 11 E, X pointing east and Y north, in metres.
var rtls = new InMemoryRtlsProvider(locationConverter: (location, type) =>
type == LocationTypeEnumeration.WGS84 && location.SwitchField == LocationFields.Local
? new Location
{
SwitchField = LocationFields.WGS84,
WGS84 = new WGS84Coordinate
{
N_S_Hemisphere = "N",
Latitude = 48.0 + (location.Local.Y / 111_320.0),
E_W_Hemisphere = "E",
Longitude = 11.0 + (location.Local.X / 74_600.0)
}
}
: null);
Without a converter, or when it returns null, GetLocation for another type
answers Bad_InvalidState.
Behavior of the methods
| Method | Result codes and semantics |
|---|---|
Scan (6.1.3.4) |
Bad_InvalidArgument for an infinite scan (duration 0, cycles 0, no DataAvailable), a negative or non-finite value, an undefined location type or a duration above the limit; Bad_InvalidState while another scan is active. The first result becomes LastScanData/LastScanTimestamp with one source timestamp |
ScanStart (6.1.3.5) |
Infinite settings allowed; Bad_InvalidState while a scan is active. The scan stops through ScanStop, when the provider completes it, when the starting session closes, or at shutdown |
ScanStop (6.1.3.6) |
Bad_InvalidState when no scan is active; any session may stop a scan |
ReadTag, WriteTag, WriteTagID, KillTag, LockTag, SetTagPassword |
A malformed identifier, empty code type, over-long read or undefined enumeration is Bad_InvalidArgument. Everything the tag decides (PASSWORD_ERROR_4, OUT_OF_RANGE_ERROR_7, ...) is returned in Status with a good call result |
GetDeviceLocation (6.1.3.7) |
Available when a location is configured. Same mapping as GetLocation: an undefined location type is Bad_InvalidArgument, a location that is not available in the requested type Bad_InvalidState. 6.1.3.7 lists only the Call Service codes of OPC 10000-4 5.12.2, where Bad_NotSupported would mean the method is not supported for the object; 5.12.2.4 allows reusing a code with the same canonical meaning |
GetLocation, GetSupportedLocationTypes (6.6.3) |
The result carries the location in the requested type. An unsupported location type is Bad_InvalidArgument, an unknown object Bad_NotFound, and a supported type the object's location is not available in Bad_InvalidState (6.6.3.2: not available, for example due to the configuration) |
A method the provider does not support is not instantiated; calling it fails with the stack's status for an unpublished method.
Events
Scan results are raised as the scan event of the reader kind
(RfidScanEventType, OcrScanEventType, OpticalScanEventType,
OpticalVerifierScanEventType, RtlsLocationEventType) with DeviceName and
the typed ScanResult array. AutoIdPresenceEventType and
AutoIdLogEntryEventType come from the sink. Commands that reached an
identifier (a scan with results, and each tag command except those answering
NO_IDENTIFIER_8, MULTIPLE_IDENTIFIERS_9, NOT_SUPPORTED_BY_DEVICE_15,
DEVICE_NOT_READY_17, INVALID_CONFIGURATION_18 or DEVICE_FAULT_20) raise
RfidAccessEventType on RFID readers and AutoIdAccessEventType otherwise, with
Client, Command and the access results. Client names the client
application that originated the command (OPC 30010 6.1.3.3 and 7.10): the
application name of the client description the session was created with, else
its application URI, else OPC UA client. The results of an asynchronous scan
name the client that called ScanStart; scans the device reports on its own name
AutoID device.
Conformance
The runtime contributes the OPC 30010 conformance units and server facets (Tables 82 to 85) of the readers it hosts; a facet is advertised only when a reader implements all its mandatory units.
| Unit | Implemented when |
|---|---|
AutoID DeviceType |
A reader exists |
AutoID Simple Sync Access |
A reader exists (LastScanData is updated by every scan) |
AutoID Sync Access |
A reader has a Scan method |
AutoID Async Access |
A reader exists (scan events) |
AutoID Async Access Control |
A reader has ScanStart and ScanStop |
The Simple Sync, Base Sync and Base Async facets are advertised
accordingly. AutoID Simple Sync Access Control (the ScanActive variable and
the ScanSettings function group) and AutoID Device Parameters (the
RuntimeParameters group) are not implemented, so the Full AutoID Server Facet
is never advertised. DeviceInfo, DeviceLocation, DeviceLocationName and
AntennaNames are published when configured.
Client
Opc.Ua.AutoID.Client wraps the generated ObjectType proxies:
using Opc.Ua.AutoID.Client;
AutoIdClient autoId = session.AutoId(telemetry); // or AddAutoIdClient() in DI
await foreach (AutoIdReaderEntry reader in autoId.EnumerateReadersAsync())
{
AutoIdDeviceSnapshot device = await autoId.ReadDeviceAsync(reader);
AutoIdScanResponse scan = await autoId.ScanAsync(
reader, new ScanSettings { Duration = 1000, DataAvailable = true });
}
await foreach (AutoIdScanEventTypeRecord e in
autoId.ObserveScanEventsAsync(session.DefaultStreaming, reader, ct))
{
// e.ScanResult holds RfidScanResult, OcrScanResult, ... instances
}
Discovery finds the devices whose type derives from one of the five concrete
reader types. A browse the server rejects throws ServiceResultException rather
than yielding an empty list, a rejected method call throws with the server's
status, and every asynchronous member honours cancellation. The status a device
reports for a tag command is returned as data. AutoIdReadOptions narrows
ReadDeviceAsync: Default reads everything the device publishes, StatusOnly
reads two variables. Whatever the options, the snapshot carries the NodeIds of
DeviceStatus, LastScanData, LastScanTimestamp and DeviceLocation and the
browse names of the methods the device publishes, taken from the same browse:
AutoIdDeviceSnapshot device = await autoId.ReadDeviceAsync(reader, AutoIdReadOptions.StatusOnly);
if (device.HasMethod(BrowseNames.ScanStart))
{
// monitor device.DeviceStatusId and device.LastScanDataId, then start the scan
}
Observe*Async methods (scan, access, presence and log entry events) take a
streaming subscription the caller owns; the monitored item lives as long as the
enumeration.
MCP tools
AddOpcUaMcpAutoId() and WithOpcUaAutoIdTools(McpToolProfile.AutoId) (profile
autoid) add autoid_list_readers, autoid_read_device, autoid_scan,
autoid_scan_start, autoid_scan_stop, autoid_observe_scan_events,
autoid_read_tag, autoid_get_supported_location_types and
autoid_get_location. No tool writes, locks or kills a tag or carries a
password. See the MCP server guide.
Sample
samples/AutoID contains AutoIdServer, a dock door with
a simulated RFID portal whose pallets enter and leave the antenna field, and
AutoIdClient, which discovers the reader, scans, reads and writes tag memory
and prints the scan and presence events. Test-AutoIdSamplePair.ps1 runs both
and checks the output.
Specification notes
- OPC 30010 6.1.3.16 prescribes the
AutoIdModelVersionstring"1.00"although the released model is 1.01; the server publishes"1.00". - The NodeSet orders the
GetLocationarguments identifier, location type, code type; the specification text orders them identifier, code type, location type. The generated proxy and the server follow the NodeSet. - The NodeSet declares the
Scanmethod ofOpticalVerifierDeviceTypewithWGS84Coordinateresults where OPC 30010 6.4.3.1 namesOpticalVerifierScanResult. The method cannot be served consistently, so a verifier does not publishScan; it reports throughScanStartand scan events, andAutoIdClient.ScanAsyncrefuses a verifier. - The
GetUnitsmethod ofRtlsDeviceTypedeclares no output argument; it is not published.
Provenance
src/Opc.Ua.AutoID/Model holds the unmodified NodeSet and identifier table
from UA-Nodeset revision 9f6c7660b94af24d32c4248d4b13bf60adab0301;
provenance.json records the source URLs and SHA-256 hashes. The published
identifier table predates model 1.01 and lacks 67 NodeSet declarations, so
Opc.Ua.AutoID.Derived.NodeIds.csv is generated from it with
tools/nodesets/derive-identifier-table.py and used as the source generator's
drift guard. NodeIds always come from the NodeSet.
The AutoID tests pin the file hashes, compare the generated identifiers with the NodeSet and the identifier table, load the model with DI, and round-trip the DataTypes through the binary, JSON and XML encodings.
| Fixture | Covers |
|---|---|
AutoIdHostingTests |
AddAutoId registration, DI ownership, options, node manager and client factories |
AutoIdCompositionTests |
Stock Machinery owner, ordered providers, DI loaded once, instance ownership, rollback, cancellation, runtime disposal, a second DI owner rejected |
AutoIdMachineryCompositionEndToEndTests |
Machinery, Pumps and AutoID in one hosted server, read back by the AutoID, Machinery and Pumps clients over a real session, with the published profiles and conformance units |
AutoIdReaderEndToEndTests |
The stand-alone server driven by AutoIdClient over a real session |