node-ads-api
Version:
NodeJS Twincat ADS protocol implementation
1,686 lines (1,516 loc) • 66.8 kB
JavaScript
// 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':