mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
3690341c80
# 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>
1071 lines
45 KiB
Markdown
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.
|