Merge pull request #2 from Pablo2048/development

Correctly handle disconnect & reconnect events
This commit is contained in:
2024-10-11 15:29:19 +02:00
committed by GitHub
3 changed files with 76 additions and 35 deletions

View File

@@ -1,5 +1,8 @@
# Modbus-RTU-Master # 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. 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. 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).

View File

@@ -55,12 +55,16 @@
<option value="odd">Odd</option> <option value="odd">Odd</option>
</select> </select>
<label for="timeout">Timeout (ms):</label> <label for="timeout">Timeout (ms):</label>
<input type="number" id="timeout" value="150"> <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: <span id="value">N/A</span></div>
<div id="error-message"></div> <div id="error-message"></div>
<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 src="../src/modbus-rtu-master.js"></script>
<script> <script>
let modbus; let modbus;
@@ -92,12 +96,12 @@
valueDisplay.textContent = registerValue; valueDisplay.textContent = registerValue;
errorDisplay.textContent = ''; // Vymazat případné staré chyby errorDisplay.textContent = ''; // Vymazat případné staré chyby
} else { } else {
console.error('Neplatná odpověď:', values); TRACE_ERROR("", 'Neplatná odpověď:', values);
valueDisplay.textContent = 'Error'; valueDisplay.textContent = 'Error';
errorDisplay.textContent = 'Invalid response received.'; errorDisplay.textContent = 'Invalid response received.';
} }
} catch (error) { } catch (error) {
console.error('Chyba při čtení registru:', error); TRACE_ERROR("", '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.';
} }
@@ -112,6 +116,7 @@
intervalId = setInterval(updateRegisterValue, 5000); // Každých 5 sekund intervalId = setInterval(updateRegisterValue, 5000); // Každých 5 sekund
} }
}); });
TRACE_INFO("", "Starting...");
</script> </script>
</body> </body>
</html> </html>

View File

@@ -9,21 +9,23 @@ class ModbusRTUMaster {
this.parity = config.parity || 'none'; this.parity = config.parity || 'none';
this.flowControl = config.flowControl || 'none'; this.flowControl = config.flowControl || 'none';
this.timeout = config.timeout || 2000; // Default timeout is 2 seconds 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() { async connect() {
try { try {
this.port = await navigator.serial.requestPort(); this.port = await navigator.serial.requestPort();
await this.port.open({ await this.openPort();
baudRate: this.baudRate, TRACE_VERBOSE("mbrtu", 'Connected to serial port with configuration:', {
dataBits: this.dataBits,
stopBits: this.stopBits,
parity: this.parity,
flowControl: this.flowControl
});
this.reader = this.port.readable.getReader();
this.writer = this.port.writable.getWriter();
console.log('Connected to serial port with configuration:', {
baudRate: this.baudRate, baudRate: this.baudRate,
dataBits: this.dataBits, dataBits: this.dataBits,
stopBits: this.stopBits, stopBits: this.stopBits,
@@ -32,12 +34,24 @@ class ModbusRTUMaster {
timeout: this.timeout timeout: this.timeout
}); });
} catch (error) { } catch (error) {
console.error('Failed to open serial port:', error); TRACE_ERROR("mbrtu", 'Failed to open serial port:', error);
} }
} }
async openPort() {
await this.port.open({
baudRate: this.baudRate,
dataBits: this.dataBits,
stopBits: this.stopBits,
parity: this.parity,
flowControl: this.flowControl
});
this.reader = this.port.readable.getReader();
this.writer = this.port.writable.getWriter();
}
async disconnect() { async disconnect() {
console.log('Disconnect requested'); TRACE_ERROR("mbrtu", "Disconnect.");
try { try {
if (this.reader) { if (this.reader) {
await this.reader.cancel(); await this.reader.cancel();
@@ -51,13 +65,42 @@ class ModbusRTUMaster {
if (this.port) { if (this.port) {
await this.port.close(); await this.port.close();
this.port = null; this.port = null;
console.log('Serial port closed'); TRACE_INFO("mbrtu", 'Serial port closed');
} }
} catch (error) { } catch (error) {
console.error('Error during disconnect:', 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) { async readHoldingRegisters(slaveId, startAddress, quantity) {
const response = await this.readRegisters(slaveId, 0x03, startAddress, quantity); const response = await this.readRegisters(slaveId, 0x03, startAddress, quantity);
return this.parseRegisterValues(response, quantity); return this.parseRegisterValues(response, quantity);
@@ -85,6 +128,7 @@ class ModbusRTUMaster {
} }
const request = this.buildRequest(slaveId, functionCode, startAddress, quantity); const request = this.buildRequest(slaveId, functionCode, startAddress, quantity);
TRACE_VERBOSE("mbrtu", "Send request");
await this.sendRequest(request); await this.sendRequest(request);
return await this.receiveResponse(slaveId, functionCode, quantity); return await this.receiveResponse(slaveId, functionCode, quantity);
} }
@@ -121,12 +165,10 @@ class ModbusRTUMaster {
async sendRequest(request) { async sendRequest(request) {
await this.writer.write(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) { async receiveResponse(slaveId, functionCode, quantity) {
// Set the expected length of the response (slaveId, functionCode, byte count, data, CRC)
let expectedLength = 5 + quantity * 2; let expectedLength = 5 + quantity * 2;
let response = new Uint8Array(expectedLength); let response = new Uint8Array(expectedLength);
let index = 0; let index = 0;
@@ -140,9 +182,8 @@ class ModbusRTUMaster {
response.set(value, index); response.set(value, index);
index += value.length; index += value.length;
// 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)) { if (index >= 2 && (response[1] & 0x80)) {
expectedLength = 5; // Override expected length in case of an exception expectedLength = 5;
} }
} }
})(), })(),
@@ -151,25 +192,23 @@ class ModbusRTUMaster {
) )
]); ]);
// Modbus Exception Codes processing if (response[1] & 0x80) {
if (response[1] & 0x80) { // Check if the highest bit of the function code is set
const exceptionCode = response[2]; const exceptionCode = response[2];
throw new Error(`Modbus Exception Code: ${this.getExceptionMessage(exceptionCode)} (Code: ${exceptionCode})`); throw new Error(`Modbus Exception Code: ${this.getExceptionMessage(exceptionCode)} (Code: ${exceptionCode})`);
} }
// Check CRC after receiving the full response
const dataWithoutCRC = response.slice(0, index - 2); const dataWithoutCRC = response.slice(0, index - 2);
const receivedCRC = (response[index - 1] << 8) | response[index - 2]; const receivedCRC = (response[index - 1] << 8) | response[index - 2];
const calculatedCRC = this.calculateCRC(dataWithoutCRC); const calculatedCRC = this.calculateCRC(dataWithoutCRC);
if (calculatedCRC === receivedCRC) { if (calculatedCRC === receivedCRC) {
//console.log('Received response with valid CRC:', response.slice(0, index)); TRACE_VERBOSE("mbrtu", 'Received response with valid CRC:', response.slice(0, index));
return response.slice(0, index); return response.slice(0, index);
} 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}.`);
} }
} catch (error) { } catch (error) {
console.error('Error receiving response:', error.message); TRACE_ERROR("mbrtu", 'Error receiving response:', error.message);
if (this.reader) { if (this.reader) {
await this.reader.cancel(); await this.reader.cancel();
await this.reader.releaseLock(); await this.reader.releaseLock();
@@ -179,16 +218,10 @@ class ModbusRTUMaster {
if (error.message.includes('Device has been lost')) { if (error.message.includes('Device has been lost')) {
await this.handleDeviceLost(); await this.handleDeviceLost();
} }
return { error: error.message }; // TODO: or throw error; ? throw error;
} }
} }
async handleDeviceLost() {
console.warn('Attempting to reconnect...');
await this.disconnect();
await this.connect();
}
getExceptionMessage(code) { getExceptionMessage(code) {
const exceptionMessages = { const exceptionMessages = {
1: 'Illegal Function', 1: 'Illegal Function',