3 changed files with 329 additions and 0 deletions
@ -0,0 +1,239 @@
@@ -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