├── .eslintignore ├── src ├── index.js ├── logger.js ├── constants.js ├── errors.js ├── queue.js ├── packet-utils.js ├── serial-helper.js ├── task.js └── master.js ├── .babelrc ├── .travis.yml ├── .npmignore ├── tests ├── tape.js ├── packet-utils.spec.js ├── serial-helper.spec.js ├── queue.spec.js ├── task.spec.js └── master.spec.js ├── .gitignore ├── package.json ├── examples ├── polling-slaves.js └── slave-model.js ├── .eslintrc.yml └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | lib -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { ModbusMaster } from './master'; 2 | export { DATA_TYPES } from './packet-utils'; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": 5 6 | } 7 | }] 8 | ] 9 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "5" 5 | 6 | script: 7 | - npm run ci 8 | 9 | cache: 10 | directories: 11 | - node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | node_modules 4 | .babelrc 5 | .eslintignore 6 | .eslintrc.yml 7 | .gitignore 8 | .travis.yml 9 | package-lock.json 10 | examples 11 | .idea 12 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | constructor(options) { 3 | this.options = options; 4 | } 5 | 6 | info(string) { 7 | if (this.options.debug) { 8 | console.log(string); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/tape.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | 3 | if (process.env.NODE_ENV !== 'test') { 4 | const tapSpec = require('tap-spec'); 5 | test.createStream() 6 | .pipe(tapSpec()) 7 | .pipe(process.stdout); 8 | } 9 | 10 | export default test; 11 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const FUNCTION_CODES = { 2 | READ_COILS: 1, 3 | READ_DISCRETE_INPUTS: 2, 4 | READ_HOLDING_REGISTERS: 3, 5 | READ_INPUT_REGISTERS: 4, 6 | WRITE_SINGLE_COIL: 5, 7 | WRITE_SINGLE_REGISTER: 6, 8 | WRITE_MULTIPLE_COILS: 15, 9 | WRITE_MULTIPLE_REGISTERS: 16, 10 | }; 11 | export const DEFAULT_RETRY_COUNT = 10; 12 | export const RESPONSE_TIMEOUT = 500; 13 | export const QUEUE_TIMEOUT = 50; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | # Created by .ignore support plugin (hsz.mobi) 32 | 33 | # IDE 34 | .idea 35 | 36 | lib -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export class ModbusCrcError extends Error { 2 | constructor() { 3 | super(); 4 | 5 | this.message = 'Received Modbus response get invalid CRC'; 6 | this.name = 'ModbusCrcError'; 7 | Error.captureStackTrace(this, ModbusCrcError); 8 | } 9 | } 10 | 11 | export class ModbusAborted extends Error { 12 | constructor() { 13 | super(); 14 | this.message = 'Aborted'; 15 | this.name = 'ModbusAborted'; 16 | Error.captureStackTrace(this, ModbusAborted); 17 | } 18 | } 19 | 20 | export class ModbusRetryLimitExceed extends Error { 21 | constructor(add) { 22 | super(); 23 | this.message = 'Retry limit exceed ' + add; 24 | this.name = 'ModbusRetryLimitExceed'; 25 | Error.captureStackTrace(this, ModbusRetryLimitExceed); 26 | } 27 | } 28 | 29 | export class ModbusResponseTimeout extends Error { 30 | constructor(time) { 31 | super(); 32 | this.message = `Response timeout of ${time}ms exceed!`; 33 | this.name = 'ModbusResponseTimeout'; 34 | Error.captureStackTrace(this, ModbusResponseTimeout); 35 | } 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modbus-rtu", 3 | "version": "0.2.1", 4 | "scripts": { 5 | "build": "rimraf lib && babel src --out-dir lib", 6 | "lint": "eslint .", 7 | "test": "cross-env NODE_ENV=test tape -r babel-register tests/**/*.spec.js | tap-spec", 8 | "ci": "npm run lint && npm run test", 9 | "prepublishOnly": "npm run build" 10 | }, 11 | "description": "modbus-rtu implementation for node.js", 12 | "keywords": [ 13 | "modbus-rtu", 14 | "modbus", 15 | "modbus master" 16 | ], 17 | "main": "lib/index.js", 18 | "homepage": "https://github.com/thekip/node-modbus-rtu", 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:thekip/node-modbus-rtu.git" 22 | }, 23 | "author": { 24 | "name": "Tim Yatsenko", 25 | "url": "https://github.com/thekip" 26 | }, 27 | "bugs": { 28 | "url": "ttps://github.com/thekip/node-modbus-rtu/issues" 29 | }, 30 | "engines": { 31 | "node": ">=5" 32 | }, 33 | "dependencies": { 34 | "bluebird": "^3.3.0", 35 | "bufferput": "0.1.x", 36 | "crc": "3.3.0", 37 | "lodash": "^4.3.0" 38 | }, 39 | "peerDependencies": { 40 | "serialport": ">=2" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "^6.24.1", 44 | "babel-preset-env": "^1.6.0", 45 | "babel-register": "^6.24.1", 46 | "cross-env": "^2.0.0", 47 | "eslint": "^4.3.0", 48 | "rimraf": "^2.6.1", 49 | "sinon": "^1.17.5", 50 | "tap-spec": "^4.1.1", 51 | "tape": "^4.7.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/queue.js: -------------------------------------------------------------------------------- 1 | export class Queue { 2 | /** 3 | * @template T 4 | * @param {number} timeout pause between queue tasks 5 | */ 6 | constructor(timeout) { 7 | /** @potected */ 8 | this.taskHandler = (task, done) => done(); 9 | 10 | /** @private */ 11 | this.queueTimeout = timeout; 12 | 13 | /** 14 | * @protected 15 | * @type {T[]} 16 | */ 17 | this.queue = []; 18 | } 19 | 20 | /** 21 | * Set handler which will be called on each task 22 | * @param {function(task: T, done: function):void} handler 23 | */ 24 | setTaskHandler(handler) { 25 | this.taskHandler = handler; 26 | } 27 | 28 | /** 29 | * @param {T} task 30 | */ 31 | push(task) { 32 | this.queue.push(task); 33 | } 34 | 35 | start() { 36 | this.isActive = true; 37 | this.handle(); 38 | } 39 | 40 | stop() { 41 | this.isActive = false; 42 | } 43 | 44 | /** 45 | * @private 46 | */ 47 | handle() { 48 | if (!this.isActive) { 49 | return; 50 | } 51 | 52 | if (this.queue.length) { 53 | const task = this.queue.shift(); 54 | this.taskHandler(task, this.continueQueue.bind(this)); 55 | } else { 56 | this.continueQueue(); 57 | } 58 | } 59 | 60 | /** 61 | * @private 62 | */ 63 | continueQueue() { 64 | // pause between calls 65 | setTimeout(this.handle.bind(this), this.queueTimeout); 66 | } 67 | } -------------------------------------------------------------------------------- /src/packet-utils.js: -------------------------------------------------------------------------------- 1 | import crc from 'crc'; 2 | import BufferPut from 'bufferput'; 3 | 4 | export const DATA_TYPES = { 5 | INT: 1, 6 | UINT: 2, 7 | ASCII: 3, 8 | }; 9 | 10 | /** 11 | * Slice header, bytes count and crc. Return buffer only with data 12 | * @param {Buffer} buffer 13 | */ 14 | export function getDataBuffer(buffer) { 15 | return buffer.slice(3, buffer.length - 2); 16 | } 17 | 18 | /** 19 | * Parse function 03 response packet (read holding registers) 20 | * @param {Buffer} buffer 21 | * @param {number} [dataType] 22 | * @returns {number[]} 23 | */ 24 | export function parseFc03Packet(buffer, dataType) { 25 | const results = []; 26 | 27 | for (let i = 0; i < buffer.length; i += 2) { 28 | results.push(readDataFromBuffer(buffer, i, dataType)); 29 | } 30 | 31 | return results; 32 | } 33 | 34 | /** 35 | * Returns new buffer signed with CRC 36 | * @param {Buffer} buf 37 | * @returns {Buffer} 38 | */ 39 | export function addCrc(buf) { 40 | return (new BufferPut()) 41 | .put(buf) 42 | .word16le(crc.crc16modbus(buf)) 43 | .buffer(); 44 | } 45 | 46 | /** 47 | * 48 | * @param {Buffer} buffer 49 | * @returns boolean 50 | */ 51 | export function checkCrc(buffer) { 52 | const pdu = buffer.slice(0, buffer.length - 2); 53 | return buffer.equals(this.addCrc(pdu)); 54 | } 55 | 56 | /** 57 | * 58 | * @param {Buffer} buffer 59 | * @param {int} offset 60 | * @param {int} [dataType] 61 | * @returns {number | string} 62 | */ 63 | function readDataFromBuffer(buffer, offset, dataType) { 64 | switch (dataType) { 65 | case DATA_TYPES.UINT: 66 | return buffer.readUInt16BE(offset); 67 | case DATA_TYPES.ASCII: 68 | return buffer.toString('ascii', offset, offset + 2); 69 | default: 70 | return buffer.readInt16BE(offset); 71 | } 72 | } -------------------------------------------------------------------------------- /tests/packet-utils.spec.js: -------------------------------------------------------------------------------- 1 | import test from './tape'; 2 | import * as packetUtils from '../src/packet-utils'; 3 | 4 | test('Parse holding registers packet', (t) => { 5 | const buf = new Buffer('11 03 06 AE41 5652 4340 49AD'.replace(/\s/g, ''), 'hex'); 6 | 7 | const results = packetUtils.parseFc03Packet(packetUtils.getDataBuffer(buf)); 8 | 9 | t.equal(results.length, 3, 'Should be 3 results in packet, because we read 3 registers'); 10 | 11 | const expectedResults = [-20927, 22098, 17216]; 12 | 13 | results.forEach((result, i) => { 14 | t.equal(results[i], expectedResults[i], `Result ${i} is equal to expected`); 15 | }); 16 | 17 | t.end(); 18 | }); 19 | 20 | test('Parse holding registers packet with Unsigned int data type', (t) => { 21 | const buf = new Buffer('11 03 02 AE41 49AD'.replace(/\s/g, ''), 'hex'); 22 | const results = packetUtils.parseFc03Packet(packetUtils.getDataBuffer(buf), packetUtils.DATA_TYPES.UINT); 23 | 24 | t.equal(results[0], 44609, 'Result should not be negative'); 25 | t.end(); 26 | }); 27 | 28 | test('Parse holding registers packet with ascii data type', (t) => { 29 | const buf = new Buffer('11 03 04 5652 5652 49AD'.replace(/\s/g, ''), 'hex'); 30 | 31 | const results = packetUtils.parseFc03Packet(packetUtils.getDataBuffer(buf), packetUtils.DATA_TYPES.ASCII); 32 | 33 | t.equal(results[0], 'VR', 'Result should be 2 ascii letters'); 34 | t.equal(results[1], 'VR', 'Result should be 2 ascii letters, and no collisions'); 35 | 36 | t.end(); 37 | }); 38 | 39 | test('Calculate and add CRC to packet', (t) => { 40 | const buf = new Buffer('11 03 06 AE41 5652 4340'.replace(/\s/g, ''), 'hex'); 41 | 42 | const signedBuffer = packetUtils.addCrc(buf); 43 | const actualCrc = signedBuffer.readUInt16LE(signedBuffer.length - 2); 44 | 45 | t.equal(signedBuffer.length, buf.length + 2, 'CRC should be added to the end of buffer'); 46 | t.equal(actualCrc, 44361, 'Added crc is valid'); 47 | 48 | t.end(); 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /examples/polling-slaves.js: -------------------------------------------------------------------------------- 1 | const SerialPort = require('serialport').SerialPort; 2 | const modbus = require('modbus-rtu'); 3 | const Promise = require('bluebird'); 4 | 5 | // Polling data from slaves in loop. 6 | // 7 | // Polling slaves is quite often usage of modbus protocol. Assume you develop a real-time app which show a temperature from thermostats. 8 | // For this app you need to poll all thermostats in loop, and update Ui when temperature changed. 9 | // 10 | // When polling in loop you have to wait response of all yours request, otherwise pause between loop will be not work. 11 | // 12 | // Check also [slave-model] example to see how make this code better and more fun. 13 | 14 | // create serial port with params. Refer to node-serialport for documentation 15 | const serialPort = new SerialPort('/dev/ttyUSB0', { 16 | baudrate: 2400, 17 | }); 18 | 19 | new modbus.Master(serialPort, function (master) { 20 | // Create an array for promises 21 | const promises = []; 22 | 23 | (function loop() { 24 | // Push all returned promises into array 25 | 26 | // Read from slave 1 27 | promises.push(master.readHoldingRegisters(1, 0, 4).then(function (data) { 28 | console.log('slave 1', data); 29 | })); 30 | 31 | // Read from slave 2 32 | promises.push(master.readHoldingRegisters(2, 0, 4).then(function (data) { 33 | console.log('slave 2', data); 34 | })); 35 | 36 | // Read from slave 3 37 | promises.push(master.readHoldingRegisters(3, 0, 4).then(function (data) { 38 | console.log('slave 3', data); 39 | })); 40 | 41 | // Wait while all requests finished, and then restart loop() with 300ms timeout. 42 | Promise.all(promises).catch(function (err) { 43 | console.log(err); // catch all errors 44 | }).finally(function () { 45 | setTimeout(loop, 300); 46 | }); 47 | })(); 48 | }); 49 | 50 | new modbus.Master(new SerialPort('/dev/ttyUSB0', { 51 | baudrate: 2400, 52 | })); -------------------------------------------------------------------------------- /tests/serial-helper.spec.js: -------------------------------------------------------------------------------- 1 | import test from './tape'; 2 | import sinon from 'sinon'; 3 | import { SerialHelper } from '../src/serial-helper'; 4 | import { Queue } from '../src/queue'; 5 | import { ModbusResponseTimeout } from '../src/errors'; 6 | import noop from 'lodash/noop'; 7 | import { EventEmitter } from 'events'; 8 | 9 | const serialPort = Object.assign(new EventEmitter(), { 10 | write: noop, 11 | }); 12 | 13 | const options = { 14 | responseTimeout: 50, 15 | queueTimeout: 0, 16 | }; 17 | 18 | class QueueStub extends Queue { 19 | push(task) { 20 | this.taskHandler(task, noop); 21 | } 22 | } 23 | 24 | const samplePayload = new Buffer('11 03 00 6B 00 03 76 87'.replace(/\s/g, ''), 'hex'); 25 | 26 | test('Should start the Queue when port opens', (t) => { 27 | const queue = new Queue(options.queueTimeout); 28 | queue.start = sinon.spy(); 29 | 30 | new SerialHelper(serialPort, queue, options); // eslint-disable-line no-new 31 | serialPort.emit('open'); 32 | 33 | t.ok(queue.start.called, 'Queue start method should be called'); 34 | t.end(); 35 | }); 36 | 37 | test('Should resolve promise with valid message', (t) => { 38 | t.plan(1); 39 | const queue = new QueueStub(options.queueTimeout); 40 | const helper = new SerialHelper(serialPort, queue, options); 41 | const msg = '11 03 06 AE 41 56 52 43 40 49 AD'.replace(/\s/g, ''); 42 | 43 | helper.write(samplePayload).then((response) => { 44 | t.equal(response.toString('hex').toUpperCase(), msg); 45 | }); 46 | 47 | for (let i = 0; i < msg.length; i += 2) { 48 | setTimeout(() => { 49 | serialPort.emit('data', new Buffer(msg.slice(i, i + 2), 'hex')); 50 | }); 51 | } 52 | }); 53 | 54 | test('Should reject promise if timeout exceed', (t) => { 55 | t.plan(1); 56 | 57 | const clock = sinon.useFakeTimers(); 58 | const queue = new QueueStub(options.queueTimeout); 59 | const helper = new SerialHelper(serialPort, queue, options); 60 | 61 | helper.write(samplePayload).catch((err) => { 62 | t.equal(err.constructor, ModbusResponseTimeout, 'Error should be a proper type'); 63 | }); 64 | 65 | clock.tick(options.responseTimeout + 10); 66 | clock.restore(); 67 | }); 68 | 69 | -------------------------------------------------------------------------------- /tests/queue.spec.js: -------------------------------------------------------------------------------- 1 | import test from './tape'; 2 | import sinon from 'sinon'; 3 | 4 | import { Queue } from '../src/queue'; 5 | 6 | /** 7 | * 8 | * @type {Queue[]} 9 | */ 10 | const queues = []; 11 | 12 | /** 13 | * @param onEachTask 14 | * @param [delay] 15 | * @returns {Queue} 16 | */ 17 | function createQueue(onEachTask, delay = 0) { 18 | const queue = new Queue(delay); 19 | queue.setTaskHandler(onEachTask); 20 | queue.start(); 21 | queues.push(queue); 22 | return queue; 23 | } 24 | 25 | // dispose all queues on tests finish 26 | test.onFinish(() => { 27 | queues.forEach((q) => q.stop()); 28 | }); 29 | 30 | test('Should handle tasks one by one', (t) => { 31 | const tasks = ['task1', 'task2', 'task3']; 32 | let i = 0; 33 | const onEachTask = (task, done) => { 34 | t.equal(task, tasks[i++]); 35 | done(); 36 | }; 37 | 38 | const queue = createQueue(onEachTask); 39 | tasks.forEach((task) => queue.push(task)); 40 | 41 | t.plan(3); 42 | }); 43 | 44 | test('Should wait until done() is called', (t) => { 45 | const tasks = ['task1', 'task2', 'task3']; 46 | let i = 0; 47 | const onEachTask = (task, done) => { 48 | t.equal(task, tasks[i++]); 49 | 50 | if (i === 0) { 51 | done(); 52 | } 53 | }; 54 | 55 | const queue = createQueue(onEachTask); 56 | tasks.forEach((task) => queue.push(task)); 57 | 58 | t.plan(1); 59 | }); 60 | 61 | test('Should handle tasks with a delay', (t) => { 62 | const clock = sinon.useFakeTimers(); 63 | const tasks = [false, false, false]; 64 | let i = 0; 65 | const onEachTask = (task, done) => { 66 | tasks[i++] = true; 67 | done(); 68 | }; 69 | 70 | const queue = createQueue(onEachTask, 200); 71 | tasks.forEach((task) => queue.push(task)); 72 | 73 | clock.tick(200); 74 | t.deepEqual(tasks, [true, false, false]); 75 | 76 | clock.tick(200); 77 | t.deepEqual(tasks, [true, true, false]); 78 | 79 | clock.tick(200); 80 | t.deepEqual(tasks, [true, true, true]); 81 | 82 | clock.restore(); 83 | t.end(); 84 | }); 85 | 86 | test('Should handle tasks added after queue is started', (t) => { 87 | const expTask = 'some task'; 88 | 89 | const onEachTask = (task, done) => { 90 | t.equal(task, expTask); 91 | t.end(); 92 | done(); 93 | }; 94 | 95 | const queue = createQueue(onEachTask); 96 | queue.push(expTask); 97 | }); -------------------------------------------------------------------------------- /tests/task.spec.js: -------------------------------------------------------------------------------- 1 | import test from './tape'; 2 | import { Task } from '../src/task'; 3 | 4 | test('Test deferred api', (t) => { 5 | const samplePayload = new Buffer('01 03 06 AE41 5652 4340 49AD'.replace(/\s/g, ''), 'hex'); 6 | const expectedData = [22, 23, 24]; 7 | 8 | const resolvedTask = new Task(samplePayload); 9 | resolvedTask.promise.then((data) => { 10 | t.equal(data, expectedData, 'Should allow resolve promise with value'); 11 | }); 12 | resolvedTask.resolve(expectedData); 13 | 14 | const rejectedTask = new Task(samplePayload); 15 | rejectedTask.promise.catch((data) => { 16 | t.equal(data, expectedData, 'Should allow reject promise with value'); 17 | }); 18 | rejectedTask.reject(expectedData); 19 | 20 | t.plan(2); 21 | }); 22 | 23 | function testMessageHandling(t, payload, input, msg) { 24 | const task = new Task(new Buffer(payload, 'hex')); 25 | 26 | for (let i = 0; i < input.length; i += 2) { 27 | setTimeout(() => { 28 | task.receiveData(new Buffer(input.slice(i, i + 2), 'hex'), (response) => { 29 | t.equal(response.toString('hex'), msg); 30 | t.end(); 31 | }); 32 | }); 33 | } 34 | } 35 | 36 | test('should return a valid Modbus RTU message', (t) => { 37 | const msg = '110306ae415652434049ad'; 38 | testMessageHandling(t, '1103006B00037687', msg, msg); 39 | }); 40 | 41 | test('should return a valid Modbus RTU exception', (t) => { 42 | const msg = '1183044136'; 43 | testMessageHandling(t, '1103006B00037687', msg, msg); 44 | }); 45 | 46 | test('Special data package, should return a valid Modbus RTU message', (t) => { 47 | const msg = '010380018301830183018301830183018301830183018301830183018301830' + 48 | '1830183018301830183018301830183018301830183018301830183018301830183018301830183' + 49 | '0183018301830183018301830183018301830183018301830183018301830183018301830183018' + 50 | '3018301830183018301830183018301830183018346e0'; 51 | testMessageHandling(t, '010300000040443A', msg, msg); 52 | }); 53 | 54 | test('Illegal start chars, should return a valid Modbus RTU message', (t) => { 55 | const illegalChars = '205454ff'; 56 | const msg = '110306ae415652434049ad'; 57 | testMessageHandling(t, '1103006B00037687', illegalChars + msg, msg); 58 | }); 59 | 60 | test('Illegal end chars, should return a valid Modbus RTU message', (t) => { 61 | const illegalChars = '205454ff'; 62 | const msg = '110306ae415652434049ad'; 63 | testMessageHandling(t, '1103006B00037687', msg + illegalChars, msg); 64 | }); -------------------------------------------------------------------------------- /src/serial-helper.js: -------------------------------------------------------------------------------- 1 | import { Task } from './task'; 2 | import { Queue } from './queue'; 3 | import { ModbusResponseTimeout } from './errors'; 4 | import { Logger } from './logger'; 5 | 6 | export class SerialHelperFactory { 7 | /** 8 | * @param {SerialPort} serialPort 9 | * @param options 10 | * @returns {SerialHelper} 11 | */ 12 | static create(serialPort, options) { 13 | const queue = new Queue(options.queueTimeout); 14 | return new SerialHelper(serialPort, queue, options); 15 | } 16 | } 17 | 18 | export class SerialHelper { 19 | /** 20 | * @param {SerialPort} serialPort 21 | * @param {Queue} queue 22 | * @param options 23 | */ 24 | constructor(serialPort, queue, options) { 25 | /** 26 | * @type {Queue} 27 | * @private 28 | */ 29 | this.queue = queue; 30 | queue.setTaskHandler(this.handleTask.bind(this)); 31 | 32 | /** 33 | * @private 34 | */ 35 | this.options = options; 36 | this.serialPort = serialPort; 37 | this.logger = new Logger(options); 38 | 39 | this.bindToSerialPort(); 40 | } 41 | 42 | /** 43 | * 44 | * @param {Buffer} buffer 45 | * @returns {Promise} 46 | */ 47 | write(buffer) { 48 | const task = new Task(buffer); 49 | this.queue.push(task); 50 | 51 | return task.promise; 52 | } 53 | 54 | /** 55 | * @private 56 | */ 57 | bindToSerialPort() { 58 | this.serialPort.on('open', () => { 59 | this.queue.start(); 60 | }); 61 | } 62 | 63 | /** 64 | * 65 | * @param {Task} task 66 | * @param {function} done 67 | * @private 68 | */ 69 | handleTask(task, done) { 70 | this.logger.info('write ' + task.payload.toString('HEX')); 71 | this.serialPort.write(task.payload, (error) => { 72 | if (error) { 73 | task.reject(error); 74 | } 75 | }); 76 | 77 | // set execution timeout for task 78 | setTimeout(() => { 79 | task.reject(new ModbusResponseTimeout(this.options.responseTimeout)); 80 | }, this.options.responseTimeout); 81 | 82 | const onData = (data) => { 83 | task.receiveData(data, (response) => { 84 | this.logger.info('resp ' + response.toString('HEX')); 85 | task.resolve(response); 86 | }); 87 | }; 88 | 89 | this.serialPort.on('data', onData); 90 | 91 | task.promise.catch(() => {}).finally(() => { 92 | this.serialPort.removeListener('data', onData); 93 | done(); 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/slave-model.js: -------------------------------------------------------------------------------- 1 | // In real projects polling all you slaves in one loop is extremely uncomfortable in JS. 2 | // The better way is create an abstraction. For example class for each you device. 3 | // 4 | // Assume we have a modbus thermostat, and we want to do something when data from thermostat is changed. 5 | // 6 | // Create class for this thermostat: 7 | 8 | const _ = require('lodash'); // npm i lodash 9 | const MicroEvent = require('microevent'); // npm i microevent 10 | 11 | module.exports = Thermostat; 12 | 13 | // Describe all slave registers 14 | let ENABLED_REGISTER = 0, 15 | ROOM_TEMP_REGISTER = 3, 16 | TEMP_SETPOINT_REGISTER = 4; 17 | 18 | function Thermostat(modbusMaster, modbusAddr) { 19 | this.modbusMaster = modbusMaster; 20 | this.modbusAddr = modbusAddr; 21 | 22 | // create properties 23 | this.enabled = false; 24 | this.roomTemp = null; 25 | this.tempSetpoint = null; 26 | 27 | this._rawData = []; 28 | this._oldData = []; 29 | 30 | this.watch(); 31 | } 32 | 33 | MicroEvent.mixin(Thermostat); 34 | 35 | _.extend(Thermostat.prototype, { 36 | update: function () { 37 | const th = this; 38 | // read data from thermostat 39 | return this.modbusMaster.readHoldingRegisters(this.modbusAddr, 0, 6) 40 | .then(function (data) { 41 | // when data received store it in the object properties 42 | th._rawData = data; 43 | 44 | th.enabled = data[ENABLED_REGISTER] !== 90; 45 | th.roomTemp = data[ROOM_TEMP_REGISTER] / 2; 46 | th.tempSetpoint = data[TEMP_SETPOINT_REGISTER] / 2; 47 | }); 48 | }, 49 | 50 | toString: function () { 51 | return 'Status: ' + (this.enabled ? 'on' : 'off') + 52 | '; Room temp: ' + this.roomTemp + 'C; Set temp: ' + this.tempSetpoint + 'C;'; 53 | }, 54 | 55 | watch: function () { 56 | const self = this; 57 | 58 | // make internal loop. 59 | self.update().finally(function () { 60 | if (!_.isEqual(self._oldData, self._rawData)) { 61 | self.trigger('change', self); 62 | self._oldData = self._rawData.slice(0); // clone data array 63 | } 64 | 65 | setTimeout(function () { 66 | self.watch(); 67 | }, 300); 68 | }).catch(function (err) { 69 | console.log(err); 70 | }); 71 | }, 72 | }); 73 | 74 | // This simple class blackboxing all modbus communication inside and provide to us simple and clean api. 75 | new modbus.Master(serialPort, function (modbus) { 76 | const t = new Thermostat(modbus, slave); 77 | t.bind('change', function () { 78 | // this code execute only when thermostat is changed 79 | console.log('Thermostat ' + i + '. ' + t.toString()); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/master.spec.js: -------------------------------------------------------------------------------- 1 | import test from './tape'; 2 | import Promise from 'bluebird'; 3 | import noop from 'lodash/noop'; 4 | 5 | import { ModbusMaster } from '../src/master'; 6 | import { DATA_TYPES } from '../src/packet-utils'; 7 | 8 | const serialPort = { 9 | on: noop, 10 | }; 11 | 12 | test('Read holding registers', (t) => { 13 | const master = new ModbusMaster(serialPort); 14 | 15 | master.request = function () { 16 | return new Promise((resolve) => { 17 | resolve(new Buffer('01 03 06 AE41 5652 4340 49AD'.replace(/\s/g, ''), 'hex')); 18 | }); 19 | }; 20 | 21 | master.readHoldingRegisters(1, 0, 3).then((data) => { 22 | t.equals(data.length, 3, 'If no callback passed, standard parser applied'); 23 | }); 24 | 25 | master.readHoldingRegisters(1, 0, 3, (buffer) => { 26 | return buffer.readUInt32BE(0); 27 | }).then((bigNumber) => { 28 | t.equals(bigNumber, 2923517522, 'If callback passed, callback used for parsing buffer'); 29 | }); 30 | 31 | master.readHoldingRegisters(1, 0, 3, DATA_TYPES.UINT).then((results) => { 32 | t.equals(results[0], 44609, 'If data type passed, it should be used in buffer parser'); 33 | }); 34 | 35 | t.plan(3); 36 | }); 37 | 38 | test('Should create valid request packet', (t) => { 39 | const master = new ModbusMaster(serialPort); 40 | 41 | master.request = function (requestPacket) { 42 | t.ok(requestPacket.equals(new Buffer('01 03 00 00 00 03'.replace(/\s/g, ''), 'hex')), 'Request packet is valid'); 43 | 44 | return new Promise((resolve) => { 45 | resolve(new Buffer(0)); 46 | }); 47 | }; 48 | 49 | master.readHoldingRegisters(1, 0, 3); 50 | 51 | t.end(); 52 | }); 53 | 54 | test('Write single register should retry if error, and throw Error if limit exceed', (t) => { 55 | const master = new ModbusMaster(serialPort); 56 | const RETRY_LIMIT = 2; 57 | let i = 0; 58 | master.request = function () { 59 | i++; 60 | 61 | return new Promise((resolve, reject) => { 62 | reject(); 63 | }); 64 | }; 65 | 66 | master.writeSingleRegister(1, 0, 3, RETRY_LIMIT).catch((err) => { 67 | t.equals(i, RETRY_LIMIT, 'Actual count of retries is correct'); 68 | t.equals(err.name, 'ModbusRetryLimitExceed', 'Throwed Error has correct type'); 69 | }); 70 | 71 | t.plan(2); 72 | }); 73 | 74 | test('Write single register should resolve promise if success', (t) => { 75 | const master = new ModbusMaster(serialPort); 76 | const RETRY_LIMIT = 2; 77 | let i = 0; 78 | 79 | master.request = function () { 80 | i++; 81 | 82 | return new Promise((resolve, reject) => { 83 | if (i === 1) { // resolve after second retry 84 | reject(); 85 | } else { 86 | resolve(); 87 | } 88 | }); 89 | }; 90 | 91 | master.writeSingleRegister(1, 0, 3, RETRY_LIMIT).then(() => { 92 | t.ok(true, 'Success handler is called'); 93 | t.equals(i, 2, 'Actual count of retries is correct'); 94 | }); 95 | 96 | t.plan(2); 97 | }); -------------------------------------------------------------------------------- /src/task.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | 3 | const EXCEPTION_LENGTH = 5; 4 | const MIN_DATA_LENGTH = 6; 5 | const MAX_BUFFER_LENGTH = 256; 6 | 7 | export class Task { 8 | /** 9 | * @param {Buffer} payload 10 | */ 11 | constructor(payload) { 12 | /** 13 | * @private 14 | */ 15 | this.deferred = this.createDeferred(); 16 | /** 17 | * 18 | * @type {Promise} 19 | */ 20 | this.promise = this.deferred.promise; 21 | 22 | /** 23 | * @type {Buffer} 24 | */ 25 | this.payload = payload; 26 | 27 | /** 28 | * @private 29 | */ 30 | this.id = payload[0]; 31 | 32 | /** 33 | * @private 34 | */ 35 | this.cmd = payload[1]; 36 | 37 | /** 38 | * @private 39 | */ 40 | this.length = this.getExpectedLength(this.cmd, payload); 41 | 42 | /** 43 | * @private 44 | */ 45 | this.buffer = new Buffer(0); 46 | } 47 | 48 | resolve(data) { 49 | this.deferred.resolve(data); 50 | } 51 | 52 | reject(error) { 53 | this.deferred.reject(error); 54 | } 55 | 56 | /** 57 | * 58 | * @param {Buffer} data 59 | * @param {function(response: Buffer)} done 60 | * @returns {Buffer} 61 | */ 62 | receiveData(data, done) { 63 | this.buffer = Buffer.concat([this.buffer, data]); 64 | 65 | const expectedLength = this.length; 66 | let bufferLength = this.buffer.length; 67 | 68 | if (expectedLength < MIN_DATA_LENGTH || bufferLength < EXCEPTION_LENGTH) { return; } 69 | 70 | if (bufferLength > MAX_BUFFER_LENGTH) { 71 | this.buffer = this.buffer.slice(-MAX_BUFFER_LENGTH); 72 | bufferLength = MAX_BUFFER_LENGTH; 73 | } 74 | 75 | // loop and check length-sized buffer chunks 76 | const maxOffset = bufferLength - EXCEPTION_LENGTH; 77 | for (let i = 0; i <= maxOffset; i++) { 78 | const unitId = this.buffer[i]; 79 | const functionCode = this.buffer[i + 1]; 80 | 81 | if (unitId !== this.id) { continue; } 82 | 83 | if (functionCode === this.cmd && i + expectedLength <= bufferLength) { 84 | return done(this.getMessage(i, expectedLength)); 85 | } 86 | if (functionCode === (0x80 | this.cmd) && i + EXCEPTION_LENGTH <= bufferLength) { 87 | return done(this.getMessage(i, EXCEPTION_LENGTH)); 88 | } 89 | 90 | // frame header matches, but still missing bytes pending 91 | if (functionCode === (0x7f & this.cmd)) { break; } 92 | } 93 | } 94 | 95 | /** 96 | * @private 97 | * @param {number} start 98 | * @param {number} length 99 | * @returns {Buffer} 100 | */ 101 | getMessage(start, length) { 102 | const msg = this.buffer.slice(start, start + length); 103 | this.buffer = this.buffer.slice(start + length); 104 | return msg; 105 | } 106 | 107 | /** 108 | * @private 109 | * @param {number} cmd 110 | * @param {Buffer} payload 111 | * @return number 112 | */ 113 | getExpectedLength(cmd, payload) { 114 | const length = payload.readUInt16BE(4); 115 | 116 | switch (cmd) { 117 | case 1: 118 | case 2: 119 | return 3 + parseInt((length - 1) / 8 + 1, 10) + 2; 120 | case 3: 121 | case 4: 122 | return 3 + 2 * length + 2; 123 | case 5: 124 | case 6: 125 | case 15: 126 | case 16: 127 | return 6 + 2; 128 | default: 129 | return 0; 130 | } 131 | } 132 | 133 | /** 134 | * @private 135 | * @returns {{}} 136 | */ 137 | createDeferred() { 138 | const deferred = {}; 139 | 140 | deferred.promise = new Promise((resolve, reject) => { 141 | deferred.resolve = resolve; 142 | deferred.reject = reject; 143 | }); 144 | 145 | return deferred; 146 | } 147 | } -------------------------------------------------------------------------------- /src/master.js: -------------------------------------------------------------------------------- 1 | import BufferPut from 'bufferput'; 2 | import Promise from 'bluebird'; 3 | import { SerialHelperFactory } from './serial-helper'; 4 | import { Logger } from './logger'; 5 | 6 | import { 7 | FUNCTION_CODES, 8 | RESPONSE_TIMEOUT, 9 | QUEUE_TIMEOUT, 10 | DEFAULT_RETRY_COUNT, 11 | } from './constants'; 12 | 13 | import { ModbusRetryLimitExceed, ModbusCrcError } from './errors'; 14 | import * as packetUtils from './packet-utils'; 15 | 16 | export class ModbusMaster { 17 | constructor(serialPort, options) { 18 | serialPort.on('error', (err) => { 19 | console.error(err); 20 | }); 21 | 22 | this._options = Object.assign({}, { 23 | responseTimeout: RESPONSE_TIMEOUT, 24 | queueTimeout: QUEUE_TIMEOUT, 25 | }, options || {}); 26 | 27 | this.logger = new Logger(this._options); 28 | this.serial = SerialHelperFactory.create(serialPort, this._options); 29 | } 30 | 31 | /** 32 | * Modbus function read holding registers 33 | * @param {number} slave 34 | * @param {number} start 35 | * @param {number} length 36 | * @param {number | function} [dataType] value from DATA_TYPES const or callback 37 | * @returns {Promise} 38 | */ 39 | readHoldingRegisters(slave, start, length, dataType) { 40 | const packet = this.createFixedPacket(slave, FUNCTION_CODES.READ_HOLDING_REGISTERS, start, length); 41 | 42 | return this.request(packet).then((buffer) => { 43 | const buf = packetUtils.getDataBuffer(buffer); 44 | 45 | if (typeof (dataType) === 'function') { 46 | return dataType(buf); 47 | } 48 | 49 | return packetUtils.parseFc03Packet(buf, dataType); 50 | }); 51 | } 52 | 53 | /** 54 | * 55 | * @param {number} slave 56 | * @param {number} register 57 | * @param {number} value 58 | * @param {number} [retryCount] 59 | */ 60 | writeSingleRegister(slave, register, value, retryCount) { 61 | const packet = this.createFixedPacket(slave, FUNCTION_CODES.WRITE_SINGLE_REGISTER, register, value); 62 | retryCount = retryCount || DEFAULT_RETRY_COUNT; 63 | 64 | const performRequest = (retry) => { 65 | return new Promise((resolve, reject) => { 66 | const funcName = 'writeSingleRegister: '; 67 | const funcId = 68 | `Slave ${slave}; Register: ${register}; Value: ${value};` + 69 | `Retry ${retryCount + 1 - retry} of ${retryCount}`; 70 | 71 | if (retry <= 0) { 72 | throw new ModbusRetryLimitExceed(funcId); 73 | } 74 | 75 | this.logger.info(funcName + 'perform request.' + funcId); 76 | 77 | this.request(packet) 78 | .then(resolve) 79 | .catch((err) => { 80 | this.logger.info(funcName + err + funcId); 81 | 82 | return performRequest(--retry) 83 | .then(resolve) 84 | .catch(reject); 85 | }); 86 | }); 87 | }; 88 | return performRequest(retryCount); 89 | } 90 | 91 | /** 92 | * 93 | * @param {number} slave 94 | * @param {number} start 95 | * @param {number[]} array 96 | */ 97 | writeMultipleRegisters(slave, start, array) { 98 | const packet = this.createVariousPacket(slave, FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS, start, array); 99 | return this.request(packet); 100 | } 101 | 102 | /** 103 | * Create modbus packet with fixed length 104 | * @private 105 | * @param {number} slave 106 | * @param {number} func 107 | * @param {number} param 108 | * @param {number} param2 109 | * @returns {Buffer} 110 | */ 111 | createFixedPacket(slave, func, param, param2) { 112 | return (new BufferPut()) 113 | .word8be(slave) 114 | .word8be(func) 115 | .word16be(param) 116 | .word16be(param2) 117 | .buffer(); 118 | } 119 | 120 | /** 121 | * Create modbus packet with various length 122 | * @private 123 | * @param {number} slave 124 | * @param {number} func 125 | * @param {number} start 126 | * @param {number[]} array 127 | * @returns {Buffer} 128 | */ 129 | createVariousPacket(slave, func, start, array) { 130 | const buf = (new BufferPut()) 131 | .word8be(slave) 132 | .word8be(func) 133 | .word16be(start) 134 | .word16be(array.length) 135 | .word8be(array.length * 2); 136 | 137 | array.forEach((value) => buf.word16be(value)); 138 | 139 | return buf.buffer(); 140 | } 141 | 142 | /** 143 | * @private 144 | * @param {Buffer} buffer 145 | * @returns {Promise} 146 | */ 147 | request(buffer) { 148 | return this.serial.write(packetUtils.addCrc(buffer)) 149 | .then((response) => { 150 | if (!packetUtils.checkCrc(response)) { 151 | throw new ModbusCrcError(); 152 | } 153 | return response; 154 | }); 155 | } 156 | } -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | 4 | parserOptions: 5 | ecmaVersion: 6 6 | sourceType: module 7 | 8 | env: 9 | node: true 10 | 11 | rules: 12 | ### Possible Errors ### 13 | 14 | comma-dangle: 15 | - error 16 | - always-multiline 17 | no-cond-assign: error 18 | no-console: off 19 | no-constant-condition: error 20 | no-control-regex: error 21 | no-debugger: error 22 | no-dupe-keys: error 23 | no-empty: error 24 | no-empty-character-class: error 25 | no-ex-assign: error 26 | no-extra-boolean-cast: error 27 | no-extra-parens: 28 | - error 29 | - functions 30 | no-extra-semi: error 31 | no-func-assign: error 32 | no-inner-declarations: 33 | - error 34 | - functions 35 | no-invalid-regexp: error 36 | no-irregular-whitespace: error 37 | no-negated-in-lhs: error 38 | no-obj-calls: error 39 | no-regex-spaces: error 40 | no-sparse-arrays: error 41 | no-unreachable: error 42 | use-isnan: error 43 | valid-jsdoc: off 44 | valid-typeof: error 45 | 46 | 47 | ### Best Practices ### 48 | 49 | block-scoped-var: error 50 | complexity: 51 | - 1 52 | - 10 53 | consistent-return: off 54 | curly: 55 | - error 56 | - all 57 | default-case: error 58 | dot-notation: error 59 | eqeqeq: 60 | - error 61 | - allow-null 62 | guard-for-in: off 63 | no-alert: error 64 | no-caller: error 65 | no-div-regex: off 66 | no-else-return: error 67 | no-eq-null: off 68 | no-eval: error 69 | no-extend-native: error 70 | no-extra-bind: error 71 | no-fallthrough: error 72 | no-floating-decimal: error 73 | no-implied-eval: error 74 | no-iterator: error 75 | no-labels: error 76 | no-lone-blocks: error 77 | no-loop-func: error 78 | no-multi-spaces: error 79 | no-multi-str: error 80 | no-native-reassign: error 81 | no-new: error 82 | no-new-func: error 83 | no-new-wrappers: error 84 | no-octal: error 85 | no-octal-escape: error 86 | no-process-env: off 87 | no-proto: error 88 | no-redeclare: error 89 | no-return-assign: error 90 | no-script-url: off 91 | no-self-compare: error 92 | no-sequences: error 93 | no-unused-expressions: error 94 | no-void: off 95 | no-warning-comments: off 96 | no-with: error 97 | radix: error 98 | vars-on-top: off 99 | wrap-iife: 100 | - error 101 | - any 102 | yoda: 103 | - error 104 | - never 105 | 106 | 107 | ### Variables ### 108 | 109 | no-catch-shadow: off 110 | no-delete-var: error 111 | no-label-var: error 112 | no-shadow: off 113 | no-shadow-restricted-names: error 114 | no-undef: error 115 | no-undef-init: error 116 | no-unused-vars: 117 | - error 118 | - { vars: all, args: after-used } 119 | no-use-before-define: off 120 | prefer-const: error 121 | no-var: error 122 | 123 | 124 | ### Node.js ### 125 | 126 | no-path-concat: off 127 | no-process-exit: off 128 | no-restricted-modules: off 129 | no-sync: off 130 | 131 | 132 | ### Requires ### 133 | 134 | global-require: off 135 | no-new-require: error 136 | no-mixed-requires: error 137 | 138 | ### Stylistic Issues ### 139 | 140 | block-spacing: 141 | - error 142 | - always 143 | brace-style: 144 | - error 145 | - 1tbs 146 | - allowSingleLine: true 147 | camelcase: error 148 | comma-spacing: 149 | - error 150 | - before: false 151 | after: true 152 | comma-style: 153 | - error 154 | - last 155 | consistent-this: 156 | - error 157 | - vm 158 | eol-last: off 159 | func-names: off 160 | func-style: off 161 | key-spacing: 162 | - error 163 | - { beforeColon: false, afterColon: true } 164 | max-len: 165 | - 0 166 | - 120 167 | max-statements: 168 | - 0 169 | - 20 170 | max-lines: off 171 | new-cap: 172 | - error 173 | - { newIsCap: true, capIsNew: false } 174 | new-parens: error 175 | no-array-constructor: error 176 | no-inline-comments: off 177 | no-lonely-if: error 178 | no-mixed-spaces-and-tabs: error 179 | no-multiple-empty-lines: 180 | - error 181 | - max: 1 182 | no-nested-ternary: error 183 | no-new-object: error 184 | no-spaced-func: error 185 | no-ternary: off 186 | no-trailing-spaces: error 187 | no-underscore-dangle: off 188 | one-var: 189 | - error 190 | - never 191 | operator-assignment: off 192 | padded-blocks: 193 | - error 194 | - never 195 | quote-props: 196 | - error 197 | - as-needed 198 | quotes: 199 | - error 200 | - single 201 | - avoid-escape 202 | semi: 203 | - error 204 | - always 205 | sort-vars: off 206 | space-before-blocks: 207 | - error 208 | - always 209 | space-in-parens: 210 | - error 211 | - never 212 | space-infix-ops: error 213 | space-unary-ops: 214 | - error 215 | - { words: true, nonwords: false } 216 | object-curly-spacing: 217 | - error 218 | - always 219 | array-bracket-spacing: 220 | - error 221 | - never 222 | computed-property-spacing: 223 | - error 224 | - never 225 | wrap-regex: off 226 | indent: 227 | - error 228 | - 4 229 | - SwitchCase: 1 230 | semi-spacing: 231 | - error 232 | - { before: false, after: true } 233 | space-before-function-paren: 234 | - error 235 | - { anonymous: always, named: never } 236 | spaced-comment: 237 | - error 238 | - always 239 | - markers: 240 | - global 241 | - globals 242 | - eslint 243 | - eslint-disable 244 | - "*package" 245 | - "!" 246 | - "," 247 | keyword-spacing: error 248 | accessor-pairs: error 249 | arrow-spacing: 250 | - error 251 | - { before: true, after: true } 252 | constructor-super: error 253 | dot-location: 254 | - error 255 | - property 256 | generator-star-spacing: 257 | - error 258 | - { before: true, after: true } 259 | handle-callback-err: 260 | - error 261 | - "^(err|error)$" 262 | no-class-assign: error 263 | no-const-assign: error 264 | no-dupe-args: error 265 | no-dupe-class-members: error 266 | no-duplicate-case: error 267 | no-this-before-super: error 268 | no-throw-literal: error 269 | no-unexpected-multiline: error 270 | no-unneeded-ternary: 271 | - error 272 | - defaultAssignment: false 273 | no-useless-call: error 274 | operator-linebreak: 275 | - error 276 | - after 277 | - overrides: { "?": before, ":": before } 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-modbus-rtu [![Build Status](https://travis-ci.org/thekip/node-modbus-rtu.svg?branch=master)](https://travis-ci.org/thekip/node-modbus-rtu) 2 | Pure NodeJS implementation of ModbusRTU protocol 3 | using [node-serialport](https://github.com/voodootikigod/node-serialport) and [Bluebird promises](https://github.com/petkaantonov/bluebird) 4 | 5 | ## Implementation notes 6 | This library implement ONLY **ModbusRTU Master** and only most important features: 7 | * **03** Read Holding Registers 8 | * **06** Write Single Register 9 | * **16** Write Multiple Registers 10 | 11 | Coil functions (readCoils, writeCoils) is not implemented yet. But you can fork and add this. 12 | 13 | ## Minimal requirements 14 | NodeJS >=5 15 | 16 | if you have older NodeJS version, you should install `modbus-rtu@0.1.*` version 17 | or update NodeJS (the 8.0 version is out, how long you will be use legacy builds? :) ) 18 | 19 | ## Installation 20 | The simplest way, install via npm, type to console: 21 | 22 | `npm i modbus-rtu serialport --save` 23 | 24 | ## Benefits 25 | 1. **Queue**. This is a killer-feature of this library. Behind the scene it use a simple queue. 26 | All request what you do stack to this queue and execute only if previous was finished. 27 | It means that using this library you can write to modbus without waiting a response of previously command. 28 | That make you code much cleaner and decoupled. See examples below. 29 | 30 | 2. **Promises** Promises is a great pattern for the last time. Promises make async code more clean and readable. 31 | All communication functions return promises, so you can easily process data or catch exceptions. 32 | 33 | ## Examples 34 | 35 | ### The basic example 36 | ```js 37 | const SerialPort = require('serialport'); 38 | const ModbusMaster = require('modbus-rtu').ModbusMaster; 39 | 40 | //create serail port with params. Refer to node-serialport for documentation 41 | const serialPort = new SerialPort("/dev/ttyUSB0", { 42 | baudRate: 2400 43 | }); 44 | 45 | //create ModbusMaster instance and pass the serial port object 46 | const master = new ModbusMaster(serialPort); 47 | 48 | //Read from slave with address 1 four holding registers starting from 0. 49 | master.readHoldingRegisters(1, 0, 4).then((data) => { 50 | //promise will be fulfilled with parsed data 51 | console.log(data); //output will be [10, 100, 110, 50] (numbers just for example) 52 | }, (err) => { 53 | //or will be rejected with error 54 | }); 55 | 56 | //Write to first slave into second register value 150. 57 | //slave, register, value 58 | master.writeSingleRegister(1, 2, 150).then(success, error); 59 | ``` 60 | 61 | ### Queueing 62 | 63 | Queue turn this: 64 | 65 | ```js 66 | // requests 67 | master.readHoldingRegisters(1, 0, 4).then((data) => { 68 | console.log(data); 69 | 70 | master.readHoldingRegisters(2, 0, 4).then((data) => { 71 | console.log(data); 72 | 73 | master.readHoldingRegisters(2, 0, 4).then((data) => { 74 | console.log(data); 75 | 76 | master.readHoldingRegisters(2, 0, 4).then((data) => { 77 | console.log(data); 78 | }) 79 | }) 80 | }) 81 | }) 82 | ``` 83 | 84 | Into this: 85 | 86 | ```js 87 | master.readHoldingRegisters(1, 0, 4).then((data) => { 88 | console.log(data); 89 | }); 90 | master.readHoldingRegisters(2, 0, 4).then((data) => { 91 | console.log(data); 92 | }); 93 | master.readHoldingRegisters(3, 0, 4).then((data) => { 94 | console.log(data); 95 | }); 96 | master.readHoldingRegisters(4, 0, 4).then((data) => { 97 | console.log(data); 98 | }); 99 | ``` 100 | 101 | This makes possible to write code in synchronous style. 102 | 103 | Check more examples in `/examples` folder in repository. 104 | 105 | ## The main problem 106 | 107 | Communicating via serial port is sequential. It means you can't write few requests and then read few responses. 108 | 109 | You have to write request then wait response and then writing another request, one by one. 110 | 111 | The first problem is, if we call functions in script in synchronous style (one by one without callbacks), 112 | they will write to port immediately. As result response from slaves will returns unordered and we receive trash. 113 | 114 | To deal with this problem all request instead of directly writing to port are put to the queue, and promise is returned. 115 | 116 | ## API Documentation 117 | 118 | ### new ModbusMaster(serialPort, [options]) 119 | 120 | Constructor of modbus class. 121 | 122 | * **serialPort** - instance of serialPort object 123 | * **options** - object with Modbus options 124 | 125 | **List of options:** 126 | * `responseTimeout`: default `500` 127 | * `debug`: default `false`; enable logging to console. 128 | 129 | Example: 130 | ```js 131 | new ModbusMaster(new SerialPort("/dev/ttyUSB0", { 132 | baudRate: 9600 133 | })) 134 | ``` 135 | 136 | ### master.readHoldingRegisters 137 | ```ts 138 | readHoldingRegisters(slave: int, start: int, length: int, [dataType = DATA_TYPES.INT]): Promise; 139 | ``` 140 | 141 | Modbus function read holding registers. 142 | 143 | Modbus holding register can store only 16-bit data types, 144 | but specification does'nt define exactly what data type can be stored. 145 | 146 | Registers could be combined together to form any of these 32-bit data types: 147 | * A 32-bit unsigned integer (a number between 0 and 4,294,967,295) 148 | * A 32-bit signed integer (a number between -2,147,483,648 and 2,147,483,647) 149 | * A 32-bit single precision IEEE floating point number. 150 | * A four character ASCII string (4 typed letters) 151 | 152 | More registers can be combined to form longer ASCII strings. 153 | Each register being used to store two ASCII characters (two bytes). 154 | 155 | To parse this combined data types, you can get raw buffer in callback and parse it on your own. 156 | 157 | By default bytes treated as **signed integer**. 158 | 159 | **Supported Data Types** 160 | 161 | * `DATA_TYPES.UINT` A 16-bit unsigned integer (a whole number between 0 and 65535) 162 | * `DATA_TYPES.INT` A 16-bit signed integer (a whole number between -32768 and 32767) 163 | * `DATA_TYPES.ASCII` A two character ASCII string (2 typed letters) 164 | 165 | **List of function arguments:** 166 | 167 | * **slave** - slave address (1..247) 168 | * **start** - start register for reading 169 | * **length** - how many registers to read 170 | * **dataType** - dataType or function. If function is provided, this will be used for parsing raw buffer. dataType is one of `DATA_TYPES` 171 | 172 | **Returns Promise** which will be fulfilled with array of data 173 | 174 | Example: 175 | ```js 176 | const {ModbusMaster, DATA_TYPES} = require('modbus-rtu'); 177 | 178 | const master = new ModbusMaster(serialPort); 179 | 180 | master.readHoldingRegisters(1, 0, 4).then((data) => { 181 | //promise will be fulfilled with parsed data 182 | console.log(data); //output will be [-10, 100, 110, 50] (numbers just for example) 183 | }, (err) => { 184 | //or will be rejected with error 185 | //for example timeout error or crc. 186 | }); 187 | 188 | master.readHoldingRegisters(1, 0, 4, DATA_TYPES.UINT).then((data) => { 189 | // data will be treat as unsigned integer 190 | console.log(data); //output will be [20, 100, 110, 50] (numbers just for example) 191 | }); 192 | 193 | master.readHoldingRegisters(1, 0, 2, (rawBuffer) => { 194 | //buffer here contains only data without pdu header and crc 195 | return rawBuffer.readUInt32BE(0); 196 | }).then((bigNumber) => { 197 | //promise will be fullfilled with result of callback 198 | console.log(bigNumber); //2923517522 199 | }); 200 | ``` 201 | 202 | ### master.writeSingleRegister 203 | ```ts 204 | writeSingleRegister(slave: int, register: int, value: int, [retryCount=10]) -> Promise 205 | ``` 206 | 207 | Modbus function write single register. 208 | If fails will be repeated `retryCount` times. 209 | 210 | * **slave** - slave address (1..247) 211 | * **register** - register number for write 212 | * **value** - int value 213 | * **retryCount** - int count of attempts. Set 1, if you don't want to retry request on fail. 214 | 215 | **Returns Promise** 216 | 217 | Example: 218 | ```js 219 | const master = new ModbusMaster(serialPort); 220 | master.writeSingleRegister(1, 2, 150); 221 | ``` 222 | 223 | ### master.writeMultipleRegisters 224 | ```ts 225 | writeMultipleRegisters(slave: int, start: int, array[int]) -> Promise 226 | ``` 227 | 228 | Modbus function write multiple registers. 229 | 230 | You can set starting register and data array. Register from `start` to `array.length` will be filled with array data 231 | 232 | * **slave** - slave address (1..247) 233 | * **start** - starting register number for write 234 | * **array** - array of values 235 | 236 | **Returns promise** 237 | 238 | Example: 239 | ```js 240 | new ModbusMaster(serialPort, (master) => { 241 | master.writeMultipleRegisters(1, 2, [150, 100, 20]); 242 | }) 243 | ``` 244 | 245 | ## Testing 246 | To run test, type to console: 247 | 248 | `npm test` 249 | 250 | Or run manually entire test (by executing test file via node). 251 | 252 | Please feel free to create PR with you tests. 253 | 254 | 255 | ## Roadmap 256 | 1. Add rest modbus functions 257 | --------------------------------------------------------------------------------