• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

WindhoverLabs / yamcs-opcua / #20

11 Jul 2024 06:48PM UTC coverage: 80.456% (+4.5%) from 76.0%
#20

push

lorenzo-gomez-windhover
-Cleanup

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

671 of 834 relevant lines covered (80.46%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.12
/src/main/java/com/windhoverlabs/yamcs/opcua/OPCUALink.java
1
/****************************************************************************
2
 *
3
 *   Copyright (c) 2024 Windhover Labs, L.L.C. All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions
7
 * are met:
8
 *
9
 * 1. Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 * 2. Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in
13
 *    the documentation and/or other materials provided with the
14
 *    distribution.
15
 * 3. Neither the name Windhover Labs nor the names of its
16
 *    contributors may be used to endorse or promote products derived
17
 *    from this software without specific prior written permission.
18
 *
19
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
23
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
26
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
27
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
 * POSSIBILITY OF SUCH DAMAGE.
31
 *
32
 *****************************************************************************/
33

34
package com.windhoverlabs.yamcs.opcua;
35

36
import static com.google.common.collect.Lists.newArrayList;
37
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
38
import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.toList;
39
import static org.yamcs.xtce.NameDescription.qualifiedName;
40

41
import com.google.gson.JsonObject;
42
import java.io.BufferedWriter;
43
import java.io.File;
44
import java.nio.file.Files;
45
import java.nio.file.Path;
46
import java.nio.file.Paths;
47
import java.nio.file.StandardOpenOption;
48
import java.time.Instant;
49
import java.time.temporal.ChronoUnit;
50
import java.util.ArrayList;
51
import java.util.Arrays;
52
import java.util.HashMap;
53
import java.util.HashSet;
54
import java.util.List;
55
import java.util.Map;
56
import java.util.Objects;
57
import java.util.Set;
58
import java.util.concurrent.CompletableFuture;
59
import java.util.concurrent.ConcurrentHashMap;
60
import java.util.concurrent.ExecutionException;
61
import java.util.concurrent.atomic.AtomicLong;
62
import java.util.function.Supplier;
63
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
64
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
65
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem;
66
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription;
67
import org.eclipse.milo.opcua.sdk.client.nodes.UaNode;
68
import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedDataItem;
69
import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedSubscription;
70
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
71
import org.eclipse.milo.opcua.stack.core.AttributeId;
72
import org.eclipse.milo.opcua.stack.core.Identifiers;
73
import org.eclipse.milo.opcua.stack.core.UaException;
74
import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
75
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
76
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
77
import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
78
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
79
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
80
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
81
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
82
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
83
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
84
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
85
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
86
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
87
import org.eclipse.milo.opcua.stack.core.types.enumerated.IdType;
88
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
89
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
90
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
91
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
92
import org.eclipse.milo.opcua.stack.core.types.structured.BrowsePath;
93
import org.eclipse.milo.opcua.stack.core.types.structured.BrowsePathResult;
94
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
95
import org.eclipse.milo.opcua.stack.core.types.structured.ContentFilter;
96
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
97
import org.eclipse.milo.opcua.stack.core.types.structured.EventFilter;
98
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
99
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
100
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
101
import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
102
import org.eclipse.milo.opcua.stack.core.types.structured.RelativePath;
103
import org.eclipse.milo.opcua.stack.core.types.structured.RelativePathElement;
104
import org.eclipse.milo.opcua.stack.core.types.structured.SimpleAttributeOperand;
105
import org.eclipse.milo.opcua.stack.core.types.structured.TranslateBrowsePathsToNodeIdsResponse;
106
import org.slf4j.Logger;
107
import org.slf4j.LoggerFactory;
108
import org.yamcs.ConfigurationException;
109
import org.yamcs.Spec;
110
import org.yamcs.Spec.OptionType;
111
import org.yamcs.StandardTupleDefinitions;
112
import org.yamcs.ValidationException;
113
import org.yamcs.YConfiguration;
114
import org.yamcs.YamcsServer;
115
import org.yamcs.http.NotFoundException;
116
import org.yamcs.mdb.XtceAssembler;
117
import org.yamcs.parameter.ParameterValue;
118
import org.yamcs.parameter.SystemParametersProducer;
119
import org.yamcs.parameter.SystemParametersService;
120
import org.yamcs.protobuf.Event.EventSeverity;
121
import org.yamcs.protobuf.Yamcs.NamedObjectId;
122
import org.yamcs.protobuf.Yamcs.Value.Type;
123
import org.yamcs.tctm.AbstractLink;
124
import org.yamcs.tctm.Link;
125
import org.yamcs.tctm.LinkAction;
126
import org.yamcs.utils.ValueUtility;
127
import org.yamcs.xtce.AggregateParameterType;
128
import org.yamcs.xtce.BinaryParameterType;
129
import org.yamcs.xtce.BooleanParameterType;
130
import org.yamcs.xtce.EnumeratedParameterType;
131
import org.yamcs.xtce.FloatParameterType;
132
import org.yamcs.xtce.IntegerParameterType;
133
import org.yamcs.xtce.Member;
134
import org.yamcs.xtce.NameDescription;
135
import org.yamcs.xtce.Parameter;
136
import org.yamcs.xtce.ParameterType;
137
import org.yamcs.xtce.SpaceSystem;
138
import org.yamcs.xtce.StringParameterType;
139
import org.yamcs.xtce.XtceDb;
140
import org.yamcs.yarch.DataType;
141
import org.yamcs.yarch.Stream;
142
import org.yamcs.yarch.Tuple;
143
import org.yamcs.yarch.TupleDefinition;
144
import org.yamcs.yarch.YarchDatabase;
145
import org.yamcs.yarch.YarchDatabaseInstance;
146
import org.yamcs.yarch.protobuf.Db.Event;
147

148
/**
149
 * Implementation of the OPCUA protocol as a YAMCS link. Maps configured nodes(see docs for details)
150
 * to yamcs PVs and subscribes to OPCUA variables for realtime updates.
151
 *
152
 * @author Lorenzo Gomez
153
 */
154
public class OPCUALink extends AbstractLink implements Runnable, SystemParametersProducer {
1✔
155

156
  class NodeIDAttrPair {
157
    NodeId nodeID;
158
    AttributeId attrID;
159

160
    public NodeIDAttrPair(NodeId newNodeID, AttributeId newAttrID) {
1✔
161
      this.nodeID = newNodeID;
1✔
162
      this.attrID = newAttrID;
1✔
163
    }
1✔
164

165
    public int hashCode() {
166
      return Objects.hash(this.nodeID, this.attrID);
1✔
167
    }
168

169
    public boolean equals(Object obj) {
170
      return (this.hashCode() == obj.hashCode());
1✔
171
    }
172
  }
173

174
  class NodePath {
1✔
175
    String path;
176

177
    HashMap<Object, Object> rootNodeID = new HashMap<Object, Object>();
1✔
178
  }
179

180
  /** Useful status for tracking initialization status of the link. */
181
  public enum OPCUAStatus {
1✔
182
    OPCUA_INIT_CONFIG,
1✔
183
    OPCUA_INIT_TREE,
1✔
184
    OPCUA_INIT_GENERATE_XTCE,
1✔
185
    OPCUA_INIT_EVENTS,
1✔
186
    OPCUA_INIT_DATA_SUBSCRIPTION,
1✔
187
    OPCUA_INIT_ALL_DATA_QUERY,
1✔
188
    OPCUA_OK
1✔
189
  }
190

191
  /* Configuration Defaults */
192
  static final String STREAM_NAME = "opcua_params";
193

194
  /* Internal member attributes. */
195
  protected Thread thread;
196

197
  private String opcuaStreamName;
198

199
  private String parametersNamespace;
200
  XtceDb mdb;
201

202
  Stream opcuaStream;
203

204
  private static TupleDefinition gftdef = StandardTupleDefinitions.PARAMETER.copy();
1✔
205

206
  private AggregateParameterType opcuaAttrsType;
207
  private AggregateParameterType opcuaNodeIdNumericType;
208
  private AggregateParameterType opcuaNodeIdStringType;
209
  private ManagedSubscription opcuaSubscription;
210

211
  private static final Logger internalLogger = LoggerFactory.getLogger(OPCUALink.class.getName());
1✔
212

213
  private int rootNamespaceIndex;
214

215
  private String rootIdentifier; // Relative to the rootNamespaceIndex
216

217
  private IdType rootIdentifierType; // Relative to the rootNamespaceIndex
218

219
  /**
220
   * @note ALWAYS re-use params as org.yamcs.parameter.ParameterRequestManager.param2RequestMap uses
221
   *     the object inside a map that was added to the mdb for the very fist time. If when
222
   *     publishing the PV, we create a new VariableParam object clients will NOT receive real-time
223
   *     updates as the new object VariableParam inside the new PV won't match the one inside
224
   *     org.yamcs.parameter.ParameterRequestManager.param2RequestMap since the object hashes do not
225
   *     match (since VariableParam does not override its hash function).
226
   */
227
  private ConcurrentHashMap<NodeIDAttrPair, VariableParam> nodeIDToParamsMap =
1✔
228
      new ConcurrentHashMap<NodeIDAttrPair, VariableParam>();
229

230
  private OpcUaClient client;
231

232
  protected AtomicLong inCount = new AtomicLong(0);
1✔
233

234
  private Status linkStatus = Status.OK;
1✔
235

236
  /* Configuration Parameters */
237

238
  private String discoverURL;
239
  private String endpointURL;
240
  private boolean queryAllNodesAtStartup;
241
  private String outputFile;
242
  private int publishInterval; // milliseconds
243

244
  private ArrayList<NodePath> relativeNodePaths = new ArrayList<NodePath>();
1✔
245

246
  private final AtomicLong clientHandles = new AtomicLong(1L);
1✔
247

248
  /* System parameters*/
249

250
  private Parameter OPCUAStatusParam;
251
  private OPCUAStatus currentOPCUAStatus;
252
  private Parameter OPCUAActiveSubsParam;
253
  private AtomicLong OPCUAActiveSubs = new AtomicLong(0);
1✔
254

255
  LinkAction startAction =
1✔
256
      new LinkAction("query_all", "Query All OPCUA Server Data") {
1✔
257
        @Override
258
        public JsonObject execute(Link link, JsonObject jsonObject) {
259

260
          internalLogger.info("Executing query_all action");
1✔
261
          CompletableFuture.supplyAsync(
1✔
262
                  (Supplier<Integer>)
263
                      () -> {
264
                        queryAllOPCUAData();
×
265

266
                        return 0;
×
267
                      })
268
              .whenComplete(
1✔
269
                  (vaue, e) -> {
270
                    internalLogger.info("query_all action Complete");
1✔
271
                  });
1✔
272

273
          return jsonObject;
1✔
274
        }
275
      };
276

277
  public OPCUAStatus getCurrentOPCUAStatus() {
278
    return currentOPCUAStatus;
1✔
279
  }
280

281
  @Override
282
  public Spec getSpec() {
283
    Spec spec = new Spec();
1✔
284

285
    /* Define our configuration parameters. */
286
    spec.addOption("name", OptionType.STRING).withRequired(true);
1✔
287
    spec.addOption("class", OptionType.STRING).withRequired(true);
1✔
288
    spec.addOption("opcuaStream", OptionType.STRING).withRequired(true);
1✔
289
    spec.addOption("endpointUrl", OptionType.STRING).withRequired(true);
1✔
290
    spec.addOption("discoveryUrl", OptionType.STRING).withRequired(true);
1✔
291
    spec.addOption("xtceOutputFile", OptionType.STRING).withRequired(true);
1✔
292
    spec.addOption("parametersNamespace", OptionType.STRING).withRequired(true);
1✔
293
    spec.addOption("publishInterval", OptionType.INTEGER).withRequired(true);
1✔
294
    spec.addOption("queryAllNodesAtStartup", OptionType.BOOLEAN).withRequired(false);
1✔
295

296
    Spec rootNodeIDSpec = new Spec();
1✔
297

298
    rootNodeIDSpec.addOption("namespaceIndex", OptionType.INTEGER).withRequired(true);
1✔
299
    rootNodeIDSpec.addOption("identifier", OptionType.STRING).withRequired(true);
1✔
300
    rootNodeIDSpec.addOption("identifierType", OptionType.STRING).withRequired(true);
1✔
301

302
    spec.addOption("rootNodeID", OptionType.MAP).withRequired(false).withSpec(rootNodeIDSpec);
1✔
303

304
    Spec nodePathSpec = new Spec();
1✔
305
    nodePathSpec.addOption("path", OptionType.STRING);
1✔
306
    nodePathSpec
1✔
307
        .addOption("rootNodeID", OptionType.MAP)
1✔
308
        .withRequired(true)
1✔
309
        .withSpec(rootNodeIDSpec);
1✔
310

311
    spec.addOption("nodePaths", OptionType.LIST)
1✔
312
        .withElementType(OptionType.MAP)
1✔
313
        .withRequired(true)
1✔
314
        .withSpec(nodePathSpec);
1✔
315

316
    return spec;
1✔
317
  }
318

319
  @Override
320
  public void init(String yamcsInstance, String serviceName, YConfiguration config)
321
      throws ConfigurationException {
322
    super.init(yamcsInstance, serviceName, config);
1✔
323

324
    /* Local variables */
325
    this.config = config;
1✔
326
    /* Validate the configuration that the user passed us. */
327
    try {
328
      config = getSpec().validate(config);
1✔
329
    } catch (ValidationException e) {
×
330
      log.error("Failed configuration validation.", e);
×
331
      notifyFailed(e);
×
332
    }
1✔
333
    YarchDatabaseInstance ydb = YarchDatabase.getInstance(yamcsInstance);
1✔
334

335
    this.opcuaStreamName = config.getString("opcuaStream");
1✔
336

337
    opcuaStream = getStream(ydb, opcuaStreamName);
1✔
338

339
    this.endpointURL = config.getString("endpointUrl");
1✔
340

341
    this.discoverURL = config.getString("discoveryUrl");
1✔
342

343
    this.parametersNamespace = config.getString("parametersNamespace");
1✔
344

345
    queryAllNodesAtStartup = config.getBoolean("queryAllNodesAtStartup", false);
1✔
346

347
    Map<Object, Object> root = config.getMap("rootNodeID");
1✔
348

349
    rootNamespaceIndex = (int) root.get("namespaceIndex");
1✔
350

351
    rootIdentifier = (String) root.get("identifier");
1✔
352
    rootIdentifierType = IdType.valueOf((String) root.get("identifierType"));
1✔
353

354
    List<Map<Object, Object>> nodePaths = config.getList("nodePaths");
1✔
355

356
    for (Map<Object, Object> path : nodePaths) {
1✔
357
      NodePath nodePath = new NodePath();
1✔
358
      nodePath.path = (String) path.get("path");
1✔
359
      nodePath.rootNodeID = (HashMap<Object, Object>) path.get("rootNodeID");
1✔
360
      relativeNodePaths.add(nodePath);
1✔
361
    }
1✔
362

363
    mdb = YamcsServer.getServer().getInstance(yamcsInstance).getXtceDb();
1✔
364

365
    outputFile = config.getString("xtceOutputFile");
1✔
366
    publishInterval = config.getInt("publishInterval");
1✔
367
  }
1✔
368

369
  private static SpaceSystem verifySpaceSystem(XtceDb mdb, String pathName) {
370
    String namespace;
371
    String name;
372
    int lastSlash = pathName.lastIndexOf('/');
1✔
373
    if ("/".equals(pathName)) {
1✔
374
      namespace = "";
1✔
375
      name = "";
1✔
376
    } else if (lastSlash == -1 || lastSlash == pathName.length() - 1) {
×
377
      namespace = "";
×
378
      name = pathName;
×
379
    } else {
380
      namespace = pathName.substring(0, lastSlash);
×
381
      name = pathName.substring(lastSlash + 1);
×
382
    }
383

384
    // First try with a prefixed slash (should be the common case)
385
    NamedObjectId id =
386
        NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build();
1✔
387
    SpaceSystem spaceSystem = mdb.getSpaceSystem(id);
1✔
388
    if (spaceSystem != null) {
1✔
389
      return spaceSystem;
1✔
390
    }
391

392
    // Maybe some non-xtce namespace like MDB:OPS Name
393
    id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build();
×
394
    spaceSystem = mdb.getSpaceSystem(id);
×
395
    if (spaceSystem != null) {
×
396
      return spaceSystem;
×
397
    }
398

399
    throw new NotFoundException("No such space system");
×
400
  }
401

402
  /**
403
   * Initializes all PV mappings to OPCUA nodes and realtime subscriptions(managed data items in
404
   * OPCUA terms).
405
   */
406
  private void opcuaInit() {
407
    createOPCUAAttrAggregateType();
1✔
408
    createOPCUANodeIdTypes();
1✔
409
    mdb.addParameterType(opcuaAttrsType, true);
1✔
410

411
    try {
412

413
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_TREE;
1✔
414

415
      browseOPCUATree(client);
1✔
416

417
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_GENERATE_XTCE;
1✔
418

419
      var spaceSystem = verifySpaceSystem(mdb, "/");
1✔
420

421
      var xtce = new XtceAssembler().toXtce(mdb, spaceSystem.getQualifiedName(), fqn -> true);
1✔
422

423
      BufferedWriter writer = null;
1✔
424

425
      if (outputFile != null) {
1✔
426
        writer =
1✔
427
            Files.newBufferedWriter(
1✔
428
                Paths.get(outputFile),
1✔
429
                StandardOpenOption.CREATE,
430
                StandardOpenOption.TRUNCATE_EXISTING);
431
      } else writer = null;
×
432

433
      writer.write(xtce);
1✔
434

435
      writer.flush();
1✔
436
      writer.close();
1✔
437

438
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_EVENTS;
1✔
439
      subscribeToEvents(client);
1✔
440

441
    } catch (Exception e) {
×
442
      // TODO Auto-generated catch block
443
      e.printStackTrace();
×
444
      return;
×
445
    }
1✔
446
    try {
447
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_DATA_SUBSCRIPTION;
1✔
448
      createOPCUASubscriptions();
1✔
449
    } catch (Exception e) {
×
450
      // TODO Auto-generated catch block
451
      e.printStackTrace();
×
452
    }
1✔
453
  }
1✔
454

455
  private void opcuaClientConnect() throws Exception {
456
    client = configureClient();
1✔
457
    connectToOPCUAServer(client);
1✔
458
  }
1✔
459

460
  private static Stream getStream(YarchDatabaseInstance ydb, String streamName) {
461
    Stream stream = ydb.getStream(streamName);
1✔
462
    if (stream == null) {
1✔
463
      try {
464
        ydb.execute("create stream " + streamName + gftdef.getStringDefinition());
1✔
465
      } catch (Exception e) {
×
466
        throw new ConfigurationException(e);
×
467
      }
1✔
468

469
      stream = ydb.getStream(streamName);
1✔
470
    }
471
    return stream;
1✔
472
  }
473

474
  @Override
475
  public void doDisable() {
476
    /* If the thread is created, interrupt it. */
477
    if (thread != null) {
1✔
478
      thread.interrupt();
1✔
479
    }
480

481
    linkStatus = Status.DISABLED;
1✔
482
  }
1✔
483

484
  @Override
485
  public void doEnable() {
486
    linkStatus = Status.OK;
1✔
487
  }
1✔
488

489
  @Override
490
  public String getDetailedStatus() {
491
    if (isDisabled()) {
1✔
492
      return String.format("DISABLED");
1✔
493
    } else {
494
      return String.format("OK, received %d packets", inCount.get());
1✔
495
    }
496
  }
497

498
  @Override
499
  public Status connectionStatus() {
500
    return linkStatus;
1✔
501
  }
502

503
  @Override
504
  protected void doStart() {
505
    try {
506
      opcuaClientConnect();
1✔
507
    } catch (Exception e) {
×
508
      e.printStackTrace();
×
509
      linkStatus = Status.FAILED;
×
510
      notifyFailed(e);
×
511
      return;
×
512
    }
1✔
513
    if (!isDisabled()) {
1✔
514
      doEnable();
1✔
515
    }
516
    startAction.addChangeListener(
1✔
517
        () -> {
518
          /**
519
           * TODO:Might be useful if we want turn off any functionality when the action is disabled
520
           * for instance..
521
           */
522
        });
×
523

524
    /* Create and start the new thread. */
525
    thread = new Thread(this);
1✔
526
    thread.setName(this.getClass().getSimpleName() + "-" + linkName);
1✔
527
    thread.start();
1✔
528

529
    notifyStarted();
1✔
530
  }
1✔
531

532
  @Override
533
  protected void doStop() {
534
    try {
535
      client.disconnect().get();
1✔
536
    } catch (InterruptedException | ExecutionException e) {
×
537
      // TODO Auto-generated catch block
538
      e.printStackTrace();
×
539
    }
1✔
540
    if (thread != null) {
1✔
541
      thread.interrupt();
1✔
542
    }
543

544
    notifyStopped();
1✔
545
  }
1✔
546

547
  @Override
548
  public void run() {
549
    opcuaInit();
1✔
550
    if (queryAllNodesAtStartup) {
1✔
551
      //            NOTE:I'm not sure if queryAllOPCUAData should block...
552
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_ALL_DATA_QUERY;
×
553
      queryAllOPCUAData();
×
554
    }
555
    /* Enter our main loop */
556
    while (isRunningAndEnabled()) {
1✔
557
      currentOPCUAStatus = OPCUAStatus.OPCUA_OK;
1✔
558
    }
559
  }
1✔
560

561
  /**
562
   * Reads all attributes of all configured Value nodes and updates their corresponding PV. Useful
563
   * for querying data from the OPCUA server once, data such as browse names, NodeIds, etc.
564
   */
565
  private void queryAllOPCUAData() {
566
    TupleDefinition tdef = gftdef.copy();
1✔
567
    List<Object> cols = new ArrayList<>(4 + nodeIDToParamsMap.keySet().size());
1✔
568

569
    tdef = gftdef.copy();
1✔
570
    long gentime = timeService.getMissionTime();
1✔
571
    cols.add(gentime);
1✔
572
    cols.add(parametersNamespace);
1✔
573
    cols.add(0);
1✔
574
    cols.add(gentime);
1✔
575

576
    int columnCount = 0;
1✔
577

578
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
579
    /**
580
     * FIXME:This is super inefficient... The reason we collect these nodeIDs in a set is because
581
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
582
     * nodeID given how nodeIDToParamsMap is designed
583
     */
584
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
585
      nodeSet.add(pair.nodeID);
1✔
586
    }
1✔
587

588
    for (NodeId nId : nodeSet) {
1✔
589
      UaNode node;
590

591
      try {
592
        node = client.getAddressSpace().getNode(nId);
1✔
593

594
        DataValue nodeClass = node.readAttribute(AttributeId.NodeClass);
1✔
595

596
        switch (NodeClass.from((int) nodeClass.getValue().getValue())) {
1✔
597
          case Variable:
598
            for (AttributeId attr : AttributeId.VARIABLE_ATTRIBUTES) {
1✔
599

600
              VariableParam p = nodeIDToParamsMap.get(new NodeIDAttrPair(nId, attr));
1✔
601

602
              if (p.getParameterType() == null) {
1✔
603

604
                String value = "";
×
605
                if (node.readAttribute(attr).getValue().isNull()) {
×
606
                  value = "NULL";
×
607
                } else {
608
                  value = node.readAttribute(attr).getValue().getValue().toString();
×
609
                }
610

611
                tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
612
                cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
613
                continue;
×
614
              }
615

616
              switch (p.getParameterType().getValueType()) {
1✔
617
                case BOOLEAN:
618
                  {
619
                    String value = "";
1✔
620
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
621
                      value = "NULL";
×
622
                    } else {
623
                      value = node.readAttribute(attr).getValue().getValue().toString();
1✔
624
                    }
625

626
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
627
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
628
                  }
629
                  break;
1✔
630
                case DOUBLE:
631
                  {
632
                    double value = 0;
1✔
633
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
634
                      //                      value = null;
635
                      //                            FIXME:Log warning
636
                    } else {
637
                      value = (double) node.readAttribute(attr).getValue().getValue();
1✔
638
                    }
639

640
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
641
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
642
                  }
643
                  break;
1✔
644
                case FLOAT:
645
                  {
646
                    float value = 0;
×
647
                    if (node.readAttribute(attr).getValue().isNull()) {
×
648
                      //                      value = null;
649
                      //                            FIXME:Log warning
650
                    } else {
651
                      value = (float) node.readAttribute(attr).getValue().getValue();
×
652
                    }
653

654
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
655
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
656
                  }
657
                  break;
×
658
                case NONE:
659
                  {
660
                    String value = "";
×
661
                    if (node.readAttribute(attr).getValue().isNull()) {
×
662
                      value = "NULL";
×
663
                    } else {
664
                      value = node.readAttribute(attr).getValue().getValue().toString();
×
665
                    }
666

667
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
668
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
669
                  }
670
                  break;
×
671
                case SINT32:
672
                  {
673
                    int value = 0;
1✔
674
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
675
                      //                      value = null;
676
                      //                            FIXME:Log warning
677
                    } else {
678
                      value = (int) node.readAttribute(attr).getValue().getValue();
1✔
679
                    }
680

681
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
682
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
683
                  }
684
                  break;
1✔
685
                case SINT64:
686
                  {
687
                    long value = 0;
1✔
688
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
689
                      //                      value = null;
690
                      //                            FIXME:Log warning
691
                    } else {
692
                      value = (long) node.readAttribute(attr).getValue().getValue();
1✔
693
                    }
694

695
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
696
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
697
                  }
698
                  break;
1✔
699
                case STRING:
700
                  {
701
                    String value = "";
1✔
702
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
703
                      value = "NULL";
1✔
704
                    } else {
705
                      value = node.readAttribute(attr).getValue().getValue().toString();
1✔
706
                    }
707

708
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
709
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
710
                  }
711
                  break;
1✔
712
                case UINT32:
713
                  {
714
                    long value = 0;
×
715
                    if (node.readAttribute(attr).getValue().isNull()) {
×
716
                      //                      value = null;
717
                      //                            FIXME:Log warning
718
                    } else {
719
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
720
                    }
721

722
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
723
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
724
                  }
725
                  break;
×
726
                case UINT64:
727
                  {
728
                    long value = 0;
×
729
                    if (node.readAttribute(attr).getValue().isNull()) {
×
730
                      //                      value = null;
731
                      //                            FIXME:Log warning
732
                    } else {
733
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
734
                    }
735

736
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
737
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
738
                  }
739
                  break;
×
740
                default:
741
                  break;
742
              }
743

744
              log.debug("Pushing {} to stream", p.toString());
1✔
745

746
              columnCount++;
1✔
747
            }
1✔
748
            break;
1✔
749
          default:
750
            break;
751
        }
752

753
      } catch (UaException e) {
×
754
        // TODO Auto-generated catch block
755
        e.printStackTrace();
×
756
        continue;
×
757
      }
1✔
758
    }
1✔
759

760
    /**
761
     * FIXME:Need to come up with a mechanism to not update certain values that are up to date...
762
     * The more I think about it, it might make sense to have "static" and "runtime" namespaces
763
     */
764
    pushTuple(tdef, cols);
×
765

766
    inCount.getAndAdd(columnCount);
×
767
  }
×
768

769
  private synchronized void pushTuple(TupleDefinition tdef, List<Object> cols) {
770
    Tuple t;
771
    t = new Tuple(tdef, cols);
1✔
772
    opcuaStream.emitTuple(t);
1✔
773
  }
1✔
774

775
  private static ParameterType getOrCreateType(
776
      XtceDb mdb, String name, Supplier<ParameterType.Builder<?>> supplier) {
777

778
    String fqn = XtceDb.YAMCS_SPACESYSTEM_NAME + NameDescription.PATH_SEPARATOR + name;
1✔
779
    ParameterType ptype = mdb.getParameterType(fqn);
1✔
780
    if (ptype != null) {
1✔
781
      return ptype;
1✔
782
    }
783
    ParameterType.Builder<?> typeb = supplier.get().setName(name);
1✔
784

785
    ptype = typeb.build();
1✔
786
    ((NameDescription) ptype).setQualifiedName(fqn);
1✔
787

788
    return mdb.addSystemParameterType(ptype);
1✔
789
  }
790

791
  public static ParameterType getBasicType(XtceDb mdb, Type type) {
792
    ParameterType pType = null;
1✔
793
    switch (type) {
1✔
794
      case BINARY:
795
        return getOrCreateType(mdb, "binary", () -> new BinaryParameterType.Builder());
×
796
      case BOOLEAN:
797
        return getOrCreateType(mdb, "boolean", () -> new BooleanParameterType.Builder());
1✔
798
      case STRING:
799
        return getOrCreateType(mdb, "string", () -> new StringParameterType.Builder());
1✔
800

801
      case FLOAT:
802
        return getOrCreateType(
1✔
803
            mdb, "float32", () -> new FloatParameterType.Builder().setSizeInBits(32));
1✔
804
      case DOUBLE:
805
        return getOrCreateType(
1✔
806
            mdb, "float64", () -> new FloatParameterType.Builder().setSizeInBits(64));
1✔
807
      case SINT32:
808
        return getOrCreateType(
1✔
809
            mdb,
810
            "sint32",
811
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(true));
×
812
      case SINT64:
813
        return getOrCreateType(
1✔
814
            mdb,
815
            "sint64",
816
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(true));
1✔
817
      case UINT32:
818
        return getOrCreateType(
×
819
            mdb,
820
            "uint32",
821
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(false));
×
822
      case UINT64:
823
        return getOrCreateType(
1✔
824
            mdb,
825
            "uint64",
826
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(false));
×
827
      default:
828
        break;
829
    }
830

UNCOV
831
    return pType;
×
832
  }
833

834
  public static ParameterValue getNewPv(Parameter parameter, long time) {
835
    ParameterValue pv = new ParameterValue(parameter);
1✔
836
    pv.setAcquisitionTime(time);
1✔
837
    pv.setGenerationTime(time);
1✔
838
    return pv;
1✔
839
  }
840

841
  public static ParameterValue getPV(Parameter parameter, long time, String v) {
842
    ParameterValue pv = getNewPv(parameter, time);
1✔
843
    pv.setEngValue(ValueUtility.getStringValue(v));
1✔
844
    return pv;
1✔
845
  }
846

847
  public static ParameterValue getPV(Parameter parameter, long time, double v) {
848
    ParameterValue pv = getNewPv(parameter, time);
1✔
849
    pv.setEngValue(ValueUtility.getDoubleValue(v));
1✔
850
    return pv;
1✔
851
  }
852

853
  public static ParameterValue getPV(Parameter parameter, long time, float v) {
854
    ParameterValue pv = getNewPv(parameter, time);
1✔
855
    pv.setEngValue(ValueUtility.getFloatValue(v));
1✔
856
    return pv;
1✔
857
  }
858

859
  public static ParameterValue getPV(Parameter parameter, long time, boolean v) {
860
    ParameterValue pv = getNewPv(parameter, time);
1✔
861
    pv.setEngValue(ValueUtility.getBooleanValue(v));
1✔
862
    return pv;
1✔
863
  }
864

865
  public static ParameterValue getPV(Parameter parameter, long time, long v) {
866
    ParameterValue pv = getNewPv(parameter, time);
1✔
867
    pv.setEngValue(ValueUtility.getSint64Value(v));
1✔
868
    return pv;
1✔
869
  }
870

871
  public static ParameterValue getUnsignedIntPV(Parameter parameter, long time, int v) {
872
    ParameterValue pv = getNewPv(parameter, time);
×
873
    pv.setEngValue(ValueUtility.getUint64Value(v));
×
874
    return pv;
×
875
  }
876

877
  @Override
878
  public Status getLinkStatus() {
879
    return linkStatus;
1✔
880
  }
881

882
  @Override
883
  public boolean isDisabled() {
884
    // TODO Auto-generated method stub
885
    return linkStatus == Status.DISABLED;
1✔
886
  }
887

888
  @Override
889
  public long getDataInCount() {
890
    // TODO Auto-generated method stub
891
    return inCount.get();
1✔
892
  }
893

894
  @Override
895
  public long getDataOutCount() {
896
    // TODO Auto-generated method stub
897
    return 0;
1✔
898
  }
899

900
  @Override
901
  public void resetCounters() {
902
    // TODO Auto-generated method stub
903
    inCount.set(0);
1✔
904
  }
1✔
905

906
  /**
907
   * Selects first non-secured endpoint from endpoints found at discover URL. At the moment secured
908
   * endpoints are not supported.
909
   *
910
   * @return
911
   * @throws Exception
912
   */
913
  private OpcUaClient configureClient() throws Exception {
914
    Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "client", "security");
1✔
915
    Files.createDirectories(securityTempDir);
1✔
916
    if (!Files.exists(securityTempDir)) {
1✔
917
      throw new Exception("unable to create security dir: " + securityTempDir);
×
918
    }
919

920
    File pkiDir = securityTempDir.resolve("pki").toFile();
1✔
921

922
    LoggerFactory.getLogger(getClass()).info("security dir: {}", securityTempDir.toAbsolutePath());
1✔
923
    LoggerFactory.getLogger(getClass()).info("security pki dir: {}", pkiDir.getAbsolutePath());
1✔
924

925
    List<EndpointDescription> endpoints = DiscoveryClient.getEndpoints(discoverURL).get();
1✔
926

927
    //    FIXME:At the moment, we do not support certificates...
928
    EndpointDescription selectedEndpoint = null;
1✔
929
    for (var endpoint : endpoints) {
1✔
930
      switch (endpoint.getSecurityMode()) {
1✔
931
        case Invalid:
932
          //                        FIXME:Add log message
933
          break;
×
934
        case None:
935
          //                        FIXME:Add log message
936
          selectedEndpoint = endpoint;
1✔
937
          break;
1✔
938
          //                        FIXME:Add log message
939
        case Sign:
940
          break;
×
941
        case SignAndEncrypt:
942
          //                        FIXME:Add log message
943
          break;
×
944
        default:
945
          break;
946
      }
947

948
      if (selectedEndpoint != null) {
1✔
949
        break;
1✔
950
      }
951
    }
×
952

953
    if (selectedEndpoint == null) {
1✔
954
      throw new Exception("No viable endpoint found from list:" + endpoints);
×
955
    }
956

957
    OpcUaClientConfig builder = OpcUaClientConfig.builder().setEndpoint(selectedEndpoint).build();
1✔
958

959
    return OpcUaClient.create(builder);
1✔
960
  }
961

962
  /**
963
   * Browse all nodes starting from browseRoot.
964
   *
965
   * @param indent
966
   * @param client
967
   * @param browseRoot
968
   */
969
  private void browseNodeWithReferences(String indent, OpcUaClient client, NodeId browseRoot) {
970
    BrowseDescription browse =
1✔
971
        new BrowseDescription(
972
            browseRoot,
973
            BrowseDirection.Forward,
974
            Identifiers.References,
975
            true,
1✔
976
            uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
1✔
977
            uint(BrowseResultMask.All.getValue()));
1✔
978

979
    try {
980

981
      BrowseResult browseResult = client.browse(browse).get();
1✔
982

983
      List<ReferenceDescription> references = toList(browseResult.getReferences());
1✔
984

985
      if (references.isEmpty()) {
1✔
986
        //              FIXME:Add log here
987

988
        return;
1✔
989
      }
990

991
      for (ReferenceDescription rd : references) {
1✔
992
        Object desc = null;
1✔
993
        Object value = null;
1✔
994
        UaNode node = null;
1✔
995
        try {
996

997
          node =
1✔
998
              client
999
                  .getAddressSpace()
1✔
1000
                  .getNode(rd.getNodeId().toNodeId(client.getNamespaceTable()).get());
1✔
1001
          DataValue attr = node.readAttribute(AttributeId.Description);
1✔
1002
          desc = attr.getValue().getValue();
1✔
1003

1004
          attr = node.readAttribute(AttributeId.Value);
1✔
1005

1006
          value = attr.getValue();
1✔
1007

1008
        } catch (UaException e) {
×
1009
          // TODO Auto-generated catch block
1010
          e.printStackTrace();
×
1011
        }
1✔
1012

1013
        if (node != null) {
1✔
1014
          addOPCUAPV(client, node);
1✔
1015

1016
          log.debug(
1✔
1017
              "{} Node={}, Desc={}, Value={}", indent, rd.getBrowseName().getName(), desc, value);
1✔
1018

1019
          if (rd.getIsForward()) {}
1✔
1020

1021
          // recursively browse to children
1022
          rd.getNodeId()
1✔
1023
              .toNodeId(client.getNamespaceTable())
1✔
1024
              .ifPresent(nodeId -> browseNodeWithReferences(indent + "  ", client, nodeId));
1✔
1025
        }
1026
      }
1✔
1027

1028
    } catch (InterruptedException e1) {
×
1029
      // TODO Auto-generated catch block
1030
      e1.printStackTrace();
×
1031
    } catch (ExecutionException e1) {
×
1032
      // TODO Auto-generated catch block
1033
      e1.printStackTrace();
×
1034
    }
1✔
1035
  }
1✔
1036

1037
  /**
1038
   * Adds new PV with the name of node.
1039
   *
1040
   * @param client
1041
   * @param node
1042
   */
1043
  private void addOPCUAPV(OpcUaClient client, UaNode node) {
1044

1045
    if (node.getBrowseName()
1✔
1046
        .getName()
1✔
1047
        .contains(Character.toString(NameDescription.PATH_SEPARATOR))) {
1✔
1048
      internalLogger.info(
1✔
1049
          "{} ignored since it contains a {} character",
1050
          node.getBrowseName().getName(),
1✔
1051
          Character.toString(NameDescription.PATH_SEPARATOR));
1✔
1052

1053
    } else {
1054

1055
      /**
1056
       * NOTE:For now we'll just flatten all the attributes instead of using an aggregate type for
1057
       * attributes
1058
       */
1059
      for (AttributeId attr : AttributeId.values()) {
1✔
1060

1061
        ParameterType ptype = OPCUAAttrTypeToParamType(attr, node);
1✔
1062

1063
        String opcuaTranslatedQName = translateNodeToParamQName(client, node, attr);
1✔
1064
        Parameter p = VariableParam.getForFullyQualifiedName(opcuaTranslatedQName);
1✔
1065

1066
        p.setParameterType(ptype);
1✔
1067

1068
        if (mdb.getParameter(p.getQualifiedName()) == null) {
1✔
1069
          log.debug("Adding OPCUA object as parameter to mdb:{}", p.getQualifiedName());
1✔
1070
          mdb.addParameter(p, true);
1✔
1071

1072
          nodeIDToParamsMap.put(new NodeIDAttrPair(node.getNodeId(), attr), (VariableParam) p);
1✔
1073
        }
1074
      }
1075
    }
1076
  }
1✔
1077

1078
  /**
1079
   * Map nodeID name to a qualified name that can be used for a YAMCS PV.
1080
   *
1081
   * @param client
1082
   * @param node
1083
   * @param attr
1084
   * @return
1085
   */
1086
  private String translateNodeToParamQName(OpcUaClient client, UaNode node, AttributeId attr) {
1087
    LocalizedText localizedDisplayName = null;
1✔
1088
    try {
1089

1090
      localizedDisplayName =
1✔
1091
          (LocalizedText) (node.readAttribute(AttributeId.DisplayName).getValue().getValue());
1✔
1092
    } catch (UaException e) {
×
1093
      // TODO Auto-generated catch block
1094
      e.printStackTrace();
×
1095
    }
1✔
1096
    String opcuaTranslatedQName =
1✔
1097
        qualifiedName(
1✔
1098
            parametersNamespace
1099
                + NameDescription.PATH_SEPARATOR
1100
                + node.getNodeId().toParseableString().replace(";", "-")
1✔
1101
                + NameDescription.PATH_SEPARATOR
1102
                + localizedDisplayName.getText(),
1✔
1103
            attr.toString());
1✔
1104

1105
    return opcuaTranslatedQName;
1✔
1106
  }
1107

1108
  /**
1109
   * Browse node at nodePath relative to browseRoot.
1110
   *
1111
   * @param indent
1112
   * @param client
1113
   * @param browseRoot
1114
   * @param nodePath in the format of "0:Root,0:Objects,2:HelloWorld,2:MyObject,2:Bar"
1115
   */
1116
  private void browsePath(String indent, OpcUaClient client, NodeId startingNode, String nodePath) {
1117
    internalLogger.info("Browsing at " + startingNode);
1✔
1118
    ArrayList<String> rPathTokens = new ArrayList<String>();
1✔
1119
    ArrayList<RelativePathElement> relaitivePathElements = new ArrayList<RelativePathElement>();
1✔
1120

1121
    for (var pathToken : nodePath.split(",")) {
1✔
1122
      rPathTokens.add(nodePath);
1✔
1123

1124
      int namespaceIndex = 0;
1✔
1125

1126
      String namespaceName = "";
1✔
1127

1128
      namespaceIndex = Integer.parseInt(pathToken.split(":")[0]);
1✔
1129

1130
      namespaceName = pathToken.split(":")[1];
1✔
1131

1132
      relaitivePathElements.add(
1✔
1133
          new RelativePathElement(
1134
              Identifiers.HierarchicalReferences,
1135
              false,
1✔
1136
              true,
1✔
1137
              new QualifiedName(namespaceIndex, namespaceName)));
1138
    }
1139

1140
    ArrayList<BrowsePath> list = new ArrayList<BrowsePath>();
1✔
1141

1142
    RelativePathElement[] elements = new RelativePathElement[relaitivePathElements.size()];
1✔
1143

1144
    relaitivePathElements.toArray(elements);
1✔
1145

1146
    list.add(new BrowsePath(startingNode, new RelativePath(elements)));
1✔
1147

1148
    TranslateBrowsePathsToNodeIdsResponse response = null;
1✔
1149
    try {
1150
      response = client.translateBrowsePaths(list).get();
1✔
1151
    } catch (InterruptedException e) {
×
1152
      // TODO Auto-generated catch block
1153
      e.printStackTrace();
×
1154
    } catch (ExecutionException e) {
×
1155
      // TODO Auto-generated catch block
1156
      e.printStackTrace();
×
1157
    }
1✔
1158

1159
    BrowsePathResult result = Arrays.asList(response.getResults()).get(0);
1✔
1160
    StatusCode statusCode = result.getStatusCode();
1✔
1161

1162
    if (statusCode.isBad()) {
1✔
1163
      log.warn("Bad status code:" + statusCode);
×
1164
      return;
×
1165
    } else if (statusCode.isUncertain()) {
1✔
1166
      log.warn("Uncertain status code:" + statusCode);
×
1167
      return;
×
1168
    }
1169

1170
    try {
1171
      UaNode node =
1✔
1172
          client
1173
              .getAddressSpace()
1✔
1174
              .getNode(
1✔
1175
                  result.getTargets()[0].getTargetId().toNodeId(client.getNamespaceTable()).get());
1✔
1176

1177
      addOPCUAPV(client, node);
1✔
1178
    } catch (UaException e) {
×
1179
      // TODO Auto-generated catch block
1180
      e.printStackTrace();
×
1181
    }
1✔
1182
  }
1✔
1183

1184
  private void createOPCUASubscriptions() {
1185
    createDataChangeListener();
1✔
1186
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
1187
    /**
1188
     * FIXME:This is super inefficient... The reason we collect these nodeIDs in a set is because
1189
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
1190
     * nodeID given how nodeIDToParamsMap is designed
1191
     */
1192
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
1193
      nodeSet.add(pair.nodeID);
1✔
1194
    }
1✔
1195
    for (NodeId id : nodeSet) {
1✔
1196
      Variant nodeClass = null;
1✔
1197
      try {
1198
        UaNode node = client.getAddressSpace().getNode(id);
1✔
1199

1200
        nodeClass = node.readAttribute(AttributeId.NodeClass).getValue();
1✔
1201

1202
      } catch (UaException e) {
×
1203
        // TODO Auto-generated catch block
1204
        e.printStackTrace();
×
1205
      }
1✔
1206
      if (nodeClass != null) {
1✔
1207
        try {
1208
          switch (NodeClass.from((int) nodeClass.getValue())) {
1✔
1209
              // As per the spec, the only thing we can subscribe to is Variables
1210
            case Variable:
1211
              ManagedDataItem dataItem = opcuaSubscription.createDataItem(id);
1✔
1212
              OPCUAActiveSubs.addAndGet(1);
1✔
1213
              log.debug("Status code for dataItem:{}", dataItem.getStatusCode());
1✔
1214
              break;
1215
          }
1216
        } catch (UaException e) {
×
1217
          // TODO Auto-generated catch block
1218
          e.printStackTrace();
×
1219
        }
1✔
1220
      }
1221
    }
1✔
1222
  }
1✔
1223

1224
  public void connectToOPCUAServer(OpcUaClient client) throws Exception {
1225
    internalLogger.info("Connecting to OPCUA server...");
1✔
1226
    client.connect().get();
1✔
1227

1228
    addAction(startAction);
1✔
1229
    startAction.setEnabled(true);
1✔
1230
  }
1✔
1231

1232
  /**
1233
   * Browses the tree on the OPCUA server and maps them to YAMCS Parameters.
1234
   *
1235
   * @param client
1236
   */
1237
  private void browseOPCUATree(OpcUaClient client) {
1238
    // start browsing at root folder
1239
    internalLogger.info("Browsing OPCUA...");
1✔
1240
    for (var p : relativeNodePaths) {
1✔
1241
      int namespaceIndex = (int) p.rootNodeID.get("namespaceIndex");
1✔
1242
      String identifier = (String) p.rootNodeID.get("identifier");
1✔
1243
      IdType identifierType = IdType.valueOf((String) p.rootNodeID.get("identifierType"));
1✔
1244

1245
      browsePath(
1✔
1246
          endpointURL, client, getNewNodeID(identifierType, namespaceIndex, identifier), p.path);
1✔
1247
    }
1✔
1248

1249
    NodeId nodeID = null;
1✔
1250
    nodeID = getNewNodeID(rootIdentifierType, rootNamespaceIndex, rootIdentifier);
1✔
1251

1252
    //  FIXME:Make root default when no namespaceIndex/identifier pair is specified
1253
    browseNodeWithReferences("", client, nodeID);
1✔
1254
  }
1✔
1255

1256
  /**
1257
   * Get new OPCUA-compliant NodeID object that is created from NamespaceIndex and Identifier. At
1258
   * the moment only String and Numeric node ids are supported.
1259
   *
1260
   * @param rootIdentifierType
1261
   * @param NamespaceIndex
1262
   * @param Identifier
1263
   * @return
1264
   */
1265
  private NodeId getNewNodeID(IdType rootIdentifierType, int NamespaceIndex, String Identifier) {
1266
    NodeId nodeID = null;
1✔
1267
    switch (rootIdentifierType) {
1✔
1268
      case Guid:
1269
        //                FIXME
1270
        break;
×
1271
      case Numeric:
1272
        nodeID = new NodeId(NamespaceIndex, Integer.parseInt(Identifier));
1✔
1273
        break;
1✔
1274
      case Opaque:
1275
        //                FIXME
1276
        break;
×
1277
      case String:
1278
        nodeID = new NodeId(NamespaceIndex, Identifier);
×
1279
        break;
×
1280
      default:
1281
        break;
1282
    }
1283
    return nodeID;
1✔
1284
  }
1285

1286
  /** Data listener for realtime OPCUA server updates. */
1287
  private void createDataChangeListener() {
1288
    try {
1289
      opcuaSubscription = ManagedSubscription.create(client, publishInterval);
1✔
1290
    } catch (UaException e) {
×
1291
      // TODO Auto-generated catch block
1292
      e.printStackTrace();
×
1293
    }
1✔
1294
    opcuaSubscription.addDataChangeListener(
1✔
1295
        (items, values) -> {
1296
          for (int i = 0; i < items.size(); i++) {
1✔
1297
            NodeIDAttrPair nodeAttrKey =
1✔
1298
                new NodeIDAttrPair(items.get(i).getNodeId(), AttributeId.Value);
1✔
1299
            log.debug(
1✔
1300
                "subscription value received: item={}, value={}",
1301
                items.get(i).getNodeId(),
1✔
1302
                values.get(i).getValue());
1✔
1303

1304
            log.debug(
1✔
1305
                "Pushing new PV for param name {} which is mapped to NodeID {}",
1306
                nodeIDToParamsMap.get(nodeAttrKey),
1✔
1307
                items.get(i).getNodeId());
1✔
1308

1309
            TupleDefinition tdef = gftdef.copy();
1✔
1310
            List<Object> cols = new ArrayList<>(4 + 1);
1✔
1311
            //            FIXME: Add leap seconds.... as config or get it from YAMCS API.
1312
            long gentime =
1✔
1313
                values
1314
                    .get(i)
1✔
1315
                    .getSourceTime()
1✔
1316
                    .getJavaInstant()
1✔
1317
                    .plus(37, ChronoUnit.SECONDS)
1✔
1318
                    .toEpochMilli();
1✔
1319
            cols.add(gentime);
1✔
1320
            cols.add(parametersNamespace);
1✔
1321
            cols.add(0);
1✔
1322
            cols.add(gentime);
1✔
1323

1324
            /**
1325
             * TODO:Not sure if this is the best way to do this since the aggregate values will be
1326
             * partially updated. Another potential approach might be to decouple the live OPCUA
1327
             * data(subscriptions) via namespaces. For example; have a "special" namespace called
1328
             * "subscriptions" that ONLY gets updated with items. And maybe another namespace for
1329
             * static data...maybe.
1330
             *
1331
             * <p>Another option is to flatten everything and have no aggregate types at all. That
1332
             * approach might even simplify the code quite a bit...
1333
             *
1334
             * <p>Another question worth answering before moving forward is to find out whether or
1335
             * not it is concrete in the OPCUA protocol what data can change in real time and which
1336
             * data is "static". Not sure if there is any "static" data given that clients have the
1337
             * ability of writing to values... might be worth a test.
1338
             */
1339
            log.debug(
1✔
1340
                "Data({}) chnage triggered for {}",
1341
                values.get(i).getValue(),
1✔
1342
                nodeIDToParamsMap.get(nodeAttrKey));
1✔
1343

1344
            if (nodeIDToParamsMap.get(nodeAttrKey) == null) {
1✔
1345
              log.debug("No parameter mapping found for {}", nodeAttrKey.nodeID);
×
1346
              continue;
×
1347
            } else {
1348
              log.debug(
1✔
1349
                  String.format(
1✔
1350
                      "parameter mapping found for {} and {}",
1351
                      nodeAttrKey.nodeID,
1352
                      nodeAttrKey.attrID));
1353
            }
1354

1355
            if (values.get(i).getValue() != null && values.get(i).getValue().getValue() != null) {
1✔
1356

1357
              switch (nodeIDToParamsMap.get(nodeAttrKey).getParameterType().getValueType()) {
1✔
1358
                case AGGREGATE:
1359
                  {
1360
                    String value = (String) values.get(i).getValue().getValue();
×
1361

1362
                    tdef.addColumn(
×
1363
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1364
                        DataType.PARAMETER_VALUE);
1365
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1366
                  }
1367
                  break;
×
1368
                case ARRAY:
1369
                  {
1370
                    String value = (String) values.get(i).getValue().getValue();
×
1371

1372
                    tdef.addColumn(
×
1373
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1374
                        DataType.PARAMETER_VALUE);
1375
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1376
                  }
1377
                  break;
×
1378
                case BINARY:
1379
                  {
1380
                    String value = (String) values.get(i).getValue().getValue();
×
1381
                    tdef.addColumn(
×
1382
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1383
                        DataType.PARAMETER_VALUE);
1384
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1385
                  }
1386
                  break;
×
1387
                case BOOLEAN:
1388
                  {
1389
                    boolean value = (boolean) values.get(i).getValue().getValue();
1✔
1390

1391
                    tdef.addColumn(
1✔
1392
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1393
                        DataType.PARAMETER_VALUE);
1394
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1395
                  }
1396
                  break;
1✔
1397
                case DOUBLE:
1398
                  {
1399
                    double value = (double) values.get(i).getValue().getValue();
1✔
1400

1401
                    tdef.addColumn(
1✔
1402
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1403
                        DataType.PARAMETER_VALUE);
1404
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1405
                  }
1406
                  break;
1✔
1407
                case ENUMERATED:
1408
                  {
1409
                    String value = (String) values.get(i).getValue().getValue();
×
1410

1411
                    tdef.addColumn(
×
1412
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1413
                        DataType.PARAMETER_VALUE);
1414
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1415
                  }
1416
                  break;
×
1417
                case FLOAT:
1418
                  {
1419
                    float value = (float) values.get(i).getValue().getValue();
1✔
1420

1421
                    tdef.addColumn(
1✔
1422
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1423
                        DataType.PARAMETER_VALUE);
1424
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1425
                  }
1426
                  break;
1✔
1427
                case NONE:
1428
                  {
1429
                    String value = (String) values.get(i).getValue().getValue();
×
1430

1431
                    tdef.addColumn(
×
1432
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1433
                        DataType.PARAMETER_VALUE);
1434
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1435
                  }
1436
                  break;
×
1437
                case SINT32:
1438
                  {
1439
                    int value = (int) values.get(i).getValue().getValue();
1✔
1440
                    tdef.addColumn(
1✔
1441
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1442
                        DataType.PARAMETER_VALUE);
1443
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1444
                  }
1445
                  break;
1✔
1446
                case SINT64:
1447
                  {
1448
                    long value = (long) values.get(i).getValue().getValue();
1✔
1449

1450
                    tdef.addColumn(
1✔
1451
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1452
                        DataType.PARAMETER_VALUE);
1453
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1454
                  }
1455
                  break;
1✔
1456
                case STRING:
1457
                  {
1458
                    String value = (String) values.get(i).getValue().getValue().toString();
1✔
1459

1460
                    tdef.addColumn(
1✔
1461
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1462
                        DataType.PARAMETER_VALUE);
1463
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1464
                  }
1465
                  break;
1✔
1466
                case TIMESTAMP:
1467
                  {
1468
                    String value = (String) values.get(i).getValue().getValue();
×
1469

1470
                    tdef.addColumn(
×
1471
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1472
                        DataType.PARAMETER_VALUE);
1473
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1474
                  }
1475
                  break;
×
1476
                case UINT32:
1477
                  {
1478
                    int value = (int) values.get(i).getValue().getValue();
×
1479
                    tdef.addColumn(
×
1480
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1481
                        DataType.PARAMETER_VALUE);
1482
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1483
                  }
1484
                  break;
×
1485
                case UINT64:
1486
                  {
1487
                    long value = (long) values.get(i).getValue().getValue();
×
1488

1489
                    tdef.addColumn(
×
1490
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1491
                        DataType.PARAMETER_VALUE);
1492
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1493
                  }
1494
                  break;
×
1495
                default:
1496
                  break;
1497
              }
1498

1499
              pushTuple(tdef, cols);
1✔
1500

1501
              inCount.getAndAdd(1);
1✔
1502
            } else {
1503
              // TODO:Add some type emptyValue count for OPS.
1504
              log.warn(
1✔
1505
                  "Data chnage triggered for {}, but it empty. This should not happen.",
1506
                  nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName());
1✔
1507
            }
1508
          }
1509
        });
1✔
1510
  }
1✔
1511

1512
  /**
1513
   * This method is here for future growth in case we find there is a benefit to using aggregate
1514
   * types
1515
   */
1516
  private void createOPCUAAttrAggregateType() {
1517

1518
    AggregateParameterType.Builder opcuaAttrsTypeBuidlder = new AggregateParameterType.Builder();
1✔
1519

1520
    opcuaAttrsType = new AggregateParameterType.Builder().setName("OPCUObjectAttributes").build();
1✔
1521

1522
    opcuaAttrsTypeBuidlder.setName("OPCUObjectAttributes");
1✔
1523
    for (AttributeId attr : AttributeId.values()) {
1✔
1524
      opcuaAttrsTypeBuidlder.addMember(new Member(attr.toString(), getBasicType(mdb, Type.STRING)));
1✔
1525
    }
1526

1527
    opcuaAttrsType = opcuaAttrsTypeBuidlder.build();
1✔
1528
    ((NameDescription) opcuaAttrsType)
1✔
1529
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaAttrsType.getName()));
1✔
1530
  }
1✔
1531

1532
  /**
1533
   * This method is here for future growth in case we find there is a benefit to using aggregate
1534
   * types
1535
   */
1536
  private void createOPCUANodeIdTypes() {
1537
    AggregateParameterType.Builder opcuaAttrsNumericNodeIdBuidlder =
1✔
1538
        new AggregateParameterType.Builder();
1539

1540
    opcuaAttrsNumericNodeIdBuidlder.setName("OPCUA_Numeric_NodeId");
1✔
1541
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1542
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1543
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1544
        new Member("identifier", getBasicType(mdb, Type.UINT64)));
1✔
1545

1546
    opcuaNodeIdNumericType = opcuaAttrsNumericNodeIdBuidlder.build();
1✔
1547
    ((NameDescription) opcuaNodeIdNumericType)
1✔
1548
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdNumericType.getName()));
1✔
1549

1550
    AggregateParameterType.Builder opcuaAttrsTypeStringBuidlder =
1✔
1551
        new AggregateParameterType.Builder();
1552

1553
    opcuaAttrsTypeStringBuidlder.setName("OPCUA_String_NodeId");
1✔
1554
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1555
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1556
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1557
        new Member("identifier", getBasicType(mdb, Type.STRING)));
1✔
1558

1559
    opcuaNodeIdStringType = opcuaAttrsTypeStringBuidlder.build();
1✔
1560
    ((NameDescription) opcuaNodeIdStringType)
1✔
1561
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdStringType.getName()));
1✔
1562

1563
    mdb.addParameterType(opcuaNodeIdNumericType, true);
1✔
1564
    mdb.addParameterType(opcuaNodeIdStringType, true);
1✔
1565
  }
1✔
1566

1567
  /**
1568
   * Get new ParameterType for the specified attribute of the node. Particularly useful for Value
1569
   * attributes of nodes.
1570
   *
1571
   * @param attr
1572
   * @param node
1573
   * @return
1574
   */
1575
  private ParameterType OPCUAAttrTypeToParamType(AttributeId attr, UaNode node) {
1576
    ParameterType pType = null;
1✔
1577

1578
    switch (attr) {
1✔
1579
      case AccessLevel:
1580
        pType = getBasicType(mdb, Type.STRING);
1✔
1581
        break;
1✔
1582
      case ArrayDimensions:
1583
        pType = getBasicType(mdb, Type.STRING);
1✔
1584
        break;
1✔
1585
      case BrowseName:
1586
        pType = getBasicType(mdb, Type.STRING);
1✔
1587
        break;
1✔
1588
      case ContainsNoLoops:
1589
        pType = getBasicType(mdb, Type.STRING);
1✔
1590
        break;
1✔
1591
      case DataType:
1592
        pType = getBasicType(mdb, Type.STRING);
1✔
1593
        break;
1✔
1594
      case Description:
1595
        pType = getBasicType(mdb, Type.STRING);
1✔
1596
        break;
1✔
1597
      case DisplayName:
1598
        pType = getBasicType(mdb, Type.STRING);
1✔
1599
        break;
1✔
1600
      case EventNotifier:
1601
        pType = getBasicType(mdb, Type.STRING);
1✔
1602
        break;
1✔
1603
      case Executable:
1604
        pType = getBasicType(mdb, Type.STRING);
1✔
1605
        break;
1✔
1606
      case Historizing:
1607
        pType = getBasicType(mdb, Type.STRING);
1✔
1608
        break;
1✔
1609
      case InverseName:
1610
        pType = getBasicType(mdb, Type.STRING);
1✔
1611
        break;
1✔
1612
      case IsAbstract:
1613
        pType = getBasicType(mdb, Type.STRING);
1✔
1614
        break;
1✔
1615
      case MinimumSamplingInterval:
1616
        pType = getBasicType(mdb, Type.STRING);
1✔
1617
        break;
1✔
1618
      case NodeClass:
1619
        pType = getBasicType(mdb, Type.STRING);
1✔
1620
        break;
1✔
1621
      case NodeId:
1622
        pType = getBasicType(mdb, Type.STRING);
1✔
1623
        break;
1✔
1624
      case Symmetric:
1625
        pType = getBasicType(mdb, Type.STRING);
1✔
1626
        break;
1✔
1627
      case UserAccessLevel:
1628
        pType = getBasicType(mdb, Type.STRING);
1✔
1629
        break;
1✔
1630
      case UserExecutable:
1631
        pType = getBasicType(mdb, Type.STRING);
1✔
1632
        break;
1✔
1633
      case UserWriteMask:
1634
        pType = getBasicType(mdb, Type.STRING);
1✔
1635
        break;
1✔
1636
      case Value:
1637
        try {
1638

1639
          var value = node.readAttribute(attr).getValue();
1✔
1640

1641
          if (value.isNotNull()) {
1✔
1642

1643
            Object valueObject = value.getValue();
1✔
1644

1645
            if (valueObject instanceof Short) {
1✔
1646
              pType = getBasicType(mdb, Type.SINT32);
1✔
1647
            } else if (valueObject instanceof Integer) {
1✔
1648
              pType = getBasicType(mdb, Type.SINT32);
1✔
1649

1650
            } else if (valueObject instanceof Long) {
1✔
1651
              pType = getBasicType(mdb, Type.SINT64);
1✔
1652
            } else if (valueObject instanceof Double) {
1✔
1653
              pType = getBasicType(mdb, Type.DOUBLE);
1✔
1654
            } else if (valueObject instanceof Float) {
1✔
1655
              pType = getBasicType(mdb, Type.FLOAT);
1✔
1656
            } else if (valueObject instanceof Character) {
1✔
1657
              pType = getBasicType(mdb, Type.STRING);
×
1658
            } else if (valueObject instanceof String) {
1✔
1659
              pType = getBasicType(mdb, Type.STRING);
1✔
1660

1661
            } else if (valueObject instanceof Boolean) {
1✔
1662
              pType = getBasicType(mdb, Type.BOOLEAN);
1✔
1663
            } else {
1664
              pType = getBasicType(mdb, Type.STRING);
1✔
1665
            }
1666
          } else {
1✔
1667
            pType = getBasicType(mdb, Type.STRING);
1✔
1668
          }
1669

1670
        } catch (UaException e) {
×
1671
          // TODO Auto-generated catch block
1672
          e.printStackTrace();
×
1673
          //                        FIXME:Add log message
1674
        }
1✔
1675
        break;
×
1676
      case ValueRank:
1677
        pType = getBasicType(mdb, Type.STRING);
1✔
1678
        break;
1✔
1679
      case WriteMask:
1680
        pType = getBasicType(mdb, Type.STRING);
1✔
1681
        break;
1✔
1682
      default:
1683
        break;
1684
    }
1685

1686
    return pType;
1✔
1687
  }
1688

1689
  private void subscribeToEvents(OpcUaClient client)
1690
      throws InterruptedException, ExecutionException {
1691
    // create a subscription and a monitored item
1692
    UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
1✔
1693

1694
    ReadValueId readValueId =
1✔
1695
        new ReadValueId(
1696
            Identifiers.Server, AttributeId.EventNotifier.uid(), null, QualifiedName.NULL_VALUE);
1✔
1697

1698
    // client handle must be unique per item
1699
    UInteger clientHandle = uint(clientHandles.getAndIncrement());
1✔
1700

1701
    EventFilter eventFilter =
1✔
1702
        new EventFilter(
1703
            new SimpleAttributeOperand[] {
1704
              new SimpleAttributeOperand(
1705
                  Identifiers.BaseEventType,
1706
                  new QualifiedName[] {new QualifiedName(0, "EventId")},
1707
                  AttributeId.Value.uid(),
1✔
1708
                  null),
1709
              new SimpleAttributeOperand(
1710
                  Identifiers.BaseEventType,
1711
                  new QualifiedName[] {new QualifiedName(0, "EventType")},
1712
                  AttributeId.Value.uid(),
1✔
1713
                  null),
1714
              new SimpleAttributeOperand(
1715
                  Identifiers.BaseEventType,
1716
                  new QualifiedName[] {new QualifiedName(0, "Severity")},
1717
                  AttributeId.Value.uid(),
1✔
1718
                  null),
1719
              new SimpleAttributeOperand(
1720
                  Identifiers.BaseEventType,
1721
                  new QualifiedName[] {new QualifiedName(0, "Time")},
1722
                  AttributeId.Value.uid(),
1✔
1723
                  null),
1724
              new SimpleAttributeOperand(
1725
                  Identifiers.BaseEventType,
1726
                  new QualifiedName[] {new QualifiedName(0, "Message")},
1727
                  AttributeId.Value.uid(),
1✔
1728
                  null)
1729
            },
1730
            new ContentFilter(null));
1731

1732
    MonitoringParameters parameters =
1✔
1733
        new MonitoringParameters(
1734
            clientHandle,
1735
            0.0,
1✔
1736
            ExtensionObject.encode(client.getStaticSerializationContext(), eventFilter),
1✔
1737
            uint(10),
1✔
1738
            true);
1✔
1739

1740
    MonitoredItemCreateRequest request =
1✔
1741
        new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
1742

1743
    List<UaMonitoredItem> items =
1✔
1744
        subscription.createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get();
1✔
1745

1746
    // do something with the value updates
1747
    UaMonitoredItem monitoredItem = items.get(0);
1✔
1748

1749
    monitoredItem.setEventConsumer(
1✔
1750
        (item, vs) -> {
1751
          internalLogger.info("Event Received from {}", item.getReadValueId().getNodeId());
1✔
1752

1753
          StringBuilder eventText = new StringBuilder();
1✔
1754

1755
          ByteString eventId;
1756
          NodeId eventType;
1757
          UShort eventSeverity;
1758
          DateTime eventTime;
1759
          LocalizedText eventMessage;
1760

1761
          for (int i = 0; i < vs.length; i++) {
1✔
1762
            internalLogger.info("\tvariant[{}]: {}", i, vs[i].getValue());
1✔
1763
          }
1764

1765
          eventId = (ByteString) vs[0].getValue();
1✔
1766
          eventType = (NodeId) vs[1].getValue();
1✔
1767
          eventSeverity = (UShort) vs[2].getValue();
1✔
1768
          eventTime = (DateTime) vs[3].getValue();
1✔
1769
          eventMessage = (LocalizedText) vs[4].getValue();
1✔
1770

1771
          //          FIXME:Map these values to YAMCS API
1772
          eventText.append("eventId:" + eventId);
1✔
1773
          eventText.append("\n");
1✔
1774
          eventText.append("eventType:" + eventType);
1✔
1775
          eventText.append("\n");
1✔
1776
          eventText.append("eventSeverity:" + eventSeverity);
1✔
1777
          eventText.append("\n");
1✔
1778
          eventText.append("eventTime:" + eventTime);
1✔
1779
          eventText.append("\n");
1✔
1780
          eventText.append("eventMessage:" + eventMessage);
1✔
1781
          org.yamcs.yarch.protobuf.Db.Event ev =
1782
              Event.newBuilder()
1✔
1783
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1784
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1785
                  .setSource(this.linkName)
1✔
1786
                  .setType(this.linkName)
1✔
1787
                  .setMessage(eventText.toString())
1✔
1788
                  .setSeverity(EventSeverity.INFO)
1✔
1789
                  .build();
1✔
1790
          eventProducer.sendEvent(ev);
1✔
1791
        });
1✔
1792
  }
1✔
1793

1794
  @Override
1795
  public void setupSystemParameters(SystemParametersService sysParamService) {
1796
    super.setupSystemParameters(sysParamService);
1✔
1797
    OPCUAStatusParam =
1✔
1798
        sysParamService.createEnumeratedSystemParameter(
1✔
1799
            linkName + "/OPCUAStatusParam",
1800
            OPCUAStatus.class,
1801
            "The current status of OPCUA client");
1802
    EnumeratedParameterType spLinkStatusType =
1✔
1803
        (EnumeratedParameterType) OPCUAStatusParam.getParameterType();
1✔
1804
    spLinkStatusType
1✔
1805
        .enumValue(OPCUAStatus.OPCUA_INIT_CONFIG.name())
1✔
1806
        .setDescription(
1✔
1807
            "This link is in the configuration stage(Configuring OPCUA parameters such as certificates)");
1808
    spLinkStatusType
1✔
1809
        .enumValue(OPCUAStatus.OPCUA_INIT_TREE.name())
1✔
1810
        .setDescription(
1✔
1811
            "The link is parsing the OPCUA Tree and mapping them to PVs."
1812
                + " Depending on configuration, this can take a while.");
1813
    spLinkStatusType
1✔
1814
        .enumValue(OPCUAStatus.OPCUA_INIT_EVENTS.name())
1✔
1815
        .setDescription("The link is configuring and subscribing to OPCUA events");
1✔
1816
    spLinkStatusType
1✔
1817
        .enumValue(OPCUAStatus.OPCUA_INIT_DATA_SUBSCRIPTION.name())
1✔
1818
        .setDescription(
1✔
1819
            "The link is creating subscriptions for each node that was parsed from the tree"
1820
                + "that has a Value attribute.");
1821
    spLinkStatusType
1✔
1822
        .enumValue(OPCUAStatus.OPCUA_INIT_ALL_DATA_QUERY.name())
1✔
1823
        .setDescription(
1✔
1824
            "The link is querying all attributes of all parsed nodes."
1825
                + "This is can be configured to be done at startup.");
1826
    spLinkStatusType
1✔
1827
        .enumValue(OPCUAStatus.OPCUA_OK.name())
1✔
1828
        .setDescription(
1✔
1829
            "The link is done with all OPCUA initialization. It is in an usable state.");
1830

1831
    OPCUAActiveSubsParam =
1✔
1832
        sysParamService.createSystemParameter(
1✔
1833
            linkName + "/OPCUAActiveSubs",
1834
            Type.UINT64,
1835
            "The total number of active opcua subscriptions");
1836
  }
1✔
1837

1838
  @Override
1839
  public List<ParameterValue> getSystemParameters() {
1840
    long time = getCurrentTime();
1✔
1841

1842
    ArrayList<ParameterValue> list = new ArrayList<>();
1✔
1843

1844
    list.add(
1✔
1845
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1846
            OPCUAStatusParam, time, currentOPCUAStatus));
1847

1848
    list.add(
1✔
1849
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1850
            OPCUAActiveSubsParam, time, OPCUAActiveSubs.get()));
1✔
1851
    try {
1852
      super.collectSystemParameters(time, list);
1✔
1853
    } catch (Exception e) {
×
1854
      log.error("Exception caught when collecting link system parameters", e);
×
1855
    }
1✔
1856
    return list;
1✔
1857
  }
1858
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc