modbus-stack
Version:
A `StreamStack` implementation of the MODBUS protocol.
278 lines (258 loc) • 9.58 kB
JavaScript
var StreamStack = require('stream-stack').StreamStack;
var Put = require('put');
var BufferList = require('bufferlist').BufferList;
var Binary = require('bufferlist/binary').Binary;
var inherits = require('util').inherits;
var client = require('./client');
var server = require('./server');
var slice = Array.prototype.slice;
// The byte length of the "MODBUS Application Protocol" header.
const MBAP_LENGTH = 7;
// The byte length of the "MODBUS Function Code".
const FUNCTION_CODE_LENGTH = 1;
// An exception response from a MODBUS slave (server) will have
// the high-bit (0x80) set on it's function code.
const EXCEPTION_BIT = 1 << 7;
// If it's an exception response, then the next byte will be one
// these exception codes, indicating the reason for the failure.
exports.EXCEPTION_CODES = {
1 : 'Illegal Function',
2 : 'Illegal Data Address',
3 : 'Illegal Data Value',
4 : 'Slave Device Failure',
5 : 'Acknowledge',
6 : 'Slave Device Busy',
8 : 'Memory Parity Error',
10: 'Gateway Path Unavailable',
11: 'Gateway Target Path Failed to Respond'
};
// Each of the function codes has a potentially different body payload
// and potentially different parameters to send. Each function code needs
// a 'request' and 'response' parser in the "client.js" and "server.js" files.
exports.FUNCTION_CODES = {
READ_COILS: 1,
READ_DISCRETE_INPUTS: 2,
READ_HOLDING_REGISTERS: 3,
READ_INPUT_REGISTERS: 4,
WRITE_SINGLE_COIL: 5,
WRITE_SINGLE_REGISTER: 6,
READ_EXCEPTION_STATUS: 7, // Serial Line only
DIAGNOSTICS: 8, // Serial Line only
PROGRAM_484: 9,
POLL_484: 10,
GET_COMM_EVENT_COUNTER: 11,// Serial Line only
GET_COMM_EVENT_LOG: 12,// Serial Line only
PROGRAM_CONTROLLER: 13,
POLL_CONTROLLER: 14,
WRITE_MULTIPLE_COILS: 15,
WRITE_MULTIPLE_REGISTERS: 16,
REPORT_SLAVE_ID: 17,// Serial Line only
PROGRAM_884_M84: 18,
RESET_COMM_LINK: 19,
READ_FILE_RECORD: 20,
WRITE_FILE_RECORD: 21,
MASK_WRITE_REGISTER: 22,
READ_WRITE_MULTIPLE_REGISTERS:23,
READ_FIFO_QUEUE: 24,
ENCAPSULATED_INFERFACE_TRANSPORT:43
};
function ModbusRequestStack(stream) {
StreamStack.call(this, stream, {
data: this._onData
});
this.bufferlist = new BufferList();
if (this.stream._reqNum) {
this.stream._reqNum++;
} else {
this.stream._reqNum = 1;
}
}
inherits(ModbusRequestStack, StreamStack);
exports.ModbusRequestStack = ModbusRequestStack;
// The 'version' of the MODBUS protocol. Only version 0 is defined.
ModbusRequestStack.prototype.protocolVersion = 0;
// The unit identifier to request. This is usually important for
// serial lines, not so much over TCP.
ModbusRequestStack.prototype.unitIdentifier = 1;
// Make a MODBUS request for a given 'function code'.
ModbusRequestStack.prototype.request = function(functionCode) {
if (!client.REQUESTS[functionCode]) {
throw new Error('"REQUESTS['+functionCode+']" in "client.js" is not implemented!');
}
this.functionCode = functionCode;
var argsLen = arguments.length;
if (argsLen > 1) {
var callback = arguments[argsLen-1];
if (typeof(callback) == 'function') {
this.on('error', callback);
this.on('response', function(res) {
callback(null, res);
});
} else { callback = null; }
}
var args = slice.call(arguments, 1, argsLen-(callback?1:0));
try {
var pdu = client.REQUESTS[functionCode].apply(this, args);
} catch(e) {
this.emit('error', e);
return false;
}
var buf = Put()
.word16be(this.stream._reqNum)
.word16be(this.protocolVersion)
.word16be(pdu.length+2)
.word8(this.unitIdentifier)
.word8(functionCode)
.put(pdu)
.buffer();
return this.stream.write(buf);
}
ModbusRequestStack.prototype._onData = function(chunk) {
if (chunk) {
this.bufferlist.push(chunk);
//console.log(chunk);
}
if (!this.responseHeader && this.bufferlist.length >= MBAP_LENGTH) {
this.responseHeader = readMBAP(this.bufferlist);
// Re-check the bufferlist to see if we have the rest of the response already
this._onData();
} else if (!this._resFunctionCode && this.responseHeader && this.bufferlist.length >= 1) {
// Get the function code
this._resFunctionCode = readFunctionCode(this.bufferlist);
//console.log(this._resFunctionCode);
this._onData();
} else if (this.responseHeader && this._resFunctionCode >= 1 && this.bufferlist.length >= (this.responseHeader.length-2)) {
// We have the complete response.
if (this._resFunctionCode & EXCEPTION_BIT) {
// An exception was returned as the response!
var code = this.bufferlist.take(1)[0];
this.bufferlist.advance(1);
var err = new Error(exports.EXCEPTION_CODES[code]);
err.errno = code;
this.emit('error', err);
} else {
if (!client.RESPONSES[this._resFunctionCode]) {
return this.emit('error', new Error('"RESPONSES['+this._resFunctionCode+']" in "client.js" is not implemented!'));
}
try {
var response = client.RESPONSES[this._resFunctionCode].call(this, this.bufferlist);
} catch(e) {
return this.emit('error', e);
}
this.bufferlist.advance(this.responseHeader.length-2);
// Explicitly set the 'functionCode' property.
response.functionCode = this._resFunctionCode;
delete this._resFunctionCode;
// Modbus request/response complete; invoke callbacks and cleanup!
this.emit('response', response);
}
this.cleanup();
}
}
function ModbusResponseStack(stream) {
StreamStack.call(this, stream, {
data: this._onData
});
this.bufferlist = new BufferList();
}
inherits(ModbusResponseStack, StreamStack);
exports.ModbusResponseStack = ModbusResponseStack;
ModbusResponseStack.prototype._onData = function(chunk) {
if (chunk) {
this.bufferlist.push(chunk);
//console.log(chunk);
}
if (!this.requestHeader && this.bufferlist.length >= MBAP_LENGTH) {
this.requestHeader = readMBAP(this.bufferlist);
//console.log(this.requestHeader);
// Re-check the bufferlist to see if we have the rest of the request
// already (we probably do, it's usually sent in the same packet).
this._onData();
} else if (!this.functionCode && this.requestHeader && this.bufferlist.length >= 1) {
// Get the function code
this.functionCode = readFunctionCode(this.bufferlist);
//console.log(this.functionCode);
this._onData();
} else if (this.requestHeader && this.functionCode >= 1 && this.bufferlist.length >= (this.requestHeader.length-2)) {
// We have the complete request.
if (!server.REQUESTS[this.functionCode]) {
return this.emit('error', new Error('"REQUESTS['+this.functionCode+']" in "server.js" is not implemented!'));
}
try {
this.request = server.REQUESTS[this.functionCode].call(this, this.bufferlist);
} catch(e) {
return this.emit('error', e);
}
this.request.functionCode = this.functionCode;
for (var key in this.requestHeader) {
this.request[key] = this.requestHeader[key];
}
delete this.request.length;
this.bufferlist.advance(this.requestHeader.length-2);
//console.log('bufferlist.length: ' + this.bufferlist.length);
this._gotRequest = true;
this.emit('request', this.request);
}
}
// Sends back the response after receiving a request.
ModbusResponseStack.prototype.writeResponse = function() {
if (!this._gotRequest) {
throw new Error('Can\'t call "writeResponse" until after the "request" event');
}
if (!server.RESPONSES[this.functionCode]) {
throw new Error('"RESPONSES['+this.functionCode+']" in "server.js" is not implemented!');
}
try {
var pdu = server.RESPONSES[this.functionCode].apply(this, arguments);
} catch(e) {
this.emit('error', e);
return false;
}
var rtn = this.stream.write(Put()
.word16be(this.request.transactionId)
.word16be(this.request.protocolVersion)
.word16be(pdu.length+2)
.word8(this.request.unitIdentifier)
.word8(this.functionCode)
.put(pdu)
.buffer());
this.cleanup();
return rtn;
}
// Sends back an exception response after receiving a request.
ModbusResponseStack.prototype.writeException = function(exceptionCode) {
if (!this._gotRequest) {
throw new Error('Can\'t call "writeException" until after the "request" event');
}
var rtn = this.stream.write(Put()
.word16be(this.request.transactionId)
.word16be(this.request.protocolVersion)
.word16be(3)
.word8(this.request.unitIdentifier)
.word8(this.functionCode | EXCEPTION_BIT)
.word8(exceptionCode)
.buffer());
this.cleanup();
return rtn;
}
// Reads the "MODBUS Application Protocol" header from the given bufferlist,
// and return a object with 'transactionId', 'protocolVersion', 'length', and
// 'unitIdentifier' properties (standard for both client and server).
function readMBAP(bufferlist) {
var mbap = Binary(bufferlist)
.getWord16be('transactionId')
.getWord16be('protocolVersion')
.getWord16be('length')
.getWord8('unitIdentifier')
.end().vars;
bufferlist.advance(MBAP_LENGTH);
return mbap;
}
exports.readMBAP = readMBAP;
// Reads the "MODBUS Function Code", which comes immediately after the MBAP.
function readFunctionCode(bufferlist) {
var rtn = bufferlist.take(FUNCTION_CODE_LENGTH)[0];
bufferlist.advance(FUNCTION_CODE_LENGTH);
return rtn;
}
exports.readFunctionCode = readFunctionCode;