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

WindhoverLabs / yamcs-opcua / #25

12 Jul 2024 12:20AM UTC coverage: 82.728% (-1.0%) from 83.756%
#25

push

lorenzo-gomez-windhover
-Cleanup

1 of 12 new or added lines in 1 file covered. (8.33%)

9 existing lines in 1 file now uncovered.

661 of 799 relevant lines covered (82.73%)

0.83 hits per line

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

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

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

153
  class NodeIDAttrPair {
154
    NodeId nodeID;
155
    AttributeId attrID;
156

157
    public NodeIDAttrPair(NodeId newNodeID, AttributeId newAttrID) {
1✔
158
      this.nodeID = newNodeID;
1✔
159
      this.attrID = newAttrID;
1✔
160
    }
1✔
161

162
    public int hashCode() {
163
      return Objects.hash(this.nodeID, this.attrID);
1✔
164
    }
165

166
    public boolean equals(Object obj) {
167
      return (this.hashCode() == obj.hashCode());
1✔
168
    }
169
  }
170

171
  class NodePath {
1✔
172
    String path;
173

174
    HashMap<Object, Object> rootNodeID = new HashMap<Object, Object>();
1✔
175
  }
176

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

188
  /* Configuration Defaults */
189
  static final String STREAM_NAME = "opcua_params";
190

191
  /* Internal member attributes. */
192
  protected Thread thread;
193

194
  private String opcuaStreamName;
195

196
  private String parametersNamespace;
197
  XtceDb mdb;
198

199
  Stream opcuaStream;
200

201
  private static TupleDefinition gftdef = StandardTupleDefinitions.PARAMETER.copy();
1✔
202

203
  private AggregateParameterType opcuaAttrsType;
204
  private AggregateParameterType opcuaNodeIdNumericType;
205
  private AggregateParameterType opcuaNodeIdStringType;
206
  private ManagedSubscription opcuaSubscription;
207

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

210
  private int rootNamespaceIndex;
211
  private String rootIdentifier; // Relative to the rootNamespaceIndex
212
  private IdType rootIdentifierType; // Relative to the rootNamespaceIndex
213

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

225
  private OpcUaClient client;
226

227
  protected AtomicLong inCount = new AtomicLong(0);
1✔
228

229
  private Status linkStatus = Status.OK;
1✔
230

231
  /* Configuration Parameters */
232

233
  private String discoverURL;
234
  private String endpointURL;
235
  private boolean queryAllNodesAtStartup;
236
  private String outputFile;
237
  private int publishInterval; // milliseconds
238

239
  private ArrayList<NodePath> relativeNodePaths = new ArrayList<NodePath>();
1✔
240

241
  private final AtomicLong clientHandles = new AtomicLong(1L);
1✔
242

243
  /* System parameters*/
244

245
  private Parameter OPCUAStatusParam;
246
  private OPCUAStatus currentOPCUAStatus;
247
  private Parameter OPCUAActiveSubsParam;
248
  private AtomicLong OPCUAActiveSubs = new AtomicLong(0);
1✔
249

250
  LinkAction startAction =
1✔
251
      new LinkAction("query_all", "Query All OPCUA Server Data") {
1✔
252
        @Override
253
        public JsonObject execute(Link link, JsonObject jsonObject) {
254

255
          internalLogger.info("Executing query_all action");
1✔
256
          CompletableFuture.supplyAsync(
1✔
257
                  (Supplier<Integer>)
258
                      () -> {
259
                        queryAllOPCUAData();
×
260

261
                        return 0;
×
262
                      })
263
              .whenComplete(
1✔
264
                  (vaue, e) -> {
265
                    internalLogger.info("query_all action Complete");
1✔
266
                  });
1✔
267

268
          return jsonObject;
1✔
269
        }
270
      };
271

272
  public OPCUAStatus getCurrentOPCUAStatus() {
273
    return currentOPCUAStatus;
1✔
274
  }
275

276
  @Override
277
  public Spec getSpec() {
278
    Spec spec = new Spec();
1✔
279

280
    /* Define our configuration parameters. */
281
    spec.addOption("name", OptionType.STRING).withRequired(true);
1✔
282
    spec.addOption("class", OptionType.STRING).withRequired(true);
1✔
283
    spec.addOption("opcuaStream", OptionType.STRING).withRequired(true);
1✔
284
    spec.addOption("endpointUrl", OptionType.STRING).withRequired(true);
1✔
285
    spec.addOption("discoveryUrl", OptionType.STRING).withRequired(true);
1✔
286
    spec.addOption("xtceOutputFile", OptionType.STRING).withRequired(true);
1✔
287
    spec.addOption("parametersNamespace", OptionType.STRING).withRequired(true);
1✔
288
    spec.addOption("publishInterval", OptionType.INTEGER).withRequired(true);
1✔
289
    spec.addOption("queryAllNodesAtStartup", OptionType.BOOLEAN).withRequired(false);
1✔
290

291
    Spec rootNodeIDSpec = new Spec();
1✔
292

293
    rootNodeIDSpec.addOption("namespaceIndex", OptionType.INTEGER).withRequired(true);
1✔
294
    rootNodeIDSpec.addOption("identifier", OptionType.STRING).withRequired(true);
1✔
295
    rootNodeIDSpec.addOption("identifierType", OptionType.STRING).withRequired(true);
1✔
296

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

299
    Spec nodePathSpec = new Spec();
1✔
300
    nodePathSpec.addOption("path", OptionType.STRING);
1✔
301
    nodePathSpec
1✔
302
        .addOption("rootNodeID", OptionType.MAP)
1✔
303
        .withRequired(true)
1✔
304
        .withSpec(rootNodeIDSpec);
1✔
305

306
    spec.addOption("nodePaths", OptionType.LIST)
1✔
307
        .withElementType(OptionType.MAP)
1✔
308
        .withRequired(true)
1✔
309
        .withSpec(nodePathSpec);
1✔
310

311
    return spec;
1✔
312
  }
313

314
  @Override
315
  public void init(String yamcsInstance, String serviceName, YConfiguration config)
316
      throws ConfigurationException {
317
    super.init(yamcsInstance, serviceName, config);
1✔
318

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

330
    this.opcuaStreamName = config.getString("opcuaStream");
1✔
331

332
    this.opcuaStream = getStream(ydb, opcuaStreamName);
1✔
333

334
    this.endpointURL = config.getString("endpointUrl");
1✔
335
    this.discoverURL = config.getString("discoveryUrl");
1✔
336

337
    this.parametersNamespace = config.getString("parametersNamespace");
1✔
338
    this.queryAllNodesAtStartup = config.getBoolean("queryAllNodesAtStartup", false);
1✔
339

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

342
    rootNamespaceIndex = (int) root.get("namespaceIndex");
1✔
343

344
    rootIdentifier = (String) root.get("identifier");
1✔
345
    rootIdentifierType = IdType.valueOf((String) root.get("identifierType"));
1✔
346

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

349
    for (Map<Object, Object> path : nodePaths) {
1✔
350
      NodePath nodePath = new NodePath();
1✔
351
      nodePath.path = (String) path.get("path");
1✔
352
      nodePath.rootNodeID = (HashMap<Object, Object>) path.get("rootNodeID");
1✔
353
      relativeNodePaths.add(nodePath);
1✔
354
    }
1✔
355

356
    this.mdb = YamcsServer.getServer().getInstance(yamcsInstance).getXtceDb();
1✔
357

358
    outputFile = config.getString("xtceOutputFile");
1✔
359
    publishInterval = config.getInt("publishInterval");
1✔
360
  }
1✔
361

362
  private static SpaceSystem verifySpaceSystem(XtceDb mdb, String pathName) {
363
    String namespace;
364
    String name;
365
    int lastSlash = pathName.lastIndexOf('/');
1✔
366
    if ("/".equals(pathName)) {
1✔
367
      namespace = "";
1✔
368
      name = "";
1✔
369
    } else if (lastSlash == -1 || lastSlash == pathName.length() - 1) {
×
370
      namespace = "";
×
371
      name = pathName;
×
372
    } else {
373
      namespace = pathName.substring(0, lastSlash);
×
374
      name = pathName.substring(lastSlash + 1);
×
375
    }
376

377
    // First try with a prefixed slash (should be the common case)
378
    NamedObjectId id =
379
        NamedObjectId.newBuilder().setNamespace("/" + namespace).setName(name).build();
1✔
380
    SpaceSystem spaceSystem = mdb.getSpaceSystem(id);
1✔
381
    if (spaceSystem != null) {
1✔
382
      return spaceSystem;
1✔
383
    }
384

385
    // Maybe some non-xtce namespace like MDB:OPS Name
386
    id = NamedObjectId.newBuilder().setNamespace(namespace).setName(name).build();
×
387
    spaceSystem = mdb.getSpaceSystem(id);
×
388
    if (spaceSystem != null) {
×
389
      return spaceSystem;
×
390
    }
391

392
    throw new NotFoundException("No such space system");
×
393
  }
394

395
  /**
396
   * Initializes all PV mappings to OPCUA nodes and realtime subscriptions(managed data items in
397
   * OPCUA terms).
398
   */
399
  private void opcuaInit() {
400
    createOPCUAAttrAggregateType();
1✔
401
    createOPCUANodeIdTypes();
1✔
402
    mdb.addParameterType(opcuaAttrsType, true);
1✔
403

404
    try {
405

406
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_TREE;
1✔
407

408
      browseOPCUATree(client);
1✔
409

410
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_GENERATE_XTCE;
1✔
411

412
      var spaceSystem = verifySpaceSystem(mdb, "/");
1✔
413

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

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
      } else writer = null;
×
425

426
      writer.write(xtce);
1✔
427

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

431
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_EVENTS;
1✔
432
      subscribeToEvents(client);
1✔
433

434
    } catch (Exception e) {
×
UNCOV
435
      e.printStackTrace();
×
436
      return;
×
437
    }
1✔
438
    try {
439
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_DATA_SUBSCRIPTION;
1✔
440
      createOPCUASubscriptions();
1✔
441
    } catch (Exception e) {
×
UNCOV
442
      e.printStackTrace();
×
443
    }
1✔
444
  }
1✔
445

446
  private void opcuaClientConnect() throws Exception {
447
    client = configureClient();
1✔
448
    connectToOPCUAServer(client);
1✔
449
  }
1✔
450

451
  private static Stream getStream(YarchDatabaseInstance ydb, String streamName) {
452
    Stream stream = ydb.getStream(streamName);
1✔
453
    if (stream == null) {
1✔
454
      try {
455
        ydb.execute("create stream " + streamName + gftdef.getStringDefinition());
1✔
456
      } catch (Exception e) {
×
457
        throw new ConfigurationException(e);
×
458
      }
1✔
459

460
      stream = ydb.getStream(streamName);
1✔
461
    }
462
    return stream;
1✔
463
  }
464

465
  @Override
466
  public void doDisable() {
467
    /* If the thread is created, interrupt it. */
468
    if (thread != null) {
1✔
469
      thread.interrupt();
1✔
470
    }
471

472
    linkStatus = Status.DISABLED;
1✔
473
  }
1✔
474

475
  @Override
476
  public void doEnable() {
477
    linkStatus = Status.OK;
1✔
478
  }
1✔
479

480
  @Override
481
  public String getDetailedStatus() {
482
    if (isDisabled()) {
1✔
483
      return String.format("DISABLED");
1✔
484
    } else {
485
      return String.format("OK, received %d packets", inCount.get());
1✔
486
    }
487
  }
488

489
  @Override
490
  public Status connectionStatus() {
491
    return linkStatus;
1✔
492
  }
493

494
  @Override
495
  protected void doStart() {
496
    try {
497
      opcuaClientConnect();
1✔
498
    } catch (Exception e) {
×
499
      e.printStackTrace();
×
500
      linkStatus = Status.FAILED;
×
501
      notifyFailed(e);
×
502
      return;
×
503
    }
1✔
504
    if (!isDisabled()) {
1✔
505
      doEnable();
1✔
506
    }
507
    startAction.addChangeListener(
1✔
508
        () -> {
509
          /**
510
           * TODO:Might be useful if we want turn off any functionality when the action is disabled
511
           * for instance..
512
           */
513
        });
×
514

515
    /* Create and start the new thread. */
516
    thread = new Thread(this);
1✔
517
    thread.setName(this.getClass().getSimpleName() + "-" + linkName);
1✔
518
    thread.start();
1✔
519

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

523
  @Override
524
  protected void doStop() {
525
    try {
526
      client.disconnect().get();
1✔
527
    } catch (InterruptedException | ExecutionException e) {
×
528
      // TODO Auto-generated catch block
529
      e.printStackTrace();
×
530
    }
1✔
531
    if (thread != null) {
1✔
532
      thread.interrupt();
1✔
533
    }
534

535
    notifyStopped();
1✔
536
  }
1✔
537

538
  @Override
539
  public void run() {
540
    opcuaInit();
1✔
541
    if (queryAllNodesAtStartup) {
1✔
542
      //            NOTE:I'm not sure if queryAllOPCUAData should block...
543
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_ALL_DATA_QUERY;
×
544
      queryAllOPCUAData();
×
545
    }
546
    /* Enter our main loop */
547
    while (isRunningAndEnabled()) {
1✔
548
      currentOPCUAStatus = OPCUAStatus.OPCUA_OK;
1✔
549
    }
550
  }
1✔
551

552
  /**
553
   * Reads all attributes of all configured Value nodes and updates their corresponding PV. Useful
554
   * for querying data from the OPCUA server once, data such as browse names, NodeIds, etc.
555
   */
556
  private void queryAllOPCUAData() {
557
    TupleDefinition tdef = gftdef.copy();
1✔
558
    List<Object> cols = new ArrayList<>(4 + nodeIDToParamsMap.keySet().size());
1✔
559

560
    tdef = gftdef.copy();
1✔
561
    long gentime = timeService.getMissionTime();
1✔
562
    cols.add(gentime);
1✔
563
    cols.add(parametersNamespace);
1✔
564
    cols.add(0);
1✔
565
    cols.add(gentime);
1✔
566

567
    int columnCount = 0;
1✔
568

569
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
570
    /**
571
     * NOTE:This is super inefficient... The reason we collect these nodeIDs in a set is because
572
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
573
     * nodeID given how nodeIDToParamsMap is designed
574
     */
575
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
576
      nodeSet.add(pair.nodeID);
1✔
577
    }
1✔
578

579
    for (NodeId nId : nodeSet) {
1✔
580
      UaNode node;
581

582
      try {
583
        node = client.getAddressSpace().getNode(nId);
1✔
584

585
        DataValue nodeClass = node.readAttribute(AttributeId.NodeClass);
1✔
586

587
        switch (NodeClass.from((int) nodeClass.getValue().getValue())) {
1✔
588
          case Variable:
589
            for (AttributeId attr : AttributeId.VARIABLE_ATTRIBUTES) {
1✔
590

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

593
              if (p.getParameterType() == null) {
1✔
NEW
594
                internalLogger.warn(
×
595
                    "{} ignored since it does not have a Parameter type",
596
                    p,
NEW
597
                    Character.toString(NameDescription.PATH_SEPARATOR));
×
UNCOV
598
                continue;
×
599
              }
600

601
              switch (p.getParameterType().getValueType()) {
1✔
602
                case BOOLEAN:
603
                  {
604
                    String value = "";
1✔
605
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
606
                      value = "NULL";
×
607
                    } else {
608
                      value = node.readAttribute(attr).getValue().getValue().toString();
1✔
609
                    }
610

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

624
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
625
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
626
                  }
627
                  break;
1✔
628
                case FLOAT:
629
                  {
630
                    float value = 0;
×
631
                    if (node.readAttribute(attr).getValue().isNull()) {
×
NEW
632
                      internalLogger.warn("node {} has a Null variant.", node);
×
633
                    } else {
634
                      value = (float) node.readAttribute(attr).getValue().getValue();
×
635
                    }
636

637
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
638
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
639
                  }
640
                  break;
×
641
                case SINT32:
642
                  {
643
                    int value = 0;
1✔
644
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
NEW
645
                      internalLogger.warn("node {} has a Null variant.", node);
×
646
                    } else {
647
                      value = (int) node.readAttribute(attr).getValue().getValue();
1✔
648
                    }
649

650
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
651
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
652
                  }
653
                  break;
1✔
654
                case SINT64:
655
                  {
656
                    long value = 0;
1✔
657
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
NEW
658
                      internalLogger.warn("node {} has a Null variant.", node);
×
659
                    } else {
660
                      value = (long) node.readAttribute(attr).getValue().getValue();
1✔
661
                    }
662

663
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
664
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
665
                  }
666
                  break;
1✔
667
                case STRING:
668
                  {
669
                    String value = "";
1✔
670
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
671
                      value = "NULL";
1✔
672
                    } else {
673
                      value = node.readAttribute(attr).getValue().getValue().toString();
1✔
674
                    }
675

676
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
677
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
678
                  }
679
                  break;
1✔
680
                case UINT32:
681
                  {
682
                    long value = 0;
×
683
                    if (node.readAttribute(attr).getValue().isNull()) {
×
NEW
684
                      internalLogger.warn("node {} has a Null variant.", node);
×
685
                    } else {
686
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
687
                    }
688

689
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
690
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
691
                  }
692
                  break;
×
693
                case UINT64:
694
                  {
695
                    long value = 0;
×
696
                    if (node.readAttribute(attr).getValue().isNull()) {
×
NEW
697
                      internalLogger.warn("node {} has a Null variant.", node);
×
698
                    } else {
699
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
700
                    }
701

702
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
703
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
704
                  }
705
                  break;
×
706
                default:
707
                  break;
708
              }
709

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

712
              columnCount++;
1✔
713
            }
1✔
714
            break;
1✔
715
          default:
716
            break;
717
        }
718

719
      } catch (UaException e) {
×
720
        // TODO Auto-generated catch block
721
        e.printStackTrace();
×
722
        continue;
×
723
      }
1✔
724
    }
1✔
725

UNCOV
726
    pushTuple(tdef, cols);
×
727

728
    inCount.getAndAdd(columnCount);
×
729
  }
×
730

731
  private synchronized void pushTuple(TupleDefinition tdef, List<Object> cols) {
732
    Tuple t;
733
    t = new Tuple(tdef, cols);
1✔
734
    opcuaStream.emitTuple(t);
1✔
735
  }
1✔
736

737
  private static ParameterType getOrCreateType(
738
      XtceDb mdb, String name, Supplier<ParameterType.Builder<?>> supplier) {
739

740
    String fqn = XtceDb.YAMCS_SPACESYSTEM_NAME + NameDescription.PATH_SEPARATOR + name;
1✔
741
    ParameterType ptype = mdb.getParameterType(fqn);
1✔
742
    if (ptype != null) {
1✔
743
      return ptype;
1✔
744
    }
745
    ParameterType.Builder<?> typeb = supplier.get().setName(name);
1✔
746

747
    ptype = typeb.build();
1✔
748
    ((NameDescription) ptype).setQualifiedName(fqn);
1✔
749

750
    return mdb.addSystemParameterType(ptype);
1✔
751
  }
752

753
  public static ParameterType getBasicType(XtceDb mdb, Type type) {
754
    ParameterType pType = null;
1✔
755
    switch (type) {
1✔
756
      case BOOLEAN:
757
        return getOrCreateType(mdb, "boolean", () -> new BooleanParameterType.Builder());
1✔
758
      case STRING:
759
        return getOrCreateType(mdb, "string", () -> new StringParameterType.Builder());
1✔
760

761
      case FLOAT:
762
        return getOrCreateType(
1✔
763
            mdb, "float32", () -> new FloatParameterType.Builder().setSizeInBits(32));
1✔
764
      case DOUBLE:
765
        return getOrCreateType(
1✔
766
            mdb, "float64", () -> new FloatParameterType.Builder().setSizeInBits(64));
1✔
767
      case SINT32:
768
        return getOrCreateType(
1✔
769
            mdb,
770
            "sint32",
771
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(true));
×
772
      case SINT64:
773
        return getOrCreateType(
1✔
774
            mdb,
775
            "sint64",
776
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(true));
1✔
777
      case UINT32:
778
        return getOrCreateType(
×
779
            mdb,
780
            "uint32",
781
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(false));
×
782
      case UINT64:
783
        return getOrCreateType(
1✔
784
            mdb,
785
            "uint64",
786
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(false));
×
787
      default:
788
        break;
789
    }
790

791
    return pType;
×
792
  }
793

794
  public static ParameterValue getNewPv(Parameter parameter, long time) {
795
    ParameterValue pv = new ParameterValue(parameter);
1✔
796
    pv.setAcquisitionTime(time);
1✔
797
    pv.setGenerationTime(time);
1✔
798
    return pv;
1✔
799
  }
800

801
  public static ParameterValue getPV(Parameter parameter, long time, String v) {
802
    ParameterValue pv = getNewPv(parameter, time);
1✔
803
    pv.setEngValue(ValueUtility.getStringValue(v));
1✔
804
    return pv;
1✔
805
  }
806

807
  public static ParameterValue getPV(Parameter parameter, long time, double v) {
808
    ParameterValue pv = getNewPv(parameter, time);
1✔
809
    pv.setEngValue(ValueUtility.getDoubleValue(v));
1✔
810
    return pv;
1✔
811
  }
812

813
  public static ParameterValue getPV(Parameter parameter, long time, float v) {
814
    ParameterValue pv = getNewPv(parameter, time);
×
815
    pv.setEngValue(ValueUtility.getFloatValue(v));
×
816
    return pv;
×
817
  }
818

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

825
  public static ParameterValue getPV(Parameter parameter, long time, long v) {
826
    ParameterValue pv = getNewPv(parameter, time);
1✔
827
    pv.setEngValue(ValueUtility.getSint64Value(v));
1✔
828
    return pv;
1✔
829
  }
830

831
  @Override
832
  public Status getLinkStatus() {
833
    return linkStatus;
1✔
834
  }
835

836
  @Override
837
  public boolean isDisabled() {
838
    return linkStatus == Status.DISABLED;
1✔
839
  }
840

841
  @Override
842
  public long getDataInCount() {
843
    return inCount.get();
1✔
844
  }
845

846
  @Override
847
  public long getDataOutCount() {
848
    return 0;
1✔
849
  }
850

851
  @Override
852
  public void resetCounters() {
853
    inCount.set(0);
1✔
854
  }
1✔
855

856
  /**
857
   * Selects first non-secured endpoint from endpoints found at discover URL. At the moment secured
858
   * endpoints are not supported.
859
   *
860
   * @return
861
   * @throws Exception
862
   */
863
  private OpcUaClient configureClient() throws Exception {
864

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

867
    // At the moment, we do not support certificates.
868
    EndpointDescription selectedEndpoint = null;
1✔
869
    for (var endpoint : endpoints) {
1✔
870
      switch (endpoint.getSecurityMode()) {
1✔
871
        case Invalid:
NEW
872
          internalLogger.warn("Endpoint mode {} is not supported.", endpoint.getSecurityMode());
×
873
          break;
×
874
        case None:
875
          selectedEndpoint = endpoint;
1✔
876
          break;
1✔
877
        case Sign:
NEW
878
          internalLogger.warn("Endpoint mode {} is not supported.", endpoint.getSecurityMode());
×
UNCOV
879
          break;
×
880
        case SignAndEncrypt:
NEW
881
          internalLogger.warn("Endpoint mode {} is not supported.", endpoint.getSecurityMode());
×
882
          break;
883
      }
884

885
      if (selectedEndpoint != null) {
1✔
886
        break;
1✔
887
      }
888
    }
×
889

890
    if (selectedEndpoint == null) {
1✔
891
      throw new Exception("No viable endpoint found from list:" + endpoints);
×
892
    }
893

894
    OpcUaClientConfig builder = OpcUaClientConfig.builder().setEndpoint(selectedEndpoint).build();
1✔
895

896
    return OpcUaClient.create(builder);
1✔
897
  }
898

899
  /**
900
   * Browse all nodes starting from browseRoot.
901
   *
902
   * @param indent
903
   * @param client
904
   * @param browseRoot
905
   */
906
  private void browseNodeWithReferences(String indent, OpcUaClient client, NodeId browseRoot) {
907
    BrowseDescription browse =
1✔
908
        new BrowseDescription(
909
            browseRoot,
910
            BrowseDirection.Forward,
911
            Identifiers.References,
912
            true,
1✔
913
            uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
1✔
914
            uint(BrowseResultMask.All.getValue()));
1✔
915

916
    try {
917

918
      BrowseResult browseResult = client.browse(browse).get();
1✔
919

920
      List<ReferenceDescription> references = toList(browseResult.getReferences());
1✔
921

922
      if (references.isEmpty()) {
1✔
923
        internalLogger.warn("Found empty reference list under {}.", browseRoot);
1✔
924
        return;
1✔
925
      }
926

927
      for (ReferenceDescription rd : references) {
1✔
928
        Object desc = null;
1✔
929
        Object value = null;
1✔
930
        UaNode node = null;
1✔
931
        try {
932

933
          node =
1✔
934
              client
935
                  .getAddressSpace()
1✔
936
                  .getNode(rd.getNodeId().toNodeId(client.getNamespaceTable()).get());
1✔
937
          DataValue attr = node.readAttribute(AttributeId.Description);
1✔
938
          desc = attr.getValue().getValue();
1✔
939

940
          attr = node.readAttribute(AttributeId.Value);
1✔
941

942
          value = attr.getValue();
1✔
943

944
        } catch (UaException e) {
×
UNCOV
945
          e.printStackTrace();
×
946
        }
1✔
947

948
        if (node != null) {
1✔
949
          addOPCUAPV(client, node);
1✔
950

951
          log.debug(
1✔
952
              "{} Node={}, Desc={}, Value={}", indent, rd.getBrowseName().getName(), desc, value);
1✔
953

954
          // recursively browse to children
955
          rd.getNodeId()
1✔
956
              .toNodeId(client.getNamespaceTable())
1✔
957
              .ifPresent(nodeId -> browseNodeWithReferences(indent + "  ", client, nodeId));
1✔
958
        }
959
      }
1✔
960

961
    } catch (InterruptedException e1) {
×
UNCOV
962
      e1.printStackTrace();
×
963
    } catch (ExecutionException e1) {
×
UNCOV
964
      e1.printStackTrace();
×
965
    }
1✔
966
  }
1✔
967

968
  /**
969
   * Adds new PV with the name of node.
970
   *
971
   * @param client
972
   * @param node
973
   */
974
  private void addOPCUAPV(OpcUaClient client, UaNode node) {
975

976
    if (node.getBrowseName()
1✔
977
        .getName()
1✔
978
        .contains(Character.toString(NameDescription.PATH_SEPARATOR))) {
1✔
979
      internalLogger.info(
1✔
980
          "{} ignored since it contains a {} character",
981
          node.getBrowseName().getName(),
1✔
982
          Character.toString(NameDescription.PATH_SEPARATOR));
1✔
983

984
    } else {
985

986
      /**
987
       * NOTE:For now we'll just flatten all the attributes instead of using an aggregate type for
988
       * attributes
989
       */
990
      for (AttributeId attr : AttributeId.values()) {
1✔
991

992
        ParameterType ptype = OPCUAAttrTypeToParamType(attr, node);
1✔
993

994
        String opcuaTranslatedQName = translateNodeToParamQName(client, node, attr);
1✔
995
        Parameter p = VariableParam.getForFullyQualifiedName(opcuaTranslatedQName);
1✔
996

997
        p.setParameterType(ptype);
1✔
998

999
        if (mdb.getParameter(p.getQualifiedName()) == null) {
1✔
1000
          log.debug("Adding OPCUA object as parameter to mdb:{}", p.getQualifiedName());
1✔
1001
          mdb.addParameter(p, true);
1✔
1002

1003
          nodeIDToParamsMap.put(new NodeIDAttrPair(node.getNodeId(), attr), (VariableParam) p);
1✔
1004
        }
1005
      }
1006
    }
1007
  }
1✔
1008

1009
  /**
1010
   * Map nodeID name to a qualified name that can be used for a YAMCS PV.
1011
   *
1012
   * @param client
1013
   * @param node
1014
   * @param attr
1015
   * @return
1016
   */
1017
  private String translateNodeToParamQName(OpcUaClient client, UaNode node, AttributeId attr) {
1018
    LocalizedText localizedDisplayName = null;
1✔
1019
    try {
1020

1021
      localizedDisplayName =
1✔
1022
          (LocalizedText) (node.readAttribute(AttributeId.DisplayName).getValue().getValue());
1✔
1023
    } catch (UaException e) {
×
1024
      // TODO Auto-generated catch block
1025
      e.printStackTrace();
×
1026
    }
1✔
1027
    String opcuaTranslatedQName =
1✔
1028
        qualifiedName(
1✔
1029
            parametersNamespace
1030
                + NameDescription.PATH_SEPARATOR
1031
                + node.getNodeId().toParseableString().replace(";", "-")
1✔
1032
                + NameDescription.PATH_SEPARATOR
1033
                + localizedDisplayName.getText(),
1✔
1034
            attr.toString());
1✔
1035

1036
    return opcuaTranslatedQName;
1✔
1037
  }
1038

1039
  /**
1040
   * Browse node at nodePath relative to browseRoot.
1041
   *
1042
   * @param indent
1043
   * @param client
1044
   * @param browseRoot
1045
   * @param nodePath in the format of "0:Root,0:Objects,2:HelloWorld,2:MyObject,2:Bar"
1046
   */
1047
  private void browsePath(String indent, OpcUaClient client, NodeId startingNode, String nodePath) {
1048
    internalLogger.info("Browsing at " + startingNode);
1✔
1049
    ArrayList<String> rPathTokens = new ArrayList<String>();
1✔
1050
    ArrayList<RelativePathElement> relaitivePathElements = new ArrayList<RelativePathElement>();
1✔
1051

1052
    for (var pathToken : nodePath.split(",")) {
1✔
1053
      rPathTokens.add(nodePath);
1✔
1054

1055
      int namespaceIndex = 0;
1✔
1056

1057
      String namespaceName = "";
1✔
1058

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

1061
      namespaceName = pathToken.split(":")[1];
1✔
1062

1063
      relaitivePathElements.add(
1✔
1064
          new RelativePathElement(
1065
              Identifiers.HierarchicalReferences,
1066
              false,
1✔
1067
              true,
1✔
1068
              new QualifiedName(namespaceIndex, namespaceName)));
1069
    }
1070

1071
    ArrayList<BrowsePath> list = new ArrayList<BrowsePath>();
1✔
1072

1073
    RelativePathElement[] elements = new RelativePathElement[relaitivePathElements.size()];
1✔
1074

1075
    relaitivePathElements.toArray(elements);
1✔
1076

1077
    list.add(new BrowsePath(startingNode, new RelativePath(elements)));
1✔
1078

1079
    TranslateBrowsePathsToNodeIdsResponse response = null;
1✔
1080
    try {
1081
      response = client.translateBrowsePaths(list).get();
1✔
1082
    } catch (InterruptedException e) {
×
1083
      // TODO Auto-generated catch block
1084
      e.printStackTrace();
×
1085
    } catch (ExecutionException e) {
×
1086
      // TODO Auto-generated catch block
1087
      e.printStackTrace();
×
1088
    }
1✔
1089

1090
    BrowsePathResult result = Arrays.asList(response.getResults()).get(0);
1✔
1091
    StatusCode statusCode = result.getStatusCode();
1✔
1092

1093
    if (statusCode.isBad()) {
1✔
1094
      log.warn("Bad status code:" + statusCode);
×
1095
      return;
×
1096
    } else if (statusCode.isUncertain()) {
1✔
1097
      log.warn("Uncertain status code:" + statusCode);
×
1098
      return;
×
1099
    }
1100

1101
    try {
1102
      UaNode node =
1✔
1103
          client
1104
              .getAddressSpace()
1✔
1105
              .getNode(
1✔
1106
                  result.getTargets()[0].getTargetId().toNodeId(client.getNamespaceTable()).get());
1✔
1107

1108
      addOPCUAPV(client, node);
1✔
1109
    } catch (UaException e) {
×
1110
      // TODO Auto-generated catch block
1111
      e.printStackTrace();
×
1112
    }
1✔
1113
  }
1✔
1114

1115
  private void createOPCUASubscriptions() {
1116
    createDataChangeListener();
1✔
1117
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
1118
    /**
1119
     * FIXME:This is super inefficient... The reason we collect these nodeIDs in a set is because
1120
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
1121
     * nodeID given how nodeIDToParamsMap is designed
1122
     */
1123
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
1124
      nodeSet.add(pair.nodeID);
1✔
1125
    }
1✔
1126

1127
    ArrayList<NodeId> variableNodes = new ArrayList<NodeId>();
1✔
1128
    for (NodeId id : nodeSet) {
1✔
1129
      Variant nodeClass = null;
1✔
1130
      try {
1131
        UaNode node = client.getAddressSpace().getNode(id);
1✔
1132

1133
        nodeClass = node.readAttribute(AttributeId.NodeClass).getValue();
1✔
1134

1135
      } catch (UaException e) {
×
1136
        // TODO Auto-generated catch block
1137
        e.printStackTrace();
×
1138
      }
1✔
1139
      if (nodeClass != null) {
1✔
1140
        //        try {
1141
        switch (NodeClass.from((int) nodeClass.getValue())) {
1✔
1142
            // As per the spec, the only thing we can subscribe to is Variables
1143
          case Variable:
1144
            variableNodes.add(id);
1✔
1145
            break;
1146
        }
1147
      }
1148
    }
1✔
1149

1150
    try {
1151
      List<ManagedDataItem> dataItems = opcuaSubscription.createDataItems(variableNodes);
1✔
1152
      for (var dataItem : dataItems) {
1✔
1153
        log.debug("Status code for dataItem:{}", dataItem.getStatusCode());
1✔
1154
        OPCUAActiveSubs.addAndGet(1);
1✔
1155
      }
1✔
1156
    } catch (UaException e) {
×
1157
      // TODO Auto-generated catch block
1158
      e.printStackTrace();
×
1159
    }
1✔
1160
  }
1✔
1161

1162
  public void connectToOPCUAServer(OpcUaClient client) throws Exception {
1163
    internalLogger.info("Connecting to OPCUA server...");
1✔
1164
    client.connect().get();
1✔
1165

1166
    addAction(startAction);
1✔
1167
    startAction.setEnabled(true);
1✔
1168
  }
1✔
1169

1170
  /**
1171
   * Browses the tree on the OPCUA server and maps them to YAMCS Parameters.
1172
   *
1173
   * @param client
1174
   */
1175
  private void browseOPCUATree(OpcUaClient client) {
1176
    // start browsing at root folder
1177
    internalLogger.info("Browsing OPCUA...");
1✔
1178
    for (var p : relativeNodePaths) {
1✔
1179
      int namespaceIndex = (int) p.rootNodeID.get("namespaceIndex");
1✔
1180
      String identifier = (String) p.rootNodeID.get("identifier");
1✔
1181
      IdType identifierType = IdType.valueOf((String) p.rootNodeID.get("identifierType"));
1✔
1182

1183
      browsePath(
1✔
1184
          endpointURL, client, getNewNodeID(identifierType, namespaceIndex, identifier), p.path);
1✔
1185
    }
1✔
1186

1187
    NodeId nodeID = null;
1✔
1188
    nodeID = getNewNodeID(rootIdentifierType, rootNamespaceIndex, rootIdentifier);
1✔
1189

1190
    //  TODO:Make root default when no namespaceIndex/identifier pair is specified
1191
    browseNodeWithReferences("", client, nodeID);
1✔
1192
  }
1✔
1193

1194
  /**
1195
   * Get new OPCUA-compliant NodeID object that is created from NamespaceIndex and Identifier. At
1196
   * the moment only String and Numeric node ids are supported.
1197
   *
1198
   * @param rootIdentifierType
1199
   * @param NamespaceIndex
1200
   * @param Identifier
1201
   * @return
1202
   */
1203
  private NodeId getNewNodeID(IdType rootIdentifierType, int NamespaceIndex, String Identifier) {
1204
    NodeId nodeID = null;
1✔
1205
    switch (rootIdentifierType) {
1✔
1206
      case Guid:
1207
        //                FIXME
1208
        break;
×
1209
      case Numeric:
1210
        nodeID = new NodeId(NamespaceIndex, Integer.parseInt(Identifier));
1✔
1211
        break;
1✔
1212
      case Opaque:
1213
        //                FIXME
1214
        break;
×
1215
      case String:
1216
        nodeID = new NodeId(NamespaceIndex, Identifier);
×
1217
        break;
×
1218
      default:
1219
        break;
1220
    }
1221
    return nodeID;
1✔
1222
  }
1223

1224
  /** Data listener for realtime OPCUA server updates. */
1225
  private void createDataChangeListener() {
1226
    try {
1227
      opcuaSubscription = ManagedSubscription.create(client, publishInterval);
1✔
1228
    } catch (UaException e) {
×
1229
      // TODO Auto-generated catch block
1230
      e.printStackTrace();
×
1231
    }
1✔
1232
    opcuaSubscription.addDataChangeListener(
1✔
1233
        (items, values) -> {
1234
          for (int i = 0; i < items.size(); i++) {
1✔
1235
            NodeIDAttrPair nodeAttrKey =
1✔
1236
                new NodeIDAttrPair(items.get(i).getNodeId(), AttributeId.Value);
1✔
1237
            log.debug(
1✔
1238
                "subscription value received: item={}, value={}",
1239
                items.get(i).getNodeId(),
1✔
1240
                values.get(i).getValue());
1✔
1241

1242
            log.debug(
1✔
1243
                "Pushing new PV for param name {} which is mapped to NodeID {}",
1244
                nodeIDToParamsMap.get(nodeAttrKey),
1✔
1245
                items.get(i).getNodeId());
1✔
1246

1247
            TupleDefinition tdef = gftdef.copy();
1✔
1248
            List<Object> cols = new ArrayList<>(4 + 1);
1✔
1249
            //            FIXME: Add leap seconds.... as config or get it from YAMCS API.
1250
            long gentime =
1✔
1251
                values
1252
                    .get(i)
1✔
1253
                    .getSourceTime()
1✔
1254
                    .getJavaInstant()
1✔
1255
                    .plus(37, ChronoUnit.SECONDS)
1✔
1256
                    .toEpochMilli();
1✔
1257
            cols.add(gentime);
1✔
1258
            cols.add(parametersNamespace);
1✔
1259
            cols.add(0);
1✔
1260
            cols.add(gentime);
1✔
1261

1262
            /**
1263
             * TODO:Not sure if this is the best way to do this since the aggregate values will be
1264
             * partially updated. Another potential approach might be to decouple the live OPCUA
1265
             * data(subscriptions) via namespaces. For example; have a "special" namespace called
1266
             * "subscriptions" that ONLY gets updated with items. And maybe another namespace for
1267
             * static data...maybe.
1268
             *
1269
             * <p>Another option is to flatten everything and have no aggregate types at all. That
1270
             * approach might even simplify the code quite a bit...
1271
             *
1272
             * <p>Another question worth answering before moving forward is to find out whether or
1273
             * not it is concrete in the OPCUA protocol what data can change in real time and which
1274
             * data is "static". Not sure if there is any "static" data given that clients have the
1275
             * ability of writing to values... might be worth a test.
1276
             */
1277
            log.debug(
1✔
1278
                "Data({}) chnage triggered for {}",
1279
                values.get(i).getValue(),
1✔
1280
                nodeIDToParamsMap.get(nodeAttrKey));
1✔
1281

1282
            if (nodeIDToParamsMap.get(nodeAttrKey) == null) {
1✔
1283
              log.debug("No parameter mapping found for {}", nodeAttrKey.nodeID);
×
1284
              continue;
×
1285
            } else {
1286
              log.debug(
1✔
1287
                  String.format(
1✔
1288
                      "parameter mapping found for {} and {}",
1289
                      nodeAttrKey.nodeID,
1290
                      nodeAttrKey.attrID));
1291
            }
1292

1293
            if (values.get(i).getValue() != null && values.get(i).getValue().getValue() != null) {
1✔
1294

1295
              switch (nodeIDToParamsMap.get(nodeAttrKey).getParameterType().getValueType()) {
1✔
1296
                case BOOLEAN:
1297
                  {
1298
                    boolean value = (boolean) values.get(i).getValue().getValue();
1✔
1299

1300
                    tdef.addColumn(
1✔
1301
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1302
                        DataType.PARAMETER_VALUE);
1303
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1304
                  }
1305
                  break;
1✔
1306
                case DOUBLE:
1307
                  {
1308
                    double value = (double) values.get(i).getValue().getValue();
1✔
1309

1310
                    tdef.addColumn(
1✔
1311
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1312
                        DataType.PARAMETER_VALUE);
1313
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1314
                  }
1315
                  break;
1✔
1316
                case FLOAT:
1317
                  {
1318
                    float value = (float) values.get(i).getValue().getValue();
×
1319

1320
                    tdef.addColumn(
×
1321
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1322
                        DataType.PARAMETER_VALUE);
1323
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1324
                  }
1325
                  break;
×
1326
                case SINT32:
1327
                  {
1328
                    int value = (int) values.get(i).getValue().getValue();
1✔
1329
                    tdef.addColumn(
1✔
1330
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1331
                        DataType.PARAMETER_VALUE);
1332
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1333
                  }
1334
                  break;
1✔
1335
                case SINT64:
1336
                  {
1337
                    long value = (long) values.get(i).getValue().getValue();
1✔
1338

1339
                    tdef.addColumn(
1✔
1340
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1341
                        DataType.PARAMETER_VALUE);
1342
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1343
                  }
1344
                  break;
1✔
1345
                case STRING:
1346
                  {
1347
                    String value = (String) values.get(i).getValue().getValue().toString();
1✔
1348

1349
                    tdef.addColumn(
1✔
1350
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1351
                        DataType.PARAMETER_VALUE);
1352
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1353
                  }
1354
                  break;
1✔
1355
                case TIMESTAMP:
1356
                  {
1357
                    String value = (String) values.get(i).getValue().getValue();
×
1358

1359
                    tdef.addColumn(
×
1360
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1361
                        DataType.PARAMETER_VALUE);
1362
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1363
                  }
1364
                  break;
×
1365
                case UINT32:
1366
                  {
1367
                    int value = (int) values.get(i).getValue().getValue();
×
1368
                    tdef.addColumn(
×
1369
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1370
                        DataType.PARAMETER_VALUE);
1371
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1372
                  }
1373
                  break;
×
1374
                case UINT64:
1375
                  {
1376
                    long value = (long) values.get(i).getValue().getValue();
×
1377

1378
                    tdef.addColumn(
×
1379
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1380
                        DataType.PARAMETER_VALUE);
1381
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1382
                  }
1383
                  break;
×
1384
                default:
1385
                  break;
1386
              }
1387

1388
              pushTuple(tdef, cols);
1✔
1389

1390
              inCount.getAndAdd(1);
1✔
1391
            } else {
1392
              // TODO:Add some type emptyValue count for OPS.
1393
              log.warn(
1✔
1394
                  "Data chnage triggered for {}, but it empty. This should not happen.",
1395
                  nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName());
1✔
1396
            }
1397
          }
1398
        });
1✔
1399
  }
1✔
1400

1401
  /**
1402
   * This method is here for future growth in case we find there is a benefit to using aggregate
1403
   * types
1404
   */
1405
  private void createOPCUAAttrAggregateType() {
1406

1407
    AggregateParameterType.Builder opcuaAttrsTypeBuidlder = new AggregateParameterType.Builder();
1✔
1408

1409
    opcuaAttrsType = new AggregateParameterType.Builder().setName("OPCUObjectAttributes").build();
1✔
1410

1411
    opcuaAttrsTypeBuidlder.setName("OPCUObjectAttributes");
1✔
1412
    for (AttributeId attr : AttributeId.values()) {
1✔
1413
      opcuaAttrsTypeBuidlder.addMember(new Member(attr.toString(), getBasicType(mdb, Type.STRING)));
1✔
1414
    }
1415

1416
    opcuaAttrsType = opcuaAttrsTypeBuidlder.build();
1✔
1417
    ((NameDescription) opcuaAttrsType)
1✔
1418
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaAttrsType.getName()));
1✔
1419
  }
1✔
1420

1421
  /**
1422
   * This method is here for future growth in case we find there is a benefit to using aggregate
1423
   * types
1424
   */
1425
  private void createOPCUANodeIdTypes() {
1426
    AggregateParameterType.Builder opcuaAttrsNumericNodeIdBuidlder =
1✔
1427
        new AggregateParameterType.Builder();
1428

1429
    opcuaAttrsNumericNodeIdBuidlder.setName("OPCUA_Numeric_NodeId");
1✔
1430
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1431
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1432
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1433
        new Member("identifier", getBasicType(mdb, Type.UINT64)));
1✔
1434

1435
    opcuaNodeIdNumericType = opcuaAttrsNumericNodeIdBuidlder.build();
1✔
1436
    ((NameDescription) opcuaNodeIdNumericType)
1✔
1437
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdNumericType.getName()));
1✔
1438

1439
    AggregateParameterType.Builder opcuaAttrsTypeStringBuidlder =
1✔
1440
        new AggregateParameterType.Builder();
1441

1442
    opcuaAttrsTypeStringBuidlder.setName("OPCUA_String_NodeId");
1✔
1443
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1444
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1445
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1446
        new Member("identifier", getBasicType(mdb, Type.STRING)));
1✔
1447

1448
    opcuaNodeIdStringType = opcuaAttrsTypeStringBuidlder.build();
1✔
1449
    ((NameDescription) opcuaNodeIdStringType)
1✔
1450
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdStringType.getName()));
1✔
1451

1452
    mdb.addParameterType(opcuaNodeIdNumericType, true);
1✔
1453
    mdb.addParameterType(opcuaNodeIdStringType, true);
1✔
1454
  }
1✔
1455

1456
  /**
1457
   * Get new ParameterType for the specified attribute of the node. Particularly useful for Value
1458
   * attributes of nodes.
1459
   *
1460
   * @param attr
1461
   * @param node
1462
   * @return
1463
   */
1464
  private ParameterType OPCUAAttrTypeToParamType(AttributeId attr, UaNode node) {
1465
    ParameterType pType = null;
1✔
1466

1467
    switch (attr) {
1✔
1468
      case AccessLevel:
1469
        pType = getBasicType(mdb, Type.STRING);
1✔
1470
        break;
1✔
1471
      case ArrayDimensions:
1472
        pType = getBasicType(mdb, Type.STRING);
1✔
1473
        break;
1✔
1474
      case BrowseName:
1475
        pType = getBasicType(mdb, Type.STRING);
1✔
1476
        break;
1✔
1477
      case ContainsNoLoops:
1478
        pType = getBasicType(mdb, Type.STRING);
1✔
1479
        break;
1✔
1480
      case DataType:
1481
        pType = getBasicType(mdb, Type.STRING);
1✔
1482
        break;
1✔
1483
      case Description:
1484
        pType = getBasicType(mdb, Type.STRING);
1✔
1485
        break;
1✔
1486
      case DisplayName:
1487
        pType = getBasicType(mdb, Type.STRING);
1✔
1488
        break;
1✔
1489
      case EventNotifier:
1490
        pType = getBasicType(mdb, Type.STRING);
1✔
1491
        break;
1✔
1492
      case Executable:
1493
        pType = getBasicType(mdb, Type.STRING);
1✔
1494
        break;
1✔
1495
      case Historizing:
1496
        pType = getBasicType(mdb, Type.STRING);
1✔
1497
        break;
1✔
1498
      case InverseName:
1499
        pType = getBasicType(mdb, Type.STRING);
1✔
1500
        break;
1✔
1501
      case IsAbstract:
1502
        pType = getBasicType(mdb, Type.STRING);
1✔
1503
        break;
1✔
1504
      case MinimumSamplingInterval:
1505
        pType = getBasicType(mdb, Type.STRING);
1✔
1506
        break;
1✔
1507
      case NodeClass:
1508
        pType = getBasicType(mdb, Type.STRING);
1✔
1509
        break;
1✔
1510
      case NodeId:
1511
        pType = getBasicType(mdb, Type.STRING);
1✔
1512
        break;
1✔
1513
      case Symmetric:
1514
        pType = getBasicType(mdb, Type.STRING);
1✔
1515
        break;
1✔
1516
      case UserAccessLevel:
1517
        pType = getBasicType(mdb, Type.STRING);
1✔
1518
        break;
1✔
1519
      case UserExecutable:
1520
        pType = getBasicType(mdb, Type.STRING);
1✔
1521
        break;
1✔
1522
      case UserWriteMask:
1523
        pType = getBasicType(mdb, Type.STRING);
1✔
1524
        break;
1✔
1525
      case Value:
1526
        try {
1527

1528
          var value = node.readAttribute(attr).getValue();
1✔
1529

1530
          if (value.isNotNull()) {
1✔
1531

1532
            Object valueObject = value.getValue();
1✔
1533

1534
            if (valueObject instanceof Short) {
1✔
1535
              pType = getBasicType(mdb, Type.SINT32);
1✔
1536
            } else if (valueObject instanceof Integer) {
1✔
1537
              pType = getBasicType(mdb, Type.SINT32);
1✔
1538

1539
            } else if (valueObject instanceof Long) {
1✔
1540
              pType = getBasicType(mdb, Type.SINT64);
1✔
1541
            } else if (valueObject instanceof Double) {
1✔
1542
              pType = getBasicType(mdb, Type.DOUBLE);
1✔
1543
            } else if (valueObject instanceof Float) {
1✔
1544
              pType = getBasicType(mdb, Type.FLOAT);
1✔
1545
            } else if (valueObject instanceof Character) {
1✔
1546
              pType = getBasicType(mdb, Type.STRING);
×
1547
            } else if (valueObject instanceof String) {
1✔
1548
              pType = getBasicType(mdb, Type.STRING);
1✔
1549

1550
            } else if (valueObject instanceof Boolean) {
1✔
1551
              pType = getBasicType(mdb, Type.BOOLEAN);
1✔
1552
            } else {
1553
              pType = getBasicType(mdb, Type.STRING);
1✔
1554
            }
1555
          } else {
1✔
1556
            pType = getBasicType(mdb, Type.STRING);
1✔
1557
          }
1558

1559
        } catch (UaException e) {
×
UNCOV
1560
          e.printStackTrace();
×
1561
          //                        FIXME:Add log message
1562
        }
1✔
1563
        break;
×
1564
      case ValueRank:
1565
        pType = getBasicType(mdb, Type.STRING);
1✔
1566
        break;
1✔
1567
      case WriteMask:
1568
        pType = getBasicType(mdb, Type.STRING);
1✔
1569
        break;
1✔
1570
      default:
1571
        break;
1572
    }
1573

1574
    return pType;
1✔
1575
  }
1576

1577
  private void subscribeToEvents(OpcUaClient client)
1578
      throws InterruptedException, ExecutionException {
1579
    // create a subscription and a monitored item
1580
    UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
1✔
1581

1582
    ReadValueId readValueId =
1✔
1583
        new ReadValueId(
1584
            Identifiers.Server, AttributeId.EventNotifier.uid(), null, QualifiedName.NULL_VALUE);
1✔
1585

1586
    // client handle must be unique per item
1587
    UInteger clientHandle = uint(clientHandles.getAndIncrement());
1✔
1588

1589
    EventFilter eventFilter =
1✔
1590
        new EventFilter(
1591
            new SimpleAttributeOperand[] {
1592
              new SimpleAttributeOperand(
1593
                  Identifiers.BaseEventType,
1594
                  new QualifiedName[] {new QualifiedName(0, "EventId")},
1595
                  AttributeId.Value.uid(),
1✔
1596
                  null),
1597
              new SimpleAttributeOperand(
1598
                  Identifiers.BaseEventType,
1599
                  new QualifiedName[] {new QualifiedName(0, "EventType")},
1600
                  AttributeId.Value.uid(),
1✔
1601
                  null),
1602
              new SimpleAttributeOperand(
1603
                  Identifiers.BaseEventType,
1604
                  new QualifiedName[] {new QualifiedName(0, "Severity")},
1605
                  AttributeId.Value.uid(),
1✔
1606
                  null),
1607
              new SimpleAttributeOperand(
1608
                  Identifiers.BaseEventType,
1609
                  new QualifiedName[] {new QualifiedName(0, "Time")},
1610
                  AttributeId.Value.uid(),
1✔
1611
                  null),
1612
              new SimpleAttributeOperand(
1613
                  Identifiers.BaseEventType,
1614
                  new QualifiedName[] {new QualifiedName(0, "Message")},
1615
                  AttributeId.Value.uid(),
1✔
1616
                  null)
1617
            },
1618
            new ContentFilter(null));
1619

1620
    MonitoringParameters parameters =
1✔
1621
        new MonitoringParameters(
1622
            clientHandle,
1623
            0.0,
1✔
1624
            ExtensionObject.encode(client.getStaticSerializationContext(), eventFilter),
1✔
1625
            uint(10),
1✔
1626
            true);
1✔
1627

1628
    MonitoredItemCreateRequest request =
1✔
1629
        new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
1630

1631
    List<UaMonitoredItem> items =
1✔
1632
        subscription.createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get();
1✔
1633

1634
    // do something with the value updates
1635
    UaMonitoredItem monitoredItem = items.get(0);
1✔
1636

1637
    monitoredItem.setEventConsumer(
1✔
1638
        (item, vs) -> {
1639
          internalLogger.info("Event Received from {}", item.getReadValueId().getNodeId());
1✔
1640

1641
          StringBuilder eventText = new StringBuilder();
1✔
1642

1643
          ByteString eventId;
1644
          NodeId eventType;
1645
          UShort eventSeverity;
1646
          DateTime eventTime;
1647
          LocalizedText eventMessage;
1648

1649
          for (int i = 0; i < vs.length; i++) {
1✔
1650
            internalLogger.info("\tvariant[{}]: {}", i, vs[i].getValue());
1✔
1651
          }
1652

1653
          eventId = (ByteString) vs[0].getValue();
1✔
1654
          eventType = (NodeId) vs[1].getValue();
1✔
1655
          eventSeverity = (UShort) vs[2].getValue();
1✔
1656
          eventTime = (DateTime) vs[3].getValue();
1✔
1657
          eventMessage = (LocalizedText) vs[4].getValue();
1✔
1658

1659
          //          FIXME:Map these values to YAMCS API
1660
          eventText.append("eventId:" + eventId);
1✔
1661
          eventText.append("\n");
1✔
1662
          eventText.append("eventType:" + eventType);
1✔
1663
          eventText.append("\n");
1✔
1664
          eventText.append("eventSeverity:" + eventSeverity);
1✔
1665
          eventText.append("\n");
1✔
1666
          eventText.append("eventTime:" + eventTime);
1✔
1667
          eventText.append("\n");
1✔
1668
          eventText.append("eventMessage:" + eventMessage);
1✔
1669
          org.yamcs.yarch.protobuf.Db.Event ev =
1670
              Event.newBuilder()
1✔
1671
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1672
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1673
                  .setSource(this.linkName)
1✔
1674
                  .setType(this.linkName)
1✔
1675
                  .setMessage(eventText.toString())
1✔
1676
                  .setSeverity(EventSeverity.INFO)
1✔
1677
                  .build();
1✔
1678
          eventProducer.sendEvent(ev);
1✔
1679
        });
1✔
1680
  }
1✔
1681

1682
  @Override
1683
  public void setupSystemParameters(SystemParametersService sysParamService) {
1684
    super.setupSystemParameters(sysParamService);
1✔
1685
    OPCUAStatusParam =
1✔
1686
        sysParamService.createEnumeratedSystemParameter(
1✔
1687
            linkName + "/OPCUAStatusParam",
1688
            OPCUAStatus.class,
1689
            "The current status of OPCUA client");
1690
    EnumeratedParameterType spLinkStatusType =
1✔
1691
        (EnumeratedParameterType) OPCUAStatusParam.getParameterType();
1✔
1692
    spLinkStatusType
1✔
1693
        .enumValue(OPCUAStatus.OPCUA_INIT_CONFIG.name())
1✔
1694
        .setDescription(
1✔
1695
            "This link is in the configuration stage(Configuring OPCUA parameters such as certificates)");
1696
    spLinkStatusType
1✔
1697
        .enumValue(OPCUAStatus.OPCUA_INIT_TREE.name())
1✔
1698
        .setDescription(
1✔
1699
            "The link is parsing the OPCUA Tree and mapping them to PVs."
1700
                + " Depending on configuration, this can take a while.");
1701
    spLinkStatusType
1✔
1702
        .enumValue(OPCUAStatus.OPCUA_INIT_EVENTS.name())
1✔
1703
        .setDescription("The link is configuring and subscribing to OPCUA events");
1✔
1704
    spLinkStatusType
1✔
1705
        .enumValue(OPCUAStatus.OPCUA_INIT_DATA_SUBSCRIPTION.name())
1✔
1706
        .setDescription(
1✔
1707
            "The link is creating subscriptions for each node that was parsed from the tree"
1708
                + "that has a Value attribute.");
1709
    spLinkStatusType
1✔
1710
        .enumValue(OPCUAStatus.OPCUA_INIT_ALL_DATA_QUERY.name())
1✔
1711
        .setDescription(
1✔
1712
            "The link is querying all attributes of all parsed nodes."
1713
                + "This is can be configured to be done at startup.");
1714
    spLinkStatusType
1✔
1715
        .enumValue(OPCUAStatus.OPCUA_OK.name())
1✔
1716
        .setDescription(
1✔
1717
            "The link is done with all OPCUA initialization. It is in an usable state.");
1718

1719
    OPCUAActiveSubsParam =
1✔
1720
        sysParamService.createSystemParameter(
1✔
1721
            linkName + "/OPCUAActiveSubs",
1722
            Type.UINT64,
1723
            "The total number of active opcua subscriptions");
1724
  }
1✔
1725

1726
  @Override
1727
  public List<ParameterValue> getSystemParameters() {
1728
    long time = getCurrentTime();
1✔
1729

1730
    ArrayList<ParameterValue> list = new ArrayList<>();
1✔
1731

1732
    list.add(
1✔
1733
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1734
            OPCUAStatusParam, time, currentOPCUAStatus));
1735

1736
    list.add(
1✔
1737
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1738
            OPCUAActiveSubsParam, time, OPCUAActiveSubs.get()));
1✔
1739
    try {
1740
      super.collectSystemParameters(time, list);
1✔
1741
    } catch (Exception e) {
×
1742
      log.error("Exception caught when collecting link system parameters", e);
×
1743
    }
1✔
1744
    return list;
1✔
1745
  }
1746
}
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