Files
LocalNetMsg/docs/connection-status.md
xiaoshuai 2a641b1eff feat(serial): USB 页面下线, 改成串口映射 + 串口助手
## 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 区在, 波特率/数据位/停止位/校验/循环发送/清屏/保存日志 按钮齐全,
方向切换会换设备列表 (我用对方的 = 对方设备, 对方用我的 = 本机设备)。
2026-10-21 18:04:55 +08:00

86 lines
4.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 连接状态模型 (在线 / 通道可用)
聊天框顶部那个小圆点 + 旁边的文字,到底表示什么?这份文档说清楚,
以及为什么状态必须**拉取式**同步而不是只靠事件推送。
## 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` | 顶部的点与文案 |