UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

155 lines (139 loc) 4.53 kB
// 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 }