mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-16 21:02:51 +08:00
e8bec27d5d
# Description
Complete WoT Binding preservation and the two-pump aggregation workflow
without implicit overlays or multi-source action fan-out.
- Add verified linked-document export and bounded partition merging.
Preserve complete source facts, reject contradictory archival content,
and retain usable readable content when a future native grammar is
unsupported.
- Wire local Methods and Condition events through the existing
injectable binding/runtime infrastructure. Keep local and upstream
identities distinct, translate namespace-bearing values, and bound
occurrence routes by source and generation.
- Wait for asynchronous event-producer readiness before completing
subscription creation, so the first alarm is not lost. Preserve
cancellation, teardown, and startup-failure reporting.
- Import runtime types in inheritance order and attach typed Method
argument Properties. Keep generated Double spelling deterministic across
supported frameworks and remove an AOT-unsafe registry validation
conversion.
- Generate both pumps and their asset/group projections. Expose fifteen
typed readings per pump, separate source-owned controls, and
acknowledge/confirm/reset workflows. Extend the demo script for managed
and NativeAOT execution, bounded failures, and exact-process cleanup.
- Synchronize current-state documentation and specification fixtures.
Compose fixture paths from platform-independent components and emit/pin
LF pump XML so byte-exact regeneration holds on Windows, Linux, and
macOS.
- Keep sample JSON readable with sorted members, two-space indentation,
and LF line endings. Normalize generated filenames to single hyphens
without changing registry resource IDs, document values, logical links,
or dependency ordering; the manifest maps resource IDs to their physical
filenames.
Most changed files are deterministic regenerations of checked-in sample
documents. The demo manifest contains 533 linked resources; the
aggregation server remains generic and has no Pump-specific compiled
model dependency.
No new alarm vocabulary, shelving/suppression/dialog/ConditionRefresh
transport mapping, Azure SDK changes, or complete Pump/OpenUSD parity is
introduced.
## Related Issues
Fixes #4430, which records the approved implementation scope, design
decisions, and acceptance criteria.
Follow-up to #4411. Specification fixtures are pinned to
[OPCF-Members/spec-drafts commit
82620d9](https://github.com/OPCF-Members/spec-drafts/commit/82620d9e313f35386dfc7477f7875cd47d2fd268),
associated with OPCF-Members/spec-drafts#35.
## Validation
- .NET 10 and .NET 8: complete affected converter/schema, WoT
Connectivity, and binding suites passed.
- .NET Framework 4.8: affected libraries, converter/generator, projected
runtime, readiness, and NodeSet-import compatibility runs passed. The
existing filesystem test exceeding the host's legacy path limit passed
through a temporary short-path alias, without changing its assertions.
- All 29 aggregation sample tests passed, including the independent
companion-model oracle.
- Managed/default-port and NativeAOT/alternate-port demo runs completed
with 533 documents, two pumps, thirty Good readings, and all four
pump/source control and alarm workflows.
- Timeout and duplicate-port failure paths were exercised; owned
processes and ports were released.
- Specification artifact/rule checks, byte-identical example mirroring,
statement-digest verification, and changed-file whitespace checks
passed.
- The [Build and Test .NET 10.0 workflow for
278b092](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/34095520510)
passed, including the previously failing Linux/macOS Types and WoT
Connectivity jobs. Portability fixes preserve the existing assertions
and add an explicit LF-output regression.
- After the readability changes in `dc61d032`, all 131 targeted
sample/projected-runtime cases passed on both .NET 10 and .NET Framework
4.8, and all 29 aggregation sample tests passed after rebuilding. All
539 JSON assets meet the formatting and filename rules; the 538 document
JSON values are unchanged, with only physical paths changing in the
manifest. Byte-exact regeneration assertions remain in place.
Local validation was scoped to affected projects rather than the entire
UA.slnx solution. Numeric coverage percentages and remaining CI gates
remain for CI; coverage thresholds were not changed. Existing unrelated
analyzer warnings were left unchanged.
## Checklist
- [ ] 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.
- [ ] 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.
- [x] I have addressed **all** PR feedback received.
Regression and integration tests are included and passed as described
above; the combined tests/coverage checkbox remains open because numeric
coverage was not measured locally.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6a9f41b1-72f0-4c74-bd7f-26fa531e5738
490 lines
28 KiB
Markdown
490 lines
28 KiB
Markdown
# WoT aggregation sample
|
|
|
|
The WoT aggregation sample loads the DI, Machinery, and Pumps companion models
|
|
and two pump instances from linked WoT documents. A generic aggregation server
|
|
binds their properties, management actions, and alarm notifications to two
|
|
independent OPC UA source servers.
|
|
|
|
The aggregation server contains no Pump-specific generated code and does not reference the DI, Machinery, or Pumps server/model assemblies. The complete DI/Machinery/Pumps/Pump instance shape is runtime-loaded from the files in [`samples/WotCon/AggregationClient/Documents`](AggregationClient/Documents) through the generic WoT-to-NodeSet converter, runtime NodeSet loader, and target-mapping binding runtime.
|
|
|
|
The documents ship with the client because the client is what uploads them. The aggregation server has no build-time or run-time dependency on them: it receives whatever the client writes into the registry.
|
|
|
|
## Topology
|
|
|
|
There are three long-running server processes:
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
client["AggregationClient<br/>Loader and optional control demo"]
|
|
aggregation["AggregationServer<br/>WoT registry and binding runtime"]
|
|
sourceA["FlatTagServer<br/>Source A"]
|
|
sourceB["FlatTagServer<br/>Source B"]
|
|
pump1["Pump1<br/>Data, identity, actions, alarms"]
|
|
pump2["Pump2<br/>Data, identity, actions, alarms"]
|
|
|
|
client -->|"upload documents and Refresh"| aggregation
|
|
client -->|"browse, read, invoke, subscribe"| aggregation
|
|
aggregation -->|"reads, calls, events"| sourceA
|
|
aggregation -->|"reads, calls, events"| sourceB
|
|
aggregation --> pump1
|
|
aggregation --> pump2
|
|
```
|
|
|
|
`AggregationClient` is a fourth, short-lived process. It uploads the document
|
|
manifest, calls `Refresh`, discovers both pumps by browsing from `Objects`, and
|
|
reads fifteen values per pump. With `--exerciseControls true`, it also invokes
|
|
Start, Stop, and Reset, subscribes to projected alarms, and acknowledges and
|
|
confirms occurrences through the aggregate while inspecting independent source
|
|
state. Controls are opt-in because they change the source servers.
|
|
|
|
Source A and Source B expose deliberately flat variables. They do not expose a Pump companion-model hierarchy. The aggregation server creates that hierarchy from the WoT documents and routes each materialized variable to its selected upstream source.
|
|
|
|
Each flat source exposes two upstream pump roots, `Pump1` and `Pump2`. The
|
|
aggregate preserves this separation: each action has one explicit source
|
|
owner, and each projected alarm has a source- and pump-specific occurrence
|
|
route. Alternative forms are not an instruction to invoke multiple sources.
|
|
|
|
## Prerequisites
|
|
|
|
The complete sample requires .NET 8, .NET 9, or .NET 10. `AggregationServer` intentionally targets only `net8.0`, `net9.0`, and `net10.0` because the OPC UA executor required by the checked-in mappings is available only on those frameworks. Legacy `CustomTestTarget` solution builds replace that project with the repository's no-op shell; those matrix builds are not runnable aggregation-server configurations. `FlatTagServer` and `AggregationClient` remain on the shared application target matrix for standalone compatibility, but a runnable end-to-end topology always requires the modern aggregation server.
|
|
|
|
Run the commands below from the repository root with the .NET 10 SDK. The samples accept unencrypted anonymous OPC UA connections for local demonstration and auto-accept untrusted certificates; do not copy those security settings into a production deployment.
|
|
|
|
## Run the sample
|
|
|
|
From the repository root, the demo script builds all three applications, starts
|
|
the two source servers and aggregation server with isolated PKI stores, waits
|
|
for their endpoints, and runs the complete client workflow. Success requires
|
|
every manifest resource, thirty Good values, and all four pump/source control
|
|
and alarm round trips. The script stops only the processes it started:
|
|
|
|
```powershell
|
|
pwsh samples/WotCon/run-aggregation-demo.ps1
|
|
```
|
|
|
|
Use different local ports or keep the captured logs and PKI stores with:
|
|
|
|
```powershell
|
|
pwsh samples/WotCon/run-aggregation-demo.ps1 `
|
|
-AggregationPort 62650 `
|
|
-SourceAPort 62651 `
|
|
-SourceBPort 62652 `
|
|
-Keep
|
|
```
|
|
|
|
With the platform's NativeAOT prerequisites installed, the same workflow can
|
|
publish and run native executables:
|
|
|
|
```powershell
|
|
pwsh samples/WotCon/run-aggregation-demo.ps1 -NativeAot
|
|
```
|
|
|
|
To run each process manually instead, use the four terminals below.
|
|
|
|
Start Source A in the first terminal:
|
|
|
|
```powershell
|
|
dotnet run --project samples/WotCon/FlatTagServer/FlatTagServer.csproj -f net10.0 -- `
|
|
--port 62551 `
|
|
--instanceName SourceA `
|
|
--applicationName FlatTagServerSourceA `
|
|
--namespace urn:opcfoundation.org:UA:WotAggregation:SourceA `
|
|
--differentialPressure 111.25 `
|
|
--fluidTemperature 301.15 `
|
|
--massFlow 0.42 `
|
|
--level 4.25 `
|
|
--cavitation true `
|
|
--pump2DifferentialPressure 211.25 `
|
|
--pump2FluidTemperature 304.15 `
|
|
--pump2MassFlow 0.52 `
|
|
--pump2Level 4.75 `
|
|
--pump2Cavitation false
|
|
```
|
|
|
|
Start Source B in the second terminal:
|
|
|
|
```powershell
|
|
dotnet run --project samples/WotCon/FlatTagServer/FlatTagServer.csproj -f net10.0 -- `
|
|
--port 62552 `
|
|
--instanceName SourceB `
|
|
--applicationName FlatTagServerSourceB `
|
|
--namespace urn:opcfoundation.org:UA:WotAggregation:SourceB `
|
|
--bearingTemperature 333.15 `
|
|
--pumpPowerInput 17.75 `
|
|
--pumpEfficiency 91.5 `
|
|
--numberOfStarts 23 `
|
|
--motorOverheat true `
|
|
--pump2BearingTemperature 337.15 `
|
|
--pump2PumpPowerInput 19.75 `
|
|
--pump2PumpEfficiency 89.5 `
|
|
--pump2NumberOfStarts 31 `
|
|
--pump2MotorOverheat false
|
|
```
|
|
|
|
Start the generic aggregation server in the third terminal:
|
|
|
|
```powershell
|
|
dotnet run --project samples/WotCon/AggregationServer/AggregationServer.csproj -f net10.0 -- `
|
|
--port 62550 `
|
|
--applicationName AggregationServer
|
|
```
|
|
|
|
Run the loader/client in a fourth terminal after all three servers are listening:
|
|
|
|
```powershell
|
|
dotnet run --project samples/WotCon/AggregationClient/AggregationClient.csproj -f net10.0 -- `
|
|
--aggregationEndpoint opc.tcp://localhost:62550/AggregationServer `
|
|
--sourceAEndpoint opc.tcp://localhost:62551/SourceA `
|
|
--sourceBEndpoint opc.tcp://localhost:62552/SourceB `
|
|
--documentsDirectory ./samples/WotCon/AggregationClient/Documents `
|
|
--exerciseControls true `
|
|
--timeoutSeconds 480
|
|
```
|
|
|
|
The client reports the actual manifest resource count, refresh outcomes, both
|
|
recursively browsed pump hierarchies, and their typed values. The
|
|
`WOT_AGGREGATION_CONTROLS_OK` marker appears only after all four pump/source
|
|
workflows finish. `-ClientTimeoutSeconds` bounds the script's client process;
|
|
`--timeoutSeconds` bounds a manually launched client.
|
|
|
|
## Command-line and programmatic options
|
|
|
|
`FlatTagServer` reads the following command-line configuration keys:
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `endpoint` | unset | Complete endpoint URL; when set, overrides `host`, `port`, and the generated path. |
|
|
| `host` | `localhost` | Endpoint host used when `endpoint` is unset. |
|
|
| `port` | `62551` | Endpoint port used when `endpoint` is unset. |
|
|
| `instanceName` | `SourceA` | Endpoint path and source instance name. |
|
|
| `applicationName` | `FlatTagServer` | OPC UA application name. |
|
|
| `namespace` | Source A namespace URI | Must be exactly the Source A or Source B namespace URI. |
|
|
| `pkiRoot` | temporary application directory | Optional certificate-store root. |
|
|
| `manufacturer` | `SimPump Corp` | Pump1 Manufacturer, exposed as LocalizedText. |
|
|
| `serialNumber` | `SN-001` | Pump1 SerialNumber. |
|
|
| `productInstanceUri` | `urn:simdevice:SimPump:PumpX-2000:SN-001` | Pump1 stable product identity. |
|
|
| `pump2Manufacturer` | `SimPump Corp` | Pump2 Manufacturer. |
|
|
| `pump2SerialNumber` | `SN-002` | Pump2 SerialNumber. |
|
|
| `pump2ProductInstanceUri` | `urn:simdevice:SimPump:PumpX-2000:SN-002` | Pump2 stable product identity. |
|
|
| `differentialPressure` | `2.75` | Flat source value. |
|
|
| `fluidTemperature` | `315.65` | Flat source value. |
|
|
| `massFlow` | `0.1825` | Flat source value. |
|
|
| `level` | `6.75` | Flat source value. |
|
|
| `cavitation` | `false` | Flat source value. |
|
|
| `bearingTemperature` | `328.4` | Flat source value. |
|
|
| `pumpPowerInput` | `12.5` | Flat source value. |
|
|
| `pumpEfficiency` | `88.0` | Flat source value. |
|
|
| `numberOfStarts` | `17` | Flat source value. |
|
|
| `motorOverheat` | `false` | Flat source value. |
|
|
| `pump2DifferentialPressure` | `3.25` | Pump2 flat source value. |
|
|
| `pump2FluidTemperature` | `318.15` | Pump2 flat source value. |
|
|
| `pump2MassFlow` | `0.275` | Pump2 flat source value. |
|
|
| `pump2Level` | `5.5` | Pump2 flat source value. |
|
|
| `pump2Cavitation` | `false` | Pump2 flat source value. |
|
|
| `pump2BearingTemperature` | `331.4` | Pump2 flat source value. |
|
|
| `pump2PumpPowerInput` | `14.25` | Pump2 flat source value. |
|
|
| `pump2PumpEfficiency` | `84.5` | Pump2 flat source value. |
|
|
| `pump2NumberOfStarts` | `29` | Pump2 flat source value. |
|
|
| `pump2MotorOverheat` | `false` | Pump2 flat source value. |
|
|
|
|
`AggregationServer` reads `endpoint`, `host` (`localhost`), `port` (`62550`), `applicationName`
|
|
(`AggregationServer`), `pkiRoot`, and `maximumDocumentBytes` (`33554432`).
|
|
|
|
`AggregationClient` reads `aggregationEndpoint`, `sourceAEndpoint`, `sourceBEndpoint`,
|
|
`applicationName` (`AggregationClient`), `pkiRoot`, `documentsDirectory`,
|
|
`exerciseControls` (`false`), and `timeoutSeconds` (`480`, range 1-3600).
|
|
|
|
## Checked-in document set
|
|
|
|
[`documents.json`](AggregationClient/Documents/documents.json) is the authoritative
|
|
inventory. It contains linked DI, Machinery, and Pumps documents, two pump
|
|
instance closures, and six projection documents per pump: Members,
|
|
ProcessData, ConditionData, Supervision, Management, and Asset.
|
|
|
|
`WotAggregationDocumentGenerator` starts with the checked-in companion NodeSets
|
|
and `SamplePump.NodeSet2.xml`. `FromNodeSetDocumentsAsync` verifies each linked
|
|
export by reconstructing the complete source facts. The generator then enriches
|
|
existing affordances by local identity with source-specific forms. It does not
|
|
replace the generated maps or add Nodes over an authoritative native partition.
|
|
Dependencies describe actual document references and ownership, not an arbitrary
|
|
upload chain. Use the manifest rather than assuming a fixed resource count.
|
|
|
|
Checked-in JSON uses sorted members, two-space indentation, and LF line endings.
|
|
Generated filenames use single hyphens with no trailing hyphen before `.json`.
|
|
The manifest maps those filenames to stable registry resource IDs; document
|
|
links and dependencies use the resource IDs, not inferred filenames.
|
|
|
|
Localizations, type bindings, method arguments, engineering units and ranges,
|
|
ordered event selections, and Condition-action relationships remain in the
|
|
generated documents. Event severity comes from the source occurrence, not an
|
|
invented severity property on an asset projection. See
|
|
[WoT / NodeSet conversion](../../docs/WoTNodeSetConversion.md) for the representation
|
|
and preservation rules.
|
|
|
|
The DI documents use **DI 1.05.0**. `ConnectsTo` derives from
|
|
`NonHierarchicalReferences`, as required by
|
|
[OPC 10000-100](https://reference.opcfoundation.org/specs/OPC-10000-100/5.5)
|
|
Section 5.5 Table 48. `DiConnectsToIsANonHierarchicalReference` pins that
|
|
relationship when the source NodeSet is refreshed.
|
|
|
|
### Asset projection shape
|
|
|
|
The checked-in Asset documents use the same shape for each modeled unit:
|
|
|
|
* A member projection selects the affordances that belong to the unit and keeps them addressable by stable local names.
|
|
* `ProcessData` and `ConditionData` are dataset projections. Their selected properties are annotated as
|
|
`dataPoint` members so a consumer can browse measurements separately from the larger unit.
|
|
* `Supervision` contains the two supervision signal Variables and their two EventTypes.
|
|
* `Management` contains two Running Variables, six source-owned management Methods,
|
|
four Condition-action Methods, and the two EventTypes those actions reference.
|
|
* The Asset projection organizes those four groups and selects only identity data at the Asset level. The group
|
|
documents therefore shape browsing; they do not define another copy of the selected affordances.
|
|
|
|
Every selected member must resolve to a real local Node. A projection shapes
|
|
browsing; it does not synthesize missing Methods, EventTypes, or pump instances.
|
|
Both ProcessData and ConditionData contain four measurement Nodes. Supervision
|
|
contains four Nodes and Management fourteen, with no cross-pump membership.
|
|
|
|
### Native preservation within a linked set
|
|
|
|
An incomplete readable partition carries its complete authoritative `uav:nodes`
|
|
representation. This preserves model facts that are not reproduced by its
|
|
readable view; it is neither a whole-model singleton disguised as a linked set
|
|
nor an invitation to overlay additional Nodes. Complete readable partitions do
|
|
not need the native representation.
|
|
|
|
Nested Variable Properties, including `EURange` and `EngineeringUnits`, are
|
|
part of conversion and browsing. The source comparison covers model metadata,
|
|
attributes, references, values, and extensions as well as Node identities.
|
|
Top-level XML Node ordering can change when partitioning; ordered method
|
|
arguments, definition fields, and references remain significant. Routing-only
|
|
JSON added by the sample is retained as residue and accounted for separately
|
|
from the original model facts.
|
|
|
|
### Upload order is not a server requirement
|
|
|
|
The registry accepts documents in any order. Upload order affects only when the Pump becomes visible, never whether it can be materialized:
|
|
|
|
* A dependency closure is projected only when it is complete. While a referenced Thing Model is still missing, `WotDependencyGraph` reports the closure as not projectable, `WotMaterializationCoordinator` marks its members failed in the `DependencyResolution` phase, and the previously active generation is retained. Nothing partial is published into the address space.
|
|
* As soon as the last missing document arrives, the same closure becomes projectable and the Pump appears in one atomic generation switch.
|
|
|
|
The dependency declarations in `documents.json` are therefore a description of the model, not an upload protocol. The manifest is still validated locally before upload, because a manifest that references a document it does not contain, or that contains a cycle, is an authoring error in the sample rather than a legitimate partial upload.
|
|
|
|
`WotRegistryClient.LoadDocumentsAsync` additionally guarantees that Thing Models are processed before Thing Descriptions while preserving the caller's relative order within each document kind.
|
|
|
|
### Progress while a closure is incomplete
|
|
|
|
Both refresh models are valid, and the sample shows the second one:
|
|
|
|
* **`AutoRefresh = true` (registry default).** Every upload triggers a refresh, so a client can watch the closure fill in. Members of an incomplete closure raise `WoTLoadFailureEventType` with `LoadState` and a `DependencyResolution` reason naming the unresolved reference, and each completed pass raises `WoTRefreshCompletedEventType` with its summary and generation. Subscribing to the registry object therefore yields per-document progress and status until the closure completes.
|
|
* **`AutoRefresh = false`.** No intermediate projections and no intermediate events. The caller uploads everything and then triggers exactly one `Refresh`.
|
|
|
|
## Endpoint placeholder substitution
|
|
|
|
The pump documents contain `${SOURCE_A_ENDPOINT}` and `${SOURCE_B_ENDPOINT}`
|
|
placeholders. `AggregationClientRunner.LoadDocumentsAsync` substitutes exact
|
|
`href` values in document-level and affordance-level forms across the manifest,
|
|
entirely in memory. It does not interpret links, schema defaults, native
|
|
projections, preservation envelopes, or vendor metadata as endpoint addresses.
|
|
Documents without a substitutable form retain their original bytes. No generated
|
|
file is rewritten by the client.
|
|
|
|
Each property form also contains a portable upstream `uav:id` using the source server's `nsu=` namespace URI. The property affordance contains a separate `uav:mapToNodeId` using the materialized Pump-instance namespace. The form therefore describes where to read, while the affordance describes where the value belongs in the aggregate model.
|
|
|
|
## Registry upload and Refresh
|
|
|
|
The client creates a `WotRegistryClient` through `AddWotRegistryClient`, loads the manifest documents, and calls:
|
|
|
|
```csharp
|
|
WotRegistryBulkLoadResult loadResult = await client.LoadDocumentsAsync(
|
|
documents,
|
|
refresh: true,
|
|
requestId: Guid.NewGuid().ToString("N"),
|
|
ct: cancellationToken);
|
|
```
|
|
|
|
For each document, the registry client get-or-creates the correct Thing Model or Thing Description group, get-or-creates the resource, and uploads a new version through the inherited OPC UA `FileType` transfer. With `refresh: true`, it then calls `RefreshAllAsync`. The aggregation server validates dependencies, converts each document closure to NodeSet2, prepares its binding plans, imports the NodeSets, wires the OPC UA target mappings, and publishes the new generation.
|
|
|
|
The sample aggregation server sets `AutoRefresh = false` so manifest uploads do
|
|
not cause intermediate projections. The explicit final `Refresh` activates the
|
|
complete dependency closure. A deployment that wants live progress instead
|
|
leaves `AutoRefresh` at its default `true` and consumes the events described above.
|
|
|
|
## Pump companion-model shape
|
|
|
|
The materialized namespace is `urn:opcfoundation.org:UA:WotAggregation:PumpInstance`,
|
|
with roots `Pump1` and `Pump2` organized beneath `Objects`. Both use the same
|
|
companion-model hierarchy; the names below use Pump1 as an example:
|
|
|
|
* `Pump1` has the Pumps `PumpType` definition.
|
|
* `Pump1.Identification` uses the Pumps `PumpIdentificationType`, which OPC 40223 declares for `PumpType.Identification`. It is a subtype of Machinery's `MachineryItemIdentificationType` and ultimately of DI's `FunctionalGroupType`, so the DI identification properties remain available on it.
|
|
* `Operational`, `Operational.Measurements`, `Events`, `Events.SupervisionProcessFluid`, and `Events.SupervisionPumpOperation` use their Pumps type definitions.
|
|
* The hierarchy contains Identification, Operational, Events, and Maintenance groups.
|
|
* Measurements contain DifferentialPressure, FluidTemperature, BearingTemperature, PumpPowerInput, MassFlow, PumpEfficiency, Level, and NumberOfStarts.
|
|
* Event supervision groups contain Cavitation and MotorOverheat variables.
|
|
|
|
These nodes are not compiled into `AggregationServer`. They are produced from the DI, Machinery, Pumps, and Sample Pump WoT documents at runtime.
|
|
|
|
### Cross-checked against a hand-written server
|
|
|
|
[`WotPumpAddressSpaceComparisonTests`](../../tests/Opc.Ua.WotCon.Samples.Tests/WotPumpAddressSpaceComparisonTests.cs)
|
|
compares the declared companion-model subset against
|
|
[`PumpDeviceIntegrationServer`](../DI/PumpDeviceIntegrationServer), which builds
|
|
an OPC 40223 Pump from generated companion-model code. This provides an
|
|
independent oracle rather than only comparing the WoT documents with themselves.
|
|
|
|
It asserts two things separately:
|
|
|
|
* The complete original companion-model subset has the same BrowseNames,
|
|
NodeClasses, and type definitions as the native `Pump_1`. The aggregation
|
|
sample's source-specific controls and Condition proxy subtrees are separate
|
|
sample extensions, not claimed as native Pump parity.
|
|
* The DI, Machinery and Pumps *type* definitions are equal in both servers. Both derive from the same companion models, so a difference there is a defect rather than a scope decision.
|
|
|
|
Comparison is on namespace URIs and browse names throughout; NodeIds, namespace indexes, modelling rules and values legitimately differ between the two servers and are ignored.
|
|
|
|
The native server also supplies additional Identification, alarm, simulation,
|
|
and OpenUSD features. Those are outside this sample's declared subset.
|
|
|
|
## Values from both sources
|
|
|
|
Each pump routes four measurement Variables and one supervision signal to each
|
|
source:
|
|
|
|
| Materialized Pump property | Upstream source |
|
|
| --- | --- |
|
|
| DifferentialPressure | Source A |
|
|
| FluidTemperature | Source A |
|
|
| MassFlow | Source A |
|
|
| Level | Source A |
|
|
| Cavitation | Source A |
|
|
| BearingTemperature | Source B |
|
|
| PumpPowerInput | Source B |
|
|
| PumpEfficiency | Source B |
|
|
| NumberOfStarts | Source B |
|
|
| MotorOverheat | Source B |
|
|
|
|
Three identity Properties come from Source A: Manufacturer (`LocalizedText`),
|
|
SerialNumber (`String`), and ProductInstanceUri (`String`). SourceARunning and
|
|
SourceBRunning expose each source's independent Boolean state. Together these
|
|
make fifteen readings per pump.
|
|
|
|
The example command values distinguish the pumps: Pump1 DifferentialPressure
|
|
is `111.25`, while Pump2 is `211.25`; their BearingTemperature values are
|
|
`333.15` and `337.15`. Readings use the aggregate's local NodeIds. Direct source
|
|
connections are used only by the optional control demonstration as an independent
|
|
side-effect oracle.
|
|
|
|
## Management and alarm workflow
|
|
|
|
For each pump, `SourceAStart`, `SourceAStop`, and `SourceAReset` invoke only Source
|
|
A. The corresponding `SourceB*` Methods invoke only Source B. Reset clears that
|
|
source's supervision signal; it does not acknowledge or confirm operator
|
|
obligations.
|
|
|
|
`CavitationAlarm` and `MotorOverheatAlarm` identify local EventTypes. Their
|
|
mutable Condition instances and emitted occurrences are distinct objects.
|
|
Event forms subscribe to the owning upstream pump with explicit selected
|
|
fields, including EventId and Condition identity/state. The aggregate publishes
|
|
through its local notifier hierarchy and maps each local occurrence EventId
|
|
back to the selected source.
|
|
|
|
`CavitationAcknowledge` / `CavitationConfirm` and
|
|
`MotorOverheatAcknowledge` / `MotorOverheatConfirm` carry
|
|
`uav:conditionAction` and same-document `uav:actsOn` relationships. A native OPC
|
|
UA call supplies EventId followed by Comment; use `LocalizedText.Null` for an
|
|
omitted comment. A WoT invocation may omit an optional Comment, which the OPC
|
|
UA binding normalizes to that second slot. After acknowledgement, use the
|
|
EventId from the updated occurrence for confirmation.
|
|
|
|
The demo initializes the source Condition, subscribes through the aggregate,
|
|
trips the signal, acknowledges and confirms the emitted occurrence, then calls
|
|
Reset. Return to normal alone leaves an unacknowledged or unconfirmed Condition
|
|
retained. Wrong or evicted occurrence IDs do not fall back to another source.
|
|
Shelving, suppression, dialog, and cross-WoT ConditionRefresh mappings are not
|
|
part of this demonstration.
|
|
|
|
## Local monitored items
|
|
|
|
The target-mapping runtime wires the materialized variables to async read handlers. Local OPC UA monitored items on the aggregation server sample those handlers, so creating a monitored item for `Pump1.Operational.Measurements.DifferentialPressure` reads Source A through the same compiled form and lazy OPC UA channel used by an ordinary Read.
|
|
|
|
The runtime does not create a second upstream observation bridge for the form's `observeproperty` operation. This keeps local sampling under the aggregation server's subscription engine and avoids duplicate upstream subscriptions.
|
|
|
|
The integration test creates a real subscription and monitored item with a 50 ms requested sampling interval, publishes the initial Source A differential-pressure value, and keeps that monitored item alive across a generation replacement.
|
|
|
|
## Version replacement and shadow drain
|
|
|
|
Uploading another version of a pump document and calling `RefreshAllAsync`
|
|
creates a shadow runtime NodeSet generation. New reads and monitored items use
|
|
the new generation without disconnecting clients. Existing monitored items
|
|
remain attached to the old generation until they are deleted or otherwise drain.
|
|
|
|
[`WotSampleEndToEndTests.cs`](../../tests/Opc.Ua.WotCon.Samples.Tests/WotSampleEndToEndTests.cs)
|
|
locates the actual document owning Pump1 DifferentialPressure, changes its
|
|
mapping to Source B BearingTemperature, uploads a replacement, and refreshes.
|
|
New reads use Source B while an existing monitored item can continue receiving
|
|
Source A from the retired generation. Deleting that subscription releases its
|
|
retired-generation consumer.
|
|
|
|
If conversion, mapping resolution, channel wiring, or shadow activation fails, the previous active generation remains in service and the failed refresh reports per-resource diagnostics.
|
|
|
|
## Troubleshooting
|
|
|
|
### The client cannot connect
|
|
|
|
Verify that all three server processes are listening and that the client endpoint paths exactly match `/SourceA`, `/SourceB`, and `/AggregationServer`. The generic-host command-line syntax requires the `--` separator after `dotnet run` options.
|
|
|
|
### The source namespace is rejected
|
|
|
|
`FlatTagServer` accepts only `urn:opcfoundation.org:UA:WotAggregation:SourceA` or `urn:opcfoundation.org:UA:WotAggregation:SourceB`. Use the matching namespace and endpoint placeholder for each process.
|
|
|
|
### The manifest fails before upload
|
|
|
|
`documents.json contains a missing or cyclic dependency` means a `dependsOn` id is absent, duplicated, or cyclic. Keep resource ids unique and preserve the DI → Machinery → Pumps → Pump TD dependency graph.
|
|
|
|
### Refresh reports a document failure
|
|
|
|
Inspect the printed resource phase, outcome, and message. Invalid JSON fails format validation. A malformed or unresolved `uav:mapToNodeId` fails activation. A missing upstream server normally allows upload and projection but causes a mapped Read to fail when its lazy channel first connects.
|
|
|
|
### Reads return `BadNodeIdUnknown` or `BadNodeIdInvalid`
|
|
|
|
Check both NodeIds in the Pump TD. The form's `uav:id` must use the correct Source A or Source B namespace URI and source variable id. The affordance's `uav:mapToNodeId` must use the Pump-instance namespace and a node created by the runtime-loaded documents. Prefer `nsu=` to numeric namespace indexes.
|
|
|
|
### Values appear to come from the wrong source
|
|
|
|
Check the endpoint placeholder assignment first, then inspect the property form's upstream `uav:id`. The five Source A and five Source B assignments are listed above. The aggregation client prints stable materialized property names, so a wrong value usually indicates a TD form mapping rather than a browse problem.
|
|
|
|
### A replaced value is still observed
|
|
|
|
An existing monitored item intentionally remains on the retired generation until it drains. Create a new monitored item or delete the old subscription to observe the replacement generation immediately. Ordinary reads after the successful shadow switch use the new generation.
|
|
|
|
## Run the integration tests
|
|
|
|
The real sample tests launch all three servers in process with isolated ports and PKI roots:
|
|
|
|
```powershell
|
|
dotnet test tests/Opc.Ua.WotCon.Samples.Tests/Opc.Ua.WotCon.Samples.Tests.csproj -f net10.0 --filter "TestCategory=Samples"
|
|
```
|
|
|
|
The suite covers both pump hierarchies and all typed readings, exact projection
|
|
membership, source-owned management actions, Condition round trips, local
|
|
monitored items, replacement and shadow drain, invalid documents, missing
|
|
dependencies, invalid target mappings, and unavailable upstream endpoints.
|
|
|
|
## NativeAOT publishing
|
|
|
|
All three server/client sample projects set `PublishAot` for `net10.0`. Publish each process for the target runtime identifier; for Windows x64:
|
|
|
|
```powershell
|
|
dotnet publish samples/WotCon/FlatTagServer/FlatTagServer.csproj -c Release -f net10.0 -r win-x64 --self-contained true
|
|
dotnet publish samples/WotCon/AggregationServer/AggregationServer.csproj -c Release -f net10.0 -r win-x64 --self-contained true
|
|
dotnet publish samples/WotCon/AggregationClient/AggregationClient.csproj -c Release -f net10.0 -r win-x64 --self-contained true
|
|
```
|
|
|
|
Use the published `FlatTagServer.exe` twice with the Source A and Source B arguments above. The client project includes the checked-in `Documents` tree in its publish output, so omit `--documentsDirectory` to use `<publish-directory>/Documents` or pass an explicit external copy.
|
|
|
|
## Related documentation
|
|
|
|
* [WoT protocol bindings](../../docs/WotBindings.md)
|
|
* [WoT Connectivity model, server, registry, and client](../../docs/WoTConnectivity.md)
|