mirror of
https://github.com/Pablo2048/modbus-rtu-master.git
synced 2025-11-01 00:18:35 +01:00
First commit
This commit is contained in:
211
src/modbus-rtu-master.js
Normal file
211
src/modbus-rtu-master.js
Normal file
@@ -0,0 +1,211 @@
|
||||
class ModbusRTUMaster {
|
||||
constructor(config = {}) {
|
||||
this.port = null;
|
||||
this.reader = null;
|
||||
this.writer = null;
|
||||
this.baudRate = config.baudRate || 9600;
|
||||
this.dataBits = config.dataBits || 8;
|
||||
this.stopBits = config.stopBits || 1;
|
||||
this.parity = config.parity || 'none';
|
||||
this.flowControl = config.flowControl || 'none';
|
||||
this.timeout = config.timeout || 2000; // Default timeout is 2 seconds
|
||||
}
|
||||
|
||||
async connect() {
|
||||
try {
|
||||
this.port = await navigator.serial.requestPort();
|
||||
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();
|
||||
console.log('Connected to serial port with configuration:', {
|
||||
baudRate: this.baudRate,
|
||||
dataBits: this.dataBits,
|
||||
stopBits: this.stopBits,
|
||||
parity: this.parity,
|
||||
flowControl: this.flowControl,
|
||||
timeout: this.timeout
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to open serial port:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this.reader) {
|
||||
await this.reader.releaseLock();
|
||||
}
|
||||
if (this.writer) {
|
||||
await this.writer.releaseLock();
|
||||
}
|
||||
if (this.port) {
|
||||
await this.port.close();
|
||||
console.log('Serial port closed');
|
||||
}
|
||||
}
|
||||
|
||||
async readHoldingRegisters(slaveId, startAddress, quantity) {
|
||||
const response = await this.readRegisters(slaveId, 0x03, startAddress, quantity);
|
||||
return this.parseRegisterValues(response, quantity);
|
||||
}
|
||||
|
||||
async readInputRegisters(slaveId, startAddress, quantity) {
|
||||
const response = await this.readRegisters(slaveId, 0x04, startAddress, quantity);
|
||||
return this.parseRegisterValues(response, quantity);
|
||||
}
|
||||
|
||||
async readCoils(slaveId, startAddress, quantity) {
|
||||
const response = await this.readRegisters(slaveId, 0x01, startAddress, quantity);
|
||||
return this.parseCoilValues(response, quantity);
|
||||
}
|
||||
|
||||
async readDiscreteInputs(slaveId, startAddress, quantity) {
|
||||
const response = await this.readRegisters(slaveId, 0x02, startAddress, quantity);
|
||||
return this.parseCoilValues(response, quantity);
|
||||
}
|
||||
|
||||
async readRegisters(slaveId, functionCode, startAddress, quantity) {
|
||||
const frameLength = 8; // 8 bytes for reading (slave ID, function code, start address, quantity, CRC)
|
||||
if (frameLength > 250) {
|
||||
throw new Error('Read frame length exceeded: frame too long.');
|
||||
}
|
||||
|
||||
const request = this.buildRequest(slaveId, functionCode, startAddress, quantity);
|
||||
await this.sendRequest(request);
|
||||
return await this.receiveResponse(slaveId, functionCode, quantity);
|
||||
}
|
||||
|
||||
buildRequest(slaveId, functionCode, address, quantity) {
|
||||
const request = new Uint8Array(8);
|
||||
request[0] = slaveId;
|
||||
request[1] = functionCode;
|
||||
request[2] = (address >> 8) & 0xFF;
|
||||
request[3] = address & 0xFF;
|
||||
request[4] = (quantity >> 8) & 0xFF;
|
||||
request[5] = quantity & 0xFF;
|
||||
const crc = this.calculateCRC(request.subarray(0, 6));
|
||||
request[6] = crc & 0xFF;
|
||||
request[7] = (crc >> 8) & 0xFF;
|
||||
return request;
|
||||
}
|
||||
|
||||
calculateCRC(buffer) {
|
||||
let crc = 0xFFFF;
|
||||
for (let pos = 0; pos < buffer.length; pos++) {
|
||||
crc ^= buffer[pos];
|
||||
for (let i = 8; i !== 0; i--) {
|
||||
if ((crc & 0x0001) !== 0) {
|
||||
crc >>= 1;
|
||||
crc ^= 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
async sendRequest(request) {
|
||||
await this.writer.write(request);
|
||||
//console.log('Request sent:', request);
|
||||
}
|
||||
|
||||
// Receiving and validating response with timeout detection and Modbus Exception Codes processing
|
||||
async receiveResponse(slaveId, functionCode, quantity) {
|
||||
// Set the expected length of the response (slaveId, functionCode, byte count, data, CRC)
|
||||
let expectedLength = 5 + quantity * 2;
|
||||
let response = new Uint8Array(expectedLength);
|
||||
let index = 0;
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
while (index < expectedLength) {
|
||||
const { value, done } = await this.reader.read();
|
||||
if (done) throw new Error('Device has been lost');
|
||||
response.set(value, index);
|
||||
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)) {
|
||||
expectedLength = 5; // Override expected length in case of an exception
|
||||
}
|
||||
}
|
||||
})(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout: No response received within the time limit.')), this.timeout)
|
||||
)
|
||||
]);
|
||||
|
||||
// Modbus Exception Codes processing
|
||||
if (response[1] & 0x80) { // Check if the highest bit of the function code is set
|
||||
const exceptionCode = response[2];
|
||||
throw new Error(`Modbus Exception Code: ${this.getExceptionMessage(exceptionCode)} (Code: ${exceptionCode})`);
|
||||
}
|
||||
|
||||
// Check CRC after receiving the full response
|
||||
const dataWithoutCRC = response.slice(0, index - 2);
|
||||
const receivedCRC = (response[index - 1] << 8) | response[index - 2];
|
||||
const calculatedCRC = this.calculateCRC(dataWithoutCRC);
|
||||
|
||||
if (calculatedCRC === receivedCRC) {
|
||||
//console.log('Received response with valid CRC:', response.slice(0, index));
|
||||
return response.slice(0, index);
|
||||
} else {
|
||||
throw new Error(`CRC Error: Calculated CRC ${calculatedCRC} does not match received CRC ${receivedCRC}.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error receiving response:', error.message);
|
||||
if (error.message.includes('Device has been lost')) {
|
||||
await this.handleDeviceLost();
|
||||
}
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeviceLost() {
|
||||
console.warn('Attempting to reconnect...');
|
||||
await this.disconnect();
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
getExceptionMessage(code) {
|
||||
const exceptionMessages = {
|
||||
1: 'Illegal Function',
|
||||
2: 'Illegal Data Address',
|
||||
3: 'Illegal Data Value',
|
||||
4: 'Slave Device Failure',
|
||||
5: 'Acknowledge',
|
||||
6: 'Slave Device Busy',
|
||||
8: 'Memory Parity Error',
|
||||
10: 'Gateway Path Unavailable',
|
||||
11: 'Gateway Target Device Failed to Respond'
|
||||
};
|
||||
return exceptionMessages[code] || 'Unknown Error';
|
||||
}
|
||||
|
||||
parseRegisterValues(response, quantity) {
|
||||
const values = [];
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const value = (response[3 + i * 2] << 8) | response[4 + i * 2];
|
||||
values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
parseCoilValues(response, quantity) {
|
||||
const values = [];
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const byteIndex = 3 + Math.floor(i / 8);
|
||||
const bitIndex = i % 8;
|
||||
const value = (response[byteIndex] & (1 << bitIndex)) !== 0 ? 1 : 0;
|
||||
values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user