Files
UA-.NETStandard/docs/DeviceIntegration.md
T
Marc Schier 1bb4bdca1d Add Machine Vision Parts 1 and 2 (#4733)
# Description

Implement the released [OPC
40100-1](https://reference.opcfoundation.org/specs/OPC-40100-1) and [OPC
40100-2](https://reference.opcfoundation.org/specs/OPC-40100-2)
specifications with generated models, provider-backed runtimes, typed
clients, MCP workflows, and DI/Machinery integration.

- **Part 1 stays in `Opc.Ua.Vision`** and its client/server packages
alongside the distinct Perception model. Provide control/acquisition
state machines, jobs and synchronized steps,
recipe/product/configuration/result management, file transfers,
diagnostics, events, and conditions.
- **Part 2 is in `Opc.Ua.MachineVision`, `.Client`, and `.Server`.**
Provide asset categories, identification, image-handling aspects,
health/maintenance data, topology, and transactional updates. The base
Vision assemblies do not reference these optional packages; the Part 2
wire model does not depend on Part 1.
- Preserve the release-pinned model bytes, wire namespaces and NodeIds.
Perception types are in `Opc.Ua.Vision.Perception`; the AMCM generated
namespace remains `Opc.Ua.Vision.MachineVision.AMCM`, and asset
client/server extension namespaces remain `Opc.Ua.Vision.Client` and
`Opc.Ua.Vision.Server`.
- Use `AddOpcUaMachineVision` and `AddOpcUaMachineVisionAMCM` for model
loading and factory registration. Each has node-collection,
encodeable-factory-builder and node-state-factory-builder overloads.
These short entry points delegate to the generated implementations;
existing namespace-derived generated names remain available for
compatibility.
- Support multiple independent named systems/assets under one
namespace-owning manager. Reuse injectable providers, file binding,
bounded memory stores, optional transactional `IFileSystemProvider`
persistence, state machines, telemetry and conformance infrastructure.
- Keep one `docs/Vision.md` guide, titled `Machine Vision (OPC 40100-1
and OPC 40100-2)`, and one `Opc.Ua.Mcp.Vision` package. Include
deterministic server/client samples.
- Expose 17 Machine Vision MCP tools, including capabilities/status,
bounded relationships and observation, complete small-file
uploads/downloads, management, explicit control, conditions, and chunked
transfers. Catalogue counts: 39 Vision-specific, 43 with connection
tools, 81 Vision+Robotics, 155 Full. Every Machine Vision wire parameter
has a schema description.

**Compatibility and limits:** `AddVision()` remains Perception-only
unless released parts are selected. Standalone Part 2 uses
`AddVision().AddMachineVision()`; Machinery composition uses
`AddVisionToMachinery()`. Existing tool defaults and command signatures
are preserved. Observation owns and cleans up its subscription: 5
seconds/50 notifications by default, capped at 60 seconds/100
notifications. Complete-file tools are capped at 1 MiB; larger files use
chunked transfers. Domain Error values remain distinct from UA service
failures.

**Versioning:** the seven Vision/MachineVision/MCP packages and their
Debug variants follow the repository root version instead of being
forced to preview. Vision.OpenUsd and its descendants remain
preview-only. Development builds still inherit the root prerelease
version. No release version, tag, release branch, publication policy, or
feed promotion is changed.

**Review actions:** all fourteen review threads have action/evidence
replies and are resolved. Changes include the unified guide, Perception
namespace, separate Part 2 packages and final naming, multi-system
ownership explanation, interrupted-write cleanup, observer failure
isolation, complete MCP parameter descriptions, and explicit package
inventory. The preview-layout migration topic and the requested
ReleaseProcess caveat were removed. The test Publish collector now
retries recognized transient failures within one cancellation budget and
propagates other exceptions. Commit 69b1f5db9 applies the requested
short loader names and renames standalone hosting to `AddMachineVision`,
with callers, tests and documentation updated. The intervening master
merge is preserved.

### Verification

**Latest API naming changes, verified locally:**

| Scope | Result |
| --- | --- |
| Full Vision suite, net10 | 572 passed, 1 existing skip, 0 failed |
| Full Vision suite, net48 | 515 passed, 0 skipped, 0 failed |
| Final loader/hosting cases after cross-TFM null-guard adjustment | 9
passed, 0 failed, 0 skipped |
| Machine Vision MCP and profile cases | 198 passed, 0 failed, 0 skipped
|
| Main and MCP AOT test-project compilation | Both built with 0
warnings/errors |
| MCP AOT companion, managed execution | 5 passed, 0 failed, 0 skipped;
not claimed as native execution |

`ModelLoaderNamesRegisterTypedFactories`,
`ModelLoaderNamesRejectNullArguments`, and
`GeneratedModelsLoadTogetherAndCreateTypedInstances` cover all six
loader overloads, fluent return identities, typed instances, namespace
coexistence and null guards. Existing standalone Part 2 tests verify
idempotent registration, one-owner enforcement and null handling through
`AddMachineVision`. No source-generator implementation, CLR namespace,
wire identity or model payload was changed for these naming updates.

The current 69b1f5db9 revision is running
[CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826937),
[package
validation](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165823359),
[MCP
usability](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826874),
[CodeQL](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826906),
and [Images
CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826884).
These runs are not claimed as completed results.

**Earlier completed verification:** all seven default-TFM package
archives were inspected after the package rename: exact IDs, 27 clean
DLLs, correct dependencies/readmes/licenses, and unchanged pinned model
hashes. [CI on
c75ec4fa6](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38142519760)
passed, including the formerly failing Linux batch and required summary.
Its unchanged coverage gate passed at **88.56% lines, 78.96% branches,
84.28% patch** against the 75% patch floor. The later
bounded-Publish-helper regressions reproduced 13 failures before the fix
and passed afterward; the full mainline triggering fixture passed 67
tests on each of net10/net48. These are historical results, not current
full-matrix results.

<details>
<summary>Executed API naming validation commands and outcomes</summary>

Run from the worktree root using the established artifact trees.
Dependency builds were performed before the no-build full net10 run; the
focused loader/hosting selection was rebuilt and rerun after the final
null-guard adjustment.

```powershell
$files = 'C:\Users\mschier\.copilot\session-state\deb2ba10-b0e9-4a44-8783-8331bc850814\files'
$env:DOTNET_CLI_USE_MSBUILD_SERVER = '0'
$env:TEMP = 'C:\Temp\mv-deb2\followup'
$env:TMP = $env:TEMP

# Passed: 572; Failed: 0; Skipped: 1.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-build --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net10" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

# Passed: 9; Failed: 0; Skipped: 0, on the final source.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~MachineVisionModelTests|FullyQualifiedName~StandalonePartTwoRegistrationUsesOneOwnerAndOneModelProvider|FullyQualifiedName~StandalonePartTwoRequiresRegistrationAndRejectsCompetingOwnership' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-model" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

# Passed: 198; Failed: 0; Skipped: 0.
dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~VisionMachineToolsTests|FullyQualifiedName~McpHostBuilderTests|FullyQualifiedName~McpToolProfileCompositionTests' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-mcp" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

$env:TEMP = 'C:\Temp\mv-deb2\followup48'
$env:TMP = $env:TEMP

# Passed: 515; Failed: 0; Skipped: 0.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Debug -f net48 --no-restore --artifacts-path "$files\solution-net48-artifacts" -p:CustomTestTarget=net48 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net48-final" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup48'
```

Opt-in long-running/stress/durable tiers have not been rerun locally;
the unqualified **all tests** checklist item remains open. Original
implementation matrix counts are not presented as current results.

</details>

## Related Issues

- Fixes #4732.

The requester reviewed and approved the implementation plan and
subsequent packaging/API decisions in the development session. The
tracking issue records the design; maintainer architectural
discussion/sign-off there is still pending. This PR remains a draft.

## 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.
- [x] 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-12 07:01:27 +00:00

45 KiB

OPC UA Device Integration (DI)

Glossary: In this document, DI refers to OPC UA Device Integration (companion specification OPC 10000-100), not to .NET Dependency Injection (covered in DependencyInjection.md). Wherever this document uses the unqualified initialism "DI" it always means the companion spec; .NET-DI mentions are spelled out as "Dependency Injection (.NET DI)" or appear with Microsoft.Extensions.DependencyInjection in context.

End-to-end developer guide for the Opc.Ua.Di* library trio shipped with this repository and the services.AddOpcUa() hosting model that plugs it together.

Contents

Library layout

Library Role
Opc.Ua.Di Model assembly: source-generated NodeId tables, DataTypes, ObjectType client proxies, AddOpcUaDi(NodeStateCollection) extension.
Opc.Ua.Di.Server Server: DiNodeManager, fluent topology/device builders, locking service, software-update package store, hosting integration.
Opc.Ua.Di.Client Client: DiDeviceClient, DiDiscoveryClient, DiTopologyClient, DiLockClient, SoftwareUpdateClient, hosting integration.

The running example is samples/DI/PumpDeviceIntegrationServer, a companion-spec server that exposes a configurable number of fully simulated PumpType instances. The sample uses the hand-written fluent node-manager style for the common pump model and simulation wiring, then uses ConfigureDevicesFor with the topology-element builder to add an ad-hoc Diagnostics functional group to each pump. The simulated asset is specified by its own product datasheet, samples/DI/PumpDeviceIntegrationServer/DATASHEET.md, which maps every nameplate field, engineering range and alarm trip point to its OPC UA browse path.

Quick start

Plain Device Integration (DI) server

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
    .AddOpcUa()
    .AddServer(o =>
    {
        o.ApplicationName = "MyDiServer";
        o.EndpointUrls.Add("opc.tcp://localhost:48010/MyDiServer");
    })
    .AddOpcUaDi()
    .ConfigureDevicesFor<DiNodeManager>(async ctx =>
    {
        var device = await ctx.CreateDeviceAsync(
            new QualifiedName("Sensor_1", ctx.Manager.InstanceNamespaceIndex));
        device.WithIdentification(id =>
        {
            id.Manufacturer = new LocalizedText("Acme");
            id.SerialNumber = "SN-001";
            id.DeviceClass = "Sensor";
        });
    });

await builder.Build().RunAsync();

Companion-spec server (Pumps + Device Integration)

builder.Services
    .AddOpcUa()
    .AddServer(o => { ... })
    .AddNodeManager<Pumps.PumpNodeManagerFactory>()
    // Pump factory already loads OPC UA Device Integration via its LoadPredefinedNodesAsync
    // direct chain; do NOT call AddOpcUaDi() — that would
    // double-register the OPC UA Device Integration namespace.
    .ConfigureDevicesFor<Pumps.PumpNodeManager>(ctx =>
    {
        var manager = (Pumps.PumpNodeManager)ctx.Manager;
        foreach (NodeId pumpNodeId in manager.PumpNodeIds)
        {
            ITopologyElementBuilder<PumpState> pump =
                ctx.TopologyElement<PumpState>(pumpNodeId);
            pump.WithFunctionalGroup(
                new QualifiedName("Diagnostics", manager.InstanceNamespaceIndex),
                group => group.Configure(node =>
                    node.WithProperty("LastError", Variant.From(string.Empty))));
        }

        return ValueTask.CompletedTask;
    });

Client

builder.Services
    .AddOpcUa()
    .AddClient(o => { o.Configuration = ...; o.Session.Endpoint = ...; })
    .AddOpcUaDi();

public sealed class MyAppService(
    IDiDiscoveryService discovery,
    Func<NodeId, CancellationToken, ValueTask<DiDeviceClient>> deviceFactory,
    Func<CancellationToken, ValueTask<DiTopologyClient>> topologyFactory)
{
    public async Task PrintAsync(CancellationToken ct)
    {
        DiTopologyClient topology = await topologyFactory(ct);
        await foreach (TopologyEntry entry in topology.EnumerateDevicesAsync(ct))
        {
            DiDeviceClient device = await deviceFactory(entry.NodeId, ct);
            DeviceIdentification id = await device.ReadIdentificationAsync(ct);
            Console.WriteLine($"{id.Manufacturer} {id.Model} ({id.SerialNumber})");
        }
    }
}

Device builder

The IDeviceBuilder<TDevice> fluent surface is the recommended way to create and configure Device Integration device instances programmatically. It lives in Opc.Ua.Di.Server.Builders and integrates with the broader fluent API for node managers.

Runtime-created NodeIds use the server namespace. DiNodeManager now mints NodeIds for device and topology instances in InstanceNamespaceIndex, the server application's namespace derived from ApplicationConfiguration.ApplicationUri, rather than in the DI or companion-spec namespace of the parent. This follows the companion-spec modelling rules (for example OPC 10000-100 §13.4 and OPC 40223): nodes not defined by a specification must not use the standard namespace. The local server defines such nodes in its Local Server URI namespace. BrowseNames defined by DI or another companion specification keep that specification's namespace. Use InstanceNamespaceIndex for application-defined instance names, functional groups, and properties.

Entry points

All entry points live on DiNodeManager:

// Default DeviceState organized by the Device Integration DeviceSet (or attached
// below whatever ResolveDefaultDeviceParent() returns).
ValueTask<IDeviceBuilder<DeviceState>> CreateDeviceAsync(
    QualifiedName browseName,
    NodeState? parent = null,
    CancellationToken ct = default);

// Typed factory for ComponentType-derived companion-spec devices.
ValueTask<IDeviceBuilder<TDevice>> CreateDeviceAsync<TDevice>(
    QualifiedName browseName,
    NodeId typeDefinitionId,
    Func<NodeState, TDevice> factory,
    NodeState? parent = null,
    CancellationToken ct = default)
    where TDevice : ComponentState;

// Wrap an existing device.
IDeviceBuilder<TDevice> Device<TDevice>(TDevice device)
    where TDevice : ComponentState;
IDeviceBuilder<TDevice> Device<TDevice>(NodeId nodeId)
    where TDevice : ComponentState;
IDeviceBuilder<TDevice> DeviceByBrowseName<TDevice>(
    QualifiedName browseName,
    NodeState? parent = null)
    where TDevice : ComponentState;

// Wrap an existing TopologyElementType-derived companion instance.
ITopologyElementBuilder<TElement> TopologyElement<TElement>(TElement element)
    where TElement : TopologyElementState;
ITopologyElementBuilder<TElement> TopologyElement<TElement>(NodeId nodeId)
    where TElement : TopologyElementState;
ITopologyElementBuilder<TElement> TopologyElementByBrowseName<TElement>(
    QualifiedName browseName,
    NodeState? parent = null)
    where TElement : TopologyElementState;

CreateDeviceAsync performs five steps:

  1. Resolves the parent (default: Device Integration DeviceSet; subclasses override ResolveDefaultDeviceParent() — e.g. machinery managers can return the Machines folder).

  2. Uses Organizes for the well-known DI DeviceSet; explicit custom parents retain HasComponent.

  3. Fails fast if a child with the same browse name already exists (StatusCodes.BadBrowseNameDuplicated).

  4. Materializes the instance through the source-generated CreateInstanceOf<Type> factory, such as CreateInstanceOfDeviceType. The factory adds the type's mandatory children and HasInterface references. For DeviceType, the children are eight nameplate variables: Manufacturer, Model, HardwareRevision, SoftwareRevision, DeviceRevision, DeviceManual, SerialNumber, and RevisionCounter. It assigns each variable a DI-namespace BrowseName.

    Because the factory receives a BrowseName, it also rebases the subtree onto per-instance NodeIds in the server's application namespace (DiNodeManager.InstanceNamespaceIndex, the Local Server URI namespace). This prevents instances of the same type from colliding on the generator's type NodeIds. It also keeps application nodes out of DI and other companion-spec namespaces. Finally, the factory sets BrowseName, SymbolicName, and DisplayName, and stamps TypeDefinitionId.

  5. Calls the real AsyncCustomNodeManager.AddPredefinedNodeAsync so the node create lifecycle, subscription wiring, type-tree registration, and root-notifier propagation all happen exactly as for nodes loaded from a NodeSet2.

Machinery-aware subclasses can additionally reference managed machines from the OPC 40001 Machines entry point. DiNodeManager's helper adds an Organizes reference to the existing instance instead of re-parenting it, so the same machine remains organized by DeviceSet while also being browsable from Machines.

Fluent surface

IDeviceBuilder<TDevice>
    .WithIdentification(action<DeviceIdentificationData>)
    .WithIdentificationGroup(action<IFunctionalGroupBuilder>)
    .WithConfigurationGroup(action<IFunctionalGroupBuilder>)
    .WithMaintenanceGroup(action<IFunctionalGroupBuilder>)
    .WithDiagnosticsGroup(action<IFunctionalGroupBuilder>)
    .WithStatusGroup(action<IFunctionalGroupBuilder>)
    .WithOperationalGroup(action<IFunctionalGroupBuilder>)
    .WithStatisticsGroup(action<IFunctionalGroupBuilder>)
    .WithOperationCountersGroup(action<IFunctionalGroupBuilder>)
    .WithFunctionalGroup(QualifiedName name, action<IFunctionalGroupBuilder>)
    .ConnectsTo(NodeId other)
    .ConnectsToParent(NodeId parent)
    .Configure(action<TDevice, ISystemContext>)
    .WithDeviceHealth(DeviceHealthEnumeration)   // extension; requires TDevice : DeviceState

ITopologyElementBuilder<TElement> is the parallel surface for generated companion types that derive directly from TopologyElementType. It exposes only common topology operations: functional groups, ConnectsTo references, typed node access, and raw state configuration. The internal device-builder implementation reuses the same behavior without changing the existing public IDeviceBuilder<TDevice> contract. Component nameplates, support information, software update, lifetime indications, and DeviceHealth remain on their narrower component/device builders.

Identification properties

WithIdentification(action) populates the standard Device Integration nameplate properties on the device. The mutable DeviceIdentificationData record holds:

  • Manufacturer (LocalizedText, IsNull = unset)
  • ManufacturerUri (string?)
  • Model (LocalizedText, IsNull = unset)
  • HardwareRevision, SoftwareRevision, DeviceRevision, ProductCode, DeviceManual, DeviceClass, SerialNumber, ProductInstanceUri (string?)
  • RevisionCounter (int?)

Properties that exist as typed children on the device are updated in place. The eight mandatory nameplate properties are always present (materialised by CreateInstanceOfDeviceType), so setting them takes the in-place path. Optional nameplate properties (ManufacturerUri, ProductCode, ProductInstanceUri, DeviceClass) are created on demand via NodeState.AddProperty<T, VariantBuilder>, re-qualified into the DI namespace (never namespace 0), and registered with the manager.

DI companion-spec compliance

Devices created through CreateDeviceAsync are modelled to the DI companion spec: mandatory DeviceType children are present with DI-namespace BrowseNames, the type's HasInterface references are carried, and the software-update state-machine method children point at their in-type MethodDeclarationIds. The model is intentionally lean — OPC UA does not require optional interface members (IVendorNameplateType/ITagNameplateType/ISupportInfoType members such as AssetId, ProductCode, DeviceTypeImage, Documentation, ImageSet, ProtocolSupport) to be instantiated, so they are omitted unless the application adds them. An external compliance checker may still flag omitted optional members. It may also report placeholder warnings for application-added HasComponent children, such as a custom Diagnostics functional group or the SoftwareUpdate facet. The specification permits these omissions and extensions; they are not modelling errors.

Functional groups

Common Device Integration functional groups have typed builder methods. Spec-defined groups keep their standard BrowseName namespace, for example Identification, Maintenance, and Operational in the DI namespace and Events in the Pumps namespace. Application-defined groups use the server instance namespace; the diagnostics helper follows that rule because the diagnostics group it creates is vendor-defined. Arbitrary group names go through WithFunctionalGroup(qualifiedName, action). Each call is idempotent: invoking the same accessor twice reuses the existing group. The order of operations inside the builder is:

  1. Create the FunctionalGroupState child (via TopologyElementState.AddIdentification or AddGroupIdentifier).
  2. Normalise BrowseName, SymbolicName, DisplayName, NodeId (via Context.NodeIdFactory) and TypeDefinitionId.
  3. Register the group with the manager via AddPredefinedNodeAsync.
  4. Hand the IFunctionalGroupBuilder to the configure delegate.
device.WithMaintenanceGroup(fg =>
{
    fg.Organizes(device.Manufacturer!.NodeId);
    fg.Node.WithProperty("LastMaintenanceDate", DateTime.UtcNow);
});

For vendor-specific groups, use the non-typed WithFunctionalGroup(QualifiedName, action) overload. The browse name's namespace index controls where the group lives; pass DiNodeManager.InstanceNamespaceIndex for application-defined groups. Reserve DI, Pumps, Machinery, or other companion-spec BrowseName namespaces for nodes that are defined by those specifications. The PumpDeviceIntegrationServer sample demonstrates this on Pump #2 to surface a custom Diagnostics folder:

pump.WithFunctionalGroup(
    new QualifiedName("Diagnostics", ctx.Manager.InstanceNamespaceIndex),
    fg => fg.Configure(node =>
        node.WithProperty("LastError", string.Empty)
            .WithProperty("ErrorCount", 0)
            .WithProperty("LastSelfTest", DateTime.UtcNow)));

Topology references

ConnectsTo(NodeId) adds a forward Opc.Ua.Di.ReferenceTypes.ConnectsTo reference; ConnectsToParent(NodeId) adds the inverse. Useful for declaring physical / logical topology relationships outside the hierarchical address-space tree.

Device health

WithDeviceHealth(DeviceHealthEnumeration) is an extension method constrained to TDevice : DeviceState (where the typed DeviceHealth child variable exists). Throws StatusCodes.BadInvalidState if the device was constructed without the DeviceHealth child — typically because the factory was p => new DeviceState(p) rather than the generator-produced CreateDeviceType factory. Callers can pre-populate the child via Configure((dev, ctx) => ...) to avoid the error.

Subclass support

Companion-spec managers (PumpNodeManager, machinery managers) inherit from DiNodeManager and gain the entire builder surface for free. They typically override ResolveDefaultDeviceParent() to return the companion-spec-specific container (Machines folder, etc.) and supply their own typed factories to CreateDeviceAsync<TPumpType>(...).

Device sub-type extensions

Three extension surfaces on top of IDeviceBuilder<TDevice> cover optional OPC 10000-100 features:

Software / Block / ConfigurableObject (§5.4 advanced sub-types)

DeviceBuilderTypeExtensions adds typed materialisation helpers for the §5.4 sub-types that are not provided by WithDeviceType itself:

SoftwareState           sw    = builder.AddSoftware(qn("Firmware"));
BlockState              blk   = builder.AddBlock(qn("InputBlock"));
ConfigurableObjectState cfg   = builder.AddConfigurableObject(qn("DriverFolder"));

Each helper creates a typed child under the device, stamps a manager-assigned NodeId via INodeIdFactory, sets the HasComponent reference, registers the child via AddPredefinedNodeAsync, and invokes an optional configure callback. The returned typed state is ready for further configuration through the standard NodeState APIs.

Lifetime indication (§10.6)

DeviceBuilderLifetimeExtensions.AddLifetimeIndication creates a LifetimeVariableState under the device with a LifetimeIndicationKind classifier covering all six §10.6 indication sub-types:

device.AddLifetimeIndication(
    qn("OperatingHours"),
    LifetimeIndicationKind.Time,
    startValue: 0.0);

device.AddLifetimeIndication(
    qn("PartsProduced"),
    LifetimeIndicationKind.NumberOfParts,
    startValue: 0.0);

The classifier enum maps directly to OPC 10000-100 ObjectType ids:

LifetimeIndicationKind OPC UA ObjectType
Time TimeIndicationType
NumberOfParts NumberOfPartsIndicationType
NumberOfUsages NumberOfUsagesIndicationType
Length LengthIndicationType
Diameter DiameterIndicationType
SubstanceVolume SubstanceVolumeIndicationType

DeviceBuilderLifetimeExtensions.ResolveIndicationTypeId(kind, namespaceUris) resolves the matching NodeId at runtime when applications need to materialise the indication classifier object themselves.

Support info (§5.15)

DeviceBuilderSupportInfoExtensions.WithSupportInfo creates an ISupportInfoState child on the device (idempotent — re-uses the existing instance on subsequent calls) and yields it through a configure callback. The interface exposes DocumentationFiles / ImageSet / ProtocolSupport folder properties; consumers populate them by attaching FileState or BaseObjectState children through the standard NodeState API. File-backed children commonly use FileState wired to an IFileSystemProvider from src/Opc.Ua.Server/FileSystem.

device.WithSupportInfo(info =>
{
    info.Description = new LocalizedText("Support information for device 1");
    // attach FileState children for DocumentationFiles / ImageSet
    // / ProtocolSupport folders here, using IFileSystemProvider
    // for content backing.
});

Hosting integration

Use AddOpcUaDi() and ConfigureDevicesFor<TNodeManager>() to add the Device Integration library to the unified AddOpcUa() Microsoft.Extensions.DependencyInjection hosting pattern.

Server-side surface

Microsoft.Extensions.DependencyInjection.OpcUaServerDiBuilderExtensions adds three methods on IOpcUaServerBuilder:

IOpcUaServerBuilder AddOpcUaDi();

IOpcUaServerBuilder ConfigureDevicesFor<TNodeManager>(
    Action<IDiPostSetupContext> configure)
    where TNodeManager : DiNodeManager;

IOpcUaServerBuilder ConfigureDevicesFor<TNodeManager>(
    Func<IDiPostSetupContext, ValueTask> configure)
    where TNodeManager : DiNodeManager;

When to use AddOpcUaDi()

Call this when you want a plain Device Integration (DI) server without any companion-spec subclass. It registers the DiNodeManagerFactory so the hosted service stands up a pure DiNodeManager.

services.AddOpcUa()
    .AddServer(o => { ... })
    .AddOpcUaDi();

Do NOT call AddOpcUaDi() alongside a companion-spec factory that already loads OPC UA Device Integration (e.g. PumpNodeManagerFactory which loads Device Integration + Machinery + Pumps). The Device Integration namespace would be registered twice and the OPC UA server may reject the duplicate. AddOpcUaDi() throws on its second invocation to surface the misuse early.

ConfigureDevicesFor<TNodeManager>(action)

Registers a post-setup configurator targeted at a specific manager type. The runner invokes each matching delegate after:

  • the manager's LoadPredefinedNodesAsync has populated PredefinedNodes;
  • the manager's CreateAddressSpaceAsync base has wired up the type tree and root notifiers;
  • (for FluentNodeManagerBase subclasses) the user's ConfigureAsync(builder, ct) / Configure(builder) wiring and the subsequent await builder.SealAsync(ct) are complete.

Configurator-type matching follows Type.IsAssignableFrom. A delegate targeting DiNodeManager runs against every Device Integration-derived manager (including PumpNodeManager); a delegate targeting PumpNodeManager will NOT run against a plain DiNodeManager.

Multiple ConfigureDevicesFor<T> calls accumulate and run in registration order. Exception semantics are fail-fast: any exception thrown by a configurator aborts hosted-server startup with a diagnostic that identifies the failing index and target type.

Context surface

The configurator receives an IDiPostSetupContext:

public interface IDiPostSetupContext
{
    DiNodeManager Manager { get; }
    CancellationToken CancellationToken { get; }
    T GetRequiredService<T>() where T : notnull;

    ValueTask<IDeviceBuilder<DeviceState>> CreateDeviceAsync(...);
    ValueTask<IDeviceBuilder<TDevice>> CreateDeviceAsync<TDevice>(...) where TDevice : ComponentState;
    IDeviceBuilder<TDevice> Device<TDevice>(NodeId nodeId) where TDevice : ComponentState;
    IDeviceBuilder<TDevice> DeviceByBrowseName<TDevice>(QualifiedName name, NodeState? parent = null)
        where TDevice : ComponentState;
    ITopologyElementBuilder<TElement> TopologyElement<TElement>(NodeId nodeId)
        where TElement : TopologyElementState;
    ITopologyElementBuilder<TElement> TopologyElementByBrowseName<TElement>(
        QualifiedName name,
        NodeState? parent = null)
        where TElement : TopologyElementState;
}

The context does not expose a raw IServiceProvider. Use GetRequiredService<T>() to resolve application services (intentionally narrow to discourage service-locator anti-patterns and lifetime traps).

Architecture

Component Lifetime Where
IDiPostSetupRunner Singleton (registered by AddOpcUaDi/ConfigureDevicesFor) Opc.Ua.Di.Server.Hosting
IDiPostSetupConfigurator Singleton (one per ConfigureDevicesFor call) Opc.Ua.Di.Server.Hosting
DiNodeManagerFactory / PumpNodeManagerFactory Singleton (.NET-DI-aware ctor injects runner) server / app
DiNodeManager / PumpNodeManager Per-server startup (factory passes runner) app

The runner is injected into the manager via the factory. The base DiNodeManager auto-invokes it at the end of its own CreateAddressSpaceAsync for every concrete subclass:

  • The base DiNodeManager.CreateAddressSpaceAsync calls LoadPredefinedNodesAsync → CreateFluentBuilder(...) → ConfigureAsync(builder, ct) → RegisterAuthoredNodesAsync → CompleteConfigureAsync → PostSetupRunner.RunAsync(this, ct) → SealConfigurationAsync(builder, ct) in that order. Sealing comes last because the fluent registries (simulations, event sources) are owned by the manager, not by a single builder, and sealing starts them — a post-setup configurator must still be able to register a simulation loop.
  • Subclasses (e.g. PumpNodeManager) override the protected virtual ValueTask ConfigureAsync(INodeManagerBuilder, CancellationToken) hook to materialise instances and drive the fluent INodeManagerBuilder. Because the hook is awaited, asynchronous setup and synchronous wiring live in one method; the base class owns node registration and sealing. The runner fires automatically once the builder is sealed — subclasses do not need to call PostSetupRunner.RunAsync themselves.
sequenceDiagram
    autonumber
    participant Host as Hosting / factory
    participant Base as DiNodeManager
    participant Sub as Subclass, e.g. PumpNodeManager
    participant B as NodeManagerBuilder
    participant Run as IDiPostSetupRunner

    Host->>Base: CreateAddressSpaceAsync(externalReferences, ct)
    Base->>Base: await LoadPredefinedNodesAsync
    Note over Base: predefined nodes loaded, type tree wired
    Base->>B: CreateFluentBuilder(InstanceNamespaceIndex)
    Note over B: attached to the manager's event-source<br/>and simulation registries

    Base->>Sub: await ConfigureAsync(builder, ct)
    Note over Sub: asynchronous setup and synchronous wiring<br/>in one awaited method

    Base->>Base: await RegisterAuthoredNodesAsync(builder, ct)
    Base->>Base: await CompleteConfigureAsync(externalReferences, ct)

    Base->>Run: await RunAsync(this, ct)
    Note over Run: configurators still register simulation<br/>loops and event sources here

    Base->>B: await SealConfigurationAsync(builder, ct)
    Note over B: drain staged root notifiers,<br/>then start the simulations

Sealing is the last step for a reason: the fluent registries belong to the manager rather than to one builder, and sealing starts them. Sealing before RunAsync would leave the post-setup configurators unable to register a simulation loop of their own.

Runtime conformance advertisement

DiNodeManager implements IConformanceContributor, so the hosted server publishes the DI facets and conformance units that are actually wired at startup. The server merges these contributions into Server/ServerCapabilities/ServerProfileArray and Server/ServerCapabilities/ConformanceUnits alongside the base server profiles.

Companions composed into the same manager can call RegisterConformanceContributor(IConformanceContributor) instead of creating a subclass solely to append units. The registration is idempotent by contributor instance, preserves the DI units, and reevaluates the contributor on each publication. Contributors must not refer back to their owning manager. The Machine Vision integration uses this path for Machinery-hosted released models; it does not invent profile URIs.

The conformance units follow what is built:

Runtime condition Conformance units
DI DeviceSet exists DI DeviceTopology
A LockingServicesState has InitLock, RenewLock, or ExitLock wired DI Locking
The same lock service also wires BreakLock DI BreakLocking
Any software-update loading mode is recorded DI SU Software Update, DI SU PrepareForUpdate, DI SU Resume Update
SoftwareLoadingMode.Direct DI SU DirectLoading
SoftwareLoadingMode.Cached DI SU CachedLoading

A facet is advertised only while every conformance unit the DI 1.05 profile database marks mandatory for it is advertised:

Server profile URI Mandatory conformance units Advertised
http://opcfoundation.org/UA-Profile/DI/Server/DeviceIntegrationHost DI DeviceTopology, DI Offline No: offline and online device representations are not built
http://opcfoundation.org/UA-Profile/DI/Server/Locking DI Locking When a lock service is wired
http://opcfoundation.org/UA-Profile/DI/Server/SoftwareUpdateBase DI SU Software Update When a software update is attached
http://opcfoundation.org/UA-Profile/DI/Server/FileSystemLoading Software Update Base, DI SU FileSystem Loading, DI SU Installation for File System No: Package builds a PackageLoadingType, not a FileSystemLoadingType, and the installation offers InstallSoftwarePackage
http://opcfoundation.org/UA-Profile/DI/Server/DirectLoading Software Update Base, DI SU DirectLoading, DI SU UpdateStatus No: the UpdateStatus variable is not built
http://opcfoundation.org/UA-Profile/DI/Server/CachedLoading Software Update Base, DI SU CachedLoading, DI SU Installation for Cached Loading, DI SU UpdateStatus No: the UpdateStatus variable is not built and the installation offers InstallFiles

Client-side surface

Microsoft.Extensions.DependencyInjection.OpcUaClientDiBuilderExtensions adds a single extension method on IOpcUaClientBuilder:

IOpcUaClientBuilder AddOpcUaDi();

This registers four .NET-DI-friendly services that wrap the lazy ManagedSession accessor produced by AddClient(...):

  • IDiDiscoveryService — recursive device discovery from the Objects folder; streams DeviceEntry records as IAsyncEnumerable<DeviceEntry> so callers can begin processing before the browse completes.
  • Func<NodeId, CancellationToken, ValueTask<DiDeviceClient>> — validates a device NodeId and returns a typed client.
  • Func<NodeId, CancellationToken, ValueTask<DiLockClient>> — client wrapper for LockingServicesType.InitLock / RenewLock / ExitLock / BreakLock.
  • Func<CancellationToken, ValueTask<DiTopologyClient>> — topology browser for the Device Integration well-known folders.
  • Func<NodeId, CancellationToken, ValueTask<SoftwareUpdateClient>> — software-version reader for a SoftwareUpdate instance.

Requires AddClient(...) to be called first — the extension throws InvalidOperationException if the managed-session accessor is missing when one of the factories is resolved.

Lock service

The lock service implements the OPC 10000-100 §10.5 locking facet. It tracks ownership of every TopologyElementType.Lock instance in the address space, enforces configurable timeouts, releases locks automatically when the owning session closes, and exposes the four spec-defined methods through the typed LockingServicesState proxy.

Components

Type Purpose
ILockService Application-facing facade: InitLock, RenewLock, ExitLock, BreakLock, GetState. Thread-safe.
LockState Snapshot record (Locked, LockingClient, LockingUser, RemainingLockTimeSeconds).
LockStatus OPC 10000-100 status-code constants.
DefaultLockService Default in-memory implementation backed by a ConcurrentDictionary.
LockingServicesExtensions.BindToLockService Wires the four generated *MethodState.OnCall handlers on a LockingServicesState instance through an ILockService.

Namespaces: Opc.Ua.Di.Server.Locking.

Status codes

Following OPC 10000-100 §10.5:

Constant Value Returned from
LockStatus.Ok 0 All methods (success)
LockStatus.AlreadyLocked 1 InitLock only
LockStatus.CouldNotLock 2 InitLock only
LockStatus.NotLocked 1 RenewLock / ExitLock / BreakLock
LockStatus.WrongClient 2 RenewLock / ExitLock

Default lock service

var lockService = new DefaultLockService(
    lockDuration: TimeSpan.FromMinutes(5),   // optional, defaults to 5 min
    timeProvider: TimeProvider.System);       // optional, for tests

// Hook session-close so locks held by a disconnecting session are
// released automatically.
lockService.AttachToSessionManager(server.SessionManager);

Lock ownership is keyed by ServerSystemContext.SessionId. When the service is invoked from a test (SystemContext rather than ServerSystemContext), it falls back to a synthetic session id derived from ISystemContext.UserId so unit tests can still distinguish callers.

Binding to a LockingServicesState

The Device Integration spec defines a Lock child on every TopologyElementType that implements four method invocations. Wire them through your service:

LockingServicesState lockNode = device.Lock!;
lockNode.BindToLockService(elementId: device.NodeId, service: lockService);

BindToLockService reads lockNode.InitLock, RenewLock, ExitLock, BreakLock and replaces their OnCall handlers with delegates that route into the ILockService. The elementId argument keys the service's internal dictionary — typically the topology element's own NodeId so two different devices can hold independent locks.

End-to-end flow

  1. Client calls InitLock(context: "tag") on a device's Lock.
  2. The method's OnCall runs the bound ILockService.InitLock, which checks the per-device record:
    • If unlocked / expired → record (SessionId, "tag", UserId, now + duration), return Ok (0).
    • If already locked by another session → return AlreadyLocked (1).
  3. RenewLock (same session) extends the expiry timestamp; from a different session it returns WrongClient (2).
  4. ExitLock removes the record (subject to ownership check).
  5. BreakLock removes the record regardless of ownership; intended for administrative recovery.
  6. If the owning session closes (server-emitted SessionClosing event), the DefaultLockService walks its records and releases anything that was held by that session.

Hosting

Register the service as a singleton through standard Microsoft.Extensions.DependencyInjection:

services.AddSingleton<ILockService, DefaultLockService>();

Then attach the session-close hook from inside a ConfigureDevicesFor<TNodeManager> configurator once the server's SessionManager is available:

.ConfigureDevicesFor<DiNodeManager>(ctx =>
{
    ILockService svc = ctx.GetRequiredService<ILockService>();
    if (svc is DefaultLockService defaultSvc)
    {
        defaultSvc.AttachToSessionManager(ctx.Manager.Server.SessionManager);
    }
    foreach (DeviceState device in ctx.Manager.PredefinedNodes
        .Values.OfType<DeviceState>())
    {
        device.Lock?.BindToLockService(device.NodeId, svc);
    }
});

Software update

The software-update facet implements OPC 10000-100 workflows for loading, preparation, installation, power cycle, and confirmation. Attach it with WithSoftwareUpdate; application handlers supply device-specific behavior. The SDK supplies the state-machine wiring and file-transfer infrastructure.

Server-side: package store

ISoftwarePackageStore stores package metadata and payloads. ISoftwareFolder maintains each device's Current / Previous / Future versions. Both have memory and file-system implementations. See Software Update storage abstractions for their contracts and persistence choices.

Surface

Use the package-store contract for operations, metadata, and stream ownership. The Software Update guide also owns WithSoftwareUpdate options and handler hooks.

FileSystemPackageStore layout

Use the store through ISoftwarePackageStore, rather than modifying its backing files directly. See file-system layout and provider composition for the file-system provider and persistence options.

Composing with IFileSystemProvider

Reuse the configured provider as described in provider composition.

Hosting

Register the package store with the host and attach it to the device's software-update facet. See the hosted-server walkthrough and package seeding example.

Client-side software update

SoftwareUpdateClient reads versions, uploads packages, and exposes typed state-machine operations. The usual workflow is upload, prepare, install, and confirm; supported steps depend on the device's advertised update capabilities. See uploading a package and typed state-machine operations for runnable call sequences.

The DI client integration registers Func<NodeId, CancellationToken, ValueTask<SoftwareUpdateClient>> through services.AddOpcUa().AddClient(...).AddOpcUaDi().

Client helpers

Beyond DiDeviceClient and DiDiscoveryClient, the Opc.Ua.Di.Client library ships three typed client wrappers that compose the standard OPC UA Session surface.

Helper Purpose
DiLockClient Wraps the four LockingServicesType methods.
DiTopologyClient Browses DeviceSet, NetworkSet, DeviceTopology.
SoftwareUpdateClient Reads the software-version property of a SoftwareUpdateType instance.

All three live in Opc.Ua.Di.Client and are registered automatically when applications call services.AddOpcUa().AddClient(...).AddOpcUaDi() on the client-side builder.

DiLockClient

public sealed class DiLockClient
{
    public DiLockClient(ISession session, NodeId lockNodeId, ITelemetryContext telemetry);
    public ValueTask<int> InitLockAsync(string context, CancellationToken ct = default);
    public ValueTask<int> RenewLockAsync(CancellationToken ct = default);
    public ValueTask<int> ExitLockAsync(CancellationToken ct = default);
    public ValueTask<int> BreakLockAsync(CancellationToken ct = default);
}

Each method invokes the corresponding LockingServicesType.{Method} generated NodeId on the server and returns the integer status code (see the LockStatus table for the OPC spec values).

The constructor takes the NodeId of the Lock instance — typically the device.Lock child, not the device itself. Resolve it via Session.TranslateBrowsePathsToNodeIdsAsync or by browsing the device's HasComponent references for a child named "Lock".

DiLockClient lockClient = new(session, lockNodeId, telemetry);
int status = await lockClient.InitLockAsync("client-tag");
if (status == LockStatus.Ok)
{
    try
    {
        // Do work that requires exclusive access.
    }
    finally
    {
        await lockClient.ExitLockAsync();
    }
}

DiTopologyClient

public sealed class DiTopologyClient
{
    public DiTopologyClient(ISession session, ITelemetryContext telemetry);
    public NodeId DeviceSetId { get; }
    public NodeId NetworkSetId { get; }
    public NodeId DeviceTopologyId { get; }

    public IAsyncEnumerable<TopologyEntry> EnumerateDevicesAsync(CancellationToken ct = default);
    public IAsyncEnumerable<TopologyEntry> EnumerateNetworksAsync(CancellationToken ct = default);
    public IAsyncEnumerable<TopologyEntry> EnumerateChildrenAsync(NodeId parentNodeId, CancellationToken ct = default);
}

public sealed record TopologyEntry(
    NodeId NodeId,
    string DisplayName,
    NodeId TypeDefinitionId,
    QualifiedName BrowseName);

EnumerateDevicesAsync and EnumerateNetworksAsync browse the hierarchical references of DeviceSet and NetworkSet and return the direct children that are Objects. EnumerateChildrenAsync provides the same surface for arbitrary nodes — useful when walking the DeviceTopology tree.

DiTopologyClient topology = new(session, telemetry);
await foreach (TopologyEntry device in topology.EnumerateDevicesAsync())
{
    Console.WriteLine($"{device.BrowseName}: {device.DisplayName}");
}

Hosting registration

When services.AddOpcUa().AddClient(o => { ... }).AddOpcUaDi() is called, the following factories are registered as singletons:

Func<NodeId, CancellationToken, ValueTask<DiDeviceClient>>
Func<NodeId, CancellationToken, ValueTask<DiLockClient>>
Func<CancellationToken, ValueTask<DiTopologyClient>>
Func<NodeId, CancellationToken, ValueTask<SoftwareUpdateClient>>
IDiDiscoveryService

Inject the factory you need into your application services; the factory opens (or reuses) the lazy ManagedSession on first call.

public sealed class MyMonitor(
    Func<CancellationToken, ValueTask<DiTopologyClient>> topologyFactory,
    Func<NodeId, CancellationToken, ValueTask<DiDeviceClient>> deviceFactory,
    Func<NodeId, CancellationToken, ValueTask<DiLockClient>> lockFactory)
{
    public async Task ScanAsync(CancellationToken ct)
    {
        DiTopologyClient topo = await topologyFactory(ct);
        await foreach (TopologyEntry e in topo.EnumerateDevicesAsync(ct))
        {
            DiDeviceClient dev = await deviceFactory(e.NodeId, ct);
            DeviceIdentification id = await dev.ReadIdentificationAsync(ct);
            // ...
        }
    }
}

What is supported (OPC 10000-100)

References are to the OPC 10000-100 (DI v1.05) specification sections.

Foundation — nameplates and topology elements

  • TopologyElementType (§5.2) — abstract base exposed through ITopologyElementBuilder<TElement>.
  • IVendorNameplateType (§5.10) exposes Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision, DeviceRevision, DeviceManual, DeviceClass, ProductInstanceUri, and ProductCode. Populate these properties with IDeviceBuilder.WithIdentification(...).
  • ITagNameplateType (§5.11) — AssetId, ComponentName, DeviceRevision. Populated by the same builder.
  • IDeviceHealthType (§5.12) — DeviceHealth enum plus the four NAMUR alarm references.
  • IAssetLocationIndicationType (§5.13) — StartLocationIndication / StopLocationIndication methods.
  • IOperationCounterType (§5.14) — PowerOnDuration, OperationDuration, EstimatedReturnedOperationDuration, EstimatedReturnedPowerOnDuration.

Device tree

  • ComponentType (§5.3) — abstract; materialised via IDeviceBuilder.
  • DeviceType (§5.4) — IDeviceBuilder.WithDeviceType<TDeviceState>(factory).
  • SoftwareType, BlockType, ConfigurableObjectType (§5.4 advanced sub-types) — IDeviceBuilder.AddSoftware, AddBlock, AddConfigurableObject.

Topology references

  • ConnectsTo (§5.6.2) — ITopologyElementBuilder.ConnectsTo(other).
  • ConnectsToParent (§5.6.3) — ITopologyElementBuilder.ConnectsToParent(other).
  • IsOnline (§5.6.4) — exposed for online-component wiring.
  • UpdateParent, CanUpdate (§5.6.5–§5.6.6) — used by software update.

Topology containers

  • DeviceSet (§6.2) — well-known instance, auto-created by DiNodeManager under ObjectsFolder; IDeviceBuilder parents devices here by default.
  • NetworkSet (§6.3) — well-known instance.
  • DeviceTopology (§6.4) — well-known instance; topology references attach here.
  • NetworkType, ConnectionPointType, ProtocolType (§6.5–§6.7).

Functional groups (§5.7)

All eight well-known Device Integration functional groups are exposed through typed methods on ITopologyElementBuilder; device builders inherit the same surface:

  • Identification, Configuration, Maintenance, Diagnostics, Statistics, Status, Operational, OperationCounters.

Custom groups go through WithFunctionalGroup(qualifiedName, action).

Lock service (§10.5)

  • LockingServicesType and four method types: InitLockMethodType, RenewLockMethodType, ExitLockMethodType, and BreakLockMethodType. ILockService and DefaultLockService wire these methods and provide session ownership, configurable timeouts, and cleanup when a session closes.

Software update (§10.3)

  • SoftwareUpdateType — orchestrates the loading + state-machine wiring.
  • SoftwareLoadingType, PackageLoadingType (§10.3.4) — abstract bases.
  • DirectLoadingType, CachedLoadingType, FileSystemLoadingType (§10.3.4) — the three loading variants.
  • SoftwareVersionType (§10.3.6) — Manufacturer, ProductInstanceUri, SoftwareRevision, PatchIdentifiers, ReleaseDate, ChangeLog, Hash.
  • Generated proxies for PrepareForUpdateStateMachineType (§10.3.7), InstallationStateMachineType (§10.3.8), PowerCycleStateMachineType (§10.3.9), and ConfirmationStateMachineType (§10.3.10), driven by the application.
  • Storage abstraction: ISoftwarePackageStore with MemoryPackageStore and FileSystemPackageStore implementations.

Support info & lifetime indication

  • ISupportInfoType (§5.15) — IDeviceBuilder.WithSupportInfo(configure). Backing folders DocumentationFiles / ImageSet / ProtocolSupport accept FileState / BaseObjectState children; file content commonly backed by IFileSystemProvider.
  • LifetimeVariableType + 6 indication sub-types (§10.6) — IDeviceBuilder.AddLifetimeIndication(kind, …). Classifier kinds: Time, NumberOfParts, NumberOfUsages, Length, Diameter, SubstanceVolume.

NAMUR alarms (§10.2)

  • DeviceHealthDiagnosticAlarmType (abstract) and the four concrete alarm types (FailureAlarmType, CheckFunctionAlarmType, OffSpecAlarmType, MaintenanceRequiredAlarmType) — generated proxies wired via the fluent IAlarmBuilder<TState> + ActivatesAlarm patterns from src/Opc.Ua.Server/Fluent.

DataTypes & VariableTypes

The Opc.Ua.Di model library includes these source-generated Device Integration DataTypes:

  • DeviceHealthEnumeration
  • SoftwareClass
  • LocationIndicationType
  • SoftwareVersionFileType
  • UpdateBehavior
  • FetchResultDataType
  • TransferResultErrorDataType
  • TransferResultDataDataType
  • ParameterResultDataType

Not yet implemented

  • TransferServicesType (§10.4) — parameter set transfer.
  • Offline and online device representations (§6.3) — needed for the DI Offline conformance unit and so for the DeviceIntegrationHost facet.
  • The UpdateStatus variable of SoftwareUpdateType (§8.4.1.8) and a FileSystemLoadingType loading mode — needed for the Direct, Cached and FileSystem Loading facets (see Runtime conformance advertisement).

See also