APIs

Show:
#!/usr/bin/env node
/* eslint no-process-exit: 0 */
"use strict";
const path = require("path");
const _ = require("underscore");
const assert = require("assert");
const opcua = require("node-opcua");


Error.stackTraceLimit = Infinity;

function constructFilename(filename) {
    return path.join(__dirname,"../",filename);
}

const yargs = require("yargs/yargs");

const argv = yargs(process.argv)
    .wrap(132)

    .string("alternateHostname")
    .describe("alternateHostname")

    .number("port")
    .default("port",26543)

    .number("maxAllowedSessionNumber")
    .describe("maxAllowedSessionNumber","the maximum number of concurrent client session that the server will accept")
    .default("maxAllowedSessionNumber",500)

    .number("maxAllowedSubscriptionNumber")
    .describe("maxAllowedSubscriptionNumber","the maximum number of concurrent subscriptions")

    .boolean("silent")
    .default("silent",false)
    .describe("silent","no trace")

    .alias("a", "alternateHostname")
    .alias("p", "port")
    .alias("m", "maxAllowedSessionNumber")

    .number("keySize")
    .describe("keySize","certificate keySize [1024|2048|3072|4096]")
    .default("keySize",2048)
    .alias("k","keySize")

    .help(true)
    .argv;

const OPCUAServer = opcua.OPCUAServer;
const Variant = opcua.Variant;
const DataType = opcua.DataType;
const DataValue = opcua.DataValue;
const get_fully_qualified_domain_name = opcua.get_fully_qualified_domain_name;
const makeApplicationUrn = opcua.makeApplicationUrn;

const install_optional_cpu_and_memory_usage_node = opcua.install_optional_cpu_and_memory_usage_node;


const port = argv.port;
const maxAllowedSessionNumber   = argv.maxAllowedSessionNumber;
const maxConnectionsPerEndpoint = maxAllowedSessionNumber;
const maxAllowedSubscriptionNumber = argv.maxAllowedSubscriptionNumber  || 50;
opcua.OPCUAServer.MAX_SUBSCRIPTION = maxAllowedSubscriptionNumber;

const userManager = {
    isValidUser: function (userName, password) {

        if (userName === "user1" && password === "password1") {
            return true;
        }
        if (userName === "user2" && password === "password2") {
            return true;
        }
        return false;
    }
};



const keySize = argv.keySize;


//const server_certificate_file            = constructFilename("certificates/server_cert_"+ keySize +".pem");
const server_certificate_file              = constructFilename("certificates/server_selfsigned_cert_"+ keySize +".pem");
//const server_certificate_file            = constructFilename("certificates/server_selfsigned_cert_"+ keySize +".pem");
//const server_certificate_file            = constructFilename("certificates/server_cert_"+ keySize +"_outofdate.pem");
const server_certificate_privatekey_file   = constructFilename("certificates/server_key_"+ keySize +".pem");


console.log(" server certificate : ", server_certificate_file);

const server_options = {

    certificateFile: server_certificate_file,
    privateKeyFile: server_certificate_privatekey_file,

    port: port,
    //xx (not used: causes UAExpert to get confused) resourcePath: "UA/Server",

    maxAllowedSessionNumber: maxAllowedSessionNumber,
    maxConnectionsPerEndpoint: maxConnectionsPerEndpoint,

    nodeset_filename: [
        opcua.nodesets.standard_nodeset_file,
        opcua.nodesets.di_nodeset_filename
    ],

    serverInfo: {
        applicationUri: makeApplicationUrn(get_fully_qualified_domain_name(), "NodeOPCUA-Server"),
        productUri: "NodeOPCUA-Server",
        applicationName: {text: "NodeOPCUA" ,locale:"en"},
        gatewayServerUri: null,
        discoveryProfileUri: null,
        discoveryUrls: []
    },
    buildInfo: {
        buildNumber: "1234"
    },
    serverCapabilities: {
        maxBrowseContinuationPoints: 10,
        maxHistoryContinuationPoints: 10,
        // maxInactiveLockTime
        operationLimits: {
            maxNodesPerRead: 1000,
            maxNodesPerWrite: 1000,
            maxNodesPerHistoryReadData: 100,
            maxNodesPerBrowse: 1000,
        }
    },
    userManager: userManager,
    
    isAuditing: false,

    //registerServerMethod: opcua.RegisterServerMethod.HIDDEN,
    //registerServerMethod: opcua.RegisterServerMethod.MDNS,
    registerServerMethod: opcua.RegisterServerMethod.LDS,

};

process.title = "Node OPCUA Server on port : " + server_options.port;

server_options.alternateHostname = argv.alternateHostname;

const server = new OPCUAServer(server_options);

const endpointUrl = server.endpoints[0].endpointDescriptions()[0].endpointUrl;

const hostname = require("os").hostname();


server.on("post_initialize", function () {

    opcua.build_address_space_for_conformance_testing(server.engine.addressSpace);

    install_optional_cpu_and_memory_usage_node(server);

    const addressSpace = server.engine.addressSpace;

    const rootFolder = addressSpace.findNode("RootFolder");
    assert(rootFolder.browseName.toString() === "Root");

    const namespace = addressSpace.getOwnNamespace();

    const myDevices = namespace.addFolder(rootFolder.objects, {browseName: "MyDevices"});


    /*
     * variation 0:
     * ------------
     *
     * Add a variable in folder using a raw Variant.
     * Use this variation when the variable has to be read or written by the OPCUA clients
     */
    const variable0 = namespace.addVariable({
        organizedBy: myDevices,
        browseName: "FanSpeed",
        nodeId: "ns=1;s=FanSpeed",
        dataType: "Double",
        value: new Variant({dataType: DataType.Double, value: 1000.0})
    });

    setInterval(function () {
        const fluctuation = Math.random() * 100 - 50;
        variable0.setValueFromSource(new Variant({dataType: DataType.Double, value: 1000.0 + fluctuation}));
    }, 10);


    /*
     * variation 1:
     * ------------
     *
     * Add a variable in folder using a single get function which returns the up to date variable value in Variant.
     * The server will set the timestamps automatically for us.
     * Use this variation when the variable value is controlled by the getter function
     * Avoid using this variation if the variable has to be made writable, as the server will call the getter
     * function prior to returning its value upon client read requests.
     */
    namespace.addVariable({
        organizedBy: myDevices,
        browseName: "PumpSpeed",
        nodeId: "ns=1;s=PumpSpeed",
        dataType: "Double",
        value: {
            /**
             * returns the  current value as a Variant
             * @method get
             * @return {Variant}
             */
            get: function () {
                const pump_speed = 200 + 100 * Math.sin(Date.now() / 10000);
                return new Variant({dataType: DataType.Double, value: pump_speed});
            }
        }
    });

    namespace.addVariable({
        organizedBy: myDevices,
        browseName: "SomeDate",
        nodeId: "ns=1;s=SomeDate",
        dataType: "DateTime",
        value: {
            get: function () {
                return new Variant({dataType: DataType.DateTime, value: new Date(Date.UTC(2016, 9, 13, 8, 40, 0))});
            }
        }
    });


    /*
     * variation 2:
     * ------------
     *
     * Add a variable in folder. This variable gets its value and source timestamps from the provided function.
     * The value and source timestamps are held in a external object.
     * The value and source timestamps are updated on a regular basis using a timer function.
     */
    const external_value_with_sourceTimestamp = new opcua.DataValue({
        value: new Variant({dataType: DataType.Double, value: 10.0}),
        sourceTimestamp: null,
        sourcePicoseconds: 0
    });
    setInterval(function () {
        external_value_with_sourceTimestamp.value.value = Math.random();
        external_value_with_sourceTimestamp.sourceTimestamp = new Date();
    }, 1000);

    namespace.addVariable({
        organizedBy: myDevices,
        browseName: "Pressure",
        nodeId: "ns=1;s=Pressure",
        dataType: "Double",
        value: {
            timestamped_get: function () {
                return external_value_with_sourceTimestamp;
            }
        }
    });


    /*
     * variation 3:
     * ------------
     *
     * Add a variable in a folder. This variable gets its value  and source timestamps from the provided
     * asynchronous function.
     * The asynchronous function is called only when needed by the opcua Server read services and monitored item services
     *
     */

    namespace.addVariable({
        organizedBy: myDevices,
        browseName: "Temperature",
        nodeId: "s=Temperature",
        dataType: "Double",

        value: {
            refreshFunc: function (callback) {

                const temperature = 20 + 10 * Math.sin(Date.now() / 10000);
                const value = new Variant({dataType: DataType.Double, value: temperature});
                const sourceTimestamp = new Date();

                // simulate a asynchronous behaviour
                setTimeout(function () {
                    callback(null, new DataValue({value: value, sourceTimestamp: sourceTimestamp}));
                }, 100);
            }
        }
    });

    // UAAnalogItem
    // add a UAAnalogItem
    const node = namespace.addAnalogDataItem({

        organizedBy: myDevices,

        nodeId: "s=TemperatureAnalogItem",
        browseName: "TemperatureAnalogItem",
        definition: "(tempA -25) + tempB",
        valuePrecision: 0.5,
        engineeringUnitsRange: {low: 100, high: 200},
        instrumentRange: {low: -100, high: +200},
        engineeringUnits: opcua.standardUnits.degree_celsius,
        dataType: "Double",
        value: {
            get: function () {
                return new Variant({dataType: DataType.Double, value: Math.random() + 19.0});
            }
        }
    });


   const m3x3 =  namespace.addVariable({
        organizedBy: addressSpace.rootFolder.objects,
        nodeId: "s=Matrix",
        browseName: "Matrix",
        dataType: "Double",
        valueRank: 2,
        arrayDimensions: [3, 3],
        value: {
            get: function(){
                return new opcua.Variant({
                    dataType: opcua.DataType.Double,
                    arrayType: opcua.VariantArrayType.Matrix,
                    dimensions: [3, 3],
                    value: [1, 2, 3, 4, 5, 6, 7, 8, 9]
                });
            }
        }
    });

    const xyz =  namespace.addVariable({
        organizedBy: addressSpace.rootFolder.objects,
        nodeId: "s=Position",
        browseName: "Position",
        dataType: "Double",
        valueRank: 1,
        arrayDimensions: null,
        value: {
            get: function(){
                return new opcua.Variant({
                    dataType: opcua.DataType.Double,
                    arrayType: opcua.VariantArrayType.Array,
                    value: [1, 2, 3, 4]
                });
            }
        }
    });


    //------------------------------------------------------------------------------
    // Add a view
    //------------------------------------------------------------------------------
    const view = namespace.addView({
        organizedBy: rootFolder.views,
        browseName: "MyView"
    });

    view.addReference({
        referenceType:"Organizes",
        nodeId: node.nodeId
    });

});


function dumpObject(obj) {
    function w(str, width) {
        const tmp = str + "                                        ";
        return tmp.substr(0, width);
    }

    return _.map(obj, function (value, key) {
        return "      " + w(key, 30).green + "  : " + ((value === null) ? null : value.toString());
    }).join("\n");
}


console.log("  server PID          :".yellow, process.pid);
console.log("  silent              :".yellow, argv.silent);

server.start(function (err) {
    if (err) {
        console.log(" Server failed to start ... exiting");
        process.exit(-3);
    }
    console.log("  server on port      :".yellow, server.endpoints[0].port.toString().cyan);
    console.log("  endpointUrl         :".yellow, endpointUrl.cyan);

    console.log("  serverInfo          :".yellow);
    console.log(dumpObject(server.serverInfo));
    console.log("  buildInfo           :".yellow);
    console.log(dumpObject(server.engine.buildInfo));

    console.log("\n  server now waiting for connections. CTRL+C to stop".yellow);

    if (argv.silent) {
        console.log(" silent");
        console.log = function() {};
    }
    //  console.log = function(){};

});

server.on("create_session", function (session) {
    console.log(" SESSION CREATED");
    console.log("    client application URI: ".cyan, session.clientDescription.applicationUri);
    console.log("        client product URI: ".cyan, session.clientDescription.productUri);
    console.log("   client application name: ".cyan, session.clientDescription.applicationName.toString());
    console.log("   client application type: ".cyan, session.clientDescription.applicationType.toString());
    console.log("              session name: ".cyan, session.sessionName ? session.sessionName.toString() : "<null>");
    console.log("           session timeout: ".cyan, session.sessionTimeout);
    console.log("                session id: ".cyan, session.sessionId);
});

server.on("session_closed", function (session, reason) {
    console.log(" SESSION CLOSED :", reason);
    console.log("              session name: ".cyan, session.sessionName ? session.sessionName.toString() : "<null>");
});

function w(s, w) {
    return ("000" + s).substr(-w);
}
function t(d) {
    return w(d.getHours(), 2) + ":" + w(d.getMinutes(), 2) + ":" + w(d.getSeconds(), 2) + ":" + w(d.getMilliseconds(), 3);
}

server.on("response", function (response) {

    if (argv.silent) { return;}

    console.log(t(response.responseHeader.timeStamp), response.responseHeader.requestHandle,
        response._schema.name.cyan, " status = ", response.responseHeader.serviceResult.toString().cyan);
    switch (response._schema.name) {
        case "xxModifySubscriptionResponse":
        case "xxCreateMonitoredItemsResponse":
        case "xxModifyMonitoredItemsResponse":
        case "xxRepublishResponse":
        case "xxCreateSessionResponse":
        case "xxActivateSessionResponse":
        case "xxCloseSessionResponse":
        case "xxBrowseResponse":
        case "xxCreateSubscriptionResponse":
        case "xxTranslateBrowsePathsToNodeIdsResponse":
        case "xxSetPublishingModeResponse":
        case "WriteResponse":
            console.log(response.toString());
            break;
        case "xxPublishResponse":
            console.log(response.toString());
            console.log("PublishResponse.subscriptionId = ",response.subscriptionId.toString());
            break;
    }

});

function indent(str, nb) {
    const spacer = "                                             ".slice(0, nb);
    return str.split("\n").map(function (s) {
        return spacer + s;
    }).join("\n");
}
server.on("request", function (request, channel) {

    if (argv.silent) { return;}

    console.log(t(request.requestHeader.timeStamp), request.requestHeader.requestHandle,
        request._schema.name.yellow, " ID =", channel.secureChannelId.toString().cyan);
    switch (request._schema.name) {
        case "xxModifySubscriptionRequest":
        case "xxCreateMonitoredItemsRequest":
        case "xxModifyMonitoredItemsRequest":
        case "xxRepublishRequest":
            console.log(request.toString());
            break;
        case "xxReadRequest":
            const str = "    ";
            if (request.nodesToRead) {
                request.nodesToRead.map(function (node) {
                    str += node.nodeId.toString() + " " + node.attributeId + " " + node.indexRange;
                });
            }
            console.log(str);
            break;
        case "WriteRequest":
            console.log(request.toString());
           break;
           if (request.nodesToWrite) {
                const lines = request.nodesToWrite.map(function (node) {
                    return "     " + node.nodeId.toString().green + " " + node.attributeId + " " + node.indexRange + "\n" + indent("" + node.value.toString(), 10) + "\n";
                });
                console.log(lines.join("\n"));
            }
            break;

        case "xxTranslateBrowsePathsToNodeIdsRequest":
        case "xxBrowseRequest":
        case "xxCreateSessionRequest":
        case "xxActivateSessionRequest":
        case "xxCloseSessionRequest":
        case "xxCreateSubscriptionRequest":
        case "xxSetPublishingModeRequest":
            // do special console output
            //console.log(util.inspect(request, {colors: true, depth: 10}));
            console.log(request.toString());
            break;
        case "xxPublishRequest":
            console.log(request.toString());
            break;
    }
});

process.on("SIGINT", function () {
    // only work on linux apparently
    console.error(" Received server interruption from user ".red.bold);
    console.error(" shutting down ...".red.bold);
    server.shutdown(1000, function () {
        console.error(" shutting down completed ".red.bold);
        console.error(" done ".red.bold);
        console.error("");
        process.exit(-1);
    });
});

const discovery_server_endpointUrl = "opc.tcp://" + hostname + ":4840/UADiscovery";

console.log("\nregistering server to :".yellow + discovery_server_endpointUrl);

server.on("serverRegistered",function() {
    console.log("server has been registered");
});
server.on("serverUnregistered",function() {
    console.log("server has been unregistered");
});
server.on("serverRegistrationRenewed",function() {
    console.log("server registration has been renewed");
});
server.on("serverRegistrationPending",function() {
    console.log("server registration is still pending (is Local Discovery Server up and running ?)");
});


server.on("newChannel",function(channel) {
    console.log("Client connected with address = ".bgYellow,channel.remoteAddress," port = ",channel.remotePort);
});

server.on("closeChannel",function(channel) {
    console.log("Client disconnected with address = ".bgCyan,channel.remoteAddress," port = ",channel.remotePort);
    if ( global.gc) {
        global.gc();
    }
});