1. 概述
在医疗临床试验 SaaS 系统中,前端需要与各类体征测量设备(血压计、体温计、身高体重秤等)进行实时数据通信。由于浏览器安全策略限制,JavaScript 无法直接通过 HID 协议操作 USB 设备,因此需要借助本地中间服务(Local Middleware Service)作为桥接层。
本文以 type2.js 为例,详细解析基于「HTTP + WebSocket」的 HID 设备对接方案的设计与实现。
相关文件:
src/components/DeviceSetting/type2.js--- 中间服务连接实现src/components/DeviceSetting/type1.js--- 串口直连实现(Web Serial API)src/components/DeviceSetting/parseDeviceData.js--- 设备数据解析引擎src/components/DeviceSetting/index.vue--- 设备选择与连接管理 UI
2. 架构设计
2.1 整体架构
scss
┌─────────────────────────────────────────────────────────────┐
│ 浏览器 (WebApp) │
│ ┌───────────────────────┐ ┌─────────────────────────────┐ │
│ │ type1.js (串口直连) │ │ type2.js (中间服务模式) │ │
│ │ Web Serial API │ │ HTTP + WebSocket │ │
│ └──────────┬────────────┘ └──────────────┬──────────────┘ │
│ │ │ │
│ ┌──────┴──────┐ ┌───────┴───────┐ │
│ │ 串口数据流 │ │ HTTP/WS 通信 │ │
│ └──────┬──────┘ └───────┬───────┘ │
└─────────────┼───────────────────────────────┼──────────────┘
│ │
┌─────────┴──────────┐ ┌────────────┴────────────┐
│ USB 串口设备 │ │ 本地中间服务 (Electron) │
│ (串口直连) │ │ HTTP :17001 │
└────────────────────┘ │ WebSocket :17001/ws │
│ HID 协议操作 USB 设备 │
└─────────────────────────┘
系统支持两种连接方式:
| 连接方式 | 适用场景 | 技术方案 | 浏览器要求 |
|---|---|---|---|
| type1 | 串口设备 | Web Serial API | Chrome 89+ |
| type2 | HID 设备 / 跨平台 | 本地中间服务 + WebSocket | 通用 |
2.2 选择策略
在 index.vue 中,根据 equipmentType 字段动态选择连接实现:
js
// equipmentType 为 2 的设备使用中间服务方式,其余使用串口直连方式
const connectImpl = props.deviceType == 2 ? connectDeviceType2 : connectDeviceType1
3. type2.js 深度解析:中间服务连接模式
3.1 模块导出与初始化
js
export default function connectDevice({ openSuccess, dataCallback, errorCallback }) {
const app = getApp()
let conecteStatus = ref(false) // 连接状态
let deviceInfo = app.globalData._deviceInfo || null // 设备信息缓存
let ws = null // WebSocket 实例
let serverUrl = '' // 中间服务地址
// 如果设备信息存在,尝试直接连接
if (deviceInfo) {
connect(deviceInfo)
}
// ...
}
设计要点:
- 状态驱动 :
conecteStatus是响应式引用(ref),驱动 UI 层显示连接/断开状态 - 缓存复用 :
app.globalData._deviceInfo缓存上次成功的设备配置,避免每次打开页面都重新选择设备 - 回调注入:三个回调函数(openSuccess / dataCallback / errorCallback)由调用方注入,解耦连接逻辑与业务逻辑
3.2 设备配置解析
js
function getMiddleConfig(deviceInfo) {
const params = deviceInfo.equipmentParameters || {}
const { serverUrl: url, ip, port, vendorId, vid, productId, pid, hidPath } = params
// 中间服务地址构建
let baseUrl = url || (ip && port ? `http://${ip}:${port}` : '') || 'http://localhost:17001'
// 兼容未带协议前缀的地址
if (baseUrl && !/^https?:\/\//i.test(baseUrl)) {
baseUrl = 'http://' + baseUrl
}
// VID/PID 解析
let vId = vendorId || vid
let pId = productId || pid
if (hidPath) {
const [hvid, hpid] = String(hidPath).split('&')
vId = hvid || vId
pId = hpid || pId
}
return { serverUrl: baseUrl, vendorId: vId, productId: pId }
}
地址解析优先级:
| 配置来源 | 示例 | 优先级 |
|---|---|---|
serverUrl 完整地址 |
http://192.168.1.100:17001 |
最高 |
ip + port 组合 |
http://192.168.1.100:17001 |
中 |
| 本地默认地址 | http://localhost:17001 |
最低(兜底) |
HID 设备标识解析:
hidPath字段格式为"VID&PID"- 兼容字段
vendorId/vid和productId/pid作为独立字段传入 - 最终组合为
{ vendorId, productId }传递给中间服务用于 USB 设备识别
3.3 连接建立
js
async function connect(options) {
const { serverUrl: baseUrl, vendorId, productId } = getMiddleConfig(options)
if (!baseUrl) {
errorCallback('未配置中间服务地址')
return
}
serverUrl = baseUrl
try {
const res = await fetch(`${baseUrl}/api/connect`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vendorId, productId })
})
const d = await res.json()
if (d.online) {
conecteStatus.value = true
app.globalData._deviceInfo = options
openWs() // 建立 WebSocket 监听
openSuccess(options) // 回调通知上层
} else {
errorCallback('设备连接失败')
}
} catch (e) {
errorCallback(e) // 中间服务未启动等网络异常
}
}
连接流程:
- 配置解析 → 获取中间服务地址和 HID 标识
- HTTP POST → 请求
/api/connect通知中间服务连接 USB 设备 - 状态判断 → 中间服务返回
{ online: true/false }表示设备是否在线 - 成功处理 → 更新状态、缓存配置、建立 WebSocket 通道、回调通知
- 失败处理 → 错误回调,展示错误信息(中间服务未启动、设备未插拔等)
3.4 WebSocket 数据通道
js
function openWs() {
if (ws) {
ws.close()
ws = null
}
// HTTP → WebSocket 协议升级
const wsUrl = serverUrl.replace(/^http(s)?:/, (_, s) => (s ? 'wss:' : 'ws:')) + '/ws'
ws = new WebSocket(wsUrl)
ws.onopen = () => {}
ws.onclose = () => {
conecteStatus.value = false
}
ws.onerror = () => {}
ws.onmessage = (ev) => {
dataCallback(
parseDeviceData(
app.globalData._deviceInfo?.equipmentParameters,
ev.data,
app.globalData._deviceInfo?.equipmentType
)
)
}
}
WebSocket 协议升级:
bash
http://localhost:17001 → ws://localhost:17001/ws
https://192.168.1.100 → wss://192.168.1.100/ws
数据通道设计模式:
| 通道 | 用途 | 方向 | 协议 |
|---|---|---|---|
| HTTP | 连接控制、设备初始化 | 浏览器 → 中间服务 | POST /api/connect |
| WebSocket | 实时数据推送 | 中间服务 → 浏览器 | ws://host/ws |
这种「HTTP 控制 + WebSocket 推送」的双通道模式,是物联网设备对接的经典模式:
- HTTP 连接用于一次性控制指令(连接、断开、配置)
- WebSocket 用于持续数据流(测量数据推送)
- 协议分离,职责清晰,WebSocket 断开不影响 HTTP 重连
3.5 断开连接
js
async function closeSerialPort(callback = function () {}) {
const status = conecteStatus.value
if (conecteStatus.value) {
conecteStatus.value = false
if (ws) {
ws.close()
ws = null
}
if (serverUrl) {
try {
// 发送空 VID/PID 通知中间服务断开设备
await fetch(`${serverUrl}/api/connect`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vendorId: 'FFFFFFFF', productId: 'FFFFFFFF' })
})
} catch (e) {
console.log('断开连接失败', e)
}
}
}
callback(status)
}
断开策略:
FFFFFFFF作为特殊的 VID/PID 标识,通知中间服务释放 USB 设备句柄- 先关闭 WebSocket,再发送 HTTP 断开请求,避免资源泄漏
- 调用
callback返回断开前的连接状态,供上层做状态清理
3.6 重连机制
js
async function initDevice(options) {
if (conecteStatus.value) {
await closeSerialPort() // 先断开旧连接
}
await connect(options) // 重新连接
}
先断后连的「clean reconnect」模式,确保设备状态始终一致,避免重复连接导致资源冲突。
4. 设备数据解析引擎
parseDeviceData.js 提供两种解析模式:
4.1 正则解析(parsingMethod = 1)
js
function parseByRegex(option, str) {
const { fieldRules } = option || {}
;(fieldRules || []).forEach((item) => {
const regexp = new RegExp(item.regexp)
const res = str.match(regexp) || []
let val = res[item.regexpIndex || 0]
// 小数点处理、前缀零处理...
obj[item.code] = { name: item.name, value: val }
})
return obj
}
适用于设备返回固定格式的字符串,如 "SYS:120, DIA:80, HR:75"。
4.2 位置解析(parsingMethod = 2)
js
function parseByPosition(option, str) {
const { splitChar, fieldRules } = option || {}
const arr = str.split(splitChar)
const obj = {}
fieldRules.forEach((item) => {
let val = arr[item.regexpIndex]
// 小数点处理、前缀零处理...
obj[item.code] = { name: item.name, value: val }
})
return obj
}
适用于设备返回固定分隔符的字符串,如 "120,80,75"。
4.3 数据解析配置示例
设备配置中的 equipmentParameters 字段包含解析规则:
json
{
"equipmentParameters": {
"parsingMethod": 1,
"splitChar": ",",
"fieldRules": [
{ "code": "SYS", "name": "收缩压", "regexp": "SYS:(\\d+)", "regexpIndex": 1 },
{ "code": "DIA", "name": "舒张压", "regexp": "DIA:(\\d+)", "regexpIndex": 1 },
{ "code": "HR", "name": "心率", "regexp": "HR:(\\d+)", "regexpIndex": 1 }
],
"decimalPlaces": 0,
"prefixZero": false,
"decimalZero": 2
}
}
5. 与 type1(串口直连)的对比
| 对比维度 | type1(串口直连) | type2(中间服务模式) |
|---|---|---|
| 底层 API | Web Serial API | HTTP + WebSocket |
| 浏览器要求 | Chrome 89+ | 通用(所有浏览器) |
| 设备兼容性 | 仅串口设备 | HID / 串口 / 蓝牙 |
| 安装依赖 | 无 | 需安装本地中间服务 |
| 连接控制 | navigator.serial.requestPort() |
POST /api/connect |
| 数据接收 | serialPort.readable.getReader() |
WebSocket.onmessage |
| 数据粘包 | 需要防抖 + 超时机制 | 中间服务负责分包 |
| 跨平台 | 仅桌面 Chrome | 全平台(依赖中间服务) |
| 安全性 | 用户主动选择端口 | 需白名单配置 |
6. 中间服务技术要求
本地中间服务需实现:
| 端点 | 方法 | 功能 | 请求体 |
|---|---|---|---|
/api/connect |
POST | 连接/断开 HID 设备 | { vendorId, productId } |
/ws |
WebSocket | 推送设备测量数据 | 原始数据字符串 |
中间服务技术选型建议:
- Electron + node-hid(跨平台 HID 操作)
- Python + pywinusb/hidapi(Windows 环境)
- C# + HidLibrary(Windows 环境)
7. 错误处理与异常场景
7.1 错误分类
| 错误类型 | 触发条件 | 处理方式 |
|---|---|---|
| 中间服务未启动 | fetch 抛出网络异常 |
弹窗提示"请启动本地中间服务" |
| 设备未连接 | 返回 { online: false } |
提示"请插入设备后重试" |
| WebSocket 断开 | onclose 触发 |
重置连接状态,显示"设备已断开" |
| 设备类型不匹配 | 返回的 equipmentType 与预期不符 |
自动断开并提示"设备类型不匹配" |
7.2 错误展示
vue
<!-- 错误提示弹窗 -->
<ts-modal v-model:visible="showError" title="设备连接失败" width="400px" :footer="false">
<ts-modal-content>
<view style="font-weight: bold">请将下方的"错误提示"拍照发送给系统工程师:</view>
<view style="background-color: #f5f5f5; padding: 10px; border-radius: 5px; margin-top: 10px">
{{ errorMsg }}
</view>
</ts-modal-content>
</ts-modal>
8. 总结
type2.js 实现的「本地中间服务 + HTTP + WebSocket」模式,是一种成熟、可靠的 HID 设备对接方案:
- 跨浏览器兼容:不依赖 Web Serial / WebUSB 等新兴 API,所有浏览器均可使用
- 职责分离:浏览器负责 UI 和业务逻辑,中间服务负责硬件协议交互
- 数据流清晰:HTTP 控制连接生命周期,WebSocket 承载实时数据流
- 可扩展性:中间服务可对接任意类型的物理设备(HID、串口、蓝牙),对前端透明
- 错误隔离:设备连接异常不会影响主应用稳定性
这种模式适用于医疗临床试验、工业物联网、实验室设备对接等需要与 USB 外设通信的 Web 应用场景。