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

WindhoverLabs / yamcs-opcua / #19

11 Jul 2024 06:21PM UTC coverage: 76.0% (+6.1%) from 69.919%
#19

push

lorenzo-gomez-windhover
-Cleanup

665 of 875 relevant lines covered (76.0%)

0.76 hits per line

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

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

34
package com.windhoverlabs.yamcs.opcua;
35

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

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

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

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

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

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

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

175
  class NodePath {
1✔
176
    String path;
177

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

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

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

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

198
  private String opcuaStreamName;
199

200
  private String parametersNamespace;
201
  XtceDb mdb;
202

203
  Stream opcuaStream;
204

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

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

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

214
  private int rootNamespaceIndex;
215

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

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

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

231
  private OpcUaClient client;
232

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

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

237
  /* Configuration Parameters */
238

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

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

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

249
  /* System parameters*/
250

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

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

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

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

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

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

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

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

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

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

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

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

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

317
    return spec;
1✔
318
  }
319

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

412
    try {
413

414
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_TREE;
1✔
415

416
      browseOPCUATree(client);
1✔
417

418
      currentOPCUAStatus = OPCUAStatus.OPCUA_INIT_GENERATE_XTCE;
1✔
419

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

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

424
      BufferedWriter writer = null;
1✔
425

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

434
      writer.write(xtce);
1✔
435

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

577
    int columnCount = 0;
1✔
578

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

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

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

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

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

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

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

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

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

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

627
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
628
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
629
                  }
630
                  break;
×
631
                case ARRAY:
632
                  {
633
                    String value = "";
×
634
                    if (node.readAttribute(attr).getValue().isNull()) {
×
635
                      value = "NULL";
×
636
                    } else {
637
                      value = node.readAttribute(attr).getValue().getValue().toString();
×
638
                    }
639

640
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
641
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
642
                  }
643
                  break;
×
644
                case BINARY:
645
                  {
646
                    String value = "";
×
647
                    if (node.readAttribute(attr).getValue().isNull()) {
×
648
                      value = "NULL";
×
649
                    } else {
650
                      value = node.readAttribute(attr).getValue().getValue().toString();
×
651
                    }
652

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

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

680
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
681
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
682
                  }
683
                  break;
1✔
684
                case ENUMERATED:
685
                  {
686
                    String value = "";
×
687
                    if (node.readAttribute(attr).getValue().isNull()) {
×
688
                      value = "NULL";
×
689
                    } else {
690
                      value = node.readAttribute(attr).getValue().getValue().toString();
×
691
                    }
692

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

707
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
708
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
709
                  }
710
                  break;
×
711
                case NONE:
712
                  {
713
                    String value = "";
×
714
                    if (node.readAttribute(attr).getValue().isNull()) {
×
715
                      value = "NULL";
×
716
                    } else {
717
                      value = node.readAttribute(attr).getValue().getValue().toString();
×
718
                    }
719

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

734
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
735
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
736
                  }
737
                  break;
1✔
738
                case SINT64:
739
                  {
740
                    long value = 0;
1✔
741
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
742
                      //                      value = null;
743
                      //                            FIXME:Log warning
744
                    } else {
745
                      value = (long) node.readAttribute(attr).getValue().getValue();
1✔
746
                    }
747

748
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
749
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
750
                  }
751
                  break;
1✔
752
                case STRING:
753
                  {
754
                    String value = "";
1✔
755
                    if (node.readAttribute(attr).getValue().isNull()) {
1✔
756
                      value = "NULL";
1✔
757
                    } else {
758
                      value = node.readAttribute(attr).getValue().getValue().toString();
1✔
759
                    }
760

761
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
1✔
762
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
1✔
763
                  }
764
                  break;
1✔
765
                case TIMESTAMP:
766
                  {
767
                    String value = "";
×
768
                    if (node.readAttribute(attr).getValue().isNull()) {
×
769
                      value = "NULL";
×
770
                    } else {
771
                      value = node.readAttribute(attr).getValue().getValue().toString();
×
772
                    }
773

774
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
775
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
776
                  }
777
                  break;
×
778
                case UINT32:
779
                  {
780
                    long value = 0;
×
781
                    if (node.readAttribute(attr).getValue().isNull()) {
×
782
                      //                      value = null;
783
                      //                            FIXME:Log warning
784
                    } else {
785
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
786
                    }
787

788
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
789
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
790
                  }
791
                  break;
×
792
                case UINT64:
793
                  {
794
                    long value = 0;
×
795
                    if (node.readAttribute(attr).getValue().isNull()) {
×
796
                      //                      value = null;
797
                      //                            FIXME:Log warning
798
                    } else {
799
                      value = (long) node.readAttribute(attr).getValue().getValue();
×
800
                    }
801

802
                    tdef.addColumn(p.getQualifiedName(), DataType.PARAMETER_VALUE);
×
803
                    cols.add(getPV(p, Instant.now().toEpochMilli(), value));
×
804
                  }
805
                  break;
×
806
                default:
807
                  break;
808
              }
809

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

812
              columnCount++;
1✔
813
            }
1✔
814
            break;
1✔
815
          default:
816
            break;
817
        }
818

819
      } catch (UaException e) {
×
820
        // TODO Auto-generated catch block
821
        e.printStackTrace();
×
822
        continue;
×
823
      }
1✔
824
    }
1✔
825

826
    /**
827
     * FIXME:Need to come up with a mechanism to not update certain values that are up to date...
828
     * The more I think about it, it might make sense to have "static" and "runtime" namespaces
829
     */
830
    pushTuple(tdef, cols);
×
831

832
    inCount.getAndAdd(columnCount);
×
833
  }
×
834

835
  private synchronized void pushTuple(TupleDefinition tdef, List<Object> cols) {
836
    Tuple t;
837
    t = new Tuple(tdef, cols);
1✔
838
    opcuaStream.emitTuple(t);
1✔
839
  }
1✔
840

841
  private static ParameterType getOrCreateType(
842
      XtceDb mdb, String name, Supplier<ParameterType.Builder<?>> supplier) {
843

844
    String fqn = XtceDb.YAMCS_SPACESYSTEM_NAME + NameDescription.PATH_SEPARATOR + name;
1✔
845
    ParameterType ptype = mdb.getParameterType(fqn);
1✔
846
    if (ptype != null) {
1✔
847
      return ptype;
1✔
848
    }
849
    ParameterType.Builder<?> typeb = supplier.get().setName(name);
1✔
850

851
    ptype = typeb.build();
1✔
852
    ((NameDescription) ptype).setQualifiedName(fqn);
1✔
853

854
    return mdb.addSystemParameterType(ptype);
1✔
855
  }
856

857
  public static ParameterType getBasicType(XtceDb mdb, Type type) {
858
    ParameterType pType = null;
1✔
859
    switch (type) {
1✔
860
      case BINARY:
861
        return getOrCreateType(mdb, "binary", () -> new BinaryParameterType.Builder());
×
862
      case BOOLEAN:
863
        return getOrCreateType(mdb, "boolean", () -> new BooleanParameterType.Builder());
1✔
864
      case STRING:
865
        pType = getOrCreateType(mdb, "string", () -> new StringParameterType.Builder());
1✔
866
        break;
1✔
867
      case FLOAT:
868
        return getOrCreateType(
1✔
869
            mdb, "float32", () -> new FloatParameterType.Builder().setSizeInBits(32));
1✔
870
      case DOUBLE:
871
        return getOrCreateType(
1✔
872
            mdb, "float64", () -> new FloatParameterType.Builder().setSizeInBits(64));
1✔
873
      case SINT32:
874
        return getOrCreateType(
1✔
875
            mdb,
876
            "sint32",
877
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(true));
×
878
      case SINT64:
879
        return getOrCreateType(
1✔
880
            mdb,
881
            "sint64",
882
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(true));
1✔
883
      case UINT32:
884
        return getOrCreateType(
×
885
            mdb,
886
            "uint32",
887
            () -> new IntegerParameterType.Builder().setSizeInBits(32).setSigned(false));
×
888
      case UINT64:
889
        return getOrCreateType(
1✔
890
            mdb,
891
            "uint64",
892
            () -> new IntegerParameterType.Builder().setSizeInBits(64).setSigned(false));
×
893
      case TIMESTAMP:
894
        return getOrCreateType(mdb, "time", () -> new AbsoluteTimeParameterType.Builder());
×
895
      case ENUMERATED:
896
        return getOrCreateType(mdb, "enum", () -> new EnumeratedParameterType.Builder());
×
897
      case AGGREGATE:
898
        break;
×
899
      case ARRAY:
900
        break;
×
901
      case NONE:
902
        break;
×
903
      default:
904
        break;
905
    }
906

907
    return pType;
1✔
908
  }
909

910
  public static ParameterValue getNewPv(Parameter parameter, long time) {
911
    ParameterValue pv = new ParameterValue(parameter);
1✔
912
    pv.setAcquisitionTime(time);
1✔
913
    pv.setGenerationTime(time);
1✔
914
    return pv;
1✔
915
  }
916

917
  public static ParameterValue getPV(Parameter parameter, long time, String v) {
918
    ParameterValue pv = getNewPv(parameter, time);
1✔
919
    pv.setEngValue(ValueUtility.getStringValue(v));
1✔
920
    return pv;
1✔
921
  }
922

923
  public static ParameterValue getPV(Parameter parameter, long time, double v) {
924
    ParameterValue pv = getNewPv(parameter, time);
1✔
925
    pv.setEngValue(ValueUtility.getDoubleValue(v));
1✔
926
    return pv;
1✔
927
  }
928

929
  public static ParameterValue getPV(Parameter parameter, long time, float v) {
930
    ParameterValue pv = getNewPv(parameter, time);
×
931
    pv.setEngValue(ValueUtility.getFloatValue(v));
×
932
    return pv;
×
933
  }
934

935
  public static ParameterValue getPV(Parameter parameter, long time, boolean v) {
936
    ParameterValue pv = getNewPv(parameter, time);
1✔
937
    pv.setEngValue(ValueUtility.getBooleanValue(v));
1✔
938
    return pv;
1✔
939
  }
940

941
  public static ParameterValue getPV(Parameter parameter, long time, long v) {
942
    ParameterValue pv = getNewPv(parameter, time);
1✔
943
    pv.setEngValue(ValueUtility.getSint64Value(v));
1✔
944
    return pv;
1✔
945
  }
946

947
  public static ParameterValue getUnsignedIntPV(Parameter parameter, long time, int v) {
948
    ParameterValue pv = getNewPv(parameter, time);
×
949
    pv.setEngValue(ValueUtility.getUint64Value(v));
×
950
    return pv;
×
951
  }
952

953
  @Override
954
  public Status getLinkStatus() {
955
    return linkStatus;
1✔
956
  }
957

958
  @Override
959
  public boolean isDisabled() {
960
    // TODO Auto-generated method stub
961
    return linkStatus == Status.DISABLED;
1✔
962
  }
963

964
  @Override
965
  public long getDataInCount() {
966
    // TODO Auto-generated method stub
967
    return inCount.get();
1✔
968
  }
969

970
  @Override
971
  public long getDataOutCount() {
972
    // TODO Auto-generated method stub
973
    return 0;
1✔
974
  }
975

976
  @Override
977
  public void resetCounters() {
978
    // TODO Auto-generated method stub
979
    inCount.set(0);
1✔
980
  }
1✔
981

982
  /**
983
   * Selects first non-secured endpoint from endpoints found at discover URL. At the moment secured
984
   * endpoints are not supported.
985
   *
986
   * @return
987
   * @throws Exception
988
   */
989
  private OpcUaClient configureClient() throws Exception {
990
    Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "client", "security");
1✔
991
    Files.createDirectories(securityTempDir);
1✔
992
    if (!Files.exists(securityTempDir)) {
1✔
993
      throw new Exception("unable to create security dir: " + securityTempDir);
×
994
    }
995

996
    File pkiDir = securityTempDir.resolve("pki").toFile();
1✔
997

998
    LoggerFactory.getLogger(getClass()).info("security dir: {}", securityTempDir.toAbsolutePath());
1✔
999
    LoggerFactory.getLogger(getClass()).info("security pki dir: {}", pkiDir.getAbsolutePath());
1✔
1000

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

1003
    //    FIXME:At the moment, we do not support certificates...
1004
    EndpointDescription selectedEndpoint = null;
1✔
1005
    for (var endpoint : endpoints) {
1✔
1006
      switch (endpoint.getSecurityMode()) {
1✔
1007
        case Invalid:
1008
          //                        FIXME:Add log message
1009
          break;
×
1010
        case None:
1011
          //                        FIXME:Add log message
1012
          selectedEndpoint = endpoint;
1✔
1013
          break;
1✔
1014
          //                        FIXME:Add log message
1015
        case Sign:
1016
          break;
×
1017
        case SignAndEncrypt:
1018
          //                        FIXME:Add log message
1019
          break;
×
1020
        default:
1021
          break;
1022
      }
1023

1024
      if (selectedEndpoint != null) {
1✔
1025
        break;
1✔
1026
      }
1027
    }
×
1028

1029
    if (selectedEndpoint == null) {
1✔
1030
      throw new Exception("No viable endpoint found from list:" + endpoints);
×
1031
    }
1032

1033
    OpcUaClientConfig builder = OpcUaClientConfig.builder().setEndpoint(selectedEndpoint).build();
1✔
1034

1035
    return OpcUaClient.create(builder);
1✔
1036
  }
1037

1038
  /**
1039
   * Browse all nodes starting from browseRoot.
1040
   *
1041
   * @param indent
1042
   * @param client
1043
   * @param browseRoot
1044
   */
1045
  private void browseNodeWithReferences(String indent, OpcUaClient client, NodeId browseRoot) {
1046
    BrowseDescription browse =
1✔
1047
        new BrowseDescription(
1048
            browseRoot,
1049
            BrowseDirection.Forward,
1050
            Identifiers.References,
1051
            true,
1✔
1052
            uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
1✔
1053
            uint(BrowseResultMask.All.getValue()));
1✔
1054

1055
    try {
1056

1057
      BrowseResult browseResult = client.browse(browse).get();
1✔
1058

1059
      List<ReferenceDescription> references = toList(browseResult.getReferences());
1✔
1060

1061
      if (references.isEmpty()) {
1✔
1062
        //              FIXME:Add log here
1063

1064
        return;
1✔
1065
      }
1066

1067
      for (ReferenceDescription rd : references) {
1✔
1068
        Object desc = null;
1✔
1069
        Object value = null;
1✔
1070
        UaNode node = null;
1✔
1071
        try {
1072

1073
          node =
1✔
1074
              client
1075
                  .getAddressSpace()
1✔
1076
                  .getNode(rd.getNodeId().toNodeId(client.getNamespaceTable()).get());
1✔
1077
          DataValue attr = node.readAttribute(AttributeId.Description);
1✔
1078
          desc = attr.getValue().getValue();
1✔
1079

1080
          attr = node.readAttribute(AttributeId.Value);
1✔
1081

1082
          value = attr.getValue();
1✔
1083

1084
        } catch (UaException e) {
×
1085
          // TODO Auto-generated catch block
1086
          e.printStackTrace();
×
1087
        }
1✔
1088

1089
        if (node != null) {
1✔
1090
          addOPCUAPV(client, node);
1✔
1091

1092
          log.debug(
1✔
1093
              "{} Node={}, Desc={}, Value={}", indent, rd.getBrowseName().getName(), desc, value);
1✔
1094

1095
          if (rd.getIsForward()) {}
1✔
1096

1097
          // recursively browse to children
1098
          rd.getNodeId()
1✔
1099
              .toNodeId(client.getNamespaceTable())
1✔
1100
              .ifPresent(nodeId -> browseNodeWithReferences(indent + "  ", client, nodeId));
1✔
1101
        }
1102
      }
1✔
1103

1104
    } catch (InterruptedException e1) {
×
1105
      // TODO Auto-generated catch block
1106
      e1.printStackTrace();
×
1107
    } catch (ExecutionException e1) {
×
1108
      // TODO Auto-generated catch block
1109
      e1.printStackTrace();
×
1110
    }
1✔
1111
  }
1✔
1112

1113
  /**
1114
   * Adds new PV with the name of node.
1115
   *
1116
   * @param client
1117
   * @param node
1118
   */
1119
  private void addOPCUAPV(OpcUaClient client, UaNode node) {
1120

1121
    if (node.getBrowseName()
1✔
1122
        .getName()
1✔
1123
        .contains(Character.toString(NameDescription.PATH_SEPARATOR))) {
1✔
1124
      internalLogger.info(
1✔
1125
          "{} ignored since it contains a {} character",
1126
          node.getBrowseName().getName(),
1✔
1127
          Character.toString(NameDescription.PATH_SEPARATOR));
1✔
1128

1129
    } else {
1130

1131
      /**
1132
       * NOTE:For now we'll just flatten all the attributes instead of using an aggregate type for
1133
       * attributes
1134
       */
1135
      for (AttributeId attr : AttributeId.values()) {
1✔
1136

1137
        ParameterType ptype = OPCUAAttrTypeToParamType(attr, node);
1✔
1138

1139
        String opcuaTranslatedQName = translateNodeToParamQName(client, node, attr);
1✔
1140
        Parameter p = VariableParam.getForFullyQualifiedName(opcuaTranslatedQName);
1✔
1141

1142
        p.setParameterType(ptype);
1✔
1143

1144
        if (mdb.getParameter(p.getQualifiedName()) == null) {
1✔
1145
          log.debug("Adding OPCUA object as parameter to mdb:{}", p.getQualifiedName());
1✔
1146
          mdb.addParameter(p, true);
1✔
1147

1148
          nodeIDToParamsMap.put(new NodeIDAttrPair(node.getNodeId(), attr), (VariableParam) p);
1✔
1149
        }
1150
      }
1151
    }
1152
  }
1✔
1153

1154
  /**
1155
   * Map nodeID name to a qualified name that can be used for a YAMCS PV.
1156
   *
1157
   * @param client
1158
   * @param node
1159
   * @param attr
1160
   * @return
1161
   */
1162
  private String translateNodeToParamQName(OpcUaClient client, UaNode node, AttributeId attr) {
1163
    LocalizedText localizedDisplayName = null;
1✔
1164
    try {
1165

1166
      localizedDisplayName =
1✔
1167
          (LocalizedText) (node.readAttribute(AttributeId.DisplayName).getValue().getValue());
1✔
1168
    } catch (UaException e) {
×
1169
      // TODO Auto-generated catch block
1170
      e.printStackTrace();
×
1171
    }
1✔
1172
    String opcuaTranslatedQName =
1✔
1173
        qualifiedName(
1✔
1174
            parametersNamespace
1175
                + NameDescription.PATH_SEPARATOR
1176
                + node.getNodeId().toParseableString().replace(";", "-")
1✔
1177
                + NameDescription.PATH_SEPARATOR
1178
                + localizedDisplayName.getText(),
1✔
1179
            attr.toString());
1✔
1180

1181
    return opcuaTranslatedQName;
1✔
1182
  }
1183

1184
  /**
1185
   * Browse node at nodePath relative to browseRoot.
1186
   *
1187
   * @param indent
1188
   * @param client
1189
   * @param browseRoot
1190
   * @param nodePath in the format of "0:Root,0:Objects,2:HelloWorld,2:MyObject,2:Bar"
1191
   */
1192
  private void browsePath(String indent, OpcUaClient client, NodeId startingNode, String nodePath) {
1193
    internalLogger.info("Browsing at " + startingNode);
1✔
1194
    ArrayList<String> rPathTokens = new ArrayList<String>();
1✔
1195
    ArrayList<RelativePathElement> relaitivePathElements = new ArrayList<RelativePathElement>();
1✔
1196

1197
    for (var pathToken : nodePath.split(",")) {
1✔
1198
      rPathTokens.add(nodePath);
1✔
1199

1200
      int namespaceIndex = 0;
1✔
1201

1202
      String namespaceName = "";
1✔
1203

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

1206
      namespaceName = pathToken.split(":")[1];
1✔
1207

1208
      relaitivePathElements.add(
1✔
1209
          new RelativePathElement(
1210
              Identifiers.HierarchicalReferences,
1211
              false,
1✔
1212
              true,
1✔
1213
              new QualifiedName(namespaceIndex, namespaceName)));
1214
    }
1215

1216
    ArrayList<BrowsePath> list = new ArrayList<BrowsePath>();
1✔
1217

1218
    RelativePathElement[] elements = new RelativePathElement[relaitivePathElements.size()];
1✔
1219

1220
    relaitivePathElements.toArray(elements);
1✔
1221

1222
    list.add(new BrowsePath(startingNode, new RelativePath(elements)));
1✔
1223

1224
    TranslateBrowsePathsToNodeIdsResponse response = null;
1✔
1225
    try {
1226
      response = client.translateBrowsePaths(list).get();
1✔
1227
    } catch (InterruptedException e) {
×
1228
      // TODO Auto-generated catch block
1229
      e.printStackTrace();
×
1230
    } catch (ExecutionException e) {
×
1231
      // TODO Auto-generated catch block
1232
      e.printStackTrace();
×
1233
    }
1✔
1234

1235
    BrowsePathResult result = Arrays.asList(response.getResults()).get(0);
1✔
1236
    StatusCode statusCode = result.getStatusCode();
1✔
1237

1238
    if (statusCode.isBad()) {
1✔
1239
      log.warn("Bad status code:" + statusCode);
×
1240
      return;
×
1241
    } else if (statusCode.isUncertain()) {
1✔
1242
      log.warn("Uncertain status code:" + statusCode);
×
1243
      return;
×
1244
    }
1245

1246
    try {
1247
      UaNode node =
1✔
1248
          client
1249
              .getAddressSpace()
1✔
1250
              .getNode(
1✔
1251
                  result.getTargets()[0].getTargetId().toNodeId(client.getNamespaceTable()).get());
1✔
1252

1253
      addOPCUAPV(client, node);
1✔
1254
    } catch (UaException e) {
×
1255
      // TODO Auto-generated catch block
1256
      e.printStackTrace();
×
1257
    }
1✔
1258
  }
1✔
1259

1260
  private void createOPCUASubscriptions() {
1261
    createDataChangeListener();
1✔
1262
    Set<NodeId> nodeSet = new HashSet<NodeId>();
1✔
1263
    /**
1264
     * FIXME:This is super inefficient... The reason we collect these nodeIDs in a set is because
1265
     * otherwise we will have redundant subscription(s) since there is more than 1 attribute per
1266
     * nodeID given how nodeIDToParamsMap is designed
1267
     */
1268
    for (NodeIDAttrPair pair : nodeIDToParamsMap.keySet()) {
1✔
1269
      nodeSet.add(pair.nodeID);
1✔
1270
    }
1✔
1271
    for (NodeId id : nodeSet) {
1✔
1272
      Variant nodeClass = null;
1✔
1273
      try {
1274
        UaNode node = client.getAddressSpace().getNode(id);
1✔
1275

1276
        nodeClass = node.readAttribute(AttributeId.NodeClass).getValue();
1✔
1277

1278
      } catch (UaException e) {
×
1279
        // TODO Auto-generated catch block
1280
        e.printStackTrace();
×
1281
      }
1✔
1282
      if (nodeClass != null) {
1✔
1283
        try {
1284
          switch (NodeClass.from((int) nodeClass.getValue())) {
1✔
1285
              // As per the spec, the only thing we can subscribe to is Variables
1286
            case Variable:
1287
              ManagedDataItem dataItem = opcuaSubscription.createDataItem(id);
1✔
1288
              OPCUAActiveSubs.addAndGet(1);
1✔
1289
              log.debug("Status code for dataItem:{}", dataItem.getStatusCode());
1✔
1290
              break;
1291
          }
1292
        } catch (UaException e) {
×
1293
          // TODO Auto-generated catch block
1294
          e.printStackTrace();
×
1295
        }
1✔
1296
      }
1297
    }
1✔
1298
  }
1✔
1299

1300
  public void connectToOPCUAServer(OpcUaClient client) throws Exception {
1301
    internalLogger.info("Connecting to OPCUA server...");
1✔
1302
    client.connect().get();
1✔
1303

1304
    addAction(startAction);
1✔
1305
    startAction.setEnabled(true);
1✔
1306
  }
1✔
1307

1308
  /**
1309
   * Browses the tree on the OPCUA server and maps them to YAMCS Parameters.
1310
   *
1311
   * @param client
1312
   */
1313
  private void browseOPCUATree(OpcUaClient client) {
1314
    // start browsing at root folder
1315
    internalLogger.info("Browsing OPCUA...");
1✔
1316
    for (var p : relativeNodePaths) {
1✔
1317
      int namespaceIndex = (int) p.rootNodeID.get("namespaceIndex");
1✔
1318
      String identifier = (String) p.rootNodeID.get("identifier");
1✔
1319
      IdType identifierType = IdType.valueOf((String) p.rootNodeID.get("identifierType"));
1✔
1320

1321
      browsePath(
1✔
1322
          endpointURL, client, getNewNodeID(identifierType, namespaceIndex, identifier), p.path);
1✔
1323
    }
1✔
1324

1325
    NodeId nodeID = null;
1✔
1326
    nodeID = getNewNodeID(rootIdentifierType, rootNamespaceIndex, rootIdentifier);
1✔
1327

1328
    //  FIXME:Make root default when no namespaceIndex/identifier pair is specified
1329
    browseNodeWithReferences("", client, nodeID);
1✔
1330
  }
1✔
1331

1332
  /**
1333
   * Get new OPCUA-compliant NodeID object that is created from NamespaceIndex and Identifier. At
1334
   * the moment only String and Numeric node ids are supported.
1335
   *
1336
   * @param rootIdentifierType
1337
   * @param NamespaceIndex
1338
   * @param Identifier
1339
   * @return
1340
   */
1341
  private NodeId getNewNodeID(IdType rootIdentifierType, int NamespaceIndex, String Identifier) {
1342
    NodeId nodeID = null;
1✔
1343
    switch (rootIdentifierType) {
1✔
1344
      case Guid:
1345
        //                FIXME
1346
        break;
×
1347
      case Numeric:
1348
        nodeID = new NodeId(NamespaceIndex, Integer.parseInt(Identifier));
1✔
1349
        break;
1✔
1350
      case Opaque:
1351
        //                FIXME
1352
        break;
×
1353
      case String:
1354
        nodeID = new NodeId(NamespaceIndex, Identifier);
×
1355
        break;
×
1356
      default:
1357
        break;
1358
    }
1359
    return nodeID;
1✔
1360
  }
1361

1362
  /** Data listener for realtime OPCUA server updates. */
1363
  private void createDataChangeListener() {
1364
    try {
1365
      opcuaSubscription = ManagedSubscription.create(client, publishInterval);
1✔
1366
    } catch (UaException e) {
×
1367
      // TODO Auto-generated catch block
1368
      e.printStackTrace();
×
1369
    }
1✔
1370
    opcuaSubscription.addDataChangeListener(
1✔
1371
        (items, values) -> {
1372
          for (int i = 0; i < items.size(); i++) {
1✔
1373
            NodeIDAttrPair nodeAttrKey =
1✔
1374
                new NodeIDAttrPair(items.get(i).getNodeId(), AttributeId.Value);
1✔
1375
            log.debug(
1✔
1376
                "subscription value received: item={}, value={}",
1377
                items.get(i).getNodeId(),
1✔
1378
                values.get(i).getValue());
1✔
1379

1380
            log.debug(
1✔
1381
                "Pushing new PV for param name {} which is mapped to NodeID {}",
1382
                nodeIDToParamsMap.get(nodeAttrKey),
1✔
1383
                items.get(i).getNodeId());
1✔
1384

1385
            TupleDefinition tdef = gftdef.copy();
1✔
1386
            List<Object> cols = new ArrayList<>(4 + 1);
1✔
1387
            //            FIXME: Add leap seconds.... as config or get it from YAMCS API.
1388
            long gentime =
1✔
1389
                values
1390
                    .get(i)
1✔
1391
                    .getSourceTime()
1✔
1392
                    .getJavaInstant()
1✔
1393
                    .plus(37, ChronoUnit.SECONDS)
1✔
1394
                    .toEpochMilli();
1✔
1395
            cols.add(gentime);
1✔
1396
            cols.add(parametersNamespace);
1✔
1397
            cols.add(0);
1✔
1398
            cols.add(gentime);
1✔
1399

1400
            /**
1401
             * TODO:Not sure if this is the best way to do this since the aggregate values will be
1402
             * partially updated. Another potential approach might be to decouple the live OPCUA
1403
             * data(subscriptions) via namespaces. For example; have a "special" namespace called
1404
             * "subscriptions" that ONLY gets updated with items. And maybe another namespace for
1405
             * static data...maybe.
1406
             *
1407
             * <p>Another option is to flatten everything and have no aggregate types at all. That
1408
             * approach might even simplify the code quite a bit...
1409
             *
1410
             * <p>Another question worth answering before moving forward is to find out whether or
1411
             * not it is concrete in the OPCUA protocol what data can change in real time and which
1412
             * data is "static". Not sure if there is any "static" data given that clients have the
1413
             * ability of writing to values... might be worth a test.
1414
             */
1415
            log.debug(
1✔
1416
                "Data({}) chnage triggered for {}",
1417
                values.get(i).getValue(),
1✔
1418
                nodeIDToParamsMap.get(nodeAttrKey));
1✔
1419

1420
            if (nodeIDToParamsMap.get(nodeAttrKey) == null) {
1✔
1421
              log.debug("No parameter mapping found for {}", nodeAttrKey.nodeID);
×
1422
              continue;
×
1423
            } else {
1424
              log.debug(
1✔
1425
                  String.format(
1✔
1426
                      "parameter mapping found for {} and {}",
1427
                      nodeAttrKey.nodeID,
1428
                      nodeAttrKey.attrID));
1429
            }
1430

1431
            if (values.get(i).getValue() != null && values.get(i).getValue().getValue() != null) {
1✔
1432

1433
              switch (nodeIDToParamsMap.get(nodeAttrKey).getParameterType().getValueType()) {
1✔
1434
                case AGGREGATE:
1435
                  {
1436
                    String value = (String) values.get(i).getValue().getValue();
×
1437

1438
                    tdef.addColumn(
×
1439
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1440
                        DataType.PARAMETER_VALUE);
1441
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1442
                  }
1443
                  break;
×
1444
                case ARRAY:
1445
                  {
1446
                    String value = (String) values.get(i).getValue().getValue();
×
1447

1448
                    tdef.addColumn(
×
1449
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1450
                        DataType.PARAMETER_VALUE);
1451
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1452
                  }
1453
                  break;
×
1454
                case BINARY:
1455
                  {
1456
                    String value = (String) values.get(i).getValue().getValue();
×
1457
                    tdef.addColumn(
×
1458
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1459
                        DataType.PARAMETER_VALUE);
1460
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1461
                  }
1462
                  break;
×
1463
                case BOOLEAN:
1464
                  {
1465
                    boolean value = (boolean) values.get(i).getValue().getValue();
1✔
1466

1467
                    tdef.addColumn(
1✔
1468
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1469
                        DataType.PARAMETER_VALUE);
1470
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1471
                  }
1472
                  break;
1✔
1473
                case DOUBLE:
1474
                  {
1475
                    double value = (double) values.get(i).getValue().getValue();
1✔
1476

1477
                    tdef.addColumn(
1✔
1478
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1479
                        DataType.PARAMETER_VALUE);
1480
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1481
                  }
1482
                  break;
1✔
1483
                case ENUMERATED:
1484
                  {
1485
                    String value = (String) values.get(i).getValue().getValue();
×
1486

1487
                    tdef.addColumn(
×
1488
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1489
                        DataType.PARAMETER_VALUE);
1490
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1491
                  }
1492
                  break;
×
1493
                case FLOAT:
1494
                  {
1495
                    float value = (float) values.get(i).getValue().getValue();
×
1496

1497
                    tdef.addColumn(
×
1498
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1499
                        DataType.PARAMETER_VALUE);
1500
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1501
                  }
1502
                  break;
×
1503
                case NONE:
1504
                  {
1505
                    String value = (String) values.get(i).getValue().getValue();
×
1506

1507
                    tdef.addColumn(
×
1508
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1509
                        DataType.PARAMETER_VALUE);
1510
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1511
                  }
1512
                  break;
×
1513
                case SINT32:
1514
                  {
1515
                    int value = (int) values.get(i).getValue().getValue();
1✔
1516
                    tdef.addColumn(
1✔
1517
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1518
                        DataType.PARAMETER_VALUE);
1519
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1520
                  }
1521
                  break;
1✔
1522
                case SINT64:
1523
                  {
1524
                    long value = (long) values.get(i).getValue().getValue();
1✔
1525

1526
                    tdef.addColumn(
1✔
1527
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1528
                        DataType.PARAMETER_VALUE);
1529
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1530
                  }
1531
                  break;
1✔
1532
                case STRING:
1533
                  {
1534
                    String value = (String) values.get(i).getValue().getValue().toString();
1✔
1535

1536
                    tdef.addColumn(
1✔
1537
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
1✔
1538
                        DataType.PARAMETER_VALUE);
1539
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
1✔
1540
                  }
1541
                  break;
1✔
1542
                case TIMESTAMP:
1543
                  {
1544
                    String value = (String) values.get(i).getValue().getValue();
×
1545

1546
                    tdef.addColumn(
×
1547
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1548
                        DataType.PARAMETER_VALUE);
1549
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1550
                  }
1551
                  break;
×
1552
                case UINT32:
1553
                  {
1554
                    int value = (int) values.get(i).getValue().getValue();
×
1555
                    tdef.addColumn(
×
1556
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1557
                        DataType.PARAMETER_VALUE);
1558
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1559
                  }
1560
                  break;
×
1561
                case UINT64:
1562
                  {
1563
                    long value = (long) values.get(i).getValue().getValue();
×
1564

1565
                    tdef.addColumn(
×
1566
                        nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName(),
×
1567
                        DataType.PARAMETER_VALUE);
1568
                    cols.add(getPV(nodeIDToParamsMap.get(nodeAttrKey), gentime, value));
×
1569
                  }
1570
                  break;
×
1571
                default:
1572
                  break;
1573
              }
1574

1575
              pushTuple(tdef, cols);
1✔
1576

1577
              inCount.getAndAdd(1);
1✔
1578
            } else {
1579
              // TODO:Add some type emptyValue count for OPS.
1580
              log.warn(
1✔
1581
                  "Data chnage triggered for {}, but it empty. This should not happen.",
1582
                  nodeIDToParamsMap.get(nodeAttrKey).getQualifiedName());
1✔
1583
            }
1584
          }
1585
        });
1✔
1586
  }
1✔
1587

1588
  /**
1589
   * This method is here for future growth in case we find there is a benefit to using aggregate
1590
   * types
1591
   */
1592
  private void createOPCUAAttrAggregateType() {
1593

1594
    AggregateParameterType.Builder opcuaAttrsTypeBuidlder = new AggregateParameterType.Builder();
1✔
1595

1596
    opcuaAttrsType = new AggregateParameterType.Builder().setName("OPCUObjectAttributes").build();
1✔
1597

1598
    opcuaAttrsTypeBuidlder.setName("OPCUObjectAttributes");
1✔
1599
    for (AttributeId attr : AttributeId.values()) {
1✔
1600
      opcuaAttrsTypeBuidlder.addMember(new Member(attr.toString(), getBasicType(mdb, Type.STRING)));
1✔
1601
    }
1602

1603
    opcuaAttrsType = opcuaAttrsTypeBuidlder.build();
1✔
1604
    ((NameDescription) opcuaAttrsType)
1✔
1605
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaAttrsType.getName()));
1✔
1606
  }
1✔
1607

1608
  /**
1609
   * This method is here for future growth in case we find there is a benefit to using aggregate
1610
   * types
1611
   */
1612
  private void createOPCUANodeIdTypes() {
1613
    AggregateParameterType.Builder opcuaAttrsNumericNodeIdBuidlder =
1✔
1614
        new AggregateParameterType.Builder();
1615

1616
    opcuaAttrsNumericNodeIdBuidlder.setName("OPCUA_Numeric_NodeId");
1✔
1617
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1618
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1619
    opcuaAttrsNumericNodeIdBuidlder.addMember(
1✔
1620
        new Member("identifier", getBasicType(mdb, Type.UINT64)));
1✔
1621

1622
    opcuaNodeIdNumericType = opcuaAttrsNumericNodeIdBuidlder.build();
1✔
1623
    ((NameDescription) opcuaNodeIdNumericType)
1✔
1624
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdNumericType.getName()));
1✔
1625

1626
    AggregateParameterType.Builder opcuaAttrsTypeStringBuidlder =
1✔
1627
        new AggregateParameterType.Builder();
1628

1629
    opcuaAttrsTypeStringBuidlder.setName("OPCUA_String_NodeId");
1✔
1630
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1631
        new Member("namespaceIndex", getBasicType(mdb, Type.UINT64)));
1✔
1632
    opcuaAttrsTypeStringBuidlder.addMember(
1✔
1633
        new Member("identifier", getBasicType(mdb, Type.STRING)));
1✔
1634

1635
    opcuaNodeIdStringType = opcuaAttrsTypeStringBuidlder.build();
1✔
1636
    ((NameDescription) opcuaNodeIdStringType)
1✔
1637
        .setQualifiedName(qualifiedName(parametersNamespace, opcuaNodeIdStringType.getName()));
1✔
1638

1639
    mdb.addParameterType(opcuaNodeIdNumericType, true);
1✔
1640
    mdb.addParameterType(opcuaNodeIdStringType, true);
1✔
1641
  }
1✔
1642

1643
  /**
1644
   * Get new ParameterType for the specified attribute of the node. Particularly useful for Value
1645
   * attributes of nodes.
1646
   *
1647
   * @param attr
1648
   * @param node
1649
   * @return
1650
   */
1651
  private ParameterType OPCUAAttrTypeToParamType(AttributeId attr, UaNode node) {
1652
    ParameterType pType = null;
1✔
1653

1654
    switch (attr) {
1✔
1655
      case AccessLevel:
1656
        pType = getBasicType(mdb, Type.STRING);
1✔
1657
        break;
1✔
1658
      case ArrayDimensions:
1659
        pType = getBasicType(mdb, Type.STRING);
1✔
1660
        break;
1✔
1661
      case BrowseName:
1662
        pType = getBasicType(mdb, Type.STRING);
1✔
1663
        break;
1✔
1664
      case ContainsNoLoops:
1665
        pType = getBasicType(mdb, Type.STRING);
1✔
1666
        break;
1✔
1667
      case DataType:
1668
        pType = getBasicType(mdb, Type.STRING);
1✔
1669
        break;
1✔
1670
      case Description:
1671
        pType = getBasicType(mdb, Type.STRING);
1✔
1672
        break;
1✔
1673
      case DisplayName:
1674
        pType = getBasicType(mdb, Type.STRING);
1✔
1675
        break;
1✔
1676
      case EventNotifier:
1677
        pType = getBasicType(mdb, Type.STRING);
1✔
1678
        break;
1✔
1679
      case Executable:
1680
        pType = getBasicType(mdb, Type.STRING);
1✔
1681
        break;
1✔
1682
      case Historizing:
1683
        pType = getBasicType(mdb, Type.STRING);
1✔
1684
        break;
1✔
1685
      case InverseName:
1686
        pType = getBasicType(mdb, Type.STRING);
1✔
1687
        break;
1✔
1688
      case IsAbstract:
1689
        pType = getBasicType(mdb, Type.STRING);
1✔
1690
        break;
1✔
1691
      case MinimumSamplingInterval:
1692
        pType = getBasicType(mdb, Type.STRING);
1✔
1693
        break;
1✔
1694
      case NodeClass:
1695
        pType = getBasicType(mdb, Type.STRING);
1✔
1696
        break;
1✔
1697
      case NodeId:
1698
        pType = getBasicType(mdb, Type.STRING);
1✔
1699
        break;
1✔
1700
      case Symmetric:
1701
        pType = getBasicType(mdb, Type.STRING);
1✔
1702
        break;
1✔
1703
      case UserAccessLevel:
1704
        pType = getBasicType(mdb, Type.STRING);
1✔
1705
        break;
1✔
1706
      case UserExecutable:
1707
        pType = getBasicType(mdb, Type.STRING);
1✔
1708
        break;
1✔
1709
      case UserWriteMask:
1710
        pType = getBasicType(mdb, Type.STRING);
1✔
1711
        break;
1✔
1712
      case Value:
1713
        try {
1714

1715
          var value = node.readAttribute(attr).getValue();
1✔
1716

1717
          if (value.isNotNull()) {
1✔
1718

1719
            Object valueObject = value.getValue();
1✔
1720

1721
            if (valueObject instanceof Short) {
1✔
1722
              pType = getBasicType(mdb, Type.SINT32);
1✔
1723
            } else if (valueObject instanceof Integer) {
1✔
1724
              pType = getBasicType(mdb, Type.SINT32);
1✔
1725

1726
            } else if (valueObject instanceof Long) {
1✔
1727
              pType = getBasicType(mdb, Type.SINT64);
1✔
1728
            } else if (valueObject instanceof Double) {
1✔
1729
              pType = getBasicType(mdb, Type.DOUBLE);
1✔
1730
            } else if (valueObject instanceof Float) {
1✔
1731
              pType = getBasicType(mdb, Type.FLOAT);
1✔
1732
            } else if (valueObject instanceof Character) {
1✔
1733
              pType = getBasicType(mdb, Type.STRING);
×
1734
            } else if (valueObject instanceof String) {
1✔
1735
              pType = getBasicType(mdb, Type.STRING);
1✔
1736

1737
            } else if (valueObject instanceof Boolean) {
1✔
1738
              pType = getBasicType(mdb, Type.BOOLEAN);
1✔
1739
            } else {
1740
              pType = getBasicType(mdb, Type.STRING);
1✔
1741
            }
1742
          } else {
1✔
1743
            pType = getBasicType(mdb, Type.STRING);
1✔
1744
          }
1745

1746
        } catch (UaException e) {
×
1747
          // TODO Auto-generated catch block
1748
          e.printStackTrace();
×
1749
          //                        FIXME:Add log message
1750
        }
1✔
1751
        break;
×
1752
      case ValueRank:
1753
        pType = getBasicType(mdb, Type.STRING);
1✔
1754
        break;
1✔
1755
      case WriteMask:
1756
        pType = getBasicType(mdb, Type.STRING);
1✔
1757
        break;
1✔
1758
      default:
1759
        break;
1760
    }
1761

1762
    return pType;
1✔
1763
  }
1764

1765
  private void subscribeToEvents(OpcUaClient client)
1766
      throws InterruptedException, ExecutionException {
1767
    // create a subscription and a monitored item
1768
    UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
1✔
1769

1770
    ReadValueId readValueId =
1✔
1771
        new ReadValueId(
1772
            Identifiers.Server, AttributeId.EventNotifier.uid(), null, QualifiedName.NULL_VALUE);
1✔
1773

1774
    // client handle must be unique per item
1775
    UInteger clientHandle = uint(clientHandles.getAndIncrement());
1✔
1776

1777
    EventFilter eventFilter =
1✔
1778
        new EventFilter(
1779
            new SimpleAttributeOperand[] {
1780
              new SimpleAttributeOperand(
1781
                  Identifiers.BaseEventType,
1782
                  new QualifiedName[] {new QualifiedName(0, "EventId")},
1783
                  AttributeId.Value.uid(),
1✔
1784
                  null),
1785
              new SimpleAttributeOperand(
1786
                  Identifiers.BaseEventType,
1787
                  new QualifiedName[] {new QualifiedName(0, "EventType")},
1788
                  AttributeId.Value.uid(),
1✔
1789
                  null),
1790
              new SimpleAttributeOperand(
1791
                  Identifiers.BaseEventType,
1792
                  new QualifiedName[] {new QualifiedName(0, "Severity")},
1793
                  AttributeId.Value.uid(),
1✔
1794
                  null),
1795
              new SimpleAttributeOperand(
1796
                  Identifiers.BaseEventType,
1797
                  new QualifiedName[] {new QualifiedName(0, "Time")},
1798
                  AttributeId.Value.uid(),
1✔
1799
                  null),
1800
              new SimpleAttributeOperand(
1801
                  Identifiers.BaseEventType,
1802
                  new QualifiedName[] {new QualifiedName(0, "Message")},
1803
                  AttributeId.Value.uid(),
1✔
1804
                  null)
1805
            },
1806
            new ContentFilter(null));
1807

1808
    MonitoringParameters parameters =
1✔
1809
        new MonitoringParameters(
1810
            clientHandle,
1811
            0.0,
1✔
1812
            ExtensionObject.encode(client.getStaticSerializationContext(), eventFilter),
1✔
1813
            uint(10),
1✔
1814
            true);
1✔
1815

1816
    MonitoredItemCreateRequest request =
1✔
1817
        new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
1818

1819
    List<UaMonitoredItem> items =
1✔
1820
        subscription.createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get();
1✔
1821

1822
    // do something with the value updates
1823
    UaMonitoredItem monitoredItem = items.get(0);
1✔
1824

1825
    monitoredItem.setEventConsumer(
1✔
1826
        (item, vs) -> {
1827
          internalLogger.info("Event Received from {}", item.getReadValueId().getNodeId());
1✔
1828

1829
          StringBuilder eventText = new StringBuilder();
1✔
1830

1831
          ByteString eventId;
1832
          NodeId eventType;
1833
          UShort eventSeverity;
1834
          DateTime eventTime;
1835
          LocalizedText eventMessage;
1836

1837
          for (int i = 0; i < vs.length; i++) {
1✔
1838
            internalLogger.info("\tvariant[{}]: {}", i, vs[i].getValue());
1✔
1839
          }
1840

1841
          eventId = (ByteString) vs[0].getValue();
1✔
1842
          eventType = (NodeId) vs[1].getValue();
1✔
1843
          eventSeverity = (UShort) vs[2].getValue();
1✔
1844
          eventTime = (DateTime) vs[3].getValue();
1✔
1845
          eventMessage = (LocalizedText) vs[4].getValue();
1✔
1846

1847
          //          FIXME:Map these values to YAMCS API
1848
          eventText.append("eventId:" + eventId);
1✔
1849
          eventText.append("\n");
1✔
1850
          eventText.append("eventType:" + eventType);
1✔
1851
          eventText.append("\n");
1✔
1852
          eventText.append("eventSeverity:" + eventSeverity);
1✔
1853
          eventText.append("\n");
1✔
1854
          eventText.append("eventTime:" + eventTime);
1✔
1855
          eventText.append("\n");
1✔
1856
          eventText.append("eventMessage:" + eventMessage);
1✔
1857
          org.yamcs.yarch.protobuf.Db.Event ev =
1858
              Event.newBuilder()
1✔
1859
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1860
                  .setGenerationTime(YamcsServer.getTimeService(yamcsInstance).getMissionTime())
1✔
1861
                  .setSource(this.linkName)
1✔
1862
                  .setType(this.linkName)
1✔
1863
                  .setMessage(eventText.toString())
1✔
1864
                  .setSeverity(EventSeverity.INFO)
1✔
1865
                  .build();
1✔
1866
          eventProducer.sendEvent(ev);
1✔
1867
        });
1✔
1868
  }
1✔
1869

1870
  @Override
1871
  public void setupSystemParameters(SystemParametersService sysParamService) {
1872
    super.setupSystemParameters(sysParamService);
1✔
1873
    OPCUAStatusParam =
1✔
1874
        sysParamService.createEnumeratedSystemParameter(
1✔
1875
            linkName + "/OPCUAStatusParam",
1876
            OPCUAStatus.class,
1877
            "The current status of OPCUA client");
1878
    EnumeratedParameterType spLinkStatusType =
1✔
1879
        (EnumeratedParameterType) OPCUAStatusParam.getParameterType();
1✔
1880
    spLinkStatusType
1✔
1881
        .enumValue(OPCUAStatus.OPCUA_INIT_CONFIG.name())
1✔
1882
        .setDescription(
1✔
1883
            "This link is in the configuration stage(Configuring OPCUA parameters such as certificates)");
1884
    spLinkStatusType
1✔
1885
        .enumValue(OPCUAStatus.OPCUA_INIT_TREE.name())
1✔
1886
        .setDescription(
1✔
1887
            "The link is parsing the OPCUA Tree and mapping them to PVs."
1888
                + " Depending on configuration, this can take a while.");
1889
    spLinkStatusType
1✔
1890
        .enumValue(OPCUAStatus.OPCUA_INIT_EVENTS.name())
1✔
1891
        .setDescription("The link is configuring and subscribing to OPCUA events");
1✔
1892
    spLinkStatusType
1✔
1893
        .enumValue(OPCUAStatus.OPCUA_INIT_DATA_SUBSCRIPTION.name())
1✔
1894
        .setDescription(
1✔
1895
            "The link is creating subscriptions for each node that was parsed from the tree"
1896
                + "that has a Value attribute.");
1897
    spLinkStatusType
1✔
1898
        .enumValue(OPCUAStatus.OPCUA_INIT_ALL_DATA_QUERY.name())
1✔
1899
        .setDescription(
1✔
1900
            "The link is querying all attributes of all parsed nodes."
1901
                + "This is can be configured to be done at startup.");
1902
    spLinkStatusType
1✔
1903
        .enumValue(OPCUAStatus.OPCUA_OK.name())
1✔
1904
        .setDescription(
1✔
1905
            "The link is done with all OPCUA initialization. It is in an usable state.");
1906

1907
    OPCUAActiveSubsParam =
1✔
1908
        sysParamService.createSystemParameter(
1✔
1909
            linkName + "/OPCUAActiveSubs",
1910
            Type.UINT64,
1911
            "The total number of active opcua subscriptions");
1912
  }
1✔
1913

1914
  @Override
1915
  public List<ParameterValue> getSystemParameters() {
1916
    long time = getCurrentTime();
1✔
1917

1918
    ArrayList<ParameterValue> list = new ArrayList<>();
1✔
1919

1920
    list.add(
1✔
1921
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1922
            OPCUAStatusParam, time, currentOPCUAStatus));
1923

1924
    list.add(
1✔
1925
        org.yamcs.parameter.SystemParametersService.getPV(
1✔
1926
            OPCUAActiveSubsParam, time, OPCUAActiveSubs.get()));
1✔
1927
    try {
1928
      super.collectSystemParameters(time, list);
1✔
1929
    } catch (Exception e) {
×
1930
      log.error("Exception caught when collecting link system parameters", e);
×
1931
    }
1✔
1932
    return list;
1✔
1933
  }
1934
}
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