From a1b3eb95cd33ffd2f800008447affe312551dedf Mon Sep 17 00:00:00 2001 From: Dru Steeby Date: Thu, 20 Nov 2025 11:49:32 -0500 Subject: [PATCH 1/3] FC23 implementation without prettier formatting --- ModbusRTU.d.ts | 2 +- apis/promise.js | 107 ++++++++++++++++++++++++------------------------ index.js | 80 ++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 54 deletions(-) diff --git a/ModbusRTU.d.ts b/ModbusRTU.d.ts index 247cc2f..8b60bc1 100644 --- a/ModbusRTU.d.ts +++ b/ModbusRTU.d.ts @@ -22,7 +22,7 @@ export class ModbusRTU { writeFC15(address: number, dataAddress: number, states: Array, next: NodeStyleCallback): void; writeFC16(address: number, dataAddress: number, values: Array, next: NodeStyleCallback): void; writeFC22(address: number, dataAddress: number, andMask: number, orMask: number, next: NodeStyleCallback): void; - + writeFC23(address: number, startingReadAddress: number, numReadRegisters: number, startingWriteAddress: number, numWriteRegisters: number, valuesToWrite: Array | Buffer, next: NodeStyleCallback): void; writeCustomFC(address: number, functionCode: number, data: Array, next: NodeStyleCallback): void; // Connection shorthand API diff --git a/apis/promise.js b/apis/promise.js index ce01ece..faa111a 100644 --- a/apis/promise.js +++ b/apis/promise.js @@ -24,47 +24,47 @@ */ const _convert = function(f) { const converted = function(...args) { - const client = this; - const id = this._unitID; + const client = this; + const id = this._unitID; - // The last argument might be the callback (next) - const next = args[args.length - 1]; + // The last argument might be the callback (next) + const next = args[args.length - 1]; - // Determine if the last argument is actually a callback + // Determine if the last argument is actually a callback const hasCallback = typeof next === "function"; - if (hasCallback) { - // If there is a callback, call the function with the appropriate arguments - if (args.length === 1) { - // This case is used for client close method - f.bind(client)(next); - } else { - // This case is used for client writeFC methods - f.bind(client)(id, ...args); - } - } else { - // Otherwise, use a promise + if (hasCallback) { + // If there is a callback, call the function with the appropriate arguments + if (args.length === 1) { + // This case is used for client close method + f.bind(client)(next); + } else { + // This case is used for client writeFC methods + f.bind(client)(id, ...args); + } + } else { + // Otherwise, use a promise return new Promise(function(resolve, reject) { - function cb(err, data) { - if (err) { - reject(err); - } else { - resolve(data); - } - } + function cb(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + } - if (args.length === 0) { - // This case is used for client close method - f.bind(client)(cb); - } else { - // This case is used for client writeFC methods - f.bind(client)(id, ...args, cb); - } - }); + if (args.length === 0) { + // This case is used for client close method + f.bind(client)(cb); + } else { + // This case is used for client writeFC methods + f.bind(client)(id, ...args, cb); } - }; + }); + } + }; - return converted; + return converted; }; /** @@ -74,33 +74,34 @@ const _convert = function(f) { */ const addPromiseAPI = function(Modbus) { - const cl = Modbus.prototype; + const cl = Modbus.prototype; - // set/get unitID + // set/get unitID cl.setID = function(id) {this._unitID = Number(id);}; cl.getID = function() {return this._unitID;}; - // set/get timeout + // set/get timeout cl.setTimeout = function(timeout) {this._timeout = timeout;}; cl.getTimeout = function() {return this._timeout;}; - // convert functions to return promises - cl.close = _convert(cl.close); - cl.readCoils = _convert(cl.writeFC1); - cl.readDiscreteInputs = _convert(cl.writeFC2); - cl.readHoldingRegisters = _convert(cl.writeFC3); - cl.readRegistersEnron = _convert(cl.writeFC3); - cl.readInputRegisters = _convert(cl.writeFC4); - cl.writeCoil = _convert(cl.writeFC5); - cl.writeRegister = _convert(cl.writeFC6); - cl.writeRegisterEnron = _convert(cl.writeFC6); - cl.writeCoils = _convert(cl.writeFC15); - cl.writeRegisters = _convert(cl.writeFC16); - cl.reportServerID = _convert(cl.writeFC17); - cl.readFileRecords = _convert(cl.writeFC20); - cl.maskWriteRegister = _convert(cl.writeFC22); - cl.readDeviceIdentification = _convert(cl.writeFC43); - cl.customFunction = _convert(cl.writeCustomFC); + // convert functions to return promises + cl.close = _convert(cl.close); + cl.readCoils = _convert(cl.writeFC1); + cl.readDiscreteInputs = _convert(cl.writeFC2); + cl.readHoldingRegisters = _convert(cl.writeFC3); + cl.readRegistersEnron = _convert(cl.writeFC3); + cl.readInputRegisters = _convert(cl.writeFC4); + cl.writeCoil = _convert(cl.writeFC5); + cl.writeRegister = _convert(cl.writeFC6); + cl.writeRegisterEnron = _convert(cl.writeFC6); + cl.writeCoils = _convert(cl.writeFC15); + cl.writeRegisters = _convert(cl.writeFC16); + cl.reportServerID = _convert(cl.writeFC17); + cl.readFileRecords = _convert(cl.writeFC20); + cl.maskWriteRegister = _convert(cl.writeFC22); + cl.readWriteRegisters = _convert(cl.writeFC23); + cl.readDeviceIdentification = _convert(cl.writeFC43); + cl.customFunction = _convert(cl.writeCustomFC); }; /** diff --git a/index.js b/index.js index 35273c7..0ffb704 100644 --- a/index.js +++ b/index.js @@ -265,6 +265,25 @@ function _readFC22(data, next) { next(null, { "address": dataAddress, "andMask": andMask, "orMask": orMask }); } + /** + * Parse the data for a Modbus - + * Read Write Multiple Registers (FC=23) + * + * @param {Buffer} data the data buffer to parse. + * @param {Function} next the function to call next. + */ + function _readFC23(data, next) { + const bytes = data.readInt8(2); + const values = []; + + for (let i = 0; i < bytes; i += 2) { + const reg = data.readUInt16BE(3 + i); + values.push(reg); + } + + if (next) next(null, { data: values }); + } + /** * Parse the data for a Modbus - * Read Device Identification (FC=43) @@ -580,6 +599,9 @@ function _onReceive(data) { case 22: _readFC22(data, next); break; + case 23: + _readFC23(data, next); + break; case 43: // read device identification _readFC43(data, modbus, next); @@ -1221,6 +1243,64 @@ class ModbusRTU extends EventEmitter { _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); } + writeFC23(address, startingReadAddress, numReadRegisters, startingWriteAddress, + numWriteRegisters, valuesToWrite, next) { + if (this.isOpen !== true) { + if (next) next(new PortNotOpenError()); + return; + } + + if (typeof address === "undefined" || typeof startingReadAddress === "undefined" || typeof startingWriteAddress === "undefined") { + if (next) next(new BadAddressError()); + return; + } + + if (!Array.isArray(valuesToWrite) && !Buffer.isBuffer(valuesToWrite)) { + if (next) + next(new Error("Parameter valuesToWrite must be an array or buffer")); + return; + } + + const code = 23; + + // Calculate byte count for write data (should be numWriteRegisters * 2) + const writeByteCount = numWriteRegisters * 2; + + // Expected response length: address(1) + code(1) + byteCount(1) + data(numReadRegisters*2) + crc(2) + const responseLength = 3 + numReadRegisters * 2 + 2; + + this._transactions[this._port._transactionIdWrite] = { + nextAddress: address, + nextCode: code, + nextLength: responseLength, + next: next, + }; + + // Request: address(1) + code(1) + readAddr(2) + readQty(2) + writeAddr(2) + writeQty(2) + byteCount(1) + writeData(N*2) + crc(2) + const codeLength = 11 + writeByteCount; + const buf = Buffer.alloc(codeLength + 2); // add 2 crc bytes + + buf.writeUInt8(address, 0); + buf.writeUInt8(code, 1); + buf.writeUInt16BE(startingReadAddress, 2); + buf.writeUInt16BE(numReadRegisters, 4); + buf.writeUInt16BE(startingWriteAddress, 6); + buf.writeUInt16BE(numWriteRegisters, 8); + buf.writeUInt8(writeByteCount, 10); + + // Write the values to the buffer + if (Buffer.isBuffer(valuesToWrite)) { + valuesToWrite.copy(buf, 11); + } else { + for (let i = 0; i < numWriteRegisters; i++) { + buf.writeUInt16BE(valuesToWrite[i], 11 + 2 * i); + } + } + + buf.writeUInt16LE(crc16(buf.slice(0, -2)), codeLength); + _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); + } + /** * Write a Modbus "Custom Function Code" (FC=65-72, 100-110) to serial port. * From 3b4ec1c4f3364fd57398ce890ead070c99eda925 Mon Sep 17 00:00:00 2001 From: Dru Steeby Date: Thu, 20 Nov 2025 11:51:49 -0500 Subject: [PATCH 2/3] revert prettier --- apis/promise.js | 108 ++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/apis/promise.js b/apis/promise.js index faa111a..d4fab29 100644 --- a/apis/promise.js +++ b/apis/promise.js @@ -24,47 +24,47 @@ */ const _convert = function(f) { const converted = function(...args) { - const client = this; - const id = this._unitID; + const client = this; + const id = this._unitID; - // The last argument might be the callback (next) - const next = args[args.length - 1]; + // The last argument might be the callback (next) + const next = args[args.length - 1]; - // Determine if the last argument is actually a callback + // Determine if the last argument is actually a callback const hasCallback = typeof next === "function"; - if (hasCallback) { - // If there is a callback, call the function with the appropriate arguments - if (args.length === 1) { - // This case is used for client close method - f.bind(client)(next); - } else { - // This case is used for client writeFC methods - f.bind(client)(id, ...args); - } - } else { - // Otherwise, use a promise + if (hasCallback) { + // If there is a callback, call the function with the appropriate arguments + if (args.length === 1) { + // This case is used for client close method + f.bind(client)(next); + } else { + // This case is used for client writeFC methods + f.bind(client)(id, ...args); + } + } else { + // Otherwise, use a promise return new Promise(function(resolve, reject) { - function cb(err, data) { - if (err) { - reject(err); - } else { - resolve(data); - } - } + function cb(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + } - if (args.length === 0) { - // This case is used for client close method - f.bind(client)(cb); - } else { - // This case is used for client writeFC methods - f.bind(client)(id, ...args, cb); + if (args.length === 0) { + // This case is used for client close method + f.bind(client)(cb); + } else { + // This case is used for client writeFC methods + f.bind(client)(id, ...args, cb); + } + }); } - }); - } - }; + }; - return converted; + return converted; }; /** @@ -74,34 +74,34 @@ const _convert = function(f) { */ const addPromiseAPI = function(Modbus) { - const cl = Modbus.prototype; + const cl = Modbus.prototype; - // set/get unitID + // set/get unitID cl.setID = function(id) {this._unitID = Number(id);}; cl.getID = function() {return this._unitID;}; - // set/get timeout + // set/get timeout cl.setTimeout = function(timeout) {this._timeout = timeout;}; cl.getTimeout = function() {return this._timeout;}; - // convert functions to return promises - cl.close = _convert(cl.close); - cl.readCoils = _convert(cl.writeFC1); - cl.readDiscreteInputs = _convert(cl.writeFC2); - cl.readHoldingRegisters = _convert(cl.writeFC3); - cl.readRegistersEnron = _convert(cl.writeFC3); - cl.readInputRegisters = _convert(cl.writeFC4); - cl.writeCoil = _convert(cl.writeFC5); - cl.writeRegister = _convert(cl.writeFC6); - cl.writeRegisterEnron = _convert(cl.writeFC6); - cl.writeCoils = _convert(cl.writeFC15); - cl.writeRegisters = _convert(cl.writeFC16); - cl.reportServerID = _convert(cl.writeFC17); - cl.readFileRecords = _convert(cl.writeFC20); - cl.maskWriteRegister = _convert(cl.writeFC22); - cl.readWriteRegisters = _convert(cl.writeFC23); - cl.readDeviceIdentification = _convert(cl.writeFC43); - cl.customFunction = _convert(cl.writeCustomFC); + // convert functions to return promises + cl.close = _convert(cl.close); + cl.readCoils = _convert(cl.writeFC1); + cl.readDiscreteInputs = _convert(cl.writeFC2); + cl.readHoldingRegisters = _convert(cl.writeFC3); + cl.readRegistersEnron = _convert(cl.writeFC3); + cl.readInputRegisters = _convert(cl.writeFC4); + cl.writeCoil = _convert(cl.writeFC5); + cl.writeRegister = _convert(cl.writeFC6); + cl.writeRegisterEnron = _convert(cl.writeFC6); + cl.writeCoils = _convert(cl.writeFC15); + cl.writeRegisters = _convert(cl.writeFC16); + cl.reportServerID = _convert(cl.writeFC17); + cl.readFileRecords = _convert(cl.writeFC20); + cl.maskWriteRegister = _convert(cl.writeFC22); + cl.readWriteRegisters = _convert(cl.writeFC23); + cl.readDeviceIdentification = _convert(cl.writeFC43); + cl.customFunction = _convert(cl.writeCustomFC); }; /** From 340e9de9cccc620f3fdf738e3e7b856cb083eccc Mon Sep 17 00:00:00 2001 From: Dru Steeby Date: Thu, 20 Nov 2025 11:55:36 -0500 Subject: [PATCH 3/3] add param comments --- index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 0ffb704..74a58cf 100644 --- a/index.js +++ b/index.js @@ -1243,6 +1243,17 @@ class ModbusRTU extends EventEmitter { _writeBufferToPort.call(this, buf, this._port._transactionIdWrite); } + /** + * Write a Modbus "Read/Write Multiple Registers" (FC=23) to serial port. + * + * @param {number} address the slave unit address. + * @param {number} startingReadAddress the Data Address of the first register to read. + * @param {number} numReadRegisters the number of registers to read. + * @param {number} startingWriteAddress the Data Address of the first register to write. + * @param {number} numWriteRegisters the number of registers to write. + * @param {Array|Buffer} valuesToWrite the array of values or buffer to write to registers. + * @param {Function} next the function to call next. + */ writeFC23(address, startingReadAddress, numReadRegisters, startingWriteAddress, numWriteRegisters, valuesToWrite, next) { if (this.isOpen !== true) { @@ -1288,7 +1299,6 @@ class ModbusRTU extends EventEmitter { buf.writeUInt16BE(numWriteRegisters, 8); buf.writeUInt8(writeByteCount, 10); - // Write the values to the buffer if (Buffer.isBuffer(valuesToWrite)) { valuesToWrite.copy(buf, 11); } else {