#!/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")
.number("keySize")
.describe("keySize","certificate keySize [1024|2048|3072|4096]")
.default("keySize",2048)
.alias("k","keySize")
.string("applicationName")
.describe("applicationName","the application name")
.default("applicationName","NodeOPCUA-Server")
.alias("a", "alternateHostname")
.alias("m", "maxAllowedSessionNumber")
.alias("n","applicationName")
.alias("p", "port")
.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_selfsigned_cert_"+ keySize +".pem");
const server_certificate_privatekey_file = constructFilename("certificates/server_key_"+ keySize +".pem");
console.log(" server certificate : ", server_certificate_file);
const productUri= argv.applicationName || "NodeOPCUA-Server";
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(), productUri),
productUri: productUri,
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,
//xx registerServerMethod: opcua.RegisterServerMethod.HIDDEN,
//xx 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();
}
});