-
Notifications
You must be signed in to change notification settings - Fork 38
/
SampleNodeManager.cs
417 lines (335 loc) · 19 KB
/
SampleNodeManager.cs
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
// Copyright (c) Traeger Industry Components GmbH. All Rights Reserved.
namespace AE
{
using System;
using System.Collections.Generic;
using System.Threading;
using Opc.UaFx;
using Opc.UaFx.Server;
/// <summary>
/// Represents a sample implementation of a custom OpcNodeManager.
/// </summary>
internal class SampleNodeManager : OpcNodeManager
{
#region ---------- Private fields ----------
/// <summary>
/// Stores a variable node which is used to control the "job" processing.
/// </summary>
private OpcDataVariableNode<bool> isActiveNode;
/// <summary>
/// Stores a variable node which is used to indicate the progress status of the current
/// "job" processing including pre- and post-setup stages.
/// </summary>
private OpcDataVariableNode<byte> statusNode;
/// <summary>
/// Stores a dialog condition node (an alarm with interaction) used to query clients
/// whether to continue with the "next job".
/// </summary>
private OpcDialogConditionNode statusChangeNode;
/// <summary>
/// Stores a analog item node used to store the volatile position of a "mechanical" part of
/// the "machine" represented by the server.
/// </summary>
private OpcAnalogItemNode<int> positionNode;
/// <summary>
/// Stores a limit alarm node (an alarm with lower and upper bounds) used to notify about
/// the reaching of a limit through the position of a "mechanical" part of the "machine".
/// </summary>
private OpcExclusiveLimitAlarmNode positionLimitNode;
/// <summary>
/// Stores a variable node which is used to represent a fictive temperature measured at the
/// "mechanical" part of the "machine".
/// </summary>
private OpcAnalogItemNode<double> temperatureNode;
/// <summary>
/// Stores a simple alarm node to notify about the reaching of technical supported
/// temperature values without to inform about the defined limits using the alarm.
/// </summary>
private OpcAlarmConditionNode temperatureCriticalNode;
#endregion
#region ---------- Public constructors ----------
/// <summary>
/// Initializes a new instance of the <see cref="SampleNodeManager"/> class.
/// </summary>
public SampleNodeManager()
: base("http://sampleserver/machines")
{
}
#endregion
#region ---------- Public methods ----------
/// <summary>
/// Simulates a continuous running progress which can be ended using the
/// <paramref name="semaphore"/> specified.
/// </summary>
/// <param name="semaphore">The <see cref="SemaphoreSlim"/> which is used
/// to determine whether the simulation is to be canceled. If it is released
/// the simulation is cancelled.</param>
public void Simulate(SemaphoreSlim semaphore)
{
// By default we define each condition as acknowledged, because we will change it
// depending on outcome of the evaluations bound to the alarms.
this.positionLimitNode.ChangeIsAcked(this.SystemContext, true);
this.temperatureCriticalNode.ChangeIsAcked(this.SystemContext, true);
var run = 0;
var random = new Random(45);
while (!semaphore.Wait(1000)) {
// Only perform "job"-simulation in case the "machine" is active.
if (!this.isActiveNode.Value)
continue;
this.SimulatePosition(run, random);
this.SimulateTemperature(run, random);
this.SimulateStatus(run, random);
run = unchecked(run + 1);
}
}
#endregion
#region ---------- Protected methods ----------
/// <summary>
/// Creates the nodes provided and associated with the node manager.
/// </summary>
/// <param name="references">A dictionary used to determine the logical references between
/// existing nodes (e.g. OPC default nodes) and the nodes provided by the node
/// manager.</param>
/// <returns>An enumerable containing the root nodes of the node manager.</returns>
/// <remarks>This method will be only called once by the server on start up.</remarks>
protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
{
// It is necessary to assign to all root nodes one of the namespaces used to
// identify one of the associated namespaces (see the ctor of the class). This
// namespace does identify the node as member of the namespace of the node
// manager. Optionally it is possible to assign namespace to the child nodes
// too. But by default their missing namespace will be auto-completed through the
// namespace of their parent node.
var machineOne = new OpcFolderNode(this.DefaultNamespace.GetName("Machine_1"));
// In case a client requests a condition referesh it queries the current event
// information which is gathered using the CreateEvent method from each active
// and retained alarm nodes.
machineOne.QueryEventsCallback = (context, events) => {
// Ensure that an re-entrance upon notifier cross-references will not add
// events to the collection which are already stored in.
if (events.Count != 0)
return;
if (this.statusChangeNode.IsRetained)
events.Add(this.statusChangeNode.CreateEvent(context));
if (this.positionLimitNode.IsRetained)
events.Add(this.positionLimitNode.CreateEvent(context));
if (this.temperatureCriticalNode.IsRetained)
events.Add(this.temperatureCriticalNode.CreateEvent(context));
};
// Add new reference to make the node visible beneath the ObjectsFolder
// (the top most root node within every OPC UA server).
references.Add(machineOne, OpcObjectTypes.ObjectsFolder);
new OpcDataVariableNode<string>(machineOne, "Name", "Machine 1");
this.isActiveNode = new OpcDataVariableNode<bool>(machineOne, "IsActive", true);
//// An alarm node have to be a notifier for another node or for the whole server.
//// Is a alarm a notifier of another node:
//// -> this node (the notified one) needs to be subscribed by the client to receive
//// the alarm data.
//// Is a alarm a notifier of the whole server:
//// -> the OpcObjectTypes.Server needs to be subscribed by the client to receive
//// the alarm data.
// Machine 1, Status nodes setup
{
this.statusNode = new OpcDataVariableNode<byte>(machineOne, "Status", 1);
// Define an alarm used to request a dialog which requires a dedicated response
// action by a client. This kind of node can be used for service / operator tasks.
this.statusChangeNode = new OpcDialogConditionNode(machineOne, "StatusChange");
this.statusChangeNode.AutoReportChanges = true;
this.statusChangeNode.Message = "Operator requested";
this.statusChangeNode.Prompt = "The job has been finished, continue with the next one?";
this.statusChangeNode.ResponseOptions = new OpcText[] { "Yes", "No" };
this.statusChangeNode.DefaultResponse = 0;
this.statusChangeNode.CancelResponse = 1;
this.statusChangeNode.OkResponse = 0;
// Handle any client response on an active dialog through applying the response
// using RespondDialog and configuring the dialog as inactive.
this.statusChangeNode.RespondCallback = (context, response) => {
this.isActiveNode.Value = (response == this.statusChangeNode.OkResponse);
this.isActiveNode.ApplyChanges(context);
this.statusChangeNode.RespondDialog(context, response);
this.statusChangeNode.Message = "No operator required";
this.statusChangeNode.IsRetained = false;
return OpcStatusCode.Good;
};
// Define the alarm as the notifier of the machineOne node.
machineOne.AddNotifier(this.SystemContext, this.statusChangeNode);
}
// Machine 1, Position nodes setup
{
this.positionNode = new OpcAnalogItemNode<int>(machineOne, "Position", -1);
this.positionNode.InstrumentRange = new OpcValueRange(low: 120, high: 1);
this.positionNode.EngineeringUnit = new OpcEngineeringUnitInfo(4732211, "mm", "millimetre");
this.positionNode.EngineeringUnitRange = new OpcValueRange(byte.MaxValue);
// Define an alarm used to indicate the reaching of one or more limits during
// a progress. Such limits may be predefined or progress dependent.
this.positionLimitNode = new OpcExclusiveLimitAlarmNode(
machineOne, "PositionLimit", OpcLimitAlarmStates.All);
this.positionLimitNode.HighHighLimit = 120; // e.g. mm
this.positionLimitNode.HighLimit = 100; // e.g. mm
this.positionLimitNode.LowLimit = 5; // e.g. mm
this.positionLimitNode.LowLowLimit = 1; // e.g. mm
this.positionLimitNode.Message = "No range problems";
this.positionLimitNode.ReceiveTime = DateTime.UtcNow;
this.positionLimitNode.AcknowledgeCallback = (context, eventId, comment) => {
this.positionLimitNode.Message = "Acknowledged with " + comment;
return OpcStatusCode.Good;
};
// Define the alarm as the notifier of the machineOne node.
machineOne.AddNotifier(this.SystemContext, this.positionLimitNode);
}
// Machine 1, Temperature nodes setup
{
this.temperatureNode = new OpcAnalogItemNode<double>(machineOne, "Temperature", 18.3);
this.temperatureNode.InstrumentRange = new OpcValueRange(80.0, -40.0);
this.temperatureNode.EngineeringUnit = new OpcEngineeringUnitInfo(4408652, "°C", "degree Celsius");
this.temperatureNode.EngineeringUnitRange = new OpcValueRange(70.8, 5.0);
// Define an alarm which just indicates the fulfillment of an alarm associated
// condition. Such simple alarms only notify about the fulfillment without to
// define additional prerequisites defined by the alarm itself. Much more
// specialized alarms are subclasses of this type of alarm node.
this.temperatureCriticalNode = new OpcAlarmConditionNode(machineOne, "TemperatureCritical");
// Define the alarm as the notifier of the machineOne node.
machineOne.AddNotifier(this.SystemContext, this.temperatureCriticalNode);
// Define the alarm as the notifier of the whole Server node.
this.AddNotifierNode(this.temperatureCriticalNode);
}
return new IOpcNode[] { machineOne };
}
#endregion
#region ---------- Private methods ----------
/// <summary>
/// Simulates a progress which influences the <see cref="statusNode"/>,
/// <see cref="isActiveNode"/> and publishes alarms using the
/// <see cref="statusChangeNode"/>.
/// </summary>
/// <param name="run">The sequence number of the simulation run within the status
/// simulation is to be performed.</param>
/// <param name="random">The <see cref="Random"/> instance used for random number
/// generation.</param>
private void SimulateStatus(int run, Random random)
{
this.statusNode.Value = (byte)(run % 20);
// This will trigger DataChange notification being send to DataChange subscriptions.
this.statusNode.ApplyChanges(this.SystemContext);
if (this.statusNode.Value % 5 == 0) {
this.isActiveNode.Value = false;
// This will trigger DataChange notification being send to DataChange subscriptions.
this.isActiveNode.ApplyChanges(this.SystemContext);
this.statusChangeNode.ReceiveTime = DateTime.UtcNow;
this.statusChangeNode.Time = DateTime.UtcNow;
this.statusChangeNode.Message = "Operator requested";
this.statusChangeNode.IsRetained = true;
this.statusChangeNode.ActivateDialog(this.SystemContext);
// This will trigger Event notification being send to Event subscriptions.
this.statusChangeNode.ReportEventFrom(
this.SystemContext, this.statusNode);
}
}
/// <summary>
/// Simulates a progress which influences the <see cref="positionNode"/> and publishes
/// alarms using the <see cref="positionLimitNode"/>.
/// </summary>
/// <param name="run">The sequence number of the simulation run within the position
/// simulation is to be performed.</param>
/// <param name="random">The <see cref="Random"/> instance used for random number
/// generation.</param>
private void SimulatePosition(int run, Random random)
{
if (this.positionNode.Value == -1) {
this.positionLimitNode.ChangeLimitState(
this.SystemContext, OpcLimitAlarmStates.Inactive);
}
var ackRequired = (this.positionLimitNode.ReceiveTime.AddSeconds(45) < DateTime.UtcNow);
if (!this.positionLimitNode.IsActive || (!ackRequired || this.positionLimitNode.IsAcked)) {
var positionValue = random.Next(
(int)(this.positionLimitNode.LowLowLimit - run % 3),
(int)(this.positionLimitNode.HighHighLimit + run % 7));
this.positionNode.Value = positionValue;
// This will trigger DataChange notification being send to DataChange subscriptions.
this.positionNode.ApplyChanges(this.SystemContext);
var severity = OpcEventSeverity.Low;
var limits = OpcLimitAlarmStates.Inactive;
var message = "No range problems";
if (positionValue <= this.positionLimitNode.LowLowLimit) {
limits = OpcLimitAlarmStates.LowLow;
message = "Out of lower bound range!";
severity = OpcEventSeverity.Medium;
}
else if (positionValue <= this.positionLimitNode.LowLimit) {
limits = OpcLimitAlarmStates.Low;
message = "About to reach lower bound!";
severity = OpcEventSeverity.MediumHigh;
}
else if (positionValue >= this.positionLimitNode.HighLimit) {
limits = OpcLimitAlarmStates.High;
message = "About to reach upper bound!";
severity = OpcEventSeverity.MediumHigh;
}
else if (positionValue >= this.positionLimitNode.HighHighLimit) {
limits = OpcLimitAlarmStates.HighHigh;
message = "Out of upper bound range!";
severity = OpcEventSeverity.High;
}
this.positionLimitNode.ChangeSeverity(this.SystemContext, severity);
this.positionLimitNode.ChangeLimitState(this.SystemContext, limits);
if (this.positionLimitNode.IsActive) {
this.positionLimitNode.Time = DateTime.UtcNow;
if (ackRequired) {
this.positionLimitNode.Message = message + " - Acknowledgement is required!";
this.positionLimitNode.ChangeIsAcked(this.SystemContext, false);
this.positionLimitNode.ChangeIsConfirmed(this.SystemContext, false);
this.positionLimitNode.ReceiveTime = DateTime.UtcNow;
}
}
// This will trigger Event notification being send to Event subscriptions.
this.positionLimitNode.ReportEventFrom(
this.SystemContext, this.positionNode);
}
}
/// <summary>
/// Simulates a progress which influences the <see cref="temperatureNode"/> and publishes
/// alarms using the <see cref="temperatureCriticalNode"/>.
/// </summary>
/// <param name="run">The sequence number of the simulation run within the temperature
/// simulation is to be performed.</param>
/// <param name="random">The <see cref="Random"/> instance used for random number
/// generation.</param>
private void SimulateTemperature(int run, Random random)
{
var temperatureValue = random.Next(12, 20 * (((run % 7) / 4) + 1));
this.temperatureNode.Value = temperatureValue;
// This will trigger DataChange notification being send to DataChange subscriptions.
this.temperatureNode.ApplyChanges(this.SystemContext);
if (temperatureValue <= 20) {
this.temperatureCriticalNode.ChangeIsActive(this.SystemContext, false);
}
else {
var message = "The temperature is higher than 20°C!";
var severity = OpcEventSeverity.Low;
if (temperatureValue <= 25) {
severity = OpcEventSeverity.Medium;
}
else if (temperatureValue <= 30) {
message = "The temperature is near to 30°C!";
severity = OpcEventSeverity.MediumHigh;
}
else if (temperatureValue <= 35) {
severity = OpcEventSeverity.High;
}
else {
message = "The temperature is near to 40°C!";
severity = OpcEventSeverity.Max;
}
this.temperatureCriticalNode.Message = message;
this.temperatureCriticalNode.ReceiveTime = DateTime.UtcNow;
this.temperatureCriticalNode.Time = DateTime.UtcNow;
this.temperatureCriticalNode.ChangeSeverity(this.SystemContext, severity);
this.temperatureCriticalNode.ChangeIsActive(this.SystemContext, true);
// This will trigger Event notification being send to Event subscriptions.
this.temperatureCriticalNode.ReportEventFrom(
this.SystemContext, this.temperatureNode);
}
}
#endregion
}
}