#!/usr/bin/env node
/* eslint no-process-exit: 0 */
"use strict";
const readline = require("readline");
const treeify = require("treeify");
require("colors");
const sprintf = require("sprintf");
const util = require("util");
const fs = require("fs");
const path = require("path");
const _ = require("underscore");
const opcua = require("node-opcua");
const UAProxyManager = opcua.UAProxyManager;
const utils = opcua.utils;
const assert = require("node-opcua-assert").assert;
console.log(" Version ", opcua.version);
const sessionTimeout = 2 * 60 * 1000; // 2 minutes
const client = new opcua.OPCUAClient({
requestedSessionTimeout: sessionTimeout,
keepSessionAlive: true
});
let the_session = null;
let proxyManager = null;
let crawler = null;
let dumpPacket = false;
const dumpMessageChunk = false;
let endpoints_history = [];
const endpoints_history_file = path.join(__dirname, ".history_endpoints");
let curNode = null;
let curNodeCompletion = [];
function save_endpoint_history(callback) {
if (endpoints_history.length > 0) {
fs.writeFileSync(endpoints_history_file, endpoints_history.join("\n"), "ascii");
}
if (callback) {
callback();
}
}
function add_endpoint_to_history(endpoint) {
if (endpoints_history.indexOf(endpoint) >= 0) {
return;
}
endpoints_history.push(endpoint);
save_endpoint_history();
}
let lines = [];
if (fs.existsSync(endpoints_history_file)) {
lines = fs.readFileSync(endpoints_history_file, "ascii");
endpoints_history = lines.split(/\r\n|\n/);
}
const history_file = path.join(__dirname, ".history");
function completer(line,callback) {
let completions, hits;
if ( (line.trim() === "" ) && curNode) {
// console.log(" completions ",completions);
let c = [".."].concat(curNodeCompletion);
if (curNodeCompletion.length === 1) {
c = curNodeCompletion;
}
return callback(null, [ c, line]);
}
if ("open".match(new RegExp("^" + line.trim()))) {
completions = ["open localhost:port"];
return callback(null, [ completions, line]);
} else {
if (the_session === null) {
if (client._secureChannel) {
completions = "createSession cs getEndpoints gep quit".split(" ");
} else {
completions = "open quit".split(" ");
}
} else {
completions = "browse read readall crawl closeSession disconnect quit getEndpoints".split(" ");
}
}
assert(completions.length >= 0);
hits = completions.filter(function (c) {
return c.indexOf(line) === 0;
});
return callback(null,[hits.length ? hits : completions, line]);
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer
});
let the_prompt = ">".cyan;
rl.setPrompt(the_prompt);
rl.prompt();
function save_history(callback) {
const history_uniq = _.uniq(rl.history);
fs.writeFileSync(history_file, history_uniq.join("\n"), "ascii");
callback();
}
function w(str,width) {
return (str + " ").substr(0,width);
}
/**
* @method toDate
* @param str
*
* @example
* toDate("now");
* toDate("13:00"); => today at 13:00
* toDate("1 hour ago"); => today one our ago....
*
* @return {Date}
*/
function toDate(str) {
console.log(" parsing : '" + str + "'");
const now = new Date();
if (!str) {
return now;
}
if (str.toLowerCase() === "now") {
return now;
}
// check if provided date is <HH>:<MM>
const t = /([0-9]{1,2}):([0-9]{1,2})/;
const tt = str.match(t);
if (tt) {
// HH:MM of current date
const year = now.getFullYear();
const month = now.getMonth(); // 0 : jan , 1: feb etc ...
const day = now.getDate();
const hours = parseInt(tt[1]);
const minutes = parseInt(tt[2]);
const seconds = 0;
const date = new Date(year,month,day,hours,minutes,seconds);
return date;
}
// check if provided date looks like "3 hours ago" | "1 day ago" etc...
const r = /([0-9]*)(day|days|d|hours|hour|h|minutes|minutes|m)\s?((ago)?)/;
const m = str.match(r);
if (m) {
let tvalue = parseInt(m[1]);
switch(m[2][0]) {
case "d":
tvalue *= 24*3600;
break;
case "h":
tvalue *= 3600;
break;
case "m":
tvalue *= 60;
break;
default:
throw new Error(" invalidate date");
}
return new Date(now - tvalue*1000);
} else {
return new Date(str);
}
}
function log()
{
rl.pause();
rl.clearLine(process.stdout);
const str =_.map(arguments).join(" ");
process.stdout.write(str);
rl.resume();
}
let rootFolder = null;
let nodePath = [];
let nodePathName = [];
const lowerFirstLetter = opcua.utils.lowerFirstLetter;
function setCurrentNode(node) {
curNode = node;
const curNodeBrowseName = lowerFirstLetter(curNode.browseName.name.toString());
nodePathName.push(curNodeBrowseName);
nodePath.push(node);
curNodeCompletion = node.$components.map(function(c) {
if (!c.browseName) {
return "???";
}
return lowerFirstLetter(c.browseName.name.toString());
});
the_prompt = nodePathName.join(".").yellow+">";
rl.setPrompt(the_prompt);
}
function setRootNode(node) {
nodePath = [];
nodePathName = [];
setCurrentNode(node);
}
function moveToChild(browseName) {
if (browseName === "..") {
nodePathName.pop();
curNode = nodePath.splice(-1,1)[0];
the_prompt = nodePathName.join(".").yellow+">";
rl.setPrompt(the_prompt);
return;
}
const child= curNode[browseName];
if (!child) {
return;
}
setCurrentNode(child);
}
function get_root_folder(callback) {
if(!rootFolder) {
rl.pause();
proxyManager.getObject(opcua.makeNodeId(opcua.ObjectIds.RootFolder),function(err,data) {
if (!err) {
rootFolder = data;
assert(rootFolder,"expecting rootFolder");
setRootNode(rootFolder);
rl.resume();
}
callback();
});
} else {
setCurrentNode(rootFolder);
callback();
}
}
client.on("send_chunk", function (message_chunk) {
if (dumpMessageChunk) {
process.stdout.write(">> " + message_chunk.length + "\r");
}
});
client.on("receive_chunk", function (message_chunk) {
if (dumpMessageChunk) {
process.stdout.write("<< " + message_chunk.length + "\r");
}
});
client.on("send_request", function (message) {
if (dumpPacket) {
log(" sending request".red);
opcua.analyze_object_binary_encoding(message);
}
});
client.on("receive_response", function (message) {
if (dumpPacket) {
assert(message);
log(" receive response".cyan.bold);
opcua.analyze_object_binary_encoding(message);
}
});
function dumpNodeResult(node) {
const str = sprintf(" %-30s%s%s", node.browseName.name, (node.isForward ? "->" : "<-"), node.nodeId.displayText());
log(str);
}
function colorize(value) {
return ("" + value).yellow.bold;
}
if (rl.history) {
if (fs.existsSync(history_file)) {
lines = fs.readFileSync(history_file, "ascii");
lines = lines.split(/\r\n|\n/);
}
if (lines.length === 0) {
let hostname = require("os").hostname();
hostname = hostname.toLowerCase();
rl.history.push("open opc.tcp://opcua.demo-this.com:51210/UA/SampleServer");
rl.history.push("open opc.tcp://" + hostname + ":51210/UA/SampleServer");
rl.history.push("open opc.tcp://" + hostname + ":4841");
rl.history.push("open opc.tcp://" + "localhost" + ":51210/UA/SampleServer");
rl.history.push("open opc.tcp://" + hostname + ":6543/UA/SampleServer");
rl.history.push("open opc.tcp://" + hostname + ":53530/OPCUA/SimulationServer");
rl.history.push("b ObjectsFolder");
rl.history.push("r ns=2;s=Furnace_1.Temperature");
} else {
rl.history = rl.history.concat(lines);
}
}
process.on("uncaughtException", function (e) {
util.puts(e.stack.red);
rl.prompt();
});
function apply_command(cmd,func,callback) {
callback = callback || function(){};
rl.pause();
func(function(err) {
callback();
rl.resume();
rl.prompt(the_prompt);
});
}
function apply_on_valid_session(cmd, func ,callback) {
assert(_.isFunction(func));
assert(func.length === 2);
if (the_session) {
apply_command(cmd,function(callback) {
func(the_session,callback);
});
} else {
log("command : ", cmd.yellow, " requires a valid session , use createSession first");
}
}
function dump_dataValues(nodesToRead, dataValues) {
for (let i = 0; i < dataValues.length; i++) {
const dataValue = dataValues[i];
log(" Node : ", (nodesToRead[i].nodeId.toString()).cyan.bold, nodesToRead[i].attributeId.toString());
if (dataValue.value) {
log(" type : ", colorize(dataValue.value.dataType.key));
log(" value: ", colorize(dataValue.value.value));
} else {
log(" value: <null>");
}
log(" statusCode: 0x", dataValue.statusCode.toString(16));
log(" sourceTimestamp: ", dataValue.sourceTimestamp, dataValue.sourcePicoseconds);
}
}
function dump_historyDataValues(nodeToRead,startDate,endDate, historyReadResult) {
log(" Node : ", (nodeToRead.nodeId.toString()).cyan.bold, nodeToRead.attributeId.toString());
log(" startDate : ",startDate);
log(" endDate : ",endDate);
if (historyReadResult.statusCode !== opcua.StatusCodes.Good) {
log(" error ",historyReadResult.statusCode.toString());
return;
}
log("historyReadResult = ",historyReadResult.toString());
const dataValues = historyReadResult.historyData.dataValues;
log(" Length = ",dataValues.length);
if (!dataValues || dataValues.length === 0) {
log(" No Data");
return;
}
if (dataValues.length > 0 && dataValues[0].value) {
log(" type : ", colorize(dataValues[0].value.dataType.key));
}
for (let i = 0; i < dataValues.length; i++) {
const dataValue = dataValues[i];
if (dataValue.value) {
log(
dataValue.sourceTimestamp,
w(dataValue.sourcePicoseconds,4),
colorize(w(dataValue.value.value,15)),
w(dataValue.statusCode.toString(16),16));
} else {
log(" value: <null>" , dataValue.toString());
}
}
}
function open_session(callback) {
if (the_session !== null) {
log(" a session exists already ! use closeSession First");
return callback();
} else {
client.requestedSessionTimeout = sessionTimeout;
client.createSession(function (err, session) {
if (err) {
log("Error : ".red, err);
} else {
the_session = session;
log("session created ", session.sessionId.toString());
proxyManager = new UAProxyManager(the_session);
the_prompt = "session:".cyan + the_session.sessionId.toString().yellow + ">".cyan;
rl.setPrompt(the_prompt);
assert(!crawler);
rl.prompt(the_prompt);
}
callback();
});
client.on("close", function () {
log(" Server has disconnected ".red);
the_session = null;
crawler = null;
});
}
}
function close_session(outer_callback) {
apply_on_valid_session("closeSession", function (session,inner_callback) {
session.close(function (err) {
the_session = null;
crawler = null;
if (!outer_callback) {
inner_callback(err);
} else {
assert(_.isFunction(outer_callback));
outer_callback(inner_callback);
}
});
});
}
function set_debug(flag) {
if (flag) {
dumpPacket = true;
process.env.DEBUG = "ALL";
log(" Debug is ON");
} else {
dumpPacket = true;
delete process.env.DEBUG;
log(" Debug is OFF");
}
}
function process_line(line) {
let nodes;
const args = line.trim().split(/ +/);
const cmd = args[0];
if (curNode) {
moveToChild(cmd);
return;
}
switch (cmd) {
case "debug":
const flag = (!args[1]) ? true : ( ["ON", "TRUE", "1"].indexOf(args[1].toUpperCase()) >= 0);
set_debug(flag);
break;
case "open":
let endpointUrl = args[1];
if (!endpointUrl.match(/^opc.tcp:\/\//)) {
endpointUrl = "opc.tcp://" + endpointUrl;
}
const p = opcua.parseEndpointUrl(endpointUrl);
const hostname = p.hostname;
const port = p.port;
log(" open url : ", endpointUrl);
log(" hostname : ", (hostname || "<null>").yellow);
log(" port : ", port.toString().yellow);
apply_command(cmd,function(callback) {
client.connect(endpointUrl, function (err) {
if (err) {
log("client connected err=", err);
} else {
log("client connected : ", "OK".green);
add_endpoint_to_history(endpointUrl);
save_history(function () {
});
}
callback(err);
});
});
break;
case "fs":
case "FindServers":
apply_command(cmd,function(callback){
client.findServers({}, function (err, servers) {
if (err) {
log(err.message);
}
log(treeify.asTree(servers, true));
callback(err);
});
});
break;
case "gep":
case "getEndpoints":
apply_command(cmd,function(callback){
client.getEndpoints(function (err, endpoints) {
if (err) {
log(err.message);
}
endpoints = utils.replaceBufferWithHexDump(endpoints);
log(treeify.asTree(endpoints, true));
callback(err);
});
});
break;
case "createSession":
case "cs":
apply_command(cmd,open_session);
break;
case "closeSession":
close_session(function() { });
break;
case "disconnect":
if (the_session) {
close_session(function (callback) {
client.disconnect(function () {
rl.write("client disconnected");
callback();
});
});
} else {
client.disconnect(function () {
rl.write("client disconnected");
});
}
break;
case "b":
case "browse":
apply_on_valid_session(cmd, function (the_session,callback) {
nodes = [args[1]];
the_session.browse(nodes, function (err, nodeResults) {
if (err) {
log(err);
log(nodeResults);
} else {
save_history(function () {
});
for (let i = 0; i < nodeResults.length; i++) {
log("Node: ", nodes[i]);
log(" StatusCode =", nodeResults[i].statusCode.toString(16));
nodeResults[i].references.forEach(dumpNodeResult);
}
}
callback();
});
});
break;
case "rootFolder":
apply_on_valid_session(cmd, function (the_session,callback) {
get_root_folder(callback);
});
break;
case "hr":
case "readHistoryValue":
apply_on_valid_session(cmd, function (the_session,callback ) {
// example:
// hr ns=2;s=Demo.History.DoubleWithHistory 13:45 13:59
nodes = [args[1]];
let startTime = toDate(args[2]);// "2015-06-10T09:00:00.000Z"
let endTime = toDate(args[3]); // "2015-06-10T09:01:00.000Z"
if (startTime>endTime) {
const tmp = endTime;endTime = startTime;startTime =tmp;
}
nodes = nodes.map(opcua.coerceNodeId);
the_session.readHistoryValue(nodes,startTime,endTime, function (err, historyReadResults) {
if (err) {
log(err);
log(historyReadResults.toString());
} else {
save_history(function () {});
assert(historyReadResults.length === 1);
dump_historyDataValues({
nodeId: nodes[0],
attributeId: 13
}, startTime,endTime,historyReadResults[0]);
}
callback();
});
});
break;
case "r":
case "read":
apply_on_valid_session(cmd, function (the_session,callback ) {
nodes = [args[1]];
nodes = nodes.map(opcua.coerceNodeId);
the_session.readVariableValue(nodes, function (err, dataValues) {
if (err) {
log(err);
log(dataValues);
} else {
save_history(function () {
});
dump_dataValues([{
nodeId: nodes[0],
attributeId: 13
}], dataValues);
}
callback();
});
});
break;
case "ra":
case "readall":
apply_on_valid_session(cmd, function (the_session, callback) {
const node = args[1];
the_session.readAllAttributes(node, function (err, result/*,diagnosticInfos*/) {
if (!err) {
save_history(function () {
});
console.log(result);
//xx dump_dataValues(nodesToRead, dataValues);
}
callback();
});
});
break;
case "tb":
apply_on_valid_session(cmd, function (the_session, callback) {
const path = args[1];
the_session.translateBrowsePath(path, function (err, results) {
if (err) {
log(err.message);
}
log(" Path ", path, " is ");
log(util.inspect(results, {colors: true, depth: 100}));
callback();
});
});
break;
case "crawl":
{
apply_on_valid_session(cmd, function (the_session, callback) {
if (!crawler) {
crawler = new opcua.NodeCrawler(the_session);
crawler.on("browsed", function (element) {
// log("->",element.browseName.name,element.nodeId.toString());
});
}
const nodeId = args[1] || "ObjectsFolder";
log("now crawling " + nodeId.yellow + " ...please wait...");
crawler.read(nodeId, function (err, obj) {
if (!err) {
log(" crawling done ");
// todo : treeify.asTree performance is *very* slow on large object, replace with better implementation
//xx log(treeify.asTree(obj, true));
treeify.asLines(obj, true, true, function (line) {
log(line);
});
} else {
log("err ", err.message);
}
crawler = null;
callback();
});
});
}
break;
case ".info":
log(" bytesRead ", client.bytesRead, " bytes");
log(" bytesWritten ", client.bytesWritten, " bytes");
log("transactionsPerformed ", client.transactionsPerformed, "");
// -----------------------------------------------------------------------
// number of subscriptions
// -----------------------------------------------------------------------
// number of monitored items by subscription
break;
case ".quit":
process.exit(0);
break;
default:
if (cmd.trim().length>0) {
log("Say what? I might have heard `" + cmd.trim() + "`");
}
break;
}
}
rl.on("line", function (line) {
try {
process_line(line);
rl.prompt();
}
catch (err) {
log("------------------------------------------------".red);
log(err.message.bgRed.yellow.bold);
log(err.stack);
log("------------------------------------------------".red);
rl.resume();
}
});