prisme-flow
Version:
prisme platform flow engine
705 lines (620 loc) • 22.9 kB
JavaScript
/**
* Created by prisme.io on 09/06/2017.
*/
/**
* Modbus connection node.
* @module NodeRedModbusClient
*
* @param RED
*/
module.exports = function (RED) {
'use strict'
let coreModbusClient = require('./core/modbus-client-core')
function ModbusClientNode (config) {
RED.nodes.createNode(this, config)
// create an empty modbus client
let ModbusRTU = require('modbus-serial')
const unlimitedListeners = 0
const minCommandDelayMilliseconds = 1
const defaultUnitId = 1
const serialConnectionDelayTimeMS = 500
const timeoutTimeMS = 1000
const reconnectTimeMS = 2000
this.clienttype = config.clienttype
this.bufferCommands = config.bufferCommands
this.stateLogEnabled = config.stateLogEnabled
this.tcpHost = config.tcpHost
this.tcpPort = parseInt(config.tcpPort) || 502
this.tcpType = config.tcpType
this.serialPort = config.serialPort
this.serialBaudrate = config.serialBaudrate
this.serialDatabits = config.serialDatabits
this.serialStopbits = config.serialStopbits
this.serialParity = config.serialParity
this.serialType = config.serialType
this.serialConnectionDelay = parseInt(config.serialConnectionDelay) || serialConnectionDelayTimeMS
this.unit_id = parseInt(config.unit_id) || defaultUnitId
this.commandDelay = parseInt(config.commandDelay) || minCommandDelayMilliseconds
this.clientTimeout = parseInt(config.clientTimeout) || timeoutTimeMS
this.reconnectTimeout = parseInt(config.reconnectTimeout) || reconnectTimeMS
let node = this
node.client = null
node.bufferCommandList = new Map()
node.sendAllowed = new Map()
node.unitSendingAllowed = []
node.messageAllowedStates = coreModbusClient.messagesAllowedStates
node.statlyMachine = null
node.statlyMachine = coreModbusClient.createStatelyMachine()
node.initQueue = function () {
node.bufferCommandList.clear()
node.sendAllowed.clear()
node.unitSendingAllowed = []
for (let step = 0; step <= 255; step++) {
node.bufferCommandList.set(step, [])
node.sendAllowed.set(step, true)
}
}
node.checkUnitId = function (unitid) {
if (node.clienttype === 'tcp') {
return unitid >= 0 && unitid <= 255
} else {
return unitid >= 1 && unitid <= 247
}
}
node.setUnitIdFromPayload = function (msg) {
let unit = parseInt(msg.payload.unitid)
if (Number.isInteger(unit)) {
node.client.setID(unit)
msg.queueUnitId = unit
} else {
if (!node.checkUnitId(node.unit_id)) {
node.unit_id = defaultUnitId
}
node.client.setID(node.unit_id)
msg.queueUnitId = node.unit_id
}
}
if (Number.isNaN(node.unit_id) || !node.checkUnitId(node.unit_id)) {
node.unit_id = defaultUnitId
}
node.sequentialDequeueCommand = function () {
let command = null
let noneCommandSent = true
let serialUnit = parseInt(node.unitSendingAllowed.shift())
if (Number.isInteger(serialUnit) &&
node.bufferCommandList.get(serialUnit).length > 0) {
queueLog(JSON.stringify({
type: 'queue check',
unitid: serialUnit,
sendAllowed: node.sendAllowed.get(serialUnit),
queueLength: node.bufferCommandList.get(serialUnit).length
}))
if (node.sendAllowed.get(serialUnit)) {
command = node.bufferCommandList.get(serialUnit).shift()
if (command) {
node.sendAllowed.set(serialUnit, false)
queueLog(JSON.stringify({
type: 'serial sending and wait',
unitid: serialUnit,
commandData: command,
queueLength: node.bufferCommandList.get(serialUnit).length,
sendAllowedForNext: node.sendAllowed.get(serialUnit),
delay: node.commandDelay
}))
if (node.bufferCommandList.get(serialUnit).length > 0) {
node.unitSendingAllowed.push(serialUnit)
}
noneCommandSent = false
command.callModbus(command.msg, command.cb, command.cberr)
}
}
} else {
queueLog(JSON.stringify({
type: 'queue check is not a unit',
unitid: serialUnit
}))
}
if (noneCommandSent) {
node.statlyMachine.empty()
}
}
node.dequeueCommand = function () {
let state = node.statlyMachine.getMachineState()
if (node.messageAllowedStates.indexOf(state) === -1) {
queueLog(JSON.stringify({
state: state,
message: 'dequeue command disallowed state',
delay: node.commandDelay
}))
} else {
queueLog(JSON.stringify({
state: state,
message: 'dequeue command ' + node.clienttype,
delay: node.commandDelay
}))
node.sequentialDequeueCommand()
}
if (node.checkQueuesAreEmpty()) {
node.statlyMachine.empty()
}
}
node.checkQueuesAreEmpty = function () {
let queueIsEmpty = true
for (let step = 0; step <= 255; step++) {
queueIsEmpty &= (node.bufferCommandList.get(step).length > 0)
}
return queueIsEmpty
}
let serverInfo = ''
if (node.clienttype === 'tcp') {
serverInfo = ' TCP@' + node.tcpHost + ':' + node.tcpPort
} else {
serverInfo = ' Serial@' + node.serialPort + ':' + node.serialBaudrate + 'bit/s'
}
serverInfo += ' default Unit-Id: ' + node.unit_id
function verboseWarn (logMessage) {
if (RED.settings.verbose) {
node.warn('Client -> ' + logMessage + serverInfo)
}
}
function verboseLog (logMessage) {
if (RED.settings.verbose) {
coreModbusClient.debugLog('Client -> ' + logMessage + serverInfo)
}
}
function stateLog (logMessage) {
if (node.stateLogEnabled) {
verboseLog(logMessage)
}
}
function queueLog (logMessage) {
if (node.bufferCommands) {
verboseLog(logMessage)
}
}
node.statlyMachine.onINIT = function (event, oldState, newState) {
node.initQueue()
setTimeout(node.connectClient, node.reconnectTimeout)
verboseWarn('reconnect in ' + node.reconnectTimeout + ' ms')
verboseLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mbinit')
}
node.statlyMachine.onCONNECTED = function (event, oldState, newState) {
stateLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mbconnected')
node.statlyMachine.activate()
}
node.statlyMachine.onACTIVATED = function (event, oldState, newState) {
stateLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mbactive')
if (node.bufferCommands) {
node.statlyMachine.queue()
}
}
node.statlyMachine.onQUEUEING = function (event, oldState, newState) {
stateLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
setTimeout(node.dequeueCommand, node.commandDelay)
node.emit('mbqueue')
}
node.statlyMachine.onOPENED = function (event, oldState, newState) {
verboseLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mbopen')
}
node.statlyMachine.onCLOSED = function (event, oldState, newState) {
stateLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mbclosed')
node.statlyMachine.break()
}
node.statlyMachine.onFAILED = function (event, oldState, newState) {
stateLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mberror', 'FSM Reset On State ' + oldState)
node.statlyMachine.break()
}
node.statlyMachine.onBROKEN = function (event, oldState, newState) {
stateLog('event: ' + event + ' old: ' + oldState + ' new: ' + newState)
node.emit('mbbroken')
if (node.reconnectTimeout <= 0) {
node.reconnectTimeout = reconnectTimeMS
}
verboseWarn('try to reconnect by init in ' + node.reconnectTimeout + ' ms')
setTimeout(node.statlyMachine.init, node.reconnectTimeout)
}
node.connectClient = function () {
if (node.client) {
try {
node.client.close(function () {
verboseLog('connection closed')
})
} catch (err) {
verboseLog(err)
}
}
node.client = null
node.client = new ModbusRTU()
if (!node.clientTimeout) {
node.clientTimeout = timeoutTimeMS
}
if (!node.reconnectTimeout) {
node.reconnectTimeout = reconnectTimeMS
}
if (node.clienttype === 'tcp') {
if (!node.checkUnitId(node.unit_id)) {
node.error('wrong unit-id (0..255)', {payload: node.unit_id})
node.statlyMachine.failure()
return
}
switch (node.tcpType) {
case 'C701':
verboseLog('C701 port UDP bridge')
node.client.connectC701(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.then(node.setTCPConnected)
.catch(node.modbusErrorHandling)
break
case 'TELNET':
verboseLog('Telnet port')
node.client.connectTelnet(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.catch(node.modbusErrorHandling)
break
case 'TPC-RTU-BUFFERED':
verboseLog('TCP RTU buffered port')
node.client.connectTcpRTUBuffered(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.catch(node.modbusErrorHandling)
break
default:
verboseLog('TCP port')
node.client.connectTCP(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.catch(node.modbusErrorHandling)
}
} else {
if (!node.checkUnitId(node.unit_id)) {
node.error('wrong unit-id serial (1..247)', {payload: node.unit_id})
node.statlyMachine.failure()
return
}
if (!node.serialConnectionDelay) {
node.serialConnectionDelay = serialConnectionDelayTimeMS
}
switch (node.serialType) {
case 'ASCII':
verboseLog('ASCII port serial')
node.client.connectAsciiSerial(node.serialPort, {
baudRate: parseInt(node.serialBaudrate),
dataBits: parseInt(node.serialDatabits),
stopBits: parseInt(node.serialStopbits),
parity: node.serialParity,
autoOpen: false
}).then(node.setSerialConnectionOptions)
.catch(node.modbusErrorHandling)
break
case 'RTU':
verboseLog('RTU port serial')
node.client.connectRTU(node.serialPort, {
baudRate: parseInt(node.serialBaudrate),
dataBits: parseInt(node.serialDatabits),
stopBits: parseInt(node.serialStopbits),
parity: node.serialParity,
autoOpen: false
}).then(node.setSerialConnectionOptions)
.catch(node.modbusErrorHandling)
break
default:
verboseLog('RTU buffered port serial')
node.client.connectRTUBuffered(node.serialPort, {
baudRate: parseInt(node.serialBaudrate),
dataBits: parseInt(node.serialDatabits),
stopBits: parseInt(node.serialStopbits),
parity: node.serialParity,
autoOpen: false
}).then(node.setSerialConnectionOptions)
.catch(node.modbusErrorHandling)
break
}
}
}
node.setTCPConnectionOptions = function () {
node.client.setID(node.unit_id)
node.client.setTimeout(node.clientTimeout)
node.statlyMachine.connect()
}
node.setTCPConnected = function () {
coreModbusClient.modbusDebugLog('modbus tcp connected on ' + node.tcpHost)
}
node.setSerialConnectionOptions = function () {
node.statlyMachine.openserial()
setTimeout(node.openSerialClient, parseInt(node.serialConnectionDelay))
}
node.modbusErrorHandling = function (err) {
coreModbusClient.modbusDebugLog(JSON.stringify(err))
coreModbusClient.modbusDebugLog(err.message)
if (coreModbusClient.networkErrors.includes(err.errno)) {
node.statlyMachine.failure()
}
}
node.openSerialClient = function () {
// some delay for windows
if (node.statlyMachine.getMachineState() === 'OPENED') {
verboseLog('time to open Unit ' + node.unit_id)
coreModbusClient.modbusDebugLog('modbus connection opened')
node.client.setID(node.unit_id)
node.client.setTimeout(parseInt(node.clientTimeout))
node.client._port.on('close', node.onModbusClose)
node.statlyMachine.connect()
} else {
verboseLog('wrong state on connect serial ' + node.statlyMachine.getMachineState())
coreModbusClient.modbusDebugLog('modbus connection not opened state is %s', node.statlyMachine.getMachineState())
node.statlyMachine.failure()
}
}
node.onModbusClose = function () {
verboseWarn('modbus closed port')
coreModbusClient.modbusDebugLog('modbus closed port')
node.statlyMachine.close()
}
node.getQueueNumber = function (msg) {
let unit = parseInt(msg.payload.unitid)
if (Number.isInteger(unit)) {
return node.bufferCommandList.get(unit).length
} else {
return node.bufferCommandList.get(node.unit_id).length
}
}
node.pushToQueueByUnitId = function (callModbus, msg, cb, cberr) {
let unit = parseInt(msg.payload.unitid)
if (Number.isInteger(unit)) {
msg.queueUnit = unit
queueLog(JSON.stringify({
info: 'push to Queue by Unit-Id',
msg: msg,
unit: unit
}))
if (node.unitSendingAllowed.indexOf(unit) === -1) {
node.unitSendingAllowed.push(unit)
}
node.bufferCommandList.get(unit).push({callModbus: callModbus, msg: msg, cb: cb, cberr: cberr})
} else {
msg.queueUnit = node.unit_id
queueLog(JSON.stringify({
info: 'push to Queue by default Unit-Id',
msg: msg,
unit: node.unit_id
}))
if (node.unitSendingAllowed.indexOf(node.unit_id) === -1) {
node.unitSendingAllowed.push(node.unit_id)
}
node.bufferCommandList.get(node.unit_id).push({callModbus: callModbus, msg: msg, cb: cb, cberr: cberr})
}
}
node.on('readModbus', function (msg, cb, cberr) {
let state = node.statlyMachine.getMachineState()
if (node.messageAllowedStates.indexOf(state) === -1) {
cberr('FSM Not Ready To Read', msg)
return
}
if (node.bufferCommands) {
msg.queueNumber = node.getQueueNumber(msg)
node.pushToQueueByUnitId(node.readModbus, msg, cb, cberr)
node.statlyMachine.queue()
queueLog(JSON.stringify({
info: 'queue read msg',
msg: msg,
state: state,
queueLength: node.bufferCommandList.get(msg.queueUnit).length
}))
} else {
node.readModbus(msg, cb, cberr)
}
})
node.readModbus = function (msg, cb, cberr) {
if (!node.client) {
return
}
if (!node.bufferCommands) {
node.statlyMachine.read()
}
node.setUnitIdFromPayload(msg)
node.client.setTimeout(node.clientTimeout)
queueLog(JSON.stringify({
info: 'read msg',
msg: msg,
unitid: msg.queueUnitId,
timeout: node.client.getTimeout(),
state: node.statlyMachine.getMachineState()
}))
switch (parseInt(msg.payload.fc)) {
case 1: // FC: 1
node.client.readCoils(parseInt(msg.payload.address), parseInt(msg.payload.quantity)).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
break
case 2: // FC: 2
node.client.readDiscreteInputs(parseInt(msg.payload.address), parseInt(msg.payload.quantity)).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
break
case 3: // FC: 3
node.client.readHoldingRegisters(parseInt(msg.payload.address), parseInt(msg.payload.quantity)).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
break
case 4: // FC: 4
node.client.readInputRegisters(parseInt(msg.payload.address), parseInt(msg.payload.quantity)).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
break
default:
node.activateSending(msg)
cberr('Function Code Unknown', msg)
coreModbusClient.modbusDebugLog('Function Code Unknown %s', JSON.stringify(msg))
break
}
}
node.on('writeModbus', function (msg, cb, cberr) {
let state = node.statlyMachine.getMachineState()
if (node.messageAllowedStates.indexOf(state) === -1) {
cberr('FSM Not Ready To Write', msg)
return
}
if (node.bufferCommands) {
msg.queueNumber = node.getQueueNumber(msg)
node.pushToQueueByUnitId(node.writeModbus, msg, cb, cberr)
node.statlyMachine.queue()
queueLog(JSON.stringify({
info: 'queue write msg',
msg: msg,
state: state,
queueLength: node.bufferCommandList.get(msg.queueUnit).length
}))
} else {
node.writeModbus(msg, cb, cberr)
}
})
node.writeModbus = function (msg, cb, cberr) {
if (!node.client) {
return
}
if (!node.bufferCommands) {
node.statlyMachine.write()
}
node.setUnitIdFromPayload(msg)
node.client.setTimeout(node.clientTimeout)
queueLog(JSON.stringify({
info: 'write msg',
msg: msg,
unitid: msg.queueUnitId,
timeout: node.client.getTimeout(),
state: node.statlyMachine.getMachineState()
}))
switch (parseInt(msg.payload.fc)) {
case 15: // FC: 15
if (parseInt(msg.payload.value.length) !== parseInt(msg.payload.quantity)) {
node.activateSending(msg)
cberr('Quantity should be less or equal to coil payload array length: ' +
msg.payload.value.length + ' Addr: ' + msg.payload.address + ' Q: ' + msg.payload.quantity, msg)
} else {
node.client.writeCoils(parseInt(msg.payload.address), msg.payload.value).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
}
break
case 5: // FC: 5
node.client.writeCoil(parseInt(msg.payload.address), (msg.payload.value === true)).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
break
case 16: // FC: 16
if (parseInt(msg.payload.value.length) !== parseInt(msg.payload.quantity)) {
node.activateSending(msg)
cberr('Quantity should be less or equal to register payload array length: ' +
msg.payload.value.length + ' Addr: ' + msg.payload.address + ' Q: ' + msg.payload.quantity, msg)
} else {
node.client.writeRegisters(parseInt(msg.payload.address), msg.payload.value).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
}
break
case 6: // FC: 6
node.client.writeRegister(parseInt(msg.payload.address), parseInt(msg.payload.value)).then(function (resp) {
node.activateSending(msg)
cb(resp, msg)
}).catch(function (err) {
node.activateSending(msg)
cberr(err, msg)
node.modbusErrorHandling(err)
})
break
default:
node.activateSending(msg)
cberr('Function Code Unknown', msg)
coreModbusClient.modbusDebugLog('Function Code Unknown %s', JSON.stringify(msg))
break
}
}
node.activateSending = function (msg) {
if (node.bufferCommands) {
node.sendAllowed.set(msg.queueUnit, true)
queueLog(JSON.stringify({
info: 'queue response activate sending',
msg: msg,
queueLength: node.bufferCommandList.length
}))
}
node.statlyMachine.activate()
}
verboseLog('initialized')
node.setMaxListeners(unlimitedListeners)
node.statlyMachine.init()
node.on('reconnect', function () {
node.statlyMachine.failure().close()
})
node.on('close', function (done) {
node.statlyMachine.failure().stop()
verboseLog('close node')
if (node.client) {
node.client.close(function () {
verboseLog('connection closed')
done()
}).catch(function (err) {
verboseLog(err)
done()
})
} else {
done()
}
})
}
RED.nodes.registerType('modbus-client', ModbusClientNode)
RED.httpAdmin.get('/serialports', RED.auth.needsPermission('serial.read'), function (req, res) {
let SerialPort = require('serialport')
SerialPort.list(function (err, ports) {
if (err) console.log(err)
res.json(ports)
})
})
}