Skip to content

Instantly share code, notes, and snippets.

@nOy39
Last active August 16, 2018 12:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nOy39/882ab1d6e9db334c09eff5c16a41c037 to your computer and use it in GitHub Desktop.
Save nOy39/882ab1d6e9db334c09eff5c16a41c037 to your computer and use it in GitHub Desktop.
/*!
* Honcho
*
* Multiple PLC Manager
*
*/
/**
* Module dependencies
*/
var crypto = require('crypto')
, fs = require('fs')
, util = require('util')
, path = require('path')
, async = require('async')
;
/**
* Expose
*/
exports = module.exports;
/**
* Current active subscription collection
*/
var subscriptions = {};
/**
* PLC interface collection
*/
var controllers = {};
/**
* Regular Expression for path matching tags to controllers
*/
var TAGREG = new RegExp(/^([\w-]+)\/(.+)$/);
/**
* TAG CACHE
*/
var tagCache = {};
/**
* Tag File Cache
* Pointers to Conntroller Tags keyed on the tagfile path
* Allows for multiple controllers to use the same lookup saving memory and
* startup time on large tag files.
*/
var tagFileLookCache = {};
/**
* Absolute path to tag file directory
*/
var tagFileDir;
/**
* Honcho
* Public API
*/
exports.configure = function(config, cb){
console.log('configuring ' + config.controllers.length +' controllers');
tagFileDir = config.tagFileDir;
async.each(config.controllers, function(ctrl, cback) {
if(ctrl){
controllers[ctrl.connection_name] = new Controller(ctrl, function(){
cback();
});
}else{
cback();// Could cback('Null controller')?
}
}, function(err) {
if (err) {
console.log('An error was received processing controllers in honcho');
}
controllers['default'] = controllers[config.defaultController];
cb();
});
}
function bounce(n){return n;}
function Controller(conf, cb){
var self = this
, tagfile
, cparams;
var Conn = require(conf.type);
self.conn = new Conn();
self.conn.setTranslationCB(bounce);
cparams = conf; // Passing the entire object, not just port and host, allows protocol-specific options
// bind controller functions to Connection
['addItems','removeItems', 'readAllItems', 'findItem', 'writeItems'].forEach(function(method){
self[method] = self.conn[method].bind(self.conn);
});
//console.log(self);
tagfile = path.join(tagFileDir, conf.tagfile);
if(tagFileLookCache[tagfile]){
//console.log('Loading Shared Tags '+tagfile);
self.tags = tagFileLookCache[tagfile];
self.conn.initiateConnection(cparams, cb);
}else{
var l,m, input;
self.tags = {};
input = fs.createReadStream(tagfile);
readLines(input, function(data, done){
l = data.toString().replace(/\s/g,'');
m = l.match(/^([A-Z|a-z|0-1|_].+)=(.+)$/);
if(m){
self.tags[m[1]]=m[2];
}
if(done){
tagFileLookCache[tagfile] = self.tags;
self.conn.initiateConnection(cparams, cb);
}
});
}
}
Controller.prototype.simulateSlowReadAllItems = function(cb){
var self = this;
setTimeout(function(){
self.readAllItems(cb);
}, 500);
}
Controller.prototype.createPoll = function(packet, cb, timeout){
var self = this
, values = packet.values
, tags = packet.tags
, tick;
self.addItems(values);
function poll(ts){
tick = Date.now();
// toggle this to simulate slow results
//self.simulateSlowReadAllItems(
// console.log(self.ts);
self.readAllItems(
function(){
var results = {}, value;
for(var i=0;i<values.length;i++){
value = self.findItem(values[i]);
if(typeof value === 'undefined'){
results[tags[i]] = 'UNDF';
}
else if(value.quality != 'OK'){
results[tags[i]] = value.quality;
}
else{
results[tags[i]] = value.value;
}
}
diff = Math.max(timeout-(Date.now()-tick),0);
diff = Math.min(diff,timeout); // In case clock changes while running
//console.log(diff);
cb(null, results);
// console.log('@@@ Poll', self.ts == ts);
if(self.ts == ts){
self.ts = Date.now();
self.pollId = setTimeout(poll,diff, self.ts);
}
// console.log('@@@ Poll NEW', self.pollId);
});
}
poll();
}
Controller.prototype.clearPoll = function(){
var self = this;
self.ts = null;
clearTimeout(self.pollId);
// console.log('!!! clearPoll', self.pollId);
}
exports.findItem = function(tags, cb){
tags = [].concat(tags);
generateControllerPackets(tags, function(err, controllerPackets){
if(err){
console.log(err);
}
findPackets(controllerPackets, cb);
});
}
exports.read = function(tags, cb){
// register tags for lookup against controller
if(typeof tags !== 'string' && toString.call(tags) !== '[object Array]'){
cb(new Error('@tags must to be a String or an Array'));
return;
}
// clone tags
tags = [].concat(tags);
generateControllerPackets(tags, function(err, controllerPackets){
if(err){
console.log("ERROR:", err);
}
//console.log('controllerPackets', controllerPackets);
readPackets(controllerPackets, cb);
});
}
exports.write = function(tag, value, cb){
tagLookup(tag, function(err, tagObj) {
if(!err) {
// TODO: improve security
// if (tagObj.ctrl && controllers[tagObj.ctrl].cparams.allowWrite) {
console.log('Writing value ' + value + ' to '+ util.format(tagObj));
controllers[tagObj.ctrl].writeItems(tagObj.value, value, cb);
// } else {
// console.log('Not writing value ' + value + ' to '+ util.format(tagObj) + ' due to error (no allowWrite?)!!!');
// }
}
// process.exit();
});
}
exports.createSubscription = function(tags, cb, timeout, callCBAnyway){
if(typeof timeout === 'undefined'){
timeout = 0;
}
// skip match requests for the same tag data
token = crypto.createHash('md5').update(String(tags)).digest('hex');
if(subscriptions[token]){
return;
}
generateControllerPackets(tags, function(err, controllerPackets){
subscriptions[token] = {
ts:Date.now(),
controllerPackets: controllerPackets
}
if(err){
console.log(err);
}
Object.keys(controllerPackets).forEach(function(key) {
if (controllers[key]) {
controllers[key].createPoll(controllerPackets[key], cb, timeout);
} else if (callCBAnyway) {
setInterval(function() {
cb(null, {});
}, timeout);
}
});
});
return token;
}
exports.removeSubscription = function(token){
var controllerPacketSubscription = subscriptions[token];
if(typeof controllerPacketSubscription === 'undefined'){
throw new Error('A valid token must be supplied to remove a subscription');
}
Object.keys(controllerPacketSubscription.controllerPackets).forEach(function(key) {
controllers[key] && controllers[key].clearPoll(); // It is possible to have undefined controllers here now.
});
}
/**
* Get Tags
*/
exports.browse = function(parent, cb){
var tagset = [];
if(typeof cb === "undefined" && typeof parent === "function") {
cb = parent;
}
if(typeof cb !== "function"){
throw new Error("You must specify a callback");
}
if(typeof parent === 'string'){
// TODO: filter tags by parent
}else{
// clone root tags
}
cb(tagset);
}
exports.alltags = function(){
var self = this;
var tagobj = {};
Object.keys(controllers).forEach(function(ct){
if (ct !== "default") {
tagobj[ct] = controllers[ct].tags;
}
});
return tagobj;
}
/**
* INTERNAL functions
*/
/**
*
*/
function findPackets(controllerPackets, cb){
Object.keys(controllerPackets).forEach(function(key) {
var values = controllerPackets[key];
//console.log(values);
controllers[key].findItem(values,cb);
});
}
/**
* Sends all tags to a controllers
*/
// FIXME: tmp solution
var XXX_CACHE = {};
function readPackets(controllerPackets,cb){
// should we wait for everyone
// could add a for wait
Object.keys(controllerPackets).forEach(function(key) {
var values = controllerPackets[key].values;
var tags = controllerPackets[key].tags;
//console.log('TAGS:',tags);
var shasum = crypto.createHash('sha1');
shasum.update(String(values));
var token = shasum.digest('hex');
if(!XXX_CACHE[token]){
XXX_CACHE[token] = values;
controllers[key].addItems(values);
}
controllers[key].readAllItems(
function(){
var results = {}, value;
for(var i=0;i<values.length;i++){
value = controllers[key].findItem(values[i]);
if(typeof value === 'undefined'){
results[tags[i]] = 'UNDF';
}
else if(value.quality != 'OK'){
results[tags[i]] = value.quality;
}
else{
results[tags[i]] = value.value;
}
}
//setTimeout(cb,1000, null, results);
//cb(null, results);
});
});
}
/**
* Generates an array of tags assocaited to a controller
*/
function generateControllerPackets(tags, cb){
var controllerPackets = {};
function next(tag) {
if(tag) {
tagLookup(tag, function(err, packet){
if(err){
console.log(err);
}else{
var p = controllerPackets[packet.ctrl] || {};
p.values = p.values || [];
p.values.push(packet.value);
p.tags = p.tags || [];
p.tags.push(tag);
controllerPackets[packet.ctrl] = p;
}
return next(tags.shift());
});
} else {
if (typeof (tag) === 'undefined') {
cb(null, controllerPackets);
} else {
return next(tags.shift());
}
}
}
next(tags.shift());
}
/**
* Tag lookup
*/
function tagLookup(tag, cb){
var ctrl,packet,m,t;
packet = tagCache[tag];
if(packet){
//console.log(tag,'from cache');
cb(null, packet);
return;
}
// first check default controller
ctrl = controllers['default'];
if(ctrl && ctrl.tags && ctrl.tags[tag]){
tagCache[tag] = {ctrl:'default',value:ctrl.tags[tag]};
cb(null, tagCache[tag]);
return;
}
// check other controllers
m=TAGREG.exec(tag);
//console.log(m);
if (!m) {
console.log(tag);
//cb(new Error("INVALID TAG FORMAT"));
//return;
}
//
ctrl = controllers[m[1]];
if(typeof ctrl === 'undefined' || typeof ctrl.tags[m[2]] === 'undefined'){
if (typeof ctrl !== 'undefined') {
console.log("ctrl.tags[m[2]]");
console.log(ctrl.tags[m[2]]);
console.log("m[2]");
console.log(m[2]);
} else {
console.log("Undefined controller");
}
// We no longer return on invalid tags. cb(new Error('INVALID TAG'));
// We no longer return on invalid tags. return;
if (!ctrl) {
ctrl = {};
}
if (!ctrl.tags) {
ctrl.tags = {};
}
ctrl.tags[m[2]] = {};
ctrl.tags[m[2]].value = "UNDF";
}
tagCache[tag] = {ctrl:m[1],value:ctrl.tags[m[2]]};
cb(null, tagCache[tag]);
}
/*
* Load Tagfiles
*/
function readLines(input, func) {
var remaining = '';
input.on('data', function(data) {
remaining += data;
var index = remaining.indexOf('\n');
var last = 0;
while (index > -1) {
var line = remaining.substring(last, index);
last = index + 1;
func(line);
index = remaining.indexOf('\n', last);
}
remaining = remaining.substring(last);
});
input.on('end', function() {
func(remaining, true);
});
}
var honcho = require('honcho');
function readDone(err, vars) {
console.log(vars);
// Or stream to a Websocket, etc
}
honcho.configure({
defaultController: 'TESTPLC',
tagFileDir: '.',
controllers: [
{ host: '192.168.0.1',
connection_name: 'TESTPLC',
port: 102,
slot: 2,
type: 'nodes7',
tagfile: './testplctags.txt' }
],
/* Define one or more tagsets to be subscribed to */
tagsets: ['status'],
/* Define one or more tags to be subscribed to */
tags : {
'MYTAG':{
tagsets:['status']
},
'Real':{
tagsets:['status']
}
}
}, () => {
honcho.createSubscription(['MYTAG','Real'], readDone, 500);
});
MYTAG=DB9,INT2
Real=DB9,REAL8
@nOy39
Copy link
Author

nOy39 commented Aug 16, 2018

На работе дали ТЗ сделать в веб-браузере "инфографику" расходов компонентов на производственном оборудовании, на основном оборудовании у нас стоят PLC серии Siemens, что по сути не играет роли в моем вопросе, для получения этих данных я решил использовать библиотеку honcho, я скачал через npm пакет, установил, сделал файл
plcTest.js запускаю его с Идеи и как видно по скрину данные я получаю... Вопрос как мне внедрить эти данные в store vuex vue.js?
image

@orangeRat
Copy link

GET запросом?
если нужна инициатива от сервера, то WebSocket. Только не голый, а что-нибудь вроде Socket.io или SockJS

@nOy39
Copy link
Author

nOy39 commented Aug 16, 2018

GET запросом?
если нужна инициатива от сервера, то WebSocket. Только не голый, а что-нибудь вроде Socket.io или SockJS

Уточню, у меня идет опрос PLC каждые 500ms, Я пожалуй бы хотел, чтобы это делалось на каком-то легковесном сервере, и веб-морда тупо получала данные от него

@nOy39
Copy link
Author

nOy39 commented Aug 16, 2018

Короче копать в сторону WebSocket (Socket.io или SockJS)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment