1 Commits

Author SHA1 Message Date
google-labs-jules[bot]
8ba43c2ba4 feat: Add Modbus RTU write operations and update documentation
This commit introduces methods for writing to Modbus RTU devices:
- writeSingleCoil (FC05)
- writeSingleRegister (FC06)
- writeMultipleCoils (FC15)
- writeMultipleRegisters (FC16)

Key changes include:
- Implementation of the four write functions in `src/modbus-rtu-master.js`.
- Addition of helper methods for building Modbus request frames for these operations.
- Refinements to response handling (`_receiveResponse`) for increased robustness and to support various response types.
- Introduction of `ModbusRTUMaster.FUNCTION_CODES` for better code clarity and maintainability.
- Input validation for addresses, values, and quantities according to Modbus PDU limits.
- Thorough response validation for write operations to ensure commands were processed correctly by the slave device.
- Updated `README.md` with detailed documentation and usage examples for the new write methods.
- Enhanced `example/index.html` with UI elements and JavaScript logic to demonstrate and manually test all new write operations.

The changes also incorporate improvements based on an initial code review, such as the use of named constants and more structured request/response handling.
2025-05-26 10:01:53 +00:00
3 changed files with 875 additions and 79 deletions

127
README.md
View File

@@ -5,8 +5,131 @@
Implementation of a Modbus RTU master device in JavaScript, usable in web applications in Chrome and Firefox browsers.
The module was primarily written for web applications used by my beloved wife and serves for commissioning and testing devices at our workplace. It supports reading and writing from all Modbus areas, but only input registers and holding registers are tested/actively used. The repository also includes a simple usage example that allows setting communication parameters and periodically reads one register every 5 seconds, displaying it on the web page (please excuse the Czech comments in the source code).
The module was primarily written for web applications used by my beloved wife and serves for commissioning and testing devices at our workplace. The library implements all standard Modbus read functions (Read Coils, Read Discrete Inputs, Read Holding Registers, Read Input Registers) and common write functions (Write Single Coil, Write Single Register, Write Multiple Coils, Write Multiple Registers). While all functions aim for specification compliance, active testing has been primarily focused on holding and input registers for reads, and now on coils and holding registers for writes.
# Usage in Firefox Browser
The repository also includes a simple usage example that allows setting communication parameters and periodically reads one register every 5 seconds, displaying it on the web page (please excuse the Czech comments in the source code).
## Supported Operations
This section details the Modbus functions implemented by the library. All methods are asynchronous and return a Promise.
### Read Operations
These methods allow reading data from a Modbus slave device.
* **`readCoils(slaveId, startAddress, quantity)`**: Reads a sequence of coils (digital ON/OFF states).
* **`readDiscreteInputs(slaveId, startAddress, quantity)`**: Reads a sequence of discrete inputs (digital ON/OFF states, typically read-only).
* **`readHoldingRegisters(slaveId, startAddress, quantity)`**: Reads a sequence of holding registers (16-bit analog/data values).
* **`readInputRegisters(slaveId, startAddress, quantity)`**: Reads a sequence of input registers (16-bit analog/data values, typically read-only).
For detailed parameters and return values (typically `Promise<Array<number|boolean>>`), please refer to the JSDoc comments within the source code.
### Write Operations
These methods allow writing data to a Modbus slave device.
#### `writeSingleCoil(slaveId, address, value)`
* **Description**: Writes a single coil (digital ON/OFF state) to a Modbus slave device.
* **Parameters**:
* `slaveId (number)`: The Modbus slave ID (typically 1-247).
* `address (number)`: The coil address to write to (0x0000 - 0xFFFF).
* `value (boolean)`: The value to write. `true` for ON (writes 0xFF00), `false` for OFF (writes 0x0000).
* **Returns**: `Promise<boolean>` - Resolves to `true` if the operation was successful and validated by the slave. Rejects with an error on failure (e.g., Modbus exception, CRC error, timeout, response mismatch).
* **JavaScript Usage Example**:
```javascript
async function exampleWriteSingleCoil(modbusMaster, slaveId, coilAddress, coilValue) {
try {
const success = await modbusMaster.writeSingleCoil(slaveId, coilAddress, coilValue);
if (success) {
console.log(`Successfully wrote coil at address ${coilAddress} to ${coilValue ? 'ON' : 'OFF'}.`);
}
} catch (error) {
console.error(`Failed to write single coil at address ${coilAddress}:`, error);
}
}
// await exampleWriteSingleCoil(modbusMaster, 1, 100, true);
```
#### `writeSingleRegister(slaveId, address, value)`
* **Description**: Writes a single holding register (16-bit value) to a Modbus slave device.
* **Parameters**:
* `slaveId (number)`: The Modbus slave ID (typically 1-247).
* `address (number)`: The register address to write to (0x0000 - 0xFFFF).
* `value (number)`: The 16-bit integer value to write (0 - 65535).
* **Returns**: `Promise<boolean>` - Resolves to `true` if the operation was successful and validated by the slave. Rejects with an error on failure.
* **JavaScript Usage Example**:
```javascript
async function exampleWriteSingleRegister(modbusMaster, slaveId, regAddress, regValue) {
try {
const success = await modbusMaster.writeSingleRegister(slaveId, regAddress, regValue);
if (success) {
console.log(`Successfully wrote register at address ${regAddress} with value ${regValue}.`);
}
} catch (error) {
console.error(`Failed to write single register at address ${regAddress}:`, error);
}
}
// await exampleWriteSingleRegister(modbusMaster, 1, 200, 12345);
```
#### `writeMultipleCoils(slaveId, address, values)`
* **Description**: Writes a sequence of coils (digital ON/OFF states) to a Modbus slave device.
* **Parameters**:
* `slaveId (number)`: The Modbus slave ID (typically 1-247).
* `address (number)`: The starting address of the coils to write (0x0000 - 0xFFFF).
* `values (Array<boolean|number>)`: An array of boolean or numeric (0 or 1) values to write. The number of coils (quantity) must be between 1 and 1968.
* **Returns**: `Promise<boolean>` - Resolves to `true` if the operation was successful and validated by the slave. Rejects with an error on failure.
* **JavaScript Usage Example**:
```javascript
async function exampleWriteMultipleCoils(modbusMaster, slaveId, startAddress, coilValues) {
try {
const success = await modbusMaster.writeMultipleCoils(slaveId, startAddress, coilValues);
if (success) {
console.log(`Successfully wrote ${coilValues.length} coils starting at address ${startAddress}.`);
}
} catch (error) {
console.error(`Failed to write multiple coils starting at address ${startAddress}:`, error);
}
}
// await exampleWriteMultipleCoils(modbusMaster, 1, 300, [true, false, true, true, false]);
```
#### `writeMultipleRegisters(slaveId, address, values)`
* **Description**: Writes a sequence of holding registers (16-bit values) to a Modbus slave device.
* **Parameters**:
* `slaveId (number)`: The Modbus slave ID (typically 1-247).
* `address (number)`: The starting address of the registers to write (0x0000 - 0xFFFF).
* `values (Array<number>)`: An array of 16-bit integer values (0 - 65535) to write. The number of registers (quantity) must be between 1 and 123.
* **Returns**: `Promise<boolean>` - Resolves to `true` if the operation was successful and validated by the slave. Rejects with an error on failure.
* **JavaScript Usage Example**:
```javascript
async function exampleWriteMultipleRegisters(modbusMaster, slaveId, startAddress, registerValues) {
try {
const success = await modbusMaster.writeMultipleRegisters(slaveId, startAddress, registerValues);
if (success) {
console.log(`Successfully wrote ${registerValues.length} registers starting at address ${startAddress}.`);
}
} catch (error) {
console.error(`Failed to write multiple registers starting at address ${startAddress}:`, error);
}
}
// await exampleWriteMultipleRegisters(modbusMaster, 1, 400, [100, 200, 300, 400, 500]);
```
### Developer Notes
#### Function Codes
The `ModbusRTUMaster` class exposes a static property `ModbusRTUMaster.FUNCTION_CODES`. This object maps human-readable Modbus function names (e.g., `READ_COILS`, `WRITE_SINGLE_REGISTER`) to their respective numerical function codes (e.g., `0x01`, `0x06`). This can be useful for debugging, understanding the underlying protocol, or potentially extending the library with less common Modbus functions.
Example:
```javascript
console.log(ModbusRTUMaster.FUNCTION_CODES.WRITE_MULTIPLE_COILS); // Outputs: 15 (0x0F)
```
## Usage in Firefox Browser
Unfortunately, Firefox does not natively support the WebSerial API, but luckily, you can use this plugin https://addons.mozilla.org/en-US/firefox/addon/webserial-for-firefox/ to add support. However, uploading via file:// does not work, because of Firefox policy, so you need to run practically any web server locally, for example, in Python using `python -m http.server`. The web application will then be available at http://localhost:8000, and WebSerial will function normally.

View File

@@ -11,25 +11,46 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
/* justify-content: center; // Adjusted for longer content */
/* height: 100vh; // Adjusted for longer content */
margin-bottom: 50px; /* Add some bottom margin */
}
#register-value {
font-size: 2em;
margin-top: 20px;
}
#error-message {
color: red;
#error-message, #write-status-message {
margin-top: 10px;
min-height: 20px; /* Ensure space even if empty */
}
button, input, select {
padding: 10px;
margin: 5px;
font-size: 1em;
#error-message { color: red; }
#write-status-message { color: green; }
button, input, select, label {
padding: 8px; /* Slightly reduced padding */
margin: 4px; /* Slightly reduced margin */
font-size: 0.9em; /* Slightly reduced font size */
cursor: pointer;
}
label {
margin-top: 10px;
margin-top: 8px;
display: inline-block; /* Keep labels aligned */
}
fieldset {
margin-top: 20px;
padding: 15px;
border: 1px solid #ccc;
width: auto; /* Adjust width as needed */
max-width: 500px; /* Max width for fieldsets */
}
legend {
font-weight: bold;
font-size: 1.1em;
}
.input-group label {
min-width: 120px; /* Align labels */
}
.input-group input[type="text"], .input-group input[type="number"] {
width: 250px; /* Adjust width of text/number inputs */
}
</style>
</head>
@@ -58,8 +79,80 @@
<input type="number" id="timeout" value="300">
</div>
<button id="connect-btn">Připojit k sériovému portu</button>
<div id="register-value">Hodnota registru: <span id="value">N/A</span></div>
<div id="register-value">Hodnota registru (ID:25, Addr:0, Len:3): <span id="value">N/A</span></div>
<div id="error-message"></div>
<h2>Write Operations</h2>
<div id="write-status-message"></div>
<fieldset>
<legend>Write Single Coil (FC05)</legend>
<div class="input-group">
<label for="scSlaveId">Slave ID:</label>
<input type="number" id="scSlaveId" value="25">
</div>
<div class="input-group">
<label for="scAddress">Coil Address:</label>
<input type="number" id="scAddress" value="0">
</div>
<div class="input-group">
<label for="scValue">Value (ON/OFF):</label>
<input type="checkbox" id="scValue" checked> (Checked = ON)
</div>
<button id="btnWriteSingleCoil">Write Single Coil</button>
</fieldset>
<fieldset>
<legend>Write Single Register (FC06)</legend>
<div class="input-group">
<label for="srSlaveId">Slave ID:</label>
<input type="number" id="srSlaveId" value="25">
</div>
<div class="input-group">
<label for="srAddress">Register Address:</label>
<input type="number" id="srAddress" value="1">
</div>
<div class="input-group">
<label for="srValue">Value (0-65535):</label>
<input type="number" id="srValue" value="12345" min="0" max="65535">
</div>
<button id="btnWriteSingleRegister">Write Single Register</button>
</fieldset>
<fieldset>
<legend>Write Multiple Coils (FC15)</legend>
<div class="input-group">
<label for="mcSlaveId">Slave ID:</label>
<input type="number" id="mcSlaveId" value="25">
</div>
<div class="input-group">
<label for="mcAddress">Start Address:</label>
<input type="number" id="mcAddress" value="10">
</div>
<div class="input-group">
<label for="mcValues">Values (comma-sep 0/1):</label>
<input type="text" id="mcValues" value="1,0,1,1,0">
</div>
<button id="btnWriteMultipleCoils">Write Multiple Coils</button>
</fieldset>
<fieldset>
<legend>Write Multiple Registers (FC16)</legend>
<div class="input-group">
<label for="mrSlaveId">Slave ID:</label>
<input type="number" id="mrSlaveId" value="25">
</div>
<div class="input-group">
<label for="mrAddress">Start Address:</label>
<input type="number" id="mrAddress" value="20">
</div>
<div class="input-group">
<label for="mrValues">Values (comma-sep 0-65535):</label>
<input type="text" id="mrValues" value="100,200,300,400">
</div>
<button id="btnWriteMultipleRegisters">Write Multiple Registers</button>
</fieldset>
<script>
window.LOG_LEVEL = 'VERBOSE';
window.ENABLED_MODULES = ['*'];
@@ -71,6 +164,7 @@
const connectButton = document.getElementById('connect-btn');
const valueDisplay = document.getElementById('value');
const errorDisplay = document.getElementById('error-message');
const writeStatusDisplay = document.getElementById('write-status-message');
let intervalId = null;
// Funkce pro získání hodnot z formuláře
@@ -86,37 +180,198 @@
// Funkce pro aktualizaci hodnoty registru každých 5 sekund
async function updateRegisterValue() {
if (!modbus || !modbus.port) { // Ensure modbus is connected
TRACE_VERBOSE("Read", "Modbus not connected, skipping read.");
return;
}
try {
// Čtení registru s adresou 0x0000 z zařízení s ID 0x19
// Čtení registru s adresou 0x0000 z zařízení s ID 0x19 (dec 25)
const values = await modbus.readInputRegisters(0x19, 0x0000, 3);
if (values && values.length > 0) {
// Zobrazí hodnotu prvního (a jediného) registru
const registerValue = values[0];
valueDisplay.textContent = registerValue;
valueDisplay.textContent = values.join(', '); // Display all read values
errorDisplay.textContent = ''; // Vymazat případné staré chyby
} else {
TRACE_ERROR("", 'Neplatná odpověď:', values);
valueDisplay.textContent = 'Error';
errorDisplay.textContent = 'Invalid response received.';
TRACE_ERROR("Read", 'Neplatná odpověď:', values);
valueDisplay.textContent = 'Error/No Values';
errorDisplay.textContent = 'Invalid response or no values received.';
}
} catch (error) {
TRACE_ERROR("", 'Chyba při čtení registru:', error);
TRACE_ERROR("Read", 'Chyba při čtení registru:', error);
valueDisplay.textContent = 'Error';
errorDisplay.textContent = error.message || 'Unknown error occurred.';
errorDisplay.textContent = error.message || 'Unknown error occurred reading register.';
}
}
// Připojení k sériovému portu a spuštění čtení
connectButton.addEventListener('click', async () => {
const config = getConfig();
modbus = new ModbusRTUMaster(config);
await modbus.connect();
if (intervalId === null) {
intervalId = setInterval(updateRegisterValue, 5000); // Každých 5 sekund
modbus = new ModbusRTUMaster(config); // Initialize or re-initialize
try {
await modbus.connect();
errorDisplay.textContent = 'Připojeno k portu.'; // Connected to port.
connectButton.textContent = 'Odpojit od sériového portu'; // Disconnect
if (intervalId === null) {
updateRegisterValue(); // Initial read
intervalId = setInterval(updateRegisterValue, 5000); // Každých 5 sekund
}
} catch (err) {
errorDisplay.textContent = 'Nepodařilo se připojit: ' + err.message; // Failed to connect
if (modbus) await modbus.disconnect(); // Clean up
modbus = null;
}
});
TRACE_INFO("", "Starting...");
connectButton.addEventListener('click', async () => { // Re-binding to handle disconnect logic as well
if (modbus && modbus.port) { // If connected, then disconnect
clearInterval(intervalId);
intervalId = null;
await modbus.disconnect();
modbus = null;
connectButton.textContent = 'Připojit k sériovému portu'; // Connect
errorDisplay.textContent = 'Odpojeno.'; // Disconnected
valueDisplay.textContent = 'N/A';
} else { // If not connected, then connect
const config = getConfig();
modbus = new ModbusRTUMaster(config);
try {
await modbus.connect();
errorDisplay.textContent = 'Připojeno k portu.';
connectButton.textContent = 'Odpojit od sériového portu';
updateRegisterValue(); // Initial read
intervalId = setInterval(updateRegisterValue, 5000);
} catch (err) {
errorDisplay.textContent = 'Nepodařilo se připojit: ' + err.message;
if (modbus) await modbus.disconnect();
modbus = null;
}
}
});
// --- Write Single Coil (FC05) ---
document.getElementById('btnWriteSingleCoil').addEventListener('click', async () => {
if (!modbus || !modbus.port) {
writeStatusDisplay.textContent = 'Error: Modbus not connected.';
return;
}
try {
const slaveId = parseInt(document.getElementById('scSlaveId').value);
const address = parseInt(document.getElementById('scAddress').value);
const value = document.getElementById('scValue').checked;
writeStatusDisplay.textContent = 'Writing Single Coil...';
const success = await modbus.writeSingleCoil(slaveId, address, value);
if (success) {
writeStatusDisplay.textContent = `Success: Single Coil written at address ${address} to ${value ? 'ON' : 'OFF'}.`;
} else { // Should not happen if promise resolves, error should be thrown
writeStatusDisplay.textContent = 'Failed: Write Single Coil returned false (unexpected).';
}
} catch (error) {
TRACE_ERROR("WriteSingleCoil", "Error:", error);
writeStatusDisplay.textContent = 'Error writing Single Coil: ' + error.message;
}
});
// --- Write Single Register (FC06) ---
document.getElementById('btnWriteSingleRegister').addEventListener('click', async () => {
if (!modbus || !modbus.port) {
writeStatusDisplay.textContent = 'Error: Modbus not connected.';
return;
}
try {
const slaveId = parseInt(document.getElementById('srSlaveId').value);
const address = parseInt(document.getElementById('srAddress').value);
const value = parseInt(document.getElementById('srValue').value);
if (isNaN(value) || value < 0 || value > 65535) {
writeStatusDisplay.textContent = 'Error: Invalid register value. Must be 0-65535.';
return;
}
writeStatusDisplay.textContent = 'Writing Single Register...';
const success = await modbus.writeSingleRegister(slaveId, address, value);
if (success) {
writeStatusDisplay.textContent = `Success: Single Register written at address ${address} with value ${value}.`;
}
} catch (error) {
TRACE_ERROR("WriteSingleRegister", "Error:", error);
writeStatusDisplay.textContent = 'Error writing Single Register: ' + error.message;
}
});
// --- Write Multiple Coils (FC15) ---
document.getElementById('btnWriteMultipleCoils').addEventListener('click', async () => {
if (!modbus || !modbus.port) {
writeStatusDisplay.textContent = 'Error: Modbus not connected.';
return;
}
try {
const slaveId = parseInt(document.getElementById('mcSlaveId').value);
const address = parseInt(document.getElementById('mcAddress').value);
const valuesStr = document.getElementById('mcValues').value;
const valuesArray = valuesStr.split(',').map(v => parseInt(v.trim())).filter(v => !isNaN(v) && (v === 0 || v === 1));
if (valuesArray.length === 0 && valuesStr.trim() !== "") {
writeStatusDisplay.textContent = 'Error: Invalid coil values. Must be comma-separated 0s or 1s.';
return;
}
if (valuesArray.length === 0 && valuesStr.trim() === "") {
writeStatusDisplay.textContent = 'Error: Coil values cannot be empty.';
return;
}
writeStatusDisplay.textContent = 'Writing Multiple Coils...';
const success = await modbus.writeMultipleCoils(slaveId, address, valuesArray.map(v => v === 1)); // Convert to boolean array
if (success) {
writeStatusDisplay.textContent = `Success: ${valuesArray.length} Coils written starting at address ${address}.`;
}
} catch (error) {
TRACE_ERROR("WriteMultipleCoils", "Error:", error);
writeStatusDisplay.textContent = 'Error writing Multiple Coils: ' + error.message;
}
});
// --- Write Multiple Registers (FC16) ---
document.getElementById('btnWriteMultipleRegisters').addEventListener('click', async () => {
if (!modbus || !modbus.port) {
writeStatusDisplay.textContent = 'Error: Modbus not connected.';
return;
}
try {
const slaveId = parseInt(document.getElementById('mrSlaveId').value);
const address = parseInt(document.getElementById('mrAddress').value);
const valuesStr = document.getElementById('mrValues').value;
const valuesArray = valuesStr.split(',').map(v => parseInt(v.trim()));
if (valuesArray.some(isNaN)) {
writeStatusDisplay.textContent = 'Error: Invalid register values. Must be comma-separated numbers.';
return;
}
if (valuesArray.length === 0 && valuesStr.trim() === "") {
writeStatusDisplay.textContent = 'Error: Register values cannot be empty.';
return;
}
for (const val of valuesArray) {
if (val < 0 || val > 65535) {
writeStatusDisplay.textContent = `Error: Invalid register value ${val}. All values must be 0-65535.`;
return;
}
}
writeStatusDisplay.textContent = 'Writing Multiple Registers...';
const success = await modbus.writeMultipleRegisters(slaveId, address, valuesArray);
if (success) {
writeStatusDisplay.textContent = `Success: ${valuesArray.length} Registers written starting at address ${address}.`;
}
} catch (error) {
TRACE_ERROR("WriteMultipleRegisters", "Error:", error);
writeStatusDisplay.textContent = 'Error writing Multiple Registers: ' + error.message;
}
});
TRACE_INFO("Example", "Modbus RTU Web Application Example Initialized.");
</script>
</body>
</html>

View File

@@ -1,4 +1,16 @@
class ModbusRTUMaster {
// Define Modbus Function Codes
static FUNCTION_CODES = {
READ_COILS: 0x01,
READ_DISCRETE_INPUTS: 0x02,
READ_HOLDING_REGISTERS: 0x03,
READ_INPUT_REGISTERS: 0x04,
WRITE_SINGLE_COIL: 0x05,
WRITE_SINGLE_REGISTER: 0x06,
WRITE_MULTIPLE_COILS: 0x0F,
WRITE_MULTIPLE_REGISTERS: 0x10,
};
constructor(config = {}) {
this.port = null;
this.reader = null;
@@ -102,45 +114,142 @@ class ModbusRTUMaster {
}
async readHoldingRegisters(slaveId, startAddress, quantity) {
const response = await this.readRegisters(slaveId, 0x03, startAddress, quantity);
const response = await this.readRegisters(slaveId, ModbusRTUMaster.FUNCTION_CODES.READ_HOLDING_REGISTERS, startAddress, quantity);
return this.parseRegisterValues(response, quantity);
}
async readInputRegisters(slaveId, startAddress, quantity) {
const response = await this.readRegisters(slaveId, 0x04, startAddress, quantity);
const response = await this.readRegisters(slaveId, ModbusRTUMaster.FUNCTION_CODES.READ_INPUT_REGISTERS, startAddress, quantity);
return this.parseRegisterValues(response, quantity);
}
async readCoils(slaveId, startAddress, quantity) {
const response = await this.readRegisters(slaveId, 0x01, startAddress, quantity);
const response = await this.readRegisters(slaveId, ModbusRTUMaster.FUNCTION_CODES.READ_COILS, startAddress, quantity);
return this.parseCoilValues(response, quantity);
}
async readDiscreteInputs(slaveId, startAddress, quantity) {
const response = await this.readRegisters(slaveId, 0x02, startAddress, quantity);
const response = await this.readRegisters(slaveId, ModbusRTUMaster.FUNCTION_CODES.READ_DISCRETE_INPUTS, startAddress, quantity);
return this.parseCoilValues(response, quantity);
}
async readRegisters(slaveId, functionCode, startAddress, quantity) {
const frameLength = 8; // 8 bytes for reading (slave ID, function code, start address, quantity, CRC)
if (frameLength > 250) {
throw new Error('Read frame length exceeded: frame too long.');
}
const frameLength = 8; // Request frame length for read operations
// This check is not very useful as request frame for reads is fixed at 8 bytes.
// Max PDU is 253, Max ADU is 256.
// if (frameLength > 250) {
// throw new Error('Read frame length exceeded: frame too long.');
// }
const request = this.buildRequest(slaveId, functionCode, startAddress, quantity);
TRACE_VERBOSE("mbrtu", "Send request");
const request = this._buildReadRequest(slaveId, functionCode, startAddress, quantity);
TRACE_VERBOSE("mbrtu", "Send read request", request);
await this.sendRequest(request);
return await this.receiveResponse(slaveId, functionCode, quantity);
// Determine expected response length based on function code
let expectedPduDataLength;
if (functionCode === ModbusRTUMaster.FUNCTION_CODES.READ_HOLDING_REGISTERS || functionCode === ModbusRTUMaster.FUNCTION_CODES.READ_INPUT_REGISTERS) {
expectedPduDataLength = 1 + quantity * 2; // byte count + register data
} else if (functionCode === ModbusRTUMaster.FUNCTION_CODES.READ_COILS || functionCode === ModbusRTUMaster.FUNCTION_CODES.READ_DISCRETE_INPUTS) {
expectedPduDataLength = 1 + Math.ceil(quantity / 8); // byte count + coil data
} else {
throw new Error(`Unknown function code for read operation: ${functionCode}`);
}
// Total ADU length = Slave ID (1) + Function Code (1) + PDU Data Length + CRC (2)
const expectedAduResponseLength = 1 + 1 + expectedPduDataLength + 2;
return await this._receiveResponse(slaveId, functionCode, expectedAduResponseLength);
}
buildRequest(slaveId, functionCode, address, quantity) {
const request = new Uint8Array(8);
/**
* @private
* Builds a Modbus request frame for read operations.
* @param {number} slaveId - The slave ID.
* @param {number} functionCode - The function code.
* @param {number} address - The starting address.
* @param {number} quantity - The quantity of items to read.
* @returns {Uint8Array} The request frame.
*/
_buildReadRequest(slaveId, functionCode, address, quantity) {
const request = new Uint8Array(8); // Read requests are always 8 bytes
request[0] = slaveId;
request[1] = functionCode;
request[2] = (address >> 8) & 0xFF;
request[3] = address & 0xFF;
request[4] = (quantity >> 8) & 0xFF;
request[5] = quantity & 0xFF;
request[2] = (address >> 8) & 0xFF; // Start Address High
request[3] = address & 0xFF; // Start Address Low
request[4] = (quantity >> 8) & 0xFF; // Quantity High
request[5] = quantity & 0xFF; // Quantity Low
const crc = this.calculateCRC(request.subarray(0, 6));
request[6] = crc & 0xFF;
request[7] = (crc >> 8) & 0xFF;
return request;
}
/**
* @private
* Builds a Modbus request frame for Write Single Coil operation.
* @param {number} slaveId - The slave ID.
* @param {number} address - The coil address.
* @param {number} outputValue - The value to write (0xFF00 for ON, 0x0000 for OFF).
* @returns {Uint8Array} The request frame.
*/
_buildWriteSingleCoilRequest(slaveId, address, outputValue) {
const request = new Uint8Array(8); // Write Single Coil requests are always 8 bytes
request[0] = slaveId;
request[1] = ModbusRTUMaster.FUNCTION_CODES.WRITE_SINGLE_COIL;
request[2] = (address >> 8) & 0xFF; // Coil Address High
request[3] = address & 0xFF; // Coil Address Low
request[4] = (outputValue >> 8) & 0xFF; // Output Value High
request[5] = outputValue & 0xFF; // Output Value Low
const crc = this.calculateCRC(request.subarray(0, 6));
request[6] = crc & 0xFF;
request[7] = (crc >> 8) & 0xFF;
return request;
}
/**
* @private
* Builds a Modbus request frame for Write Multiple Coils operation.
* @param {number} slaveId - The slave ID.
* @param {number} address - The starting coil address.
* @param {number} quantity - The quantity of coils to write.
* @param {number} byteCount - The number of bytes of coil data.
* @param {Uint8Array} coilBytes - The coil data bytes.
* @returns {Uint8Array} The request frame.
*/
_buildWriteMultipleCoilsRequest(slaveId, address, quantity, byteCount, coilBytes) {
const requestPdu = new Uint8Array(6 + byteCount); // FC (1) + Address (2) + Quantity (2) + ByteCount (1) + CoilBytes (byteCount)
requestPdu[0] = ModbusRTUMaster.FUNCTION_CODES.WRITE_MULTIPLE_COILS;
requestPdu[1] = (address >> 8) & 0xFF;
requestPdu[2] = address & 0xFF;
requestPdu[3] = (quantity >> 8) & 0xFF;
requestPdu[4] = quantity & 0xFF;
requestPdu[5] = byteCount;
requestPdu.set(coilBytes, 6);
const requestAdu = new Uint8Array(1 + requestPdu.length + 2); // SlaveID + PDU + CRC
requestAdu[0] = slaveId;
requestAdu.set(requestPdu, 1);
const crc = this.calculateCRC(requestAdu.subarray(0, 1 + requestPdu.length));
requestAdu[1 + requestPdu.length] = crc & 0xFF;
requestAdu[1 + requestPdu.length + 1] = (crc >> 8) & 0xFF;
return requestAdu;
}
/**
* @private
* Builds a Modbus request frame for Write Single Register operation.
* @param {number} slaveId - The slave ID.
* @param {number} address - The register address.
* @param {number} value - The 16-bit value to write to the register.
* @returns {Uint8Array} The request frame.
*/
_buildWriteSingleRegisterRequest(slaveId, address, value) {
const request = new Uint8Array(8); // Write Single Register requests are always 8 bytes
request[0] = slaveId;
request[1] = ModbusRTUMaster.FUNCTION_CODES.WRITE_SINGLE_REGISTER;
request[2] = (address >> 8) & 0xFF; // Register Address High
request[3] = address & 0xFF; // Register Address Low
request[4] = (value >> 8) & 0xFF; // Register Value High
request[5] = value & 0xFF; // Register Value Low
const crc = this.calculateCRC(request.subarray(0, 6));
request[6] = crc & 0xFF;
request[7] = (crc >> 8) & 0xFF;
@@ -168,73 +277,382 @@ class ModbusRTUMaster {
TRACE_VERBOSE("mbrtu", 'Request sent:', request);
}
async receiveResponse(slaveId, functionCode, quantity) {
let expectedLength = 5 + quantity * 2;
let response = new Uint8Array(expectedLength);
let index = 0;
/**
* @private
* Receives a Modbus response frame.
* @param {number} slaveId - The expected slave ID in the response.
* @param {number} functionCode - The expected function code in the response.
* @param {number} expectedAduLength - The total expected length of the ADU (including slave ID, FC, data, and CRC).
* @returns {Promise<Uint8Array>} A promise that resolves with the validated response frame.
* @throws {Error} If there's a timeout, CRC error, Modbus exception, or other communication error.
*/
async _receiveResponse(slaveId, functionCode, expectedAduLength) {
let responseFrame = new Uint8Array(expectedAduLength); // Initialize with fixed expected length
let bytesReceived = 0;
let actualExpectedAduLength = expectedAduLength; // Can change if an exception is detected
try {
await Promise.race([
(async () => {
while (index < expectedLength) {
let buffer = new Uint8Array(256); // Max Modbus ADU size
while (bytesReceived < actualExpectedAduLength) {
const { value, done } = await this.reader.read();
if (done) throw new Error('Device has been lost');
response.set(value, index);
index += value.length;
if (done) throw new Error('Device has been lost during read.');
if (index >= 2 && (response[1] & 0x80)) {
expectedLength = 5;
// Copy new data into a temporary larger buffer if needed
if (bytesReceived + value.length > buffer.length) {
const newBuffer = new Uint8Array(bytesReceived + value.length + 50); // Add some extra space
newBuffer.set(buffer.subarray(0, bytesReceived), 0);
buffer = newBuffer;
}
buffer.set(value, bytesReceived);
bytesReceived += value.length;
// Check for Modbus exception response early (Slave ID, Func Code + 0x80, Exception Code, CRC H, CRC L) = 5 bytes
if (bytesReceived >= 2 && (buffer[1] & 0x80) && buffer[0] === slaveId) {
// If it's an exception for this function code
if ((buffer[1] ^ 0x80) === functionCode) {
actualExpectedAduLength = 5; // Exception response is always 5 bytes
// If we've already received enough for an exception, break
if (bytesReceived >= actualExpectedAduLength) break;
}
}
}
responseFrame = buffer.subarray(0, bytesReceived);
})(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout: No response received within the time limit.')), this.timeout)
setTimeout(() => reject(new Error(`Timeout: No response received within ${this.timeout}ms for FC ${functionCode}. Expected ${actualExpectedAduLength} bytes.`)), this.timeout)
)
]);
if (response[1] & 0x80) {
const exceptionCode = response[2];
throw new Error(`Modbus Exception Code: ${this.getExceptionMessage(exceptionCode)} (Code: ${exceptionCode})`);
if (bytesReceived < actualExpectedAduLength && !(responseFrame[1] & 0x80 && bytesReceived === 5) ) {
throw new Error(`Incomplete response: Received ${bytesReceived} bytes, expected ${actualExpectedAduLength} bytes.`);
}
const dataWithoutCRC = response.slice(0, index - 2);
const receivedCRC = (response[index - 1] << 8) | response[index - 2];
// Final trim to actualExpectedAduLength if more was read due to timing
if (bytesReceived > actualExpectedAduLength) {
responseFrame = responseFrame.subarray(0, actualExpectedAduLength);
}
// Validate Slave ID
if (responseFrame[0] !== slaveId) {
throw new Error(`Invalid Slave ID: Expected ${slaveId}, received ${responseFrame[0]}`);
}
// Check for Modbus exception
if (responseFrame[1] & 0x80) {
if ((responseFrame[1] ^ 0x80) === functionCode) { // Check if the exception function code matches the request
if (responseFrame.length !== 5) {
throw new Error(`Invalid exception response length: Received ${responseFrame.length} bytes, expected 5.`);
}
const exceptionCode = responseFrame[2];
// CRC check for exception response
const dataWithoutCRCex = responseFrame.subarray(0, 3); // SID, FC_ERR, ERR_CODE
const receivedCRCex = (responseFrame[4] << 8) | responseFrame[3]; // CRC is LSB first
const calculatedCRCex = this.calculateCRC(dataWithoutCRCex);
if (calculatedCRCex !== receivedCRCex) {
throw new Error(`CRC Error in exception response: Calculated ${calculatedCRCex}, received ${receivedCRCex}. Response: ${responseFrame}`);
}
throw new Error(`Modbus Exception Code: ${this.getExceptionMessage(exceptionCode)} (Function: ${functionCode}, Code: ${exceptionCode})`);
} else {
// This case should ideally not happen if slave responds correctly
throw new Error(`Received error for unexpected function code. Requested ${functionCode}, got error for ${responseFrame[1] ^ 0x80}`);
}
}
// Validate function code for normal response
if (responseFrame[1] !== functionCode) {
throw new Error(`Invalid Function Code: Expected ${functionCode}, received ${responseFrame[1]}`);
}
// Validate CRC for normal response
const dataWithoutCRC = responseFrame.subarray(0, responseFrame.length - 2);
const receivedCRC = (responseFrame[responseFrame.length - 1] << 8) | responseFrame[responseFrame.length - 2]; // CRC is LSB first
const calculatedCRC = this.calculateCRC(dataWithoutCRC);
if (calculatedCRC === receivedCRC) {
TRACE_VERBOSE("mbrtu", 'Received response with valid CRC:', response.slice(0, index));
return response.slice(0, index);
TRACE_VERBOSE("mbrtu", 'Received valid response:', responseFrame);
return responseFrame;
} else {
throw new Error(`CRC Error: Calculated CRC ${calculatedCRC} does not match received CRC ${receivedCRC}.`);
throw new Error(`CRC Error: Calculated CRC ${calculatedCRC} does not match received CRC ${receivedCRC}. Response: ${responseFrame}`);
}
} catch (error) {
TRACE_ERROR("mbrtu", 'Error receiving response:', error.message);
TRACE_ERROR("mbrtu", `Error receiving response for FC ${functionCode}:`, error.message, error.stack);
// Attempt to clear the serial buffer by cancelling the reader
if (this.reader) {
await this.reader.cancel();
await this.reader.releaseLock();
this.reader = null;
this.reader = this.port.readable.getReader();
try {
// A short read to attempt to clear any pending data after cancel
const abortController = new AbortController();
const abortSignal = abortController.signal;
setTimeout(() => abortController.abort(), 50); // Quick timeout for this clear attempt
await this.reader.read({ signal: abortSignal }).catch(() => {}); // Ignore error from this read
await this.reader.cancel().catch(e => TRACE_ERROR("mbrtu", "Error cancelling reader:", e)); // Log cancel error but continue
await this.reader.releaseLock().catch(e => TRACE_ERROR("mbrtu", "Error releasing lock:", e)); // Log release error
} catch (cancelError) {
TRACE_ERROR("mbrtu", "Exception during reader cancellation/release:", cancelError);
} finally {
this.reader = null; // Ensure reader is nullified
if (this.port && this.port.readable) { // Check if port and readable stream still exist
try {
this.reader = this.port.readable.getReader();
} catch (getReaderError){
TRACE_ERROR("mbrtu", "Failed to re-acquire reader:", getReaderError);
// This might indicate a more severe port issue
}
} else {
TRACE_WARNING("mbrtu", "Port or readable stream not available to re-acquire reader.");
}
}
}
if (error.message.includes('Device has been lost')) {
await this.handleDeviceLost();
await this.handleDeviceLost(); // This will attempt to reconnect
}
throw error;
throw error; // Re-throw the original error
}
}
getExceptionMessage(code) {
const exceptionMessages = {
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 Device Failed to Respond'
0x01: 'Illegal Function',
0x02: 'Illegal Data Address',
0x03: 'Illegal Data Value',
0x04: 'Slave Device Failure',
0x05: 'Acknowledge',
0x06: 'Slave Device Busy',
0x08: 'Memory Parity Error',
0x0A: 'Gateway Path Unavailable',
0x0B: 'Gateway Target Device Failed to Respond'
};
return exceptionMessages[code] || 'Unknown Error';
return exceptionMessages[code] || `Unknown Error (Code: ${code})`;
}
/**
* Writes a single coil to a Modbus slave device.
* @param {number} slaveId - The ID of the slave device.
* @param {number} address - The address of the coil to write.
* @param {boolean} value - The value to write to the coil (true for ON, false for OFF).
* @returns {Promise<boolean>} A promise that resolves to true if the write was successful.
* @throws {Error} If the write operation fails or a Modbus exception occurs.
*/
async writeSingleCoil(slaveId, address, value) {
const outputValue = value ? 0xFF00 : 0x0000; // ON is 0xFF00, OFF is 0x0000
const request = this._buildWriteSingleCoilRequest(slaveId, address, outputValue);
TRACE_VERBOSE("mbrtu", "Send writeSingleCoil request", request);
await this.sendRequest(request);
// Expected response for Write Single Coil is an echo of the request (8 bytes)
const expectedAduResponseLength = 8;
const response = await this._receiveResponse(slaveId, ModbusRTUMaster.FUNCTION_CODES.WRITE_SINGLE_COIL, expectedAduResponseLength);
// Validate the response: it should be an echo of the request PDU (excluding CRC)
// Request: [SlaveID, FuncCode, AddressHI, AddressLO, ValueHI, ValueLO, CRCHI, CRCLO]
// Response: [SlaveID, FuncCode, AddressHI, AddressLO, ValueHI, ValueLO, CRCHI, CRCLO]
if (response.length !== 8) {
throw new Error(`WriteSingleCoil: Invalid response length. Expected 8, got ${response.length}`);
}
for (let i = 0; i < 6; i++) { // Compare first 6 bytes (SlaveID to ValueLO)
if (request[i] !== response[i]) {
throw new Error(
`WriteSingleCoil: Response mismatch. Request: [${request.slice(0,6).join(',')}] Response: [${response.slice(0,6).join(',')}]`
);
}
}
TRACE_INFO("mbrtu", `writeSingleCoil successful to slave ${slaveId}, address ${address}, value ${value}`);
return true;
}
/**
* Writes a single register to a Modbus slave device.
* @param {number} slaveId - The ID of the slave device.
* @param {number} address - The address of the register to write (0x0000 to 0xFFFF).
* @param {number} value - The 16-bit value to write to the register (0 to 65535).
* @returns {Promise<boolean>} A promise that resolves to true if the write was successful.
* @throws {Error} If the write operation fails, value is out of range, or a Modbus exception occurs.
*/
async writeSingleRegister(slaveId, address, value) {
if (value < 0 || value > 0xFFFF) {
throw new Error(`Invalid register value: ${value}. Must be between 0 and 65535.`);
}
if (address < 0 || address > 0xFFFF) {
throw new Error(`Invalid register address: ${address}. Must be between 0x0000 and 0xFFFF.`);
}
const request = this._buildWriteSingleRegisterRequest(slaveId, address, value);
TRACE_VERBOSE("mbrtu", "Send writeSingleRegister request", request);
await this.sendRequest(request);
// Expected response for Write Single Register is an echo of the request (8 bytes)
const expectedAduResponseLength = 8;
const response = await this._receiveResponse(slaveId, ModbusRTUMaster.FUNCTION_CODES.WRITE_SINGLE_REGISTER, expectedAduResponseLength);
// Validate the response: it should be an echo of the request PDU (excluding CRC)
// Request: [SlaveID, FuncCode, AddressHI, AddressLO, ValueHI, ValueLO, CRCLO, CRCHI]
// Response: [SlaveID, FuncCode, AddressHI, AddressLO, ValueHI, ValueLO, CRCLO, CRCHI]
if (response.length !== 8) {
throw new Error(`WriteSingleRegister: Invalid response length. Expected 8, got ${response.length}`);
}
for (let i = 0; i < 6; i++) { // Compare first 6 bytes (SlaveID to ValueLO)
if (request[i] !== response[i]) {
throw new Error(
`WriteSingleRegister: Response mismatch. Request: [${request.slice(0,6).join(',')}] Response: [${response.slice(0,6).join(',')}]`
);
}
}
TRACE_INFO("mbrtu", `writeSingleRegister successful to slave ${slaveId}, address ${address}, value ${value}`);
return true;
}
/**
* Writes multiple coils to a Modbus slave device (Function Code 0x0F).
* @param {number} slaveId - The ID of the slave device.
* @param {number} address - The starting address of the coils to write (0x0000 to 0xFFFF).
* @param {Array<boolean|number>} values - An array of boolean or numeric (0 or 1) values to write.
* @returns {Promise<boolean>} A promise that resolves to true if the write was successful.
* @throws {Error} If the write operation fails, inputs are invalid, or a Modbus exception occurs.
*/
async writeMultipleCoils(slaveId, address, values) {
if (!Array.isArray(values) || values.length === 0) {
throw new Error('Invalid values: Must be a non-empty array.');
}
const quantity = values.length;
if (quantity < 1 || quantity > 1968) { // Max 1968 coils (0x07B0) => 246 data bytes
throw new Error(`Invalid quantity of coils: ${quantity}. Must be between 1 and 1968.`);
}
if (address < 0 || address > 0xFFFF) {
throw new Error(`Invalid coil address: ${address}. Must be between 0x0000 and 0xFFFF.`);
}
if ((address + quantity -1) > 0xFFFF) {
throw new Error(`Invalid address range: Start address ${address} + quantity ${quantity} exceeds 0xFFFF.`);
}
const byteCount = Math.ceil(quantity / 8);
const coilBytes = new Uint8Array(byteCount);
for (let i = 0; i < quantity; i++) {
if (values[i]) { // true or 1
coilBytes[Math.floor(i / 8)] |= (1 << (i % 8));
}
}
const request = this._buildWriteMultipleCoilsRequest(slaveId, address, quantity, byteCount, coilBytes);
TRACE_VERBOSE("mbrtu", "Send writeMultipleCoils request", request);
await this.sendRequest(request);
// Expected response for Write Multiple Coils: [SlaveID, FuncCode, StartAddrHI, StartAddrLO, QuantityHI, QuantityLO, CRC_LO, CRC_HI] (8 bytes)
const expectedAduResponseLength = 8;
const response = await this._receiveResponse(slaveId, ModbusRTUMaster.FUNCTION_CODES.WRITE_MULTIPLE_COILS, expectedAduResponseLength);
if (response.length !== 8) {
throw new Error(`WriteMultipleCoils: Invalid response length. Expected 8, got ${response.length}`);
}
const responseAddress = (response[2] << 8) | response[3];
const responseQuantity = (response[4] << 8) | response[5];
if (responseAddress !== address) {
throw new Error(`WriteMultipleCoils: Response address mismatch. Sent ${address}, received ${responseAddress}.`);
}
if (responseQuantity !== quantity) {
throw new Error(`WriteMultipleCoils: Response quantity mismatch. Sent ${quantity}, received ${responseQuantity}.`);
}
TRACE_INFO("mbrtu", `writeMultipleCoils successful to slave ${slaveId}, address ${address}, quantity ${quantity}`);
return true;
}
/**
* @private
* Builds a Modbus request frame for Write Multiple Registers operation.
* @param {number} slaveId - The slave ID.
* @param {number} address - The starting register address.
* @param {number} quantity - The quantity of registers to write.
* @param {number} byteCount - The number of bytes of register data (quantity * 2).
* @param {Uint8Array} registerBytesData - The register data bytes (each register as 2 bytes, H L).
* @returns {Uint8Array} The request frame.
*/
_buildWriteMultipleRegistersRequest(slaveId, address, quantity, byteCount, registerBytesData) {
const requestPdu = new Uint8Array(6 + byteCount); // FC (1) + Address (2) + Quantity (2) + ByteCount (1) + RegisterBytes (byteCount)
requestPdu[0] = ModbusRTUMaster.FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS;
requestPdu[1] = (address >> 8) & 0xFF;
requestPdu[2] = address & 0xFF;
requestPdu[3] = (quantity >> 8) & 0xFF;
requestPdu[4] = quantity & 0xFF;
requestPdu[5] = byteCount;
requestPdu.set(registerBytesData, 6);
const requestAdu = new Uint8Array(1 + requestPdu.length + 2); // SlaveID + PDU + CRC
requestAdu[0] = slaveId;
requestAdu.set(requestPdu, 1);
const crc = this.calculateCRC(requestAdu.subarray(0, 1 + requestPdu.length));
requestAdu[1 + requestPdu.length] = crc & 0xFF;
requestAdu[1 + requestPdu.length + 1] = (crc >> 8) & 0xFF;
return requestAdu;
}
/**
* Writes multiple registers to a Modbus slave device (Function Code 0x10).
* @param {number} slaveId - The ID of the slave device.
* @param {number} address - The starting address of the registers to write (0x0000 to 0xFFFF).
* @param {Array<number>} values - An array of 16-bit integer values to write.
* @returns {Promise<boolean>} A promise that resolves to true if the write was successful.
* @throws {Error} If the write operation fails, inputs are invalid, or a Modbus exception occurs.
*/
async writeMultipleRegisters(slaveId, address, values) {
if (!Array.isArray(values) || values.length === 0) {
throw new Error('Invalid values: Must be a non-empty array.');
}
const quantity = values.length;
if (quantity < 1 || quantity > 123) { // Max 123 registers (0x007B) => 246 data bytes
throw new Error(`Invalid quantity of registers: ${quantity}. Must be between 1 and 123.`);
}
if (address < 0 || address > 0xFFFF) {
throw new Error(`Invalid register address: ${address}. Must be between 0x0000 and 0xFFFF.`);
}
if ((address + quantity - 1) > 0xFFFF) {
throw new Error(`Invalid address range: Start address ${address} + quantity ${quantity} exceeds 0xFFFF.`);
}
const byteCount = quantity * 2;
const registerBytesData = new Uint8Array(byteCount);
for (let i = 0; i < quantity; i++) {
const val = values[i];
if (val < 0 || val > 0xFFFF) {
throw new Error(`Invalid register value at index ${i}: ${val}. Must be between 0 and 65535.`);
}
registerBytesData[i * 2] = (val >> 8) & 0xFF; // High byte
registerBytesData[i * 2 + 1] = val & 0xFF; // Low byte
}
const request = this._buildWriteMultipleRegistersRequest(slaveId, address, quantity, byteCount, registerBytesData);
TRACE_VERBOSE("mbrtu", "Send writeMultipleRegisters request", request);
await this.sendRequest(request);
// Expected response for Write Multiple Registers: [SlaveID, FuncCode, StartAddrHI, StartAddrLO, QuantityHI, QuantityLO, CRC_LO, CRC_HI] (8 bytes)
const expectedAduResponseLength = 8;
const response = await this._receiveResponse(slaveId, ModbusRTUMaster.FUNCTION_CODES.WRITE_MULTIPLE_REGISTERS, expectedAduResponseLength);
if (response.length !== 8) {
throw new Error(`WriteMultipleRegisters: Invalid response length. Expected 8, got ${response.length}`);
}
const responseAddress = (response[2] << 8) | response[3];
const responseQuantity = (response[4] << 8) | response[5];
if (responseAddress !== address) {
throw new Error(`WriteMultipleRegisters: Response address mismatch. Sent ${address}, received ${responseAddress}.`);
}
if (responseQuantity !== quantity) {
throw new Error(`WriteMultipleRegisters: Response quantity mismatch. Sent ${quantity}, received ${responseQuantity}.`);
}
TRACE_INFO("mbrtu", `writeMultipleRegisters successful to slave ${slaveId}, address ${address}, quantity ${quantity}`);
return true;
}
parseRegisterValues(response, quantity) {