modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
155 lines (139 loc) • 4.53 kB
JavaScript
// transport/node-serialport.js
const { SerialPort } = require("serialport");
const {
concatUint8Arrays,
sliceUint8Array,
allocUint8Array,
isUint8Array
} = require('../utils/utils.js');
const logger = require('../logger.js');
class NodeSerialTransport {
constructor(port, options = {}) {
this.path = port;
this.options = {
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: 'none',
readTimeout: 1000,
writeTimeout: 1000,
reconnectInterval: 3000, // ms
maxReconnectAttempts: Infinity, // or some number
...options
};
this.port = null;
this.readBuffer = allocUint8Array(0);
this.isOpen = false;
this._reconnectAttempts = 0;
this._shouldReconnect = true;
}
async connect() {
return new Promise((resolve, reject) => {
this.port = new SerialPort({
path: this.path,
baudRate: this.options.baudRate,
dataBits: this.options.dataBits,
stopBits: this.options.stopBits,
parity: this.options.parity,
autoOpen: false
});
this.port.open(err => {
if (err) {
logger.error(`Failed to open serial port ${this.path}: ${err.message}`);
this._scheduleReconnect();
return reject(err);
}
this.isOpen = true;
this._reconnectAttempts = 0;
this.port.on('data', this._onData.bind(this));
this.port.on('error', this._onError.bind(this));
this.port.on('close', this._onClose.bind(this));
logger.info(`Serial port ${this.path} opened`);
resolve();
});
});
}
_onData(data) {
const chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
this.readBuffer = concatUint8Arrays([this.readBuffer, chunk]);
}
_onError(err) {
logger.error(`Serial port ${this.path} error: ${err.message}`);
}
_onClose() {
logger.warn(`Serial port ${this.path} closed`);
this.isOpen = false;
if (this._shouldReconnect) {
this._scheduleReconnect();
}
}
_scheduleReconnect() {
if (this._reconnectAttempts >= this.options.maxReconnectAttempts) {
logger.error(`Max reconnect attempts reached for ${this.path}`);
return;
}
this._reconnectAttempts++;
logger.info(`Reconnecting to ${this.path} in ${this.options.reconnectInterval} ms (attempt ${this._reconnectAttempts})`);
setTimeout(() => {
this.connect().catch(err => {
logger.warn(`Reconnect attempt failed: ${err.message}`);
});
}, this.options.reconnectInterval);
}
async write(buffer) {
if (!this.isOpen) {
logger.warn(`Write attempted on closed port ${this.path}`);
throw new Error('Port is closed');
}
return new Promise((resolve, reject) => {
this.port.write(buffer, err => {
if (err) {
logger.error(`Write error on port ${this.path}: ${err.message}`);
return reject(err);
}
this.port.drain(drainErr => {
if (drainErr) {
logger.error(`Drain error on port ${this.path}: ${drainErr.message}`);
return reject(drainErr);
}
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 ${this.path}`);
return resolve(data);
}
if (Date.now() - start > timeout) {
logger.warn(`Read timeout on ${this.path}`);
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.port.close(err => {
if (err) {
logger.error(`Error closing port ${this.path}: ${err.message}`);
return reject(err);
}
this.isOpen = false;
logger.info(`Serial port ${this.path} closed`);
resolve();
});
});
}
}
module.exports = { NodeSerialTransport }