2a641b1eff
## USB 整体下线 把 USB 功能真删掉, 不是只藏 UI: - 删 `src/main/remote/usb.ts` + `src/renderer/src/components/remote/UsbPanel.vue` - 删 `usb` (node-usb) 和 `node-hid` 两个依赖 —— 查过, 本来就没被真正用起来 - `asarUnpack` 去掉 node-usb/usb/node-hid 的规则 - ChatView 的 tab 由 "USB" 改成 "串口" ## 串口映射 + 串口助手 (新) `src/main/remote/serial.ts` (取代 usb.ts) 只做串口, 一个文件两件事: - **串口映射**: 把一端的真实串口映射成另一端可用的虚拟串口 (Linux/macOS 用 socat 建 PTY 配对, Windows 用 com0com 建 COM 对)。 方向做成 "我用对方的" / "对方用我的" 两种, 两边都能发起。 - **串口助手**: 不建虚拟口时, 应用自己当助手 —— 字节流直接显示, 能发文本/HEX, 能追加 LF, 能循环发送, 能清屏/存日志。 `src/renderer/src/components/remote/SerialPanel.vue` (新) 把两者做在一个面板里: 设备表 (串口 / VID:PID / 产品 / 制造商)、串口参数 (波特率/数据位/停止位/校验)、 TX/RX 时间戳 + hex + ascii 视图、虚拟口路径横幅 (可一键复制)。 ## 串口助手要能调 Linux Linux 上调串口最常见的两个坑, 都做成产品功能: - **没权限**: 诊断 dialout 组 + socat + 串口列表, 一键提权修复 (`sudo usermod -aG dialout $USER`, 提示要重新登录才生效)。 "设备没插" 和 "没权限" 分开报错, 不再混成一句 "打开失败"。 - **缺 socat**: 建虚拟口前先探 socat, 没有就弹安装指引 (apt/dnf), 免得 attach 做到一半才失败。 平台化文案也分开了: Linux 提示 dialout, Windows 提示设备管理器, macOS 提示隐私授权。 ## 线上格式一个字节没动 (兼容性关键) 局域网里常有一台机器跑旧版本, 所以: - `PROTOCOL_VERSION` 仍然 = 1 - WS 帧类型仍然叫 `'usb'` - 只把内部类型改名 (`SerialDeviceInfo` / `SerialAttachConfig` / ...) 代价: 混版本时已下线的 USB 字节桥不可用。换来: 新旧版本聊天照常, 串口映射也照常, 不会因为升协议被踢下线。**别手贱顺手改对** (AGENTS.md 坑 #10)。 ## 其它 - 主进程 `output` 事件带方向 (rx/tx), 助手才能分清"我发的"和"设备回的"; 客户端自己发出的字节不回显成 rx (无回环, S5 用例守住) - 虚拟串口 (vcom) 模式下 app 不读内部端, 避免和 socat 抢数据 - `src/main/permissions.ts` 重写成串口专用, 删掉 USB/HID 相关诊断与修复 - 保留 `src/main/remote/vcom.ts` (socat/com0com 配对) 不动, 与 serial.ts 分离 ## 验证 - npm run typecheck ✅ - npm run selftest ✅ 79 通过 / 0 失败 (USB 用例整体换成 S1-S9 串口用例) - npm run build ✅ - npm run package:linux ✅ 新 deb (84MB) - npm run selftest:package ✅ 4/4 (改成校验 serialport 原生绑定已解包) 真机 (CDP 驱动真实窗口) 验证过: 串口 tab 能打开, 设备表列到 32 个 /dev/ttyS*, 助手区 + hex 区在, 波特率/数据位/停止位/校验/循环发送/清屏/保存日志 按钮齐全, 方向切换会换设备列表 (我用对方的 = 对方设备, 对方用我的 = 本机设备)。
86 lines
4.0 KiB
Markdown
86 lines
4.0 KiB
Markdown
# 连接状态模型 (在线 / 通道可用)
|
||
|
||
聊天框顶部那个小圆点 + 旁边的文字,到底表示什么?这份文档说清楚,
|
||
以及为什么状态必须**拉取式**同步而不是只靠事件推送。
|
||
|
||
## 1. 两种"在线",别混
|
||
|
||
| 概念 | 含义 | 数据来源 | 表现 |
|
||
|---|---|---|---|
|
||
| **设备在线** | 对方机器还在局域网里活着 | UDP 广播发现 (`discovery.ts`),15s 没心跳算 lost | 侧边栏"在线"绿标 |
|
||
| **通道可用** | 我们和对方之间有活的 WS,能收发消息 | `outgoing` (我们连过去的) 或 `inbound` (对方连过来的) 任一活着 | 聊天框顶部绿点 |
|
||
|
||
- 设备在线 ≠ 通道可用。对方可能开着防火墙挡住 WS,UDP 还在广播。
|
||
- 通道可用 **不等于** 设备在线:对端进程刚起、还没被 UDP 发现到,但已经连上来了。
|
||
|
||
**通道可用**的判定(`src/main/ws-state.ts` 的 `computeWsState`):
|
||
|
||
```ts
|
||
alive = clientStatus[id] === 'open' || server.isConnected(id)
|
||
// ↑ outgoing: 我们主动连的 ↑ inbound: 对方连过来的
|
||
```
|
||
|
||
两个方向是独立的两条腿。早期实现只看 outgoing,结果对方只连进来、
|
||
我们没连上时,UI 会误报"等待重连"——而消息其实收发正常。
|
||
|
||
## 2. 状态必须能"随时拉取"
|
||
|
||
启动顺序里有一处天然竞态(`src/main/index.ts`):
|
||
|
||
```
|
||
discovery.start() ← 先启动
|
||
chatClient.connectTo(...) ← 已入库设备, 立刻发起 WS 连接 (setTimeout 0)
|
||
createMainWindow() ← 之后才建窗口
|
||
```
|
||
|
||
WS 通常几十毫秒内就握手成功 → 主进程广播 `device:wsState` →
|
||
**可渲染层还没加载完、还没注册监听 → 这条事件被整个丢掉**。
|
||
之后再没有 'open' 事件了(连接一直保持着),UI 就永久停在"等待重连…"。
|
||
|
||
**结论**:状态事件只管"变化通知",不能当"当前状态"的唯一来源。
|
||
任何时候想知道现在的状态,都得能主动查一次。
|
||
|
||
| 时机 | 谁来拉 | 拉什么 |
|
||
|---|---|---|
|
||
| 渲染层 mount | 渲染层 | `ipcRenderer.invoke('app:state')` → `{ ws: {...} }` |
|
||
| 主进程 `boot:ready` | 主进程推 | 带上同一份 `ws` 快照 |
|
||
| 连接断开 | 主进程推 | 全量快照 `{ ws: {...} }` |
|
||
|
||
`device:wsState` 事件带的是**全量快照**而不是"某个设备变了"的增量:
|
||
增量事件会漏(订阅晚了就没了),全量快照任何时刻都能自愈。
|
||
|
||
渲染层还有两条纪律(`src/renderer/src/App.vue`):
|
||
|
||
1. **先注册监听,再做 await**。任何 `await` 都会让出事件循环,期间推来的事件就丢了。
|
||
2. 快照里的设备 = "没连上"(按 false 处理),不做增量合并,避免留下过期状态。
|
||
|
||
## 3. 文案
|
||
|
||
| 条件 | 显示 |
|
||
|---|---|
|
||
| 通道可用 | `在线 · 192.168.0.7`(绿点) |
|
||
| 设备在线但通道不通 | `在线 · 192.168.0.7 (连接中…)`(黄点,后台在 backoff 重连) |
|
||
| 都不可用 | `离线 · 12:05`(灰点) |
|
||
|
||
远程功能(终端 / 转发 / 串口 / 网盘)都走 WS 通道,所以入口按钮在
|
||
`设备离线 && 通道不通` 时才禁用——只看设备在线会把"能连但没连上"的情况误判成可用。
|
||
|
||
## 4. 回归测试
|
||
|
||
`npm run selftest` 里的 `[通道状态]` 组(W1–W3):
|
||
|
||
- **W1** 通道状态可随时拉取(连接早已建好,现在才查也查得到)
|
||
- **W2** 只有 inbound 通(对方连我,我没主动连)也算通道可用
|
||
- **W3** 两条腿都断了 → 状态回落 false
|
||
|
||
## 5. 相关文件
|
||
|
||
| 文件 | 作用 |
|
||
|---|---|
|
||
| `src/main/ws-state.ts` | `computeWsState` 纯函数(无 electron 依赖,方便自测) |
|
||
| `src/main/chat-server.ts` | `isConnected(deviceId)` — 查 inbound 连接 |
|
||
| `src/main/chat-client.ts` | `getStatus()` — 各对端的 outgoing 状态 |
|
||
| `src/main/ipc.ts` | `collectWsState()` / `broadcastWsState()` / `app:state` IPC |
|
||
| `src/renderer/src/stores/device.ts` | `wsOpen` 映射 + `setWsStates()` 全量替换 |
|
||
| `src/renderer/src/components/ChatView.vue` | 顶部的点与文案 |
|