Compare commits

...

3 Commits

  1. 10
      main.js
  2. 187
      md/webview-sensor.md
  3. 298
      md/webview-usb-serial.md
  4. 80
      preload.js
  5. 239
      usb-serial-bridge.js

10
main.js

@ -11,6 +11,7 @@ const { app, BrowserWindow, ipcMain } = require('electron') @@ -11,6 +11,7 @@ const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const { WitMotionSensor } = require('./sensor')
const { UsbSerialBridge } = require('./usb-serial-bridge')
// 缓存文件路径(存储在应用用户数据目录下)
const cacheDir = app.getPath('userData')
@ -60,6 +61,9 @@ const createWindow = () => { @@ -60,6 +61,9 @@ const createWindow = () => {
win.loadFile('./dist/index.html')
// 初始化 USB Serial 桥接模块
const usbBridge = new UsbSerialBridge(win)
// 窗口创建后连接传感器
sensor = new WitMotionSensor({
onData: (data) => {
@ -94,4 +98,10 @@ app.whenReady().then(() => { @@ -94,4 +98,10 @@ app.whenReady().then(() => {
})
createWindow()
// 应用退出时清理
app.on('before-quit', () => {
if (sensor) sensor.disconnect()
// usbBridge 的 cleanup 由窗口 close 事件触发
})
})

187
md/webview-sensor.md

@ -0,0 +1,187 @@ @@ -0,0 +1,187 @@
# Android Web容器文档
## WebView 注入对象
android端在拉起web容器时,会向其js上下文中注入全局对象
### app对象
app对象包含几个和android互操作的封装方法,基本如下
| 方法 | 描述 | 备注 |
| ------------------------------------- | -------------------------------------------------------------------------------- | --- |
| clearCache() | 清除本地缓存,并且刷新页面 | - |
| versionCode(): Int | 获取当前app版本号 | - |
| setCache(key:String, value:String) | 设置缓存数据 | - |
| getCache(key) :String | 获取缓存数据,返回string或者null | - |
| offlinePage() | 进入离线模式页面 | - |
| onlinePage() | 进入在线模式页面 | - |
| fileList(dir) | 获取当前dir下的文件,dir可传空,返回值为多行字符串,一行代表一个文件的绝对路径 | - |
| fileWriteString(path, content, token) | 参数path路径,content为文本,token为回调标记,完成后会调用 app.onFileWrite(token, path, success, err) | - |
| fileReadString(path, token) | 参数path路径,token为回调标记,完成后会调用 app.onFileRead(token, path, content) | - |
| fileDelete(path, token) | 参数path路径,token为回调标记,完成后会调用 app.onFileDelete(token, path, success) | - |
注意path参数,如不知道具体路径,则以 "/" 开头,目前不支持文件夹操作
### net对象
net包含部分网络操作封装
| 方法 | 描述 | 备注 |
| ------------------------------------------- | -------------------------- | --- |
| ping(host: String, timeoutMs: Int): Boolean | 向指定主机地址ping,不可达或超时则返回false | - |
### sensor对象
sensor对象包含传感器初始化和交互的方法,已有方法如下
| 方法 | 描述 | 备注 |
| --------------------------------------------------- | --------------- | ----------------------------------------------------- |
| initAll() | 检测并且初始化现有的所有设备 | 当前方法会阻塞调用 |
| flush() | 刷新当前检测到的所有设备 | 用于USB设备插拔后,调用此方法后,应当调用sensorList()获取最新的传感器信息 |
| sensorList():List\<Sensor> | 获取当前传感器列表数据 | |
| init(id:String) | 初始化指定传感器设备并获取权限 | 当前方法会阻塞调用 |
| open(id:String) | 打开指定传感器设备 | 开启指定传感器设备,传感器开启状态通过sensorList()获取 |
| close(id:String) | 关闭指定传感器设备 | |
| operation(id:String, opId:String, args:String) | 对传感器设备进行操作 | 此方法会在sensorList()下返回对应的操作对象, id与opId用于区分传感器和操作方法 |
| operationAsync(id:String, opId:String, args:String) | 异步对传感器设备进行操作 | 此方法调用后无返回值,而是会异步调用sensor.onOperationCallback()方法下回调参数 |
### appSensor对象
| 方法 | 描述 | 备注 |
| -------- | --------------------------- | --- |
| open() | 打开传感器管理器对象,尝试打开本地所有已物理连接的设备 | - |
| close() | 关闭传感器管理器对象 | - |
| isOpen() | 判断传感器管理器是否打开 | - |
当管理器开始工作后,会持续调用`appSensor.onValueCallback(obj)`方法,前端需要实现此方法
```js
if (appSensor != null) {
appSensor.onValueCallback = function(obj) {
console.log("obj:", obj)
}
}
```
其中obj对象结构如下,其中value可能是对象类型
```json
{
"sensor": "HWT6053",
"method": "read-speed",
"value": "1"
}
```
枚举上报值
```kotlin
enum class SensorMethod(
val sensor: Sensor,
val code: String,
val methodDesc: String
) {
TOF5000_M1(Sensor.TOF5000, "read-speed", "读取速度"),
TOF5000_M2(Sensor.TOF5000, "read-baud-rate", "获取波特率"),
TOF5000_M3(Sensor.TOF5000, "read-slave-address", "获取从站地址"),
TOF5000_M4(Sensor.TOF5000, "read-laser-switch", "获取激光开关状态"),
TOF5000_M5(Sensor.TOF5000, "read-range", "获取当前距离"),
HWT6053_M1(Sensor.HWT6053, "read-version", "获取版本号"),
HWT6053_M2(Sensor.HWT6053, "read-temp", "获取温度"),
HWT6053_M3(Sensor.HWT6053, "read-euler", "获取角度"),
HWT6053_M4(Sensor.HWT6053, "read-heading-and-vertical", "获取朝向和垂直夹角"),
CHC_CGI_430_M1(Sensor.CHC_CGI_430, "read-rtk", "获取RTK数据"),
LOCAL_EULER_M1(Sensor.LOCAL_EULER, "euler", "获取设备欧拉角数据"),
LOCAL_EULER_M2(Sensor.LOCAL_EULER, "euler-filter", "获取设备滤波后欧拉角数据"),
LOCAL_EULER_M3(Sensor.LOCAL_EULER, "yaw", "偏航角"),
LOCAL_EULER_M4(Sensor.LOCAL_EULER, "magnetic", "磁场强度"),
LOCAL_EULER_M5(Sensor.LOCAL_EULER, "has-disturb", "是否有干扰"),
LOCAL_LOCATION_M1(Sensor.LOCAL_LOCATION, "support", "本设备是否支持"),
LOCAL_LOCATION_M2(Sensor.LOCAL_LOCATION, "location", "获取定位位置"),
}
```
### appLocation对象
此对象封装了获取定位经纬度的操作函数
| 方法 | 描述 | 备注 |
| ----------- | ---------------------- | --------------------------------------------------------------- |
| isSupport() | 检查当前设备是否支持GPS定位(非基站定位) | 返回 true/false |
| isOpen() | 返回当前定位设备是否已打开 | 返回 true/false |
| open() | app侧自行申请授权并开始定位 | 此方法为异步方法,异步回调`onOpened(status)`、`onLocationChanged(location)`函数 |
| close() | 关闭定位 | |
除了web调用app侧的函数,还需要web侧实现函数用于异步回调
无特殊说明,函数均在此实例对象下,如`onOpen`函数,web侧应当如此定义
```js
if (appLocation != null) {
appLocation.onOpen = function(status) {
console.log("location device status:", status)
}
}
```
| 方法 | 描述 | 备注 |
| ---------------------- | ----------------------------------------------------------------- | --- |
| onOpened(status) | 当调用`open()`方法后回调,入参status可能值如下 0-开启成功 1-设备不支持GPS 2-用户拒绝授权 | |
| onLocationChanged(loc) | 当调用`open()`方法后回调,入参`loc`是Object类型,包含latitude,longitude,默认WGS84坐标系 | |
#### 传感器回调函数
js中,在sensor对象下自行添加实现两个函数,以接收传感器插拔的回调
| 方法 | 描述 | 备注 |
| ------------------------------------- | ---------------- | --- |
| onSensorAttached(id, name) | 当传感器插入时回调 | |
| onSensorDetached(id, name) | 当传感器拔出时回调 | |
| onOperationCallback(id, opId, result) | 异步对传感器设备进行操作后的回调 | |
## 传感器
描述传感器现有提供适配的传感器驱动
### HWT6053-水平传感器
波特率9600,从站地址0x50
| 操作 | 描述 | 备注 |
| ----------------------------- | --------- | ------------------------------- |
| $id#read-version | 获取当前传感器版本 | - |
| $id#read-temp | 获取设备温度 | - |
| $id#read-euler | 获取设备角度 | 返回值为包含roll,pitch,yaw三个角度的json字符 |
| $id#reset-euler | 重置角度传感器基准 | 返回0为成功 |
| $id#read-heading-and-vertical | 获取朝向和垂直夹角 | 返回0为成功 |
### TOF5000-距离传感器
波特率115200,从站地址0x01
| 操作 | 描述 | 备注 |
| -------------- | --------- | --- |
| $id#read-speed | 获取设备读取速度 | - |
| $id#read-range | 读取当前传感器距离 | - |
### CHC CGI-430
波特率230400,无从站地址,接入设备后会以1s的间隔下发数据
| 操作 | 描述 | 备注 |
| ------------ | ------------- | -------------------------------------------------- |
| $id#read-rtk | 获取传感设备下发的定位数据 | web端调用后,会获取到设备下发的最后一条数据,数据同手册内容,但是可能会返回截断数据,需要注意处理 |
## 离线交互流程
1. 首次启动app后,会向`223.5.5.5`ping 并且最多等待1s
2. ping成功进入在线页面
3. ping不成功进入离线页面
4. 离线页面需要在android版本发布时嵌入发布包中(后续改成在在线页面下下载?)
5. 在线离线页面中需要对应切换页面按钮,或者使用侧滑菜单选择进入对应的在线/离线页面
TODO

298
md/webview-usb-serial.md

@ -0,0 +1,298 @@ @@ -0,0 +1,298 @@
# USB Serial JS 桥接文档
## 概述
`usbSerial` 对象由 Android 端注入 WebView,提供 USB 串口设备的发现、探测、连接及数据读写能力。
## 注入对象
Android 端在初始化 WebView 时会注入全局对象 `usbSerial`,web 端可直接通过 `window.usbSerial``usbSerial` 访问。
## 方法
### 设备发现
| 方法 | 描述 | 参数 | 返回值 |
| -------------------- | -------------------------- | ---- | ------ |
| startDiscovery() | 开始 USB 设备发现 | 无 | 无 |
| stopDiscovery() | 停止 USB 设备发现 | 无 | 无 |
| getDiscoveredDevices() | 获取已发现的设备列表 | 无 | 空数组 `"[]"`(预留接口) |
### 设备探测
| 方法 | 描述 | 参数 | 返回值 |
| ------------------------------------------------------------ | -------------------------- | ----------------------------------------------------------------------- | ------ |
| deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) | 手动探测指定设备的串口响应 | deviceName: 设备名称 (String)<br>portNumber: 端口号 (Int)<br>baudRate: 波特率 (Int)<br>hexCmd: 探测命令十六进制字符串,不发送时传空串 `""` (String)<br>timeoutMs: 超时毫秒数 (Int) | 无 |
### 设备连接
| 方法 | 描述 | 参数 | 返回值 |
| -------------------------------------------- | ------------------ | ---------------------------------------------------------------------------- | ------ |
| deviceOpen(deviceName, portNumber, baudRate) | 打开设备串口连接 | deviceName: 设备名称 (String)<br>portNumber: 端口号 (Int)<br>baudRate: 波特率 (Int) | 无 |
| deviceClose(deviceName, portNumber) | 关闭设备串口连接 | deviceName: 设备名称 (String)<br>portNumber: 端口号 (Int) | 无 |
### 数据写入
| 方法 | 描述 | 参数 | 返回值 |
| ---------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------- | ------ |
| deviceWrite(deviceName, portNumber, hexData) | 向已连接的设备发送数据 | deviceName: 设备名称 (String)<br>portNumber: 端口号 (Int)<br>hexData: 十六进制字符串 (String) | 无 |
> **注意**:`deviceWrite` 仅在连接状态为 `CONNECTED` 时才会实际发送数据,且需要 hexData 非空。
---
## 回调函数
web 端需要在 `usbSerial` 对象上实现以下回调函数,以接收 Android 端的异步通知。所有回调函数均无返回值。
> 回调函数注册方式:
> ```js
> usbSerial.onDeviceAttached = function(jsonStr) {
> const data = JSON.parse(jsonStr);
> console.log("设备插入:", data);
> };
> ```
### 设备插拔回调
| 方法 | 描述 | 参数 |
| ------------------------------- | ------------------ | -------------------------------- |
| onDeviceAttached(jsonStr) | USB 设备插入时回调 | JSON 字符串,结构见下文 |
| onDeviceDetached(jsonStr) | USB 设备拔出时回调 | JSON 字符串,结构见下文 |
### 设备发现回调
| 方法 | 描述 | 参数 |
| ----------------------------------- | ---------------------------------- | -------------------------------- |
| onDeviceDiscovered(jsonStr) | 设备匹配到探测协议时回调 | JSON 字符串,结构见下文 |
| onDeviceProbeResult(jsonStr) | `deviceProbe()` 执行结果回调 | JSON 字符串,结构见下文 |
### 设备连接回调
| 方法 | 描述 | 参数 |
| ------------------------------- | ------------------------ | -------------------------------- |
| onDeviceOpened(jsonStr) | `deviceOpen()` 执行结果 | JSON 字符串,结构见下文 |
| onDeviceState(jsonStr) | 连接状态变化时回调 | JSON 字符串,结构见下文 |
### 数据接收回调
| 方法 | 描述 | 参数 |
| --------------------------- | -------------------------- | -------------------------------- |
| onDeviceData(jsonStr) | 收到设备上报数据时回调 | JSON 字符串,结构见下文 |
---
## 数据结构
### onDeviceAttached 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"deviceId": 1002,
"ports": [1, 2]
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | ------------------------ |
| deviceName | String | 设备路径名称 |
| deviceId | Number | USB 设备 ID |
| ports | Array | 可用串口端口号列表 |
### onDeviceDetached 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"deviceId": 1002
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | ------------------------ |
| deviceName | String | 设备路径名称 |
| deviceId | Number | USB 设备 ID |
### onDeviceDiscovered 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"port": 1,
"probeId": "my-probe",
"baudRate": 9600,
"responseHex": "01020304"
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | -------------------------------- |
| deviceName | String | 设备路径名称 |
| port | Number | 端口号 |
| probeId | String | 匹配的探测协议 ID |
| baudRate | Number | 探测到的波特率 |
| responseHex | String | 设备响应数据的十六进制字符串 |
### onDeviceProbeResult 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"port": 1,
"baudRate": 115200,
"success": true,
"responseHex": "AABBCC",
"error": ""
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | ------------------------------------------ |
| deviceName | String | 设备路径名称 |
| port | Number | 端口号 |
| baudRate | Number | 探测使用的波特率 |
| success | Boolean | 探测是否成功 |
| responseHex | String | 设备响应数据的十六进制字符串(失败时为空串) |
| error | String | 错误信息(成功时为空串) |
### onDeviceOpened 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"port": 1,
"success": true,
"error": ""
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | ------------------------------------------ |
| deviceName | String | 设备路径名称 |
| port | Number | 端口号 |
| success | Boolean | 打开是否成功 |
| error | String | 错误信息(成功时为空串);可能值:`"Device not found"`、`"Port not found"` |
### onDeviceState 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"port": 1,
"state": "CONNECTED"
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | ------------------------------------------------------ |
| deviceName | String | 设备路径名称 |
| port | Number | 端口号 |
| state | String | 连接状态:`"CONNECTED"` \| `"CONNECTING"` \| `"DISCONNECTED"` |
### onDeviceData 回调
```json
{
"deviceName": "/dev/bus/usb/001/002",
"port": 1,
"hexData": "01020304AABB"
}
```
| 字段 | 类型 | 描述 |
| ------------ | -------- | ------------------------------ |
| deviceName | String | 设备路径名称 |
| port | Number | 端口号 |
| hexData | String | 收到的数据,十六进制大写字符串 |
---
## 连接状态枚举
| 状态 | 触发时机 |
| ----------------- | ------------------------------------- |
| `CONNECTING` | `deviceOpen` 后正在尝试打开串口 |
| `CONNECTED` | 串口打开成功,可以发送和接收数据 |
| `DISCONNECTED` | 串口未连接、连接失败或被拔出 |
---
## 典型使用流程
### 1. 启动发现
```js
// 注册回调
usbSerial.onDeviceAttached = function(json) {
const dev = JSON.parse(json);
console.log("发现设备:", dev.deviceName, "ports:", dev.ports);
};
usbSerial.onDeviceDetached = function(json) {
const dev = JSON.parse(json);
console.log("设备拔除:", dev.deviceName);
};
usbSerial.onDeviceState = function(json) {
const s = JSON.parse(json);
console.log("连接状态:", s.deviceName, s.port, s.state);
};
usbSerial.onDeviceData = function(json) {
const d = JSON.parse(json);
console.log("收到数据:", d.hexData);
};
// 开始发现
usbSerial.startDiscovery();
```
### 2. 手动探测设备
```js
usbSerial.onDeviceProbeResult = function(json) {
const result = JSON.parse(json);
if (result.success) {
console.log("探测成功, 响应:", result.responseHex);
} else {
console.log("探测失败:", result.error);
}
};
// 对 /dev/bus/usb/001/002 端口1 以 9600 波特率发 "010300000001" 超时 500ms
usbSerial.deviceProbe("/dev/bus/usb/001/002", 1, 9600, "010300000001", 500);
```
### 3. 打开设备进行通信
```js
usbSerial.onDeviceOpened = function(json) {
const result = JSON.parse(json);
if (result.success) {
console.log("设备打开成功");
// 发送数据(十六进制字符串)
usbSerial.deviceWrite(result.deviceName, result.port, "010300000001");
} else {
console.log("打开失败:", result.error);
}
};
usbSerial.deviceOpen("/dev/bus/usb/001/002", 1, 9600);
```
### 4. 关闭设备
```js
usbSerial.deviceClose("/dev/bus/usb/001/002", 1);
```
---
## 注意事项
1. **串口参数固定**:数据位 8、停止位 1、无校验位,不可配置。
2. **设备键值**:内部以 `"deviceName:portNumber"` 唯一标识一个连接,同一设备不同端口可同时打开。
3. **拔除自动清理**:设备拔出时对应连接会自动销毁,无需手动调用 `deviceClose`
4. **线程安全**:所有方法可以在 JS 主线程直接调用,Android 侧会处理线程调度。
5. **`getDiscoveredDevices`** 为预留接口,当前仅返回 `"[]"`,后续版本会实现。

80
preload.js

@ -102,7 +102,87 @@ const app = { @@ -102,7 +102,87 @@ const app = {
}
}
/**
* USB Serial JS 桥接对象
* 静态页面可直接使用 usbSerial window.usbSerial
* 接口规范: docs/webview-usb-serial.md
*/
const usbSerialCallbacks = {
onDeviceAttached: null,
onDeviceDetached: null,
onDeviceDiscovered: null,
onDeviceProbeResult: null,
onDeviceOpened: null,
onDeviceState: null,
onDeviceData: null
}
// 监听主进程发送的 usb 事件,转换为回调调用
function setupUsbSerialListeners() {
const events = [
['usb-callback-device-attached', 'onDeviceAttached'],
['usb-callback-device-detached', 'onDeviceDetached'],
['usb-callback-device-discovered', 'onDeviceDiscovered'],
['usb-callback-device-probe-result', 'onDeviceProbeResult'],
['usb-callback-device-opened', 'onDeviceOpened'],
['usb-callback-device-state', 'onDeviceState'],
['usb-callback-device-data', 'onDeviceData']
]
for (const [eventName, cbName] of events) {
ipcRenderer.on(eventName, (_event, jsonStr) => {
const cb = usbSerialCallbacks[cbName]
if (typeof cb === 'function') {
cb(jsonStr)
}
})
}
}
setupUsbSerialListeners()
const usbSerial = {
// --- 回调属性(可读写)---
get onDeviceAttached() { return usbSerialCallbacks.onDeviceAttached },
set onDeviceAttached(fn) { usbSerialCallbacks.onDeviceAttached = fn },
get onDeviceDetached() { return usbSerialCallbacks.onDeviceDetached },
set onDeviceDetached(fn) { usbSerialCallbacks.onDeviceDetached = fn },
get onDeviceDiscovered() { return usbSerialCallbacks.onDeviceDiscovered },
set onDeviceDiscovered(fn) { usbSerialCallbacks.onDeviceDiscovered = fn },
get onDeviceProbeResult() { return usbSerialCallbacks.onDeviceProbeResult },
set onDeviceProbeResult(fn) { usbSerialCallbacks.onDeviceProbeResult = fn },
get onDeviceOpened() { return usbSerialCallbacks.onDeviceOpened },
set onDeviceOpened(fn) { usbSerialCallbacks.onDeviceOpened = fn },
get onDeviceState() { return usbSerialCallbacks.onDeviceState },
set onDeviceState(fn) { usbSerialCallbacks.onDeviceState = fn },
get onDeviceData() { return usbSerialCallbacks.onDeviceData },
set onDeviceData(fn) { usbSerialCallbacks.onDeviceData = fn },
// --- 方法 ---
startDiscovery() {
ipcRenderer.send('usb-start-discovery')
},
stopDiscovery() {
ipcRenderer.send('usb-stop-discovery')
},
getDiscoveredDevices() {
return ipcRenderer.invoke('usb-get-discovered')
},
deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) {
ipcRenderer.send('usb-device-probe', { deviceName, portNumber, baudRate, hexCmd, timeoutMs })
},
deviceOpen(deviceName, portNumber, baudRate) {
ipcRenderer.send('usb-device-open', { deviceName, portNumber, baudRate })
},
deviceClose(deviceName, portNumber) {
ipcRenderer.send('usb-device-close', { deviceName, portNumber })
},
deviceWrite(deviceName, portNumber, hexData) {
ipcRenderer.send('usb-device-write', { deviceName, portNumber, hexData })
}
}
// 安全暴露 API 给渲染进程
contextBridge.exposeInMainWorld('net', networkUtil)
contextBridge.exposeInMainWorld('appSensor', appSensor)
contextBridge.exposeInMainWorld('app', app)
contextBridge.exposeInMainWorld('usbSerial', usbSerial)

239
usb-serial-bridge.js

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