# 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>
OPC 30010 AutoID samples
Two console applications that show the Opc.Ua.AutoID* libraries end to end: a
server that publishes a simulated RFID reader, and a client that discovers it,
scans, reads and writes tag memory and prints the events.
| Sample | What it does |
|---|---|
| AutoIdServer | Serves the reader DockDoor1 backed by InMemoryRfidReaderProvider: two pallets in range, a third entering and leaving every two seconds, a triggered scan every six |
| AutoIdClient | Discovers the readers, prints the device, runs a synchronous scan, reads and writes tag memory, then starts a scan and prints scan and presence events |
The AutoID guide explains every API used here.
Run
In two terminals:
dotnet run --project samples/AutoID/AutoIdServer -- --port 62549
dotnet run --project samples/AutoID/AutoIdClient -- --insecure --seconds 10
--insecure makes the client accept the server's self-signed certificate;
never use it in production. The client also takes --url <endpoint> (default
opc.tcp://localhost:62549/AutoIdServer) and --seconds <n> (how long to watch
events, 0 to skip). The server takes --port, --host, --pki-root,
--autoaccept and --run-seconds. The default port is in the dynamic range and
is not assigned by IANA.
The client prints something like:
Rfid reader DockDoor1 (ns=1;i=4146858853)
DockDoor1: Idle, Example Automation RFID portal R-100, serial R100-0001
Dock door antenna portal at Dock 1, gate A
antennas: 1=Left post, 2=Right post
Scan: SUCCESS, 2 tag(s)
E28011600000200000000101 antenna 2, -48 dBm
E28011600000200000000201 antenna 1, -51 dBm
ReadTag user memory: SUCCESS, "PALLET-0001"
WriteTag: SUCCESS; read back: "CHECKED"
ScanStart, watching scan events for 10 s ...
ScanStart: SUCCESS
scan event at 10:15:31: 000101, 000201
presence: 3 tag(s) in range
scan event at 10:15:32: 000101, 000201, 000301
ScanStop: stopped
AutoIdClient/Test-AutoIdSamplePair.ps1 starts the server on a free port, runs the
client and checks the output:
./samples/AutoID/AutoIdClient/Test-AutoIdSamplePair.ps1 `
-ServerExecutable samples/AutoID/AutoIdServer/bin/Debug/net10.0/AutoIdServer.dll `
-ClientExecutable samples/AutoID/AutoIdClient/bin/Debug/net10.0/AutoIdClient.dll