3 changed files with 329 additions and 0 deletions
@ -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 } |
||||||
Loading…
Reference in new issue