Browse Source

feat: 迁移安卓端代码,待验证

master
WIN-87ES3P38OPV\EDY 1 month ago
parent
commit
b4a2e14b0c
  1. 238
      main.js
  2. 59
      preload.js
  3. 325
      src/business/sensor-controller.js
  4. 0
      src/business/sensor.js
  5. 201
      src/business/serial-protocols.js
  6. 261
      src/business/usb-serial-bridge.js
  7. 239
      usb-serial-bridge.js

238
main.js

@ -1,18 +1,8 @@ @@ -1,18 +1,8 @@
/*
* @Author: 李皓 hao_li_work@163.com
* @Date: 2026-07-28 15:22:20
* @LastEditors: 李皓 hao_li_work@163.com
* @LastEditTime: 2026-07-28 16:59:56
* @FilePath: \pc\main.js
* @Description:
* @Version: 1.0.0
*/
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const { SerialPort } = require('serialport')
const { WitMotionSensor } = require('./sensor')
const { UsbSerialBridge } = require('./usb-serial-bridge')
const { SensorController } = require('./src/business/sensor-controller')
const { UsbSerialBridge } = require('./src/business/usb-serial-bridge')
const pkg = require('./package.json')
const cacheDir = app.getPath('userData')
@ -20,11 +10,8 @@ const cacheFile = path.join(cacheDir, 'app-cache.json') @@ -20,11 +10,8 @@ const cacheFile = path.join(cacheDir, 'app-cache.json')
const fileRoot = path.join(cacheDir, 'files')
let mainWindow = null
let sensor = null
let usbBridge = null
let sensorOpen = false
let sensorDevices = []
let lastSensorValues = {}
let sensorController = null
function ensureDir(dir) {
try {
@ -66,112 +53,36 @@ function fileList(dir) { @@ -66,112 +53,36 @@ function fileList(dir) {
}
}
function sensorOperations(id) {
return [
{ id: 'read-version', name: '获取当前传感器版本' },
{ id: 'read-temp', name: '获取设备温度' },
{ id: 'read-euler', name: '获取设备角度' },
{ id: 'reset-euler', name: '重置角度传感器基准' },
{ id: 'read-heading-and-vertical', name: '获取朝向和垂直夹角' }
].map(op => ({ ...op, sensorId: id }))
}
async function refreshSensorDevices() {
try {
const ports = await SerialPort.list()
sensorDevices = ports
.filter(p => p.path && /^COM\d+$/i.test(p.path))
.map(p => ({
id: `HWT6053:${p.path}`,
name: 'HWT6053',
port: p.path,
baudRate: 9600,
isOpen: sensorOpen,
operations: sensorOperations(`HWT6053:${p.path}`)
}))
for (const device of sensorDevices) {
function emit(channel, ...args) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('sensor-attached', { id: device.id, name: device.name })
}
}
} catch (err) {
console.error('[sensor] 刷新设备失败:', err.message)
sensorDevices = []
}
}
function normalizeSensorData(data) {
if (!data || data.sensor !== 'HWT6053') return data
if (data.method === 'temperature') {
const value = data.value && typeof data.value === 'object' ? data.value.temp : data.value
return { sensor: 'HWT6053', method: 'read-temp', value: String(value ?? '') }
}
if (data.method === 'angle') {
return { sensor: 'HWT6053', method: 'read-euler', value: data.value }
mainWindow.webContents.send(channel, ...args)
}
if (data.method === 'magnetic') {
return { sensor: 'HWT6053', method: 'read-heading-and-vertical', value: data.value }
}
return data
}
function cacheSensorValue(data) {
const normalized = normalizeSensorData(data)
if (!normalized) return
lastSensorValues[normalized.method] = normalized.value
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('sensor-data', normalized)
}
function lastSensorValue(id, opId) {
if (!sensorController) return null
const device = sensorController.devices.find(d => d.id === id) ||
sensorController.devices.find(d => d.sensor === id) ||
sensorController.devices.find(d => d.sensor === String(id || '').split(':')[0])
if (!device) return null
return sensorController.lastValues.get(`${device.sensor}:${opId}`) ?? null
}
function ensureSensor() {
if (sensor) return sensor
sensor = new WitMotionSensor({
onData: cacheSensorValue,
onLog: (level, msg) => {
console.log(`[sensor] ${msg}`)
if (mainWindow && !mainWindow.isDestroyed()) {
try { mainWindow.webContents.send('sensor-log', { level, msg }) } catch {}
}
}
function createSensorController() {
sensorController = new SensorController({
onValue: data => emit('sensor-data', data),
onDeviceAttached: device => emit('sensor-attached', { id: device.id, name: device.name }),
onDeviceDetached: device => emit('sensor-detached', { id: device.id, name: device.name }),
onLog: (level, msg) => emit('sensor-log', { level, msg })
})
return sensor
}
function openAppSensor() {
sensorOpen = true
refreshSensorDevices()
ensureSensor().connect()
return true
}
function closeAppSensor() {
sensorOpen = false
if (sensor) sensor.disconnect()
sensor = null
return true
}
function runSensorOperation(opId) {
if (opId === 'read-version') return pkg.version || '1'
if (opId === 'read-temp') return lastSensorValues['read-temp'] ?? null
if (opId === 'read-euler') {
const value = lastSensorValues['read-euler']
return value == null ? null : JSON.stringify(value)
}
if (opId === 'read-heading-and-vertical') {
const value = lastSensorValues['read-heading-and-vertical']
return value == null ? null : JSON.stringify(value)
}
if (opId === 'reset-euler') return '0'
return null
sensorController.refresh().catch(err => console.error('[sensor] refresh failed:', err.message))
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
width: 1280,
height: 800,
fullscreen: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@ -180,12 +91,17 @@ function createWindow() { @@ -180,12 +91,17 @@ function createWindow() {
}
})
mainWindow.loadFile('./dist/index.html')
usbBridge = new UsbSerialBridge(mainWindow)
refreshSensorDevices()
createSensorController()
usbBridge = new UsbSerialBridge(mainWindow, {
onDeviceDiscovered: () => {
if (sensorController) sensorController.refresh().catch(() => {})
}
})
function registerIpc() {
mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html'))
}
function registerAppIpc() {
ipcMain.on('app-clear-cache-sync', (event) => {
writeCache({})
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.reload()
@ -221,13 +137,8 @@ function registerIpc() { @@ -221,13 +137,8 @@ function registerIpc() {
return true
})
ipcMain.on('app-offline-page', () => {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('app-page-mode', 'offline')
})
ipcMain.on('app-online-page', () => {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('app-page-mode', 'online')
})
ipcMain.on('app-offline-page', () => emit('app-page-mode', 'offline'))
ipcMain.on('app-online-page', () => emit('app-page-mode', 'online'))
ipcMain.on('app-file-list-sync', (event, dir) => {
event.returnValue = fileList(dir)
@ -255,38 +166,87 @@ function registerIpc() { @@ -255,38 +166,87 @@ function registerIpc() {
.catch(() => event.sender.send('app-file-delete-callback', token, target, false))
})
ipcMain.on('app-sensor-open-sync', (event) => { event.returnValue = openAppSensor() })
ipcMain.on('app-sensor-close-sync', (event) => { event.returnValue = closeAppSensor() })
ipcMain.on('app-sensor-is-open-sync', (event) => { event.returnValue = sensorOpen })
ipcMain.on('app-export-file', (event, filename, content, token) => {
const target = resolveAppFile(filename || `export-${Date.now()}.txt`)
fs.promises.mkdir(path.dirname(target), { recursive: true })
.then(() => fs.promises.writeFile(target, String(content ?? ''), 'utf-8'))
.then(() => event.sender.send('app-file-write-callback', token || filename, target, true, ''))
.catch(err => event.sender.send('app-file-write-callback', token || filename, target, false, err.message))
})
}
function registerSensorIpc() {
ipcMain.on('app-sensor-open-sync', (event) => {
if (sensorController) sensorController.open().catch(() => {})
event.returnValue = true
})
ipcMain.on('app-sensor-close-sync', (event) => {
event.returnValue = sensorController ? sensorController.close() : true
})
ipcMain.on('app-sensor-is-open-sync', (event) => {
event.returnValue = sensorController ? sensorController.isOpen : false
})
ipcMain.on('sensor-init-all-sync', (event) => {
refreshSensorDevices()
if (sensorController) sensorController.initAll().catch(() => {})
event.returnValue = true
})
ipcMain.on('sensor-flush-sync', (event) => {
refreshSensorDevices()
if (sensorController) sensorController.refresh().catch(() => {})
event.returnValue = true
})
ipcMain.on('sensor-list-sync', (event) => {
event.returnValue = sensorDevices
event.returnValue = sensorController ? sensorController.list() : []
})
ipcMain.on('sensor-init-sync', (event, id) => {
event.returnValue = sensorDevices.some(device => device.id === id)
event.returnValue = sensorController ? sensorController.init(id) : false
})
ipcMain.on('sensor-open-sync', (event) => { event.returnValue = openAppSensor() })
ipcMain.on('sensor-close-sync', (event) => { event.returnValue = closeAppSensor() })
ipcMain.on('sensor-operation-sync', (event, _id, opId) => {
event.returnValue = runSensorOperation(opId)
ipcMain.on('sensor-open-sync', (event) => {
if (sensorController) sensorController.open().catch(() => {})
event.returnValue = true
})
ipcMain.on('sensor-operation-async', (event, id, opId) => {
const result = runSensorOperation(opId)
event.sender.send('sensor-operation-callback', id, opId, result)
ipcMain.on('sensor-close-sync', (event) => {
event.returnValue = sensorController ? sensorController.close() : true
})
ipcMain.on('sensor-operation-sync', (event, id, opId) => {
const cached = lastSensorValue(id, opId)
if (sensorController) {
sensorController.operation(id, opId).catch(() => {})
}
event.returnValue = cached
})
ipcMain.on('sensor-operation-async', (event, id, opId, args) => {
if (!sensorController) {
event.sender.send('sensor-operation-callback', id, opId, null)
return
}
sensorController.operation(id, opId, args)
.then(result => event.sender.send('sensor-operation-callback', id, opId, result))
.catch(err => event.sender.send('sensor-operation-callback', id, opId, err.message))
})
}
function registerLocationAndBluetoothIpc() {
ipcMain.on('app-location-is-support-sync', (event) => { event.returnValue = false })
ipcMain.on('app-location-is-open-sync', (event) => { event.returnValue = false })
ipcMain.on('app-location-open', (event) => { event.sender.send('app-location-opened', 1) })
ipcMain.on('app-location-close', () => {})
ipcMain.on('bluetooth-device-select', (event, namePrefix) => {
event.sender.send('bluetooth-device-selected', 'false', namePrefix || '')
})
ipcMain.on('bluetooth-device-open', (event, mac) => {
event.sender.send('bluetooth-device-opened', 'false', mac || '')
event.sender.send('bluetooth-device-state-changed', mac || '', 'UNSUPPORTED')
})
ipcMain.on('bluetooth-device-write', () => {})
ipcMain.on('bluetooth-device-close', () => {})
}
function registerIpc() {
registerAppIpc()
registerSensorIpc()
registerLocationAndBluetoothIpc()
}
app.whenReady().then(() => {
@ -295,7 +255,11 @@ app.whenReady().then(() => { @@ -295,7 +255,11 @@ app.whenReady().then(() => {
createWindow()
app.on('before-quit', () => {
if (sensor) sensor.disconnect()
if (sensorController) sensorController.close()
if (usbBridge) usbBridge.disconnectAll()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

59
preload.js

@ -63,6 +63,7 @@ const app = { @@ -63,6 +63,7 @@ const app = {
getCache(key) { return ipcRenderer.sendSync('get-cache-sync', key) },
offlinePage() { ipcRenderer.send('app-offline-page') },
onlinePage() { ipcRenderer.send('app-online-page') },
exportFile(filename, content, token) { ipcRenderer.send('app-export-file', filename, content, token) },
fileList(dir) { return ipcRenderer.sendSync('app-file-list-sync', dir) },
fileWriteString(filePath, content, token) { ipcRenderer.send('app-file-write-string', filePath, content, token) },
fileReadString(filePath, token) { ipcRenderer.send('app-file-read-string', filePath, token) },
@ -96,7 +97,10 @@ ipcRenderer.on('sensor-operation-callback', (_event, id, opId, result) => { @@ -96,7 +97,10 @@ ipcRenderer.on('sensor-operation-callback', (_event, id, opId, result) => {
const sensor = {
initAll() { return ipcRenderer.sendSync('sensor-init-all-sync') },
flush() { return ipcRenderer.sendSync('sensor-flush-sync') },
sensorList() { return ipcRenderer.sendSync('sensor-list-sync') },
sensorList() {
const data = ipcRenderer.sendSync('sensor-list-sync')
return JSON.stringify({ code: 0, data: Array.isArray(data) ? data : [] })
},
init(id) { return ipcRenderer.sendSync('sensor-init-sync', id) },
open(id) { return ipcRenderer.sendSync('sensor-open-sync', id) },
close(id) { return ipcRenderer.sendSync('sensor-close-sync', id) },
@ -114,6 +118,7 @@ const appSensorCallbacks = { onValueCallback: null } @@ -114,6 +118,7 @@ const appSensorCallbacks = { onValueCallback: null }
const appSensor = {
version: '1.0.0',
name: 'appSensor',
dataArr: [],
open() { return ipcRenderer.sendSync('app-sensor-open-sync') },
close() { return ipcRenderer.sendSync('app-sensor-close-sync') },
isOpen() { return ipcRenderer.sendSync('app-sensor-is-open-sync') },
@ -122,6 +127,14 @@ const appSensor = { @@ -122,6 +127,14 @@ const appSensor = {
}
ipcRenderer.on('sensor-data', (_event, data) => {
if (data && data.sensor && data.method) {
const index = appSensor.dataArr.findIndex(item => item.sensor === data.sensor && item.method === data.method)
if (index >= 0) {
appSensor.dataArr.splice(index, 1, data)
} else {
appSensor.dataArr.push(data)
}
}
if (typeof appSensorCallbacks.onValueCallback === 'function') appSensorCallbacks.onValueCallback(data)
})
@ -205,9 +218,53 @@ const usbSerial = { @@ -205,9 +218,53 @@ const usbSerial = {
}
}
const bluetoothSppCallbacks = {
onDeviceSelected: null,
onDeviceOpened: null,
onDeviceDataReceived: null,
onDeviceStateChanged: null
}
ipcRenderer.on('bluetooth-device-selected', (_event, success, address) => {
if (typeof bluetoothSppCallbacks.onDeviceSelected === 'function') {
bluetoothSppCallbacks.onDeviceSelected(success, address)
}
})
ipcRenderer.on('bluetooth-device-opened', (_event, success, mac) => {
if (typeof bluetoothSppCallbacks.onDeviceOpened === 'function') {
bluetoothSppCallbacks.onDeviceOpened(success, mac)
}
})
ipcRenderer.on('bluetooth-device-data-received', (_event, mac, data) => {
if (typeof bluetoothSppCallbacks.onDeviceDataReceived === 'function') {
bluetoothSppCallbacks.onDeviceDataReceived(mac, data)
}
})
ipcRenderer.on('bluetooth-device-state-changed', (_event, mac, state) => {
if (typeof bluetoothSppCallbacks.onDeviceStateChanged === 'function') {
bluetoothSppCallbacks.onDeviceStateChanged(mac, state)
}
})
const bluetoothSpp = {
deviceSelect(namePrefix) { ipcRenderer.send('bluetooth-device-select', namePrefix) },
deviceOpen(mac) { ipcRenderer.send('bluetooth-device-open', mac) },
deviceWrite(mac, data) { ipcRenderer.send('bluetooth-device-write', mac, data) },
deviceClose(mac) { ipcRenderer.send('bluetooth-device-close', mac) },
get onDeviceSelected() { return bluetoothSppCallbacks.onDeviceSelected },
set onDeviceSelected(fn) { bluetoothSppCallbacks.onDeviceSelected = fn },
get onDeviceOpened() { return bluetoothSppCallbacks.onDeviceOpened },
set onDeviceOpened(fn) { bluetoothSppCallbacks.onDeviceOpened = fn },
get onDeviceDataReceived() { return bluetoothSppCallbacks.onDeviceDataReceived },
set onDeviceDataReceived(fn) { bluetoothSppCallbacks.onDeviceDataReceived = fn },
get onDeviceStateChanged() { return bluetoothSppCallbacks.onDeviceStateChanged },
set onDeviceStateChanged(fn) { bluetoothSppCallbacks.onDeviceStateChanged = fn }
}
contextBridge.exposeInMainWorld('app', app)
contextBridge.exposeInMainWorld('net', net)
contextBridge.exposeInMainWorld('sensor', sensor)
contextBridge.exposeInMainWorld('appSensor', appSensor)
contextBridge.exposeInMainWorld('appLocation', appLocation)
contextBridge.exposeInMainWorld('usbSerial', usbSerial)
contextBridge.exposeInMainWorld('bluetoothSpp', bluetoothSpp)

325
src/business/sensor-controller.js

@ -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
sensor.js → src/business/sensor.js

201
src/business/serial-protocols.js

@ -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
}

261
src/business/usb-serial-bridge.js

@ -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 }

239
usb-serial-bridge.js

@ -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…
Cancel
Save