You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
7.3 KiB
239 lines
7.3 KiB
/** |
|
* 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 }
|
|
|