Files
Alexander Köpke 0de25de89a Change PumpDeviceIntegrationServer to use PumpType (#4066)
This pull request updates the Device Integration documentation and the
`PumpDeviceIntegrationServer` sample to introduce and demonstrate the
new `ITopologyElementBuilder<TElement>` API for working with OPC UA
Device Integration topology elements. The changes clarify the
distinction between device and topology-element builders, update sample
code to use the new builder, and improve the overall clarity and
accuracy of both the documentation and the sample implementation.

**API and Documentation Updates:**

* Introduced `ITopologyElementBuilder<TElement>` as the unified builder
for topology elements, exposing common operations such as functional
groups and topology references, and updated all references and examples
to use this builder where appropriate.
[[1]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaR164-R173)
[[2]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaR219-R228)
[[3]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaL897-R926)
[[4]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaL912-R943)
[[5]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaL926-R958)
* Clarified the distinction between device builders
(`IDeviceBuilder<TDevice>`) and topology-element builders in both the
documentation and sample code, and documented the available builder
methods and their intended use.
[[1]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaL33-R38)
[[2]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaL141-R146)
[[3]](diffhunk://#diff-93e580c17580d8cb3c5c4206fdf6b64c1deabc1e00e4f83d64583661a97f76eaR495-R500)

**Sample Code Modernization:**

* Refactored `samples/PumpDeviceIntegrationServer/Program.cs` to use the
new `ITopologyElementBuilder<TElement>` API for creating and configuring
the second pump, including setting identification properties and
functional groups, and removed legacy code for device health and
software update facets.
[[1]](diffhunk://#diff-7e065631de4a55cb923ba4fa9a32fffcff804ce5ef3e09a70a2eead76425b18dL74-R98)
[[2]](diffhunk://#diff-7e065631de4a55cb923ba4fa9a32fffcff804ce5ef3e09a70a2eead76425b18dL133-L139)
[[3]](diffhunk://#diff-7e065631de4a55cb923ba4fa9a32fffcff804ce5ef3e09a70a2eead76425b18dL53-L57)
[[4]](diffhunk://#diff-7e065631de4a55cb923ba4fa9a32fffcff804ce5ef3e09a70a2eead76425b18dL37-R37)
* Cleaned up and modernized the sample project description and removed
unused or outdated fields and comments.
[[1]](diffhunk://#diff-e3976eed0bf7d7b96d6762d58e8101a0670b2ef60aa04609e8d5ff4be30f64beL8-R8)
[[2]](diffhunk://#diff-70693dd3c39308e8e25d4e009281950f860a914afcf92b2e100a0710b5bb8db1L58-L76)
[[3]](diffhunk://#diff-70693dd3c39308e8e25d4e009281950f860a914afcf92b2e100a0710b5bb8db1L110-R91)
[[4]](diffhunk://#diff-70693dd3c39308e8e25d4e009281950f860a914afcf92b2e100a0710b5bb8db1L129-R110)

**Software Update Example Improvements:**

* Updated the software update walkthrough in the documentation to use
the new builder APIs and improved the clarity of the example, including
more accurate references to the simulated device and software update
facet.
[[1]](diffhunk://#diff-5342d39e79e59ca4fc77a9d6277377841c321d17476979f612c0487c53342238L300-R303)
[[2]](diffhunk://#diff-5342d39e79e59ca4fc77a9d6277377841c321d17476979f612c0487c53342238L313-R337)
[[3]](diffhunk://#diff-5342d39e79e59ca4fc77a9d6277377841c321d17476979f612c0487c53342238L378-L379)

These changes make it easier to work with OPC UA Device Integration
topology elements, provide clearer guidance in the documentation, and
ensure the sample code reflects current best practices.# Description

_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- Fixes #github-issue-number, ...

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [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.
- [ ] 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>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-22 04:06:03 +02:00
..

PumpDeviceIntegrationServer

A self-contained, NativeAOT-friendly OPC UA server that demonstrates the OPC 40223 Pumps companion specification with a full live simulation, wired through the fluent INodeManagerBuilder API and the additive OPC 10000-100 topology-element builder integration.

The pump sample is the integration test for every fluent API extension shipped under src/Opc.Ua.Server/Fluent/. Each extension is documented in Source-generated NodeManagers — Building richer node managers.

Running the sample

cd samples/PumpDeviceIntegrationServer
dotnet run -c Release

The server listens on opc.tcp://localhost:62542/PumpDeviceIntegrationServer by default. Override with --port 62550.

Sample console output:

info: Opc.Ua.Server.MasterNodeManager
      MasterNodeManager.Startup - NodeManagers=3
info: Pumps.PumpNodeManager
      Configuring PumpNodeManager fluent wiring...
info: Pumps.PumpNodeManager
      PumpNodeManager: address space ready (10330 predefined nodes).
info: Opc.Ua.Server.StandardServer
      OPC UA server listening at opc.tcp://localhost:62542/PumpDeviceIntegrationServer.

Browse to Objects > DeviceSet > Pump #1 in any OPC UA client (e.g. UaExpert) to explore the simulated pump. A second declarative pump, Pump #2, sits alongside under the same DeviceSet parent — it demonstrates the DI hosting ConfigureDevicesFor flow without the hand-wired fluent simulation.

Running in Docker

A Dockerfile is provided that builds the Release publish output on the .NET AzureLinux 3 base images and runs it as a non-root user.

Build from the repository root, not from this folder. The image needs the full source tree (src/, src/, tools/), so the Docker build context must be the repo root and the Dockerfile is selected with -f. Running docker build . from inside this folder fails fast with a message telling you the correct command.

# from the repository root:
docker build -f samples/PumpDeviceIntegrationServer/Dockerfile `
             -t pumpdeviceintegrationserver:local .

Run it, publishing the OPC UA port:

docker run --rm -p 62542:62542 pumpdeviceintegrationserver:local

Inside the container the endpoint binds to 0.0.0.0 so it is reachable from the host. Override the bind host and port via environment variables:

docker run --rm -p 62550:62550 `
           -e host=0.0.0.0 -e port=62550 `
           pumpdeviceintegrationserver:local

The server creates its certificate/PKI store under /app at runtime. To persist certificates across container restarts, mount a volume:

docker run --rm -p 62542:62542 `
           -v pump-pki:/app/pki `
           pumpdeviceintegrationserver:local

The image is built and published to the GitHub Container Registry by the pump-device-integration-server-docker.yml workflow on every push to master and on manual dispatch.

What the sample demonstrates

Feature Where
AddOpcUa().AddServer(...).AddNodeManager<T>() hosting Program.cs
Multi-model composition (DI library + locally source-generated Machinery + Pumps) PumpNodeManager.cs LoadPredefinedNodesAsync
Identification properties via WithProperty(name, value) PumpNodeManager.Configure.cs WithIdentification
Optional-child materialisation via generator-emitted AddXxx(context) helpers (Operational / Measurements / Events / SupervisionProcessFluid / SupervisionPumpOperation / Maintenance) PumpNodeManager.cs MaterialisePumpOptionalChildren
Engineering units / EURange via WithEngineeringUnits / WithEURange WithMeasurements
Discrete NumberOfStarts counter wired via Variable<uint>(...).OnRead(...) WithMeasurements
250 ms simulation tick via Simulation(...).OnTick(...) Configure → AdvanceSimulation
Limit alarm with thresholds and acknowledge handler via CreateLimitAlarm(...).WithLimits(...) WithSupervision
Boolean supervision (TwoStateDiscreteState) → alarm activation via .ActivatesAlarm(...) WithSupervision
Cross-namespace path resolution (Pump #1 in Pumps NS → Operational in Machinery NS → Measurements in Pumps NS, all in one unqualified browse path) src/Opc.Ua.Server/Fluent/BrowsePathResolver.cs
Generated PumpType instance + typed Identification group configuration Program.cs (Pump #2)

Architecture

PumpDeviceIntegrationServer/
├── Program.cs                          # AddOpcUa().AddServer(...).AddNodeManager<T>()
│                                       # + ConfigureDevicesFor declarative Pump #2
├── PumpNodeManager.cs                  # Hand-written FluentNodeManagerBase
│                                       # + LoadPredefinedNodesAsync (multi-model)
│                                       # + CreateAddressSpaceAsync (builder setup)
├── PumpNodeManager.Configure.cs        # partial — fluent wiring + simulation tick
├── PumpDeviceIntegrationServer.csproj  # ProjectReference to Opc.Ua.Di model lib
│                                       # AdditionalFiles for Machinery + Pumps
│                                       # NodeSet2 (consumed by source generator)
├── Model/
│   ├── Opc.Ua.Machinery.NodeSet2.xml   # AdditionalFiles — build-time only
│   └── Opc.Ua.Pumps.NodeSet2.xml       # AdditionalFiles — build-time only
└── Properties/AssemblyInfo.cs

The Opc.Ua.Di model library is consumed as a project reference (its types live under the Opc.Ua.Di namespace and are source-generated from the ModelDesign XML). Cross-namespace references from Machinery and Pumps to DI types resolve through the [assembly: ModelDependencyAttribute] carried in the Opc.Ua.Di assembly — no DI NodeSet2 XML needed in this project. The unified attribute carries the compact type-table payload that the consumer's source generator imports at compile time; see ModelDependencies.md for the wire format and consumer-side flow.

The Machinery and Pumps NodeSet2 XMLs are source-generated locally inside this assembly via the <AdditionalFiles> plumbing in the .csproj. The generator emits typed *State classes, NodeId tables, and the AddOpcUaMachinery / AddOpcUaPumps extension methods that LoadPredefinedNodesAsync calls. No runtime XML loading happens — the Model/ folder is a build-time input only. Consumer assemblies that want to reference Machinery or Pumps the same way they reference Opc.Ua.Di should source-generate against the model XML inside their own assembly using the same <AdditionalFiles> pattern.

Extending the sample

  • Add a measurement: open PumpNodeManager.Configure.cs, add a call to AddMeasurement(builder, browsePath, getter, units, min, max) inside WithMeasurements, then add a field + line to AdvanceSimulation that updates the value each tick.
  • Add an alarm: inside WithSupervision, chain another builder.Node("Pump #1/Events").CreateLimitAlarm(...).WithLimits(...) and wire the triggering boolean variable via .ActivatesAlarm(...).
  • Add a second pump: two patterns are demonstrated in the sample.
    • Hand-rolled (used for Pump #1): in PumpNodeManager.CreatePumpAsync, call context.CreateInstanceOfPumpType(deviceSet, browseName), attach it to the DI DeviceSet, and AddPredefinedNodeAsync(pump). The fluent Configure.cs then wires its measurements, alarms, and simulation by browse path.
    • DI declarative (used for Pump #2): in Program.cs, call PumpNodeManager.CreatePumpAsync(...) from a ConfigureDevicesFor<PumpNodeManager> block, wrap the generated PumpState with ctx.TopologyElement<PumpState>(...), then configure the mandatory Identification group. This preserves the PumpType type definition while exposing only topology-element operations.

NativeAOT publishing

cd samples/PumpDeviceIntegrationServer
dotnet publish -c Release -r win-x64

The pump server publishes cleanly under NativeAOT — no trim or AOT warnings — because every fluent extension is reflection-free and the generated model factories are statically rooted.

See also

  • docs/DeviceIntegration.md — full developer guide for the DI library trio (device builder, hosting integration, lock service, software-update package store, client helpers).