Files
UA-.NETStandard/docs/DeviceIntegration.md
Klaus Landsdorf 3690341c80 DI: wire the PrepareForUpdate and Installation methods per spec and advertise only the facets that are met (#4617)
# Description

**Software update methods** (`fix(di): wire PrepareForUpdate
Abort/Resume and Installation Resume`)
- New internal `SoftwareUpdateTransitions` (one per `SoftwareUpdate`)
  checks the current state and moves it under one lock, so concurrent
  calls cannot both pass a check.
- `Prepare` (§8.4.8.3) is accepted only from `Idle`. The application's
`OnPrepare` callback gets a token that `Abort` cancels; the token source
is disposed only after both the Prepare call and a racing Abort are done
  with it, so a handler still running after Abort can use its token.
- `Abort` (§8.4.8.4) moves `Preparing` to `Idle` (`PreparingToIdle`),
  cancels the preparation in flight and reports `Failed` ("Aborted")
  through `OnPrepareStateChanged`; the aborted `Prepare` call returns
  `Bad_InvalidState`. Outside `Preparing` it returns `Bad_InvalidState`.
- `Resume` (§8.4.8.5) no longer runs the Prepare callback. It moves
  `PreparedForUpdate` through `Resuming` to `Idle` and is rejected with
`Bad_InvalidState` outside `PreparedForUpdate` or while the installation
  is `Installing`.
- `Installation.Resume` (§8.4.9) moves `Error` to `Idle`
(`ErrorToIdle`),
  `Bad_InvalidState` otherwise.
- The dispatcher knows the `Resuming` state and the `ResumingToIdle`
  transition, so `CurrentState` / `LastTransition` show their names.

**Advertising** (`fix(di): advertise only the DI facets whose mandatory
units are met`)
- `DI Offline`, `DI SU UpdateStatus`, `DI SU FileSystem Loading`,
`DI SU Installation for File System` and `DI SU Installation for Cached
  Loading` are no longer claimed (see the issue for why each is unmet).
- The facets are derived from the advertised units with the mandatory
  units of the DI 1.05 profile database, so a facet cannot be listed
  without them. Today this leaves Locking and Software Update Base;
  DeviceIntegrationHost, FileSystem, Direct and Cached Loading come back
  by themselves once the missing pieces are built.
- `docs/DeviceIntegration.md` documents both tables and the missing
pieces.

Tests: `SoftwareUpdateMethodTransitionTests` (9 tests) and
`DiNodeManagerConformanceTests` (5 tests) are new;
`DiConformanceFacetTests`
and `PumpAddressSpaceComplianceTests` expected the old claims and are
updated. All 9 transition tests and 8 of the 10 conformance tests fail
against master; the other two are the unchanged locking test and a guard
that every advertised facet carries its mandatory units (master
over-claims
the units too, so the guard alone holds there).

Behaviour change for users: a plain DI server no longer lists the
DeviceIntegrationHost facet, and Direct/Cached/Package software updates
list only Software Update Base.

Fork CI (full matrix incl. macOS) and CodeQL: green — [CI run
36779552603](https://github.com/biancode/UA-.NETStandard/actions/runs/36779552603),
[CodeQL run
36779556125](https://github.com/biancode/UA-.NETStandard/actions/runs/36779556125).

## Related Issues

- Fixes #4616

## 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.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-05 06:26:33 +02:00

1071 lines
45 KiB
Markdown

# OPC UA Device Integration (DI)
> **Glossary:** In this document, **DI** refers to **OPC UA Device
> Integration** (companion specification [OPC 10000-100](https://reference.opcfoundation.org/specs/OPC-10000-100)),
> *not* to .NET Dependency Injection (covered in
> [DependencyInjection.md](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-layout)
- [Quick start](#quick-start)
- [Device builder](#device-builder)
- [Device sub-type extensions](#device-sub-type-extensions)
- [Hosting integration](#hosting-integration)
- [Lock service](#lock-service)
- [Software update](#software-update)
- [Client helpers](#client-helpers)
- [What is supported (OPC 10000-100)](#what-is-supported-opc-10000-100)
- [See also](#see-also)
## 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`](../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
```csharp
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)
```csharp
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
```csharp
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`:
```csharp
// 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
```csharp
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 `MethodDeclarationId`s. 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.
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
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`.
```csharp
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`:
```csharp
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`.
```csharp
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`:
```csharp
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.
```mermaid
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.
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`:
```csharp
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
```csharp
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:
```csharp
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`:
```csharp
services.AddSingleton<ILockService, DefaultLockService>();
```
Then attach the session-close hook from inside a
`ConfigureDevicesFor<TNodeManager>` configurator once the server's
`SessionManager` is available:
```csharp
.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](SoftwareUpdate.md#storage-abstractions)
for their contracts and persistence choices.
#### Surface
Use the [package-store contract](SoftwareUpdate.md#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](SoftwareUpdate.md#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](SoftwareUpdate.md#file-system-layout-and-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](SoftwareUpdate.md#hosted-server-walkthrough)
and [package seeding example](SoftwareUpdate.md#seeding-a-package).
### 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](SoftwareUpdate.md#uploading-a-package) and
[typed state-machine operations](SoftwareUpdate.md#typed-part-16-state-machine-surface)
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
```csharp
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](#status-codes) 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"`.
```csharp
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
```csharp
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.
```csharp
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:
```csharp
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.
```csharp
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](#runtime-conformance-advertisement)).
## See also
- [FileSystemClient](FileSystemClient.md) — file-transfer client used by `SoftwareUpdateClient` and `FileSystemPackageStore`.
- [Source-generated NodeManagers](NodeManagers.md#source-generated-node-managers) — the underlying source generator and fluent API.