UNPKG

node-ads-api

Version:

NodeJS Twincat ADS protocol implementation

1,686 lines (1,516 loc) 66.8 kB
// Copyright (c) 2014 Inando (edit by roccomuso and PLCHome) // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. 'use strict' var debug = require('debug')('node-ads') var net = require('net') var events = require('events') var Buffer = require('safe-buffer').Buffer Buffer.INSPECT_MAX_BYTES = 200 exports.connect = function (options, cb) { var adsClient = getAdsObject(options) adsClient.connect(cb) return adsClient } var getAdsObject = function (options) { var ads = {} ads.connected = false ads.options = parseOptions(options) ads.invokeId = 0 ads.pending = {} ads.symHandlesToRelease = [] ads.notificationsToRelease = [] ads.notifications = {} ads.dataStream = null ads.tcpHeaderSize = 6 ads.amsHeaderSize = 32 var emitter = new events.EventEmitter() ads.adsClient = Object.create(emitter) ads.adsClient.connect = function (cb) { return connect.call(ads, cb) } ads.adsClient.end = function (cb) { return end.call(ads, cb) } ads.adsClient.readDeviceInfo = function (cb) { return readDeviceInfo.call(ads, cb) } ads.adsClient.read = function (handle, cb) { return read.call(ads, handle, cb) } ads.adsClient.write = function (handle, cb) { return write.call(ads, handle, cb) } ads.adsClient.readState = function (cb) { return readState.call(ads, cb) } ads.adsClient.notify = function (handle, cb) { return notify.call(ads, handle, cb) } ads.adsClient.releaseNotificationHandles = function (cb) { return releaseNotificationHandles.call(ads, cb) } ads.adsClient.releaseNotificationHandle = function (handle, cb) { return releaseNotificationHandle.call(ads, handle, cb) } ads.adsClient.writeRead = function (handle, cb) { return writeReadCommand.call(ads, handle, cb) } ads.adsClient.getSymbols = function (cb, raw) { return getSymbols.call(ads, cb, raw) } ads.adsClient.getDatatyps = function (cb) { return getDatatyps.call(ads, cb) } ads.adsClient.multiRead = function (handles, cb) { return multiRead.call(ads, handles, cb) } ads.adsClient.multiWrite = function (handles, cb) { return multiWrite.call(ads, handles, cb) } ads.adsClient.getHandles = function (handles, cb) { return getHandles.call(ads, handles, cb) } Object.defineProperty(ads.adsClient, 'options', { get options () { return ads.options }, set options (v) { ads.options = v } }) return ads.adsClient } var connect = function (cb) { var ads = this var opt = { port: ads.options.port, host: ads.options.host } if (typeof ads.options.localAddress !== 'undefined') { opt.localAddress = ads.options.localAddress } if (typeof ads.options.localPort !== 'undefined') { opt.localPort = ads.options.localPort } if (typeof ads.options.family !== 'undefined') { opt.family = ads.options.family } ads.tcpClient = net.connect( opt, function () { ads.connected = true cb.apply(ads.adsClient) } ) // ads.tcpClient.setKeepAlive(true) ads.tcpClient.setNoDelay(true) ads.dataCallback = function (data) { try { if (ads.dataStream === null) { ads.dataStream = data } else { ads.dataStream = Buffer.concat([ads.dataStream, data]) } checkResponseStream.call(ads) } catch (e) { ads.adsClient.emit('error', e) } } ads.tcpClient.on('data', ads.dataCallback) ads.timeoutCallback = function (t) { ads.connected = false; ads.adsClient.emit('timeout', t) ads.tcpClient.end() } ads.tcpClient.on('timeout', ads.timeoutCallback) ads.errorCallback = function (e) { ads.connected = false; ads.adsClient.emit('error', e) ads.tcpClient.end() } ads.tcpClient.on('error', ads.errorCallback) } var end = function (cb) { var ads = this releaseSymHandles.call(ads, function () { releaseNotificationHandles.call(ads, function () { if (ads.tcpClient) { ads.connected = false ads.tcpClient.removeListener('error', ads.errorCallback) ads.tcpClient.removeListener('timeout', ads.timeoutCallback) ads.tcpClient.removeListener('data', ads.dataCallback) ads.tcpClient.end(function () { ads.tcpClient.destroy() if (typeof cb !== 'undefined') cb.call(ads) }) } else if (typeof cb !== 'undefined') cb.call(ads) }) }) } var ID_READ_DEVICE_INFO = 1 var ID_READ = 2 var ID_WRITE = 3 var ID_READ_STATE = 4 var ID_WRITE_CONTROL = 5 var ID_ADD_NOTIFICATION = 6 var ID_DEL_NOTIFICATION = 7 var ID_NOTIFICATION = 8 var ID_READ_WRITE = 9 var processDataByte = function (inByte) { var ads = this ads._buffer = ads._buffer || [] ads._buffer.push(inByte) var headerSize = ads.tcpHeaderSize + ads.amsHeaderSize if (ads._buffer.length > headerSize) { var length = ads._buffer.readUInt32LE(26) if (ads._buffer.length >= headerSize + length) { ads.dataStream = Buffer.from(ads._buffer) debug('ads:', ads.dataStream) ads._buffer = [] analyseResponse.call(ads) } } } var checkResponseStream = function () { var ads = this if (ads.dataStream !== null) { var headerSize = ads.tcpHeaderSize + ads.amsHeaderSize if (ads.dataStream.length > headerSize) { var length = ads.dataStream.readUInt32LE(26) if (ads.dataStream.length >= headerSize + length) { analyseResponse.call(ads) } } } } var analyseResponse = function () { var ads = this var commandId = ads.dataStream.readUInt16LE(22) var length = ads.dataStream.readUInt32LE(26) var errorId = ads.dataStream.readUInt32LE(30) var invokeId = ads.dataStream.readUInt32LE(34) logPackage.call(ads, 'receiving', ads.dataStream, commandId, invokeId) emitAdsError.call(ads, errorId) var totHeadSize = ads.tcpHeaderSize + ads.amsHeaderSize var data = Buffer.alloc(length) ads.dataStream.copy(data, 0, totHeadSize, totHeadSize + length) if (ads.dataStream.length > totHeadSize + length) { var nextdata = Buffer.alloc(ads.dataStream.length - totHeadSize - length) ads.dataStream.copy(nextdata, 0, totHeadSize + length) ads.dataStream = nextdata } else ads.dataStream = null if (commandId === ID_NOTIFICATION) { // Special case: Notifications are initialised from the server socket getNotificationResult.call(ads, data) } else if (ads.pending[invokeId]) { var cb = ads.pending[invokeId].cb clearTimeout(ads.pending[invokeId].timeout) delete ads.pending[invokeId] if (!cb) { debug(ads.dataStream, invokeId, commandId) throw new Error("Received a response, but I can't find the request") } switch (commandId) { case ID_READ_DEVICE_INFO: getDeviceInfoResult.call(ads, data, cb) break case ID_READ: getReadResult.call(ads, data, cb) break case ID_WRITE: getWriteResult.call(ads, data, cb) break case ID_READ_STATE: getReadStateResult.call(ads, data, cb) break case ID_WRITE_CONTROL: // writeControl.call(ads, data, cb) break case ID_ADD_NOTIFICATION: getAddDeviceNotificationResult.call(ads, data, cb) break case ID_DEL_NOTIFICATION: getDeleteDeviceNotificationResult.call(ads, data, cb) break case ID_READ_WRITE: getWriteReadResult.call(ads, data, cb) break default: throw new Error('Unknown command') } } checkResponseStream.call(ads) } /// //////////////////// ADS FUNCTIONS /////////////////////// var readDeviceInfo = function (cb) { var ads = this var buf = Buffer.alloc(0) var options = { commandId: ID_READ_DEVICE_INFO, data: buf, cb: cb } runCommand.call(ads, options) } var readState = function (cb) { var ads = this var buf = Buffer.alloc(0) var options = { commandId: ID_READ_STATE, data: buf, cb: cb } runCommand.call(ads, options) } var multiRead = function (handles, cb) { var ads = this getHandles.call(ads, handles, function (err, handles) { if (!err) { var countreads = 0 var readlen = 0 handles.forEach(function(handle) { if (!handle.err){ countreads++ readlen += handle.totalByteLength+4 } }) if (countreads>0) { var buf = Buffer.alloc(12*countreads) var index = 0 handles.forEach(function(handle) { if (!handle.err){ buf.writeUInt32LE(handle.indexGroup || ADSIGRP.RW_SYMVAL_BYHANDLE,index) buf.writeUInt32LE(handle.indexOffset || handle.symhandle,index+4) buf.writeUInt32LE(handle.totalByteLength,index+8) index+=12 } }) var commandOptions = { indexGroup: ADSIGRP.SUMUP_READ, indexOffset: countreads, writeBuffer: buf, readLength: readlen, symname: 'multiRead' } writeReadCommand.call(ads, commandOptions, function (err, result) { if (err) { if (typeof cb !== 'undefined') cb.call(ads.adsClient, err) } else { if (result.length > 0) { var resultpos = 0 var handlespos = countreads*4 handles.forEach( function (handle){ if (!handle.err){ var adsError = result.readUInt32LE(resultpos) resultpos+=4 if (adsError!=0) { handle.err = adsError } if (handle.totalByteLength>0) { var integrate = Buffer.alloc(handle.totalByteLength) result.copy(integrate,0,handlespos,handlespos+handle.totalByteLength) integrateResultInHandle(handle, integrate) } handlespos+=handle.totalByteLength } }) } cb.call(ads.adsClient, null, handles) } }) } else { cb.call(ads.adsClient, null, handles) } } else { cb.call(ads.adsClient, err) } }) } var multiWrite = function (handles, cb) { var ads = this getHandles.call(ads, handles, function (err, handles) { if (!err) { var countwrites = 0 var writelen = 0 handles.forEach(function(handle) { if (!handle.err){ countwrites++ writelen+=12+handle.totalByteLength } }) if (countwrites>0) { var buf = Buffer.alloc(writelen) var index = 0 var valindex = 12*countwrites handles.forEach(function(handle) { if (!handle.err){ buf.writeUInt32LE(handle.indexGroup || ADSIGRP.RW_SYMVAL_BYHANDLE,0) buf.writeUInt32LE(handle.indexOffset || handle.symhandle,4) buf.writeUInt32LE(handle.totalByteLength,8) index+=12 getBytesFromHandle(handle) handle.bytes.copy(buf,valindex,0,handle.bytes.length) valindex+=handle.totalByteLength } }) var commandOptions = { indexGroup: ADSIGRP.SUMUP_WRITE, indexOffset: countwrites, writeBuffer: buf, readLength: countwrites*4, symname: 'multiWrite' } writeReadCommand.call(ads, commandOptions, function (err, result) { if (err) { cb.call(ads.adsClient, err) } else { if (result.length > 0) { var resultpos = 0 handles.forEach( function (handle){ if (!handle.err){ var adsError = result.readUInt32LE(resultpos) resultpos+=4 if (adsError!=0) { handle.err = adsError } } }) } cb.call(ads.adsClient, null, handles) } }) } else { cb.call(ads.adsClient, null, handles) } } else { cb.call(ads.adsClient, err) } }) } var getHandles = function (handles, cb) { var ads = this var countsymnames = 0 var buflength = 0 handles.forEach( function (handle){ handle = parseHandle(handle) if (typeof handle.symname !== 'undefined') { countsymnames++ buflength+=17+handle.symname.length } }) if (countsymnames>0){ var buf = Buffer.alloc(buflength) var index = 0 var indexsynname = countsymnames*16 handles.forEach( function (handle){ if (typeof handle.symname === 'undefined') { handle.symname = handle.indexOffset } else { var bufsymname = stringToBuffer(handle.symname) bufsymname.copy(buf,indexsynname,0,bufsymname.length) indexsynname+=bufsymname.length buf.writeUInt32LE(ADSIGRP.GET_SYMHANDLE_BYNAME,index+0) buf.writeUInt32LE(0x00000000,index+4) buf.writeUInt32LE(0x00000004,index+8) buf.writeUInt32LE(bufsymname.length,index+12) index+=16 } }) var commandOptions = { indexGroup: ADSIGRP.SUMUP_READWRITE, indexOffset: countsymnames, writeBuffer: buf, readLength: countsymnames*16, symname: 'getMultiHandle' } writeReadCommand.call(ads, commandOptions, function (err, result) { if (err) { cb.call(ads.adsClient, err) } else { if (result.length > 0) { var resultpos = 0 var handlespos = countsymnames*8 handles.forEach( function (handle){ if (typeof handle.symname !== 'undefined') { var adsError = result.readUInt32LE(resultpos) if (adsError) { handle.err = adsError } var symhandlebyte = result.readUInt32LE(resultpos+4) resultpos+=8 if (symhandlebyte==4) { handle.symhandle = result.readUInt32LE(handlespos) } handlespos+=symhandlebyte var symHandleToRelease = Buffer.alloc(4) symHandleToRelease.writeUInt32LE(handle.symhandle,0) ads.symHandlesToRelease.push(symHandleToRelease) } }) } cb.call(ads.adsClient, null, handles) } }) } else cb.call(ads.adsClient, null, handles) } var read = function (handle, cb) { var ads = this getHandle.call(ads, handle, function (err, handle) { if (!err) { var commandOptions = { indexGroup: handle.indexGroup || ADSIGRP.RW_SYMVAL_BYHANDLE, indexOffset: handle.indexOffset || handle.symhandle, bytelength: handle.totalByteLength, symname: handle.symname } readCommand.call(ads, commandOptions, function (err, result) { if (result) integrateResultInHandle(handle, result) cb.call(ads.adsClient, err, handle) }) } else { cb.call(ads.adsClient, err) } }) } var write = function (handle, cb) { var ads = this getHandle.call(ads, handle, function (err, handle) { if (!err) { getBytesFromHandle(handle) var commandOptions = { indexGroup: handle.indexGroup || ADSIGRP.RW_SYMVAL_BYHANDLE, indexOffset: handle.indexOffset || handle.symhandle, bytelength: handle.totalByteLength, bytes: handle.bytes, symname: handle.symname } writeCommand.call(ads, commandOptions, function (err, result) { cb.call(ads.adsClient, err) }) } else { cb.call(ads.adsClient, err) } }) } var notify = function (handle, cb) { var ads = this getHandle.call(ads, handle, function (err, handle) { if (!err) { var commandOptions = { indexGroup: handle.indexGroup || ADSIGRP.RW_SYMVAL_BYHANDLE, indexOffset: handle.indexOffset || handle.symhandle, bytelength: handle.totalByteLength, transmissionMode: handle.transmissionMode, maxDelay: handle.maxDelay, cycleTime: handle.cycleTime, symname: handle.symname } addNotificationCommand.call(ads, commandOptions, function (err, notiHandle) { if (!err) { if (ads.options.verbose > 0) { debug('Add notiHandle ' + notiHandle) } handle.notifyHandle = notiHandle this.notifications[notiHandle] = handle } if (typeof cb !== 'undefined') { cb.call(ads.adsClient, err) } }) } else if (typeof cb !== 'undefined') cb.call(ads.adsClient, err) }) } var getSymbols = function (cb, raw) { var ads = this var cmdLength = { indexGroup: ADSIGRP.SYM_UPLOADINFO2, indexOffset: 0x00000000, bytelength: 0x30 } var cmdSymbols = { indexGroup: ADSIGRP.SYM_UPLOAD, indexOffset: 0x00000000 } readCommand.call(ads, cmdLength, function (err, result) { if (!err) { var data = result.readInt32LE(4) cmdSymbols.bytelength = data readCommand.call(ads, cmdSymbols, function (err, result) { var symbols = [] var initialPos = 0 if (!err) { while (initialPos < result.length) { var symbol = {} var pos = initialPos var readLength = result.readUInt32LE(pos) initialPos = initialPos + readLength symbol.indexGroup = result.readUInt32LE(pos + 4) symbol.indexOffset = result.readUInt32LE(pos + 8) symbol.size = result.readUInt32LE(pos + 12) symbol.ntype = result.readUInt32LE(pos + 16) //ADST_ ... symbol.something = result.readUInt32LE(pos + 20) var nameLength = result.readUInt16LE(pos + 24) + 1 var typeLength = result.readUInt16LE(pos + 26) + 1 var commentLength = result.readUInt16LE(pos + 28) + 1 pos = pos + 30 var nameBuf = Buffer.alloc(nameLength) result.copy(nameBuf, 0, pos, pos + nameLength) symbol.name = nameBuf.toString('binary', 0, findStringEnd(nameBuf, 0)) pos = pos + nameLength var typeBuf = Buffer.alloc(typeLength) result.copy(typeBuf, 0, pos, pos + typeLength) symbol.type = typeBuf.toString('binary', 0, findStringEnd(typeBuf, 0)) pos = pos + typeLength var commentBuf = Buffer.alloc(commentLength) result.copy(commentBuf, 0, pos, pos + commentLength) symbol.comment = commentBuf.toString('binary', 0, findStringEnd(commentBuf, 0)) pos = pos + commentLength if (!raw && symbol.type.indexOf('ARRAY') > -1) { var re = /ARRAY[\s]+\[([\-\d]+)\.\.([\-\d]+)\][\s]+of[\s]+(.*)/i var m if ((m = re.exec(symbol.type)) !== null) { if (m.index === re.lastIndex) { re.lastIndex++ } m[1] = parseInt(m[1]) m[2] = parseInt(m[2]) for (var i = m[1]; i <= m[2]; i++) { var newSymbol = JSON.parse(JSON.stringify(symbol)) newSymbol.arrayid = i + 0 newSymbol.type = m[3] + '' newSymbol.name += '[' + i + ']' symbols.push(newSymbol) } } } else { symbols.push(symbol) } } } cb.call(ads.adsClient, err, symbols) }) } else { cb.call(ads.adsClient, err) } }) } var getDatatyps = function (cb) { var ads = this var cmdLength = { indexGroup: ADSIGRP.SYM_UPLOADINFO2, indexOffset: 0x00000000, bytelength: 0x30 } var cmdDatatye = { indexGroup: ADSIGRP.SYM_DT_UPLOAD, indexOffset: 0x00000000 } readCommand.call(ads, cmdLength, function (err, result) { if (!err) { var data = result.readInt32LE(12) cmdDatatye.bytelength = data readCommand.call(ads, cmdDatatye, function (err, result) { var datatyps = [] var pos = 0 if (!err) { function getDatatypEntry(datatyps,result,p,index) { var pos = p var datatyp = {} datatyps.push(datatyp) if (index) { datatyp.index = index } var readLength = result.readUInt32LE(pos) datatyp.version = result.readUInt32LE(pos+4) //datatyp.hashValue = result.readUInt32LE(pos+8) //offsGetCode //datatyp.typeHashValue = result.readUInt32LE(pos+12) //offsSetCode datatyp.size = result.readUInt32LE(pos+16) datatyp.offset = result.readUInt32LE(pos+20) datatyp.dataType = result.readUInt32LE(pos+24) var flags = result.readUInt32LE(pos+28) if (flags==2) { datatyp.offs = result.readUInt32LE(pos+20) } var nameLength = result.readUInt16LE(pos + 32) + 1 var typeLength = result.readUInt16LE(pos + 34) + 1 var commentLength = result.readUInt16LE(pos + 36) + 1 var arrayDim = result.readUInt16LE(pos + 38) datatyp.arrayDim = arrayDim var subItems = result.readUInt16LE(pos + 40) datatyp.subItems = subItems pos = pos + 42 var nameBuf = Buffer.alloc(nameLength) result.copy(nameBuf, 0, pos, pos + nameLength) datatyp.name = nameBuf.toString('binary', 0, findStringEnd(nameBuf, 0)) pos = pos + nameLength var typeBuf = Buffer.alloc(typeLength) result.copy(typeBuf, 0, pos, pos + typeLength) datatyp.type = typeBuf.toString('binary', 0, findStringEnd(typeBuf, 0)) pos = pos + typeLength var commentBuf = Buffer.alloc(commentLength) result.copy(commentBuf, 0, pos, pos + commentLength) datatyp.comment = commentBuf.toString('binary', 0, findStringEnd(commentBuf, 0)) pos = pos + commentLength if (arrayDim>0) { datatyp.array = [] for (var i=0; i<arrayDim;i++){ datatyp.array[i] = {lBound: result.readInt32LE(pos), elements: result.readInt32LE(pos+4) } pos = pos+8 } } if (subItems>0) { datatyp.datatyps = [] for (var i=0; i<subItems;i++){ pos = getDatatypEntry(datatyp.datatyps,result,pos,i+1) } } return readLength+p } while (pos < result.length) { pos = getDatatypEntry(datatyps,result,pos) } } cb.call(ads.adsClient, err, datatyps) }) } else { cb.call(ads.adsClient, err) } }) } var getHandle = function (handle, cb) { var ads = this handle = parseHandle(handle) if (typeof handle.symname === 'undefined') { handle.symname = handle.indexOffset cb.call(ads, null, handle) } else { var buf = stringToBuffer(handle.symname) if (typeof handle.symhandle === 'undefined') { var commandOptions = { indexGroup: ADSIGRP.GET_SYMHANDLE_BYNAME, indexOffset: 0x00000000, writeBuffer: buf, readLength: 4, symname: handle.symname } writeReadCommand.call(ads, commandOptions, function (err, result) { if (err) { cb.call(ads, err) } else { if (result.length > 0) { ads.symHandlesToRelease.push(result) handle.symhandle = result.readUInt32LE(0) cb.call(ads, null, handle) } } }) } else cb.call(ads, null, handle) } } var releaseSymHandles = function (cb) { var ads = this if (ads.symHandlesToRelease.length > 0) { var symHandle = ads.symHandlesToRelease.shift() releaseSymHandle.call(ads, symHandle, function () { releaseSymHandles.call(ads, cb) }) } else if (typeof cb !== 'undefined') cb.call(ads) } var releaseSymHandle = function (symhandle, cb) { var ads = this if (ads.connected) { var commandOptions = { indexGroup: ADSIGRP.RELEASE_SYMHANDLE, indexOffset: 0x00000000, bytelength: symhandle.length, bytes: symhandle } writeCommand.call(ads, commandOptions, function (err) { if (typeof cb !== 'undefined') cb.call(ads, err) }) } else if (typeof cb !== 'undefined') cb.call(ads) } var releaseNotificationHandles = function (cb) { var ads = this if (ads.notificationsToRelease.length > 0) { var notificationHandle = ads.notificationsToRelease.shift() deleteDeviceNotificationCommand.call(ads, notificationHandle, function () { releaseNotificationHandles.call(ads, cb) }) } else if (typeof cb !== 'undefined') cb.call(ads) } var releaseNotificationHandle = function (handle,cb) { var ads = this if (handle.notifyHandle === 'undefined'){ throw new Error("The handle doesn't have a notifyHandle!") } var index = ads.notificationsToRelease.indexOf(handle.notifyHandle) if (index>-1) { delete ads.notifications[handle.notifyHandle] ads.notificationsToRelease.splice(index,1) deleteDeviceNotificationCommand.call(ads, handle.notifyHandle, function () { delete handle.notifyHandle if (typeof cb !== 'undefined') cb.call(ads) }) } } /// ///////////////////// COMMANDS /////////////////////// var readCommand = function (commandOptions, cb) { var buf = Buffer.alloc(12) buf.writeUInt32LE(commandOptions.indexGroup, 0) buf.writeUInt32LE(commandOptions.indexOffset, 4) buf.writeUInt32LE(commandOptions.bytelength, 8) var options = { commandId: ID_READ, data: buf, cb: cb, symname: commandOptions.symname } runCommand.call(this, options) } var writeCommand = function (commandOptions, cb) { var buf = Buffer.alloc(12 + commandOptions.bytelength) buf.writeUInt32LE(commandOptions.indexGroup, 0) buf.writeUInt32LE(commandOptions.indexOffset, 4) buf.writeUInt32LE(commandOptions.bytelength, 8) commandOptions.bytes.copy(buf, 12) var options = { commandId: ID_WRITE, data: buf, cb: cb, symname: commandOptions.symname } runCommand.call(this, options) } var addNotificationCommand = function (commandOptions, cb) { var buf = Buffer.alloc(40); buf.writeUInt32LE(commandOptions.indexGroup, 0) buf.writeUInt32LE(commandOptions.indexOffset, 4) buf.writeUInt32LE(commandOptions.bytelength, 8) buf.writeUInt32LE(commandOptions.transmissionMode, 12) buf.writeUInt32LE(commandOptions.maxDelay, 16) buf.writeUInt32LE(commandOptions.cycleTime * 10000, 20) buf.writeUInt32LE(0, 24) buf.writeUInt32LE(0, 28) buf.writeUInt32LE(0, 32) buf.writeUInt32LE(0, 36) var options = { commandId: ID_ADD_NOTIFICATION, data: buf, cb: cb, symname: commandOptions.symname } runCommand.call(this, options) } var writeReadCommand = function (commandOptions, cb) { var buf = Buffer.alloc(16 + commandOptions.writeBuffer.length) buf.writeUInt32LE(commandOptions.indexGroup, 0) buf.writeUInt32LE(commandOptions.indexOffset, 4) buf.writeUInt32LE(commandOptions.readLength, 8) buf.writeUInt32LE(commandOptions.writeBuffer.length, 12) commandOptions.writeBuffer.copy(buf, 16) var options = { commandId: ID_READ_WRITE, data: buf, cb: cb, symname: commandOptions.symname } runCommand.call(this, options) } var deleteDeviceNotificationCommand = function (notificationHandle, cb) { var ads = this if (ads.connected) { var buf = Buffer.alloc(4) buf.writeUInt32LE(notificationHandle, 0) var options = { commandId: ID_DEL_NOTIFICATION, data: buf, cb: cb } runCommand.call(ads, options) } else if (typeof cb !== 'undefined') cb.call(ads) } var runCommand = function (options) { var ads = this var tcpHeaderSize = 6 var headerSize = 32 var offset = 0 if (!options.cb) { throw new Error('A command needs a callback function!') } var header = Buffer.alloc(headerSize + tcpHeaderSize) // 2 bytes resserver (=0) header.writeUInt16LE(0, offset) offset += 2 // 4 bytes length header.writeUInt32LE(headerSize + options.data.length, offset) offset += 4 // 6 bytes: amsNetIdTarget var amsNetIdTarget = ads.options.amsNetIdTarget.split('.') for (var i = 0; i < amsNetIdTarget.length; i++) { if (i >= 6) { throw new Error('Incorrect amsNetIdTarget length!') } amsNetIdTarget[i] = parseInt(amsNetIdTarget[i], 10) header.writeUInt8(amsNetIdTarget[i], offset) offset++ } // 2 bytes: amsPortTarget header.writeUInt16LE(ads.options.amsPortTarget, offset) offset += 2 // 6 bytes amsNetIdSource var amsNetIdSource = ads.options.amsNetIdSource.split('.') for (i = 0; i < amsNetIdSource.length; i++) { if (i >= 6) { throw new Error('Incorrect amsNetIdSource length!') } amsNetIdSource[i] = parseInt(amsNetIdSource[i], 10) header.writeUInt8(amsNetIdSource[i], offset) offset++ } // 2 bytes: amsPortTarget header.writeUInt16LE(ads.options.amsPortSource, offset) offset += 2 // 2 bytes: Command ID header.writeUInt16LE(options.commandId, offset) offset += 2 // 2 bytes: state flags (ads request tcp) header.writeUInt16LE(4, offset) offset += 2 // 4 bytes: length of the data header.writeUInt32LE(options.data.length, offset) offset += 4 // 4 bytes: error code header.writeUInt32LE(0, offset) offset += 4 // 4 bytes: invoke id header.writeUInt32LE(++ads.invokeId, offset) offset += 4 var buf = Buffer.alloc(tcpHeaderSize + headerSize + options.data.length) header.copy(buf, 0, 0) options.data.copy(buf, tcpHeaderSize + headerSize, 0) ads.pending[ads.invokeId] = {cb: options.cb, timeout: setTimeout(function () { delete ads.pending[ads.invokeId] options.cb('timeout') }.bind(ads), ads.options.timeout)} logPackage.call(ads, 'sending', buf, options.commandId, ads.invokeId, options.symname) ads.tcpClient.write(buf) } /// ////////////////// COMMAND RESULT PARSING //////////////////////////// var getDeviceInfoResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) // emitAdsError.call(ads, adsError) var err = getError(adsError) var result = {} if (!err) { try { result = { majorVersion: data.readUInt8(4), // <==== "try catch" because => if plc is offline (e.g.: in debug mode) => RangeError: Index out of range minorVersion: data.readUInt8(5), versionBuild: data.readUInt16LE(6), deviceName: data.toString('binary', 8, findStringEnd(data, 8)) } } catch(error) { cb.call(ads.adsClient, err, result) } } cb.call(ads.adsClient, err, result) } var getReadResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) var result // emitAdsError.call(ads, adsError) var err = getError(adsError) if (!err) { var bytelength = data.readUInt32LE(4) result = Buffer.alloc(bytelength) data.copy(result, 0, 8, 8 + bytelength) } cb.call(ads, err, result) } var getWriteReadResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) var result // emitAdsError.call(ads, adsError) var err = getError(adsError) if (!err) { var bytelength = data.readUInt32LE(4) result = Buffer.alloc(bytelength) data.copy(result, 0, 8, 8 + bytelength) } cb.call(ads, err, result) } var getWriteResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) var err = getError(adsError) // emitAdsError.call(ads, adsError) cb.call(ads, err) } var getReadStateResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) // emitAdsError.call(ads, adsError) var err = getError(adsError) var result if (!err) { result = { adsState: data.readUInt16LE(4), deviceState: data.readUInt16LE(6) } } cb.call(ads.adsClient, err, result) } var getAddDeviceNotificationResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) var notificationHandle // emitAdsError.call(ads, adsError) var err = getError(adsError) if (!err) { notificationHandle = data.readUInt32LE(4) ads.notificationsToRelease.push(notificationHandle) } cb.call(ads, err, notificationHandle) } var getDeleteDeviceNotificationResult = function (data, cb) { var ads = this var adsError = data.readUInt32LE(0) // emitAdsError.call(ads, adsError) var err = getError(adsError) cb.call(ads, err) } var getNotificationResult = function (data) { var ads = this var length = data.readUInt32LE(0) var stamps = data.readUInt32LE(4) var offset = 8 var timestamp = 0 var samples = 0 var notiHandle = 0 var size = 0 for (var i = 0; i < stamps; i++) { timestamp = data.readUInt32LE(offset) // TODO 8 bytes and convert offset += 8 samples = data.readUInt32LE(offset) offset += 4 for (var j = 0; j < samples; j++) { notiHandle = data.readUInt32LE(offset) offset += 4 size = data.readUInt32LE(offset) offset += 4 var buf = Buffer.alloc(size) data.copy(buf, 0, offset) offset += size if (ads.options.verbose > 0) { debug('Get notiHandle ' + notiHandle) } var handle = ads.notifications[notiHandle] // It can happen that there is a notification before I // even have the notification handle. // In that case I just skip this notification. if (handle !== undefined) { integrateResultInHandle(handle, buf) ads.adsClient.emit('notification', handle) } else { if (ads.options.verbose > 0) { debug('skipping notification ' + notiHandle) } } } } } /// ///////////////// HELPERS ///////////////////////////////////////// var stringToBuffer = function (someString) { var buf = Buffer.alloc(someString.length + 1) buf.write(someString) buf[someString.length] = 0 return buf } var parseOptions = function (options) { // Defaults if (typeof options.port === 'undefined') { options.port = 48898 } if (typeof options.amsPortSource === 'undefined') { options.amsPortSource = 32905 } if (typeof options.amsPortTarget === 'undefined') { options.amsPortTarget = 801 } if (typeof options.timeout === 'undefined') { options.timeout = 500 } if (typeof options.host === 'undefined') { throw new Error('host not defined!') } if (typeof options.amsNetIdTarget === 'undefined') { throw new Error('amsNetIdTarget not defined!') } if (typeof options.amsNetIdSource === 'undefined') { throw new Error('amsNetIdTarget not defined!') } if (options.verbose === undefined) { options.verbose = 0 } return options } var getCommandDescription = function (commandId) { var desc = 'Unknown command' switch (commandId) { case ID_READ_DEVICE_INFO: desc = 'Read device info' break case ID_READ: desc = 'Read' break case ID_WRITE: desc = 'Write' break case ID_READ_STATE: desc = 'Read state' break case ID_WRITE_CONTROL: desc = 'Write control' break case ID_ADD_NOTIFICATION: desc = 'Add notification' break case ID_DEL_NOTIFICATION: desc = 'Delete notification' break case ID_NOTIFICATION: desc = 'Notification' break case ID_READ_WRITE: desc = 'ReadWrite' break } return desc } var getValue = function (dataName, result, offset, useLocalTimezone) { var value var timeoffset switch (dataName) { case 'BOOL': value = result.readUInt8(offset) != 0 break case 'BYTE': case 'USINT': value = result.readUInt8(offset) break case 'SINT': value = result.readInt8(offset) break case 'UINT': case 'WORD': value = result.readUInt16LE(offset) break case 'INT': value = result.readInt16LE(offset) break case 'DWORD': case 'UDINT': value = result.readUInt32LE(offset) break case 'DINT': value = result.readInt32LE(offset) break case 'REAL': value = result.readFloatLE(offset) break case 'LREAL': value = result.readDoubleLE(offset) break case 'STRING': value = result.toString('binary', offset, findStringEnd(result, offset)) break case 'TIME': case 'TIME_OF_DAY': case 'TOD': var milliseconds = result.readUInt32LE(offset) value = new Date(milliseconds) if (useLocalTimezone) { timeoffset = value.getTimezoneOffset() value = new Date(value.setMinutes(value.getMinutes() + timeoffset)) } break case 'DATE': case 'DATE_AND_TIME': case 'DT': var seconds = result.readUInt32LE(offset) value = new Date(seconds * 1000) if (useLocalTimezone) { timeoffset = value.getTimezoneOffset() value = new Date(value.setMinutes(value.getMinutes() + timeoffset)) } break } return value } var integrateResultInHandle = function (handle, result) { var offset = 0 function integrate(propname,bytelength, proppath) { var convert = {} for (var i = 0; i < bytelength.length; i++) { let prop=propname[i] getItemByteLength(bytelength[i], convert) if (convert.isStructure) { for (var idx = convert.lowIndex; idx <= convert.hiIndex; idx++){ if (convert.isAdsArray) { integrate(prop[1],bytelength[i].structure,proppath+prop[0]+"["+idx+"]"+".") } else { integrate(prop[1],bytelength[i].structure,proppath+prop[0]+".") } } } else { getItemByteLength(bytelength[i], convert) for (var idx = convert.lowIndex; idx <= convert.hiIndex; idx++){ var value = null if (result.length >= (offset+convert.length)) { if (convert.isAdsType && bytelength[i].name!='RAW') { value = getValue(bytelength[i].name, result, offset, checkUseLocalTimezone(handle,bytelength[i])) } else { value = result.slice(offset, offset + (convert.length)) } } if (convert.isAdsArray) { setObjectProperty(handle,proppath+prop+"["+idx+"]",value,true) } else { setObjectProperty(handle,proppath+prop,value,true) } offset += convert.length } } } } integrate(handle.propname,handle.bytelength,"") } function getObjectProperty(handle,propname) { var result = null //var propParts = normalisePropertyExpression(propname) normalisePropertyExpression(propname,function(err,propParts) { if (!err) { var m propParts.reduce(function(obj, key) { result = (typeof obj[key] !== "undefined" ? obj[key] : undefined) return result }, handle) } }) return result } function setObjectProperty(handle,propname,value,createMissing) { if (typeof createMissing === 'undefined') { createMissing = (typeof value !== 'undefined') } //var propParts = normalisePropertyExpression(propname) normalisePropertyExpression(propname,function(err,propParts) { if (!err) { var depth = 0 var length = propParts.length var obj = handle var key for (var i=0;i<length-1;i++) { key = propParts[i] if (typeof key === 'string' || (typeof key === 'number' && !Array.isArray(obj))) { if (obj.hasOwnProperty(key)) { obj = obj[key] } else if (createMissing) { if (typeof propParts[i+1] === 'string') { obj[key] = {} } else { obj[key] = [] } obj = obj[key] } else { return null } } else if (typeof key === 'number') { // obj is an array if (obj[key] === undefined) { if (createMissing) { if (typeof propParts[i+1] === 'string') { obj[key] = {} } else { obj[key] = [] } obj = obj[key] } else { return null } } else { obj = obj[key] } } } key = propParts[length-1] if (typeof value === "undefined") { if (typeof key === 'number' && Array.isArray(obj)) { obj.splice(key,1) } else { delete obj[key] } } else { obj[key] = value } } }) } function normalisePropertyExpression(propname,cb) { propname =String(propname) var length = propname.length if (length === 0) { cb("Invalid property expression: zero-length",null) return false } var parts = [] var start = 0 var inString = false var inBox = false var quoteChar var v for (var i=0;i<length;i++) { var c = propname[i] if (!inString) { if (c === "'" || c === '"') { if (i != start) { cb("Invalid property expression: unexpected "+c+" at position "+i,null) return false } inString = true quoteChar = c start = i+1 } else if (c === '.') { if (i===0) { cb("Invalid property expression: unexpected . at position 0",null) return false } if (start != i) { v = propname.substring(start,i) if (/^\d+$/.test(v)) { parts.push(parseInt(v)) } else { parts.push(v) } } if (i===length-1) { cb("Invalid property expression: unterminated expression",null) return false } // Next char is first char of an identifier: a-z 0-9 $ _ if (!/[a-z0-9\$\_]/i.test(propname[i+1])) { cb("Invalid property expression: unexpected "+propname[i+1]+" at position "+(i+1),null) return false } start = i+1 } else if (c === '[') { if (i === 0) { cb("Invalid property expression: unexpected "+c+" at position "+i,null) return false } if (start != i) { parts.push(propname.substring(start,i)) } if (i===length-1) { cb("Invalid property expression: unterminated expression",null) return false } // Next char is either a quote or a number if (!/["'\d]/.test(propname[i+1])) { cb("Invalid property expression: unexpected "+propname[i+1]+" at position "+(i+1),null) return false } start = i+1 inBox = true } else if (c === ']') { if (!inBox) { cb("Invalid property expression: unexpected "+c+" at position "+i,null) return false } if (start != i) { v = propname.substring(start,i) if (/^\d+$/.test(v)) { parts.push(parseInt(v)) } else { cb("Invalid property expression: unexpected array expression at position "+start,null) return false } } start = i+1 inBox = false } else if (c === ' ') { cb("Invalid property expression: unexpected ' ' at position "+i,null) return false } } else { if (c === quoteChar) { if (i-start === 0) { cb("Invalid property expression: zero-length string at position "+start,null) return false } parts.push(propname.substring(start,i)) // If inBox, next char must be a ]. Otherwise it may be [ or . if (inBox && !/\]/.test(propname[i+1])) { cb("Invalid property expression: unexpected array expression at position "+start,null) return false } else if (!inBox && i+1!==length && !/[\[\.]/.test(propname[i+1])) { cb("Invalid property expression: unexpected "+propname[i+1]+" expression at position "+(i+1),null) return false } start = i+1 inString = false } } } if (inBox || inString) { cb("Invalid property expression: unterminated expression",null) return false } if (start < length) { parts.push(propname.substring(start)) } cb(null,parts) return true } var checkUseLocalTimezone = function (handle,bl) { return (typeof bl.useLocalTimezone !== 'undefined'? bl.useLocalTimezone : (typeof handle.useLocalTimezone === 'undefined' || handle.useLocalTimezone)) } var parseHandle = function (handle) { if (typeof handle.symname === 'undefined' && (typeof handle.indexGroup === 'undefined' || typeof handle.indexOffset === 'undefined') ) { throw new Error("The handle doesn't have a symname or an indexGroup and indexOffset property!") } if (typeof handle.bytelength === 'undefined') { handle.bytelength = [exports.BOOL] } if (typeof handle.propname !== 'undefined') { if (!Array.isArray(handle.propname)) { handle.propname = [handle.propname] } } else { if (!Array.isArray(handle.bytelength) && !handle.bytelength.structure) { handle.propname = ['value'] } else { function getProp(bytelength) { let propname = [] if (!Array.isArray(bytelength)) { bytelength = [bytelength] } for (var i = 0; i < bytelength.length; i++) { if (bytelength[i].structure) { propname[i] = ['value['+i+']',getProp(bytelength[i].structure)] } else { propname[i] = 'value['+i+']' } } return propname } handle.propname = getProp(handle.bytelength) } } if (!Array.isArray(handle.bytelength)) { handle.bytelength = [handle.bytelength] } function calcBytelength (bytelength,propname) { let totalByteLength = 0 let convert = {} for (var i = 0; i < bytelength.length; i++) { let prop=propname[i] if (bytelength[i].structure) { if (!Array.isArray(prop)) { throw new Error('A byte length structure requires a array in the property array!') } if (!Array.isArray(prop[1])) { throw new Error('A byte length structure requires a array with a property array in the property array!') } normalisePropertyExpression(prop[0], function(err) { if (err) throw new Error(err) }) getItemByteLength(bytelength[i],convert) totalByteLength += calcBytelength(bytelength[i].structure,prop[1]) * convert.arrayElements } else { totalByteLength += getItemByteLength(bytelength[i],{}) normalisePropertyExpression(prop, function(err) { if (err) throw new Error(err) }) } } return totalByteLength } handle.totalByteLength = calcBytelength(handle.bytelength,handle.propname) if (typeof handle.transmissionMode === 'undefined') { handle.transmissionMode = exports.NOTIFY.ONCHANGE } if (typeof handle.maxDelay === 'undefined') { handle.maxDelay = 0 } if (typeof handle.cycleTime === 'undefined') { handle.cycleTime = 10 } return handle } var getBytesFromHandle = function (handle) { var buf = Buffer.alloc(handle.totalByteLength) var offset = 0 function fromHandle(propname,bytelength,proppath) { var convert = {} for (var i = 0; i < propname.length; i++) { let p = propname[i] getItemByteLength(bytelength[i], convert) if (convert.isStructure) { for (var idx = convert.lowIndex; idx <= convert.hiIndex; idx++){ if (convert.isAdsArray) { fromHandle(p[1],bytelength[i].structure,proppath+p[0]+"["+idx+"]"+".") } else { fromHandle(p[1],bytelength[i].structure,proppath+p[0]+".") } } } else { for (var idx = convert.lowIndex; idx <= convert.hiIndex; idx++) { var val = getObjectProperty(handle,proppath+p) if (convert.isAdsArray) { val = val[idx] } if (!convert.isAdsType) { val.copy(buf, offset,0,convert.length) }else if ((typeof val !== 'undefined') && (buf.length >= offset+convert.length)) { var datetime var timeoffset switch (bytelength[i].name) { case 'BOOL': case 'BYTE': case 'USINT': buf.writeUInt8(val, offset) break case 'SINT': buf.writeInt8(val, offset) break case 'UINT': case 'WORD': buf.writeUInt16LE(val, offset) break case 'INT': buf.writeInt16LE(val, offset) break case 'DWORD': case 'UDINT': buf.writeUInt32LE(val, offset) break case 'DINT': buf.writeInt32LE(val, offset) break case 'REAL':