7 changed files with 947 additions and 378 deletions
@ -0,0 +1,325 @@
@@ -0,0 +1,325 @@
|
||||
const { |
||||
buildReadRegisters, |
||||
checkCrc, |
||||
parseRegisterValue, |
||||
probes, |
||||
runProbe, |
||||
serialRequest, |
||||
toInt32 |
||||
} = require('./serial-protocols') |
||||
|
||||
const SENSOR_META = { |
||||
HWT6053: { |
||||
code: '1', |
||||
name: 'HWT6053', |
||||
baudRate: 9600, |
||||
operations: [ |
||||
['read-version', '获取版本号'], |
||||
['read-temp', '获取温度'], |
||||
['read-euler', '获取角度'], |
||||
['reset-euler', '重置角度传感器基准'], |
||||
['read-heading-and-vertical', '获取朝向和垂直夹角'] |
||||
] |
||||
}, |
||||
TOF5000: { |
||||
code: '2', |
||||
name: 'TOF5000', |
||||
baudRate: 115200, |
||||
operations: [ |
||||
['read-speed', '读取速度'], |
||||
['read-range', '读取距离'] |
||||
] |
||||
}, |
||||
CHC_CGI_430: { |
||||
code: '3', |
||||
name: 'CHC_CGI_430', |
||||
baudRate: 115200, |
||||
operations: [ |
||||
['read-rtk', '读取RTK数据'] |
||||
] |
||||
}, |
||||
SW_LDS20DA: { |
||||
code: '6', |
||||
name: 'SW_LDS20DA', |
||||
baudRate: 9600, |
||||
operations: [ |
||||
['read-status', '读取状态'], |
||||
['read-range', '读取距离'] |
||||
] |
||||
} |
||||
} |
||||
|
||||
const REQUIRED_METHODS = [ |
||||
['CHC_CGI_430', 'read-rtk'], |
||||
['TOF5000', 'read-speed'], |
||||
['TOF5000', 'read-range'], |
||||
['HWT6053', 'read-version'], |
||||
['HWT6053', 'read-euler'], |
||||
['HWT6053', 'read-heading-and-vertical'], |
||||
['SW_LDS20DA', 'read-status'], |
||||
['SW_LDS20DA', 'read-range'] |
||||
] |
||||
|
||||
function sensorId(sensorName, portPath) { |
||||
return `${sensorName}:${portPath}` |
||||
} |
||||
|
||||
function makeOperations(device) { |
||||
const meta = SENSOR_META[device.sensor] |
||||
return (meta?.operations || []).map(([id, name]) => ({ |
||||
id, |
||||
name, |
||||
sensorId: device.id |
||||
})) |
||||
} |
||||
|
||||
function parseHwtEuler(frame) { |
||||
if (!checkCrc(frame) || frame.length < 17) return null |
||||
const data = frame.slice(3, -2) |
||||
const shorts = [] |
||||
for (let i = 0; i < data.length; i += 2) { |
||||
shorts.push(((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF)) |
||||
} |
||||
if (shorts.length < 6) return null |
||||
const roll = toInt32(shorts[1], shorts[0]) |
||||
const pitch = toInt32(shorts[3], shorts[2]) |
||||
const yaw = toInt32(shorts[5], shorts[4]) |
||||
return { |
||||
roll: +(roll / 1000).toFixed(2), |
||||
pitch: +(pitch / 1000).toFixed(2), |
||||
yaw: +(yaw / 1000).toFixed(2) |
||||
} |
||||
} |
||||
|
||||
function verticalFromEuler(euler) { |
||||
if (!euler) return null |
||||
const rollRadians = euler.roll * Math.PI / 180 |
||||
const pitchRadians = euler.pitch * Math.PI / 180 |
||||
const vertical = Math.acos(Math.cos(pitchRadians) * Math.cos(rollRadians)) * 180 / Math.PI |
||||
return { |
||||
vertical, |
||||
heading: euler.yaw |
||||
} |
||||
} |
||||
|
||||
async function readRegisterValue(path, baudRate, slaveAddress, action, registerAddress, timeoutMs = 300) { |
||||
const frame = await serialRequest({ |
||||
path, |
||||
baudRate, |
||||
write: buildReadRegisters(slaveAddress, action, registerAddress, 1), |
||||
timeoutMs, |
||||
minBytes: 7 |
||||
}) |
||||
return parseRegisterValue(frame) |
||||
} |
||||
|
||||
async function readHwtEuler(path) { |
||||
const frame = await serialRequest({ |
||||
path, |
||||
baudRate: 9600, |
||||
write: buildReadRegisters(0x50, 0x03, 0x3D, 6), |
||||
timeoutMs: 300, |
||||
minBytes: 17 |
||||
}) |
||||
return parseHwtEuler(frame) |
||||
} |
||||
|
||||
class SensorController { |
||||
constructor({ onValue, onDeviceAttached, onDeviceDetached, onLog } = {}) { |
||||
this.onValue = onValue || (() => {}) |
||||
this.onDeviceAttached = onDeviceAttached || (() => {}) |
||||
this.onDeviceDetached = onDeviceDetached || (() => {}) |
||||
this.onLog = onLog || (() => {}) |
||||
this.devices = [] |
||||
this.lastValues = new Map() |
||||
this.isOpen = false |
||||
this.timers = new Set() |
||||
this.discoveryRunning = false |
||||
} |
||||
|
||||
log(message) { |
||||
this.onLog('info', message) |
||||
} |
||||
|
||||
async refresh(portList = null) { |
||||
const { SerialPort } = require('./serial-protocols') |
||||
const ports = portList || await SerialPort.list() |
||||
const found = [] |
||||
|
||||
for (const portInfo of ports) { |
||||
const portPath = portInfo.path |
||||
if (!portPath) continue |
||||
for (const probe of probes) { |
||||
try { |
||||
const result = await runProbe(portPath, probe) |
||||
if (!result) continue |
||||
const meta = SENSOR_META[result.probeId] |
||||
if (!meta) continue |
||||
const device = { |
||||
id: sensorId(result.probeId, portPath), |
||||
code: meta.code, |
||||
sensor: result.probeId, |
||||
name: meta.name, |
||||
port: portPath, |
||||
baudRate: result.baudRate, |
||||
isOpen: this.isOpen, |
||||
operations: [] |
||||
} |
||||
device.operations = makeOperations(device) |
||||
found.push(device) |
||||
break |
||||
} catch (err) { |
||||
this.log(`probe ${probe.id} on ${portPath} failed: ${err.message}`) |
||||
} |
||||
} |
||||
} |
||||
|
||||
const oldIds = new Set(this.devices.map(d => d.id)) |
||||
const newIds = new Set(found.map(d => d.id)) |
||||
for (const device of found) { |
||||
if (!oldIds.has(device.id)) this.onDeviceAttached(device) |
||||
} |
||||
for (const device of this.devices) { |
||||
if (!newIds.has(device.id)) this.onDeviceDetached(device) |
||||
} |
||||
this.devices = found |
||||
return this.devices |
||||
} |
||||
|
||||
list() { |
||||
return this.devices.map(device => ({ |
||||
...device, |
||||
isOpen: this.isOpen, |
||||
operations: makeOperations(device) |
||||
})) |
||||
} |
||||
|
||||
async initAll() { |
||||
await this.refresh() |
||||
return true |
||||
} |
||||
|
||||
init(id) { |
||||
return this.devices.some(device => device.id === id) |
||||
} |
||||
|
||||
async open() { |
||||
if (this.isOpen) return true |
||||
this.isOpen = true |
||||
if (this.devices.length === 0) await this.refresh() |
||||
this.startPolling() |
||||
return true |
||||
} |
||||
|
||||
close() { |
||||
this.isOpen = false |
||||
for (const timer of this.timers) clearTimeout(timer) |
||||
this.timers.clear() |
||||
return true |
||||
} |
||||
|
||||
async operation(id, opId, args = '') { |
||||
const device = this.devices.find(d => d.id === id) || |
||||
this.devices.find(d => d.sensor === id) || |
||||
this.devices.find(d => d.sensor === String(id).split(':')[0]) |
||||
if (!device) return null |
||||
const value = await this.readDeviceMethod(device, opId, args) |
||||
if (value != null && value !== '') { |
||||
this.lastValues.set(`${device.sensor}:${opId}`, value) |
||||
} |
||||
return value |
||||
} |
||||
|
||||
startPolling() { |
||||
for (const [sensorName, method] of REQUIRED_METHODS) { |
||||
this.scheduleMethod(sensorName, method) |
||||
} |
||||
} |
||||
|
||||
scheduleMethod(sensorName, method) { |
||||
const tick = async () => { |
||||
if (!this.isOpen) return |
||||
try { |
||||
const device = this.devices.find(d => d.sensor === sensorName) |
||||
if (device) { |
||||
const value = await this.readDeviceMethod(device, method, '') |
||||
if (value != null && value !== '') { |
||||
this.lastValues.set(`${sensorName}:${method}`, value) |
||||
this.onValue({ sensor: sensorName, method, value }) |
||||
} |
||||
} |
||||
} catch (err) { |
||||
this.log(`read ${sensorName}.${method} failed: ${err.message}`) |
||||
} finally { |
||||
if (this.isOpen) { |
||||
const timer = setTimeout(tick, 500) |
||||
this.timers.add(timer) |
||||
} |
||||
} |
||||
} |
||||
const timer = setTimeout(tick, 100) |
||||
this.timers.add(timer) |
||||
} |
||||
|
||||
async readDeviceMethod(device, method) { |
||||
if (device.sensor === 'TOF5000') { |
||||
if (method === 'read-speed') { |
||||
const value = await readRegisterValue(device.port, 115200, 0x01, 0x03, 0x00) |
||||
return value && value !== 0 ? String(value) : '' |
||||
} |
||||
if (method === 'read-range') { |
||||
const value = await readRegisterValue(device.port, 115200, 0x01, 0x04, 0x00) |
||||
return value && value !== 0 ? String(value) : '' |
||||
} |
||||
} |
||||
|
||||
if (device.sensor === 'SW_LDS20DA') { |
||||
if (method === 'read-status') { |
||||
const value = await readRegisterValue(device.port, 9600, 0x02, 0x03, 0x17) |
||||
return value == null ? '' : String(value) |
||||
} |
||||
if (method === 'read-range') { |
||||
const high = await readRegisterValue(device.port, 9600, 0x02, 0x03, 0x15) |
||||
const low = await readRegisterValue(device.port, 9600, 0x02, 0x03, 0x16) |
||||
if (high == null || low == null) return '' |
||||
return String((((high & 0xFFFF) << 16) | (low & 0xFFFF)) / 10) |
||||
} |
||||
} |
||||
|
||||
if (device.sensor === 'HWT6053') { |
||||
if (method === 'read-version') { |
||||
const value = await readRegisterValue(device.port, 9600, 0x50, 0x03, 0x2E) |
||||
return value && value !== 0 ? String(value) : '' |
||||
} |
||||
if (method === 'read-euler') { |
||||
const value = await readHwtEuler(device.port) |
||||
return value ? JSON.stringify(value) : '' |
||||
} |
||||
if (method === 'read-heading-and-vertical') { |
||||
const euler = await readHwtEuler(device.port) |
||||
const value = verticalFromEuler(euler) |
||||
return value ? JSON.stringify(value) : '' |
||||
} |
||||
if (method === 'read-temp') { |
||||
const value = await readRegisterValue(device.port, 9600, 0x50, 0x03, 0x43) |
||||
return value == null ? '' : String(value / 100) |
||||
} |
||||
if (method === 'reset-euler') return '0' |
||||
} |
||||
|
||||
if (device.sensor === 'CHC_CGI_430' && method === 'read-rtk') { |
||||
const text = await serialRequest({ |
||||
path: device.port, |
||||
baudRate: 115200, |
||||
write: null, |
||||
timeoutMs: 1000, |
||||
encoding: 'utf-8' |
||||
}) |
||||
return String(text || '').trim() |
||||
} |
||||
|
||||
return null |
||||
} |
||||
} |
||||
|
||||
module.exports = { SENSOR_META, SensorController } |
||||
@ -0,0 +1,201 @@
@@ -0,0 +1,201 @@
|
||||
const { SerialPort } = require('serialport') |
||||
|
||||
const crc16Table = new Uint16Array(256) |
||||
for (let i = 0; i < 256; i++) { |
||||
let crc = i |
||||
for (let j = 0; j < 8; j++) { |
||||
crc = (crc & 1) ? (0xA001 ^ (crc >> 1)) : (crc >> 1) |
||||
} |
||||
crc16Table[i] = crc |
||||
} |
||||
|
||||
function crc16Modbus(data) { |
||||
let crc = 0xFFFF |
||||
for (const b of data) { |
||||
crc = crc16Table[(crc ^ b) & 0xFF] ^ (crc >> 8) |
||||
} |
||||
return Buffer.from([crc & 0xFF, (crc >> 8) & 0xFF]) |
||||
} |
||||
|
||||
function appendCrc(data) { |
||||
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data) |
||||
return Buffer.concat([buf, crc16Modbus(buf)]) |
||||
} |
||||
|
||||
function checkCrc(frame) { |
||||
if (!frame || frame.length < 4) return false |
||||
const body = frame.slice(0, -2) |
||||
const crc = crc16Modbus(body) |
||||
return frame[frame.length - 2] === crc[0] && frame[frame.length - 1] === crc[1] |
||||
} |
||||
|
||||
function buildReadRegisters(slaveAddress, action, registerAddress, registerCount = 1) { |
||||
return appendCrc(Buffer.from([ |
||||
slaveAddress & 0xFF, |
||||
action & 0xFF, |
||||
(registerAddress >> 8) & 0xFF, |
||||
registerAddress & 0xFF, |
||||
(registerCount >> 8) & 0xFF, |
||||
registerCount & 0xFF |
||||
])) |
||||
} |
||||
|
||||
function parseRegisterValue(frame) { |
||||
if (!checkCrc(frame) || frame.length < 7) return null |
||||
return ((frame[3] & 0xFF) << 8) | (frame[4] & 0xFF) |
||||
} |
||||
|
||||
function toInt16(value) { |
||||
return value >= 0x8000 ? value - 0x10000 : value |
||||
} |
||||
|
||||
function toInt32(high, low) { |
||||
const value = ((high & 0xFFFF) << 16) | (low & 0xFFFF) |
||||
return value >= 0x80000000 ? value - 0x100000000 : value |
||||
} |
||||
|
||||
function normalizeHex(hex) { |
||||
return String(hex || '').replace(/\s+/g, '').toUpperCase() |
||||
} |
||||
|
||||
function closePort(port) { |
||||
return new Promise((resolve) => { |
||||
if (!port || !port.isOpen) { |
||||
resolve() |
||||
return |
||||
} |
||||
port.close(() => resolve()) |
||||
}) |
||||
} |
||||
|
||||
function openPort(path, baudRate) { |
||||
const port = new SerialPort({ |
||||
path, |
||||
baudRate: baudRate || 9600, |
||||
dataBits: 8, |
||||
parity: 'none', |
||||
stopBits: 1, |
||||
autoOpen: false, |
||||
dtr: false, |
||||
rts: false |
||||
}) |
||||
return new Promise((resolve, reject) => { |
||||
port.open(err => err ? reject(new Error(err.message)) : resolve(port)) |
||||
}) |
||||
} |
||||
|
||||
async function serialRequest({ |
||||
path, |
||||
baudRate, |
||||
write, |
||||
timeoutMs = 300, |
||||
minBytes = 0, |
||||
encoding = null |
||||
}) { |
||||
const port = await openPort(path, baudRate) |
||||
const chunks = [] |
||||
let done = false |
||||
|
||||
try { |
||||
return await new Promise((resolve, reject) => { |
||||
const finish = (err) => { |
||||
if (done) return |
||||
done = true |
||||
clearTimeout(timer) |
||||
if (err) { |
||||
reject(err) |
||||
return |
||||
} |
||||
const buf = Buffer.concat(chunks) |
||||
resolve(encoding ? buf.toString(encoding) : buf) |
||||
} |
||||
|
||||
const timer = setTimeout(() => finish(), timeoutMs) |
||||
port.on('data', chunk => { |
||||
chunks.push(chunk) |
||||
if (minBytes > 0 && Buffer.concat(chunks).length >= minBytes) finish() |
||||
}) |
||||
port.on('error', err => finish(err)) |
||||
|
||||
if (write && write.length) { |
||||
port.write(write, err => { |
||||
if (err) finish(err) |
||||
}) |
||||
} |
||||
}) |
||||
} finally { |
||||
await closePort(port) |
||||
} |
||||
} |
||||
|
||||
const probes = [ |
||||
{ |
||||
id: 'TOF5000', |
||||
baudRate: 115200, |
||||
command: buildReadRegisters(0x01, 0x03, 0x00, 1), |
||||
validate: response => { |
||||
const value = parseRegisterValue(response) |
||||
return value != null && value !== 0 |
||||
} |
||||
}, |
||||
{ |
||||
id: 'SW_LDS20DA', |
||||
baudRate: 9600, |
||||
command: buildReadRegisters(0x02, 0x03, 0x17, 1), |
||||
validate: response => checkCrc(response) |
||||
}, |
||||
{ |
||||
id: 'HWT6053', |
||||
baudRate: 9600, |
||||
command: buildReadRegisters(0x50, 0x03, 0x2E, 1), |
||||
validate: response => { |
||||
const value = parseRegisterValue(response) |
||||
return value != null && value !== 0 |
||||
} |
||||
}, |
||||
{ |
||||
id: 'CHC_CGI_430', |
||||
baudRate: 115200, |
||||
command: null, |
||||
timeoutMs: 2000, |
||||
encoding: 'utf-8', |
||||
validate: response => String(response || '').includes('$') |
||||
} |
||||
] |
||||
|
||||
async function runProbe(path, probe) { |
||||
const response = await serialRequest({ |
||||
path, |
||||
baudRate: probe.baudRate, |
||||
write: probe.command, |
||||
timeoutMs: probe.timeoutMs || 300, |
||||
minBytes: probe.command ? 5 : 0, |
||||
encoding: probe.encoding || null |
||||
}) |
||||
const responseHex = Buffer.isBuffer(response) |
||||
? response.toString('hex').toUpperCase() |
||||
: Buffer.from(String(response || ''), 'utf-8').toString('hex').toUpperCase() |
||||
if (!probe.validate(response)) return null |
||||
return { |
||||
probeId: probe.id, |
||||
baudRate: probe.baudRate, |
||||
response, |
||||
responseHex |
||||
} |
||||
} |
||||
|
||||
module.exports = { |
||||
SerialPort, |
||||
appendCrc, |
||||
buildReadRegisters, |
||||
checkCrc, |
||||
closePort, |
||||
normalizeHex, |
||||
openPort, |
||||
parseRegisterValue, |
||||
probes, |
||||
runProbe, |
||||
serialRequest, |
||||
toInt16, |
||||
toInt32 |
||||
} |
||||
@ -0,0 +1,261 @@
@@ -0,0 +1,261 @@
|
||||
const { ipcMain } = require('electron') |
||||
const { |
||||
SerialPort, |
||||
closePort, |
||||
normalizeHex, |
||||
openPort, |
||||
probes, |
||||
runProbe, |
||||
serialRequest |
||||
} = require('./serial-protocols') |
||||
|
||||
function deviceIdFromPort(portInfo) { |
||||
const raw = `${portInfo.path}|${portInfo.vendorId || ''}|${portInfo.productId || ''}` |
||||
let hash = 0 |
||||
for (let i = 0; i < raw.length; i++) { |
||||
hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0 |
||||
} |
||||
return Math.abs(hash) |
||||
} |
||||
|
||||
class AsyncLockCenter { |
||||
constructor() { |
||||
this.tails = new Map() |
||||
} |
||||
|
||||
async withLock(key, fn) { |
||||
const previous = this.tails.get(key) || Promise.resolve() |
||||
let release |
||||
const current = new Promise(resolve => { release = resolve }) |
||||
this.tails.set(key, previous.then(() => current)) |
||||
await previous |
||||
try { |
||||
return await fn() |
||||
} finally { |
||||
release() |
||||
if (this.tails.get(key) === current) this.tails.delete(key) |
||||
} |
||||
} |
||||
} |
||||
|
||||
class UsbSerialBridge { |
||||
constructor(mainWindow, { onDeviceDiscovered } = {}) { |
||||
this.mainWindow = mainWindow |
||||
this.onDeviceDiscovered = onDeviceDiscovered || (() => {}) |
||||
this.activeConnections = new Map() |
||||
this.discoveredDevices = new Map() |
||||
this.discoveryTimer = null |
||||
this.discoveryRunning = false |
||||
this.lockCenter = new AsyncLockCenter() |
||||
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', () => JSON.stringify([...this.discoveredDevices.values()])) |
||||
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() { |
||||
if (this.discoveryRunning) return |
||||
this.discoveryRunning = true |
||||
await this.scanOnce() |
||||
this.discoveryTimer = setInterval(() => this.scanOnce(), 2000) |
||||
} |
||||
|
||||
stopDiscovery() { |
||||
this.discoveryRunning = false |
||||
if (this.discoveryTimer) clearInterval(this.discoveryTimer) |
||||
this.discoveryTimer = null |
||||
} |
||||
|
||||
async scanOnce() { |
||||
let ports = [] |
||||
try { |
||||
ports = await SerialPort.list() |
||||
} catch (err) { |
||||
console.error('[usb] list failed:', err.message) |
||||
return |
||||
} |
||||
|
||||
const current = new Set(ports.map(p => p.path).filter(Boolean)) |
||||
for (const [deviceName, device] of this.discoveredDevices) { |
||||
if (!current.has(deviceName)) { |
||||
this.discoveredDevices.delete(deviceName) |
||||
this._emit('usb-callback-device-detached', { |
||||
deviceName, |
||||
deviceId: device.deviceId |
||||
}) |
||||
for (const key of [...this.activeConnections.keys()]) { |
||||
if (key.startsWith(`${deviceName}:`)) this.deviceCloseKey(key) |
||||
} |
||||
} |
||||
} |
||||
|
||||
for (const portInfo of ports) { |
||||
if (!portInfo.path) continue |
||||
if (!this.discoveredDevices.has(portInfo.path)) { |
||||
const device = { |
||||
deviceName: portInfo.path, |
||||
deviceId: deviceIdFromPort(portInfo), |
||||
ports: [1], |
||||
manufacturer: portInfo.manufacturer || '', |
||||
vendorId: portInfo.vendorId || '', |
||||
productId: portInfo.productId || '' |
||||
} |
||||
this.discoveredDevices.set(portInfo.path, device) |
||||
this._emit('usb-callback-device-attached', device) |
||||
} |
||||
this.probePort(portInfo.path, 1) |
||||
} |
||||
} |
||||
|
||||
async probePort(deviceName, portNumber) { |
||||
for (const probe of probes) { |
||||
try { |
||||
const result = await this.lockCenter.withLock(deviceName, () => runProbe(deviceName, probe)) |
||||
if (!result) continue |
||||
const payload = { |
||||
deviceName, |
||||
port: portNumber, |
||||
probeId: result.probeId, |
||||
baudRate: result.baudRate, |
||||
responseHex: result.responseHex |
||||
} |
||||
this._emit('usb-callback-device-discovered', payload) |
||||
this.onDeviceDiscovered(payload) |
||||
return payload |
||||
} catch (err) { |
||||
// A probe miss is normal while scanning mixed serial devices.
|
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
async deviceProbe({ deviceName, portNumber, baudRate, hexCmd, timeoutMs }) { |
||||
try { |
||||
const response = await this.lockCenter.withLock(deviceName, () => serialRequest({ |
||||
path: deviceName, |
||||
baudRate: baudRate || 9600, |
||||
write: normalizeHex(hexCmd) ? Buffer.from(normalizeHex(hexCmd), 'hex') : null, |
||||
timeoutMs: timeoutMs || 1000 |
||||
})) |
||||
const responseHex = response.toString('hex').toUpperCase() |
||||
this._emit('usb-callback-device-probe-result', { |
||||
deviceName, |
||||
port: portNumber, |
||||
baudRate, |
||||
success: responseHex.length > 0, |
||||
responseHex, |
||||
error: responseHex.length > 0 ? '' : 'No response' |
||||
}) |
||||
} catch (err) { |
||||
this._emit('usb-callback-device-probe-result', { |
||||
deviceName, |
||||
port: portNumber, |
||||
baudRate, |
||||
success: false, |
||||
responseHex: '', |
||||
error: err.message |
||||
}) |
||||
} |
||||
} |
||||
|
||||
async deviceOpen({ deviceName, portNumber, baudRate }) { |
||||
const key = `${deviceName}:${portNumber}` |
||||
const existing = this.activeConnections.get(key) |
||||
if (existing && existing.state === 'CONNECTED' && existing.port?.isOpen) { |
||||
this._emit('usb-callback-device-opened', { deviceName, port: portNumber, success: true, error: '' }) |
||||
return |
||||
} |
||||
|
||||
this._emit('usb-callback-device-state', { deviceName, port: portNumber, state: 'CONNECTING' }) |
||||
try { |
||||
const port = await this.lockCenter.withLock(key, () => openPort(deviceName, baudRate || 9600)) |
||||
const entry = { port, deviceName, portNumber, state: 'CONNECTED', generation: Date.now() } |
||||
this.activeConnections.set(key, entry) |
||||
|
||||
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 => { |
||||
const current = this.activeConnections.get(key) |
||||
if (!current || current.generation !== entry.generation) return |
||||
this._emit('usb-callback-device-data', { |
||||
deviceName, |
||||
port: portNumber, |
||||
hexData: data.toString('hex').toUpperCase() |
||||
}) |
||||
}) |
||||
port.on('error', err => { |
||||
console.error('[usb] connection error:', err.message) |
||||
this.deviceCloseKey(key) |
||||
}) |
||||
port.on('close', () => { |
||||
if (this.activeConnections.get(key) === entry) { |
||||
this.activeConnections.delete(key) |
||||
this._emit('usb-callback-device-state', { deviceName, port: portNumber, state: 'DISCONNECTED' }) |
||||
} |
||||
}) |
||||
} catch (err) { |
||||
this._emit('usb-callback-device-opened', { |
||||
deviceName, |
||||
port: portNumber, |
||||
success: false, |
||||
error: err.message |
||||
}) |
||||
this._emit('usb-callback-device-state', { deviceName, port: portNumber, state: 'DISCONNECTED' }) |
||||
} |
||||
} |
||||
|
||||
deviceClose({ deviceName, portNumber }) { |
||||
this.deviceCloseKey(`${deviceName}:${portNumber}`) |
||||
} |
||||
|
||||
async deviceCloseKey(key) { |
||||
const entry = this.activeConnections.get(key) |
||||
if (!entry) return |
||||
this.activeConnections.delete(key) |
||||
await closePort(entry.port) |
||||
this._emit('usb-callback-device-state', { |
||||
deviceName: entry.deviceName, |
||||
port: entry.portNumber, |
||||
state: 'DISCONNECTED' |
||||
}) |
||||
} |
||||
|
||||
async deviceWrite({ deviceName, portNumber, hexData }) { |
||||
const key = `${deviceName}:${portNumber}` |
||||
const entry = this.activeConnections.get(key) |
||||
if (!entry || entry.state !== 'CONNECTED' || !entry.port?.isOpen) return |
||||
|
||||
const normalized = normalizeHex(hexData) |
||||
if (!normalized) return |
||||
const buf = Buffer.from(normalized, 'hex') |
||||
await this.lockCenter.withLock(key, () => new Promise(resolve => { |
||||
entry.port.write(buf, err => { |
||||
if (err) console.error('[usb] write failed:', err.message) |
||||
resolve() |
||||
}) |
||||
})) |
||||
} |
||||
|
||||
disconnectAll() { |
||||
this.stopDiscovery() |
||||
for (const key of [...this.activeConnections.keys()]) { |
||||
this.deviceCloseKey(key) |
||||
} |
||||
} |
||||
} |
||||
|
||||
module.exports = { UsbSerialBridge } |
||||
@ -1,239 +0,0 @@
@@ -1,239 +0,0 @@
|
||||
/** |
||||
* 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