mirror of
				https://github.com/Pablo2048/modbus-rtu-master.git
				synced 2025-10-31 16:11:51 +01:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			developmen
			...
			feat/modbu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8ba43c2ba4 | ||
| 6f13fc5c0d | |||
| 11f6d7a7b5 | |||
| 92c234e0a7 | |||
| 02b564fc46 | 
							
								
								
									
										127
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								README.md
									
									
									
									
									
								
							| @@ -5,8 +5,131 @@ | |||||||
|  |  | ||||||
| Implementation of a Modbus RTU master device in JavaScript, usable in web applications in Chrome and Firefox browsers. | 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. | 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. | ||||||
|   | |||||||
| @@ -11,25 +11,46 @@ | |||||||
|             display: flex; |             display: flex; | ||||||
|             flex-direction: column; |             flex-direction: column; | ||||||
|             align-items: center; |             align-items: center; | ||||||
|             justify-content: center; |             /* justify-content: center; // Adjusted for longer content */ | ||||||
|             height: 100vh; |             /* height: 100vh; // Adjusted for longer content */ | ||||||
|  |             margin-bottom: 50px; /* Add some bottom margin */ | ||||||
|         } |         } | ||||||
|         #register-value { |         #register-value { | ||||||
|             font-size: 2em; |             font-size: 2em; | ||||||
|             margin-top: 20px; |             margin-top: 20px; | ||||||
|         } |         } | ||||||
|         #error-message { |         #error-message, #write-status-message { | ||||||
|             color: red; |  | ||||||
|             margin-top: 10px; |             margin-top: 10px; | ||||||
|  |             min-height: 20px; /* Ensure space even if empty */ | ||||||
|         } |         } | ||||||
|         button, input, select { |         #error-message { color: red; } | ||||||
|             padding: 10px; |         #write-status-message { color: green; } | ||||||
|             margin: 5px; |         button, input, select, label { | ||||||
|             font-size: 1em; |             padding: 8px; /* Slightly reduced padding */ | ||||||
|  |             margin: 4px; /* Slightly reduced margin */ | ||||||
|  |             font-size: 0.9em; /* Slightly reduced font size */ | ||||||
|             cursor: pointer; |             cursor: pointer; | ||||||
|         } |         } | ||||||
|         label { |         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> |     </style> | ||||||
| </head> | </head> | ||||||
| @@ -58,8 +79,80 @@ | |||||||
|         <input type="number" id="timeout" value="300"> |         <input type="number" id="timeout" value="300"> | ||||||
|     </div> |     </div> | ||||||
|     <button id="connect-btn">Připojit k sériovému portu</button> |     <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> |     <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> |     <script> | ||||||
|         window.LOG_LEVEL = 'VERBOSE'; |         window.LOG_LEVEL = 'VERBOSE'; | ||||||
|         window.ENABLED_MODULES = ['*']; |         window.ENABLED_MODULES = ['*']; | ||||||
| @@ -71,6 +164,7 @@ | |||||||
|         const connectButton = document.getElementById('connect-btn'); |         const connectButton = document.getElementById('connect-btn'); | ||||||
|         const valueDisplay = document.getElementById('value'); |         const valueDisplay = document.getElementById('value'); | ||||||
|         const errorDisplay = document.getElementById('error-message'); |         const errorDisplay = document.getElementById('error-message'); | ||||||
|  |         const writeStatusDisplay = document.getElementById('write-status-message'); | ||||||
|         let intervalId = null; |         let intervalId = null; | ||||||
|  |  | ||||||
|         // Funkce pro získání hodnot z formuláře |         // Funkce pro získání hodnot z formuláře | ||||||
| @@ -86,37 +180,198 @@ | |||||||
|  |  | ||||||
|         // Funkce pro aktualizaci hodnoty registru každých 5 sekund |         // Funkce pro aktualizaci hodnoty registru každých 5 sekund | ||||||
|         async function updateRegisterValue() { |         async function updateRegisterValue() { | ||||||
|  |             if (!modbus || !modbus.port) { // Ensure modbus is connected | ||||||
|  |                 TRACE_VERBOSE("Read", "Modbus not connected, skipping read."); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|             try { |             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); |                 const values = await modbus.readInputRegisters(0x19, 0x0000, 3); | ||||||
|  |  | ||||||
|                 if (values && values.length > 0) { |                 if (values && values.length > 0) { | ||||||
|                     // Zobrazí hodnotu prvního (a jediného) registru |                     // Zobrazí hodnotu prvního (a jediného) registru | ||||||
|                     const registerValue = values[0]; |                     valueDisplay.textContent = values.join(', '); // Display all read values | ||||||
|                     valueDisplay.textContent = registerValue; |  | ||||||
|                     errorDisplay.textContent = ''; // Vymazat případné staré chyby |                     errorDisplay.textContent = ''; // Vymazat případné staré chyby | ||||||
|                 } else { |                 } else { | ||||||
|                     TRACE_ERROR("", 'Neplatná odpověď:', values); |                     TRACE_ERROR("Read", 'Neplatná odpověď:', values); | ||||||
|                     valueDisplay.textContent = 'Error'; |                     valueDisplay.textContent = 'Error/No Values'; | ||||||
|                     errorDisplay.textContent = 'Invalid response received.'; |                     errorDisplay.textContent = 'Invalid response or no values received.'; | ||||||
|                 } |                 } | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 TRACE_ERROR("", 'Chyba při čtení registru:', error); |                 TRACE_ERROR("Read", 'Chyba při čtení registru:', error); | ||||||
|                 valueDisplay.textContent = '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í |         // Připojení k sériovému portu a spuštění čtení | ||||||
|         connectButton.addEventListener('click', async () => { |         connectButton.addEventListener('click', async () => { | ||||||
|             const config = getConfig(); |             const config = getConfig(); | ||||||
|             modbus = new ModbusRTUMaster(config); |             modbus = new ModbusRTUMaster(config); // Initialize or re-initialize | ||||||
|             await modbus.connect(); |             try { | ||||||
|             if (intervalId === null) { |                 await modbus.connect(); | ||||||
|                 intervalId = setInterval(updateRegisterValue, 5000); // Každých 5 sekund |                 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> |     </script> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -1,4 +1,16 @@ | |||||||
| class ModbusRTUMaster { | 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 = {}) { |     constructor(config = {}) { | ||||||
|         this.port = null; |         this.port = null; | ||||||
|         this.reader = null; |         this.reader = null; | ||||||
| @@ -102,45 +114,142 @@ class ModbusRTUMaster { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async readHoldingRegisters(slaveId, startAddress, quantity) { |     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); |         return this.parseRegisterValues(response, quantity); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async readInputRegisters(slaveId, startAddress, 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); |         return this.parseRegisterValues(response, quantity); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async readCoils(slaveId, startAddress, 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); |         return this.parseCoilValues(response, quantity); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async readDiscreteInputs(slaveId, startAddress, 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); |         return this.parseCoilValues(response, quantity); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async readRegisters(slaveId, functionCode, startAddress, quantity) { |     async readRegisters(slaveId, functionCode, startAddress, quantity) { | ||||||
|         const frameLength = 8; // 8 bytes for reading (slave ID, function code, start address, quantity, CRC) |         const frameLength = 8; // Request frame length for read operations | ||||||
|         if (frameLength > 250) { |         // This check is not very useful as request frame for reads is fixed at 8 bytes. | ||||||
|             throw new Error('Read frame length exceeded: frame too long.'); |         // 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 request"); |         TRACE_VERBOSE("mbrtu", "Send read request", request); | ||||||
|         await this.sendRequest(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[0] = slaveId; | ||||||
|         request[1] = functionCode; |         request[1] = functionCode; | ||||||
|         request[2] = (address >> 8) & 0xFF; |         request[2] = (address >> 8) & 0xFF;    // Start Address High | ||||||
|         request[3] = address & 0xFF; |         request[3] = address & 0xFF;         // Start Address Low | ||||||
|         request[4] = (quantity >> 8) & 0xFF; |         request[4] = (quantity >> 8) & 0xFF; // Quantity High | ||||||
|         request[5] = quantity & 0xFF; |         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)); |         const crc = this.calculateCRC(request.subarray(0, 6)); | ||||||
|         request[6] = crc & 0xFF; |         request[6] = crc & 0xFF; | ||||||
|         request[7] = (crc >> 8) & 0xFF; |         request[7] = (crc >> 8) & 0xFF; | ||||||
| @@ -168,73 +277,382 @@ class ModbusRTUMaster { | |||||||
|         TRACE_VERBOSE("mbrtu", 'Request sent:', request); |         TRACE_VERBOSE("mbrtu", 'Request sent:', request); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async receiveResponse(slaveId, functionCode, quantity) { |     /** | ||||||
|         let expectedLength = 5 + quantity * 2; |      * @private | ||||||
|         let response = new Uint8Array(expectedLength); |      * Receives a Modbus response frame. | ||||||
|         let index = 0; |      * @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 { |         try { | ||||||
|             await Promise.race([ |             await Promise.race([ | ||||||
|                 (async () => { |                 (async () => { | ||||||
|                     while (index < expectedLength) { |                     let buffer = new Uint8Array(256); // Max Modbus ADU size | ||||||
|  |                     while (bytesReceived < actualExpectedAduLength) { | ||||||
|                         const { value, done } = await this.reader.read(); |                         const { value, done } = await this.reader.read(); | ||||||
|                         if (done) throw new Error('Device has been lost'); |                         if (done) throw new Error('Device has been lost during read.'); | ||||||
|                         response.set(value, index); |                          | ||||||
|                         index += value.length; |                         // 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; | ||||||
|  |  | ||||||
|                         if (index >= 2 && (response[1] & 0x80)) { |                         // Check for Modbus exception response early (Slave ID, Func Code + 0x80, Exception Code, CRC H, CRC L) = 5 bytes | ||||||
|                             expectedLength = 5; |                         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) => |                 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) { |             if (bytesReceived < actualExpectedAduLength && !(responseFrame[1] & 0x80 && bytesReceived === 5) ) { | ||||||
|                 const exceptionCode = response[2]; |                  throw new Error(`Incomplete response: Received ${bytesReceived} bytes, expected ${actualExpectedAduLength} bytes.`); | ||||||
|                 throw new Error(`Modbus Exception Code: ${this.getExceptionMessage(exceptionCode)} (Code: ${exceptionCode})`); |             } | ||||||
|  |              | ||||||
|  |             // Final trim to actualExpectedAduLength if more was read due to timing | ||||||
|  |             if (bytesReceived > actualExpectedAduLength) { | ||||||
|  |                 responseFrame = responseFrame.subarray(0, actualExpectedAduLength); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             const dataWithoutCRC = response.slice(0, index - 2); |  | ||||||
|             const receivedCRC = (response[index - 1] << 8) | response[index - 2]; |             // 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); |             const calculatedCRC = this.calculateCRC(dataWithoutCRC); | ||||||
|  |  | ||||||
|             if (calculatedCRC === receivedCRC) { |             if (calculatedCRC === receivedCRC) { | ||||||
|                 TRACE_VERBOSE("mbrtu", 'Received response with valid CRC:', response.slice(0, index)); |                 TRACE_VERBOSE("mbrtu", 'Received valid response:', responseFrame); | ||||||
|                 return response.slice(0, index); |                 return responseFrame; | ||||||
|             } else { |             } 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) { |         } 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) { |             if (this.reader) { | ||||||
|                 await this.reader.cancel(); |                 try { | ||||||
|                 await this.reader.releaseLock(); |                     // A short read to attempt to clear any pending data after cancel | ||||||
|                 this.reader = null; |                     const abortController = new AbortController(); | ||||||
|                 this.reader = this.port.readable.getReader(); |                     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')) { |             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) { |     getExceptionMessage(code) { | ||||||
|         const exceptionMessages = { |         const exceptionMessages = { | ||||||
|             1: 'Illegal Function', |             0x01: 'Illegal Function', | ||||||
|             2: 'Illegal Data Address', |             0x02: 'Illegal Data Address', | ||||||
|             3: 'Illegal Data Value', |             0x03: 'Illegal Data Value', | ||||||
|             4: 'Slave Device Failure', |             0x04: 'Slave Device Failure', | ||||||
|             5: 'Acknowledge', |             0x05: 'Acknowledge', | ||||||
|             6: 'Slave Device Busy', |             0x06: 'Slave Device Busy', | ||||||
|             8: 'Memory Parity Error', |             0x08: 'Memory Parity Error', | ||||||
|             10: 'Gateway Path Unavailable', |             0x0A: 'Gateway Path Unavailable', | ||||||
|             11: 'Gateway Target Device Failed to Respond' |             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) { |     parseRegisterValues(response, quantity) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user