APIs

Show:
"use strict";
const _ = require("underscore");
const assert = require("node-opcua-assert").assert;
require("should");

const opcua = require("node-opcua");

const OPCUAServer = opcua.OPCUAServer;
const StatusCodes = opcua.StatusCodes;
const Variant = opcua.Variant;
const DataType = opcua.DataType;
const DataValue = opcua.DataValue;
const is_valid_endpointUrl = opcua.is_valid_endpointUrl;

const debugLog = require("node-opcua-debug").make_debugLog(__filename);

const address_space_for_conformance_testing = require("node-opcua-address-space-for-conformance-testing");
const build_address_space_for_conformance_testing = address_space_for_conformance_testing.build_address_space_for_conformance_testing;




/**
 * add a fake analog data item for testing
 * @method addTestUAAnalogItem
 *
 * @param parentNode
 */
function addTestUAAnalogItem(parentNode) {

//xx    assert(parentNode instanceof opcua.BaseNode);

    const addressSpace = parentNode.addressSpace;
    const namespace = addressSpace.getOwnNamespace();

    // add a UAAnalogItem
    namespace.addAnalogDataItem({
        componentOf: parentNode,
        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 userManager = {
    isValidUser: function (userName, password) {

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

/**
 * @method build_server_with_temperature_device
 *
 * create and start a fake OPCUA Server that exposes a temperature set point variable.
 *
 *    the SetPoint temperature can be set on the server side by accessing the
 *    'set_point_temperature'  of the return server object.
 *
 *    the SetPoint temperature can be accessed as a Read/Write variable by a opcua client
 *    as "Root/MyDevices/SetPointTemperature". The node id of this variable is stored into
 *    the 'temperatureVariableId' of the server object.
 *
 * @param options {options}
 * @param [options.add_simulation = false] {Boolean} add the simulation nodes in the server
 * @param done {callback}
 * @return {OPCUAServer}
 */
function build_server_with_temperature_device(options, done) {

    assert(_.isFunction(done, "expecting a callback function"));
    assert(typeof opcua.nodesets.standard_nodeset_file === "string");

    //xx console.log("xxx building server with temperature device");

    // use mini_nodeset_filename for speed up if not otherwise specified
    options.nodeset_filename = options.nodeset_filename ||
        [
            opcua.nodesets.standard_nodeset_file
        ];

    options.userManager = userManager;

    const server = new OPCUAServer(options);
    // we will connect to first server end point

    server.on("session_closed", function (session, reason) {
        //xx console.log(" server_with_temperature_device has closed a session :",reason);
        //xx console.log("              session name: ".cyan,session.sessionName.toString());
    });

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

        const addressSpace = server.engine.addressSpace;

        const namespace = addressSpace.getOwnNamespace();

        const myDevices = namespace.addFolder("ObjectsFolder", {browseName: "MyDevices"});
        assert(myDevices.browseName.toString() === "1:MyDevices");

        // create a variable with a string namepsace
        const variable0 = namespace.addVariable({
            componentOf: myDevices,
            browseName: "FanSpeed",
            nodeId: "s=FanSpeed",
            dataType: "Double",
            value: new Variant({dataType: DataType.Double, value: 1000.0})
        });
        assert(variable0.nodeId.toString() === "ns=1;s=FanSpeed");

        const setPointTemperatureId = "s=SetPointTemperature";
        // install a Read/Write variable representing a temperature set point of a temperature controller.
        server.temperatureVariableId = namespace.addVariable({
            organizedBy: myDevices,
            browseName: "SetPointTemperature",
            nodeId: setPointTemperatureId,
            dataType: "Double",
            value: {
                get: function () {
                    return new Variant({dataType: DataType.Double, value: server.set_point_temperature});
                },
                set: function (variant) {
                    // to do : test if variant can be coerce to Float or Double
                    server.set_point_temperature = parseFloat(variant.value);
                    return StatusCodes.Good;
                }
            }
        });

        // install a Read-Only variable defined with a fancy Opaque nodeid
        const pumpSpeedId = "b=0102030405060708090a0b0c0d0e0f10";

        server.pumpSpeed = namespace.addVariable({
            componentOf: myDevices,
            browseName: "PumpSpeed",
            nodeId: pumpSpeedId,
            dataType: "Double",
            value: {
                get: function () {
                    const pump_speed = 200 + Math.random();
                    return new Variant({dataType: DataType.Double, value: pump_speed});
                },
                set: function (variant) {
                    return StatusCodes.BadNotWritable;
                }
            }
        });
        assert(server.pumpSpeed.nodeId.toString() === "ns=1;"+pumpSpeedId);

        const endpointUrl = server.endpoints[0].endpointDescriptions()[0].endpointUrl;
        debugLog("endpointUrl", endpointUrl);
        is_valid_endpointUrl(endpointUrl).should.equal(true);

        if (options.add_simulation) {
            build_address_space_for_conformance_testing(server.engine.addressSpace);
        }

        // add a Analog Data Item
        addTestUAAnalogItem(myDevices);


        // add a variable that can be written asynchronously
        const asyncWriteNodeId = "s=AsynchronousVariable";
        let asyncValue = 46;

        server.asyncWriteNode = namespace.addVariable({
            componentOf: myDevices,
            browseName: "AsynchronousVariable",
            nodeId: asyncWriteNodeId,
            dataType: "Double",

            value: {
                // asynchronous read
                refreshFunc: function (callback) {

                    const dataValue = new DataValue({
                        value: {
                            dataType: DataType.Double,
                            value: asyncValue
                        },
                        sourceTimestamp: new Date()
                    });
                    // simulate a asynchronous behaviour
                    setTimeout(function () {
                        callback(null, dataValue);
                    }, 100);
                },
                set: function (variant) {
                    setTimeout(function () {
                        asyncValue = variant.value;
                    }, 1000);
                    return StatusCodes.GoodCompletesAsynchronously;
                }
            }

        });


        // add a variable that can be written asynchronously and that supports TimeStamps and StatusCodes
        const asyncWriteFullNodeId = "s=AsynchronousFullVariable";
        let asyncWriteFull_dataValue = {
            statusCode: StatusCodes.BadWaitingForInitialData
        };

        server.asyncWriteNode = namespace.addVariable({
            componentOf: myDevices,
            browseName: "AsynchronousFullVariable",
            nodeId: asyncWriteFullNodeId,
            dataType: "Double",

            value: {
                // asynchronous read
                timestamped_get: function (callback) {
                    assert(_.isFunction(callback), "callback must be a function");
                    setTimeout(function () {
                        callback(null, asyncWriteFull_dataValue);
                    }, 100);
                },
                // asynchronous write
                // in this case, we are using timestamped_set and not set
                // as we want to control and deal with the dataValue provided by the client write
                // This will allow us to handle more specifically timestamps and statusCodes
                timestamped_set: function (dataValue, callback) {
                    assert(_.isFunction(callback), "callback must be a function");
                    //xxx console.log(" DATA VALUE !!!".cyan,dataValue.toString().yellow);
                    setTimeout(function () {
                        asyncWriteFull_dataValue = new DataValue(dataValue);
                        callback();
                    }, 500);
                }
            }

        });

    });

    server.set_point_temperature = 20.0;

    function start(done) {
        server.start(function (err) {

            if (err) {
                return done(err);
            }

            assert(server.engine.status === "initialized");

            done();

        });
    }

    start(done);
    return server;
}

exports.build_server_with_temperature_device = build_server_with_temperature_device;