29 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
6f13fc5c0d Merge pull request #12 from Pablo2048/development
Refuse to use GA :-(
2024-10-12 15:35:10 +02:00
855e023758 Refuse to use GA :-( 2024-10-12 15:34:48 +02:00
11f6d7a7b5 Merge pull request #11 from Pablo2048/development
Identity update
2024-10-12 15:32:55 +02:00
33299110a3 Identity update 2024-10-12 15:32:28 +02:00
92c234e0a7 Merge pull request #10 from Pablo2048/development
Update GA with access token
2024-10-12 15:29:58 +02:00
f7494799b9 GA update 2024-10-12 15:29:18 +02:00
e5a665b29f Access token to GA 2024-10-12 15:29:01 +02:00
02b564fc46 Merge pull request #9 from Pablo2048/development
Update both main & development when rebase
2024-10-12 15:26:13 +02:00
4aff096f7b Update both main & development when rebase 2024-10-12 15:25:35 +02:00
1ac634fd8d Merge pull request #8 from Pablo2048/development
GA update
2024-10-12 15:21:13 +02:00
f634bf648a GA update 2024-10-12 15:20:32 +02:00
cd80deb142 Merge pull request #7 from Pablo2048/development
New way to sync
2024-10-12 15:16:03 +02:00
eaa95d08d0 New way to sync 2024-10-12 15:15:43 +02:00
55236c9356 Merge pull request #6 from Pablo2048/development
Documentation update
2024-10-11 15:47:31 +02:00
13a19f930f Documentation update 2024-10-11 15:46:38 +02:00
e5bcf00422 Merge pull request #5 from Pablo2048/development
Manual script to sync
2024-10-11 15:41:02 +02:00
d83a2db169 Manual script to sync 2024-10-11 15:40:22 +02:00
29b7f6de2c Merge pull request #4 from Pablo2048/development
Directory typo fix
2024-10-11 15:38:34 +02:00
02b6f692b0 Directory typo fix 2024-10-11 15:37:46 +02:00
88cb121cb2 Merge pull request #3 from Pablo2048/development
GA for automatic development sync
2024-10-11 15:36:14 +02:00
5434dcbbc8 GA for automatic development sync 2024-10-11 15:34:40 +02:00
a7c83f738f Merge pull request #2 from Pablo2048/development
Correctly handle disconnect & reconnect events
2024-10-11 15:29:19 +02:00
c7fba48b19 Update example code 2024-10-11 15:27:28 +02:00
c16d2ed08b Note about using trace.js library 2024-10-11 15:09:25 +02:00
1350335c75 Refactoring, correctly handling disconnect & reconnect, use of trace.js 2024-10-10 14:48:54 +02:00
5f3569d6be Merge pull request #1 from Pablo2048/development
Better disconnect handling
2024-10-09 14:06:56 +02:00
4f5fb3daa8 Missing reader.cancel() call. 2024-10-05 15:25:59 +02:00
6272dbf38a Better disconnect and connection timeout handling 2024-10-05 15:24:07 +02:00
4 changed files with 984 additions and 108 deletions

132
README.md
View File

@@ -1,9 +1,135 @@
# Modbus-RTU-Master
> [!NOTE]
> The library now uses my https://github.com/Pablo2048/trace.js javascript library for debugging output.
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).
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, currently, uploading via file:// does not work, 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.
## 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>
@@ -55,18 +76,95 @@
<option value="odd">Odd</option>
</select>
<label for="timeout">Timeout (ms):</label>
<input type="number" id="timeout" value="150">
<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 = ['*'];
</script>
<script src="https://pablo2048.github.io/trace.js/src/trace.js"></script>
<script src="../src/modbus-rtu-master.js"></script>
<script>
let modbus;
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
@@ -82,36 +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 {
console.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) {
console.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);
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;
}
});
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;
@@ -9,11 +21,36 @@ class ModbusRTUMaster {
this.parity = config.parity || 'none';
this.flowControl = config.flowControl || 'none';
this.timeout = config.timeout || 2000; // Default timeout is 2 seconds
navigator.serial.addEventListener('disconnect', (event) => {
TRACE_WARNING("mbrtu", 'Port disconnected, attempting to reconnect...');
this.handleDeviceLost();
});
navigator.serial.addEventListener('connect', (event) => {
TRACE_INFO("mbrtu", 'Device connected:', event);
this.reconnect();
});
}
async connect() {
try {
this.port = await navigator.serial.requestPort();
await this.openPort();
TRACE_VERBOSE("mbrtu", 'Connected to serial port with configuration:', {
baudRate: this.baudRate,
dataBits: this.dataBits,
stopBits: this.stopBits,
parity: this.parity,
flowControl: this.flowControl,
timeout: this.timeout
});
} catch (error) {
TRACE_ERROR("mbrtu", 'Failed to open serial port:', error);
}
}
async openPort() {
await this.port.open({
baudRate: this.baudRate,
dataBits: this.dataBits,
@@ -23,71 +60,196 @@ class ModbusRTUMaster {
});
this.reader = this.port.readable.getReader();
this.writer = this.port.writable.getWriter();
console.log('Connected to serial port with configuration:', {
baudRate: this.baudRate,
dataBits: this.dataBits,
stopBits: this.stopBits,
parity: this.parity,
flowControl: this.flowControl,
timeout: this.timeout
});
} catch (error) {
console.error('Failed to open serial port:', error);
}
}
async disconnect() {
TRACE_ERROR("mbrtu", "Disconnect.");
try {
if (this.reader) {
await this.reader.cancel();
await this.reader.releaseLock();
this.reader = null;
}
if (this.writer) {
await this.writer.releaseLock();
this.writer = null;
}
if (this.port) {
await this.port.close();
console.log('Serial port closed');
this.port = null;
TRACE_INFO("mbrtu", 'Serial port closed');
}
} catch (error) {
TRACE_ERROR("mbrtu", 'Error during disconnect:', error);
}
}
async handleDeviceLost() {
TRACE_WARNING("mbrtu", 'Attempting to reconnect...');
await this.disconnect();
this.reconnect();
}
reconnect() {
setTimeout(async () => {
try {
const ports = await navigator.serial.getPorts();
if (ports.length > 0) {
this.port = ports[0];
if (!this.port.readable && !this.port.writable) {
await this.openPort();
TRACE_INFO("mbrtu", 'Reconnected to serial port.');
} else {
TRACE_WARNING("mbrtu", 'Port is already open. Skipping open operation.');
}
} else {
TRACE_WARNING("mbrtu", 'Port not found, retrying...');
this.reconnect();
}
} catch (error) {
TRACE_ERROR("mbrtu", 'Reconnect failed:', error);
this.reconnect();
}
}, 1000); // Retry every 1 second
}
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);
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;
@@ -112,81 +274,385 @@ class ModbusRTUMaster {
async sendRequest(request) {
await this.writer.write(request);
//console.log('Request sent:', request);
TRACE_VERBOSE("mbrtu", 'Request sent:', request);
}
// Receiving and validating response with timeout detection and Modbus Exception Codes processing
async receiveResponse(slaveId, functionCode, quantity) {
// Set the expected length of the response (slaveId, functionCode, byte count, data, CRC)
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 an exception is detected (highest bit of the function code), set the length to 5 bytes (slaveId, functionCode, exceptionCode, CRC)
if (index >= 2 && (response[1] & 0x80)) {
expectedLength = 5; // Override expected length in case of an exception
// 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)
)
]);
// Modbus Exception Codes processing
if (response[1] & 0x80) { // Check if the highest bit of the function code is set
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.`);
}
// Check CRC after receiving the full response
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) {
//console.log('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) {
console.error('Error receiving response:', error.message);
if (error.message.includes('Device has been lost')) {
await this.handleDeviceLost();
}
return { error: 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) {
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
async handleDeviceLost() {
console.warn('Attempting to reconnect...');
await this.disconnect();
await this.connect();
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(); // This will attempt to reconnect
}
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) {

24
update_dev.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Stáhni všechny větve z origin
git fetch origin
# Přepni se na main a stáhni nejnovější změny
git checkout main
git pull origin main
# Zkontroluj, jestli existuje větev development lokálně, pokud ne, vytvoř ji
if git show-ref --quiet refs/heads/development; then
git checkout development
else
git checkout -b development origin/development
fi
# Stáhni nejnovější změny z development větve
git pull origin development
# Proveď merge z main do development s povolením nesouvisejících historií
git merge main --allow-unrelated-histories
# Pushni změny do vzdálené development větve
git push origin development