OPC-UA javadoc and general functionality

I try a minimal implementation, but when I Browse the devices, I have a read time out:

com.inductiveautomation.ignition.client.gateway_interface.GatewayException: Read timed out

image

I probably need to implement getReference but I not sure how to do that ?

package com.inductiveautomation.ignition.examples.tagdriver;

import com.inductiveautomation.ignition.gateway.opcua.server.api.Device;
import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceContext;
import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceSettingsRecord;
import org.eclipse.milo.opcua.sdk.core.AccessLevel;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.UaNodeManager;
import org.eclipse.milo.opcua.sdk.server.api.DataItem;
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.BuiltinDataType;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Set;

/*
https://forum.inductiveautomation.com/t/opc-ua-javadoc-and-general-functionality/81913/13
 */

public class ExampleDevice implements Device {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    // private final SubscriptionModel subscriptionModel;

    private final DeviceContext deviceContext;
    private final DeviceSettingsRecord deviceSettings;

    private static GenericDeviceNodeContext nodeContext;

    private String status = "Running";

    public ExampleDevice(
            DeviceContext deviceContext,
            DeviceSettingsRecord deviceSettings
    ) {
        this.deviceContext = deviceContext;
        this.deviceSettings = deviceSettings;
    }

    @Override
    public @NotNull
    String getName() {
        return deviceSettings.getName();
    }

    @Override
    public @NotNull
    String getStatus() {
        //return "Running";
        return status;
    }

    @Override
    public @NotNull
    String getTypeId() {
        return deviceSettings.getType();
    }

    @Override
    public void onDataItemsCreated(List<DataItem> dataItems) {

        for (DataItem dataItem : dataItems) {
            logger.info("onDataItemsCreated : {}", dataItem.getReadValueId().getNodeId().getIdentifier().toString());
            logger.info("onDataItemsCreated dataItem.getId() : {}", dataItem.getId());
            logger.info("onDataItemsCreated dataItem.getSubscriptionId() : {}", dataItem.getSubscriptionId());
        }

    }

    @Override
    public void onDataItemsModified(List<DataItem> dataItems) {
        //subscriptionModel.onDataItemsModified(dataItems);
        for (DataItem dataItem : dataItems) {
            logger.info("onDataItemsModified : {}", dataItem.getReadValueId().getNodeId().getIdentifier().toString());
            logger.info("onDataItemsModified dataItem.getId() : {}", dataItem.getId());
        }
    }

    @Override
    public void onDataItemsDeleted(List<DataItem> dataItems) {
        //subscriptionModel.onDataItemsDeleted(dataItems);
        for (DataItem dataItem : dataItems) {
            logger.info("onDataItemsDeleted : {}", dataItem.getReadValueId().getNodeId().getIdentifier().toString());
            logger.info("onDataItemsDeleted dataItem.getId() : {}", dataItem.getId());
        }
    }

    @Override
    public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
        //subscriptionModel.onMonitoringModeChanged(monitoredItems);
        for (MonitoredItem monitoredItem : monitoredItems) {
            logger.info("onMonitoringModeChanged : {}", monitoredItem.getReadValueId().getNodeId().getIdentifier().toString());
            logger.info("onMonitoringModeChanged dataItem.getId() : {}", monitoredItem.getId());
        }
    }


    @Override
    public void startup() {

        logger.info("startup()");

        this.nodeContext = new GenericDeviceNodeContext(deviceContext);

        UaFolderNode rootNode = new UaFolderNode(
                nodeContext,
                deviceContext.nodeId(getName()),
                deviceContext.qualifiedName(String.format("[%s]", getName())),
                new LocalizedText(String.format("[%s]", getName()))
        );

        logger.info("create node : {}", String.format("[%s]", getName()));

        // add the folder node to the server
        nodeContext.getNodeManager().addNode(rootNode);

        // add a reference to the root "Devices" folder node
        rootNode.addReference(new org.eclipse.milo.opcua.sdk.core.Reference(
                rootNode.getNodeId(),
                Identifiers.Organizes,
                deviceContext.getRootNodeId().expanded(),
                Reference.Direction.INVERSE
        ));

        addStaticNodes(rootNode, "readOnly", 10, AccessLevel.READ_ONLY);
    }

    private void addStaticNodes(UaFolderNode rootNode, String name, int count, Set<AccessLevel> accessLevel) {
        UaFolderNode folder = new UaFolderNode(
                this.nodeContext,
                deviceContext.nodeId(name),
                deviceContext.qualifiedName(name),
                new LocalizedText(name)
        );
        this.nodeContext.getNodeManager().addNode(folder);
        rootNode.addOrganizes(folder);

        String name1 = folder.getDisplayName().getText();
        for (int i = 0; i < count; i++) {
            String formattedName = String.format("%s%d", name1, i);
            UaVariableNode node = UaVariableNode.builder(this.nodeContext)
                    .setNodeId(deviceContext.nodeId(String.format("%s/node%d", formattedName, i)))
                    .setBrowseName(deviceContext.qualifiedName(formattedName))
                    .setDisplayName(new LocalizedText(formattedName))
                    .setDataType(BuiltinDataType.UInt16.getNodeId())
                    .setTypeDefinition(Identifiers.BaseDataVariableType)
                    .setAccessLevel(accessLevel)
                    .setUserAccessLevel(accessLevel)
                    .build();
            node.setValue(new DataValue(new Variant(i)));

            if (accessLevel.contains(AccessLevel.CurrentWrite)) {
                // This filter just intercepts the write to log it before
                // passing it to the next filter in the chain. The default
                // filter instance at the end will write the attribute to
                // the UaNode instance.
                node.getFilterChain().addLast(AttributeFilters.setValue((ctx, value) -> {
                    logger.info("setValue: {}", value.getValue().getValue());
                    ctx.setAttribute(AttributeId.Value, value);
                }));
            }
            nodeContext.getNodeManager().addNode(node);
            folder.addOrganizes(node);
        }
    }

    @Override
    public void shutdown() {
        logger.info("shutdown()");
    }

    @Override
    public void read(ReadContext readContext, Double aDouble, TimestampsToReturn timestampsToReturn, List<ReadValueId> list) {
        logger.info("read()");
    }

    @Override
    public void write(WriteContext writeContext, List<WriteValue> list) {
        logger.info("write()");
    }

    @Override
    public void browse(BrowseContext browseContext, ViewDescription viewDescription, NodeId nodeId) {
        logger.info("browse()");
    }

    @Override
    public void getReferences(BrowseContext browseContext, ViewDescription viewDescription, NodeId nodeId) {
        logger.info("getReferences() - viewDescription = {} - nodeId = {}",viewDescription,nodeId);
    }

    public static class GenericDeviceNodeContext implements UaNodeContext {
        public final DeviceContext deviceContext;
        public final UaNodeManager nodeManager;

        public GenericDeviceNodeContext(DeviceContext deviceContext) {
            this.deviceContext = deviceContext;
            this.nodeManager = new UaNodeManager();
        }

        @Override
        public OpcUaServer getServer() {
            return deviceContext.getServer();
        }

        @Override
        public UaNodeManager getNodeManager() {
            return nodeManager;
        }
    }

}


You need to implement all of those callbacks.

See milo/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/api/ManagedAddressSpace.java at bd150672c2cee24e7934001eecb8ca581ddc4ffc · eclipse-milo/milo · GitHub as a reference, it seems like you're just re-creating that, but with some amount of laziness to how you add your Nodes.

1 Like

@Kevin.Herron, @pturmel

in my driver use case with dynamic nodes, I create UaVariableNode with a BuiltinDataType.UInt16,
and when read request provide me the value, I know the expected data type, so I change the node dataType.

I would like to store in the node that the dataType is "known" in order to avoid to change it for next request.

Is it possible to store some "user" data in the UaVariableNode, in order to retreive it later ?
If not I can have another structure for that, but I would like to avoid to multiply search operation.

Another way could be to use the value, Null if data type not known or perhaps a quality code can be use for such case ?

No, I almost did something like this at one point in the past but it never made it.

You can just create a Map<UaVariableNode, Object> to store your own data or something.

Thanks !

another question, when we remove node:

    @Override
    public void onDataItemsDeleted(List<DataItem> dataItems) {
        subscriptionModel.onDataItemsDeleted(dataItems);
        for (DataItem dataItem : dataItems) {
            logger.info("onDataItemsDeleted : {}", dataItem.getReadValueId().getNodeId().getIdentifier().toString());
            logger.info("onDataItemsDeleted dataItem.getId() : {}", dataItem.getId());

            logger.info("onDataItemsDeleted removeNode nodeId : {}", dataItem.getReadValueId().getNodeId());
            nodeContext.getNodeManager().removeNode(dataItem.getReadValueId().getNodeId());

            for (NodeId node : nodeContext.getNodeManager().getNodeIds()){
                logger.info("node = {}",node);
            }
        }
    }

is there other instructions to call to update device opcua tree, in Ignition, deleted node are still visible ?

If they're still visible then your driver is still returning them in the browse and/or gather methods.

(assuming you've refreshed that view)

Try UaNode::delete instead, which removes both the Node and References from the NodeManager.

1 Like