/** * USB Serial JS 桥接模块 * 实现 USB 串口设备的发现、探测、连接及数据读写 * 接口规范: docs/webview-usb-serial.md */ const { SerialPort } = require('serialport') const { ipcMain } = require('electron') class UsbSerialBridge { constructor(mainWindow) { this.mainWindow = mainWindow this.activeConnections = new Map() this.discoveredDevices = new Set() this.activeProbes = new Map() this._setupIPC() } _emit(event, data) { if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.mainWindow.webContents.send(event, JSON.stringify(data)) } } _setupIPC() { ipcMain.on('usb-start-discovery', () => this._startDiscovery()) ipcMain.on('usb-stop-discovery', () => this._stopDiscovery()) ipcMain.handle('usb-get-discovered', () => this._getDiscoveredDevices()) ipcMain.on('usb-device-probe', (_e, p) => this._deviceProbe(p)) ipcMain.on('usb-device-open', (_e, p) => this._deviceOpen(p)) ipcMain.on('usb-device-close', (_e, p) => this._deviceClose(p)) ipcMain.on('usb-device-write', (_e, p) => this._deviceWrite(p)) } async _startDiscovery() { console.log('[usb] 开始发现串口设备...') try { const ports = await SerialPort.list() for (const p of ports) { if (!this.discoveredDevices.has(p.path)) { this.discoveredDevices.add(p.path) console.log('[usb] 发现设备:', p.path) this._emit('usb-callback-device-attached', { deviceName: p.path, deviceId: parseInt(p.vendorId || '0', 16) || 0, ports: [1] }) } } console.log('[usb] 发现完成, 共', ports.length, '个设备') } catch (err) { console.error('[usb] 发现失败:', err.message) } } _stopDiscovery() { console.log('[usb] 停止发现') } _getDiscoveredDevices() { return JSON.stringify([]) } async _deviceProbe({ deviceName, portNumber, baudRate, hexCmd, timeoutMs }) { const probeId = deviceName + ':' + portNumber console.log('[usb] 探测:', deviceName, 'port=', portNumber, 'baud=', baudRate) if (this.activeProbes.has(probeId)) { const old = this.activeProbes.get(probeId) clearTimeout(old.timeout) try { old.port.close() } catch {} this.activeProbes.delete(probeId) } try { const port = new SerialPort({ path: deviceName, baudRate: baudRate || 9600, dataBits: 8, parity: 'none', stopBits: 1, autoOpen: false, dtr: false, rts: false }) await new Promise((resolve, reject) => { port.open(err => err ? reject(new Error(err.message)) : resolve()) }) let responseHex = '' const timeout = setTimeout(() => { this.activeProbes.delete(probeId) try { port.close() } catch {} const success = responseHex.length > 0 console.log('[usb] 探测' + (success ? '成功' : '超时') + ':', deviceName, '响应=' + (responseHex || '无')) this._emit('usb-callback-device-probe-result', { deviceName, port: portNumber, baudRate, success, responseHex, error: success ? '' : 'No response' }) }, timeoutMs || 1000) port.on('data', (data) => { responseHex += data.toString('hex').toUpperCase() }) port.on('error', (err) => { console.error('[usb] 探测错误:', err.message) }) this.activeProbes.set(probeId, { timeout, port }) if (hexCmd) { const cmdBuf = Buffer.from(hexCmd, 'hex') port.write(cmdBuf, (err) => { if (err) { clearTimeout(timeout) this.activeProbes.delete(probeId) try { port.close() } catch {} this._emit('usb-callback-device-probe-result', { deviceName, port: portNumber, baudRate, success: false, responseHex: '', error: err.message }) } else { console.log('[usb] 已发送探测命令:', hexCmd) } }) } else { console.log('[usb] 不发送探测命令, 等待自动上报...') } } catch (err) { console.error('[usb] 探测失败:', err.message) this._emit('usb-callback-device-probe-result', { deviceName, port: portNumber, baudRate, success: false, responseHex: '', error: err.message }) } } async _deviceOpen({ deviceName, portNumber, baudRate }) { const connKey = deviceName + ':' + portNumber console.log('[usb] 打开:', connKey, 'baud=', baudRate) if (this.activeConnections.has(connKey)) { try { this.activeConnections.get(connKey).port.close() } catch {} this.activeConnections.delete(connKey) } try { const port = new SerialPort({ path: deviceName, baudRate: baudRate || 9600, dataBits: 8, parity: 'none', stopBits: 1, autoOpen: false, dtr: false, rts: false }) await new Promise((resolve, reject) => { port.open(err => err ? reject(new Error(err.message)) : resolve()) }) const entry = { port, deviceName, portNumber } this.activeConnections.set(connKey, entry) console.log('[usb] 打开成功:', connKey) this._emit('usb-callback-device-opened', { deviceName, port: portNumber, success: true, error: '' }) this._emit('usb-callback-device-state', { deviceName, port: portNumber, state: 'CONNECTED' }) port.on('data', (data) => { this._emit('usb-callback-device-data', { deviceName, port: portNumber, hexData: data.toString('hex').toUpperCase() }) }) port.on('error', (err) => { console.error('[usb] 连接错误:', err.message) }) port.on('close', () => { console.log('[usb] 连接关闭:', connKey) this.activeConnections.delete(connKey) this._emit('usb-callback-device-state', { deviceName, port: portNumber, state: 'DISCONNECTED' }) }) } catch (err) { console.error('[usb] 打开失败:', err.message) this._emit('usb-callback-device-opened', { deviceName, port: portNumber, success: false, error: err.message }) } } _deviceClose({ deviceName, portNumber }) { const connKey = deviceName + ':' + portNumber console.log('[usb] 关闭:', connKey) if (this.activeConnections.has(connKey)) { const entry = this.activeConnections.get(connKey) try { entry.port.close() } catch {} this.activeConnections.delete(connKey) } } _deviceWrite({ deviceName, portNumber, hexData }) { const connKey = deviceName + ':' + portNumber console.log('[usb] 写入:', connKey, 'hex=' + hexData) if (!this.activeConnections.has(connKey)) { console.error('[usb] 写入失败:', connKey, '未连接') return } const entry = this.activeConnections.get(connKey) const buf = Buffer.from(hexData, 'hex') entry.port.write(buf, (err) => { if (err) { console.error('[usb] 写入错误:', err.message) } }) } disconnectAll() { for (const [key, entry] of this.activeConnections) { console.log('[usb] 断开:', key) try { entry.port.close() } catch {} } this.activeConnections.clear() } } module.exports = { UsbSerialBridge }