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

WindhoverLabs / yamcs-opcua / #37

05 Aug 2024 04:23PM UTC coverage: 80.585% (-8.0%) from 88.606%
#37

push

lorenzo-gomez-windhover
-Implement AbstractTmDataLink to allow for replays. WIP.

8 of 10 new or added lines in 1 file covered. (80.0%)

59 existing lines in 1 file now uncovered.

606 of 752 relevant lines covered (80.59%)

0.81 hits per line

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

80.22
/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.yamcs.xtce.NameDescription.qualifiedName;
39

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

140
/**
141
 * Implementation of the OPCUA protocol as a YAMCS link. Maps configured nodes(see docs for details)
142
 * to yamcs PVs and subscribes to OPCUA variables for realtime updates.
143
 *
144
 * @author Lorenzo Gomez
145
 */
146
public class OPCUALink extends AbstractTmDataLink implements Runnable, SystemParametersProducer {
1✔
147

148
  class NodeIDAttrPair {
149
    NodeId nodeID;
150
    AttributeId attrID;
151

152
    public NodeIDAttrPair(NodeId newNodeID, AttributeId newAttrID) {
1✔
153
      this.nodeID = newNodeID;
1✔
154
      this.attrID = newAttrID;
1✔
155
    }
1✔
156

157
    public int hashCode() {
158
      return Objects.hash(this.nodeID, this.attrID);
1✔
159
    }
160

161
    public boolean equals(Object obj) {
162
      return (this.hashCode() == obj.hashCode());
1✔
163
    }
164
  }
165

166
  class NodePath {
1✔
167
    String path;
168
    HashMap<Object, Object> rootNodeID = new HashMap<Object, Object>();
1✔
169
  }
170

171
  /** Useful status for tracking initialization status of the link. */
172
  public enum OPCUAINITStatus {
1✔
173
    OPCUA_INIT_CONFIG,
1✔
174
    OPCUA_INIT_TREE,
1✔
175
    OPCUA_INIT_TREE_FAILED,
1✔
176
    OPCUA_INIT_GENERATE_XTCE,
1✔
177
    OPCUA_INIT_EVENTS,
1✔
178
    OPCUA_INIT_DATA_SUBSCRIPTION,
1✔
179
    OPCUA_INIT_ALL_DATA_QUERY,
1✔
180
    OPCUA_INIT_OK
1✔
181
  }
182

183
  /* Configuration Defaults */
184
  static final String STREAM_NAME = "opcua_params";
185

186
  /* Internal member attributes. */
187
  protected Thread thread;
188
  private String opcuaStreamName;
189
  private String parametersNamespace;
190
  XtceDb mdb;
191
  Stream opcuaStream;
192
  private static TupleDefinition gftdef = StandardTupleDefinitions.PARAMETER.copy();
1✔
193
  private ManagedSubscription opcuaSubscription;
194

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

197
  /**
198
   * @note ALWAYS re-use params as org.yamcs.parameter.ParameterRequestManager.param2RequestMap uses
199
   *     the object inside a map that was added to the mdb for the very fist time. If when
200
   *     publishing the PV, we create a new VariableParam object clients will NOT receive real-time
201
   *     updates as the new object VariableParam inside the new PV won't match the one inside
202
   *     org.yamcs.parameter.ParameterRequestManager.param2RequestMap since the object hashes do not
203
   *     match (since VariableParam does not override its hash function).
204
   */
205
  private ConcurrentHashMap<NodeIDAttrPair, VariableParam> nodeIDToParamsMap =
1✔
206
      new ConcurrentHashMap<NodeIDAttrPair, VariableParam>();
207

208
  private OpcUaClient client;
209

210
  protected AtomicLong inCount = new AtomicLong(0);
1✔
211

212
  private Status linkStatus = Status.OK;
1✔
213

214
  /* Configuration Parameters */
215

216
  private String discoverURL;
217
  private String endpointURL;
218
  private boolean queryAllNodesAtStartup;
219
  private String outputFile;
220
  private int publishInterval; // milliseconds
221

222
  private ArrayList<NodePath> relativeNodePaths = new ArrayList<NodePath>();
1✔
223

224
  private final AtomicLong clientHandles = new AtomicLong(1L);
1✔
225

226
  /* System parameters*/
227

228
  private Parameter OPCUAInitStatusParam;
229
  private OPCUAINITStatus currentOPCUAStatus;
230
  private Parameter OPCUAActiveSubsParam;
231
  private AtomicLong OPCUAActiveSubs = new AtomicLong(0);
1✔
232

233
  LinkAction startAction =
1✔
234
      new LinkAction("query_all", "Query All OPCUA Server Data") {
1✔
235
        @Override
236
        public JsonObject execute(Link link, JsonObject jsonObject) {
237

238
          internalLogger.info("Executing query_all action");
1✔
239
          CompletableFuture.supplyAsync(
1✔
240
                  (Supplier<Integer>)
241
                      () -> {
UNCOV
242
                        queryAllOPCUAData();
×
243

UNCOV
244
                        return 0;
×
245
                      })
246
              .whenComplete(
1✔
247
                  (vaue, e) -> {
248
                    internalLogger.info("query_all action Complete");
1✔
249
                  });
1✔
250

251
          return jsonObject;
1✔
252
        }
253
      };
254

255
  public OPCUAINITStatus getCurrentOPCUAStatus() {
256
    return currentOPCUAStatus;
1✔
257
  }
258

259
  @Override
260
  public Spec getSpec() {
261
    //    var spec =new Spec();
262
    //
263
    //        spec.addOption("name", OptionType.STRING).withRequired(true);
264
    //        spec.addOption("class", OptionType.STRING).withRequired(true);
265

266
    var spec = super.getDefaultSpec();
1✔
267

268
    //        spec.addOption("name", OptionType.STRING).withRequired(true);
269
    //        spec.addOption("class", OptionType.STRING).withRequired(true);
270

271
    /* Define our configuration parameters. */
272
    spec.addOption("opcuaStream", OptionType.STRING).withRequired(true);
1✔
273
    spec.addOption("endpointUrl", OptionType.STRING).withRequired(true);
1✔
274
    spec.addOption("discoveryUrl", OptionType.STRING).withRequired(true);
1✔
275
    spec.addOption("xtceOutputFile", OptionType.STRING).withRequired(true);
1✔
276
    spec.addOption("parametersNamespace", OptionType.STRING).withRequired(true);
1✔
277
    spec.addOption("publishInterval", OptionType.INTEGER).withRequired(true);
1✔
278
    spec.addOption("queryAllNodesAtStartup", OptionType.BOOLEAN).withRequired(false);
1✔
279

280
    Spec rootNodeIDSpec = new Spec();
1✔
281

282
    rootNodeIDSpec.addOption("namespaceIndex", OptionType.INTEGER).withRequired(true);
1✔
283
    rootNodeIDSpec.addOption("identifier", OptionType.STRING).withRequired(true);
1✔
284
    rootNodeIDSpec.addOption("identifierType", OptionType.STRING).withRequired(true);
1✔
285

286
    Spec nodePathSpec = new Spec();
1✔
287
    nodePathSpec.addOption("path", OptionType.STRING);
1✔
288
    nodePathSpec
1✔
289
        .addOption("rootNodeID", OptionType.MAP)
1✔
290
        .withRequired(true)
1✔
291
        .withSpec(rootNodeIDSpec);
1✔
292

293
    spec.addOption("nodePaths", OptionType.LIST)
1✔
294
        .withElementType(OptionType.MAP)
1✔
295
        .withRequired(true)
1✔
296
        .withSpec(nodePathSpec);
1✔
297

298
    return spec;
1✔
299
  }
300

301
  @Override
302
  public void init(String yamcsInstance, String serviceName, YConfiguration config)
303
      throws ConfigurationException {
304
    super.init(yamcsInstance, serviceName, config);
1✔
305

306
    /* Local variables */
307
    this.config = config;
1✔
308
    /* Validate the configuration that the user passed us. */
309
    try {
310
      config = getSpec().validate(config);
1✔
311
    } catch (ValidationException e) {
×
312
      log.error("Failed configuration validation.", e);
×
313
      notifyFailed(e);
×
314
    }
1✔
315
    YarchDatabaseInstance ydb = YarchDatabase.getInstance(yamcsInstance);
1✔
316

317
    this.opcuaStreamName = config.getString("opcuaStream");
1✔
318
    this.opcuaStream = getStream(ydb, opcuaStreamName);
1✔
319
    this.parametersNamespace = config.getString("parametersNamespace");
1✔
320
    this.mdb = YamcsServer.getServer().getInstance(yamcsInstance).getXtceDb();
1✔
321

322
    readOPCUAConfig(config);
1✔
323
    readNodePathsConfig(config);
1✔
324

325
    outputFile = config.getString("xtceOutputFile");
1✔
326
  }
1✔
327

328
  private void readOPCUAConfig(YConfiguration config) {
329
    this.endpointURL = config.getString("endpointUrl");
1✔
330
    this.discoverURL = config.getString("discoveryUrl");
1✔
331
    this.publishInterval = config.getInt("publishInterval");
1✔
332
    this.queryAllNodesAtStartup = config.getBoolean("queryAllNodesAtStartup", false);
1✔
333
  }
1✔
334

335
  private void readNodePathsConfig(YConfiguration config) {
336
    List<Map<Object, Object>> nodePaths = config.getList("nodePaths");
1✔
337

338
    for (Map<Object, Object> path : nodePaths) {
1✔
339
      NodePath nodePath = new NodePath();
1✔
340
      nodePath.path = (String) path.get("path");
1✔
341
      nodePath.rootNodeID = (HashMap<Object, Object>) path.get("rootNodeID");
1✔
342
      relativeNodePaths.add(nodePath);
1✔
343
    }
1✔
344
  }
1✔
345

346
  private static SpaceSystem verifySpaceSystem(XtceDb mdb, String pathName) {
347
    String namespace;
348
    String name;
349
    int lastSlash = pathName.lastIndexOf('/');
1✔
350
    if ("/".equals(pathName)) {
1✔
351
      namespace = "";
×
352
      name = "";
×
353
    } else if (lastSlash == -1 || lastSlash == pathName.length() - 1) {
1✔
354
      namespace = "";
×
355
      name = pathName;
×
356
    } else {
357
      namespace = pathName.substring(0, lastSlash);
1✔
358
      name = pathName.substring(lastSlash + 1);
1✔
359
    }
360

361
    // First try with a prefixed slash (should be the common case)
362
    NamedObjectId id =
363
        NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build();
1✔
364
    SpaceSystem spaceSystem = mdb.getSpaceSystem(id);
1✔
365
    if (spaceSystem != null) {
1✔
366
      return spaceSystem;
×
367
    }
368

369
    // Maybe some non-xtce namespace like MDB:OPS Name
370
    id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build();
1✔
371
    spaceSystem = mdb.getSpaceSystem(id);
1✔
372
    if (spaceSystem != null) {
1✔
373
      return spaceSystem;
1✔
374
    }
375

376
    throw new NotFoundException("No such space system");
×
377
  }
378

379
  /**
380
   * Initializes all PV mappings to OPCUA nodes and realtime subscriptions(managed data items in
381
   * OPCUA terms).
382
   */
383
  private void opcuaInit() {
384
    try {
385

386
      currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_TREE;
1✔
387
      browseOPCUATree(client);
1✔
388
      currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_GENERATE_XTCE;
1✔
389
      exportXTCE();
1✔
390
      currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_EVENTS;
1✔
391
      subscribeToEvents(client);
1✔
392

393
    } catch (Exception e) {
1✔
394
      internalLogger.warn(e.toString());
1✔
395
      currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_TREE_FAILED;
1✔
396
      return;
1✔
397
    }
1✔
398
    try {
399
      currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_DATA_SUBSCRIPTION;
1✔
400
      createOPCUASubscriptions();
1✔
401
    } catch (Exception e) {
×
402
      internalLogger.warn(e.toString());
×
403
      return;
×
404
    }
1✔
405
    if (queryAllNodesAtStartup) {
1✔
406
      currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_ALL_DATA_QUERY;
×
407
      queryAllOPCUAData();
×
408
    }
409

410
    currentOPCUAStatus = OPCUAINITStatus.OPCUA_INIT_OK;
1✔
411
  }
1✔
412

413
  private void exportXTCE() throws IOException {
414
    var spaceSystem = verifySpaceSystem(mdb, parametersNamespace);
1✔
415
    var xtce = new XtceAssembler().toXtce(mdb, spaceSystem.getQualifiedName(), fqn -> true);
1✔
416
    BufferedWriter writer = null;
1✔
417

418
    if (outputFile != null) {
1✔
419
      writer =
1✔
420
          Files.newBufferedWriter(
1✔
421
              Paths.get(outputFile),
1✔
422
              StandardOpenOption.CREATE,
423
              StandardOpenOption.TRUNCATE_EXISTING);
424

425
      writer.write(xtce);
1✔
426

427
      writer.flush();
1✔
428
      writer.close();
1✔
429
    }
430
  }
1✔
431

432
  private void opcuaClientConnect() throws Exception {
433
    client = configureClient();
1✔
434
    connectToOPCUAServer(client);
1✔
435
  }
1✔
436

437
  private static Stream getStream(YarchDatabaseInstance ydb, String streamName) {
438
    Stream stream = ydb.getStream(streamName);
1✔
439
    if (stream == null) {
1✔
440
      try {
441
        ydb.execute("create stream " + streamName + gftdef.getStringDefinition());
1✔
442
      } catch (Exception e) {
×
443
        throw new ConfigurationException(e);
×
444
      }
1✔
445

446
      stream = ydb.getStream(streamName);
1✔
447
    }
448
    return stream;
1✔
449
  }
450

451
  @Override
452
  public void doDisable() {
453
    /* If the thread is created, interrupt it. */
454
    if (thread != null) {
1✔
455
      thread.interrupt();
1✔
456
    }
457

458
    linkStatus = Status.DISABLED;
1✔
459
  }
1✔
460

461
  @Override
462
  public void doEnable() {
463
    linkStatus = Status.OK;
1✔
464
  }
1✔
465

466
  @Override
467
  public String getDetailedStatus() {
468
    if (isDisabled()) {
1✔
469
      return String.format("DISABLED");
1✔
470
    } else {
471
      return String.format("OK, received %d packets", inCount.get());
1✔
472
    }
473
  }
474

475
  @Override
476
  public Status connectionStatus() {
477
    return linkStatus;
1✔
478
  }
479

480
  @Override
481
  protected void doStart() {
482
    try {
483
      opcuaClientConnect();
1✔
484
    } catch (Exception e) {
×
485
      internalLogger.warn(e.toString());
×
486
      linkStatus = Status.FAILED;
×
487
      notifyFailed(e);
×
488
      return;
×
489
    }
1✔
490
    if (!isDisabled()) {
1✔
491
      doEnable();
1✔
492
    }
493
    startAction.addChangeListener(
1✔
494
        () -> {
495
          /**
496
           * TODO:Might be useful if we want turn off any functionality when the action is disabled
497
           * for instance..
498
           */
499
        });
×
500

501
    /* Create and start the new thread. */
502
    thread = new Thread(this);
1✔
503
    thread.setName(this.getClass().getSimpleName() + "-" + linkName);
1✔
504
    thread.start();
1✔
505

506
    notifyStarted();
1✔
507
  }
1✔
508

509
  @Override
510
  protected void doStop() {
511
    try {
512
      client.disconnect().get();
1✔
513
    } catch (InterruptedException | ExecutionException e) {
×
514
      internalLogger.warn(e.toString());
×
515
    }
1✔
516
    if (thread != null) {
1✔
517
      thread.interrupt();
1✔
518
    }
519

520
    notifyStopped();
1✔
521
  }
1✔
522

523
  @Override
524
  public void run() {
525
    opcuaInit();
1✔
526
    /* Enter our main loop */
527
    while (isRunningAndEnabled()) {}
1✔
528
  }
1✔
529

530
  /**
531
   * Reads all attributes of all configured Value nodes and updates their corresponding PV. Useful
532
   * for querying data from the OPCUA server once, data such as browse names, NodeIds, etc.
533
   */
534
  private void queryAllOPCUAData() {
535
    TupleDefinition tdef = gftdef.copy();
1✔
536
    List<Object> cols = new ArrayList<>(4 + nodeIDToParamsMap.keySet().size());
1✔
537

538
    tdef = gftdef.copy();
1✔
539
    long gentime = timeService.getMissionTime();
1✔
540
    cols.add(gentime);
1✔
541
    cols.add(parametersNamespace);
1✔
542
    cols.add(0);
1✔
543
    cols.add(gentime);
1✔
544

545
    int columnCount = 0;
1✔
546

547
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
548
    /**
549
     * NOTE:This is super inefficient... The reason we collect these nodeIDs in a set is because
550
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
551
     * nodeID given how nodeIDToParamsMap is designed
552
     */
553
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
554
      nodeSet.add(pair.nodeID);
1✔
555
    }
1✔
556

557
    for (NodeId nId : nodeSet) {
1✔
558
      UaNode node;
559

560
      try {
561
        node = client.getAddressSpace().getNode(nId);
1✔
562

563
        DataValue nodeClass = node.readAttribute(AttributeId.NodeClass);
1✔
564

565
        switch (NodeClass.from((int) nodeClass.getValue().getValue())) {
1✔
566
          case Variable:
567
            for (AttributeId attr : AttributeId.VARIABLE_ATTRIBUTES) {
1✔
568
              VariableParam p = nodeIDToParamsMap.get(new NodeIDAttrPair(nId, attr));
1✔
569

570
              if (p.getParameterType() == null) {
1✔
571
                internalLogger.warn(
×
572
                    "{} ignored since it does not have a Parameter type",
573
                    p,
574
                    Character.toString(NameDescription.PATH_SEPARATOR));
×
575
                continue;
×
576
              }
577

578
              switch (p.getParameterType().getValueType()) {
1✔
579
                case BOOLEAN:
580
                  {
UNCOV
581
                    Boolean value = true;
×
UNCOV
582
                    if (node.readAttribute(attr).getValue().isNull()) {
×
583
                      //                      value = "NULL";
584
                    } else {
UNCOV
585
                      value = (Boolean) node.readAttribute(attr).getValue().getValue();
×
586
                    }
587

UNCOV
588
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
UNCOV
589
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
590
                  }
UNCOV
591
                  break;
×
592
                case DOUBLE:
593
                  {
UNCOV
594
                    Number value = 0;
×
UNCOV
595
                    if (node.readAttribute(attr).getValue().isNull()) {
×
596
                      internalLogger.warn("node {} has a Null variant.", node);
×
597
                    } else {
UNCOV
598
                      value = (Number) node.readAttribute(attr).getValue().getValue();
×
599
                    }
600

UNCOV
601
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
UNCOV
602
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value.doubleValue()));
×
603
                  }
UNCOV
604
                  break;
×
605
                case FLOAT:
606
                  {
607
                    Number value = 0;
1✔
608
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
609
                      internalLogger.warn("node {} has a Null variant.", node);
×
610
                    } else {
611
                      value = (Number) node.readAttribute(attr).getValue().getValue();
1✔
612
                    }
613

614
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
615
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value.floatValue()));
1✔
616
                  }
617
                  break;
1✔
618
                case SINT32:
619
                  {
620
                    Number value = 0;
1✔
621
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
622
                      internalLogger.warn("node {} has a Null variant.", node);
×
623
                    } else {
624
                      value = (Number) node.readAttribute(attr).getValue().getValue();
1✔
625
                    }
626

627
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
628
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value.intValue()));
1✔
629

630
                    //                    var pv =
631
                    // (org.yamcs.parameter.ParameterValue)t.getColumn(tdef.getColumn(4).getName());
632
                    //
633
                    //                    byte[] packet =
634
                    // ByteBuffer.allocate(4).putInt(pv.getEngValue().getSint32Value()).array();
635

636
                    //                    var pv =
637
                    // (org.yamcs.parameter.ParameterValue)t.getColumn(tdef.getColumn(4).getName());
638

639
                    byte[] packet = ByteBuffer.allocate(4).putInt(value.intValue()).array();
1✔
640

641
                    TmPacket tmPacket = new TmPacket(timeService.getMissionTime(), packet);
1✔
642
                    tmPacket.setEarthReceptionTime(timeService.getHresMissionTime());
1✔
643

NEW
644
                    processPacket(tmPacket);
×
645
                  }
UNCOV
646
                  break;
×
647
                case SINT64:
648
                  {
UNCOV
649
                    Number value = 0;
×
UNCOV
650
                    if (node.readAttribute(attr).getValue().isNull()) {
×
651
                      internalLogger.warn("node {} has a Null variant.", node);
×
652
                    } else {
UNCOV
653
                      value = (Number) node.readAttribute(attr).getValue().getValue();
×
654
                    }
655

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

669
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
670
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
671
                  }
672
                  break;
1✔
673
                case UINT32:
674
                  {
UNCOV
675
                    Number value = 0;
×
UNCOV
676
                    if (node.readAttribute(attr).getValue().isNull()) {
×
677
                      internalLogger.warn("node {} has a Null variant.", node);
×
678
                    } else {
UNCOV
679
                      value = (Number) node.readAttribute(attr).getValue().getValue();
×
680
                    }
681

UNCOV
682
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
UNCOV
683
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value.longValue()));
×
684
                  }
UNCOV
685
                  break;
×
686
                case UINT64:
687
                  {
688
                    Number value = 0;
1✔
689
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
690
                      internalLogger.warn("node {} has a Null variant.", node);
×
691
                    } else {
692
                      value = (Number) 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.longValue()));
1✔
697
                  }
698
                  break;
1✔
699
                default:
700
                  break;
701
              }
702

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

705
              columnCount++;
1✔
706
            }
1✔
707
            break;
1✔
708
          default:
709
            break;
710
        }
711

712
      } catch (UaException e) {
×
713
        // TODO Auto-generated catch block
714
        internalLogger.warn(e.toString());
×
715
        continue;
×
716
      }
1✔
717
    }
1✔
718

UNCOV
719
    pushTuple(tdef, cols);
×
UNCOV
720
    inCount.getAndAdd(columnCount);
×
UNCOV
721
  }
×
722

723
  private synchronized void pushTuple(TupleDefinition tdef, List<Object> cols) {
724
    Tuple t;
725
    t = new Tuple(tdef, cols);
1✔
726
    opcuaStream.emitTuple(t);
1✔
727
  }
1✔
728

729
  private static ParameterType getOrCreateType(
730
      XtceDb mdb, String name, Supplier<ParameterType.Builder<?>> supplier) {
731

732
    String fqn = XtceDb.YAMCS_SPACESYSTEM_NAME + NameDescription.PATH_SEPARATOR + name;
1✔
733
    ParameterType ptype = mdb.getParameterType(fqn);
1✔
734
    if (ptype != null) {
1✔
735
      return ptype;
1✔
736
    }
737
    ParameterType.Builder<?> typeb = supplier.get().setName(name);
1✔
738

739
    ptype = typeb.build();
1✔
740
    ((NameDescription) ptype).setQualifiedName(fqn);
1✔
741

742
    return mdb.addSystemParameterType(ptype);
1✔
743
  }
744

745
  public static ParameterType getBasicType(XtceDb mdb, Type type) {
746
    ParameterType pType = null;
1✔
747
    switch (type) {
1✔
748
      case BOOLEAN:
749
        return getOrCreateType(mdb, "boolean", () -> new BooleanParameterType.Builder());
1✔
750
      case STRING:
751
        return getOrCreateType(mdb, "string", () -> new StringParameterType.Builder());
1✔
752

753
      case FLOAT:
754
        return getOrCreateType(
1✔
755
            mdb, "float32", () -> new FloatParameterType.Builder().setSizeInBits(32));
1✔
756
      case DOUBLE:
757
        return getOrCreateType(
1✔
758
            mdb, "float64", () -> new FloatParameterType.Builder().setSizeInBits(64));
1✔
759
      case SINT32:
760
        return getOrCreateType(
1✔
761
            mdb,
762
            "sint32",
763
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(true));
×
764
      case SINT64:
765
        return getOrCreateType(
1✔
766
            mdb,
767
            "sint64",
768
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(true));
1✔
769
      case UINT32:
770
        return getOrCreateType(
1✔
771
            mdb,
772
            "uint32",
773
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(false));
×
774
      case UINT64:
775
        return getOrCreateType(
1✔
776
            mdb,
777
            "uint64",
778
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(false));
×
779
      default:
780
        break;
781
    }
782

783
    return pType;
×
784
  }
785

786
  public static ParameterValue getNewPv(Parameter parameter, long time) {
787
    ParameterValue pv = new ParameterValue(parameter);
1✔
788
    pv.setAcquisitionTime(time);
1✔
789
    pv.setGenerationTime(time);
1✔
790
    return pv;
1✔
791
  }
792

793
  public static ParameterValue getPV(Parameter parameter, long time, String v) {
794
    ParameterValue pv = getNewPv(parameter, time);
1✔
795
    pv.setEngValue(ValueUtility.getStringValue(v));
1✔
796
    return pv;
1✔
797
  }
798

799
  public static ParameterValue getPV(Parameter parameter, long time, double v) {
UNCOV
800
    ParameterValue pv = getNewPv(parameter, time);
×
UNCOV
801
    pv.setEngValue(ValueUtility.getDoubleValue(v));
×
UNCOV
802
    return pv;
×
803
  }
804

805
  public static ParameterValue getPV(Parameter parameter, long time, float v) {
806
    ParameterValue pv = getNewPv(parameter, time);
1✔
807
    pv.setEngValue(ValueUtility.getFloatValue(v));
1✔
808
    return pv;
1✔
809
  }
810

811
  public static ParameterValue getPV(Parameter parameter, long time, boolean v) {
UNCOV
812
    ParameterValue pv = getNewPv(parameter, time);
×
UNCOV
813
    pv.setEngValue(ValueUtility.getBooleanValue(v));
×
UNCOV
814
    return pv;
×
815
  }
816

817
  public static ParameterValue getPV(Parameter parameter, long time, long v) {
818
    ParameterValue pv = getNewPv(parameter, time);
1✔
819
    pv.setEngValue(ValueUtility.getSint64Value(v));
1✔
820
    return pv;
1✔
821
  }
822

823
  @Override
824
  public Status getLinkStatus() {
825
    return linkStatus;
1✔
826
  }
827

828
  @Override
829
  public boolean isDisabled() {
830
    return linkStatus == Status.DISABLED;
1✔
831
  }
832

833
  @Override
834
  public long getDataInCount() {
835
    return inCount.get();
1✔
836
  }
837

838
  @Override
839
  public long getDataOutCount() {
840
    return 0;
1✔
841
  }
842

843
  @Override
844
  public void resetCounters() {
845
    inCount.set(0);
1✔
846
  }
1✔
847

848
  /**
849
   * Selects first non-secured endpoint from endpoints found at discover URL. At the moment secured
850
   * endpoints are not supported.
851
   *
852
   * @return
853
   * @throws Exception
854
   */
855
  private OpcUaClient configureClient() throws Exception {
856

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

859
    // At the moment, we do not support certificates.
860
    EndpointDescription selectedEndpoint = null;
1✔
861
    for (var endpoint : endpoints) {
1✔
862
      switch (endpoint.getSecurityMode()) {
1✔
863
        case Invalid:
864
          internalLogger.warn("Endpoint mode {} is not supported.", endpoint.getSecurityMode());
×
865
          break;
×
866
        case None:
867
          selectedEndpoint = endpoint;
1✔
868
          break;
1✔
869
        case Sign:
870
          internalLogger.warn("Endpoint mode {} is not supported.", endpoint.getSecurityMode());
×
871
          break;
×
872
        case SignAndEncrypt:
873
          internalLogger.warn("Endpoint mode {} is not supported.", endpoint.getSecurityMode());
×
874
          break;
875
      }
876

877
      if (selectedEndpoint != null) {
1✔
878
        break;
1✔
879
      }
880
    }
×
881

882
    if (selectedEndpoint == null) {
1✔
883
      throw new Exception("No viable endpoint found from list:" + endpoints);
×
884
    }
885

886
    OpcUaClientConfig builder = OpcUaClientConfig.builder().setEndpoint(selectedEndpoint).build();
1✔
887

888
    return OpcUaClient.create(builder);
1✔
889
  }
890

891
  /**
892
   * Adds new PV with the name of node.
893
   *
894
   * @param client
895
   * @param node
896
   */
897
  private void addOPCUAPV(OpcUaClient client, UaNode node) {
898

899
    if (node.getBrowseName()
1✔
900
        .getName()
1✔
901
        .contains(Character.toString(NameDescription.PATH_SEPARATOR))) {
1✔
902
      internalLogger.info(
×
903
          "{} ignored since it contains a {} character",
904
          node.getBrowseName().getName(),
×
905
          Character.toString(NameDescription.PATH_SEPARATOR));
×
906

907
    } else {
908

909
      /**
910
       * NOTE:For now we'll just flatten all the attributes instead of using an aggregate type for
911
       * attributes
912
       */
913
      for (AttributeId attr : AttributeId.values()) {
1✔
914

915
        ParameterType ptype = OPCUAAttrTypeToParamType(attr, node);
1✔
916

917
        String opcuaTranslatedQName = translateNodeToParamQName(client, node, attr);
1✔
918
        Parameter p = VariableParam.getForFullyQualifiedName(opcuaTranslatedQName);
1✔
919

920
        p.setParameterType(ptype);
1✔
921

922
        if (mdb.getParameter(p.getQualifiedName()) == null) {
1✔
923
          log.debug("Adding OPCUA object as parameter to mdb:{}", p.getQualifiedName());
1✔
924
          mdb.addParameter(p, true);
1✔
925

926
          nodeIDToParamsMap.put(new NodeIDAttrPair(node.getNodeId(), attr), (VariableParam) p);
1✔
927
        }
928
      }
929
    }
930
  }
1✔
931

932
  /**
933
   * Map nodeID name to a qualified name that can be used for a YAMCS PV.
934
   *
935
   * @param client
936
   * @param node
937
   * @param attr
938
   * @return
939
   */
940
  private String translateNodeToParamQName(OpcUaClient client, UaNode node, AttributeId attr) {
941
    LocalizedText localizedDisplayName = null;
1✔
942
    try {
943

944
      localizedDisplayName =
1✔
945
          (LocalizedText) (node.readAttribute(AttributeId.DisplayName).getValue().getValue());
1✔
946
    } catch (UaException e) {
×
947
      internalLogger.warn(e.toString());
×
948
    }
1✔
949
    String opcuaTranslatedQName =
1✔
950
        qualifiedName(
1✔
951
            parametersNamespace
952
                + NameDescription.PATH_SEPARATOR
953
                + node.getNodeId().toParseableString().replace(";", "-")
1✔
954
                + NameDescription.PATH_SEPARATOR
955
                + localizedDisplayName.getText(),
1✔
956
            attr.toString());
1✔
957

958
    return opcuaTranslatedQName;
1✔
959
  }
960

961
  /**
962
   * Browse node at nodePath relative to browseRoot.
963
   *
964
   * @param indent
965
   * @param client
966
   * @param browseRoot
967
   * @param nodePath in the format of "0:Root,0:Objects,2:HelloWorld,2:MyObject,2:Bar"
968
   * @throws Exception
969
   */
970
  private void browsePath(String indent, OpcUaClient client, NodeId startingNode, String nodePath)
971
      throws Exception {
972
    internalLogger.info("Browsing at " + startingNode);
1✔
973
    ArrayList<String> rPathTokens = new ArrayList<String>();
1✔
974
    ArrayList<RelativePathElement> relaitivePathElements = new ArrayList<RelativePathElement>();
1✔
975

976
    for (var pathToken : nodePath.split(",")) {
1✔
977
      rPathTokens.add(nodePath);
1✔
978

979
      int namespaceIndex = 0;
1✔
980

981
      String namespaceName = "";
1✔
982

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

985
      namespaceName = pathToken.split(":")[1];
1✔
986

987
      relaitivePathElements.add(
1✔
988
          new RelativePathElement(
989
              Identifiers.HierarchicalReferences,
990
              false,
1✔
991
              true,
1✔
992
              new QualifiedName(namespaceIndex, namespaceName)));
993
    }
994

995
    ArrayList<BrowsePath> list = new ArrayList<BrowsePath>();
1✔
996

997
    RelativePathElement[] elements = new RelativePathElement[relaitivePathElements.size()];
1✔
998

999
    relaitivePathElements.toArray(elements);
1✔
1000

1001
    list.add(new BrowsePath(startingNode, new RelativePath(elements)));
1✔
1002

1003
    TranslateBrowsePathsToNodeIdsResponse response = null;
1✔
1004
    try {
1005
      response = client.translateBrowsePaths(list).get();
1✔
1006
    } catch (InterruptedException e) {
×
1007
      internalLogger.warn(e.toString());
×
1008
    } catch (ExecutionException e) {
×
1009
      internalLogger.warn(e.toString());
×
1010
    }
1✔
1011

1012
    BrowsePathResult result = Arrays.asList(response.getResults()).get(0);
1✔
1013
    StatusCode statusCode = result.getStatusCode();
1✔
1014

1015
    if (statusCode.isBad()) {
1✔
1016
      log.warn("Bad status code:" + statusCode);
1✔
1017
      org.yamcs.yarch.protobuf.Db.Event ev =
1018
          Event.newBuilder()
1✔
1019
              .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1020
              .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1021
              .setSource(this.linkName)
1✔
1022
              .setType(this.linkName)
1✔
1023
              .setMessage("Failed to find node:" + nodePath + ". Error code info:" + statusCode)
1✔
1024
              .setSeverity(EventSeverity.ERROR)
1✔
1025
              .build();
1✔
1026
      eventProducer.sendEvent(ev);
1✔
1027

1028
      throw new Exception("Bad status code:" + statusCode);
1✔
1029

1030
    } else if (statusCode.isUncertain()) {
1✔
1031
      log.warn("Uncertain status code:" + statusCode);
×
1032
      return;
×
1033
    }
1034

1035
    try {
1036
      UaNode node =
1✔
1037
          client
1038
              .getAddressSpace()
1✔
1039
              .getNode(
1✔
1040
                  result.getTargets()[0].getTargetId().toNodeId(client.getNamespaceTable()).get());
1✔
1041

1042
      addOPCUAPV(client, node);
1✔
1043
    } catch (UaException e) {
×
1044
      internalLogger.warn(e.toString());
×
1045
    }
1✔
1046
  }
1✔
1047

1048
  private void createOPCUASubscriptions() {
1049
    createDataChangeListener();
1✔
1050
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
1051
    /**
1052
     * FIXME:This is super inefficient... The reason we collect these nodeIDs in a set is because
1053
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
1054
     * nodeID given how nodeIDToParamsMap is designed
1055
     */
1056
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
1057
      nodeSet.add(pair.nodeID);
1✔
1058
    }
1✔
1059

1060
    ArrayList<NodeId> variableNodes = new ArrayList<NodeId>();
1✔
1061
    for (NodeId id : nodeSet) {
1✔
1062
      Variant nodeClass = null;
1✔
1063
      try {
1064
        UaNode node = client.getAddressSpace().getNode(id);
1✔
1065

1066
        nodeClass = node.readAttribute(AttributeId.NodeClass).getValue();
1✔
1067

1068
      } catch (UaException e) {
×
1069
        internalLogger.warn(e.toString());
×
1070
      }
1✔
1071
      if (nodeClass != null) {
1✔
1072
        //        try {
1073
        switch (NodeClass.from((int) nodeClass.getValue())) {
1✔
1074
            // As per the spec, the only thing we can subscribe to is Variables
1075
          case Variable:
1076
            variableNodes.add(id);
1✔
1077
            break;
1078
        }
1079
      }
1080
    }
1✔
1081

1082
    try {
1083
      List<ManagedDataItem> dataItems = opcuaSubscription.createDataItems(variableNodes);
1✔
1084
      for (var dataItem : dataItems) {
1✔
1085
        log.debug("Status code for dataItem:{}", dataItem.getStatusCode());
1✔
1086
        OPCUAActiveSubs.addAndGet(1);
1✔
1087
      }
1✔
1088
    } catch (UaException e) {
×
1089
      internalLogger.warn(e.toString());
×
1090
    }
1✔
1091
  }
1✔
1092

1093
  /**
1094
   * Connects to OPCUA server and activates query all action.
1095
   *
1096
   * @param client
1097
   * @throws Exception
1098
   */
1099
  public void connectToOPCUAServer(OpcUaClient client) throws Exception {
1100
    internalLogger.info("Connecting to OPCUA server...");
1✔
1101
    client.connect().get();
1✔
1102

1103
    addAction(startAction);
1✔
1104
    startAction.setEnabled(true);
1✔
1105
  }
1✔
1106

1107
  /**
1108
   * Browses the tree on the OPCUA server and maps them to YAMCS Parameters.
1109
   *
1110
   * @param client
1111
   * @throws Exception
1112
   */
1113
  private void browseOPCUATree(OpcUaClient client) throws Exception {
1114
    // start browsing at root folder
1115
    internalLogger.info("Browsing OPCUA...");
1✔
1116
    for (var p : relativeNodePaths) {
1✔
1117
      int namespaceIndex = (int) p.rootNodeID.get("namespaceIndex");
1✔
1118
      String identifier = (String) p.rootNodeID.get("identifier");
1✔
1119
      IdType identifierType = IdType.valueOf((String) p.rootNodeID.get("identifierType"));
1✔
1120

1121
      browsePath(
1✔
1122
          endpointURL, client, getNewNodeID(identifierType, namespaceIndex, identifier), p.path);
1✔
1123
    }
1✔
1124
  }
1✔
1125

1126
  /**
1127
   * Get new OPCUA-compliant NodeID object that is created from NamespaceIndex and Identifier. At
1128
   * the moment only String and Numeric node ids are supported.
1129
   *
1130
   * @param rootIdentifierType
1131
   * @param NamespaceIndex
1132
   * @param Identifier
1133
   * @return
1134
   */
1135
  private NodeId getNewNodeID(IdType rootIdentifierType, int NamespaceIndex, String Identifier) {
1136
    NodeId nodeID = null;
1✔
1137
    switch (rootIdentifierType) {
1✔
1138
      case Guid:
1139
        internalLogger.warn("Guid nodeID is not supported");
×
1140
        break;
×
1141
      case Numeric:
1142
        nodeID = new NodeId(NamespaceIndex, Integer.parseInt(Identifier));
1✔
1143
        break;
1✔
1144
      case Opaque:
1145
        internalLogger.warn("Guid Opaque is not supported");
×
1146
        break;
×
1147
      case String:
1148
        nodeID = new NodeId(NamespaceIndex, Identifier);
×
1149
        break;
1150
    }
1151
    return nodeID;
1✔
1152
  }
1153

1154
  /** Data listener for realtime OPCUA server updates. */
1155
  private void createDataChangeListener() {
1156
    try {
1157
      opcuaSubscription = ManagedSubscription.create(client, publishInterval);
1✔
1158
    } catch (UaException e) {
×
1159
      internalLogger.warn(e.toString());
×
1160
    }
1✔
1161
    opcuaSubscription.addDataChangeListener(
1✔
1162
        (items, values) -> {
1163
          for (int i = 0; i < items.size(); i++) {
1✔
1164
            NodeIDAttrPair nodeAttrKey =
1✔
1165
                new NodeIDAttrPair(items.get(i).getNodeId(), AttributeId.Value);
1✔
1166
            log.debug(
1✔
1167
                "subscription value received: item={}, value={}",
1168
                items.get(i).getNodeId(),
1✔
1169
                values.get(i).getValue());
1✔
1170

1171
            log.debug(
1✔
1172
                "Pushing new PV for param name {} which is mapped to NodeID {}",
1173
                nodeIDToParamsMap.get(nodeAttrKey),
1✔
1174
                items.get(i).getNodeId());
1✔
1175

1176
            TupleDefinition tdef = gftdef.copy();
1✔
1177
            List<Object> cols = new ArrayList<>(4 + 1);
1✔
1178
            //            FIXME: Add leap seconds.... as config or get it from YAMCS API.
1179
            long gentime =
1✔
1180
                values
1181
                    .get(i)
1✔
1182
                    .getSourceTime()
1✔
1183
                    .getJavaInstant()
1✔
1184
                    .plus(37, ChronoUnit.SECONDS)
1✔
1185
                    .toEpochMilli();
1✔
1186
            cols.add(gentime);
1✔
1187
            cols.add(parametersNamespace);
1✔
1188
            cols.add(0);
1✔
1189
            cols.add(gentime);
1✔
1190

1191
            /**
1192
             * TODO:Not sure if this is the best way to do this since the aggregate values will be
1193
             * partially updated. Another potential approach might be to decouple the live OPCUA
1194
             * data(subscriptions) via namespaces. For example; have a "special" namespace called
1195
             * "subscriptions" that ONLY gets updated with items. And maybe another namespace for
1196
             * static data...maybe.
1197
             *
1198
             * <p>Another option is to flatten everything and have no aggregate types at all. That
1199
             * approach might even simplify the code quite a bit...
1200
             *
1201
             * <p>Another question worth answering before moving forward is to find out whether or
1202
             * not it is concrete in the OPCUA protocol what data can change in real time and which
1203
             * data is "static". Not sure if there is any "static" data given that clients have the
1204
             * ability of writing to values... might be worth a test.
1205
             */
1206
            log.debug(
1✔
1207
                "Data({}) chnage triggered for {}",
1208
                values.get(i).getValue(),
1✔
1209
                nodeIDToParamsMap.get(nodeAttrKey));
1✔
1210

1211
            if (nodeIDToParamsMap.get(nodeAttrKey) == null) {
1✔
1212
              log.debug("No parameter mapping found for {}", nodeAttrKey.nodeID);
×
1213
              continue;
×
1214
            } else {
1215
              log.debug(
1✔
1216
                  String.format(
1✔
1217
                      "parameter mapping found for {} and {}",
1218
                      nodeAttrKey.nodeID,
1219
                      nodeAttrKey.attrID));
1220
            }
1221

1222
            if (values.get(i).getValue() != null && values.get(i).getValue().getValue() != null) {
1✔
1223

1224
              switch (nodeIDToParamsMap.get(nodeAttrKey).getParameterType().getValueType()) {
1✔
1225
                case BOOLEAN:
1226
                  {
UNCOV
1227
                    boolean value = (boolean) values.get(i).getValue().getValue();
×
1228

UNCOV
1229
                    tdef.addColumn(
×
UNCOV
1230
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1231
                        DataType.PARAMETER_VALUE);
UNCOV
1232
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1233
                  }
UNCOV
1234
                  break;
×
1235
                case DOUBLE:
1236
                  {
UNCOV
1237
                    Number value = (Number) values.get(i).getValue().getValue();
×
1238

UNCOV
1239
                    tdef.addColumn(
×
UNCOV
1240
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1241
                        DataType.PARAMETER_VALUE);
UNCOV
1242
                    cols.add(
×
UNCOV
1243
                        getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value.doubleValue()));
×
1244
                  }
UNCOV
1245
                  break;
×
1246
                case FLOAT:
1247
                  {
1248
                    Number value = (Number) values.get(i).getValue().getValue();
1✔
1249

1250
                    tdef.addColumn(
1✔
1251
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1252
                        DataType.PARAMETER_VALUE);
1253
                    cols.add(
1✔
1254
                        getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value.floatValue()));
1✔
1255
                  }
1256
                  break;
1✔
1257
                case SINT32:
1258
                  {
1259
                    Number value = (Number) values.get(i).getValue().getValue();
1✔
1260
                    tdef.addColumn(
1✔
1261
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1262
                        DataType.PARAMETER_VALUE);
1263
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value.intValue()));
1✔
1264

1265
                    byte[] packet = ByteBuffer.allocate(4).putInt(value.intValue()).array();
1✔
1266

1267
                    TmPacket tmPacket = new TmPacket(timeService.getMissionTime(), packet);
1✔
1268
                    tmPacket.setEarthReceptionTime(timeService.getHresMissionTime());
1✔
1269

NEW
1270
                    processPacket(tmPacket);
×
1271
                  }
UNCOV
1272
                  break;
×
1273
                case SINT64:
1274
                  {
UNCOV
1275
                    Number value = (Number) values.get(i).getValue().getValue();
×
1276

UNCOV
1277
                    tdef.addColumn(
×
UNCOV
1278
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1279
                        DataType.PARAMETER_VALUE);
UNCOV
1280
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value.longValue()));
×
1281
                  }
UNCOV
1282
                  break;
×
1283
                case STRING:
1284
                  {
1285
                    String value = (String) values.get(i).getValue().getValue().toString();
1✔
1286

1287
                    tdef.addColumn(
1✔
1288
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1289
                        DataType.PARAMETER_VALUE);
1290
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1291
                  }
1292
                  break;
1✔
1293
                case UINT32:
1294
                  {
UNCOV
1295
                    Number value = (Number) values.get(i).getValue().getValue();
×
UNCOV
1296
                    tdef.addColumn(
×
UNCOV
1297
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1298
                        DataType.PARAMETER_VALUE);
UNCOV
1299
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value.longValue()));
×
1300
                  }
UNCOV
1301
                  break;
×
1302
                case UINT64:
1303
                  {
1304
                    Number value = (Number) values.get(i).getValue().getValue();
1✔
1305

1306
                    tdef.addColumn(
1✔
1307
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1308
                        DataType.PARAMETER_VALUE);
1309
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value.longValue()));
1✔
1310
                  }
1311
                  break;
1✔
1312
                default:
1313
                  break;
1314
              }
1315

1316
              pushTuple(tdef, cols);
1✔
1317

1318
              inCount.getAndAdd(1);
1✔
1319
            } else {
1320
              // TODO:Add some type emptyValue count for OPS.
1321
              log.warn(
×
1322
                  "Data chnage triggered for {}, but it empty. This should not happen.",
1323
                  nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName());
×
1324
            }
1325
          }
UNCOV
1326
        });
×
1327
  }
1✔
1328

1329
  /**
1330
   * Get new ParameterType for the specified attribute of the node. Particularly useful for Value
1331
   * attributes of nodes.
1332
   *
1333
   * @param attr
1334
   * @param node
1335
   * @return
1336
   */
1337
  private ParameterType OPCUAAttrTypeToParamType(AttributeId attr, UaNode node) {
1338
    ParameterType pType = null;
1✔
1339

1340
    switch (attr) {
1✔
1341
      case AccessLevel:
1342
        pType = getBasicType(mdb, Type.STRING);
1✔
1343
        break;
1✔
1344
      case ArrayDimensions:
1345
        pType = getBasicType(mdb, Type.STRING);
1✔
1346
        break;
1✔
1347
      case BrowseName:
1348
        pType = getBasicType(mdb, Type.STRING);
1✔
1349
        break;
1✔
1350
      case ContainsNoLoops:
1351
        pType = getBasicType(mdb, Type.STRING);
1✔
1352
        break;
1✔
1353
      case DataType:
1354
        pType = getBasicType(mdb, Type.STRING);
1✔
1355
        break;
1✔
1356
      case Description:
1357
        pType = getBasicType(mdb, Type.STRING);
1✔
1358
        break;
1✔
1359
      case DisplayName:
1360
        pType = getBasicType(mdb, Type.STRING);
1✔
1361
        break;
1✔
1362
      case EventNotifier:
1363
        pType = getBasicType(mdb, Type.STRING);
1✔
1364
        break;
1✔
1365
      case Executable:
1366
        pType = getBasicType(mdb, Type.STRING);
1✔
1367
        break;
1✔
1368
      case Historizing:
1369
        pType = getBasicType(mdb, Type.STRING);
1✔
1370
        break;
1✔
1371
      case InverseName:
1372
        pType = getBasicType(mdb, Type.STRING);
1✔
1373
        break;
1✔
1374
      case IsAbstract:
1375
        pType = getBasicType(mdb, Type.STRING);
1✔
1376
        break;
1✔
1377
      case MinimumSamplingInterval:
1378
        pType = getBasicType(mdb, Type.STRING);
1✔
1379
        break;
1✔
1380
      case NodeClass:
1381
        pType = getBasicType(mdb, Type.STRING);
1✔
1382
        break;
1✔
1383
      case NodeId:
1384
        pType = getBasicType(mdb, Type.STRING);
1✔
1385
        break;
1✔
1386
      case Symmetric:
1387
        pType = getBasicType(mdb, Type.STRING);
1✔
1388
        break;
1✔
1389
      case UserAccessLevel:
1390
        pType = getBasicType(mdb, Type.STRING);
1✔
1391
        break;
1✔
1392
      case UserExecutable:
1393
        pType = getBasicType(mdb, Type.STRING);
1✔
1394
        break;
1✔
1395
      case UserWriteMask:
1396
        pType = getBasicType(mdb, Type.STRING);
1✔
1397
        break;
1✔
1398
      case Value:
1399
        try {
1400

1401
          var value = node.readAttribute(attr).getValue();
1✔
1402

1403
          if (value.isNotNull()) {
1✔
1404
            NodeId valueObjectType =
1✔
1405
                value.getDataType().get().toNodeId(client.getNamespaceTable()).get();
1✔
1406

1407
            /** As per the spec:https://reference.opcfoundation.org/Core/Part6/v104/docs/5.1.2 */
1408
            if (valueObjectType.equals(Identifiers.SByte)) {
1✔
1409
              pType = getBasicType(mdb, Type.SINT32);
×
1410
            } else if (valueObjectType.equals(Identifiers.Byte)) {
1✔
1411
              pType = getBasicType(mdb, Type.SINT32);
×
1412
            } else if (valueObjectType.equals(Identifiers.Int16)) {
1✔
1413
              pType = getBasicType(mdb, Type.SINT32);
×
1414
            } else if (valueObjectType.equals(Identifiers.UInt16)) {
1✔
1415
              pType = getBasicType(mdb, Type.SINT32);
×
1416
            } else if (valueObjectType.equals(Identifiers.Int32)) {
1✔
1417
              pType = getBasicType(mdb, Type.SINT32);
1✔
1418
            } else if (valueObjectType.equals(Identifiers.UInt32)) {
1✔
1419
              pType = getBasicType(mdb, Type.UINT32);
1✔
1420
            } else if (valueObjectType.equals(Identifiers.Int64)) {
1✔
1421
              pType = getBasicType(mdb, Type.SINT64);
1✔
1422
            } else if (valueObjectType.equals(Identifiers.UInt64)) {
1✔
1423
              pType = getBasicType(mdb, Type.UINT64);
1✔
1424
            } else if (valueObjectType.equals(Identifiers.Float)) {
1✔
1425
              pType = getBasicType(mdb, Type.FLOAT);
1✔
1426
            } else if (valueObjectType.equals(Identifiers.Double)) {
1✔
1427
              pType = getBasicType(mdb, Type.DOUBLE);
1✔
1428
            } else if (valueObjectType.equals(Identifiers.String)) {
1✔
1429
              pType = getBasicType(mdb, Type.STRING);
1✔
1430
            } else if (valueObjectType.equals(Identifiers.Boolean)) {
1✔
1431
              pType = getBasicType(mdb, Type.BOOLEAN);
1✔
1432
            }
1433
          } else {
1✔
1434
            pType = getBasicType(mdb, Type.STRING);
×
1435
          }
1436

1437
        } catch (UaException e) {
×
1438
          internalLogger.warn(e.toString());
×
1439
        }
1✔
1440
        break;
×
1441
      case ValueRank:
1442
        pType = getBasicType(mdb, Type.STRING);
1✔
1443
        break;
1✔
1444
      case WriteMask:
1445
        pType = getBasicType(mdb, Type.STRING);
1✔
1446
        break;
1✔
1447
      default:
1448
        break;
1449
    }
1450

1451
    return pType;
1✔
1452
  }
1453

1454
  /**
1455
   * Subscribe to OPCUA events as per the
1456
   * spec:https://reference.opcfoundation.org/Core/Part5/v104/docs/6.4.2
1457
   *
1458
   * @param client
1459
   * @throws InterruptedException
1460
   * @throws ExecutionException
1461
   */
1462
  private void subscribeToEvents(OpcUaClient client)
1463
      throws InterruptedException, ExecutionException {
1464
    // create a subscription and a monitored item
1465
    UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
1✔
1466

1467
    ReadValueId readValueId =
1✔
1468
        new ReadValueId(
1469
            Identifiers.Server, AttributeId.EventNotifier.uid(), null, QualifiedName.NULL_VALUE);
1✔
1470

1471
    // client handle must be unique per item
1472
    UInteger clientHandle = uint(clientHandles.getAndIncrement());
1✔
1473

1474
    EventFilter eventFilter =
1✔
1475
        new EventFilter(
1476
            new SimpleAttributeOperand[] {
1477
              new SimpleAttributeOperand(
1478
                  Identifiers.BaseEventType,
1479
                  new QualifiedName[] {new QualifiedName(0, "EventId")},
1480
                  AttributeId.Value.uid(),
1✔
1481
                  null),
1482
              new SimpleAttributeOperand(
1483
                  Identifiers.BaseEventType,
1484
                  new QualifiedName[] {new QualifiedName(0, "EventType")},
1485
                  AttributeId.Value.uid(),
1✔
1486
                  null),
1487
              new SimpleAttributeOperand(
1488
                  Identifiers.BaseEventType,
1489
                  new QualifiedName[] {new QualifiedName(0, "Severity")},
1490
                  AttributeId.Value.uid(),
1✔
1491
                  null),
1492
              new SimpleAttributeOperand(
1493
                  Identifiers.BaseEventType,
1494
                  new QualifiedName[] {new QualifiedName(0, "Time")},
1495
                  AttributeId.Value.uid(),
1✔
1496
                  null),
1497
              new SimpleAttributeOperand(
1498
                  Identifiers.BaseEventType,
1499
                  new QualifiedName[] {new QualifiedName(0, "Message")},
1500
                  AttributeId.Value.uid(),
1✔
1501
                  null)
1502
            },
1503
            new ContentFilter(null));
1504

1505
    MonitoringParameters parameters =
1✔
1506
        new MonitoringParameters(
1507
            clientHandle,
1508
            0.0,
1✔
1509
            ExtensionObject.encode(client.getStaticSerializationContext(), eventFilter),
1✔
1510
            uint(10),
1✔
1511
            true);
1✔
1512

1513
    MonitoredItemCreateRequest request =
1✔
1514
        new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
1515

1516
    List<UaMonitoredItem> items =
1✔
1517
        subscription.createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get();
1✔
1518

1519
    // do something with the value updates
1520
    UaMonitoredItem monitoredItem = items.get(0);
1✔
1521

1522
    monitoredItem.setEventConsumer(
1✔
1523
        (item, vs) -> {
1524
          internalLogger.info("Event Received from {}", item.getReadValueId().getNodeId());
1✔
1525

1526
          StringBuilder eventText = new StringBuilder();
1✔
1527

1528
          ByteString eventId;
1529
          NodeId eventType;
1530
          UShort eventSeverity;
1531
          DateTime eventTime;
1532
          LocalizedText eventMessage;
1533

1534
          for (int i = 0; i < vs.length; i++) {
1✔
1535
            internalLogger.info("\tvariant[{}]: {}", i, vs[i].getValue());
1✔
1536
          }
1537

1538
          eventId = (ByteString) vs[0].getValue();
1✔
1539
          eventType = (NodeId) vs[1].getValue();
1✔
1540
          eventSeverity = (UShort) vs[2].getValue();
1✔
1541
          eventTime = (DateTime) vs[3].getValue();
1✔
1542
          eventMessage = (LocalizedText) vs[4].getValue();
1✔
1543

1544
          //          FIXME:Map these values to YAMCS API
1545
          eventText.append("eventId:" + eventId);
1✔
1546
          eventText.append("\n");
1✔
1547
          eventText.append("eventType:" + eventType);
1✔
1548
          eventText.append("\n");
1✔
1549
          eventText.append("eventSeverity:" + eventSeverity);
1✔
1550
          eventText.append("\n");
1✔
1551
          eventText.append("eventTime:" + eventTime);
1✔
1552
          eventText.append("\n");
1✔
1553
          eventText.append("eventMessage:" + eventMessage);
1✔
1554
          org.yamcs.yarch.protobuf.Db.Event ev =
1555
              Event.newBuilder()
1✔
1556
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1557
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1558
                  .setSource(this.linkName)
1✔
1559
                  .setType(this.linkName)
1✔
1560
                  .setMessage(eventText.toString())
1✔
1561
                  .setSeverity(EventSeverity.INFO)
1✔
1562
                  .build();
1✔
1563
          eventProducer.sendEvent(ev);
1✔
1564
        });
1✔
1565
  }
1✔
1566

1567
  @Override
1568
  public void setupSystemParameters(SystemParametersService sysParamService) {
1569
    super.setupSystemParameters(sysParamService);
1✔
1570
    OPCUAInitStatusParam =
1✔
1571
        sysParamService.createEnumeratedSystemParameter(
1✔
1572
            linkName + "/OPCUAInitStatusParam",
1573
            OPCUAINITStatus.class,
1574
            "The current initialization status of OPCUA client");
1575
    EnumeratedParameterType spLinkStatusType =
1✔
1576
        (EnumeratedParameterType) OPCUAInitStatusParam.getParameterType();
1✔
1577
    spLinkStatusType
1✔
1578
        .enumValue(OPCUAINITStatus.OPCUA_INIT_CONFIG.name())
1✔
1579
        .setDescription(
1✔
1580
            "This link is in the configuration stage(Configuring OPCUA parameters such as certificates)");
1581
    spLinkStatusType
1✔
1582
        .enumValue(OPCUAINITStatus.OPCUA_INIT_TREE.name())
1✔
1583
        .setDescription(
1✔
1584
            "The link is parsing the OPCUA Tree and mapping them to PVs."
1585
                + " Depending on configuration, this can take a while.");
1586

1587
    spLinkStatusType
1✔
1588
        .enumValue(OPCUAINITStatus.OPCUA_INIT_TREE_FAILED.name())
1✔
1589
        .setDescription("The initial parsing of configured nodes failed.");
1✔
1590
    spLinkStatusType
1✔
1591
        .enumValue(OPCUAINITStatus.OPCUA_INIT_EVENTS.name())
1✔
1592
        .setDescription("The link is configuring and subscribing to OPCUA events");
1✔
1593
    spLinkStatusType
1✔
1594
        .enumValue(OPCUAINITStatus.OPCUA_INIT_DATA_SUBSCRIPTION.name())
1✔
1595
        .setDescription(
1✔
1596
            "The link is creating subscriptions for each node that was parsed from the tree"
1597
                + "that has a Value attribute.");
1598
    spLinkStatusType
1✔
1599
        .enumValue(OPCUAINITStatus.OPCUA_INIT_ALL_DATA_QUERY.name())
1✔
1600
        .setDescription(
1✔
1601
            "The link is querying all attributes of all parsed nodes."
1602
                + "This is can be configured to be done at startup.");
1603
    spLinkStatusType
1✔
1604
        .enumValue(OPCUAINITStatus.OPCUA_INIT_OK.name())
1✔
1605
        .setDescription(
1✔
1606
            "The link is done with all OPCUA initialization. It is in an usable state.");
1607

1608
    OPCUAActiveSubsParam =
1✔
1609
        sysParamService.createSystemParameter(
1✔
1610
            linkName + "/OPCUAActiveSubs",
1611
            Type.UINT64,
1612
            "The total number of active opcua subscriptions");
1613
  }
1✔
1614

1615
  @Override
1616
  public List<ParameterValue> getSystemParameters() {
1617
    long time = getCurrentTime();
1✔
1618
    ArrayList<ParameterValue> list = new ArrayList<>();
1✔
1619

1620
    list.add(
1✔
1621
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1622
            OPCUAInitStatusParam, time, currentOPCUAStatus));
1623
    list.add(
1✔
1624
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1625
            OPCUAActiveSubsParam, time, OPCUAActiveSubs.get()));
1✔
1626
    try {
1627
      super.collectSystemParameters(time, list);
1✔
1628
    } catch (Exception e) {
×
1629
      log.error("Exception caught when collecting link system parameters", e);
×
1630
    }
1✔
1631
    return list;
1✔
1632
  }
1633
}
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

© 2025 Coveralls, Inc