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

WindhoverLabs / yamcs-opcua / #23

12 Jul 2024 12:04AM UTC coverage: 83.756% (-0.3%) from 84.051%
#23

push

lorenzo-gomez-windhover
-Update docs

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

57 existing lines in 1 file now uncovered.

660 of 788 relevant lines covered (83.76%)

0.84 hits per line

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

83.46
/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

212
  private String rootIdentifier; // Relative to the rootNamespaceIndex
213

214
  private IdType rootIdentifierType; // Relative to the rootNamespaceIndex
215

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

227
  private OpcUaClient client;
228

229
  protected AtomicLong inCount = new AtomicLong(0);
1✔
230

231
  private Status linkStatus = Status.OK;
1✔
232

233
  /* Configuration Parameters */
234

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

241
  private ArrayList<NodePath> relativeNodePaths = new ArrayList<NodePath>();
1✔
242

243
  private final AtomicLong clientHandles = new AtomicLong(1L);
1✔
244

245
  /* System parameters*/
246

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

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

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

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

270
          return jsonObject;
1✔
271
        }
272
      };
273

274
  public OPCUAStatus getCurrentOPCUAStatus() {
275
    return currentOPCUAStatus;
1✔
276
  }
277

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

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

293
    Spec rootNodeIDSpec = new Spec();
1✔
294

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

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

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

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

313
    return spec;
1✔
314
  }
315

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

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

332
    this.opcuaStreamName = config.getString("opcuaStream");
1✔
333

334
    this.opcuaStream = getStream(ydb, opcuaStreamName);
1✔
335

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

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

341
    this.queryAllNodesAtStartup = config.getBoolean("queryAllNodesAtStartup", false);
1✔
342

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

345
    rootNamespaceIndex = (int) root.get("namespaceIndex");
1✔
346

347
    rootIdentifier = (String) root.get("identifier");
1✔
348
    rootIdentifierType = IdType.valueOf((String) root.get("identifierType"));
1✔
349

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

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

359
    this.mdb = YamcsServer.getServer().getInstance(yamcsInstance).getXtceDb();
1✔
360

361
    outputFile = config.getString("xtceOutputFile");
1✔
362
    publishInterval = config.getInt("publishInterval");
1✔
363
  }
1✔
364

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

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

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

395
    throw new NotFoundException("No such space system");
×
396
  }
397

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

407
    try {
408

409
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_TREE;
1✔
410

411
      browseOPCUATree(client);
1✔
412

413
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_GENERATE_XTCE;
1✔
414

415
      var spaceSystem = verifySpaceSystem(mdb, "/");
1✔
416

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

419
      BufferedWriter writer = null;
1✔
420

421
      if (outputFile != null) {
1✔
422
        writer =
1✔
423
            Files.newBufferedWriter(
1✔
424
                Paths.get(outputFile),
1✔
425
                StandardOpenOption.CREATE,
426
                StandardOpenOption.TRUNCATE_EXISTING);
427
      } else writer = null;
×
428

429
      writer.write(xtce);
1✔
430

431
      writer.flush();
1✔
432
      writer.close();
1✔
433

434
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_EVENTS;
1✔
435
      subscribeToEvents(client);
1✔
436

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

451
  private void opcuaClientConnect() throws Exception {
452
    client = configureClient();
1✔
453
    connectToOPCUAServer(client);
1✔
454
  }
1✔
455

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

465
      stream = ydb.getStream(streamName);
1✔
466
    }
467
    return stream;
1✔
468
  }
469

470
  @Override
471
  public void doDisable() {
472
    /* If the thread is created, interrupt it. */
473
    if (thread != null) {
1✔
474
      thread.interrupt();
1✔
475
    }
476

477
    linkStatus = Status.DISABLED;
1✔
478
  }
1✔
479

480
  @Override
481
  public void doEnable() {
482
    linkStatus = Status.OK;
1✔
483
  }
1✔
484

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

494
  @Override
495
  public Status connectionStatus() {
496
    return linkStatus;
1✔
497
  }
498

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

520
    /* Create and start the new thread. */
521
    thread = new Thread(this);
1✔
522
    thread.setName(this.getClass().getSimpleName() + "-" + linkName);
1✔
523
    thread.start();
1✔
524

525
    notifyStarted();
1✔
526
  }
1✔
527

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

540
    notifyStopped();
1✔
541
  }
1✔
542

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

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

565
    tdef = gftdef.copy();
1✔
566
    long gentime = timeService.getMissionTime();
1✔
567
    cols.add(gentime);
1✔
568
    cols.add(parametersNamespace);
1✔
569
    cols.add(0);
1✔
570
    cols.add(gentime);
1✔
571

572
    int columnCount = 0;
1✔
573

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

584
    for (NodeId nId : nodeSet) {
1✔
585
      UaNode node;
586

587
      try {
588
        node = client.getAddressSpace().getNode(nId);
1✔
589

590
        DataValue nodeClass = node.readAttribute(AttributeId.NodeClass);
1✔
591

592
        switch (NodeClass.from((int) nodeClass.getValue().getValue())) {
1✔
593
          case Variable:
594
            for (AttributeId attr : AttributeId.VARIABLE_ATTRIBUTES) {
1✔
595

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

598
              if (p.getParameterType() == null) {
1✔
599
                // FIXME:Add log message
600
                continue;
×
601
              }
602

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

613
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
614
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
615
                  }
616
                  break;
1✔
617
                case DOUBLE:
618
                  {
619
                    double value = 0;
1✔
620
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
621
                      //                      value = null;
622
                      //                            FIXME:Log warning
623
                    } else {
624
                      value = (double) 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));
1✔
629
                  }
630
                  break;
1✔
631
                case FLOAT:
632
                  {
UNCOV
633
                    float value = 0;
×
UNCOV
634
                    if (node.readAttribute(attr).getValue().isNull()) {
×
635
                      //                      value = null;
636
                      //                            FIXME:Log warning
637
                    } else {
UNCOV
638
                      value = (float) node.readAttribute(attr).getValue().getValue();
×
639
                    }
640

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

655
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
656
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
657
                  }
658
                  break;
1✔
659
                case SINT64:
660
                  {
661
                    long value = 0;
1✔
662
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
663
                      //                      value = null;
664
                      //                            FIXME:Log warning
665
                    } else {
666
                      value = (long) node.readAttribute(attr).getValue().getValue();
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 STRING:
674
                  {
675
                    String value = "";
1✔
676
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
677
                      value = "NULL";
1✔
678
                    } else {
679
                      value = node.readAttribute(attr).getValue().getValue().toString();
1✔
680
                    }
681

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

UNCOV
696
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
697
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
698
                  }
UNCOV
699
                  break;
×
700
                case UINT64:
701
                  {
702
                    long value = 0;
×
UNCOV
703
                    if (node.readAttribute(attr).getValue().isNull()) {
×
704
                      //                      value = null;
705
                      //                            FIXME:Log warning
706
                    } else {
UNCOV
707
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
708
                    }
709

UNCOV
710
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
711
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
712
                  }
UNCOV
713
                  break;
×
714
                default:
715
                  break;
716
              }
717

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

720
              columnCount++;
1✔
721
            }
1✔
722
            break;
1✔
723
          default:
724
            break;
725
        }
726

UNCOV
727
      } catch (UaException e) {
×
728
        // TODO Auto-generated catch block
UNCOV
729
        e.printStackTrace();
×
UNCOV
730
        continue;
×
731
      }
1✔
732
    }
1✔
733

734
    /**
735
     * FIXME:Need to come up with a mechanism to not update certain values that are up to date...
736
     * The more I think about it, it might make sense to have "static" and "runtime" namespaces
737
     */
738
    pushTuple(tdef, cols);
×
739

UNCOV
740
    inCount.getAndAdd(columnCount);
×
UNCOV
741
  }
×
742

743
  private synchronized void pushTuple(TupleDefinition tdef, List<Object> cols) {
744
    Tuple t;
745
    t = new Tuple(tdef, cols);
1✔
746
    opcuaStream.emitTuple(t);
1✔
747
  }
1✔
748

749
  private static ParameterType getOrCreateType(
750
      XtceDb mdb, String name, Supplier<ParameterType.Builder<?>> supplier) {
751

752
    String fqn = XtceDb.YAMCS_SPACESYSTEM_NAME + NameDescription.PATH_SEPARATOR + name;
1✔
753
    ParameterType ptype = mdb.getParameterType(fqn);
1✔
754
    if (ptype != null) {
1✔
755
      return ptype;
1✔
756
    }
757
    ParameterType.Builder<?> typeb = supplier.get().setName(name);
1✔
758

759
    ptype = typeb.build();
1✔
760
    ((NameDescription) ptype).setQualifiedName(fqn);
1✔
761

762
    return mdb.addSystemParameterType(ptype);
1✔
763
  }
764

765
  public static ParameterType getBasicType(XtceDb mdb, Type type) {
766
    ParameterType pType = null;
1✔
767
    switch (type) {
1✔
768
      case BOOLEAN:
769
        return getOrCreateType(mdb, "boolean", () -> new BooleanParameterType.Builder());
1✔
770
      case STRING:
771
        return getOrCreateType(mdb, "string", () -> new StringParameterType.Builder());
1✔
772

773
      case FLOAT:
774
        return getOrCreateType(
1✔
775
            mdb, "float32", () -> new FloatParameterType.Builder().setSizeInBits(32));
1✔
776
      case DOUBLE:
777
        return getOrCreateType(
1✔
778
            mdb, "float64", () -> new FloatParameterType.Builder().setSizeInBits(64));
1✔
779
      case SINT32:
780
        return getOrCreateType(
1✔
781
            mdb,
782
            "sint32",
UNCOV
783
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(true));
×
784
      case SINT64:
785
        return getOrCreateType(
1✔
786
            mdb,
787
            "sint64",
788
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(true));
1✔
789
      case UINT32:
UNCOV
790
        return getOrCreateType(
×
791
            mdb,
792
            "uint32",
UNCOV
793
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(false));
×
794
      case UINT64:
795
        return getOrCreateType(
1✔
796
            mdb,
797
            "uint64",
UNCOV
798
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(false));
×
799
      default:
800
        break;
801
    }
802

UNCOV
803
    return pType;
×
804
  }
805

806
  public static ParameterValue getNewPv(Parameter parameter, long time) {
807
    ParameterValue pv = new ParameterValue(parameter);
1✔
808
    pv.setAcquisitionTime(time);
1✔
809
    pv.setGenerationTime(time);
1✔
810
    return pv;
1✔
811
  }
812

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

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

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

831
  public static ParameterValue getPV(Parameter parameter, long time, boolean v) {
832
    ParameterValue pv = getNewPv(parameter, time);
1✔
833
    pv.setEngValue(ValueUtility.getBooleanValue(v));
1✔
834
    return pv;
1✔
835
  }
836

837
  public static ParameterValue getPV(Parameter parameter, long time, long v) {
838
    ParameterValue pv = getNewPv(parameter, time);
1✔
839
    pv.setEngValue(ValueUtility.getSint64Value(v));
1✔
840
    return pv;
1✔
841
  }
842

843
  @Override
844
  public Status getLinkStatus() {
845
    return linkStatus;
1✔
846
  }
847

848
  @Override
849
  public boolean isDisabled() {
850
    // TODO Auto-generated method stub
851
    return linkStatus == Status.DISABLED;
1✔
852
  }
853

854
  @Override
855
  public long getDataInCount() {
856
    // TODO Auto-generated method stub
857
    return inCount.get();
1✔
858
  }
859

860
  @Override
861
  public long getDataOutCount() {
862
    // TODO Auto-generated method stub
863
    return 0;
1✔
864
  }
865

866
  @Override
867
  public void resetCounters() {
868
    // TODO Auto-generated method stub
869
    inCount.set(0);
1✔
870
  }
1✔
871

872
  /**
873
   * Selects first non-secured endpoint from endpoints found at discover URL. At the moment secured
874
   * endpoints are not supported.
875
   *
876
   * @return
877
   * @throws Exception
878
   */
879
  private OpcUaClient configureClient() throws Exception {
880

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

883
    //    FIXME:At the moment, we do not support certificates...
884
    EndpointDescription selectedEndpoint = null;
1✔
885
    for (var endpoint : endpoints) {
1✔
886
      switch (endpoint.getSecurityMode()) {
1✔
887
        case Invalid:
888
          //                        FIXME:Add log message
UNCOV
889
          break;
×
890
        case None:
891
          //                        FIXME:Add log message
892
          selectedEndpoint = endpoint;
1✔
893
          break;
1✔
894
          //                        FIXME:Add log message
895
        case Sign:
UNCOV
896
          break;
×
897
        case SignAndEncrypt:
898
          //                        FIXME:Add log message
UNCOV
899
          break;
×
900
        default:
901
          break;
902
      }
903

904
      if (selectedEndpoint != null) {
1✔
905
        break;
1✔
906
      }
UNCOV
907
    }
×
908

909
    if (selectedEndpoint == null) {
1✔
UNCOV
910
      throw new Exception("No viable endpoint found from list:" + endpoints);
×
911
    }
912

913
    OpcUaClientConfig builder = OpcUaClientConfig.builder().setEndpoint(selectedEndpoint).build();
1✔
914

915
    return OpcUaClient.create(builder);
1✔
916
  }
917

918
  /**
919
   * Browse all nodes starting from browseRoot.
920
   *
921
   * @param indent
922
   * @param client
923
   * @param browseRoot
924
   */
925
  private void browseNodeWithReferences(String indent, OpcUaClient client, NodeId browseRoot) {
926
    BrowseDescription browse =
1✔
927
        new BrowseDescription(
928
            browseRoot,
929
            BrowseDirection.Forward,
930
            Identifiers.References,
931
            true,
1✔
932
            uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
1✔
933
            uint(BrowseResultMask.All.getValue()));
1✔
934

935
    try {
936

937
      BrowseResult browseResult = client.browse(browse).get();
1✔
938

939
      List<ReferenceDescription> references = toList(browseResult.getReferences());
1✔
940

941
      if (references.isEmpty()) {
1✔
942
        //              FIXME:Add log here
943

944
        return;
1✔
945
      }
946

947
      for (ReferenceDescription rd : references) {
1✔
948
        Object desc = null;
1✔
949
        Object value = null;
1✔
950
        UaNode node = null;
1✔
951
        try {
952

953
          node =
1✔
954
              client
955
                  .getAddressSpace()
1✔
956
                  .getNode(rd.getNodeId().toNodeId(client.getNamespaceTable()).get());
1✔
957
          DataValue attr = node.readAttribute(AttributeId.Description);
1✔
958
          desc = attr.getValue().getValue();
1✔
959

960
          attr = node.readAttribute(AttributeId.Value);
1✔
961

962
          value = attr.getValue();
1✔
963

UNCOV
964
        } catch (UaException e) {
×
965
          // TODO Auto-generated catch block
UNCOV
966
          e.printStackTrace();
×
967
        }
1✔
968

969
        if (node != null) {
1✔
970
          addOPCUAPV(client, node);
1✔
971

972
          log.debug(
1✔
973
              "{} Node={}, Desc={}, Value={}", indent, rd.getBrowseName().getName(), desc, value);
1✔
974

975
          // recursively browse to children
976
          rd.getNodeId()
1✔
977
              .toNodeId(client.getNamespaceTable())
1✔
978
              .ifPresent(nodeId -> browseNodeWithReferences(indent + "  ", client, nodeId));
1✔
979
        }
980
      }
1✔
981

UNCOV
982
    } catch (InterruptedException e1) {
×
983
      // TODO Auto-generated catch block
UNCOV
984
      e1.printStackTrace();
×
UNCOV
985
    } catch (ExecutionException e1) {
×
986
      // TODO Auto-generated catch block
UNCOV
987
      e1.printStackTrace();
×
988
    }
1✔
989
  }
1✔
990

991
  /**
992
   * Adds new PV with the name of node.
993
   *
994
   * @param client
995
   * @param node
996
   */
997
  private void addOPCUAPV(OpcUaClient client, UaNode node) {
998

999
    if (node.getBrowseName()
1✔
1000
        .getName()
1✔
1001
        .contains(Character.toString(NameDescription.PATH_SEPARATOR))) {
1✔
1002
      internalLogger.info(
1✔
1003
          "{} ignored since it contains a {} character",
1004
          node.getBrowseName().getName(),
1✔
1005
          Character.toString(NameDescription.PATH_SEPARATOR));
1✔
1006

1007
    } else {
1008

1009
      /**
1010
       * NOTE:For now we'll just flatten all the attributes instead of using an aggregate type for
1011
       * attributes
1012
       */
1013
      for (AttributeId attr : AttributeId.values()) {
1✔
1014

1015
        ParameterType ptype = OPCUAAttrTypeToParamType(attr, node);
1✔
1016

1017
        String opcuaTranslatedQName = translateNodeToParamQName(client, node, attr);
1✔
1018
        Parameter p = VariableParam.getForFullyQualifiedName(opcuaTranslatedQName);
1✔
1019

1020
        p.setParameterType(ptype);
1✔
1021

1022
        if (mdb.getParameter(p.getQualifiedName()) == null) {
1✔
1023
          log.debug("Adding OPCUA object as parameter to mdb:{}", p.getQualifiedName());
1✔
1024
          mdb.addParameter(p, true);
1✔
1025

1026
          nodeIDToParamsMap.put(new NodeIDAttrPair(node.getNodeId(), attr), (VariableParam) p);
1✔
1027
        }
1028
      }
1029
    }
1030
  }
1✔
1031

1032
  /**
1033
   * Map nodeID name to a qualified name that can be used for a YAMCS PV.
1034
   *
1035
   * @param client
1036
   * @param node
1037
   * @param attr
1038
   * @return
1039
   */
1040
  private String translateNodeToParamQName(OpcUaClient client, UaNode node, AttributeId attr) {
1041
    LocalizedText localizedDisplayName = null;
1✔
1042
    try {
1043

1044
      localizedDisplayName =
1✔
1045
          (LocalizedText) (node.readAttribute(AttributeId.DisplayName).getValue().getValue());
1✔
UNCOV
1046
    } catch (UaException e) {
×
1047
      // TODO Auto-generated catch block
UNCOV
1048
      e.printStackTrace();
×
1049
    }
1✔
1050
    String opcuaTranslatedQName =
1✔
1051
        qualifiedName(
1✔
1052
            parametersNamespace
1053
                + NameDescription.PATH_SEPARATOR
1054
                + node.getNodeId().toParseableString().replace(";", "-")
1✔
1055
                + NameDescription.PATH_SEPARATOR
1056
                + localizedDisplayName.getText(),
1✔
1057
            attr.toString());
1✔
1058

1059
    return opcuaTranslatedQName;
1✔
1060
  }
1061

1062
  /**
1063
   * Browse node at nodePath relative to browseRoot.
1064
   *
1065
   * @param indent
1066
   * @param client
1067
   * @param browseRoot
1068
   * @param nodePath in the format of "0:Root,0:Objects,2:HelloWorld,2:MyObject,2:Bar"
1069
   */
1070
  private void browsePath(String indent, OpcUaClient client, NodeId startingNode, String nodePath) {
1071
    internalLogger.info("Browsing at " + startingNode);
1✔
1072
    ArrayList<String> rPathTokens = new ArrayList<String>();
1✔
1073
    ArrayList<RelativePathElement> relaitivePathElements = new ArrayList<RelativePathElement>();
1✔
1074

1075
    for (var pathToken : nodePath.split(",")) {
1✔
1076
      rPathTokens.add(nodePath);
1✔
1077

1078
      int namespaceIndex = 0;
1✔
1079

1080
      String namespaceName = "";
1✔
1081

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

1084
      namespaceName = pathToken.split(":")[1];
1✔
1085

1086
      relaitivePathElements.add(
1✔
1087
          new RelativePathElement(
1088
              Identifiers.HierarchicalReferences,
1089
              false,
1✔
1090
              true,
1✔
1091
              new QualifiedName(namespaceIndex, namespaceName)));
1092
    }
1093

1094
    ArrayList<BrowsePath> list = new ArrayList<BrowsePath>();
1✔
1095

1096
    RelativePathElement[] elements = new RelativePathElement[relaitivePathElements.size()];
1✔
1097

1098
    relaitivePathElements.toArray(elements);
1✔
1099

1100
    list.add(new BrowsePath(startingNode, new RelativePath(elements)));
1✔
1101

1102
    TranslateBrowsePathsToNodeIdsResponse response = null;
1✔
1103
    try {
1104
      response = client.translateBrowsePaths(list).get();
1✔
UNCOV
1105
    } catch (InterruptedException e) {
×
1106
      // TODO Auto-generated catch block
UNCOV
1107
      e.printStackTrace();
×
UNCOV
1108
    } catch (ExecutionException e) {
×
1109
      // TODO Auto-generated catch block
UNCOV
1110
      e.printStackTrace();
×
1111
    }
1✔
1112

1113
    BrowsePathResult result = Arrays.asList(response.getResults()).get(0);
1✔
1114
    StatusCode statusCode = result.getStatusCode();
1✔
1115

1116
    if (statusCode.isBad()) {
1✔
1117
      log.warn("Bad status code:" + statusCode);
×
UNCOV
1118
      return;
×
1119
    } else if (statusCode.isUncertain()) {
1✔
UNCOV
1120
      log.warn("Uncertain status code:" + statusCode);
×
UNCOV
1121
      return;
×
1122
    }
1123

1124
    try {
1125
      UaNode node =
1✔
1126
          client
1127
              .getAddressSpace()
1✔
1128
              .getNode(
1✔
1129
                  result.getTargets()[0].getTargetId().toNodeId(client.getNamespaceTable()).get());
1✔
1130

1131
      addOPCUAPV(client, node);
1✔
UNCOV
1132
    } catch (UaException e) {
×
1133
      // TODO Auto-generated catch block
UNCOV
1134
      e.printStackTrace();
×
1135
    }
1✔
1136
  }
1✔
1137

1138
  private void createOPCUASubscriptions() {
1139
    createDataChangeListener();
1✔
1140
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
1141
    /**
1142
     * FIXME:This is super inefficient... The reason we collect these nodeIDs in a set is because
1143
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
1144
     * nodeID given how nodeIDToParamsMap is designed
1145
     */
1146
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
1147
      nodeSet.add(pair.nodeID);
1✔
1148
    }
1✔
1149

1150
    ArrayList<NodeId> variableNodes = new ArrayList<NodeId>();
1✔
1151
    for (NodeId id : nodeSet) {
1✔
1152
      Variant nodeClass = null;
1✔
1153
      try {
1154
        UaNode node = client.getAddressSpace().getNode(id);
1✔
1155

1156
        nodeClass = node.readAttribute(AttributeId.NodeClass).getValue();
1✔
1157

UNCOV
1158
      } catch (UaException e) {
×
1159
        // TODO Auto-generated catch block
UNCOV
1160
        e.printStackTrace();
×
1161
      }
1✔
1162
      if (nodeClass != null) {
1✔
1163
        //        try {
1164
        switch (NodeClass.from((int) nodeClass.getValue())) {
1✔
1165
            // As per the spec, the only thing we can subscribe to is Variables
1166
          case Variable:
1167
            variableNodes.add(id);
1✔
1168
            break;
1169
        }
1170
      }
1171
    }
1✔
1172

1173
    try {
1174
      List<ManagedDataItem> dataItems = opcuaSubscription.createDataItems(variableNodes);
1✔
1175
      for (var dataItem : dataItems) {
1✔
1176
        log.debug("Status code for dataItem:{}", dataItem.getStatusCode());
1✔
1177
        OPCUAActiveSubs.addAndGet(1);
1✔
1178
      }
1✔
1179
    } catch (UaException e) {
×
1180
      // TODO Auto-generated catch block
UNCOV
1181
      e.printStackTrace();
×
1182
    }
1✔
1183
  }
1✔
1184

1185
  public void connectToOPCUAServer(OpcUaClient client) throws Exception {
1186
    internalLogger.info("Connecting to OPCUA server...");
1✔
1187
    client.connect().get();
1✔
1188

1189
    addAction(startAction);
1✔
1190
    startAction.setEnabled(true);
1✔
1191
  }
1✔
1192

1193
  /**
1194
   * Browses the tree on the OPCUA server and maps them to YAMCS Parameters.
1195
   *
1196
   * @param client
1197
   */
1198
  private void browseOPCUATree(OpcUaClient client) {
1199
    // start browsing at root folder
1200
    internalLogger.info("Browsing OPCUA...");
1✔
1201
    for (var p : relativeNodePaths) {
1✔
1202
      int namespaceIndex = (int) p.rootNodeID.get("namespaceIndex");
1✔
1203
      String identifier = (String) p.rootNodeID.get("identifier");
1✔
1204
      IdType identifierType = IdType.valueOf((String) p.rootNodeID.get("identifierType"));
1✔
1205

1206
      browsePath(
1✔
1207
          endpointURL, client, getNewNodeID(identifierType, namespaceIndex, identifier), p.path);
1✔
1208
    }
1✔
1209

1210
    NodeId nodeID = null;
1✔
1211
    nodeID = getNewNodeID(rootIdentifierType, rootNamespaceIndex, rootIdentifier);
1✔
1212

1213
    //  FIXME:Make root default when no namespaceIndex/identifier pair is specified
1214
    browseNodeWithReferences("", client, nodeID);
1✔
1215
  }
1✔
1216

1217
  /**
1218
   * Get new OPCUA-compliant NodeID object that is created from NamespaceIndex and Identifier. At
1219
   * the moment only String and Numeric node ids are supported.
1220
   *
1221
   * @param rootIdentifierType
1222
   * @param NamespaceIndex
1223
   * @param Identifier
1224
   * @return
1225
   */
1226
  private NodeId getNewNodeID(IdType rootIdentifierType, int NamespaceIndex, String Identifier) {
1227
    NodeId nodeID = null;
1✔
1228
    switch (rootIdentifierType) {
1✔
1229
      case Guid:
1230
        //                FIXME
1231
        break;
×
1232
      case Numeric:
1233
        nodeID = new NodeId(NamespaceIndex, Integer.parseInt(Identifier));
1✔
1234
        break;
1✔
1235
      case Opaque:
1236
        //                FIXME
1237
        break;
×
1238
      case String:
1239
        nodeID = new NodeId(NamespaceIndex, Identifier);
×
1240
        break;
×
1241
      default:
1242
        break;
1243
    }
1244
    return nodeID;
1✔
1245
  }
1246

1247
  /** Data listener for realtime OPCUA server updates. */
1248
  private void createDataChangeListener() {
1249
    try {
1250
      opcuaSubscription = ManagedSubscription.create(client, publishInterval);
1✔
1251
    } catch (UaException e) {
×
1252
      // TODO Auto-generated catch block
1253
      e.printStackTrace();
×
1254
    }
1✔
1255
    opcuaSubscription.addDataChangeListener(
1✔
1256
        (items, values) -> {
1257
          for (int i = 0; i < items.size(); i++) {
1✔
1258
            NodeIDAttrPair nodeAttrKey =
1✔
1259
                new NodeIDAttrPair(items.get(i).getNodeId(), AttributeId.Value);
1✔
1260
            log.debug(
1✔
1261
                "subscription value received: item={}, value={}",
1262
                items.get(i).getNodeId(),
1✔
1263
                values.get(i).getValue());
1✔
1264

1265
            log.debug(
1✔
1266
                "Pushing new PV for param name {} which is mapped to NodeID {}",
1267
                nodeIDToParamsMap.get(nodeAttrKey),
1✔
1268
                items.get(i).getNodeId());
1✔
1269

1270
            TupleDefinition tdef = gftdef.copy();
1✔
1271
            List<Object> cols = new ArrayList<>(4 + 1);
1✔
1272
            //            FIXME: Add leap seconds.... as config or get it from YAMCS API.
1273
            long gentime =
1✔
1274
                values
1275
                    .get(i)
1✔
1276
                    .getSourceTime()
1✔
1277
                    .getJavaInstant()
1✔
1278
                    .plus(37, ChronoUnit.SECONDS)
1✔
1279
                    .toEpochMilli();
1✔
1280
            cols.add(gentime);
1✔
1281
            cols.add(parametersNamespace);
1✔
1282
            cols.add(0);
1✔
1283
            cols.add(gentime);
1✔
1284

1285
            /**
1286
             * TODO:Not sure if this is the best way to do this since the aggregate values will be
1287
             * partially updated. Another potential approach might be to decouple the live OPCUA
1288
             * data(subscriptions) via namespaces. For example; have a "special" namespace called
1289
             * "subscriptions" that ONLY gets updated with items. And maybe another namespace for
1290
             * static data...maybe.
1291
             *
1292
             * <p>Another option is to flatten everything and have no aggregate types at all. That
1293
             * approach might even simplify the code quite a bit...
1294
             *
1295
             * <p>Another question worth answering before moving forward is to find out whether or
1296
             * not it is concrete in the OPCUA protocol what data can change in real time and which
1297
             * data is "static". Not sure if there is any "static" data given that clients have the
1298
             * ability of writing to values... might be worth a test.
1299
             */
1300
            log.debug(
1✔
1301
                "Data({}) chnage triggered for {}",
1302
                values.get(i).getValue(),
1✔
1303
                nodeIDToParamsMap.get(nodeAttrKey));
1✔
1304

1305
            if (nodeIDToParamsMap.get(nodeAttrKey) == null) {
1✔
1306
              log.debug("No parameter mapping found for {}", nodeAttrKey.nodeID);
×
1307
              continue;
×
1308
            } else {
1309
              log.debug(
1✔
1310
                  String.format(
1✔
1311
                      "parameter mapping found for {} and {}",
1312
                      nodeAttrKey.nodeID,
1313
                      nodeAttrKey.attrID));
1314
            }
1315

1316
            if (values.get(i).getValue() != null && values.get(i).getValue().getValue() != null) {
1✔
1317

1318
              switch (nodeIDToParamsMap.get(nodeAttrKey).getParameterType().getValueType()) {
1✔
1319
                case BOOLEAN:
1320
                  {
1321
                    boolean value = (boolean) values.get(i).getValue().getValue();
1✔
1322

1323
                    tdef.addColumn(
1✔
1324
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1325
                        DataType.PARAMETER_VALUE);
1326
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1327
                  }
1328
                  break;
1✔
1329
                case DOUBLE:
1330
                  {
1331
                    double value = (double) values.get(i).getValue().getValue();
1✔
1332

1333
                    tdef.addColumn(
1✔
1334
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1335
                        DataType.PARAMETER_VALUE);
1336
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1337
                  }
1338
                  break;
1✔
1339
                case FLOAT:
1340
                  {
UNCOV
1341
                    float value = (float) values.get(i).getValue().getValue();
×
1342

UNCOV
1343
                    tdef.addColumn(
×
UNCOV
1344
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1345
                        DataType.PARAMETER_VALUE);
UNCOV
1346
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1347
                  }
UNCOV
1348
                  break;
×
1349
                case SINT32:
1350
                  {
1351
                    int value = (int) values.get(i).getValue().getValue();
1✔
1352
                    tdef.addColumn(
1✔
1353
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1354
                        DataType.PARAMETER_VALUE);
1355
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1356
                  }
1357
                  break;
1✔
1358
                case SINT64:
1359
                  {
1360
                    long value = (long) values.get(i).getValue().getValue();
1✔
1361

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

1372
                    tdef.addColumn(
1✔
1373
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1374
                        DataType.PARAMETER_VALUE);
1375
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1376
                  }
1377
                  break;
1✔
1378
                case TIMESTAMP:
1379
                  {
1380
                    String value = (String) values.get(i).getValue().getValue();
×
1381

1382
                    tdef.addColumn(
×
1383
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1384
                        DataType.PARAMETER_VALUE);
1385
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1386
                  }
1387
                  break;
×
1388
                case UINT32:
1389
                  {
1390
                    int value = (int) values.get(i).getValue().getValue();
×
1391
                    tdef.addColumn(
×
1392
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1393
                        DataType.PARAMETER_VALUE);
1394
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1395
                  }
1396
                  break;
×
1397
                case UINT64:
1398
                  {
1399
                    long value = (long) values.get(i).getValue().getValue();
×
1400

1401
                    tdef.addColumn(
×
1402
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1403
                        DataType.PARAMETER_VALUE);
1404
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1405
                  }
1406
                  break;
×
1407
                default:
1408
                  break;
1409
              }
1410

1411
              pushTuple(tdef, cols);
1✔
1412

1413
              inCount.getAndAdd(1);
1✔
1414
            } else {
1415
              // TODO:Add some type emptyValue count for OPS.
1416
              log.warn(
1✔
1417
                  "Data chnage triggered for {}, but it empty. This should not happen.",
1418
                  nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName());
1✔
1419
            }
1420
          }
1421
        });
1✔
1422
  }
1✔
1423

1424
  /**
1425
   * This method is here for future growth in case we find there is a benefit to using aggregate
1426
   * types
1427
   */
1428
  private void createOPCUAAttrAggregateType() {
1429

1430
    AggregateParameterType.Builder opcuaAttrsTypeBuidlder = new AggregateParameterType.Builder();
1✔
1431

1432
    opcuaAttrsType = new AggregateParameterType.Builder().setName("OPCUObjectAttributes").build();
1✔
1433

1434
    opcuaAttrsTypeBuidlder.setName("OPCUObjectAttributes");
1✔
1435
    for (AttributeId attr : AttributeId.values()) {
1✔
1436
      opcuaAttrsTypeBuidlder.addMember(new Member(attr.toString(), getBasicType(mdb, Type.STRING)));
1✔
1437
    }
1438

1439
    opcuaAttrsType = opcuaAttrsTypeBuidlder.build();
1✔
1440
    ((NameDescription) opcuaAttrsType)
1✔
1441
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaAttrsType.getName()));
1✔
1442
  }
1✔
1443

1444
  /**
1445
   * This method is here for future growth in case we find there is a benefit to using aggregate
1446
   * types
1447
   */
1448
  private void createOPCUANodeIdTypes() {
1449
    AggregateParameterType.Builder opcuaAttrsNumericNodeIdBuidlder =
1✔
1450
        new AggregateParameterType.Builder();
1451

1452
    opcuaAttrsNumericNodeIdBuidlder.setName("OPCUA_Numeric_NodeId");
1✔
1453
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1454
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1455
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1456
        new Member("identifier", getBasicType(mdb, Type.UINT64)));
1✔
1457

1458
    opcuaNodeIdNumericType = opcuaAttrsNumericNodeIdBuidlder.build();
1✔
1459
    ((NameDescription) opcuaNodeIdNumericType)
1✔
1460
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdNumericType.getName()));
1✔
1461

1462
    AggregateParameterType.Builder opcuaAttrsTypeStringBuidlder =
1✔
1463
        new AggregateParameterType.Builder();
1464

1465
    opcuaAttrsTypeStringBuidlder.setName("OPCUA_String_NodeId");
1✔
1466
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1467
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1468
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1469
        new Member("identifier", getBasicType(mdb, Type.STRING)));
1✔
1470

1471
    opcuaNodeIdStringType = opcuaAttrsTypeStringBuidlder.build();
1✔
1472
    ((NameDescription) opcuaNodeIdStringType)
1✔
1473
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdStringType.getName()));
1✔
1474

1475
    mdb.addParameterType(opcuaNodeIdNumericType, true);
1✔
1476
    mdb.addParameterType(opcuaNodeIdStringType, true);
1✔
1477
  }
1✔
1478

1479
  /**
1480
   * Get new ParameterType for the specified attribute of the node. Particularly useful for Value
1481
   * attributes of nodes.
1482
   *
1483
   * @param attr
1484
   * @param node
1485
   * @return
1486
   */
1487
  private ParameterType OPCUAAttrTypeToParamType(AttributeId attr, UaNode node) {
1488
    ParameterType pType = null;
1✔
1489

1490
    switch (attr) {
1✔
1491
      case AccessLevel:
1492
        pType = getBasicType(mdb, Type.STRING);
1✔
1493
        break;
1✔
1494
      case ArrayDimensions:
1495
        pType = getBasicType(mdb, Type.STRING);
1✔
1496
        break;
1✔
1497
      case BrowseName:
1498
        pType = getBasicType(mdb, Type.STRING);
1✔
1499
        break;
1✔
1500
      case ContainsNoLoops:
1501
        pType = getBasicType(mdb, Type.STRING);
1✔
1502
        break;
1✔
1503
      case DataType:
1504
        pType = getBasicType(mdb, Type.STRING);
1✔
1505
        break;
1✔
1506
      case Description:
1507
        pType = getBasicType(mdb, Type.STRING);
1✔
1508
        break;
1✔
1509
      case DisplayName:
1510
        pType = getBasicType(mdb, Type.STRING);
1✔
1511
        break;
1✔
1512
      case EventNotifier:
1513
        pType = getBasicType(mdb, Type.STRING);
1✔
1514
        break;
1✔
1515
      case Executable:
1516
        pType = getBasicType(mdb, Type.STRING);
1✔
1517
        break;
1✔
1518
      case Historizing:
1519
        pType = getBasicType(mdb, Type.STRING);
1✔
1520
        break;
1✔
1521
      case InverseName:
1522
        pType = getBasicType(mdb, Type.STRING);
1✔
1523
        break;
1✔
1524
      case IsAbstract:
1525
        pType = getBasicType(mdb, Type.STRING);
1✔
1526
        break;
1✔
1527
      case MinimumSamplingInterval:
1528
        pType = getBasicType(mdb, Type.STRING);
1✔
1529
        break;
1✔
1530
      case NodeClass:
1531
        pType = getBasicType(mdb, Type.STRING);
1✔
1532
        break;
1✔
1533
      case NodeId:
1534
        pType = getBasicType(mdb, Type.STRING);
1✔
1535
        break;
1✔
1536
      case Symmetric:
1537
        pType = getBasicType(mdb, Type.STRING);
1✔
1538
        break;
1✔
1539
      case UserAccessLevel:
1540
        pType = getBasicType(mdb, Type.STRING);
1✔
1541
        break;
1✔
1542
      case UserExecutable:
1543
        pType = getBasicType(mdb, Type.STRING);
1✔
1544
        break;
1✔
1545
      case UserWriteMask:
1546
        pType = getBasicType(mdb, Type.STRING);
1✔
1547
        break;
1✔
1548
      case Value:
1549
        try {
1550

1551
          var value = node.readAttribute(attr).getValue();
1✔
1552

1553
          if (value.isNotNull()) {
1✔
1554

1555
            Object valueObject = value.getValue();
1✔
1556

1557
            if (valueObject instanceof Short) {
1✔
1558
              pType = getBasicType(mdb, Type.SINT32);
1✔
1559
            } else if (valueObject instanceof Integer) {
1✔
1560
              pType = getBasicType(mdb, Type.SINT32);
1✔
1561

1562
            } else if (valueObject instanceof Long) {
1✔
1563
              pType = getBasicType(mdb, Type.SINT64);
1✔
1564
            } else if (valueObject instanceof Double) {
1✔
1565
              pType = getBasicType(mdb, Type.DOUBLE);
1✔
1566
            } else if (valueObject instanceof Float) {
1✔
1567
              pType = getBasicType(mdb, Type.FLOAT);
1✔
1568
            } else if (valueObject instanceof Character) {
1✔
1569
              pType = getBasicType(mdb, Type.STRING);
×
1570
            } else if (valueObject instanceof String) {
1✔
1571
              pType = getBasicType(mdb, Type.STRING);
1✔
1572

1573
            } else if (valueObject instanceof Boolean) {
1✔
1574
              pType = getBasicType(mdb, Type.BOOLEAN);
1✔
1575
            } else {
1576
              pType = getBasicType(mdb, Type.STRING);
1✔
1577
            }
1578
          } else {
1✔
1579
            pType = getBasicType(mdb, Type.STRING);
1✔
1580
          }
1581

1582
        } catch (UaException e) {
×
1583
          // TODO Auto-generated catch block
1584
          e.printStackTrace();
×
1585
          //                        FIXME:Add log message
1586
        }
1✔
1587
        break;
×
1588
      case ValueRank:
1589
        pType = getBasicType(mdb, Type.STRING);
1✔
1590
        break;
1✔
1591
      case WriteMask:
1592
        pType = getBasicType(mdb, Type.STRING);
1✔
1593
        break;
1✔
1594
      default:
1595
        break;
1596
    }
1597

1598
    return pType;
1✔
1599
  }
1600

1601
  private void subscribeToEvents(OpcUaClient client)
1602
      throws InterruptedException, ExecutionException {
1603
    // create a subscription and a monitored item
1604
    UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
1✔
1605

1606
    ReadValueId readValueId =
1✔
1607
        new ReadValueId(
1608
            Identifiers.Server, AttributeId.EventNotifier.uid(), null, QualifiedName.NULL_VALUE);
1✔
1609

1610
    // client handle must be unique per item
1611
    UInteger clientHandle = uint(clientHandles.getAndIncrement());
1✔
1612

1613
    EventFilter eventFilter =
1✔
1614
        new EventFilter(
1615
            new SimpleAttributeOperand[] {
1616
              new SimpleAttributeOperand(
1617
                  Identifiers.BaseEventType,
1618
                  new QualifiedName[] {new QualifiedName(0, "EventId")},
1619
                  AttributeId.Value.uid(),
1✔
1620
                  null),
1621
              new SimpleAttributeOperand(
1622
                  Identifiers.BaseEventType,
1623
                  new QualifiedName[] {new QualifiedName(0, "EventType")},
1624
                  AttributeId.Value.uid(),
1✔
1625
                  null),
1626
              new SimpleAttributeOperand(
1627
                  Identifiers.BaseEventType,
1628
                  new QualifiedName[] {new QualifiedName(0, "Severity")},
1629
                  AttributeId.Value.uid(),
1✔
1630
                  null),
1631
              new SimpleAttributeOperand(
1632
                  Identifiers.BaseEventType,
1633
                  new QualifiedName[] {new QualifiedName(0, "Time")},
1634
                  AttributeId.Value.uid(),
1✔
1635
                  null),
1636
              new SimpleAttributeOperand(
1637
                  Identifiers.BaseEventType,
1638
                  new QualifiedName[] {new QualifiedName(0, "Message")},
1639
                  AttributeId.Value.uid(),
1✔
1640
                  null)
1641
            },
1642
            new ContentFilter(null));
1643

1644
    MonitoringParameters parameters =
1✔
1645
        new MonitoringParameters(
1646
            clientHandle,
1647
            0.0,
1✔
1648
            ExtensionObject.encode(client.getStaticSerializationContext(), eventFilter),
1✔
1649
            uint(10),
1✔
1650
            true);
1✔
1651

1652
    MonitoredItemCreateRequest request =
1✔
1653
        new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
1654

1655
    List<UaMonitoredItem> items =
1✔
1656
        subscription.createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get();
1✔
1657

1658
    // do something with the value updates
1659
    UaMonitoredItem monitoredItem = items.get(0);
1✔
1660

1661
    monitoredItem.setEventConsumer(
1✔
1662
        (item, vs) -> {
1663
          internalLogger.info("Event Received from {}", item.getReadValueId().getNodeId());
1✔
1664

1665
          StringBuilder eventText = new StringBuilder();
1✔
1666

1667
          ByteString eventId;
1668
          NodeId eventType;
1669
          UShort eventSeverity;
1670
          DateTime eventTime;
1671
          LocalizedText eventMessage;
1672

1673
          for (int i = 0; i < vs.length; i++) {
1✔
1674
            internalLogger.info("\tvariant[{}]: {}", i, vs[i].getValue());
1✔
1675
          }
1676

1677
          eventId = (ByteString) vs[0].getValue();
1✔
1678
          eventType = (NodeId) vs[1].getValue();
1✔
1679
          eventSeverity = (UShort) vs[2].getValue();
1✔
1680
          eventTime = (DateTime) vs[3].getValue();
1✔
1681
          eventMessage = (LocalizedText) vs[4].getValue();
1✔
1682

1683
          //          FIXME:Map these values to YAMCS API
1684
          eventText.append("eventId:" + eventId);
1✔
1685
          eventText.append("\n");
1✔
1686
          eventText.append("eventType:" + eventType);
1✔
1687
          eventText.append("\n");
1✔
1688
          eventText.append("eventSeverity:" + eventSeverity);
1✔
1689
          eventText.append("\n");
1✔
1690
          eventText.append("eventTime:" + eventTime);
1✔
1691
          eventText.append("\n");
1✔
1692
          eventText.append("eventMessage:" + eventMessage);
1✔
1693
          org.yamcs.yarch.protobuf.Db.Event ev =
1694
              Event.newBuilder()
1✔
1695
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1696
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1697
                  .setSource(this.linkName)
1✔
1698
                  .setType(this.linkName)
1✔
1699
                  .setMessage(eventText.toString())
1✔
1700
                  .setSeverity(EventSeverity.INFO)
1✔
1701
                  .build();
1✔
1702
          eventProducer.sendEvent(ev);
1✔
1703
        });
1✔
1704
  }
1✔
1705

1706
  @Override
1707
  public void setupSystemParameters(SystemParametersService sysParamService) {
1708
    super.setupSystemParameters(sysParamService);
1✔
1709
    OPCUAStatusParam =
1✔
1710
        sysParamService.createEnumeratedSystemParameter(
1✔
1711
            linkName + "/OPCUAStatusParam",
1712
            OPCUAStatus.class,
1713
            "The current status of OPCUA client");
1714
    EnumeratedParameterType spLinkStatusType =
1✔
1715
        (EnumeratedParameterType) OPCUAStatusParam.getParameterType();
1✔
1716
    spLinkStatusType
1✔
1717
        .enumValue(OPCUAStatus.OPCUA_INIT_CONFIG.name())
1✔
1718
        .setDescription(
1✔
1719
            "This link is in the configuration stage(Configuring OPCUA parameters such as certificates)");
1720
    spLinkStatusType
1✔
1721
        .enumValue(OPCUAStatus.OPCUA_INIT_TREE.name())
1✔
1722
        .setDescription(
1✔
1723
            "The link is parsing the OPCUA Tree and mapping them to PVs."
1724
                + " Depending on configuration, this can take a while.");
1725
    spLinkStatusType
1✔
1726
        .enumValue(OPCUAStatus.OPCUA_INIT_EVENTS.name())
1✔
1727
        .setDescription("The link is configuring and subscribing to OPCUA events");
1✔
1728
    spLinkStatusType
1✔
1729
        .enumValue(OPCUAStatus.OPCUA_INIT_DATA_SUBSCRIPTION.name())
1✔
1730
        .setDescription(
1✔
1731
            "The link is creating subscriptions for each node that was parsed from the tree"
1732
                + "that has a Value attribute.");
1733
    spLinkStatusType
1✔
1734
        .enumValue(OPCUAStatus.OPCUA_INIT_ALL_DATA_QUERY.name())
1✔
1735
        .setDescription(
1✔
1736
            "The link is querying all attributes of all parsed nodes."
1737
                + "This is can be configured to be done at startup.");
1738
    spLinkStatusType
1✔
1739
        .enumValue(OPCUAStatus.OPCUA_OK.name())
1✔
1740
        .setDescription(
1✔
1741
            "The link is done with all OPCUA initialization. It is in an usable state.");
1742

1743
    OPCUAActiveSubsParam =
1✔
1744
        sysParamService.createSystemParameter(
1✔
1745
            linkName + "/OPCUAActiveSubs",
1746
            Type.UINT64,
1747
            "The total number of active opcua subscriptions");
1748
  }
1✔
1749

1750
  @Override
1751
  public List<ParameterValue> getSystemParameters() {
1752
    long time = getCurrentTime();
1✔
1753

1754
    ArrayList<ParameterValue> list = new ArrayList<>();
1✔
1755

1756
    list.add(
1✔
1757
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1758
            OPCUAStatusParam, time, currentOPCUAStatus));
1759

1760
    list.add(
1✔
1761
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1762
            OPCUAActiveSubsParam, time, OPCUAActiveSubs.get()));
1✔
1763
    try {
1764
      super.collectSystemParameters(time, list);
1✔
1765
    } catch (Exception e) {
×
1766
      log.error("Exception caught when collecting link system parameters", e);
×
1767
    }
1✔
1768
    return list;
1✔
1769
  }
1770
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc