modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
126 lines (113 loc) • 4.19 kB
JavaScript
const net = require('net');
const {
concatUint8Arrays,
sliceUint8Array,
allocUint8Array,
isUint8Array
} = require('../utils/utils.js');
const logger = require('../logger.js');
class TcpSerialTransport {
constructor(host, options = {}) {
this.host = host;
this.portNumber = options.port || 23; // стандартный TCP порт
this.options = {
readTimeout: 100,
writeTimeout: 100,
reconnectInterval: 3000,
maxReconnectAttempts: Infinity,
...options
};
this.socket = null;
this.readBuffer = allocUint8Array(0);
this.isOpen = false;
this._reconnectAttempts = 0;
this._shouldReconnect = true;
}
async connect() {
return new Promise((resolve, reject) => {
this.socket = new net.Socket();
this.socket.connect(this.portNumber, this.host, () => {
this.isOpen = true;
this._reconnectAttempts = 0;
logger.info(`TCP connection to ${this.host}:${this.portNumber} established`);
resolve();
});
this.socket.on('data', this._onData.bind(this));
this.socket.on('error', this._onError.bind(this));
this.socket.on('close', this._onClose.bind(this));
});
}
_onData(data) {
const chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
this.readBuffer = concatUint8Arrays([this.readBuffer, chunk]);
}
_onError(err) {
logger.error(`TCP socket error ${this.host}:${this.portNumber}: ${err.message}`);
}
_onClose(hadError) {
logger.warn(`TCP connection closed ${this.host}:${this.portNumber}`);
this.isOpen = false;
if (this._shouldReconnect) {
this._scheduleReconnect();
}
}
_scheduleReconnect() {
if (this._reconnectAttempts >= this.options.maxReconnectAttempts) {
logger.error(`Max reconnect attempts reached for TCP ${this.host}:${this.portNumber}`);
return;
}
this._reconnectAttempts++;
logger.info(`Reconnecting to TCP ${this.host}:${this.portNumber} in ${this.options.reconnectInterval} ms`);
setTimeout(() => {
this.connect().catch(err => {
logger.warn(`Reconnect attempt failed: ${err.message}`);
});
}, this.options.reconnectInterval);
}
async write(buffer) {
if (!this.isOpen) {
throw new Error('TCP socket is closed');
}
return new Promise((resolve, reject) => {
this.socket.write(buffer, err => {
if (err) {
logger.error(`Write error on TCP ${this.host}:${this.portNumber}: ${err.message}`);
return reject(err);
}
resolve();
});
});
}
async read(length, timeout = this.options.readTimeout) {
const start = Date.now();
return new Promise((resolve, reject) => {
const checkData = () => {
if (this.readBuffer.length >= length) {
const data = sliceUint8Array(this.readBuffer, 0, length);
this.readBuffer = sliceUint8Array(this.readBuffer, length);
logger.debug(`Read ${length} bytes from TCP ${this.host}:${this.portNumber}`);
return resolve(data);
}
if (Date.now() - start > timeout) {
logger.warn(`Read timeout on TCP ${this.host}:${this.portNumber}`);
return reject(new Error('Read timeout'));
}
setTimeout(checkData, 10);
};
checkData();
});
}
async disconnect() {
this._shouldReconnect = false;
if (!this.isOpen) return;
return new Promise((resolve, reject) => {
this.socket.end(() => {
this.isOpen = false;
logger.info(`TCP connection to ${this.host}:${this.portNumber} closed`);
resolve();
});
this.socket.destroy(); // ensure it's closed
});
}
}
module.exports = { TcpSerialTransport }