> 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.
6.8 KiB
OPC 40223 Pumps samples
Two console applications that show the Opc.Ua.Pumps* libraries end to end:
a server that publishes pumps with the fluent builders, and a client that
discovers and reads them with PumpsClient.
| Sample | What it does |
|---|---|
| PumpsServer | Serves a duty/standby pair of cooling-water pumps and simulates them once a second |
| PumpsClient | Discovers every pump, prints the whole pump, then streams live values and supervision changes |
The Pumps developer guide explains every API used here.
Run
In two terminals:
dotnet run --project samples/Pumps/PumpsServer -- --port 62547
dotnet run --project samples/Pumps/PumpsClient -- --insecure --seconds 30
--insecure makes the client accept the server's self-signed certificate;
never use it in production. The client also takes --url <endpoint> (default
opc.tcp://localhost:62547/PumpsServer) and --seconds <n> (how long to
stream, 0 to skip).
The client prints something like:
Pump CoolingPump_A (ns=1;i=3788608060)
Acme Pumps CW-80, serial CW80-0042, built 2024/3 in DE
location: Plant 1 / Cooling skid
Design:
ClockwiseRotation True
MaximumAllowableContinuousSpeed 3000 1/min
MaximumAllowableWorkingPressure 1.6E+06 Pa
Implementation:
PumpBestEfficiency 78 %
RatedFlow 25 kg/s
RatedSpeed 2900 1/min
SystemRequirements:
Fluid Cooling water
MaximumFlow 32 kg/s
MinimumFlow 6 kg/s
Measurements:
BearingTemperature 27.92 °C
DifferentialPressure 367831 Pa [0 … 1600000]
MassFlow 21.58 kg/s [0 … 32]
NumberOfStarts 0
PumpEfficiency 75.08 % [0 … 100]
PumpPowerInput 9.64 kW
Speed 2821.17 1/min [0 … 3000]
Measurements/DriveEndBearing:
OverallVibrationVelocityRMS 2.88 mm/s [0 … 11.2]
ReferenceStandardForVibrationMeasurement ISO 10816-3
Signals:
PumpOperation True
StandBy False
MultiPump: role Master, mode RedundancyOperation, 1 of 2 pumps in operation
supervision: no active signals
maintenance: OperatingState, Level1, failure False, operating time 12480 h
port Suction: InletConnection, direction In, DN100 PN16 flange
port Discharge: OutletConnection, direction Out, DN80 PN16 flange
port Motor: Drive, direction -, no category
Motor/Measurements:
MotorCurrent 18.84 A
MotorTemperature 35.92 °C
document OperationManualLink: https://pumps.example.com/manuals/cw-80.pdf
document TechnicalDataLink: https://pumps.example.com/datasheets/cw-80.pdf
Pump CoolingPump_B (ns=1;i=1919434908)
…
Polled 7 measurements with PumpReadOptions.ValuesOnly; MassFlow = 21.58
Watching 8 values for 45s (the pumps swap duty every 90 s; cavitation appears once a minute) …
00:04:19 CoolingPump_A DriveEndBearing vRMS 2.91
00:04:19 CoolingPump_A Cavitation False
00:04:19 CoolingPump_B PumpOperation False
00:04:19 CoolingPump_B Cavitation False
00:04:19 CoolingPump_A MassFlow 21.75
00:04:19 CoolingPump_B MassFlow 0
…
00:04:56 CoolingPump_A DriveEndBearing vRMS 7.1
00:04:56 CoolingPump_A Cavitation True
00:05:02 CoolingPump_A Cavitation False
NodeIds and namespace indices are whatever the server assigned; only the browse names are stable.
What the server shows
| File | Shows |
|---|---|
Program.cs |
AddPumps() and ConfigurePumps(...) on the generic host |
PumpFleet.cs |
CreatePumpAsync, WithNameplate, every group builder (Design, Implementation, SystemRequirements, Measurements, Signals, Supervision(...), MultiPump, MaintenanceCategory(...), AddPort, Documentation), engineering units and ranges |
PumpFleet.cs |
A vibration measurement from the <Vibration> placeholder with AddVibration, and the PumpUnit holder of the builders the simulation writes through |
PumpSimulation.cs |
A BackgroundService driving values, a duty/standby exchange every 90 s, and a cavitation episode once a minute |
PumpDatasheet.cs |
Static pump data and UNECE engineering units built per OPC 10000-8 §5.6.3 |
Runtime updates. The simulation writes through the builders it kept at
configuration time. Every Set/SetAnalog/SetDiscrete publishes the
change, so subscribed clients see each update without further code.
What the client shows
Code in Program.cs |
Shows |
|---|---|
connection.Session.Pumps(telemetry) |
Creating the client; registers the OPC 40223 structured types |
pumps.IsSupported |
Detecting whether the server publishes OPC 40223 |
EnumeratePumpsAsync |
Discovery from the DI DeviceSet and the Machinery Machines folder |
ReadPumpAsync → PumpSnapshot |
The whole pump in one pass; each group has its own Read…Async too |
PumpValueSet / PumpValue |
Values with engineering unit and range, AsDouble, AsString |
PumpValueSet.Groups |
Nested groups such as the DriveEndBearing vibration measurement |
PumpSupervisionStatus.ActiveSignals |
Only the raised supervision signals |
PumpMaintenanceData |
StateOfTheItem, MaintenanceLevel, HasFailure |
ReadMeasurementsAsync(…, PumpReadOptions.ValuesOnly, …) |
Cheap polling once the metadata is known |
Streaming.SubscribeDataChangesAsync(value.NodeId, …) |
Subscribing to a reading; PumpValue.NodeId is the node that carries it, also for discrete signals |
SampleSession.cs is the session setup. It
keeps the ApplicationInstance alive until the session closes, because the
instance owns the CertificateManager the session validates with.
Publish as NativeAOT
Both samples are NativeAOT-compatible on net10.0:
dotnet publish samples/Pumps/PumpsServer -c Release -f net10.0 -r osx-arm64
Troubleshooting
| Symptom | Fix |
|---|---|
Client reports BadCertificateUntrusted |
Pass --insecure for the demo, or trust the server certificate in the client's PKI (under the local application data folder, OPC Foundation/PumpsClient/pki) |
The server publishes no pumps. |
Wait for the server log line Materialised pump … before starting the client |
| Port already in use | Start the server with --port <n> and the client with --url opc.tcp://localhost:<n>/PumpsServer |