Files
UA-.NETStandard/samples/Pumps/PumpsServer/PumpDatasheet.cs
Klaus Landsdorf 281596a24d Pumps: the OPC 40223 companion SDK (#4579)
> 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.
2026-09-30 07:23:29 +02:00

124 lines
5.6 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using Opc.Ua;
namespace PumpsSample
{
/// <summary>
/// The fixed data of the two sample pumps: a duty/standby pair of
/// centrifugal cooling-water pumps on one skid.
/// </summary>
/// <remarks>
/// Everything here is what a real server would take from the pump's
/// datasheet or asset register. The simulation only ever changes the
/// operational values.
/// </remarks>
internal static class PumpDatasheet
{
public const string DutyPump = "CoolingPump_A";
public const string StandbyPump = "CoolingPump_B";
public const string Manufacturer = "Acme Pumps";
public const string ManufacturerUri = "https://pumps.example.com";
public const string Model = "CW-80";
public const string Location = "Plant 1 / Cooling skid";
public const string OperationManual = "https://pumps.example.com/manuals/cw-80.pdf";
public const string TechnicalData = "https://pumps.example.com/datasheets/cw-80.pdf";
/// <summary>Nominal mass flow at the best efficiency point, kg/s.</summary>
public const double RatedMassFlow = 25.0;
/// <summary>Nominal differential pressure, Pa.</summary>
public const double RatedDifferentialPressure = 350_000.0;
/// <summary>Nominal speed, 1/min.</summary>
public const double RatedSpeed = 2_900.0;
/// <summary>Nominal shaft power input, kW.</summary>
public const double RatedPowerInput = 11.0;
/// <summary>Best pump efficiency, percent.</summary>
public const double BestEfficiency = 78.0;
public const double MaximumFlow = 32.0;
public const double MinimumFlow = 6.0;
public const double MaximumAllowableWorkingPressure = 1_600_000.0;
public const double MaximumAllowableContinuousSpeed = 3_000.0;
/// <summary>Bearing temperature at steady state, °C.</summary>
public const double BearingTemperature = 55.0;
/// <summary>Operating hours the pumps start the sample with.</summary>
public const double InitialOperatingHours = 12_480.0;
/// <summary>How often the duty and standby pumps swap roles.</summary>
public const int ExchangeIntervalSeconds = 90;
/// <summary>
/// Engineering units from the UNECE Recommendation 20 table, which
/// OPC 10000-8 §5.6.3 names as the source for <see cref="EUInformation"/>.
/// </summary>
public static class Units
{
public static readonly EUInformation KilogramPerSecond = Create("KGS", "kg/s", "kilogram per second");
public static readonly EUInformation Pascal = Create("PAL", "Pa", "pascal");
public static readonly EUInformation RevolutionsPerMinute = Create("RPM", "1/min", "revolutions per minute");
public static readonly EUInformation Kilowatt = Create("KWT", "kW", "kilowatt");
public static readonly EUInformation Percent = Create("P1", "%", "percent");
public static readonly EUInformation DegreeCelsius = Create("CEL", "°C", "degree Celsius");
public static readonly EUInformation Ampere = Create("AMP", "A", "ampere");
public static readonly EUInformation Hour = Create("HUR", "h", "hour");
public static readonly EUInformation MillimetrePerSecond = Create("C16", "mm/s", "millimetre per second");
/// <summary>
/// Builds the EUInformation for a UNECE common code. The UnitId is
/// the code's characters packed into an Int32, as OPC 10000-8
/// §5.6.3 specifies.
/// </summary>
private static EUInformation Create(string code, string symbol, string description)
{
int unitId = 0;
foreach (char character in code)
{
unitId = (unitId << 8) | character;
}
return new EUInformation
{
NamespaceUri = "http://www.opcfoundation.org/UA/units/un/cefact",
UnitId = unitId,
DisplayName = new LocalizedText("en", symbol),
Description = new LocalizedText("en", description)
};
}
}
}
}