@bitpoolos/edge-modbus
Version:
Modules to support reading Modbus devices.
745 lines (656 loc) • 25.5 kB
JavaScript
/**
Copyright (c) 2016,2017,2018,2019,2020,2021,2022 Klaus Landsdorf (http://node-red.plus/)
Copyright 2016 - Jason D. Harper, Argonne National Laboratory
Copyright 2015,2016 - Mika Karaila, Valmet Automation Inc.
All rights reserved.
node-red-contrib-modbus
@author <a href="mailto:klaus.landsdorf@bianco-royal.de">Klaus Landsdorf</a> (Bianco Royal)
*/
/**
* Modbus connection node.
* @module NodeRedModbusClient
*
* @param RED
*/
module.exports = function (RED) {
'use strict'
// SOURCE-MAP-REQUIRED
const mbBasics = require('./modbus-basics')
const coreModbusClient = require('./core/modbus-client-core')
const coreModbusQueue = require('./core/modbus-queue-core')
const internalDebugLog = require('debug')('contribModbus:config:client')
const _ = require('underscore')
function ModbusClientNode (config) {
RED.nodes.createNode(this, config)
// create an empty modbus client
const ModbusRTU = require('@open-p4nr/modbus-serial')
const unlimitedListeners = 0
const minCommandDelayMilliseconds = 1
const defaultUnitId = 1
const defaultTcpUnitId = 0
const serialConnectionDelayTimeMS = 500
const timeoutTimeMS = 1000
const reconnectTimeMS = 2000
const logHintText = ' Get More About It By Logging'
const serialAsciiResponseStartDelimiter = '0x3A'
this.clienttype = config.clienttype
if (config.parallelUnitIdsAllowed === undefined) {
this.bufferCommands = true
} else {
this.bufferCommands = config.bufferCommands
}
this.bpChkShowDebugWarnings = config.bpChkShowDebugWarnings
this.queueLogEnabled = config.queueLogEnabled
this.stateLogEnabled = config.stateLogEnabled
this.failureLogEnabled = config.failureLogEnabled
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.serialAsciiResponseStartDelimiter = config.serialAsciiResponseStartDelimiter || serialAsciiResponseStartDelimiter
this.unit_id = parseInt(config.unit_id)
this.commandDelay = parseInt(config.commandDelay) || minCommandDelayMilliseconds
this.clientTimeout = parseInt(config.clientTimeout) || timeoutTimeMS
this.reconnectTimeout = parseInt(config.reconnectTimeout) || reconnectTimeMS
this.reconnectOnTimeout = config.reconnectOnTimeout
if (config.parallelUnitIdsAllowed === undefined) {
this.parallelUnitIdsAllowed = true
} else {
this.parallelUnitIdsAllowed = config.parallelUnitIdsAllowed
}
this.showErrors = config.showErrors
this.showWarnings = config.showWarnings
this.showLogs = config.showLogs
const node = this
node.isFirstInitOfConnection = true
node.closingModbus = false
node.client = null
node.bufferCommandList = new Map()
node.sendingAllowed = new Map()
node.unitSendingAllowed = []
node.messageAllowedStates = coreModbusClient.messageAllowedStates
node.serverInfo = ''
node.stateMachine = null
node.stateService = null
node.stateMachine = coreModbusClient.createStateMachineService()
node.actualServiceState = node.stateMachine.initialState
node.actualServiceStateBefore = node.actualServiceState
node.stateService = coreModbusClient.startStateService(node.stateMachine)
node.reconnectTimeoutId = 0
node.serialSendingAllowed = false
node.internalDebugLog = internalDebugLog
coreModbusQueue.queueSerialLockCommand(node)
node.setDefaultUnitId = function () {
if (this.clienttype === 'tcp') {
node.unit_id = defaultTcpUnitId
} else {
node.unit_id = defaultUnitId
}
}
node.setUnitIdFromPayload = function (msg) {
const unitId = coreModbusClient.getActualUnitId(node, msg)
if (!coreModbusClient.checkUnitId(unitId, node.clienttype)) {
node.setDefaultUnitId()
}
node.client.setID(unitId)
msg.unitId = unitId
}
if (Number.isNaN(node.unit_id) || !coreModbusClient.checkUnitId(node.unit_id, node.clienttype)) {
node.setDefaultUnitId()
}
node.updateServerinfo = function () {
if (node.clienttype === 'tcp') {
node.serverInfo = ' TCP@' + node.tcpHost + ':' + node.tcpPort
} else {
node.serverInfo = ' Serial@' + node.serialPort + ':' + node.serialBaudrate + 'bit/s'
}
node.serverInfo += ' default Unit-Id: ' + node.unit_id
}
function verboseWarn (logMessage) {
if (node.bpChkShowDebugWarnings) {
node.updateServerinfo()
node.warn('Client -> ' + logMessage + ' ' + node.serverInfo)
}
}
node.errorProtocolMsg = function (err, msg) {
if (node.showErrors) {
mbBasics.logMsgError(node, err, msg)
}
}
function verboseLog (logMessage) {
if (node.bpChkShowDebugWarnings) {
coreModbusClient.internalDebug('Client -> ' + logMessage + ' ' + node.serverInfo)
}
}
function stateLog (logMessage) {
// if (node.stateLogEnabled) {
// verboseLog(logMessage)
// }
}
node.queueLog = function (logMessage) {
if (node.bufferCommands && node.queueLogEnabled) {
verboseLog(logMessage)
}
}
node.stateService.subscribe(state => {
node.actualServiceStateBefore = node.actualServiceState
node.actualServiceState = state
stateLog(state.value)
if (!state.value || node.actualServiceState.value === undefined) {
// verboseWarn('fsm ignore invalid state')
return
}
if (node.actualServiceStateBefore.value === node.actualServiceState.value) {
// verboseWarn('fsm ignore equal state ' + node.actualServiceState.value + ' after ' + node.actualServiceStateBefore.value)
return
}
if (state.matches('init')) {
verboseWarn('fsm init state after ' + node.actualServiceStateBefore.value)
node.updateServerinfo()
coreModbusQueue.initQueue(node)
node.reconnectTimeoutId = 0
try {
if (node.isFirstInitOfConnection) {
node.isFirstInitOfConnection = false
verboseWarn('first fsm init in ' + serialConnectionDelayTimeMS + ' ms')
setTimeout(node.connectClient, serialConnectionDelayTimeMS)
} else {
verboseWarn('fsm init in ' + node.reconnectTimeout + ' ms')
setTimeout(node.connectClient, node.reconnectTimeout)
}
} catch (err) {
node.error(err, { payload: 'client connection error ' + logHintText })
}
node.emit('mbinit')
}
if (state.matches('connected')) {
verboseWarn('fsm connected after state ' + node.actualServiceStateBefore.value + logHintText)
coreModbusQueue.queueSerialUnlockCommand(node)
node.emit('mbconnected')
}
if (state.matches('activated')) {
node.emit('mbactive')
if (node.bufferCommands && !coreModbusQueue.checkQueuesAreEmpty(node)) {
node.stateService.send('QUEUE')
}
}
if (state.matches('queueing')) {
if (node.clienttype === 'tcp') {
node.stateService.send('SEND')
} else {
if (node.serialSendingAllowed) {
coreModbusQueue.queueSerialLockCommand(node)
node.stateService.send('SEND')
}
}
}
if (state.matches('sending')) {
setTimeout(() => {
coreModbusQueue.dequeueCommand(node)
}, node.commandDelay)
node.emit('mbqueue')
}
if (state.matches('opened')) {
coreModbusQueue.queueSerialUnlockCommand(node)
node.emit('mbopen')
}
if (state.matches('switch')) {
node.emit('mbswitch')
node.stateService.send('CLOSE')
}
if (state.matches('closed')) {
node.emit('mbclosed')
node.stateService.send('RECONNECT')
}
if (state.matches('stopped')) {
verboseWarn('stopped state without reconnecting')
node.emit('mbclosed')
}
if (state.matches('failed')) {
verboseWarn('fsm failed state after ' + node.actualServiceStateBefore.value + logHintText)
node.emit('mberror', 'Modbus Failure On State ' + node.actualServiceStateBefore.value + logHintText)
node.stateService.send('BREAK')
}
if (state.matches('broken')) {
verboseWarn('fsm broken state after ' + node.actualServiceStateBefore.value + logHintText)
node.emit('mbbroken', 'Modbus Broken On State ' + node.actualServiceStateBefore.value + logHintText)
if (node.reconnectOnTimeout) {
node.stateService.send('RECONNECT')
} else {
node.stateService.send('ACTIVATE')
}
}
if (state.matches('reconnecting')) {
verboseWarn('fsm reconnect state after ' + node.actualServiceStateBefore.value + logHintText)
coreModbusQueue.queueSerialLockCommand(node)
node.emit('mbreconnecting')
if (node.reconnectTimeout <= 0) {
node.reconnectTimeout = reconnectTimeMS
}
setTimeout(() => {
node.reconnectTimeoutId = 0
node.stateService.send('INIT')
}, node.reconnectTimeout)
}
})
node.connectClient = function () {
try {
if (node.client) {
try {
node.client.close(function () {
verboseLog('connection closed')
})
verboseLog('connection close sent')
} catch (err) {
verboseLog(err.message)
}
}
node.client = null
node.client = new ModbusRTU()
node.client.on('error', (err) => {
node.modbusErrorHandling(err)
mbBasics.setNodeStatusTo('error', node)
})
if (!node.clientTimeout) {
node.clientTimeout = timeoutTimeMS
}
if (!node.reconnectTimeout) {
node.reconnectTimeout = reconnectTimeMS
}
if (node.clienttype === 'tcp') {
if (!coreModbusClient.checkUnitId(node.unit_id, node.clienttype)) {
node.error(new Error('wrong unit-id (0..255)'), { payload: node.unit_id })
node.stateService.send('FAILURE')
return false
}
try {
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((err) => {
node.modbusTcpErrorHandling(err)
return false
})
break
case 'TELNET':
verboseLog('Telnet port')
node.client.connectTelnet(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.catch((err) => {
node.modbusTcpErrorHandling(err)
return false
})
break
case 'TCP-RTU-BUFFERED':
verboseLog('TCP RTU buffered port')
node.client.connectTcpRTUBuffered(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.catch((err) => {
node.modbusTcpErrorHandling(err)
return false
})
break
default:
verboseLog('TCP port')
node.client.connectTCP(node.tcpHost, {
port: node.tcpPort,
autoOpen: true
}).then(node.setTCPConnectionOptions)
.catch((err) => {
node.modbusTcpErrorHandling(err)
return false
})
}
} catch (e) {
node.modbusTcpErrorHandling(e)
return false
}
} else {
if (!coreModbusClient.checkUnitId(node.unit_id, node.clienttype)) {
node.error(new Error('wrong unit-id serial (0..247)'), { payload: node.unit_id })
node.stateService.send('FAILURE')
return false
}
if (!node.serialConnectionDelay) {
node.serialConnectionDelay = serialConnectionDelayTimeMS
}
if (!node.serialPort) {
node.error(new Error('wrong serial port'), { payload: node.serialPort })
node.stateService.send('FAILURE')
return false
}
const serialPortOptions = {
baudRate: parseInt(node.serialBaudrate),
dataBits: parseInt(node.serialDatabits),
stopBits: parseInt(node.serialStopbits),
parity: node.serialParity,
autoOpen: false
}
try {
switch (node.serialType) {
case 'ASCII':
verboseLog('ASCII port serial')
// Make sure is parsed when string, otherwise just assign.
if (node.serialAsciiResponseStartDelimiter && typeof node.serialAsciiResponseStartDelimiter === 'string') {
serialPortOptions.startOfSlaveFrameChar = parseInt(node.serialAsciiResponseStartDelimiter, 16)
} else {
serialPortOptions.startOfSlaveFrameChar = node.serialAsciiResponseStartDelimiter
}
verboseLog('Using response delimiter: 0x' + serialPortOptions.startOfSlaveFrameChar.toString(16))
node.client.connectAsciiSerial(node.serialPort, serialPortOptions).then(node.setSerialConnectionOptions)
.catch((err) => {
node.modbusSerialErrorHandling(err)
return false
})
break
case 'RTU':
verboseLog('RTU port serial')
node.client.connectRTU(node.serialPort, serialPortOptions).then(node.setSerialConnectionOptions)
.catch((err) => {
node.modbusSerialErrorHandling(err)
return false
})
break
default:
verboseLog('RTU buffered port serial')
node.client.connectRTUBuffered(node.serialPort, serialPortOptions).then(node.setSerialConnectionOptions)
.catch((err) => {
node.modbusSerialErrorHandling(err)
return false
})
break
}
} catch (e) {
node.modbusSerialErrorHandling(e)
return false
}
}
} catch (err) {
node.modbusErrorHandling(err)
return false
}
return true
}
node.setTCPConnectionOptions = function () {
node.client.setID(node.unit_id)
node.client.setTimeout(node.clientTimeout)
node.stateService.send('CONNECT')
}
node.setTCPConnected = function () {
coreModbusClient.modbusSerialDebug('modbus tcp connected on ' + node.tcpHost)
}
node.setSerialConnectionOptions = function () {
node.stateService.send('OPENSERIAL')
setTimeout(node.openSerialClient, parseInt(node.serialConnectionDelay))
}
node.modbusErrorHandling = function (err) {
coreModbusQueue.queueSerialUnlockCommand(node)
if (err.message) {
coreModbusClient.modbusSerialDebug('modbusErrorHandling:' + err.message)
} else {
coreModbusClient.modbusSerialDebug('modbusErrorHandling:' + JSON.stringify(err))
}
if (err.errno && coreModbusClient.networkErrors.includes(err.errno)) {
node.stateService.send('FAILURE')
}
}
node.modbusTcpErrorHandling = function (err) {
coreModbusQueue.queueSerialUnlockCommand(node)
if (node.showErrors) {
node.error(err)
}
if (node.failureLogEnabled) {
if (err.message) {
coreModbusClient.modbusSerialDebug('modbusTcpErrorHandling:' + err.message)
} else {
coreModbusClient.modbusSerialDebug('modbusTcpErrorHandling:' + JSON.stringify(err))
}
}
if ((err.errno && coreModbusClient.networkErrors.includes(err.errno)) ||
(err.code && coreModbusClient.networkErrors.includes(err.code))) {
node.stateService.send('BREAK')
}
}
node.modbusSerialErrorHandling = function (err) {
coreModbusQueue.queueSerialUnlockCommand(node)
if (node.showErrors) {
node.error(err)
}
if (node.failureLogEnabled) {
if (err.message) {
coreModbusClient.modbusSerialDebug('modbusSerialErrorHandling:' + err.message)
} else {
coreModbusClient.modbusSerialDebug('modbusSerialErrorHandling:' + JSON.stringify(err))
}
}
node.stateService.send('BREAK')
}
node.openSerialClient = function () {
// some delay for windows
if (node.actualServiceState.value === 'opened') {
verboseLog('time to open Unit ' + node.unit_id)
coreModbusClient.modbusSerialDebug('modbus connection opened')
node.client.setID(node.unit_id)
node.client.setTimeout(parseInt(node.clientTimeout))
node.client._port.on('close', node.onModbusClose)
node.stateService.send('CONNECT')
} else {
verboseLog('wrong state on connect serial ' + node.actualServiceState.value)
coreModbusClient.modbusSerialDebug('modbus connection not opened state is %s', node.actualServiceState.value)
node.stateService.send('BREAK')
}
}
node.onModbusClose = function () {
coreModbusQueue.queueSerialUnlockCommand(node)
verboseWarn('Modbus closed port')
coreModbusClient.modbusSerialDebug('modbus closed port')
node.stateService.send('CLOSE')
}
node.on('customModbusMessage', function (msg, cb, cberr) {
// const state = node.actualServiceState
coreModbusClient.customModbusMessage(node, msg, cb, cberr)
})
node.on('readModbus', function (msg, cb, cberr) {
const state = node.actualServiceState
if (node.isInactive()) {
cberr(new Error('Client Not Ready To Read At State ' + state.value), msg)
} else {
if (node.bufferCommands) {
coreModbusQueue.pushToQueueByUnitId(node, coreModbusClient.readModbus, msg, cb, cberr).then(function () {
node.queueLog(JSON.stringify({
info: 'queued read msg',
message: msg.payload,
state: state.value,
queueLength: node.bufferCommandList.get(msg.queueUnitId).length
}))
}).catch(function (err) {
cberr(err, msg)
}).finally(function () {
node.stateService.send('QUEUE')
})
} else {
coreModbusClient.readModbus(node, msg, cb, cberr)
}
}
})
node.on('writeModbus', function (msg, cb, cberr) {
const state = node.actualServiceState
if (node.isInactive()) {
cberr(new Error('Client Not Ready To Write At State ' + state.value), msg)
} else {
if (node.bufferCommands) {
coreModbusQueue.pushToQueueByUnitId(node, coreModbusClient.writeModbus, msg, cb, cberr).then(function () {
node.queueLog(JSON.stringify({
info: 'queued write msg',
message: msg.payload,
state: state.value,
queueLength: node.bufferCommandList.get(msg.queueUnitId).length
}))
}).catch(function (err) {
cberr(err, msg)
}).finally(function () {
node.stateService.send('QUEUE')
})
} else {
coreModbusClient.writeModbus(node, msg, cb, cberr)
}
}
})
node.activateSending = function (msg) {
node.sendingAllowed.set(msg.queueUnitId, true)
coreModbusQueue.queueSerialUnlockCommand(node)
return new Promise(
function (resolve, reject) {
try {
if (node.bufferCommands) {
node.queueLog(JSON.stringify({
info: 'queue response activate sending',
queueLength: node.bufferCommandList.length,
sendingAllowed: node.sendingAllowed.get(msg.queueUnitId),
serialSendingAllowed: node.serialSendingAllowed,
queueUnitId: msg.queueUnitId
}))
if (coreModbusQueue.checkQueuesAreEmpty(node)) {
node.stateService.send('EMPTY')
}
}
resolve()
} catch (err) {
reject(err)
}
})
}
verboseLog('initialized')
node.setMaxListeners(unlimitedListeners)
node.on('reconnect', function () {
node.stateService.send('CLOSE')
})
node.on('dynamicReconnect', function (msg, cb, cberr) {
if (mbBasics.invalidPayloadIn(msg)) {
throw new Error('Message Or Payload Not Valid')
}
coreModbusClient.internalDebug('Dynamic Reconnect Parameters ' + JSON.stringify(msg.payload))
if (coreModbusClient.setNewNodeSettings(node, msg)) {
cb(msg)
} else {
cberr(new Error('Message Or Payload Not Valid'), msg)
}
coreModbusClient.internalDebug('Dynamic Reconnect Starts on actual state ' + node.actualServiceState.value)
node.stateService.send('SWITCH')
})
node.on('close', function (done) {
const nodeIdentifierName = node.name || node.id
node.closingModbus = true
verboseLog('stop fsm on close ' + nodeIdentifierName)
node.stateService.send('STOP')
verboseLog('close node ' + nodeIdentifierName)
node.internalDebugLog('close node ' + nodeIdentifierName)
node.removeAllListeners()
if (node.client) {
if (node.client.isOpen) {
node.client.close(function (err) {
if (err) {
verboseLog('Connection closed with error ' + nodeIdentifierName)
} else {
verboseLog('Connection closed well ' + nodeIdentifierName)
}
done()
})
} else {
verboseLog('connection was closed ' + nodeIdentifierName)
done()
}
} else {
verboseLog('Connection closed simple ' + nodeIdentifierName)
done()
}
})
// handle using as config node
node.registeredNodeList = {}
node.registerForModbus = function (clientUserNodeId) {
node.registeredNodeList[clientUserNodeId] = clientUserNodeId
if (Object.keys(node.registeredNodeList).length === 1) {
node.closingModbus = false
node.stateService.send('NEW')
node.stateService.send('INIT')
}
node.emit('mbregister', clientUserNodeId)
}
node.setStoppedState = function (clientUserNodeId, done) {
node.stateService.send('STOP')
node.emit('mbderegister', clientUserNodeId)
done()
}
node.closeConnectionWithoutRegisteredNodes = function (clientUserNodeId, done) {
if (Object.keys(node.registeredNodeList).length === 0) {
node.closingModbus = true
if (node.client && node.actualServiceState.value !== 'stopped') {
if (node.client.isOpen) {
node.client.close(function () {
node.setStoppedState(clientUserNodeId, done)
})
} else {
node.setStoppedState(clientUserNodeId, done)
}
} else {
node.setStoppedState(clientUserNodeId, done)
}
} else {
node.setStoppedState(clientUserNodeId, done)
}
}
node.deregisterForModbus = function (clientUserNodeId, done) {
try {
delete node.registeredNodeList[clientUserNodeId]
if (node.closingModbus) {
done()
node.emit('mbderegister', clientUserNodeId)
} else {
node.closeConnectionWithoutRegisteredNodes(clientUserNodeId, done)
}
} catch (err) {
verboseWarn(err.message + ' on de-register node ' + clientUserNodeId)
node.error(err)
done()
}
}
node.isInactive = function () {
return _.isUndefined(node.actualServiceState) || node.messageAllowedStates.indexOf(node.actualServiceState.value) === -1
}
node.isActive = function () {
return !node.isInactive()
}
node.isReadyToSend = function (node) {
if (node.actualServiceState.matches('queueing')) {
return true
}
verboseWarn('Client not ready to send')
return false
}
}
RED.nodes.registerType('bp-modbus-client', ModbusClientNode)
RED.httpAdmin.get('/modbus/serial/ports', RED.auth.needsPermission('serial.read'), function (req, res) {
const SerialPort = require('serialport')
SerialPort.SerialPort.list().then(ports => {
res.json(ports)
}).catch(err => {
res.json([err.message])
coreModbusClient.internalDebug(err.message)
})
})
}