24 Commits

Author SHA1 Message Date
855e023758 Refuse to use GA :-( 2024-10-12 15:34:48 +02:00
33299110a3 Identity update 2024-10-12 15:32:28 +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
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 122 additions and 42 deletions

View File

@@ -1,9 +1,12 @@
# 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).
# Usage in Firefox Browser # 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, 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. 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

@@ -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,23 +34,73 @@ 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() {
if (this.reader) { TRACE_ERROR("mbrtu", "Disconnect.");
await this.reader.releaseLock(); try {
} if (this.reader) {
if (this.writer) { await this.reader.cancel();
await this.writer.releaseLock(); await this.reader.releaseLock();
} this.reader = null;
if (this.port) { }
await this.port.close(); if (this.writer) {
console.log('Serial port closed'); await this.writer.releaseLock();
this.writer = null;
}
if (this.port) {
await this.port.close();
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) { 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);
@@ -76,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);
} }
@@ -112,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;
@@ -131,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;
} }
} }
})(), })(),
@@ -142,38 +192,36 @@ 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) {
await this.reader.cancel();
await this.reader.releaseLock();
this.reader = null;
this.reader = this.port.readable.getReader();
}
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 }; 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',

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