mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
281596a24d
> Tracking issue #4576. #4577 and #4578 are merged, so this branch is rebased > onto `master` and contains only the Pumps commits. ## Summary Third of four stacked PRs. Adds OPC 40223 Pumps and Vacuum Pumps as a model package plus a server and a client SDK, over the Machinery and DI packages. | Package | Content | |---|---| | `Opc.Ua.Pumps` | OPC 40223 1.0.0, generated from the unmodified UA-Nodeset publication, plus the snapshot contracts both sides share (`PumpNameplate`, `PumpConfigurationData`, `PumpOperationalData`, `PumpMaintenanceData`, `PumpSupervisionStatus`, `PumpPortDescriptor`, `MultiPumpConfiguration`, gathered into `PumpSnapshot`) | | `Opc.Ua.Pumps.Server` | `AddPumps()` / `ConfigurePumps(...)`; `PumpsNodeManager : DiNodeManager` places each pump below `DeviceSet` and organizes it into the Machinery `Machines` folder; browse-name-addressed builders reach all ~3000 optional children without a method per child and create them through the model's own `CreateChild` | | `Opc.Ua.Pumps.Client` | discovery in both places (de-duplicated, vendor subtypes included), a reader per `PumpType` group, whole-pump snapshots; the open groups come back as a `PumpValueSet` keyed by the generated `BrowseNames` | The model requires Machinery 1.0.0 and DI 1.02.2 and resolves against the newer Machinery 1.04.1 and DI packages in this repository, since a `RequiredModel` version is a lower bound. The OPC 40223 NodeSet is large, so the model commit is too; the hand-written code starts at the server commit. `samples/DI/PumpDeviceIntegrationServer` is left as it is: its local NodeSet copies are also fixtures of `Opc.Ua.Types.Tests` and `Opc.Ua.WotCon.Tests`. Moving it onto these packages would be a separate follow-up (see #4576). ## Review follow-ups Separate commits after the docs/samples commit, each with a test that fails without it. - Copilot review: `PumpValue.AsStringArray` and `PumpValueSet.Names` return `ArrayOf<string>` with its null sentinel instead of `Nullable<ArrayOf<string>>`; the `PumpBuilder` uses `Variant.Null`. - Own review with the rubric in `.github/copilot-instructions.md`: pump creation is serialized from the duplicate check to the registration, so two concurrent calls with the same name no longer both add a pump; the client lets a failed browse call propagate instead of reading it as "no children"; the `ReadSupervisionAsync` docs say which read options apply. ## Testing - `tests/Opc.Ua.Pumps.Tests`: 73 tests on net8 and net10 and with the netstandard2.1 libraries; the client/server tests connect a real session over `opc.tcp` to a hosted `PumpsNodeManager`. - `UA.slnx` builds for net472, net48, netstandard2.0/2.1 and net8/9/10 with SDK 10.0.401 and no warnings. - Changed-line coverage of this layer (`check-coverage.ps1`): 97.30 % (1550 of 1593 changed lines, against `master`). - Fork CI, full PR matrix incl. macOS: green, all 184 jobs ([run 36622243774](https://github.com/biancode/UA-.NETStandard/actions/runs/36622243774)); CodeQL: green ([run 36622248255](https://github.com/biancode/UA-.NETStandard/actions/runs/36622248255)). - Together with the other layer (Pumps and Scales merged on top of Machinery), built for every target framework and tested on net8, net10 and with the netstandard2.1 libraries, with the samples published. - Beyond the in-repo tests the implementation has been validated end to end against a separate, non-public OPC UA client and server. ## Reviewing Commits: model package → server → client → tests → docs and samples → review follow-ups.
742 lines
40 KiB
Markdown
742 lines
40 KiB
Markdown
# OPC UA for Pumps and Vacuum Pumps (OPC 40223)
|
||
|
||
The `Opc.Ua.Pumps*` packages implement
|
||
[OPC 40223](https://reference.opcfoundation.org/specs/OPC-40223), the
|
||
companion specification for pumps, compressors and vacuum pumps, on top of
|
||
OPC 40001-1 Machinery and OPC 10000-100 Device Integration. A server
|
||
publishes pumps with a fluent builder; a client discovers them and reads
|
||
them back into typed snapshots. Both sides share one set of contracts.
|
||
|
||
## Contents
|
||
|
||
- [Library layout](#library-layout)
|
||
- [The model](#the-model)
|
||
- [Where a pump lives](#where-a-pump-lives)
|
||
- [Quick start — server](#quick-start--server)
|
||
- [Quick start — client](#quick-start--client)
|
||
- [Server hosting](#server-hosting)
|
||
- [Builder reference](#builder-reference)
|
||
- [Updating values at runtime](#updating-values-at-runtime)
|
||
- [Client reference](#client-reference)
|
||
- [Contracts reference](#contracts-reference)
|
||
- [Conformance matrix](#conformance-matrix)
|
||
- [Samples](#samples)
|
||
- [Testing](#testing)
|
||
- [Troubleshooting](#troubleshooting)
|
||
- [Model provenance](#model-provenance)
|
||
- [Generator gap this model uncovered](#generator-gap-this-model-uncovered)
|
||
- [See also](#see-also)
|
||
|
||
## Library layout
|
||
|
||
| Package | Contents |
|
||
| --- | --- |
|
||
| `Opc.Ua.Pumps` | The source-generated OPC 40223 model (typed node states, `ObjectTypeIds`, `BrowseNames`, the 13 enumerations and 5 option sets, `PhysicalAddressDataType`, `AddOpcUaPumps`), `PumpsModel`, and the snapshot contracts both sides share |
|
||
| `Opc.Ua.Pumps.Server` | `PumpsNodeManager`, the fluent builders `IPumpBuilder` / `IPumpGroupBuilder`, and the `AddPumps()` / `ConfigurePumps()` hosting pipeline |
|
||
| `Opc.Ua.Pumps.Client` | `PumpsClient` for discovery, nameplate, configuration, measurements, supervision, maintenance, ports and whole-pump snapshots, plus `AddPumpsClient()` |
|
||
|
||
The contracts (`PumpNameplate`, `PumpValueSet`, `PumpSupervisionStatus`,
|
||
`PumpSnapshot`, …) live in the model package on purpose. A server fills them
|
||
from its process and a client reads them back, and neither side references the
|
||
other.
|
||
|
||
All three packages target the stack's library frameworks and are marked
|
||
NativeAOT-compatible on `net10.0`.
|
||
|
||
## The model
|
||
|
||
| Model URI | C# namespace | `Namespaces` member | Model loader |
|
||
| --- | --- | --- | --- |
|
||
| `http://opcfoundation.org/UA/Pumps/` | `Opc.Ua.Pumps` | `Pumps` | `AddOpcUaPumps` |
|
||
|
||
The NodeSet defines 51 object types, 19 data types and roughly 8,900
|
||
variables. OPC 40223 defines no methods of its own; the 408 methods in the
|
||
NodeSet all belong to the `FileType` instances of the documentation groups.
|
||
|
||
`PumpType` is the entry point. It derives directly from the OPC 10000-100 DI
|
||
`TopologyElementType` (not from DI `ComponentType` and not from any Machinery
|
||
type) and carries seven groups:
|
||
|
||
| Group | Browse-name namespace | Type | What it holds |
|
||
| --- | --- | --- | --- |
|
||
| `Identification` | DI | `PumpIdentificationType` | The nameplate. Mandatory |
|
||
| `Configuration` | DI | `ConfigurationGroupType` | `Design` (78 parameters), `Implementation` (33), `SystemRequirements` (37) |
|
||
| `Operational` | DI | `OperationalGroupType` | `Measurements` (50), `Signals` (21), `Control`, `PumpActuation`, `BypassActuation`, `ThrottleValveActuation`, `MultiPump` |
|
||
| `Maintenance` | DI | `MaintenanceGroupType` | `GeneralMaintenance`, `PreventiveMaintenance`, `ConditionBasedMaintenance`, `BreakdownMaintenance` |
|
||
| `Events` | Pumps | `SupervisionType` | Seven supervision categories, about 150 boolean fault signals |
|
||
| `Ports` | Pumps | `PortsGroupType` | Drive, inlet-connection and outlet-connection ports |
|
||
| `Documentation` | Pumps | `DocumentationType` | 22 document/link pairs |
|
||
|
||
**Mind the namespace column.** `Identification`, `Configuration`,
|
||
`Maintenance` and `Operational` are OPC 10000-100 §5.6 *instance* names in
|
||
the DI namespace, not nodes of the Pumps model, so the Pumps generator emits
|
||
no `BrowseNames` constants for them. They are in `PumpsModel.DiGroups`
|
||
instead. Qualifying one of these four with the Pumps namespace index is the
|
||
most common way to get a Pumps browse path wrong, and it fails silently: the
|
||
path simply does not resolve.
|
||
|
||
Children of a group are spread over namespaces too. The nameplate is the
|
||
extreme case: eleven fields come from OPC 10000-100, four from OPC 40001-1
|
||
and eleven from OPC 40223. The builder and the client search the Pumps, DI,
|
||
Machinery and core namespaces in that order, so callers pass bare browse
|
||
names and never have to know which namespace a field lives in.
|
||
|
||
`MultiPumpType`, despite the name, is not a pump. It is a functional group
|
||
below `Operational` describing one pump's role inside a multi-pump set.
|
||
|
||
## Where a pump lives
|
||
|
||
A `PumpType` is a Device Integration topology element that OPC 40001-1
|
||
clients also treat as a machine, so a conforming server publishes it twice:
|
||
|
||
- as a child of the DI `DeviceSet`, and
|
||
- through an `Organizes` reference from the Machinery `Machines` folder.
|
||
|
||
`PumpsNodeManager` does both by default
|
||
(`PumpsServerOptions.OrganizeIntoMachinesFolder`), and
|
||
`PumpsClient.EnumeratePumpsAsync` looks in both places and de-duplicates.
|
||
Matching goes by type hierarchy, so a vendor subtype of `PumpType` is found
|
||
as well.
|
||
|
||
## Quick start — server
|
||
|
||
```csharp
|
||
using Opc.Ua;
|
||
using Opc.Ua.Pumps;
|
||
using Opc.Ua.Pumps.Server;
|
||
using Opc.Ua.Pumps.Server.Builders;
|
||
// Opc.Ua and Opc.Ua.Pumps both declare BrowseNames, and System declares Range.
|
||
using BrowseNames = Opc.Ua.Pumps.BrowseNames;
|
||
using Range = Opc.Ua.Range;
|
||
|
||
services.AddOpcUa()
|
||
.AddServer<StandardServer>(ConfigureServer)
|
||
.AddPumps()
|
||
.ConfigurePumps(async context =>
|
||
{
|
||
IPumpBuilder pump = await context.Manager.CreatePumpAsync(
|
||
new QualifiedName("Pump_1", context.Manager.InstanceNamespaceIndex),
|
||
context.CancellationToken);
|
||
|
||
pump.WithNameplate(new PumpNameplate
|
||
{
|
||
NodeId = NodeId.Null,
|
||
Manufacturer = new LocalizedText("Acme Pumps"),
|
||
SerialNumber = "SN-001",
|
||
Model = new LocalizedText("PumpX-2000"),
|
||
Location = "Plant 1 / Utility Skid",
|
||
CountryOfOrigin = "DE"
|
||
});
|
||
|
||
pump.Measurements
|
||
.SetAnalog(BrowseNames.DifferentialPressure, 350_000,
|
||
new EUInformation { DisplayName = new LocalizedText("Pa") },
|
||
new Range { Low = 0, High = 1_000_000 })
|
||
.SetAnalog(BrowseNames.MassFlow, 12.5);
|
||
|
||
pump.Signals.SetDiscrete(BrowseNames.PumpOperation, true);
|
||
|
||
pump.Supervision(BrowseNames.SupervisionProcessFluid)
|
||
.SetDiscrete(BrowseNames.Cavitation, false);
|
||
|
||
pump.MaintenanceCategory(BrowseNames.GeneralMaintenance)
|
||
.Set(BrowseNames.StateOfTheItem, Variant.From(StateOfTheItemEnum.OperatingState));
|
||
|
||
pump.AddPort(PumpPortKind.InletConnection, "Suction")
|
||
.Set(BrowseNames.Direction, Variant.From(PortDirectionEnum.In));
|
||
});
|
||
```
|
||
|
||
The pump publishes exactly what the configurator set: the nameplate fields,
|
||
two measurements with their unit and range, one signal, one supervision flag,
|
||
one maintenance state and one port. Everything else in the ~3,000-node
|
||
`PumpType` surface stays unmaterialised.
|
||
|
||
## Quick start — client
|
||
|
||
```csharp
|
||
using Opc.Ua.Pumps;
|
||
using Opc.Ua.Pumps.Client;
|
||
using BrowseNames = Opc.Ua.Pumps.BrowseNames;
|
||
|
||
PumpsClient pumps = session.Pumps(telemetry);
|
||
if (!pumps.IsSupported)
|
||
{
|
||
return; // the server does not publish OPC 40223
|
||
}
|
||
|
||
await foreach (PumpEntry entry in pumps.EnumeratePumpsAsync(ct))
|
||
{
|
||
PumpNameplate? nameplate = await pumps.ReadNameplateAsync(entry.NodeId, ct);
|
||
PumpValueSet measurements = await pumps.ReadMeasurementsAsync(entry.NodeId, cancellationToken: ct);
|
||
|
||
double? flow = measurements.GetDouble(BrowseNames.MassFlow);
|
||
EUInformation? unit = measurements[BrowseNames.MassFlow]?.EngineeringUnits;
|
||
|
||
PumpSupervisionStatus? supervision = await pumps.ReadSupervisionAsync(entry.NodeId, cancellationToken: ct);
|
||
foreach ((string group, PumpValue signal) in supervision?.ActiveSignals ?? [])
|
||
{
|
||
Console.WriteLine($"{nameplate?.SerialNumber}: {group}/{signal.Name}");
|
||
}
|
||
}
|
||
```
|
||
|
||
Or the whole pump in one pass:
|
||
|
||
```csharp
|
||
PumpSnapshot snapshot = await pumps.ReadPumpAsync(entry.NodeId, cancellationToken: ct);
|
||
```
|
||
|
||
## Server hosting
|
||
|
||
### `AddPumps` and `ConfigurePumps`
|
||
|
||
| Call | Effect |
|
||
| --- | --- |
|
||
| `AddPumps(Action<PumpsServerOptions>? configure = null)` | Registers `PumpsNodeManager` through `PumpsNodeManagerFactory`, loads DI, IA, Machinery and Pumps, and claims the Device Integration address space |
|
||
| `ConfigurePumps(Func<IPumpsSetupContext, ValueTask>)` / `ConfigurePumps(Action<IPumpsSetupContext>)` | Runs once the address space is initialised. Several registrations run in registration order; an exception aborts server startup |
|
||
|
||
`IPumpsSetupContext` gives the configurator the initialised `Manager`, the
|
||
underlying DI context (`DiContext`), the hosting `CancellationToken` and
|
||
`GetRequiredService<T>()` for application services, e.g. the process
|
||
connection that supplies the values.
|
||
|
||
`PumpsNodeManager` derives from `DiNodeManager`, so
|
||
`ConfigureDevicesFor<PumpsNodeManager>()` works on it unchanged;
|
||
`ConfigurePumps` is the typed shortcut.
|
||
|
||
### Options
|
||
|
||
| `PumpsServerOptions` | Default | Meaning |
|
||
| --- | --- | --- |
|
||
| `OrganizeIntoMachinesFolder` | `true` | Also reference every pump from the OPC 40001-1 `Machines` folder. Turning it off hides pumps from Machinery clients |
|
||
| `LoadIndustrialAutomationModel` | `true` | Load OPC 10000-200 IA. Machinery types `MonitoringType/Status/Stacklight` with the IA `BasicStacklightType`, so without IA the Machinery model fails to load; there is rarely a reason to turn this off |
|
||
| `AdditionalNamespaceUris` | `[]` | Extra namespaces for a subclass that composes further models into the same address space |
|
||
|
||
### Load order
|
||
|
||
The load order is fixed: DI, then IA, then Machinery, then Pumps. IA has to be
|
||
in the address space before Machinery loads, and Pumps builds on both.
|
||
|
||
### Creating pumps
|
||
|
||
| `PumpsNodeManager` member | Meaning |
|
||
| --- | --- |
|
||
| `CreatePumpAsync(QualifiedName browseName, CancellationToken)` | Creates a pump below the DI `DeviceSet`. A browse name already taken there is rejected with `BadBrowseNameDuplicated` |
|
||
| `CreatePumpAsync(QualifiedName, NodeState? parent, CancellationToken)` | Same, below an explicit parent |
|
||
| `Pump(PumpState)` / `Pump(NodeId)` | A builder for a pump the manager already created (`null` for an unknown NodeId) |
|
||
| `Pumps` | Every pump created so far, in creation order |
|
||
| `NamespaceIndices` | The Pumps, DI and Machinery namespace indices, resolved once |
|
||
|
||
`CreatePumpAsync` does not use `DiNodeManager.CreateDeviceAsync`: that method
|
||
requires a `ComponentType` descendant, and OPC 40223 derives `PumpType`
|
||
directly from DI `TopologyElementType`, the base of `ComponentType`. Each pump gets its own instance NodeIds, is registered
|
||
before the builder is returned (groups added later are visible immediately),
|
||
and is marked `SubscribeToEvents` so clients can subscribe to it as an event
|
||
notifier.
|
||
|
||
### Coexisting with a server that already owns DI
|
||
|
||
`AddPumps` claims the Device Integration address space, so it cannot be
|
||
combined with `AddOpcUaDi` or another DI-owning registration; the second
|
||
registration throws `InvalidOperationException` at startup. A server that
|
||
already has a DI manager loads the model into it and configures it through
|
||
the DI pipeline:
|
||
|
||
```csharp
|
||
protected override ValueTask<NodeStateCollection> LoadPredefinedNodesAsync(
|
||
ISystemContext context, CancellationToken ct = default)
|
||
{
|
||
var nodes = new NodeStateCollection();
|
||
nodes.AddOpcUaDi(context);
|
||
nodes.AddOpcUaIA(context);
|
||
nodes.AddOpcUaMachinery(context);
|
||
nodes.AddOpcUaPumps(context);
|
||
return new ValueTask<NodeStateCollection>(nodes);
|
||
}
|
||
```
|
||
|
||
The [PumpDeviceIntegrationServer sample](../samples/DI/PumpDeviceIntegrationServer)
|
||
follows the same pattern in its own `DiNodeManager` subclass, because the same
|
||
manager also owns OpenUSD. It still source-generates its own copies of the
|
||
Machinery and Pumps models, which other test suites use as fixtures; moving it
|
||
onto these packages is left to a follow-up.
|
||
|
||
## Builder reference
|
||
|
||
### Nothing is materialised until it is asked for
|
||
|
||
The full `PumpType` surface is roughly 3,000 nodes. A pump that reports a flow
|
||
rate and a cavitation flag should publish those two, not 3,000 mostly empty
|
||
variables that cost every client a browse and a read to find out there is
|
||
nothing there. Every group and every leaf is therefore created on first use.
|
||
Touching `pump.Measurements` creates `Operational` and `Measurements`;
|
||
`SetAnalog(BrowseNames.MassFlow, …)` then creates `MassFlow` and, if a unit or
|
||
range is passed, its `EngineeringUnits` and `EURange` properties.
|
||
|
||
### Addressing children by browse name
|
||
|
||
Both the builder and the value sets address everything below a group by
|
||
browse name, using the generated `Opc.Ua.Pumps.BrowseNames` constants.
|
||
`DesignType` alone declares 78 optional variables and the model some 3,000
|
||
across its types, so a method or property per child would make an unusable
|
||
API and an unmaintainable one. Passing a generated constant keeps the call
|
||
compile-time checked all the same: a rename in the model breaks the build.
|
||
|
||
A name the group does not declare is **rejected** with an
|
||
`ArgumentException`, rather than silently creating a node. A server that
|
||
invents children is worse than one that omits them, because a client cannot
|
||
tell a vendor extension from a typo.
|
||
|
||
### `IPumpBuilder`
|
||
|
||
| Member | Returns a builder for |
|
||
| --- | --- |
|
||
| `Identification`, `Configuration`, `Operational`, `Events`, `Maintenance`, `Documentation` | The seven top-level groups (ports via `AddPort`) |
|
||
| `Design`, `Implementation`, `SystemRequirements` | The three `Configuration` sub-groups |
|
||
| `Measurements`, `Signals`, `Control`, `PumpActuation`, `MultiPump` | `Operational` sub-groups. `BypassActuation` and `ThrottleValveActuation` via `Operational.Nested(...)` |
|
||
| `Supervision(string)` | One of the seven `Events` categories (`BrowseNames.Supervision…`) |
|
||
| `MaintenanceCategory(string)` | One of the four `Maintenance` categories (`BrowseNames.…Maintenance`) |
|
||
| `WithNameplate(PumpNameplate)` | Fills `Identification` from the shared contract; see below |
|
||
| `AddPort(PumpPortKind, string name)` | Creates a drive, inlet or outlet port below `Ports` and returns its builder |
|
||
| `With(Action<PumpState>)` | Escape hatch for anything else on the pump node |
|
||
| `Pump`, `NodeId` | The pump itself |
|
||
|
||
### `IPumpGroupBuilder`
|
||
|
||
| Member | Effect |
|
||
| --- | --- |
|
||
| `Add(params string[] browseNames)` | Materialises children without setting values |
|
||
| `Set(string browseName, Variant value)` | Materialises a variable, sets its value, status `Good` and timestamp, and publishes the change |
|
||
| `SetAnalog(string, double, EUInformation?, Range?)` | As `Set`, and also publishes `EngineeringUnits` / `EURange` if the child declares them |
|
||
| `SetDiscrete(string, bool)` | Sets a boolean in either shape OPC 40223 uses (see below) |
|
||
| `Nested(string)` | Builder for a nested group (an object child) |
|
||
| `AddVibration(string name)` | Creates a named `<Vibration>` instance in a `Measurements` group; see [below](#vibration-measurements-and-other-placeholders) |
|
||
| `Child(string)` | The materialised child node, or `null` when the group declares no such child |
|
||
| `With(string, Action<BaseInstanceState>)` | Escape hatch: access levels, historizing, a method callback, `OnReadValue` |
|
||
| `Node` | The group node |
|
||
|
||
`SetDiscrete` covers both boolean shapes in the model:
|
||
|
||
- the `TwoStateDiscreteType` **variables** of the supervision groups
|
||
(`Cavitation`, `Dry`, `MotorOverheat`, …), which carry the state
|
||
themselves;
|
||
- the `DiscreteInputObjectType` / `DiscreteOutputObjectType` **objects** of the
|
||
signal and actuation groups (`PumpOperation`, …), whose state sits one level
|
||
down in `DiscreteInputValue` or `DiscreteOutputValue`.
|
||
|
||
### The nameplate
|
||
|
||
`WithNameplate` publishes every field of `PumpNameplate` that is set and
|
||
leaves the rest unmaterialised. `PumpNameplate.NodeId` and `MarkingsFolderId`
|
||
are ignored on the server side; they are filled in by the client when reading.
|
||
|
||
| Fields | Namespace |
|
||
| --- | --- |
|
||
| `Manufacturer`, `ManufacturerUri`, `Model`, `ProductCode`, `HardwareRevision`, `SoftwareRevision`, `DeviceClass`, `SerialNumber`, `ProductInstanceUri`, `AssetId`, `ComponentName` | OPC 10000-100 DI |
|
||
| `Location`, `InitialOperationDate`, `YearOfConstruction`, `MonthOfConstruction` | OPC 40001-1 Machinery |
|
||
| `DayOfConstruction`, `ArticleNumber`, `OrderProductCode`, `TypeOfProduct`, `Supplier`, `CountryOfOrigin`, `FabricationNumber`, `GTINCode`, `NationalStockNumber`, `PhysicalAddress` | OPC 40223 Pumps |
|
||
|
||
### Ports
|
||
|
||
`PortsGroupType` declares its ports as placeholders: which ports a pump has is
|
||
an instance property, not a type property. `AddPort` therefore creates the
|
||
instance from the port type and names it. The returned builder reaches the
|
||
port's own groups:
|
||
|
||
| `PumpPortKind` | Type | Groups |
|
||
| --- | --- | --- |
|
||
| `Drive` | `DrivePortType` | `Design`, `Measurements` |
|
||
| `InletConnection` | `InletConnectionPortType` | `Design`, `Implementation`, `Measurements`, `SystemRequirements` |
|
||
| `OutletConnection` | `OutletConnectionPortType` | `Design`, `Implementation`, `Measurements`, `SystemRequirements` |
|
||
|
||
```csharp
|
||
pump.AddPort(PumpPortKind.OutletConnection, "Discharge")
|
||
.Set(BrowseNames.Direction, Variant.From(PortDirectionEnum.Out));
|
||
```
|
||
|
||
### Vibration measurements and other placeholders
|
||
|
||
OPC 40223 §7.32 declares vibration measurements as the OptionalPlaceholder
|
||
`<Vibration>` of `MeasurementsType`, typed `VibrationMeasurementType` (§7.33,
|
||
35 analog values after IEC 61987 plus `ReferenceStandardForVibrationMeasurement`).
|
||
A pump publishes one instance per measuring point, each named by the
|
||
application, which is what `AddVibration` creates:
|
||
|
||
```csharp
|
||
pump.Measurements.AddVibration("DriveEndBearing")
|
||
.SetAnalog(BrowseNames.OverallVibrationVelocityRMS, 2.8,
|
||
new EUInformation { DisplayName = new LocalizedText("mm/s") })
|
||
.Set(BrowseNames.ReferenceStandardForVibrationMeasurement, Variant.From("ISO 10816-3"));
|
||
```
|
||
|
||
Asking again for an existing name returns the same instance. A group without
|
||
a `<Vibration>` placeholder rejects the call.
|
||
|
||
A placeholder is a template, not a child: `Nested("Vibration")` fails with
|
||
*"'Measurements' declares '<Vibration>' as a placeholder, not as a child -
|
||
create named instances with AddVibration(name)"*. The model has three more
|
||
placeholder kinds:
|
||
|
||
| Placeholder | Type | Builder API |
|
||
| --- | --- | --- |
|
||
| `<Vibration>` in `MeasurementsType` | `VibrationMeasurementType` | `AddVibration(name)` |
|
||
| `<Drive>`, `<InletConnection>`, `<OutletConnection>` in `PortsGroupType` | the three port types | `AddPort(kind, name)` |
|
||
| `<Marking>` in `MarkingsType` | `FileType` | none; a marking is a file, see [Documentation files](#documentation-files) |
|
||
|
||
### Documentation files
|
||
|
||
The `Documentation` group pairs each document link with a `FileType` object.
|
||
The library publishes the links; it does **not** implement the `FileType`
|
||
methods (`Open`, `Read`, …). To serve the files, attach the handlers yourself
|
||
through `With(browseName, node => …)`, or leave the file objects out and
|
||
publish only the links.
|
||
|
||
## Updating values at runtime
|
||
|
||
The builder is also how an application keeps values current, e.g. from a
|
||
polling loop against the pump controller:
|
||
|
||
```csharp
|
||
IPumpBuilder pump = manager.Pump(pumpNodeId)!;
|
||
pump.Measurements.SetAnalog(BrowseNames.MassFlow, reading.MassFlow);
|
||
pump.Supervision(BrowseNames.SupervisionProcessFluid)
|
||
.SetDiscrete(BrowseNames.Cavitation, reading.Cavitation);
|
||
```
|
||
|
||
Every `Set`, `SetAnalog` and `SetDiscrete` stamps the value with status
|
||
`Good` and the current time and publishes the change. `PumpsNodeManager`
|
||
reports data changes by exception, so this publish is what makes a monitored
|
||
item fire; a client subscribed to `MassFlow` sees each update, and
|
||
`PumpsClientServerE2eTests` checks exactly that over a real subscription.
|
||
For a signal object the publish happens on the `DiscreteInputValue` /
|
||
`DiscreteOutputValue` child that carries the state, which is also the node a
|
||
client subscribes to (`PumpValue.NodeId`).
|
||
|
||
Keep the builders you need, as the
|
||
[PumpsServer sample](../samples/Pumps/PumpsServer/PumpFleet.cs) does, instead
|
||
of resolving them on every update. Values that are only ever read and never
|
||
subscribed to can instead be bound to their source once with
|
||
`With(browseName, node => ((BaseVariableState)node).OnReadValue = …)`.
|
||
|
||
## Client reference
|
||
|
||
### Creating the client
|
||
|
||
| Entry point | Use |
|
||
| --- | --- |
|
||
| `session.Pumps(telemetry)` | Extension on `ISession`; registers the OPC 40223 structured types with the session's encodeable factory |
|
||
| `new PumpsClient(session, telemetry)` | The same without the extension |
|
||
| `AddPumpsClient()` on `AddClient(...)` | Registers `PumpsClientFactory` and a `Func<CancellationToken, Task<PumpsClient>>` over the managed session, plus the DI client services |
|
||
|
||
The registered types (`PhysicalAddressDataType` and the five option sets)
|
||
decode into the generated classes instead of staying opaque
|
||
`ExtensionObject`s.
|
||
|
||
### Discovery
|
||
|
||
| Member | Meaning |
|
||
| --- | --- |
|
||
| `IsSupported` | The server publishes the Pumps namespace |
|
||
| `PumpTypeId`, `MachinesFolderId` | Resolved against the session's namespace table, `NodeId.Null` if absent |
|
||
| `EnumeratePumpsAsync()` | Every pump below the DI `DeviceSet` and the Machinery `Machines` folder, each returned once |
|
||
| `EnumeratePumpsUnderAsync(NodeId root)` | Pumps directly below one folder |
|
||
| `DiscoverPumpsAsync()` | The NodeIds of `EnumeratePumpsAsync` |
|
||
| `IsPumpAsync(NodeId typeDefinition)` | Whether a type is or derives from `PumpType` |
|
||
| `ResolveIdentificationAsync`, `ResolveDiGroupAsync`, `ResolvePumpsChildAsync`, `ResolveOperationalAsync`, `ResolvePortsAsync` | Resolve a group's NodeId for use with `ReadValueSetAsync` |
|
||
|
||
`Topology` exposes the underlying `DiTopologyClient`.
|
||
|
||
### Reading groups
|
||
|
||
| Member | Returns |
|
||
| --- | --- |
|
||
| `ReadNameplateAsync(pump)` | `PumpNameplate?` |
|
||
| `ReadMarkingsAsync(markingsFolder)` | The entries of the nameplate's `Markings` folder (`PumpNameplate.MarkingsFolderId`) |
|
||
| `ReadConfigurationAsync(pump, options)` | `PumpConfigurationData?` with `Design`, `Implementation`, `SystemRequirements` |
|
||
| `ReadOperationalAsync(pump, options)` | `PumpOperationalData?` with all operational groups and `MultiPump` |
|
||
| `ReadMeasurementsAsync`, `ReadSignalsAsync` | One `PumpValueSet` |
|
||
| `ReadMultiPumpAsync(pump)` | `MultiPumpConfiguration?` |
|
||
| `ReadSupervisionAsync(pump, options)` | `PumpSupervisionStatus?` |
|
||
| `ReadMaintenanceAsync(pump, options)` | `PumpMaintenanceData?` |
|
||
| `ReadDocumentationAsync(pump)` | The document links as a `PumpValueSet` |
|
||
| `EnumeratePortsAsync`, `ReadPortsAsync` | `PumpPortDescriptor`s with their groups |
|
||
| `ReadPumpAsync(pump, options)` | `PumpSnapshot`: everything above in one pass |
|
||
| `ReadValueSetAsync(group, options)` | Any group by NodeId |
|
||
|
||
A `null` result means the pump does not publish that group.
|
||
|
||
### Value sets, not records
|
||
|
||
The large groups are open sets of optional variables, and a real pump
|
||
publishes a handful of them. A record with one property per variable would be
|
||
mostly `null` and would need revising with every revision of the
|
||
specification. `ReadValueSetAsync` browses the group instead and returns a
|
||
`PumpValueSet` filled from what the server actually publishes: three entries
|
||
for a pump that publishes three `DesignType` parameters, 78 for one that
|
||
publishes all of them.
|
||
|
||
- Discrete signal objects are followed automatically, and their value is
|
||
reported under the object's name (`PumpOperation`, not
|
||
`DiscreteInputValue`).
|
||
- Other nested OPC 40223 objects are groups of their own and come back in
|
||
`PumpValueSet.Groups`, one level deep: the `<Vibration>` instances below
|
||
`Measurements`, for example.
|
||
|
||
```csharp
|
||
foreach ((string point, PumpValueSet vibration) in measurements.Groups)
|
||
{
|
||
double? velocity = vibration.GetDouble(BrowseNames.OverallVibrationVelocityRMS);
|
||
}
|
||
```
|
||
|
||
Objects of the base models, such as the `FileType` documents, are left
|
||
out; they carry no values.
|
||
|
||
The closed parts of the specification are records: `PumpNameplate` (the
|
||
nameplate list is fixed by OPC 40223) and `MultiPumpConfiguration`, which
|
||
maps all eleven `MultiPumpType` variables (§7.34), including the string
|
||
arrays `DistributionPriority`, `PumpCollectiveIDs` and `RedundantPumpIDs`.
|
||
|
||
### Read options
|
||
|
||
Each `PumpValue` carries the engineering unit and instrument range next to the
|
||
reading. An OPC 40223 measurement without its unit cannot be interpreted
|
||
(350 kPa and 350 Pa are different machines), and the specification fixes no
|
||
unit for its analog variables. That costs one extra round trip per group.
|
||
|
||
| `PumpReadOptions` | Default | |
|
||
| --- | --- | --- |
|
||
| `IncludeEngineeringUnits` | `true` | Read `EngineeringUnits` |
|
||
| `IncludeRanges` | `true` | Read `EURange` |
|
||
| `IncludeTimestamps` | `false` | Return source timestamps |
|
||
| `PumpReadOptions.ValuesOnly` | | One browse and one read, no metadata |
|
||
|
||
Read once with the defaults and poll with `ValuesOnly`: the metadata does not
|
||
change.
|
||
|
||
### Supervision
|
||
|
||
`SupervisionType` spreads about 150 boolean fault signals over seven groups.
|
||
What a caller usually wants is not one named flag but the answer to "what is
|
||
this pump complaining about":
|
||
|
||
```csharp
|
||
PumpSupervisionStatus? supervision = await pumps.ReadSupervisionAsync(pumpId, cancellationToken: ct);
|
||
|
||
if (supervision?.HasActiveSignals == true)
|
||
{
|
||
foreach ((string group, PumpValue signal) in supervision.ActiveSignals)
|
||
{
|
||
Console.WriteLine($"{group}/{signal.Name}");
|
||
}
|
||
}
|
||
```
|
||
|
||
`ActiveSignals` yields only signals that are currently raised. A signal
|
||
published as `false`, or with a bad status, is left out; an empty result is
|
||
the healthy case.
|
||
|
||
### Maintenance
|
||
|
||
`PumpMaintenanceData` exposes the four categories as value sets plus three
|
||
derived answers: `StateOfTheItem` and `MaintenanceLevel` (from
|
||
`GeneralMaintenance`) and `HasFailure` (from `BreakdownMaintenance`).
|
||
|
||
## Contracts reference
|
||
|
||
| Type | Kind | Filled by |
|
||
| --- | --- | --- |
|
||
| `PumpEntry` | Record: NodeId, browse name, display name, type definition | Discovery |
|
||
| `PumpNameplate` | Closed record | `WithNameplate` / `ReadNameplateAsync` |
|
||
| `PumpValue` | One reading with status, source timestamp, unit and range; `AsDouble`, `AsBoolean`, `AsString`, `AsStringArray`, `AsUInt32`, `AsEnum<T>`, `AsDateTime` | Value sets |
|
||
| `PumpValueSet` | Browse-name-keyed readings; `GetDouble`, `GetBoolean`, `GetString`, `GetStringArray`, `GetUInt32`, `GetDateTime`, `GetEnum<T>`, indexer, `Names`, `Contains`; nested groups in `Groups` / `Group(name)` | All open groups |
|
||
| `PumpConfigurationData`, `PumpOperationalData`, `PumpMaintenanceData` | Records of value sets | Group reads |
|
||
| `MultiPumpConfiguration` | Record of all eleven `MultiPumpType` variables | `ReadMultiPumpAsync` |
|
||
| `PumpSupervisionStatus` | Seven value sets, `ActiveSignals`, `HasActiveSignals` | `ReadSupervisionAsync` |
|
||
| `PumpPortDescriptor` | Port kind, direction, category, id carrier, four value sets | Port reads |
|
||
| `PumpSnapshot` | Everything above for one pump | `ReadPumpAsync` |
|
||
|
||
Absent names read as `null`, never as an exception, and a reading with a bad
|
||
status reads as no value. Integer readings convert to `double` in `GetDouble`.
|
||
|
||
## Conformance matrix
|
||
|
||
Status key: ✅ implemented and tested over a real session ·
|
||
🧪 implemented, tested in-process only · 🔧 implemented, not covered by a test ·
|
||
❌ not shipped.
|
||
|
||
### Profiles and conformance units
|
||
|
||
OPC 40223 1.0 defines ten server conformance units (§9.1, Table 152) and two
|
||
server profiles (§9.2, Tables 153–155). It defines no client facets.
|
||
|
||
| Profile | URI | Status |
|
||
| --- | --- | --- |
|
||
| Pump Base Server Profile (Table 154) | `http://opcfoundation.org/UA-Profile/Pumps/Server/Base` | Partial: *Base System* and *Pump PumpType Mandatory Nodes* are met, but the mandatory *3:Machine Identification Writable Server Facet* is not, because client writes to the identification are not stored |
|
||
| Pump Advanced Server Profile (Table 155) | `http://opcfoundation.org/UA-Profile/Pumps/Server/Advanced` | ❌ Not met: the mandatory alarm, notifier and historical-access facets are not implemented |
|
||
|
||
`PumpsNodeManager` therefore advertises neither profile URI.
|
||
|
||
| Conformance unit (Table 152) | Status | Where |
|
||
| --- | --- | --- |
|
||
| Base System | ✅ | `PumpType` instances, see the server table below |
|
||
| Pump PumpType Mandatory Nodes | ✅ | `CreatePumpAsync` materialises every mandatory node |
|
||
| Pump Identification | ✅ | `Identification` nameplate |
|
||
| PumpClass Data | 🔧 | `PumpClass` in the `Design` group can be set through the group builder; nothing enforces it or tests it |
|
||
| Pump Connection Port | ✅ | Inlet and outlet connection ports |
|
||
| Pump Drive Port | ✅ | Drive port |
|
||
| Pump Control | 🧪 | `PumpActuation`; `Control` is not covered by a test |
|
||
| Historizing | ❌ | No historical access for measurements |
|
||
| Limit Alarm Status | ❌ | No `LimitAlarmType` alarms |
|
||
| Supervision Health Status | ❌ | No `DeviceHealthDiagnosticAlarmType` alarms |
|
||
|
||
The rows below follow the specification's structure.
|
||
|
||
### Model
|
||
|
||
| Area | Status | Source | Tests |
|
||
| --- | --- | --- | --- |
|
||
| Namespace URI, all 51 object types, 13 enumerations, 5 option sets, `PhysicalAddressDataType` generated | ✅ | [`Model/`](../src/Opc.Ua.Pumps/Model) | `PumpsModelTests` |
|
||
| DI group names outside the Pumps `BrowseNames` (`PumpsModel.DiGroups`) | 🧪 | [`PumpsModel`](../src/Opc.Ua.Pumps/PumpsModel.cs) | `PumpsModelTests` |
|
||
| Port type classification (`PumpsModel.ClassifyPort`) | 🧪 | same | `PumpsModelTests` |
|
||
| Model loading DI → IA → Machinery → Pumps | 🧪 | [`PumpsNodeManager`](../src/Opc.Ua.Pumps.Server/PumpsNodeManager.cs) | `PumpsNodeManagerTests` |
|
||
|
||
### Server
|
||
|
||
| Area | Status | Source | Tests |
|
||
| --- | --- | --- | --- |
|
||
| `PumpType` instance below `DeviceSet` with its own instance NodeIds | ✅ | [`PumpsNodeManager`](../src/Opc.Ua.Pumps.Server/PumpsNodeManager.cs) | `PumpsNodeManagerTests`; `PumpsClientServerE2eTests` |
|
||
| Organized from the Machinery `Machines` folder, optional | ✅ | same | `PumpsNodeManagerTests`; `PumpsClientServerE2eTests` |
|
||
| Duplicate pump names rejected | 🧪 | same | `PumpsNodeManagerTests` |
|
||
| Pump is an event notifier | 🧪 | same | `PumpsNodeManagerTests` |
|
||
| Lazy materialisation of groups and leaves | 🧪 | [`PumpGroupBuilder`](../src/Opc.Ua.Pumps.Server/Builders/PumpGroupBuilder.cs) | `PumpBuilderTests` |
|
||
| Materialised children carry reference type, type definition, data type and access level | ✅ | same | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| Undeclared browse names rejected | 🧪 | same | `PumpBuilderTests` |
|
||
| `Identification` nameplate across DI / Machinery / Pumps | ✅ | [`PumpBuilder`](../src/Opc.Ua.Pumps.Server/Builders/PumpBuilder.cs) | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Configuration`: `Design`, `SystemRequirements` | ✅ | [`PumpGroupBuilder`](../src/Opc.Ua.Pumps.Server/Builders/PumpGroupBuilder.cs) | `PumpsClientServerE2eTests` |
|
||
| `Configuration`: `Implementation` | 🔧 | same | — (same code path) |
|
||
| `Operational`: `Measurements` with unit and range | ✅ | same | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Operational`: `Signals` (discrete input objects) | ✅ | same | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Operational`: `PumpActuation` (discrete output objects) | 🧪 | same | `PumpBuilderTests` |
|
||
| `Operational`: `MultiPump`, all eleven variables | ✅ | same | `PumpsClientServerE2eTests` |
|
||
| `Operational`: `Control`, `BypassActuation`, `ThrottleValveActuation` | 🔧 | same | — |
|
||
| `Measurements`: `<Vibration>` instances of `VibrationMeasurementType` | ✅ | [`PumpGroupBuilder`](../src/Opc.Ua.Pumps.Server/Builders/PumpGroupBuilder.cs) | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Events`: seven supervision categories | ✅ | same | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Maintenance`: four categories | ✅ | same | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Ports`: drive, inlet and outlet ports with their groups | ✅ | [`PumpBuilder`](../src/Opc.Ua.Pumps.Server/Builders/PumpBuilder.cs) | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `Documentation` links | 🔧 | builder | — |
|
||
| `Documentation` `FileType` methods | ❌ | — | Optional (§7.5); attach through `With(...)` |
|
||
| `MarkingsType` `<Marking>` files | ❌ | — | Optional; `FileType`, see [Documentation files](#documentation-files) |
|
||
| CU *Supervision Health Status*: NE 107 alarms (`2:DeviceHealthDiagnosticAlarmType` subtypes) for supervision signals | ❌ | — | Optional (Table 152/155). OPC 40223 defines no event types of its own; the pump is an event notifier, the alarms are not emitted |
|
||
| CU *Limit Alarm Status*: `0:LimitAlarmType` alarms for measurements | ❌ | — | Optional (Table 152/155) |
|
||
| Data changes to subscribed clients on runtime updates | ✅ | [`PumpGroupBuilder`](../src/Opc.Ua.Pumps.Server/Builders/PumpGroupBuilder.cs) | `PumpBuilderTests`; `PumpsClientServerE2eTests` |
|
||
| `AddPumps` / `ConfigurePumps` hosting | ✅ | [`OpcUaPumpsServerBuilderExtensions`](../src/Opc.Ua.Pumps.Server/Hosting/OpcUaPumpsServerBuilderExtensions.cs) | `PumpsClientServerE2eTests` |
|
||
| Rejecting a second DI owner | 🔧 | same | — |
|
||
|
||
### Client
|
||
|
||
| Area | Status | Source | Tests |
|
||
| --- | --- | --- | --- |
|
||
| Discovery from `DeviceSet` and `Machines`, de-duplicated, subtypes included | ✅ | [`PumpsClient`](../src/Opc.Ua.Pumps.Client/PumpsClient.cs) | `PumpsClientServerE2eTests` |
|
||
| Nameplate | ✅ | [`PumpsClient.Identification`](../src/Opc.Ua.Pumps.Client/PumpsClient.Identification.cs) | `PumpsClientServerE2eTests` |
|
||
| Markings | 🔧 | same | — |
|
||
| Configuration, measurements, signals, supervision, maintenance | ✅ | [`PumpsClient.Groups`](../src/Opc.Ua.Pumps.Client/PumpsClient.Groups.cs) | `PumpsClientServerE2eTests` |
|
||
| Multi-pump configuration, all eleven variables | ✅ | same | `PumpsClientServerE2eTests` |
|
||
| Documentation links | 🔧 | same | — |
|
||
| Value sets with units, ranges and discrete objects | ✅ | [`PumpsClient.Values`](../src/Opc.Ua.Pumps.Client/PumpsClient.Values.cs) | `PumpValueSetTests`; `PumpsClientServerE2eTests` |
|
||
| Nested OPC 40223 objects (vibration measurements) as `PumpValueSet.Groups` | ✅ | same | `PumpsClientServerE2eTests` |
|
||
| Ports with kind and groups | ✅ | [`PumpsClient.Ports`](../src/Opc.Ua.Pumps.Client/PumpsClient.Ports.cs) | `PumpsClientServerE2eTests` |
|
||
| Whole-pump snapshot | ✅ | [`PumpsClient.Snapshot`](../src/Opc.Ua.Pumps.Client/PumpsClient.Snapshot.cs) | `PumpsClientServerE2eTests` |
|
||
| Supervision evaluation (`ActiveSignals`) | 🧪 | [`PumpSupervisionStatus`](../src/Opc.Ua.Pumps/Contracts/PumpSupervisionStatus.cs) | `PumpValueSetTests` |
|
||
| `AddPumpsClient` hosting | 🔧 | [`OpcUaPumpsClientBuilderExtensions`](../src/Opc.Ua.Pumps.Client/Hosting/OpcUaPumpsClientBuilderExtensions.cs) | — |
|
||
|
||
## Samples
|
||
|
||
| Sample | Shows |
|
||
| --- | --- |
|
||
| [PumpsServer](../samples/Pumps/PumpsServer) | `AddPumps` / `ConfigurePumps` with two pumps: nameplate, every group builder, units and ranges, ports, documentation links, and a simulation that publishes runtime updates to subscribers |
|
||
| [PumpsClient](../samples/Pumps/PumpsClient) | `PumpsClient` discovery, `ReadPumpAsync` snapshots, supervision and maintenance evaluation, `ValuesOnly` polling, and streaming subscriptions on `PumpValue.NodeId` |
|
||
| [PumpDeviceIntegrationServer](../samples/DI/PumpDeviceIntegrationServer) | A datasheet-driven pump simulation in its own `DiNodeManager` subclass with the Pumps model loaded next to OpenUSD, published data changes, and an OpenUSD twin per pump |
|
||
| [WoT aggregation](../samples/WotCon/README.md) | A DI / Machinery / Pumps `PumpType` instance loaded at runtime from WoT documents, with no Pumps assembly referenced |
|
||
| [SiteCompositionServer](../samples/OpenUsd/SiteCompositionServer) | A supervisory server composing the pump and generator servers into one scene |
|
||
|
||
## Testing
|
||
|
||
```bash
|
||
dotnet test tests/Opc.Ua.Pumps.Tests
|
||
```
|
||
|
||
| Fixture | Covers |
|
||
| --- | --- |
|
||
| `PumpsModelTests` | The generated model against the specification |
|
||
| `PumpsNodeManagerTests` | Model loading, pump creation, folders, NodeIds |
|
||
| `PumpBuilderTests` | Every builder shape in-process |
|
||
| `PumpValueSetTests` | Value-set and supervision evaluation |
|
||
| `PumpsClientServerE2eTests` | A server through `AddPumps`/`ConfigurePumps`, read back by `PumpsClient` over a real session |
|
||
|
||
The end-to-end test is the one that matters for anything a client sees. An
|
||
in-process assertion on the typed model passes even when a client sees
|
||
nothing, as the [generator gap](#generator-gap-this-model-uncovered) below
|
||
shows.
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Cause | Fix |
|
||
| --- | --- | --- |
|
||
| `CS0104: 'BrowseNames' is an ambiguous reference` | `using Opc.Ua;` and `using Opc.Ua.Pumps;` both bring a `BrowseNames` class (and `System` a `Range`) | Alias them: `using BrowseNames = Opc.Ua.Pumps.BrowseNames;`, `using Range = Opc.Ua.Range;` |
|
||
| `ArgumentException: '…' declares no child named '…'` | The browse name is not declared by that group, or belongs to another group | Use a generated `BrowseNames` constant on the group that declares it (e.g. `Cavitation` is in `SupervisionProcessFluid`, not `Measurements`) |
|
||
| Browse path to `Identification`/`Configuration`/`Operational`/`Maintenance` does not resolve | Qualified with the Pumps namespace | Use the DI namespace index; see [The model](#the-model) |
|
||
| `InvalidOperationException`: DI address space already owned | `AddPumps` combined with `AddOpcUaDi` or another DI owner | [Load the model into the existing manager](#coexisting-with-a-server-that-already-owns-di) |
|
||
| `BadBrowseNameDuplicated` from `CreatePumpAsync` | A pump of that name already exists below the parent | Use unique names, or `manager.Pump(nodeId)` to reconfigure |
|
||
| Machinery clients don't find the pumps | `OrganizeIntoMachinesFolder = false` | Leave it on |
|
||
| Machinery model fails to load | `LoadIndustrialAutomationModel = false` | Leave it on |
|
||
| `ArgumentException: … declares '<Vibration>' as a placeholder` | `Nested("Vibration")` on a placeholder | Use `AddVibration(name)` |
|
||
| A measurement has no unit on the client | Server set it without `EngineeringUnits`, or client read with `ValuesOnly` | Pass the unit to `SetAnalog`; read metadata once with the defaults |
|
||
| `PhysicalAddress` or option sets arrive as `ExtensionObject` | Client not created through `session.Pumps(...)` | Use the extension |
|
||
| A vendor port comes back as `PumpPortKind.Unknown` | Vendor subtype of `PortType` | Expected; its groups are still read |
|
||
|
||
## Model provenance
|
||
|
||
`src/Opc.Ua.Pumps/Model/Opc.Ua.Pumps.NodeSet2.xml` is the unmodified OPC
|
||
Foundation publication from
|
||
[`UA-Nodeset/latest/Pumps`](https://github.com/OPCFoundation/UA-Nodeset/tree/latest/Pumps):
|
||
model version 1.0.0, published 2021-04-19, requiring UA 1.04.7, DI 1.02.2 and
|
||
Machinery 1.0.0. The sibling `Opc.Ua.Pumps.NodeSet2.csv` pins the
|
||
symbol-to-NodeId mapping, so a client that caches NodeIds across builds keeps
|
||
working; the generator fails the build when the two disagree.
|
||
|
||
The `RequiredModel` versions are a lower bound, not a pin. The repository's
|
||
DI and Machinery models are newer and satisfy them.
|
||
|
||
## Generator gap this model uncovered
|
||
|
||
The generated state classes implement
|
||
`NodeState.CreateChild(context, browseName)` for every optional child the
|
||
model declares, which is what lets one builder cover all ~3,000 of them. That
|
||
generic path creates a child of the right CLR type and browse name, but does
|
||
**not** stamp the modelling metadata: the child comes back with no
|
||
`ReferenceTypeId`, no `TypeDefinitionId` and `BaseDataType` as its data type.
|
||
(The per-child `AddXxx(context)` helpers go through the model factory and do
|
||
set all of it.)
|
||
|
||
Such a node looks fine from inside the server and can even be read by NodeId,
|
||
but it is **invisible to a client**: every browse a client makes is filtered
|
||
on `HierarchicalReferences`, and the child has no reference type to match.
|
||
The same applies to `TranslateBrowsePathsToNodeIds`.
|
||
|
||
`PumpGroupBuilder` therefore completes each newly created child from its
|
||
instance declaration on the type, copying `ReferenceTypeId`,
|
||
`TypeDefinitionId`, `DataType`, `ValueRank`, access levels and sampling
|
||
interval before registering it, and filling only members the generic path
|
||
left unset. `PumpsClientServerE2eTests` is what catches a regression here.
|
||
|
||
## See also
|
||
|
||
- [Device Integration developer guide](DeviceIntegration.md): `DiNodeManager`,
|
||
`ConfigureDevicesFor`, the DI client
|
||
- [Machinery developer guide](Machinery.md): the `Machines` folder and the
|
||
OPC 40001-1 building blocks `PumpType` inherits
|
||
- [Subscriptions](Subscriptions.md): monitored items and data-change delivery
|
||
- [OPC 40223 on the OPC Foundation reference site](https://reference.opcfoundation.org/specs/OPC-40223)
|