Repository navigation
Expand file tree
/
Copy pathProgram.cs
More file actions
276 lines (249 loc) · 9.99 KB
/
Copy pathProgram.cs
File metadata and controls
276 lines (249 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/* ========================================================================
* 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 System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
using Opc.Ua.Pumps;
using Opc.Ua.Pumps.Client;
using PumpsSample.Client;
using BrowseNames = Opc.Ua.Pumps.BrowseNames;
PumpsClientOptions options = PumpsClientOptions.Parse(args);
ITelemetryContext telemetry = DefaultTelemetry.Create(
logging => logging.AddConsole().SetMinimumLevel(LogLevel.Warning));
using var cancellation = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cancellation.Cancel();
};
CancellationToken ct = cancellation.Token;
SampleSession connection = await SampleSession
.ConnectAsync(options, telemetry, ct)
.ConfigureAwait(false);
await using ConfiguredAsyncDisposable _ = connection.ConfigureAwait(false);
// session.Pumps(...) also teaches the session the OPC 40223 structures, so
// PhysicalAddress and the option sets decode into their generated types.
PumpsClient pumps = connection.Session.Pumps(telemetry);
if (!pumps.IsSupported)
{
Console.Error.WriteLine("The server does not publish the OPC 40223 Pumps namespace.");
return 1;
}
var watchList = new List<(string Label, NodeId NodeId)>();
NodeId firstPump = NodeId.Null;
// Discovery looks in the DI DeviceSet and the Machinery Machines folder and
// returns each pump once, vendor subtypes of PumpType included.
await foreach (PumpEntry entry in pumps.EnumeratePumpsAsync(ct).ConfigureAwait(false))
{
if (firstPump.IsNull)
{
firstPump = entry.NodeId;
}
// One call for everything the pump publishes. Each group also has its own
// Read…Async method for when you only need one of them.
PumpSnapshot pump = await pumps.ReadPumpAsync(entry.NodeId, cancellationToken: ct).ConfigureAwait(false);
PrintPump(pump);
string name = pump.BrowseName.Name ?? pump.NodeId.ToString();
AddWatch(watchList, $"{name} MassFlow", pump.Operational?.Measurements[BrowseNames.MassFlow]);
AddWatch(watchList, $"{name} PumpOperation", pump.Operational?.Signals[BrowseNames.PumpOperation]);
AddWatch(watchList, $"{name} Cavitation", pump.Supervision?.ProcessFluid[BrowseNames.Cavitation]);
// Vibration measurements are nested groups of Measurements, one per
// <Vibration> instance the server created.
foreach ((string point, PumpValueSet vibration) in pump.Operational?.Measurements.Groups ??
new Dictionary<string, PumpValueSet>())
{
AddWatch(watchList, $"{name} {point} vRMS", vibration[BrowseNames.OverallVibrationVelocityRMS]);
}
}
if (firstPump.IsNull)
{
Console.Error.WriteLine("The server publishes no pumps.");
return 1;
}
// Units and ranges never change, so a client polling on a cycle reads them
// once (the snapshot above did) and then polls values only: one browse and
// one read per group instead of two reads.
PumpValueSet polled = await pumps
.ReadMeasurementsAsync(firstPump, PumpReadOptions.ValuesOnly, ct)
.ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine(
$"Polled {polled.Count} measurements with PumpReadOptions.ValuesOnly; " +
$"MassFlow = {Format(polled[BrowseNames.MassFlow])}");
if (options.WatchSeconds > 0 && watchList.Count > 0)
{
Console.WriteLine();
Console.WriteLine(
$"Watching {watchList.Count} values for {options.WatchSeconds}s " +
"(the pumps swap duty every 90 s; cavitation appears once a minute) …");
using var window = CancellationTokenSource.CreateLinkedTokenSource(ct);
window.CancelAfter(TimeSpan.FromSeconds(options.WatchSeconds));
var watchers = new List<Task>();
foreach ((string label, NodeId nodeId) in watchList)
{
watchers.Add(WatchAsync(connection, label, nodeId, window.Token));
}
await Task.WhenAll(watchers).ConfigureAwait(false);
}
return 0;
static void PrintPump(PumpSnapshot pump)
{
Console.WriteLine();
Console.WriteLine($"Pump {pump.BrowseName.Name} ({pump.NodeId})");
PumpNameplate? nameplate = pump.Nameplate;
if (nameplate != null)
{
Console.WriteLine(
$" {nameplate.Manufacturer.Text} {nameplate.Model.Text}, serial {nameplate.SerialNumber}, " +
$"built {nameplate.YearOfConstruction}/{nameplate.MonthOfConstruction} in {nameplate.CountryOfOrigin}");
Console.WriteLine($" location: {nameplate.Location ?? "(not published)"}");
}
if (pump.Configuration is { } configuration)
{
PrintSet("Design", configuration.Design);
PrintSet("Implementation", configuration.Implementation);
PrintSet("SystemRequirements", configuration.SystemRequirements);
}
if (pump.Operational is { } operational)
{
PrintSet("Measurements", operational.Measurements);
PrintSet("Signals", operational.Signals);
if (operational.MultiPump is { } multiPump)
{
Console.WriteLine(
$" MultiPump: role {multiPump.PumpRole}, mode {multiPump.OperationMode}, " +
$"{multiPump.MaximumNumberOfPumpsInOperation} of {multiPump.NumberOfPumps} pumps in operation");
}
}
// ActiveSignals yields only the raised signals; empty is the healthy case.
if (pump.Supervision is { } supervision)
{
if (supervision.HasActiveSignals)
{
foreach ((string group, PumpValue signal) in supervision.ActiveSignals)
{
Console.WriteLine($" supervision: {group}/{signal.Name} RAISED");
}
}
else
{
Console.WriteLine(" supervision: no active signals");
}
}
if (pump.Maintenance is { } maintenance)
{
Console.WriteLine(
$" maintenance: {maintenance.StateOfTheItem}, {maintenance.MaintenanceLevel}, " +
$"failure {maintenance.HasFailure}, operating time " +
$"{Format(maintenance.General[BrowseNames.OperatingTime])}");
}
foreach (PumpPortDescriptor port in pump.Ports)
{
Console.WriteLine(
$" port {port.Name}: {port.Kind}, direction {port.Direction?.ToString() ?? "-"}, " +
$"{port.Category ?? "no category"}");
PrintSet($"{port.Name}/Measurements", port.Measurements);
}
foreach (PumpValue link in pump.Documentation)
{
Console.WriteLine($" document {link.Name}: {link.AsString()}");
}
}
static void PrintSet(string title, PumpValueSet values)
{
if (values.IsEmpty && values.Groups.Count == 0)
{
return;
}
Console.WriteLine($" {title}:");
foreach (PumpValue value in values)
{
string range = value.EuRange is { } euRange
? FormattableString.Invariant($" [{euRange.Low} … {euRange.High}]")
: string.Empty;
Console.WriteLine($" {value.Name,-34} {Format(value)}{range}");
}
foreach ((string name, PumpValueSet group) in values.Groups)
{
PrintSet($"{title}/{name}", group);
}
}
static string Format(PumpValue? value)
{
if (value == null)
{
return "(not published)";
}
string unit = value.EngineeringUnits?.DisplayName.Text is { Length: > 0 } symbol ? " " + symbol : string.Empty;
double? number = value.AsDouble();
return number.HasValue
? number.Value.ToString("G6", CultureInfo.InvariantCulture) + unit
: FormattableString.Invariant($"{value.Value}") + unit;
}
static void AddWatch(List<(string Label, NodeId NodeId)> watchList, string label, PumpValue? value)
{
// PumpValue.NodeId is the node that carries the reading; for a discrete
// signal object that is its DiscreteInputValue child, which is what a
// monitored item has to watch.
if (value != null && !value.NodeId.IsNull)
{
watchList.Add((label, value.NodeId));
}
}
static async Task WatchAsync(
SampleSession connection,
string label,
NodeId nodeId,
CancellationToken ct)
{
try
{
await foreach (DataValueChange change in connection.Streaming
.SubscribeDataChangesAsync(nodeId, ct: ct)
.ConfigureAwait(false))
{
DataValue value = change.Value;
string text = value.WrappedValue.TryGetValue(out double number)
? number.ToString("G6", CultureInfo.InvariantCulture)
: FormattableString.Invariant($"{value.WrappedValue}");
Console.WriteLine($" {DateTime.Now:HH:mm:ss} {label,-30} {text}");
}
}
catch (OperationCanceledException)
{
// The watch window elapsed or the user pressed Ctrl+C.
}
}