APIs

Show:
"use strict";
/**
 * @module opcua.server.simulation
 * @type {async|exports}
 * @class Simulator
 *
 */

const _ = require("underscore");
const assert = require("node-opcua-assert").assert;
const path = require("path");
const fs = require("fs");


const coerceNodeId = require("node-opcua-nodeid").coerceNodeId;
const makeNodeId = require("node-opcua-nodeid").makeNodeId;
const StatusCodes = require("node-opcua-status-code").StatusCodes;
const address_space = require("node-opcua-address-space");
const AddressSpace = address_space.AddressSpace;
const Namespace = address_space.Namespace;

const Variant = require("node-opcua-variant").Variant;
const DataType = require("node-opcua-variant").DataType;
const VariantArrayType = require("node-opcua-variant").VariantArrayType;
const buildVariantArray = require("node-opcua-variant").buildVariantArray;

const findBuiltInType = require("node-opcua-factory").findBuiltInType;
const DataValue = require("node-opcua-data-value").DataValue;


const ec = require("node-opcua-basic-types");
const QualifiedName = require("node-opcua-data-model").QualifiedName;
const LocalizedText = require("node-opcua-data-model").LocalizedText;

const standardUnits = require("node-opcua-data-access").standardUnits;

const add_eventGeneratorObject = require("node-opcua-address-space/test_helpers/add_event_generator_object").add_eventGeneratorObject;

function defaultValidator(/*value*/) {
    return true;
}

function getValidatorFuncForType(dataType) {
    const f = ec["isValid" + dataType];
    return f || defaultValidator;
}

function getRandomFuncForType(dataType) {

    assert(dataType);
    dataType = dataType.key;

    const f = ec["random" + dataType];

    if (f) {
        return f;
    }

    //xx console.log("xxxx dataType  ",dataType);
    switch (dataType) {
        case "Variant":
            return function () {
                return new Variant();
            };
        case "QualifiedName":
            return function () {
                return new QualifiedName({name: ec.randomString()});
            };
        case "LocalizedText":
            return function () {
                return new LocalizedText({text: ec.randomString()});
            };
        case "XmlElement" :
            return function () {
                const element = ec.randomString();
                const content = ec.randomString();
                return "<" + element + ">" + content + "</" + element + ">";
            };
        default:
            // istanbul ignore next
            throw new Error("Cannot find random" + dataType + "() func anywhere");
    }
}


function _findDataType(dataTypeName) {
    const builtInDataTypeName = findBuiltInType(dataTypeName);
    const dataType = DataType[builtInDataTypeName.name];
    // istanbul ignore next
    if (!dataType) {
        throw new Error(" dataType " + dataTypeName + " must exists");
    }
    return dataType;
}

function validate_value_or_array(isArray, variantValue, validatorFunc) {

    assert(_.isFunction(validatorFunc));
    let i, value;
    if (isArray) {

        const n = Math.min(10, variantValue.length);

        for (i = 0; i < n; i++) {
            value = variantValue[i];
            // istanbul ignore next
            if (!validatorFunc(value)) {
                throw new Error("default value must be valid for dataType " + variantValue + " at index " + i + " got " + value);
            }
        }

    } else {
        // scalar
        // istanbul ignore next
        if (!validatorFunc(variantValue)) {
            throw new Error("default value must be valid for dataType " + variantValue);
        }
    }

}

function makeVariant(dataTypeName, isArray, current_value) {

    isArray = (isArray === null) ? false : isArray;
    let arrayType = VariantArrayType.Scalar;
    if (isArray) {
        arrayType = VariantArrayType.Array;
    }
    const dataType = _findDataType(dataTypeName);
    assert(!dataType.isAbstract);

    const validatorFunc = getValidatorFuncForType(dataType);

    validate_value_or_array(isArray, current_value, validatorFunc);

    const variant = new Variant({
        dataType: dataType,
        arrayType: arrayType,
        value: current_value
    });
    return variant;
}

function _add_variable(namespace, parent, varName, dataTypeName, current_value, isArray, extra_name) {

    const addressSpace = namespace.addressSpace;
    assert(typeof extra_name === "string");
    assert(addressSpace instanceof AddressSpace);

    const variant = makeVariant(dataTypeName, isArray, current_value);

    const name = parent.browseName.name.toString() + "_" + varName + extra_name;

    const nodeId = "s=" + name;

    const placeholder = {
        variant: variant
    };

    const variable = namespace.addVariable({
        componentOf: parent,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: nodeId,
        dataType: varName,
        valueRank: isArray ? 1 : -1,
        value: variant
    });
    variable._backdoor_placeholder = placeholder;
    return variable;
}


function add_variable(namespace, parent, name, realType, default_value, extra_name) {

    assert(typeof extra_name === "string");
    const initialValue = _.isFunction(default_value) ? default_value() : default_value;
    const variable = _add_variable(namespace, parent, name, realType, initialValue, false, extra_name);
    assert(variable.valueRank === -1);
    assert(variable.accessLevel.key === "CurrentRead | CurrentWrite");
    assert(variable.userAccessLevel.key === "CurrentRead | CurrentWrite");
    assert(variable.historizing === false);
    return variable;
}

function add_variable_array(namespace, parent, dataTypeName, default_value, realTypeName, arrayLength, extra_name) {

    assert(typeof dataTypeName === "string");
    assert(typeof realTypeName === "string");

    // istanbul ignore next
    if (!DataType[realTypeName]) {
        console.log("dataTypeName", dataTypeName);
        console.log("realTypeName", realTypeName);
    }

    assert(DataType[realTypeName], " expecting a valid real type");
    arrayLength = arrayLength || 10;

    const local_defaultValue = _.isFunction(default_value) ? default_value() : default_value;

    const current_value = buildVariantArray(DataType[realTypeName], arrayLength, local_defaultValue);

    const variable = _add_variable(namespace, parent, dataTypeName, realTypeName, current_value, true, extra_name);

    assert(variable.accessLevel.key === "CurrentRead | CurrentWrite");
    assert(variable.userAccessLevel.key === "CurrentRead | CurrentWrite");
    assert(variable.historizing === false);

}


function add_mass_variables_of_type(namespace, parent, dataTypeName, default_value, realType) {
    // Mass Mass_Boolean -> Mass_Boolean_Boolean_00 ...
    const nodeName = "Scalar_Mass_" + dataTypeName;

    //xx console.log("xxxx adding mass variable ", nodeName);
    const scalarMass_Type = namespace.addObject({
        organizedBy: parent,
        browseName: nodeName,
        description: "This folder will contain 100 items per supported data-type.",
        nodeId: "s=" + nodeName,
    });
    for (let i = 0; i <= 99; i++) {
        const extra_name = "_" + ("00" + i.toString()).substr(-2);
        const local_defaultValue = _.isFunction(default_value) ? default_value() : default_value;
        _add_variable(namespace, scalarMass_Type, dataTypeName, realType, local_defaultValue, false, extra_name);
    }

}

function add_mass_variables(namespace, scalarFolder) {

    const scalarMass = namespace.addFolder(scalarFolder, {
        browseName: "Scalar_Mass",
        description: "This folder will contain 100 items per supported data-type.",
        nodeId: "s=Scalar_Mass"
    });

    typeAndDefaultValue.forEach(function (e) {
        const dataType = e.type;
        const realType = e.realType || dataType;
        add_mass_variables_of_type(namespace, scalarMass, dataType, e.defaultValue, realType);
    });
}

/**
 * @method build_address_space_for_conformance_testing
 * @param namespace {Namespace}
 * @param options
 * @param options.mass_variable {Boolean}
 */
let build_address_space_for_conformance_testing;


const DateTime_Min = new Date();
const typeAndDefaultValue = [
    {type: "Boolean", defaultValue: false},
    {type: "ByteString", defaultValue: Buffer.from("OPCUA")},
    {type: "DateTime", defaultValue: DateTime_Min},
    {type: "Double", defaultValue: 0.0},
    {type: "Float", defaultValue: 0.0},
    {type: "Guid", defaultValue: ec.emptyGuid},
    {type: "SByte", defaultValue: 0},
    {type: "Int16", defaultValue: 0},
    {type: "Int32", defaultValue: 0},
    {
        type: "NodeId", defaultValue: function () {
            return coerceNodeId("ns=" + 3+ ";g=00000000-0000-0000-0000-000000000023");
        }
    },
    {type: "String", defaultValue: "OPCUA"},
    {type: "Byte", defaultValue: 0},
    {type: "UInt16", defaultValue: 0},
    {type: "UInt32", defaultValue: 0},
    {type: "Duration", realType: "Double", defaultValue: 0.0},
    {type: "Number", realType: "UInt16", defaultValue: 0},// Number is abstract
    {type: "Integer", realType: "Int64", defaultValue: 0},// because Integer is abstract , we choose Int32
    {type: "UInteger", realType: "UInt64", defaultValue: 0},
    {
        type: "UtcTime", realType: "DateTime", defaultValue: function () {
            return new Date();
        }
    },
//xx        {  type: "Int64",         defaultValue:  0},
    {type: "LocaleId", realType: "String", defaultValue: ""},
    {
        type: "LocalizedText", defaultValue: function () {
            return new LocalizedText({});
        }
    },


    {
        type: "QualifiedName", defaultValue: function () {
            return new QualifiedName();
        }
    },
    {type: "Time", realType: "String", defaultValue: "00:00:00"},
    {type: "UInt64", defaultValue: [0, 0]},
    {type: "Int64", defaultValue: [0, 0]},
    //xx {type: "Variant",   realType:   "Variant", defaultValue:  {} },
    {type: "XmlElement", defaultValue: "<string1>OPCUA</string1>"},
    {type: "ImageBMP", realType: "ByteString", defaultValue: null},
    {type: "ImageGIF", realType: "ByteString", defaultValue: null},
    {type: "ImageJPG", realType: "ByteString", defaultValue: null},
    {type: "ImagePNG", realType: "ByteString", defaultValue: null},
    // {type: "Enumeration", realType: "UInt32" , defaultValue:0}
];


function add_simulation_variables(namespace, scalarFolder) {


    let values_to_change = [];

    function add_simulation_variable(parent, dataTypeName, defaultValue, realTypeName) {

        // the type of the default value
        realTypeName = realTypeName || dataTypeName;

        const dataType = _findDataType(realTypeName);
        const randomFunc = getRandomFuncForType(dataType);

        // istanbul ignore next
        if (!_.isFunction(randomFunc)) {
            throw new Error("a random function must exist for basicType " + dataTypeName);
        }

        const variable = _add_variable(namespace, parent, dataTypeName, realTypeName, defaultValue, false, "");

        const value_to_change = {
            dataType: dataType,
            variable: variable,
            randomFunc: randomFunc
        };

        values_to_change.push(value_to_change);

        return variable;
    }

    const simulation = namespace.addObject({
        organizedBy: scalarFolder,
        browseName: "Scalar_Simulation",
        description: "This folder will contain one item per supported data-type.",
        nodeId: "s=Scalar_Simulation"
    });


    // add simulation variables
    typeAndDefaultValue.forEach(function (e) {
        const dataType = e.type;
        const defaultValue = _.isFunction(e.defaultValue) ? e.defaultValue() : e.defaultValue;
        const realType = e.realType || dataType;
        add_simulation_variable(simulation, dataType, defaultValue, realType);
    });


    // add management nodes
    let interval = 2000;
    let enabled = true;
    let timer;


    function change_randomly() {

        values_to_change.forEach(function (element) {

            const variant = element.variable._backdoor_placeholder.variant;
            variant.value = element.randomFunc();
            element.variable.setValueFromSource(variant);

        });
    }

    function delete_Timer() {
        // delete previous timer if any
        if (timer) {
            clearInterval(timer);
            timer = null;
        }
    }

    function install_Timer() {
        delete_Timer();
        assert(!timer);
        if (enabled) {
            timer = setInterval(function () {
                change_randomly();
            }, interval);
        }
    }

    function tearDown_Timer() {
        delete_Timer();
        values_to_change = [];
    }

    const intervalVariable = namespace.addVariable({
        componentOf: simulation,
        browseName: "Interval",
        description: {locale: "en", text: "The rate (in msec) of change for all Simulated items."},
        nodeId: "s=Scalar_Simulation_Interval",
        dataType: "UInt16",
        value: new Variant({
            dataType: DataType.UInt16,
            arrayType: VariantArrayType.Scalar,
            value: interval
        })
    });

    intervalVariable.on("value_changed", function (dataValue/*,indexRange*/) {
        const variant = dataValue.value;
        assert(variant instanceof Variant);
        assert(ec.isValidUInt16(variant.value) && " value must be valid for dataType");
        interval = variant.value;
        install_Timer();
    });


    const enabledVariable = namespace.addVariable({
        componentOf: simulation,
        browseName: "Enabled",
        description: {locale: "en", text: "Enabled"},
        nodeId: "s=Scalar_Simulation_Enabled",
        dataType: "Boolean",
        value: new Variant({
            dataType: DataType.Boolean,
            arrayType: VariantArrayType.Scalar,
            value: enabled
        })
    });
    enabledVariable.on("value_changed", function (dataValue/*,indexRange*/) {
        const variant = dataValue.value;
        assert(variant instanceof Variant);
        assert(ec.isValidBoolean(variant.value) && " value must be valid for dataType");
        enabled = variant.value;
        install_Timer();
    });
    install_Timer();

    const addressSpace = namespace.addressSpace;
    addressSpace.registerShutdownTask(tearDown_Timer);

}

function add_scalar_static_variables(namespace, scalarFolder) {

    const scalarStatic = namespace.addObject({
        organizedBy: scalarFolder,
        browseName: "Scalar_Static",
        description: "This folder will contain one item per supported data-type.",
        nodeId: "s=Scalar_Static"
    });

    // add statics scalar Variables
    typeAndDefaultValue.forEach(function (e) {
        const dataType = e.type;
        const realType = e.realType || dataType;

        const defaultValue = _.isFunction(e.defaultValue) ? e.defaultValue() : e.defaultValue;
        add_variable(namespace, scalarStatic, dataType, realType, defaultValue, "");
    });

    function setImage2(imageType, filename) {
        const fullpath = path.join(__dirname, "../data", filename);
        const imageNode = namespace.findNode("s=Scalar_Static_Image" + imageType);

        const options = {
            refreshFunc: function (callback) {
                fs.readFile(fullpath, function (err, data) {
                    if (err) {
                        return callback(null, new DataValue({
                            statusCode: StatusCodes.BadInternalError,
                            value: {dataType: "ByteString", value: null}
                        }));

                    }
                    assert(data instanceof Buffer);
                    callback(null, new DataValue({value: {dataType: "ByteString", value: data}}));
                });
            }
        };
        imageNode.bindVariable(options, /*overwrite=*/true);

    }

    function setImage(imageType, filename) {
        const fullpath = path.join(__dirname, "../data", filename);
        const imageNode = namespace.findNode("s=Scalar_Static_Image" + imageType);
        fs.readFile(fullpath, function (err, data) {
            if (!err) {
                assert(data instanceof Buffer);
                imageNode.setValueFromSource(new Variant({dataType: DataType.ByteString, value: data}));
            } else {
                console.log("cannot load file =", fullpath);
            }
        });
    }

    setImage("BMP", "image.bmp");
    setImage("PNG", "tux.png");
    setImage("GIF", "gif-anime.gif");
    setImage("JPG", "tiger.jpg");


    // add statics Array Variables
    const scalarStaticArray = namespace.addObject({
        organizedBy: scalarFolder,
        browseName: "Scalar_Static_Array",
        description: "Single dimension, suggested size of 10-elements per array. Unsupported types will be missing from the address-space.",
        nodeId: "s=Scalar_Static_Array"
    });
    // add static Array
    typeAndDefaultValue.forEach(function (e) {
        const dataType = e.type;
        const realType = e.realType || dataType;
        add_variable_array(namespace, scalarStaticArray, dataType, e.defaultValue, realType, 10, "");
    });
    // add static Mass

}


function add_access_right_variables(namespace, parentFolder) {

    const accessRight_Folder = namespace.addFolder(parentFolder, {
        browseName: "AccessRight",
        description: "Folder containing various nodes with different access right behavior",
        nodeId: "s=AccessRight"
    });

    const accessLevel_All_Folder = namespace.addFolder(accessRight_Folder, {
        browseName: "AccessLevel",
        description: "Various node with different access right behavior",
        nodeId: "s=AccessLevel"
    });


    let name;

    name = "AccessLevel_CurrentRead";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,

        accessLevel: "CurrentRead",
        userAccessLevel: "CurrentRead",

        value: new Variant({
            dataType: DataType.Int32,
            arrayType: VariantArrayType.Scalar,
            value: 36
        })
    });

    name = "s=AccessLevel_CurrentWrite";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,
        accessLevel: "CurrentWrite",
        userAccessLevel: "CurrentWrite",
        value: {}

    });

    name = "AccessLevel_CurrentRead_NotUser";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,

        accessLevel: "CurrentRead",

        userAccessLevel: "",

        value: new Variant({
            dataType: DataType.Int32,
            arrayType: VariantArrayType.Scalar,
            value: 36
        })
    });

    name = "AccessLevel_CurrentWrite_NotUser";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,

        accessLevel: "CurrentWrite | CurrentRead",

        userAccessLevel: "CurrentRead",

        value: new Variant({
            dataType: DataType.Int32,
            arrayType: VariantArrayType.Scalar,
            value: 36
        })
    });

    name = "AccessLevel_CurrentRead_NotCurrentWrite";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,

        accessLevel: "CurrentRead",

        userAccessLevel: "CurrentRead",

        value: new Variant({
            dataType: DataType.Int32,
            arrayType: VariantArrayType.Scalar,
            value: 36
        })
    });


    name = "AccessLevel_CurrentWrite_NotCurrentRead";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,

        accessLevel: "CurrentWrite",

        userAccessLevel: "CurrentWrite",

        value: new Variant({
            dataType: DataType.Int32,
            arrayType: VariantArrayType.Scalar,
            value: 36
        })
    });

    name = "AccessLevel_DeniedAll";
    namespace.addVariable({
        componentOf: accessLevel_All_Folder,
        browseName: name,
        description: {locale: "en", text: name},
        nodeId: "s=" + name,
        dataType: "Int32",
        valueRank: -1,

        accessLevel: "",
        userAccessLevel: "",

        value: new Variant({
            dataType: DataType.Int32,
            arrayType: VariantArrayType.Scalar,
            value: 36
        })
    });


}

function add_path_10deep(namespace, simulation_folder) {

    let parent = simulation_folder;
    for (let i = 1; i < 15; i++) {
        const name = "Path_" + i.toString() + "Deep";

        const child = namespace.addObject({
            organizedBy: parent,
            browseName: name,
            description: "A folder at the top of " + i + " elements",
            typeDefinition: "FolderType",
            nodeId: "s=" + name
        });
        parent = child;
    }
}

function add_very_large_array_variables(namespace, objectsFolder) {

    // add statics Array Variables
    const scalarStaticLargeArray = namespace.addObject({
        organizedBy: objectsFolder,
        browseName: "Scalar_Static_Large_Array",
        description: "Single dimension, suggested size of 100k-elements per array.",
        nodeId: "s=Scalar_Static_Large_Array"
    });
    typeAndDefaultValue.forEach(function (e) {
        const dataType = e.type;
        const realType = e.realType || dataType;
        add_variable_array(namespace, scalarStaticLargeArray, dataType, e.defaultValue, realType, 50 * 1024, "");
    });

}

//      BaseDataVariableType
//         |
//      DataItemType
//         ^
//         |
//      +----------------+---------------------+
//      |                |                     |
// ArrayItemType   AnalogItemType         DiscreteItemType
//                                             ^
//                                             |
//                +-----------------------------+---------------------------------+
//                |                             |                                 |
//           TwoStateDiscreteType     MultiStateDiscreteType                MutliStateValueDiscreteType
//
function add_analog_data_items(namespace, parentFolder) {


    function _addDataItem(localParentFolder, dataType, initialValue) {

        // istanbul ignore next
        if (!(DataType[dataType])) {
            throw new Error(" Invalid dataType " + dataType);
        }

        const name = dataType + "DataItem";
        const nodeId = "s=" + name;

        namespace.addDataItem({
            componentOf: localParentFolder,
            nodeId: nodeId,
            browseName: name,
            definition: "(tempA -25) + tempB",
            dataType: dataType,
            value: new Variant({
                arrayType: VariantArrayType.Scalar,
                dataType: DataType[dataType],
                value: initialValue
            })
        });
    }

    function _addAnalogDataItem(localParentFolder, dataType, initialValue) {

        // istanbul ignore next
        if (!(DataType[dataType])) {
            throw new Error(" Invalid dataType " + dataType);
        }

        const name = dataType + "AnalogDataItem";
        const nodeId = "s=" + name;
        // UAAnalogItem
        // add a UAAnalogItem
        namespace.addAnalogDataItem({

            componentOf: localParentFolder,

            nodeId: nodeId,
            browseName: name,
            definition: "(tempA -25) + tempB",
            valuePrecision: 0.5,
            engineeringUnitsRange: {low: -200, high: 200},
            instrumentRange: {low: -200, high: 200},
            engineeringUnits: standardUnits.degree_celsius,
            dataType: dataType,
            value: new Variant({
                arrayType: VariantArrayType.Scalar,
                dataType: DataType[dataType],
                value: initialValue
            })
        });
    }

    function _addArrayAnalogDataItem(localParentFolder, dataType, initialValue) {
        // istanbul ignore next
        if (!(DataType[dataType])) {
            throw new Error(" Invalid dataType " + dataType);
        }
        const name = dataType + "ArrayAnalogDataItem";
        const nodeId = "s=" + name;
        // UAAnalogItem
        // add a UAAnalogItem
        namespace.addAnalogDataItem({

            componentOf: localParentFolder,

            nodeId: nodeId,
            browseName: name,
            definition: "(tempA -25) + tempB",
            valuePrecision: 0.5,
            engineeringUnitsRange: {low: -200, high: 200},
            instrumentRange: {low: -200, high: 200},
            engineeringUnits: standardUnits.degree_celsius,
            dataType: dataType,
            value: new Variant({
                arrayType: VariantArrayType.Array,
                dataType: DataType[dataType],
                value: [initialValue, initialValue]
            })
        });

    }

    // add statics Array Variables
    const analogItemFolder = namespace.addObject({
        organizedBy: parentFolder,
        browseName: "Simulation_AnalogDataItem",
        typeDefinition: "FolderType",
        nodeId: "s=Simulation_AnalogDataItem"
    });

    const name = "DoubleAnalogDataItemWithEU";
    const nodeId = "s=" + name;

    namespace.addAnalogDataItem({

        componentOf: analogItemFolder,
        nodeId: nodeId,
        browseName: name,
        definition: "(tempA -25) + tempB",
        valuePrecision: 0.5,
        engineeringUnitsRange: {low: 100, high: 200},
        instrumentRange: {low: -100, high: +200},
        engineeringUnits: standardUnits.degree_celsius,
        dataType: "Double",

        value: new Variant({
            dataType: DataType.Double,
            value: 19.5
        })
    });


    const data = [
        {dataType: "Double", value: 3.14},
        {dataType: "Float", value: 3.14},
        {dataType: "Int16", value: -10},
        {dataType: "UInt16", value: 10},
        {dataType: "Int32", value: -100},
        {dataType: "UInt32", value: 100},
        {dataType: "Int64", value: [0, 0]},
        {dataType: "UInt64", value: [0, 0]},
        {dataType: "Byte", value: 120},
        {dataType: "SByte", value: -123},
        {dataType: "String", value: "some string"},
        {dataType: "DateTime", value: new Date()}
    ];

    data.forEach(function (e) {
        _addAnalogDataItem(analogItemFolder, e.dataType, e.value);
    });
    data.forEach(function (e) {
        _addDataItem(analogItemFolder, e.dataType, e.value);
    });
    data.forEach(function (e) {
        _addArrayAnalogDataItem(analogItemFolder, e.dataType, e.value);
    });


}

function getDADiscreteTypeFolder(namespace, parentFolder) {

    const name = "Simulation_DA_DiscreteType";
    const nodeId = "s=Simulation_DA_DiscreteType";

    let node = parentFolder.getFolderElementByName(name);
    if (!node) {

        node = namespace.addObject({
            organizedBy: parentFolder,
            typeDefinition: "FolderType",
            browseName: name,
            nodeId: nodeId
        });
    }
    return node;
}

function add_two_state_discrete_variables(namespace, parentFolder) {

    const DADiscreteTypeFolder = getDADiscreteTypeFolder(namespace, parentFolder);

    const twoStateDiscrete001 = namespace.addTwoStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=TwoStateDiscrete001",
        browseName: "TwoStateDiscrete001",
        trueState: "Enabled",
        falseState: "Disabled"
    });


    const twoStateDiscrete002 = namespace.addTwoStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=TwoStateDiscrete002",
        browseName: "TwoStateDiscrete002",
        trueState: "On",
        falseState: "Off",
        optionals: ["TransitionTime", "EffectiveDisplayName"]
    });

    const twoStateDiscrete003 = namespace.addTwoStateDiscrete({
        browseName: "twoStateDiscrete003",
        nodeId: "s=TwoStateDiscrete003",
        optionals: ["TransitionTime"],
        isTrueSubStateOf: twoStateDiscrete002
    });

    const twoStateDiscrete004 = namespace.addTwoStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=TwoStateDiscrete004",
        browseName: "TwoStateDiscrete004",
        trueState: "InProgress",
        falseState: "Stopped"
    });

    const twoStateDiscrete005 = namespace.addTwoStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=TwoStateDiscrete005",
        browseName: "TwoStateDiscrete005",
        trueState: "InProgress",
        falseState: "Stopped"
    });

    twoStateDiscrete001.setValueFromSource({dataType: "Boolean", value: false});
    twoStateDiscrete002.setValueFromSource({dataType: "Boolean", value: false});
    twoStateDiscrete003.setValueFromSource({dataType: "Boolean", value: false});
    twoStateDiscrete004.setValueFromSource({dataType: "Boolean", value: false});
    twoStateDiscrete005.setValueFromSource({dataType: "Boolean", value: false});

}

function add_multi_state_discrete_variable(namespace, parentFolder) {

    assert(namespace instanceof Namespace);

    const DADiscreteTypeFolder = getDADiscreteTypeFolder(namespace, parentFolder);

    //MultiStateDiscrete001
    const multiStateDiscrete001 = namespace.addMultiStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=MultiStateDiscrete001",
        browseName: "MultiStateDiscrete001",
        enumStrings: ["Red", "Orange", "Green"],
        value: 1 // Orange
    });


    //MultiStateDiscrete002
    namespace.addMultiStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=MultiStateDiscrete002",
        browseName: "MultiStateDiscrete002",
        enumStrings: ["Red", "Orange", "Green"],
        value: 1 // Orange
    });

    //MultiStateDiscrete002
    namespace.addMultiStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=MultiStateDiscrete003",
        browseName: "MultiStateDiscrete003",
        enumStrings: ["Red", "Orange", "Green"],
        value: 1 // Orange
    });

    //MultiStateDiscrete002
    namespace.addMultiStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=MultiStateDiscrete004",
        browseName: "MultiStateDiscrete004",
        enumStrings: ["Red", "Orange", "Green"],
        value: 1 // Orange
    });

    //MultiStateDiscrete002
    namespace.addMultiStateDiscrete({
        organizedBy: DADiscreteTypeFolder,
        nodeId: "s=MultiStateDiscrete005",
        browseName: "MultiStateDiscrete005",
        enumStrings: ["Red", "Orange", "Green"],
        value: 1 // Orange
    });

}


function add_multi_state_value_discrete_variables(namespaceDemo, parentFolder) {

    const multistateValueDiscreteTypeFolder = namespaceDemo.addObject({
        organizedBy: parentFolder,
        typeDefinition: "FolderType",
        browseName: "Simulation_DA_MultiStateValueDiscreteType",
        nodeId: "s=Simulation_DA_MultiStateValueDiscreteType"
    });

    function _add_multi_state_variable(parentFolder, dataType) {

        const name = dataType + "MultiStateValueDiscrete";
        const nodeId = "s=" + name;

        const prop = namespaceDemo.addMultiStateValueDiscrete({
            organizedBy: parentFolder,
            browseName: name,
            nodeId: nodeId,
            dataType: dataType,
            enumValues: {"Red": 0xFF0000, "Orange": 0xFF9933, "Green": 0x00FF00, "Blue": 0x0000FF},
            value: 0xFF0000 // Red
        });

    }

    const data = [
        {dataType: "Int16", value: -10},
        {dataType: "UInt16", value: 10},
        {dataType: "Int32", value: -100},
        {dataType: "UInt32", value: 100},
        {dataType: "Int64", value: [0, 0]},
        {dataType: "UInt64", value: [0, 0]},
        {dataType: "Byte", value: 120},
        {dataType: "SByte", value: -123}
    ];
    data.forEach(function (e) {
        _add_multi_state_variable(multistateValueDiscreteTypeFolder, e.dataType);
    });

}

function add_ObjectWithMethod(namespace, parentFolder) {


    const myObject = namespace.addObject({
        nodeId: "s=ObjectWithMethods",
        organizedBy: parentFolder,
        browseName: "ObjectWithMethods"
    });

    const methodNoArgs = namespace.addMethod(myObject, {
        browseName: "MethodNoArgs",
        nodeId: "s=MethodNoArgs",
        //xx inputArguments: [],
        //xx outputArguments: []
    });
    assert(makeNodeId("MethodNoArgs", namespace.index).toString().match(/s=MethodNoArgs/));
    assert(methodNoArgs.nodeId.toString().match(/s=MethodNoArgs/));

    methodNoArgs.bindMethod(function (inputArguments, context, callback) {
        // console.log(require("util").inspect(context).toString());
        const callMethodResult = {
            statusCode: StatusCodes.Good,
            outputArguments: []
        };
        callback(null, callMethodResult);
    });


    const methodIO = namespace.addMethod(myObject, {

        ///xx modellingRule: "Mandatory",

        browseName: "MethodIO",
        nodeId: makeNodeId("MethodIO", namespace.index),

        inputArguments: [
            {
                name: "ShutterLag",
                description: {text: "specifies the number of seconds to wait before the picture is taken "},
                dataType: DataType.UInt32
            }
        ],

        outputArguments: [
            {
                name: "Result",
                description: {text: "the result"},
                dataType: "Int32"
            }
        ]
    });
    methodIO.bindMethod(function (inputArguments, context, callback) {
        // console.log(require("util").inspect(context).toString());
        const callMethodResult = {
            statusCode: StatusCodes.Good,
            outputArguments: [
                {
                    dataType: DataType.Int32,
                    value: 42
                }
            ]
        };
        callback(null, callMethodResult);
    });

    const methodI = namespace.addMethod(myObject, {

        ///xx modellingRule: "Mandatory",

        browseName: "MethodI",
        nodeId: "s=MethodI",

        inputArguments: [
            {
                name: "ShutterLag",
                description: {text: "specifies the number of seconds to wait before the picture is taken "},
                dataType: DataType.UInt32
            }
        ],
        //xx outputArguments: []

    });
    methodI.bindMethod(function (inputArguments, context, callback) {
        // console.log(require("util").inspect(context).toString());
        const callMethodResult = {
            statusCode: StatusCodes.Good,
            outputArguments: []
        };
        callback(null, callMethodResult);
    });

    const methodO = namespace.addMethod(myObject, {

        ///xx modellingRule: "Mandatory",

        browseName: "MethodO",
        nodeId: "s=MethodO",

        //xx inputArguments: [],
        outputArguments: [
            {
                name: "Result",
                description: {text: "the result"},
                dataType: "Int32"
            }
        ]

    });
    methodO.bindMethod(function (inputArguments, context, callback) {
        // console.log(require("util").inspect(context).toString());
        const callMethodResult = {
            statusCode: StatusCodes.Good,
            outputArguments: [
                {
                    dataType: DataType.Int32,
                    value: 42
                }
            ]
        };
        callback(null, callMethodResult);
    });

}


function add_enumeration_variable(namespaceDemo, parentFolder) {

    const addressSpace = namespaceDemo.addressSpace;

    const myEnumType = namespaceDemo.addEnumerationType({
        browseName: "SimulationEnumerationType",
        enumeration: [
            {value: 1, displayName: "RUNNING"},
            {value: 2, displayName: "BLOCKED"},
            {value: 3, displayName: "IDLE"},
            {value: 4, displayName: "UNDER MAINTENANCE"}
        ]
    });

    // now instantiate a variable that have this type.
    const e = namespaceDemo.addVariable({
        organizedBy: parentFolder,
        propertyOf: addressSpace.rootFolder.objects.server.venderServerInfos,
        dataType: myEnumType,
        browseName: "RunningState",
        value: {
            get: function () {
                return new Variant({dataType: DataType.Int32, value: 1})
            }
        }
    });

}

function add_trigger_nodes(namespace, parentFolder) {

    const addressSpace = namespace.addressSpace;

    // add 2 nodes that generate an event when ever they are written to.
    function _add_trigger_node(browseName, nodeId) {
        const triggerNode = namespace.addVariable({
            browseName: browseName,
            nodeId: nodeId,
            organizedBy: parentFolder,
            dataType: "Double",
            typeDefinition: makeNodeId(68)
        });

        let value = 100.0;
        const getFunc = function () {
            return new Variant({
                dataType: DataType.Double,
                value: value
            });
        };
        const setFunc = function (variant) {

            value = variant.value;

            const server = addressSpace.rootFolder.objects.server;

            server.raiseEvent("1:MyEventType", {
                message: {
                    dataType: DataType.LocalizedText,
                    value: {text: "Hello World"}
                },
                severity: {
                    dataType: DataType.UInt32,
                    value: 32
                }

            });
        };

        const options = {
            get: getFunc,
            set: setFunc
        };
        triggerNode.bindVariable(options);
    }

    const triggerNode01 = _add_trigger_node("TriggerNode01", "s=TriggerNode01");

    const triggerNode02 = _add_trigger_node("TriggerNode02", "s=TriggerNode02");
}

function add_sampleView(namespace) {

    const addressSpace = namespace.addressSpace;
    namespace.addView({
        organizedBy: addressSpace.rootFolder.views,
        browseName: "SampleView",
        nodeId: "s=SampleView"
    });
}

build_address_space_for_conformance_testing = function (addressSpace, options) {


    const namespace = addressSpace.registerNamespace("urn://node-opcua-simulator");

    options = options || {};
    options.mass_variable = options.mass_variable || false;

    assert(addressSpace instanceof AddressSpace);

    const objectsFolder = addressSpace.findNode("ObjectsFolder");

    const simulationFolder = namespace.addFolder(objectsFolder, "Simulation");

    add_access_right_variables(namespace, simulationFolder);

    const scalarFolder = namespace.addFolder(simulationFolder, {
        browseName: "Scalar",
        description: "Simply a parent folder"
    });

    add_scalar_static_variables(namespace, scalarFolder);
    if (options.mass_variables) {
        add_mass_variables(namespace, scalarFolder);
    }
    add_simulation_variables(namespace, scalarFolder);

    add_very_large_array_variables(namespace, scalarFolder);

    add_analog_data_items(namespace, simulationFolder);

    add_path_10deep(namespace, simulationFolder);

    add_ObjectWithMethod(namespace, simulationFolder);

    add_eventGeneratorObject(namespace, simulationFolder);

    add_sampleView(namespace);

    add_enumeration_variable(namespace, simulationFolder);

    add_multi_state_value_discrete_variables(namespace, simulationFolder);

    add_two_state_discrete_variables(namespace, simulationFolder);

    add_multi_state_discrete_variable(namespace, simulationFolder);

    add_trigger_nodes(namespace, simulationFolder);

};
exports.build_address_space_for_conformance_testing = build_address_space_for_conformance_testing;