diff --git a/main.js b/main.js index 2b21e48..cad90fb 100644 --- a/main.js +++ b/main.js @@ -11,6 +11,7 @@ const { app, BrowserWindow, ipcMain } = require('electron') const path = require('path') const fs = require('fs') const { WitMotionSensor } = require('./sensor') +const { UsbSerialBridge } = require('./usb-serial-bridge') // 缓存文件路径(存储在应用用户数据目录下) const cacheDir = app.getPath('userData') @@ -60,6 +61,9 @@ const createWindow = () => { win.loadFile('./dist/index.html') + // 初始化 USB Serial 桥接模块 + const usbBridge = new UsbSerialBridge(win) + // 窗口创建后连接传感器 sensor = new WitMotionSensor({ onData: (data) => { @@ -94,4 +98,10 @@ app.whenReady().then(() => { }) createWindow() + + // 应用退出时清理 + app.on('before-quit', () => { + if (sensor) sensor.disconnect() + // usbBridge 的 cleanup 由窗口 close 事件触发 + }) }) diff --git a/preload.js b/preload.js index 97d7ba7..0d4eaed 100644 --- a/preload.js +++ b/preload.js @@ -102,7 +102,87 @@ const app = { } } +/** + * USB Serial JS 桥接对象 + * 静态页面可直接使用 usbSerial 或 window.usbSerial + * 接口规范: docs/webview-usb-serial.md + */ +const usbSerialCallbacks = { + onDeviceAttached: null, + onDeviceDetached: null, + onDeviceDiscovered: null, + onDeviceProbeResult: null, + onDeviceOpened: null, + onDeviceState: null, + onDeviceData: null +} + +// 监听主进程发送的 usb 事件,转换为回调调用 +function setupUsbSerialListeners() { + const events = [ + ['usb-callback-device-attached', 'onDeviceAttached'], + ['usb-callback-device-detached', 'onDeviceDetached'], + ['usb-callback-device-discovered', 'onDeviceDiscovered'], + ['usb-callback-device-probe-result', 'onDeviceProbeResult'], + ['usb-callback-device-opened', 'onDeviceOpened'], + ['usb-callback-device-state', 'onDeviceState'], + ['usb-callback-device-data', 'onDeviceData'] + ] + + for (const [eventName, cbName] of events) { + ipcRenderer.on(eventName, (_event, jsonStr) => { + const cb = usbSerialCallbacks[cbName] + if (typeof cb === 'function') { + cb(jsonStr) + } + }) + } +} +setupUsbSerialListeners() + +const usbSerial = { + // --- 回调属性(可读写)--- + get onDeviceAttached() { return usbSerialCallbacks.onDeviceAttached }, + set onDeviceAttached(fn) { usbSerialCallbacks.onDeviceAttached = fn }, + get onDeviceDetached() { return usbSerialCallbacks.onDeviceDetached }, + set onDeviceDetached(fn) { usbSerialCallbacks.onDeviceDetached = fn }, + get onDeviceDiscovered() { return usbSerialCallbacks.onDeviceDiscovered }, + set onDeviceDiscovered(fn) { usbSerialCallbacks.onDeviceDiscovered = fn }, + get onDeviceProbeResult() { return usbSerialCallbacks.onDeviceProbeResult }, + set onDeviceProbeResult(fn) { usbSerialCallbacks.onDeviceProbeResult = fn }, + get onDeviceOpened() { return usbSerialCallbacks.onDeviceOpened }, + set onDeviceOpened(fn) { usbSerialCallbacks.onDeviceOpened = fn }, + get onDeviceState() { return usbSerialCallbacks.onDeviceState }, + set onDeviceState(fn) { usbSerialCallbacks.onDeviceState = fn }, + get onDeviceData() { return usbSerialCallbacks.onDeviceData }, + set onDeviceData(fn) { usbSerialCallbacks.onDeviceData = fn }, + + // --- 方法 --- + startDiscovery() { + ipcRenderer.send('usb-start-discovery') + }, + stopDiscovery() { + ipcRenderer.send('usb-stop-discovery') + }, + getDiscoveredDevices() { + return ipcRenderer.invoke('usb-get-discovered') + }, + deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) { + ipcRenderer.send('usb-device-probe', { deviceName, portNumber, baudRate, hexCmd, timeoutMs }) + }, + deviceOpen(deviceName, portNumber, baudRate) { + ipcRenderer.send('usb-device-open', { deviceName, portNumber, baudRate }) + }, + deviceClose(deviceName, portNumber) { + ipcRenderer.send('usb-device-close', { deviceName, portNumber }) + }, + deviceWrite(deviceName, portNumber, hexData) { + ipcRenderer.send('usb-device-write', { deviceName, portNumber, hexData }) + } +} + // 安全暴露 API 给渲染进程 contextBridge.exposeInMainWorld('net', networkUtil) contextBridge.exposeInMainWorld('appSensor', appSensor) contextBridge.exposeInMainWorld('app', app) +contextBridge.exposeInMainWorld('usbSerial', usbSerial) diff --git a/usb-serial-bridge.js b/usb-serial-bridge.js new file mode 100644 index 0000000..45f82a9 --- /dev/null +++ b/usb-serial-bridge.js @@ -0,0 +1,239 @@ +/** + * 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 }