# QSJNIC Service 完整分析报告

> 依据八维度代码审查框架对 `workspace/jyq/qsjnic/service` 进行系统性审查。
> 覆盖：数据流图、时序图、状态机、决策树、资源所有权、故障传播链、跨层契约、性能时延预算。
> 不修改代码，仅做静态分析。覆盖 Java 层与 C 层关键模块。

---

## 一、数据流图 — 从 MCU 到 App

### 1.1 电压/按键上报通路（MCU → App）

```
[MCU 设备]
   │  UART 帧 (0xFF 0xAA ...)
   ▼
[/dev/ttyS7]  ──────────────────────────────────────────────────
   │                                                            │
   ▼                                                            │
[C: live_reader_thread]  (mcu_transport.c:live_reader_thread)   │
   │  serial_read_some() → mcu_parser_feed()                    │
   ▼                                                            │
[C: live_reader_consume]                                        │
   │  1) pthread_mutex_lock(&ctx->lock)                         │
   │  2) mcu_protocol_on_frame() → auto-ACK (FT_ACK)            │
   │  3) pthread_mutex_unlock(&ctx->lock)                       │
   │  4) event_queue_push()                                     │
   │  5) notify_cb (jni_notify_cb)  ←── 线程切换点              │
   ▼                                                            │
[C++: jni_notify_cb] (jni_bridge.cpp)                           │
   │  AttachCurrentThread (若未附加)                            │
   │  NewByteArray(body_len)                                    │
   │  SetByteArrayRegion(jPayload, body)                        │
   │  CallStaticVoidMethod(FrameRouter.onMcuFrame, jPayload)    │
   │  DeleteLocalRef(jPayload)                                  │
   ▼                                                            │
[Java: FrameRouter.onMcuFrame] (在 C reader 线程执行!)          │
   │  payload.clone()  ── 拷贝点 ①                              │
   │  HANDLER.post(dispatch) ── 投递到 Looper 线程              │
   ▼                                                            │
[Java: FrameRouter.dispatch] (FrameRouter HandlerThread)        │
   │  broadcastRawFrame() → RemoteCallbackList (Binder IPC)     │
   │  bus.onFrame() → DomainBus → Feature.onFrame()             │
   ▼                                                            │
[Java: VehicleFeature.onFrame / KeyFeature.onFrame]             │
   │  解析 body → 更新 snapshot / state                         │
   │  broadcastTyped() → RemoteCallbackList.beginBroadcast()    │
   ▼                                                            │
[Binder IPC] → [App 进程] → [IMcuVehicleListener.onVoltage]     │
```

**各环节标注：**

| 环节 | 数据格式 | 线程 | 是否有拷贝 | 备注 |
|------|----------|------|-----------|------|
| MCU → UART | 原始字节流 | MCU 固件 | - | - |
| C reader | `uint8_t readbuf[256]` | `live_reader_thread` | 无 | 直接 read 到栈 buffer |
| parser → frame | `McuFrame` (含 `raw[2048]`) | 同 reader 线程 | 有 (`memcpy` 到 frame) | `mcu_decode_complete_frame` 内 `memcpy(frame->raw, raw, raw_len)` |
| auto-ACK | `uint8_t frame[MCU_MAX_FRAME_LEN]` | 同 reader 线程，持 `ctx->lock` | 有 (`mcu_build_frame_bytes`) | 构建 ACK 帧写回 UART |
| JNI notify | `jbyteArray` | C reader 线程（已 Attach JVM） | **有 (NewByteArray)** | `jni_notify_cb` 中为 body 新建 Java byte[] |
| FrameRouter.onMcuFrame | `byte[] payload` → `byte[] copy` | C reader 线程（同步回调） | **有 (clone)** | `payload == null ? null : payload.clone()` |
| dispatch → Feature | `byte[] body` | `FrameRouter` HandlerThread | 无（引用传递） | `CopyOnWriteArrayList` 遍历，无额外拷贝 |
| Feature → broadcast | `VehicleSnapshot` / `RadioState` 深拷贝 | `FrameRouter` HandlerThread | **有 (深拷贝)** | `snapshot()` 内 `synchronized(snapshot)` + `new VehicleSnapshot(snapshot)` |
| Binder → App | AIDL Parcel | Binder 线程池 | 有 (Parcel 序列化) | AIDL 自动生成 |

### 1.2 App 调 setBand → MCU 执行 → 结果回 App

```
[App]
   │  IMcuService.setBand(band)
   ▼
[Binder IPC] → [McuBinder.setBand] (service 进程 Binder 线程池)
   │  McuLink.get().sendAndWaitAck(MSG_TYPE_RADIO, SET_BAND, payload, 1500)
   ▼
[Java: McuLink.sendAndWaitAck]
   │  build payload (byte[2+bodyLen])
   │  nativeMcuLinkSendAndWaitAck (JNI)
   ▼
[C++: nativeMcuLinkSendAndWaitAck] (jni_bridge.cpp)
   │  pthread_mutex_lock(&g_link_mutex)
   │  GetByteArrayElements / ReleaseByteArrayElements
   │  mcu_link_send_data_frame() → mcu_link_wait_for_ack()
   │  pthread_mutex_unlock(&g_link_mutex)
   ▼
[C: mcu_link_send_data_frame] (mcu_transport.c)
   │  pthread_mutex_lock(&ctx->lock)
   │  live_write_frame_locked() → serial_write_all()
   │  pthread_mutex_unlock(&ctx->lock)
   ▼
[/dev/ttyS7] → [MCU] → [ACK 帧]
   ▼
[C: mcu_link_wait_for_ack]
   │  event_queue_wait_pop() (阻塞等待 ACK)
   │  FT_ACK && seq==expected ? true : deferred restore
   ▼
结果返回 Java → McuBinder: seq>=0 ? OK : ERR_TIMEOUT
   ▼
[Binder IPC] → [App] 收到 int 错误码
```

### 1.3 重点检查结论

1. **拷贝漏了吗？**
   - ✅ **安全**。`jni_notify_cb` 中 `NewByteArray` 新建了 Java 数组；`FrameRouter.onMcuFrame` 中 `payload.clone()` 确保了跨线程安全。但注意：`jni_notify_cb` 是在 C reader 线程直接回调 Java，回调结束后 `DeleteLocalRef` 立即释放，没有泄漏。
   - ⚠️ **隐患**：`ReleaseByteArrayElements` 在 JNI bridge 的 `nativeMcuLinkSendDataFrame` / `nativeMcuLinkSendAndWaitAck` 中使用 `JNI_ABORT` 模式（不拷贝回 Java 数组），这是合理的（只读）。但如果未来修改代码忘记改回 `0`，可能导致数据不一致。

2. **线程切换了吗？**
   - ✅ **设计正确**。C reader 线程 → `HANDLER.post` → FrameRouter HandlerThread → Feature 解析 → Binder 线程池。
   - ⚠️ **关键隐患**：`jni_notify_cb` 在 C reader 线程中同步执行，包括 `AttachCurrentThread`、`NewByteArray`、`CallStaticVoidMethod`。如果 JVM 操作耗时（如 GC 暂停），会**阻塞 C reader 线程**，导致 UART 读取和 auto-ACK 延迟。虽然代码注释已说明 "Must not block"，但设计上应评估是否需要在 JNI 层再加一个无锁队列做二次解耦。

3. **数据格式转换正确吗？**
   - ✅ C 的 `uint8_t` 通过 `(jint)(frame->payload[0] & 0xFFU)` 正确转为 Java `int`，避免符号扩展。
   - ✅ `FrameRouter.handlePublic` 中 `p[0] & 0xFF` 处理无符号字节。
   - ⚠️ **隐患**：`mcu_transport.c:live_reader_consume` 中 `log_update_ack` 判断 `frame->payload[0] == MT_UPDATE`，但 `MT_UPDATE` 值为 `0x07`，这是 msgType 的位置。对于非 DATA 帧（如 FT_HB），`payload_len` 可能为 0，此时访问 `frame->payload[0]` 越界？检查代码：`log_update_ack` 的赋值在 `if (outcome.send_ack && !ctx->suppress_rx_ack)` 块内，而 `outcome.send_ack` 只在 FT_HB/DATA/BOOT 时设置。FT_HB 的 payload_len 为 0，但 `log_update_ack` 还额外检查了 `frame->payload_len > 0U`，所以安全。

---

## 二、时序图 — 多线程并发

### 2.1 C 层：reader 线程 vs heartbeat 线程 vs waiter 线程

```
时间轴(↓)   reader_thread          heartbeat_thread        waiter_thread(App调用)
t1         lock(ctx->lock)
           read UART
           protocol_on_frame()
           send_ack (写UART)
           unlock(ctx->lock)
           event_queue_push()
           notify_cb() ──────────────────────────────────────→ jni_notify_cb
                                                            (Attach+CallStaticVoidMethod)
t2                                lock(ctx->lock)
                                  check last_rx_frame_ms
                                  send_hb (写UART)
                                  unlock(ctx->lock)

t3                                                          lock(g_link_mutex)
                                                            lock(ctx->lock)
                                                            send_data_frame()
                                                            unlock(ctx->lock)
                                                            wait_for_ack() (cond_wait)
                                                            ...
                                                            unlock(g_link_mutex)
```

**锁对象分析：**

| 锁 | 类型 | 保护变量 | 访问线程 |
|----|------|---------|---------|
| `g_link_mutex` | `pthread_mutex_t` (全局) | `g_link` 指针 | 所有 JNI 调用线程 |
| `ctx->lock` | `pthread_mutex_t` | `state`, `last_rx_frame_ms`, `last_hb_tx_ms`, `last_sent_seq`, `last_ack_seq`, `suppress_rx_ack`, `rx_raw_bytes`, UART 写操作 | reader, heartbeat, waiter (JNI) |
| `ctx->events.mutex` | `pthread_mutex_t` | `EventQueue` (head, tail, count, items) | reader (push), waiter (pop/restore) |

**检查结论：**

1. **锁的范围对吗？**
   - ✅ reader 线程：锁范围覆盖 "解析帧 → protocol 状态机 → auto-ACK 写 UART"，合理。
   - ✅ heartbeat 线程：锁范围仅覆盖 "检查时间 → 写 HB"，合理。
   - ✅ waiter 线程：`g_link_mutex` 外层 + `ctx->lock` 内层。`send_data_frame` 持锁期间包含 UART 写，然后释放，再进入 `wait_for_ack`（不持锁，用 cond_wait）。这是正确的——如果持锁等待 ACK，reader 线程无法 push event。

2. **锁的对象对吗？**
   - ✅ `synchronized(lock)` 在 `McuLink.open/close/isOpen` 中保护 `opened/path/baud`。
   - ✅ `synchronized(snapshot)` 在 `VehicleFeature.snapshot()` 和 `onFrame` 中保护 snapshot。
   - ✅ `synchronized(state)` 在 `RadioFeature` 中保护 state。
   - ✅ `synchronized(progress)` 在 `UpgradeFeature` 中保护 progress。
   - ✅ `synchronized(stateLock)` 在 `UpgradeManager` 中保护 running/abortRequested/progress。

3. **读需要锁吗？**
   - ⚠️ **`McuLink.isLinkAlive()`**：Java 层调用 `nativeMcuLinkIsLinked`，JNI 层锁 `g_link_mutex` 再锁 `ctx->lock` 读取 `link_state`。安全。
   - ⚠️ **`QsMcuSystemService.probePaused`**：`volatile boolean`，无锁，但仅做 boolean 读写，原子性足够。
   - ⚠️ **`KeyInjector.pendingKeyCode / pendingDownTime`**：
     - `FrameRouter` 是单线程 HandlerThread，`KeyFeature.onFrame` 在该线程调用 `injector.inject()`。
     - `inject()` 读写 `pendingKeyCode` 和 `pendingDownTime`。
     - 但 `KeyInjector.setEnabled()` 可以被任意线程调用，修改 `enabled`（无 volatile!）。
     - **风险**：其他线程调用 `setEnabled` 时，FrameRouter 线程可能正在读 `enabled`，虽然 boolean 赋值原子，但无 happens-before，可能看到旧值。建议 `enabled` 加 `volatile`。
     - **更大风险**：如果未来有人从其他线程调用 `inject()`，`pendingKeyCode` / `pendingDownTime` 存在竞态（无同步）。当前设计下只有 FrameRouter 线程调用，所以暂时安全，但属于设计脆弱点。

### 2.2 Java 层：RemoteCallbackList 的并发广播

```
时间轴(↓)   FrameRouter线程        App进程Binder线程        另一个App进程

t1         VehicleFeature.onFrame()
           snapshot.lineDetect = ...
           broadcastVehicle()
             snapshot() [synchronized]
             broadcastTyped(cb->cb.onLineDetect())
               beginBroadcast()
               for i in 0..n-1:
                 getBroadcastItem(i).onLineDetect() ──────→ (Binder IPC)
               finishBroadcast()

t2                               收到 onLineDetect

t3         同时另一个 cmd 到来
           snapshot.voltageMv = ...
           broadcastVehicle()
             新的 snapshot() 拷贝
             再次 beginBroadcast()
```

- ✅ `RemoteCallbackList` 内部使用 `ArrayMap` + `synchronized`，`beginBroadcast/finishBroadcast` 配对使用是标准做法。
- ⚠️ **潜在问题**：`VehicleFeature.broadcastVehicle()` 在一个 `broadcastTyped` 调用内部，如果 listener 的回调耗时（Binder IPC 阻塞），会导致 `FrameRouter` HandlerThread 被阻塞，后续 MCU 帧堆积在 `HANDLER` 的消息队列中。但 `HANDLER` 是 Looper，消息队列无界（`MessageQueue` 实际受内存限制），如果 App 端处理慢，可能导致内存增长。

---

## 三、状态机 — 有状态模块

### 3.1 C 层链路状态机 (mcu_protocol.c)

```
                    mcu_link_send_setup()
        ┌────────────────────────────────────────┐
        │                                        │
        ▼                                        │
   [LINK_IDLE] ──────→ [LINK_SETUP_SENT] ───────┘
        ▲                    │ 收到 SETUPACK
        │                    ▼
        │               [LINK_LINKED]
        │                    │
        │    收到 CLOSE      │ 收到 HB/DATA/BOOT
        └────────────────────┘    → send_ack
        收到 FT_CLOSE
        → link_state=LINK_IDLE
```

**状态转换表：**

| 当前状态 | 事件 (frame->ft) | 下一状态 | 副作用 | 检查 |
|---------|------------------|---------|--------|------|
| LINK_IDLE | SETUP | LINK_SETUP_SENT | send_seq=1, recv_seq=1 | ✅ |
| LINK_SETUP_SENT | SETUPACK | LINK_LINKED | link_up=true, send_seq=seq+1 | ✅ |
| LINK_LINKED | ACK | LINK_LINKED | matched_ack → send_seq++ | ✅ |
| LINK_LINKED | HB | LINK_LINKED | send_ack=true | ✅ |
| LINK_LINKED | DATA | LINK_LINKED | send_ack=true, duplicate_rx check | ✅ |
| LINK_LINKED | BOOT | LINK_LINKED | send_ack=true, duplicate_rx check | ✅ |
| LINK_LINKED | CLOSE | LINK_IDLE | link_down=true | ✅ |
| 任意 | TRANSFER | 不变 | 无 | ⚠️ 合法吗？ |

**检查结论：**

1. **状态有漏吗？**
   - ⚠️ `LINK_SETUP_SENT` 状态下如果收到非 SETUPACK 帧（如 DATA/CLOSE），状态机不做任何处理，保持 LINK_SETUP_SENT。但 `mcu_link_do_setup` 会在超时后重发 SETUP，然后重新等待 SETUPACK。这是合理的。
   - ⚠️ 如果 `LINK_LINKED` 状态下收到 `FT_SETUP`，状态机无处理。这应该不会发生，除非 MCU 异常重启。

2. **非法转换有处理吗？**
   - ✅ `LINK_IDLE` 状态下收到 DATA/HB/BOOT：直接 `break`，不处理。合理（未建链）。
   - ⚠️ 但如果 MCU 在已 LINKED 时重发 SETUPACK（重复帧），`mcu_protocol_on_frame` 中 SETUPACK 分支无条件设置 `link_up=true` 和 `state->link_state=LINK_LINKED`，不会检查是否已在 LINKED。这可能导致 `send_seq` 被重置（因为 `state->send_seq = mcu_seq_next(frame->seq)`），造成 seq 跳变。不过 `mcu_link_do_setup` 成功后会进入 LINKED，通常不会再次收到 SETUPACK。

3. **超时处理了吗？**
   - ✅ `mcu_link_do_setup`：重试 3 次，每次超时 3000ms。
   - ✅ `mcu_link_wait_for_ack`：超时 2500ms。
   - ✅ `mcu_link_wait_for_update_frame`：超时 5000ms。
   - ✅ `mcu_upgrade_run` 中每个 send 后都 wait_for_ack，带超时。

### 3.2 C 层 OTA 状态机 (mcu_upgrade.c)

```
[Idle]
  │ mcu_upgrade_run()
  ▼
[Image Load] ──失败──→ [FAIL] (ERR_IMAGE)
  │
  ▼
[SETUP] ──失败──→ [FAIL] (ERR_SETUP)
  │
  ▼
[UPDATE_REQ] ──失败──→ [FAIL] (ERR_REQ)
  │ 收到 ACK
  ▼
[UPDATE_BEGIN] ──失败──→ [FAIL] (ERR_BEGIN)
  │ 收到 ACK
  ▼
[等待 UPDATE_LEN] ──失败/超时──→ [FAIL] (ERR_LEN)
  │ 收到 pack_len
  ▼
[循环: 等待 IND_AQUIRE(N)]
  │ 收到 IND_AQUIRE
  ▼
[发送 UPDATE_DATA(N)] ──失败──→ [FAIL] (ERR_DATA_ACK)
  │ 收到 ACK (重试最多 3 次)
  ▼
[等待 UPDATE_ACK(RX_LAST)]
  │ 收到 RX_LAST
  ▼
[发送 UPDATE_END] ──失败──→ [FAIL] (ERR_END)
  │ 收到 ACK
  ▼
[等待 UPDATE_ACK(SUCCESS)]
  │ 收到 SUCCESS
  ▼
[COMPLETE] → return OK
  │ 收到 REJECT/FAIL
  ▼
[FAIL] → return ERR_REJECT_FAIL
```

**检查结论：**

1. **状态有漏吗？**
   - ⚠️ OTA 进行中时能不能同时调 `setBand`？**可以**。`mcu_upgrade_run` 运行在调用线程（Java 的 UpgradeWorker HandlerThread），但 C 层的 `ctx->lock` 只保护写 UART 操作。`mcu_link_send_data_frame` 和 `mcu_link_wait_for_ack` 交替进行。如果在 OTA 间隙（wait 期间），另一个 Java 线程通过 `McuBinder` 调用 `sendAndWaitAck`，会同样竞争 `g_link_mutex` 和 `ctx->lock`。**两个 wait 线程同时存在时，`event_queue` 中的帧会被谁先 pop 谁拿走。** 这意味着 OTA 的 `wait_for_update_frame` 可能偷走普通 ACK，或者普通 `wait_for_ack` 偷走 UPDATE 帧。
   - 🔴 **严重隐患**：`mcu_upgrade_run` 的 while 循环调用 `mcu_link_wait_for_update_frame`，该函数**静默丢弃**所有非 UPDATE 帧（注释："Non-matching frames are silently dropped (already auto-ACKed by reader)"）。如果此时 App 发起了 `setBand` 并等待 ACK，这个 ACK 会被 OTA waiter 丢弃！反之，`wait_for_ack` 会把 UPDATE 帧 deferred 回队列，但 `wait_for_update_frame` 不会 restore 非 UPDATE 帧。
   - 实际上 `UpgradeManager.startUpgrade` 中调用了 `QsMcuSystemService.pauseProbe()`，暂停了周期性探针，但没有阻塞其他 AIDL 调用。
   - **建议**：OTA 期间应在 Java 层（`UpgradeManager`）加一个全局锁或标志，拒绝其他业务命令，或在 C 层做队列隔离。

2. **非法转换有处理吗？**
   - ✅ 如果 `pack_len == 0` 时收到 `IND_AQUIRE`，报错 `ERR_DATA_IND`。
   - ✅ 如果 `index >= image.size`，报错 `ERR_DATA_IND`。
   - ✅ `end_sent` 标志防止重复发送 UPDATE_END（如果 MCU 重复发 RX_LAST）。

3. **超时处理了吗？**
   - ✅ 每个阶段都有超时：ACK_TIMEOUT_MS=2500，UPGRADE_MESSAGE_TIMEOUT_MS=5000。
   - ✅ `mcu_upgrade_abort()` 设置 `g_upgrade_abort_requested`，在 while 循环开头检查，返回 `ERR_TIMEOUT`。

### 3.3 Java 层 McuLink.opened 状态

```
[false]
  │ open()
  ▼
[true] ───────→ sendSetup / sendData / ... 允许
  │ close()
  ▼
[false]
```

- `opened` 被 `synchronized(lock)` 保护。
- `isLinkAlive()` 依赖 `opened && nativeMcuLinkIsLinked()`。
- ⚠️ 时序问题：`opened=true` 在 `nativeMcuLinkOpen` 成功后设置。但如果 `nativeMcuLinkOpen` 成功（UART 打开，线程启动），随后 `doSetup` 失败，`opened` 仍为 true，`isOpen()` 返回 true，但 `isLinkAlive()` 返回 false。`McuBinder` 的 API 通常检查 `isLinkAlive()`，所以安全。

### 3.4 Java 层 UpgradeManager 状态

```
[STATE_IDLE]
  │ startUpgrade()
  ▼
[STATE_RUNNING] (running=true)
  │ stopUpgrade()          │ runUpgrade 完成
  ▼                        ▼
[STATE_ABORTED]      [STATE_COMPLETE] / [STATE_ERROR]
```

- `running` 和 `abortRequested` 是 `volatile`，但 `progress` 字段的读写在 `synchronized(stateLock)` 内。
- ⚠️ `startUpgrade` 中 `running` 检查和设置不在同一原子操作：先 `synchronized(stateLock)` 检查 `running`，然后释放锁，再在 worker 线程运行。如果两个线程几乎同时调用 `startUpgrade`，可能都通过检查，导致两个 OTA 任务并发。但由于 `UpgradeManager` 是单例且通常只有一个调用方（App），风险较低。

---

## 四、决策树 — 函数分支逻辑

### 4.1 McuBinder.setBandWithFreq (典型 AIDL 方法)

```
入口 setBandWithFreq(band, freq)
   │
   ├──  try { Commands.Radio.setBand(band, freq) }
   │    └── catch IllegalArgumentException
   │         └── return ERR_INVALID_ARG  ─────── 叶子①
   │
   ├──  if (!McuLink.get().isLinkAlive())
   │    └── return ERR_LINK_DOWN  ───────────── 叶子②
   │
   ├──  seq = McuLink.get().sendAndWaitAck(RADIO, SET_BAND, payload, 1500)
   │    ├── seq >= 0
   │    │   └── return OK  ──────────────────── 叶子③
   │    └── seq < 0
   │        └── return ERR_TIMEOUT  ─────────── 叶子④
```

**检查结论：**

1. **所有分支都有返回值吗？**
   - ✅ 是的，4 个叶子节点都有 return。
   - ⚠️ 但注意：`sendAndWaitAck` 返回 `-2` 表示 "write error / no link"，而这里 `isLinkAlive()` 已经检查过，所以 `-2` 主要对应写错误。代码中将 `seq < 0` 统一返回 `ERR_TIMEOUT`，不区分写错误和超时。`McuBinder.sendCommand` 做了更细的分支：`seq == -1 ? ERR_TIMEOUT : ERR_LINK_DOWN`。

2. **错误码一致吗？**
   - ✅ 使用了 `McuErrorCode` 常量：`OK`, `ERR_LINK_DOWN`, `ERR_INVALID_ARG`, `ERR_TIMEOUT`。
   - ⚠️ `UpgradeManager.startBootUpgrade` 中返回 `McuErrorCode.ERR_FILE`，该错误码在 `McuBinder` 中没有对应分支透传，但 `McuBinder` 直接返回 `UpgradeManager` 的结果，所以一致。

3. **异常路径的副作用和正常路径一致吗？**
   - ✅ `setBandWithFreq` 中，`Commands.Radio.setBand` 是纯计算（构建 byte[]），无 I/O 副作用。失败时不修改任何状态。
   - ⚠️ `McuBinder.registerUpgradeListener` 同时注册了 `upgrade.register(cb)` 和 `UpgradeManager.getInstance().register(cb)`。如果其中一个 register 成功，另一个失败（理论上不会，但假设），会导致状态不一致。不过 `RemoteCallbackList.register` 不会抛出异常，安全。

### 4.2 McuBinder.sendCommand (AIDL v2 通用接口)

```
入口 sendCommand(msgType, cmd, body, timeoutMs)
   │
   ├── if (!isLinkAlive())
   │   └── return ERR_LINK_DOWN  ───────────── 叶子①
   │
   ├── if (body != null && body.length > 2040)
   │   └── return ERR_INVALID_ARG  ─────────── 叶子②
   │
   ├── seq = sendAndWaitAck(msgType, cmd, body, timeoutMs)
   │   ├── seq >= 0
   │   │   └── return OK  ──────────────────── 叶子③
   │   ├── seq == -1
   │   │   └── return ERR_TIMEOUT  ─────────── 叶子④
   │   └── seq < -1 (实际是 -2)
   │       └── return ERR_LINK_DOWN  ───────── 叶子⑤
```

- ✅ 比旧 API 更细致，区分了 timeout 和 link down。

### 4.3 mcu_link_wait_for_ack

```
入口 mcu_link_wait_for_ack(ctx, expected_seq, timeout_ms)
   │
   ├── while (now < deadline)
   │   ├── event_queue_wait_pop() → false (timeout)
   │   │   └── break → restore_front → return false  叶子①
   │   │
   │   └── event_queue_wait_pop() → true
   │       ├── ft==FT_ACK && seq==expected_seq
   │       │   └── restore_front(deferred) → return true  叶子②
   │       └── 其他帧
   │           └── deferred[deferred_count++] = event
   │               (若 deferred_count >= 128 则丢弃)
   │
   └── return false  (restore_front)  ─────────────── 叶子③
```

**检查结论：**

1. **所有分支都有返回值吗？**
   - ✅ while 内 break 后 return false；匹配 ACK return true；while 结束后 return false。
   - ⚠️ **严重隐患**：`deferred_count < ARRAY_SIZE(deferred)`（128）检查。如果在等待 ACK 期间，reader 线程收到超过 128 个非 ACK 帧（如大量 MCU 主动上报），`deferred_count` 达到 128 后，后续非 ACK 帧会被**静默丢弃**（`if (deferred_count < ARRAY_SIZE(deferred))` 不成立就不保存）。然后当 ACK 终于到来时，`restore_front` 只恢复前 128 个，后面的帧已经丢失。
   - 在正常使用中，ACK 超时 2500ms，MCU 不会发送这么多帧。但在高负载或调试场景下，这是一个隐患。建议加日志告警或增大数组。

### 4.4 KeyInjector.inject

```
入口 inject(keyValue, keyStatus)
   │
   ├── if (!enabled)
   │   └── return  (静默忽略)  ───────────────── 叶子①
   │
   ├── if (inputManager == null || injectMethod == null)
   │   └── return  (静默忽略)  ───────────────── 叶子②
   │
   ├── if (encoder 状态)
   │   └── injectEncoder() → return  ─────────── 叶子③
   │
   ├── keyCode = toAndroidKeyCode(keyValue)
   │   ├── keyCode < 0
   │   │   └── return  (静默忽略)  ───────────── 叶子④
   │   └── keyCode >= 0
   │       └── switch(keyStatus)
   │           ├── SHORT_PRESS
   │           │   └── inject DOWN + UP (新 downTime)
   │           │   └── pendingKeyCode=-1, pendingDownTime=-1
   │           │   └── return  ───────────────── 叶子⑤
   │           ├── STUCK
   │           │   └── if (keyCode != pendingKeyCode) 更新 pending
   │           │   └── inject DOWN (复用 downTime)
   │           │   └── return  ───────────────── 叶子⑥
   │           ├── RELEASE
   │           │   └── if (keyCode==pendingKeyCode && pendingDownTime>=0)
   │           │         inject UP (复用 downTime)
   │           │       else fallback 新 downTime UP
   │           │   └── pendingKeyCode=-1, pendingDownTime=-1
   │           │   └── return  ───────────────── 叶子⑦
   │           └── default
   │               └── log + return  ─────────── 叶子⑧
```

**检查结论：**

1. **所有分支都有返回值吗？**
   - ✅ `void` 方法，但所有路径都有明确的 `return` 或执行到末尾。

2. **错误码一致吗？**
   - 不适用（void 方法）。但多个错误路径（enabled=false, method=null, unmapped key）都是**静默忽略**，没有日志或回调通知调用方。调试时可能难以定位。

3. **异常路径的副作用和正常路径一致吗？**
   - ⚠️ `STUCK` 分支：如果 MCU 连续发送同一个 key 的 STUCK，`pendingKeyCode` 和 `pendingDownTime` 不变，只发 DOWN。这是正确的（长按）。
   - ⚠️ `RELEASE` fallback：如果 `keyCode != pendingKeyCode`（比如 STUCK 是 A 键，RELEASE 是 B 键），会新分配 `downTime` 发 UP。这可能导致系统看到一个没有 DOWN 的 UP 事件，Input 系统可能丢弃。但这是 MCU 数据异常场景，可以接受。
   - ⚠️ `injectKeyEvent` 反射调用：`injectMethod.getParameterCount()` 在每次调用时都反射获取参数数量，有性能开销。虽然 inject 频率不高（按键事件），但可以缓存结果。

### 4.5 QsMcuSystemService.tryOpenSerial

```
入口 tryOpenSerial(attempt)
   │
   ├── McuLink.get().open(path, baud) → true
   │   ├── start() (doSetup)
   │   ├── scheduleInitProbe(0)
   │   └── scheduleRepeatProbe()
   │   └── return  ─────────────────────────── 叶子① (成功)
   │
   └── open() → false
       ├── delay = RETRY_DELAYS_MS[min(attempt, 6)]
       ├── mainHandler.postDelayed(tryOpenSerial(attempt+1), delay)
       └── return  ─────────────────────────── 叶子② (延迟重试)
```

**检查结论：**

1. **所有分支都有返回值吗？**
   - ✅ `void` 方法，递归通过 Handler post 实现，避免栈溢出。

2. **错误码一致吗？**
   - 不适用。

3. **异常路径的副作用和正常路径一致吗？**
   - ⚠️ `McuLink.open()` 内部如果 `nativeMcuLinkOpen` 成功，但随后 `doSetup` 失败（在 `QsMcuSystemService.start()` 中调用），UART 仍然是打开的（`opened=true`），reader 线程在跑。`tryOpenSerial` 不会再重试，因为 `opened=true` 时 `open()` 直接返回 true。这意味着服务会停留在 "UART 已开但链路未建立" 的状态，靠 `scheduleInitProbe` 周期性尝试。设计上 `start()` 失败后没有关闭 UART，可能是有意为之（让心跳和 reader 继续工作，等 MCU _READY）。但需要确认这是否符合预期。

---

## 五、资源所有权图 — 谁创建、谁释放、谁兜底

### 5.1 资源清单总表

| 资源 | 类型 | 创建点 | 释放点 | 所有权 | 风险 |
|------|------|--------|--------|--------|------|
| UART fd (`ctx->fd`) | POSIX fd | `serial_open_port` (mcu_transport.c) | `mcu_link_close` → `close(fd)` | C 层 McuLink 独占 | 中 |
| McuLink ctx | `calloc` | `mcu_link_open` | `mcu_link_close` → `free(ctx)` | C 层 | 低 |
| EventQueue (mutex+cond) | `pthread_*` | `mcu_link_open` → `event_queue_init` | `mcu_link_close` → `event_queue_destroy` | C 层 | 低 |
| rx_thread / hb_thread | `pthread_t` | `mcu_link_open` → `pthread_create` | `mcu_link_close` → `pthread_join` | C 层 | 低 |
| UpgradeImage buffer | `malloc` | `upgrade_image_load` (mcu_upgrade.c) | `upgrade_image_free` (mcu_upgrade.c) | C 层 OTA 线程 | 低 |
| JNI GlobalRef (router) | `jobject` (global) | `JNI_OnLoad` → `NewGlobalRef` | **无！** | JVM | 🔴 **高** |
| JNI GlobalRef (mculink) | `jobject` (global) | `JNI_OnLoad` → `NewGlobalRef` | **无！** | JVM | 🔴 **高** |
| JNI LocalRef (jPayload) | `jbyteArray` | `jni_notify_cb` → `NewByteArray` | `jni_notify_cb` → `DeleteLocalRef` | JNI 调用帧 | 低 |
| JNI String UTFChars | `char*` | `GetStringUTFChars` | `ReleaseStringUTFChars` | JNI 调用帧 | 低 |
| JNI ByteArrayElements | `jbyte*` | `GetByteArrayElements` | `ReleaseByteArrayElements` | JNI 调用帧 | 低 |
| Java: FrameRouter WORKER | `HandlerThread` | `FrameRouter.<clinit>` | `FrameRouter.quit()` (onDestroy) | Java Service | 低 |
| Java: UpgradeWorker | `HandlerThread` | `UpgradeManager.<init>` | `UpgradeManager.shutdown()` (onDestroy) | Java Service | 低 |
| Java: RemoteCallbackList | AIDL 容器 | `register()` | `unregister()` / 进程退出 | Java Service | 低 |

### 5.2 详细路径审查

#### 5.2.1 UART fd 的完整生命周期

```
[创建] mcu_link_open()
   └── serial_open_port(port, baud)
       └── fd = open(port, O_RDWR | O_NOCTTY | O_NONBLOCK)
       └── 任何一步失败 → close(fd) → return -1  (兜底正确)

[使用] reader_thread / heartbeat_thread / waiter_thread
   └── 通过 ctx->fd 读写

[释放] mcu_link_close()
   └── ctx->running = false
   └── pthread_join(hb_thread) → 若未启动则跳过
   └── pthread_join(rx_thread) → 若未启动则跳过
   └── close(fd)
   └── ctx->fd = -1
```

**检查结论：**
- ✅ `mcu_link_open` 中任何失败路径（tcgetattr、tcsetattr、fcntl、thread create）都会 `close(fd)` 或 `free(ctx)`，无泄漏。
- ✅ `mcu_link_close` 是 idempotent 的（NULL 检查 + fd=-1 检查）。
- ⚠️ **隐患**：`QsMcuSystemService.onDestroy` 调用 `McuLink.get().sendCloseAndStop()`，它先 `waitForRxQuiet` → `sendClose` → `sleep(1000)` → `close()`。如果 `sendClose` 时 UART 已经断开（`write` 返回 -1），`close()` 仍然会被调用，fd 正确释放。
- ⚠️ **隐患**：如果 `mcu_link_open` 成功但 `QsMcuSystemService` 在 `onCreate` 后立即 `onDestroy`（如系统资源紧张），`sendCloseAndStop` 会调用 `waitForRxQuiet`，此时 reader 线程刚启动，可能还没收到任何帧，`waitForRxQuiet` 会等到 `maxWaitMs=3000ms`。这是预期行为，但会延迟 Service 销毁 3 秒。

#### 5.2.2 JNI 全局引用 — 🔴 发现泄漏

```cpp
// jni_bridge.cpp:JNI_OnLoad
jclass router = env->FindClass("com/qsjnic/mcu/service/FrameRouter");
g_router_class = (jclass)env->NewGlobalRef(router);  // ← 创建
g_router_on_frame = env->GetStaticMethodID(...);

jclass mculink = env->FindClass("com/qsjnic/mcu/service/McuLink");
g_mculink_class = (jclass)env->NewGlobalRef(mculink);  // ← 创建
g_mculink_on_upgrade = env->GetStaticMethodID(...);
```

**问题：** 整个 `jni_bridge.cpp` 中没有 `DeleteGlobalRef`。

- 在 Android 中，Service 进程通常不会被频繁重启，`libmcujni.so` 一旦加载很少卸载，所以生产中可能不会触发泄漏。
- 但如果使用 `dlclose` 或进行热修复（如 Tinker 框架替换 so），这两个 GlobalRef 会泄漏。
- **更严重的是**：`JNI_OnLoad` 没有检查 `FindClass` 失败后的清理。如果 `FrameRouter` 类找不到（比如 ProGuard 混淆），`JNI_OnLoad` 直接 `return -1`，但此时 `router` 为 null，`g_router_class` 被赋值为 null。这是安全的（不会 crash），但后续 `jni_notify_cb` 中 `g_router_class == nullptr` 直接返回，**MCU 上行帧会静默丢失**，没有日志告警。

**建议：**
1. 在 so 卸载时（如有）或 `nativeMcuLinkClose` 最后增加 `DeleteGlobalRef`。
2. 至少加注释说明：GlobalRef 依赖进程生命周期。

#### 5.2.3 JNI 局部引用与 ByteArray

```cpp
// jni_notify_cb
jbyteArray jPayload = env->NewByteArray((jsize)body_len);
env->SetByteArrayRegion(...);
env->CallStaticVoidMethod(..., jPayload);
if (jPayload != nullptr) env->DeleteLocalRef(jPayload);
```

- ✅ `NewByteArray` 失败时 `jPayload=nullptr`，传 null 给 Java 层，安全。
- ✅ `DeleteLocalRef` 在回调后立即调用。
- ⚠️ **隐患**：C reader 线程是长期 Attach 到 JVM 的（`acquire_env` 中 `needs_detach` 被忽略，注释说 "rx thread stays attached"）。局部引用表（Local Reference Table）默认容量通常是 512 或 1024（取决于 JVM 实现）。如果 MCU 帧率极高（如 >1000fps）且 `jni_notify_cb` 每帧都创建局部引用，理论上不会溢出（因为每帧都 DeleteLocalRef）。但如果 `ExceptionCheck` 分支进入后没有清理，或者未来有人在这里创建其他 LocalRef 忘记 Delete，仍有风险。

#### 5.2.4 JNI ByteArrayElements — 使用 JNI_ABORT 的风险审计

```cpp
// nativeMcuLinkSendDataFrame / nativeMcuLinkSendAndWaitAck
jbyte *bytes = env->GetByteArrayElements(jPayload, nullptr);
// ... 只读使用 ...
env->ReleaseByteArrayElements(jPayload, bytes, JNI_ABORT);
```

- ✅ 当前确实是**只读**（`payload` 传给 `mcu_link_send_data_frame`，C 层不会修改）。
- ⚠️ **脆弱性**：如果未来某次重构中，C 层函数被改为 in-place 修改 payload（比如加 checksum），`JNI_ABORT` 会导致修改丢失，Java 层的 `jPayload` 数组内容不会被更新。而调用方可能以为数组被修改了。
- **建议**：加注释 `/* JNI_ABORT because C layer treats payload as read-only */`。

#### 5.2.5 UpgradeImage (OTA 固件缓冲区)

```c
// upgrade_image_load
buffer = malloc((size_t)file_size);
// ... fread ...
image->data = buffer;

// mcu_upgrade_run 中每个错误路径
upgrade_image_free(&image);  // free(image->data)

// 成功路径最后
upgrade_image_free(&image);  // 同样释放
```

- ✅ **完全正确**。每个分支（包括 `ACK_UPDATE_SUCCESS` 和 `ACK_UPDATE_REJECT/FAIL`）都调用了 `upgrade_image_free`。
- ✅ `upgrade_image_load` 内部失败时也正确清理（fclose + free buffer）。

#### 5.2.6 Java 层 HandlerThread 生命周期

```java
// FrameRouter
static {
    WORKER = new HandlerThread("FrameRouter");
    WORKER.start();
    HANDLER = new Handler(WORKER.getLooper());
}

// QsMcuSystemService.onDestroy
FrameRouter.quit();
```

- ✅ `quitSafely()` 会处理完已 post 的消息再退出，避免丢帧。
- ⚠️ **隐患**：`FrameRouter.onMcuFrame` 中 `HANDLER.post(...)` 后，`quitSafely` 会等待消息队列清空。如果 onDestroy 时消息队列中有大量待处理帧（如 MCU 在关闭前burst发送），`onDestroy` 会被阻塞。但这是 Android Service 销毁的标准行为，可以接受。

#### 5.2.7 McuLink Java 单例 — 永不释放

```java
private static final McuLink INSTANCE = new McuLink();
```

- `INSTANCE` 是静态 final，进程生命周期内永不释放。
- `upgradeListener` 是 `volatile`，如果某次 OTA 后 `setUpgradeListener(null)` 没有调用，listener 对象会被 McuLink 长期引用，导致内存泄漏（如果 listener 是 Activity/Fragment 的匿名类）。
- ⚠️ **建议**：`UpgradeManager` 在 `shutdown()` 或 OTA 结束时显式 `McuLink.get().setUpgradeListener(null)`，切断引用。

---

## 六、故障传播链 / 故障树

### 6.1 根因：UART 物理断开 (/dev/ttyS7 掉线)

```
[UART 物理断开 / MCU 断电]
   │
   ├── C 层 reader_thread
   │   └── serial_read_some(fd, ..., 50ms)
   │       └── select() 超时 或 read() 返回 0/-1
   │   └── n <= 0，空转循环（ctx->running 仍为 true）
   │   └── 无帧可解析 → 不触发 notify_cb
   │   └── last_rx_frame_ms 停止更新
   │
   ├── C 层 heartbeat_thread
   │   └── 检查 current_ms >= last_rx_frame_ms + 2000ms → true
   │   └── 检查 current_ms >= last_hb_tx_ms + 2000ms → true
   │   └── pthread_mutex_lock(&ctx->lock)
   │   └── live_send_heartbeat_locked() → serial_write_all()
   │       └── write() 返回 -1 (errno=EIO/ENODEV/EBADF)
   │       └── serial_write_all 返回 -1
   │       └── live_send_heartbeat_locked 返回 false
   │   └── last_hb_tx_ms 不更新
   │   └── pthread_mutex_unlock(&ctx->lock)
   │   └── 100ms 后继续循环（持续写失败）
   │
   ├── C 层 mcu_link_is_linked()
   │   └── ctx->state.link_state == LINK_LINKED → true
   │   └── 返回 true（因为没收到 FT_CLOSE，状态不会变）
   │
   ├── Java 层 McuLink.isLinkAlive()
   │   └── opened=true (Java 层标志)
   │   └── nativeMcuLinkIsLinked() → true
   │   └── 返回 true
   │
   ├── Java 层 McuBinder.setBand()
   │   └── isLinkAlive() → true（误判链路存活）
   │   └── sendAndWaitAck(RADIO, SET_BAND, payload, 1500)
   │       └── nativeMcuLinkSendAndWaitAck()
   │           └── mcu_link_send_data_frame() → write() 失败
   │           └── sent = false
   │           └── 不进入 wait_for_ack
   │           └── rc = -2
   │       └── seq = -2
   │   └── seq < 0 → return ERR_TIMEOUT
   │
   └── App 层
       └── 收到 ERR_TIMEOUT
       └── 以为是 MCU 没回 ACK，可能重试
       └── 实际上 UART 已死，重试永远失败
```

**🔴 关键发现：链路假活（Zombie Link）**

- `isLinkAlive()` 在 UART 断开后仍然返回 `true`，因为 C 层状态机没有任何机制检测物理链路断开。
- 标准串口（RS-232）没有在线检测（DCD 信号未使用），纯靠读取超时判断。
- 但代码中 `last_rx_frame_ms` 停止更新后，`heartbeat_thread` 只是不断尝试发 HB，写失败也不改变 `link_state`。
- **结果**：App 看到 `isLinkAlive()=true`，发命令返回 `ERR_TIMEOUT`，永远不知道是 UART 断了。

**建议修复方向（供参考）：**
1. **写失败降级**：`serial_write_all` 连续失败 N 次后，自动设置 `link_state = LINK_IDLE`，让 `isLinkAlive()` 返回 false。
2. **HB 超时检测**：如果 `last_rx_frame_ms` 超过 `N * MCU_HB_INTERVAL_MS`（如 3 次心跳周期 = 6s）且无 RX，强制断链。
3. **Java 层暴露 `isOpen()` 与 `isLinkAlive()` 的差异**：让 App 知道 "串口开着但 MCU 没心跳"。

### 6.2 根因：MCU 异常重启

```
[MCU 重启]
   │
   ├── MCU 重新上电 → 发 SETUP 帧 (seq=1)
   ├── C 层 reader_thread 收到 FT_SETUP
   │   └── mcu_protocol_on_frame()
   │       └── FT_SETUP → default case → break (无处理!)
   │   └── 不 send_ack（因为 outcome.send_ack = false）
   │   └── event_queue_push() → Java 层无监听 SETUP 帧
   │
   ├── C 层 state.link_state 仍为 LINK_LINKED
   ├── Java 层 isLinkAlive() → true
   ├── App 发命令 → 用旧的 send_seq → MCU 不认识 → 不 ACK
   └── App 收到 ERR_TIMEOUT
```

**🔴 关键发现：MCU 重启后无法自动恢复**

- `mcu_protocol_on_frame` 对 `FT_SETUP` 的处理是 `default: break`，即在 `LINK_LINKED` 状态下收到 SETUP 完全忽略。
- 这意味着 MCU 重启后，Service 端仍然认为链路已建立，但 seq 已经不同步。
- `QsMcuSystemService` 只有 `onCreate` 时调用一次 `tryOpenSerial` + `start()`，之后不会自动重新握手。
- **结果**：必须杀掉 Service 进程或手动重新 open 才能恢复。

**建议修复方向：**
- `FT_SETUP` 在 `LINK_LINKED` 状态下应视为 MCU 重启信号，自动重置状态机到 `LINK_IDLE` 并触发重新 `doSetup`。
- 或在 Java 层增加 "N 次命令超时后自动重连" 策略。

### 6.3 根因：OTA 期间 App 并发发命令

```
[OTA 进行中]
   │
   ├── UpgradeManager.worker_thread
   │   └── mcu_upgrade_run()
   │       └── while(true)
   │           └── mcu_link_wait_for_update_frame(5000ms)
   │               └── 从 EventQueue pop 帧
   │               └── 非 UPDATE 帧 → 静默丢弃!
   │
   ├── App Binder 线程 (并发)
   │   └── McuBinder.setBand()
   │       └── sendAndWaitAck()
   │           └── mcu_link_send_data_frame() (写 UART 成功)
   │           └── mcu_link_wait_for_ack() (pop EventQueue)
   │               └── 竞争! 如果 OTA waiter 先 pop → ACK 被丢弃
   │               └── App waiter 永远等不到 ACK → ERR_TIMEOUT
   │
   └── 结果：App 命令失败，OTA 继续正常进行
```

**🔴 关键发现：EventQueue 是全局单队列，多 waiter 竞争**

- `EventQueue` 只有一个，所有 `wait_for_*` 函数都从同一个队列 pop。
- `mcu_link_wait_for_update_frame` 明确丢弃非 UPDATE 帧（不 restore）。
- 如果 App 命令的 ACK 帧在 OTA 等待期间到达，被 OTA waiter  pop 并丢弃，App  waiter 永远等不到 ACK。
- **这不是理论风险，是确定会发生的竞态**（只要 OTA 期间有其他命令）。

**建议修复方向：**
1. **业务层隔离**：`UpgradeManager.startUpgrade` 后，`McuBinder` 所有非 OTA 方法返回 `ERR_BUSY`。
2. **队列层隔离**：C 层增加 "帧过滤器注册" 机制，让 wait_for_update_frame 只 peek 不 pop 非匹配帧。
3. **最简单**：Java 层 `startUpgrade` 时加一个全局锁 `commandLock`，阻止其他 AIDL 调用进入 send 逻辑。

### 6.4 根因：Service onDestroy 时正在进行 Binder 调用

```
[用户关闭 App / 系统回收 Service]
   │
   ├── QsMcuSystemService.onDestroy()
   │   └── sInstance = null
   │   └── McuLink.get().sendCloseAndStop()
   │       └── waitForRxQuiet(1000, 3000, "close") → 可能阻塞 3s
   │       └── sendClose() → nativeMcuLinkSendClose()
   │       └── Thread.sleep(1000)
   │       └── close() → nativeMcuLinkClose()
   │           └── g_link = nullptr
   │           └── mcu_link_close() → join threads, close fd
   │
   ├── 同时：App 的 Binder 调用还没返回
   │   └── McuBinder.setBand() (在 Binder 线程池)
   │       └── 已经进入了 nativeMcuLinkSendAndWaitAck
   │       └── g_link_mutex 已锁，ctx = g_link (非 null)
   │       └── 正在 mcu_link_wait_for_ack() 阻塞
   │
   └── onDestroy 中 nativeMcuLinkClose 等 g_link_mutex?
       └── 不会！nativeMcuLinkClose 也锁 g_link_mutex
       └── 但 nativeMcuLinkSendAndWaitAck 先锁了
       └── onDestroy 被阻塞，直到 Binder 调用超时或返回
```

**🟡 发现：onDestroy 可能被 Binder 调用阻塞**

- `nativeMcuLinkClose` 和 `nativeMcuLinkSendAndWaitAck` 都竞争 `g_link_mutex`。
- 如果 onDestroy 时有一个 Binder 调用卡在 `wait_for_ack(1500ms)`，`onDestroy` 会等待最多 1.5s 才能拿到 `g_link_mutex`。
- 加上 `sendCloseAndStop` 自身的 `waitForRxQuiet(3000ms)` 和 `sleep(1000ms)`，Service 完全销毁可能需要 **5.5 秒以上**。
- Android `Service.onDestroy` 有 5 秒 ANR 限制（前台服务）或更短。这可能触发 ANR。

---

## 七、跨层契约一致性表

### 7.1 协议常量对照表

| 语义 | Java 层值 | C 层值 | 是否一致 | 位置 |
|------|----------|--------|---------|------|
| 帧头 | 无硬编码 | `0xFF 0xAA` | - | mcu_protocol.h |
| DATA 帧类型 | `0x06` (FrameRouter.FRAME_TYPE_NORMAL) | `FT_DATA = 0x06` | ✅ | FrameRouter.java / mcu_protocol.h |
| NOTIFY 帧类型 | `0x07` (FRAME_TYPE_NOTIFY) | `FT_TRANSFER = 0x07` | ⚠️ **命名不匹配** | Java 叫 NOTIFY，C 叫 TRANSFER |
| PUBLIC msgType | `0x01` | `MT_PUBLIC = 0x01` | ✅ | - |
| KEY msgType | `0x02` | 无（C 层未定义） | ⚠️ | C 层 mcu_protocol.h 只有 PUBLIC/RADIO/UPDATE |
| RADIO msgType | `0x05` | `MT_RADIO = 0x05` | ✅ | - |
| UPDATE msgType | `0x07` | `MT_UPDATE = 0x07` | ✅ | - |
| SETUP 帧类型 | 无 Java 常量 | `FT_SETUP = 0x01` | - | C 层内部使用 |
| ACK 帧类型 | 无 Java 常量 | `FT_ACK = 0x02` | - | C 层内部使用 |
| HB 帧类型 | 无 Java 常量 | `FT_HB = 0x05` | - | C 层内部使用 |
| CLOSE 帧类型 | 无 Java 常量 | `FT_CLOSE = 0x04` | - | C 层内部使用 |

**发现：**
- `FRAME_TYPE_NOTIFY = 0x07` 在 Java 层表示 NOTIFY，C 层 `FT_TRANSFER = 0x07` 表示 TRANSFER。虽然值相同，但语义命名不一致。如果未来协议升级需要区分 NOTIFY 和 TRANSFER，会引入混淆。
- `MSG_TYPE_KEY = 0x02` 在 C 层 `mcu_protocol.h` 中没有定义。C 层 parser 会把 `0x02` 当作普通 payload[0] 传给 Java，不做任何校验。这是合理的（C 层 L1 不解析业务 msgType），但属于契约缺失。

### 7.2 缓冲区尺寸对照表

| 语义 | Java 层限制 | C 层限制 | 是否一致 | 风险 |
|------|------------|---------|---------|------|
| 最大 payload | `body.length > 2040` (sendCommand) | `MCU_MAX_PAYLOAD_LEN = 2041` | ❌ **不一致** | Java 比 C 严格 1 字节 |
| 最大帧长 | 无 Java 常量 | `MCU_MAX_FRAME_LEN = 2048` | - | - |
| parser 缓冲 | 无 Java 常量 | `MCU_PARSER_BUFFER_LEN = 4096` | - | - |
| EventQueue 容量 | 无 Java 常量 | `128` | - | - |

**🔴 发现 `2040 vs 2041`：**

- Java `McuBinder.sendCommand`:
  ```java
  if (body != null && body.length > 2040) return ERR_INVALID_ARG;
  ```
- C `mcu_protocol.h`:
  ```c
  #define MCU_MAX_PAYLOAD_LEN (MCU_MAX_FRAME_LEN - 7)  // 2041
  ```
- 这意味着 `body.length == 2041` 在 Java 层被拒绝，但 C 层完全可以承载。
- 如果协议设计者本意是 `2040`（留 1 字节余量给未来扩展），则 C 层应改为 `2040`。
- 如果本意是 `2041`，则 Java 层应改为 `>= 2041` 或 `> 2041`。
- **当前是 AI 生成或人工维护时的笔误风险。**

### 7.3 超时参数对照表

| 语义 | Java 层值 | C 层值 | 是否一致 | 风险 |
|------|----------|--------|---------|------|
| ACK 超时 | `1500ms` (McuBinder 所有方法) | `2500ms` (MCU_ACK_TIMEOUT_MS) | ❌ **不一致** | Java 比 C 短 1s |
| SETUP 超时 | 无 Java 直接控制 | `3000ms` (MCU_SETUP_TIMEOUT_MS) | - | - |
| HB 间隔 | 无 Java 直接控制 | `2000ms` (MCU_HB_INTERVAL_MS) | - | - |
| OTA 消息超时 | 无 Java 直接控制 | `5000ms` | - | - |
| OTA 数据重试 | Java 无感知 | `3 次` | - | Java 层无法配置 |
| OTA settle 延迟 | Java 无感知 | `100ms / 200ms` | - | - |

**🟡 发现 ACK 超时 1500ms vs 2500ms：**

- Java 层所有 AIDL 方法（setBand, setFrequency, setMute...）统一使用 `1500ms` 调用 `sendAndWaitAck`。
- C 层 `mcu_link_wait_for_ack` 的默认超时是 `2500ms`。
- 实际上 Java 的 `1500ms` 作为参数传入，覆盖了 C 层的默认值。所以运行时行为是 `1500ms`。
- **这不是 bug，但契约不一致**。如果 C 层某个地方硬编码了 `2500ms` 做其他决策（如重传逻辑），会出问题。目前检查没有发现此类硬编码。

### 7.4 项目号/配置对照表

| 语义 | Java 层 | C 层 | 是否一致 |
|------|--------|------|---------|
| OTA 默认项目号 | `"10040"` (UpgradeManager) | 传入参数 | ✅ |
| 项目号最大长度 | 无 Java 检查 | `5` (mcu_fill_upgrade_project_bytes) | ⚠️ Java 未预审 |
| Baudrate 支持 | `460800` (SERIAL_BAUD) | `B460800` / `B115200` | ⚠️ Java 未预审非法值 |
| 串口路径 | `/dev/ttyS7` | 传入参数 | ✅ |

**发现：**
- `UpgradeManager.startUpgrade` 中 `project` 默认值 `"10040"`，长度 5，刚好等于 C 层上限。
- 如果传入 `"100400"`（长度 6），C 层 `mcu_fill_upgrade_project_bytes` 返回 `false`，`mcu_build_upgrade_request_payload` 返回 0，`mcu_upgrade_run` 返回 `ERR_REQ`。
- 但 Java 层 `UpgradeRequest.projectId` 没有任何长度校验，直接透传。
- 同样，`McuLink.open(port, baud)` 允许传入任意 `baudRate`，C 层 `baud_to_speed` 对不支持的值返回 `0`，`serial_open_port` 拒绝。Java 层会收到 `open()=false`。
- **建议**：Java 层在调用 JNI 前做参数预审，提前返回 `ERR_INVALID_ARG`，避免走到 C 层再失败。

### 7.5 AIDL 错误码一致性

McuBinder 的错误码来源：
- `McuErrorCode.ERR_LINK_DOWN` — 链路未建立
- `McuErrorCode.ERR_TIMEOUT` — wait_for_ack 超时 或 send 失败 (seq<0)
- `McuErrorCode.ERR_INVALID_ARG` — 参数非法（Java 层 catch）
- `McuErrorCode.ERR_FILE` — BootUpgrade 文件不存在（UpgradeManager 返回）
- `McuErrorCode.ERR_UPGRADE_BUSY` — OTA 正在进行
- `McuErrorCode.ERR_NOT_IMPLEMENTED` — 废弃接口

**问题：**
- `McuBinder.sendCommand` 对 `seq == -1` 返回 `ERR_TIMEOUT`，对 `seq < -1`（实际是 -2）返回 `ERR_LINK_DOWN`。
- 但旧 API（如 `setBand`）对所有 `seq < 0` 统一返回 `ERR_TIMEOUT`，不区分写失败和超时。
- **这意味着 App 无法区分 "UART 写失败" 和 "MCU 没回 ACK"**。
- 如果未来需要诊断网络/链路质量，这种粗粒度错误码会不够。

---

## 八、性能时延预算

### 8.1 时延预算方法论

时延预算（Latency Budget）= 从**触发事件**到**预期结果可见**之间，各环节允许消耗的最大时间之和。

对于 QSJNIC：
- **上行预算**：MCU 按键/电压变化 → App UI 更新的最大可接受时延（通常 < 100ms 人眼无感知）
- **下行预算**：App 调 setBand → MCU 执行并 ACK 的最大可接受时延（通常 < 200ms）
- **OTA 吞吐预算**：固件升级的最小有效速率（防止 MCU watchdog 超时）

时延分类：

| 类型 | 符号 | 含义 | 示例 |
|------|------|------|------|
| 传输时延 | Tt | 比特在媒介中传播的时间 | UART 发一帧所需时间 |
| 处理时延 | Tp | 节点解析/处理的时间 | C parser、Java Feature 解析 |
| 排队时延 | Tq | 在队列中等待的时间 | HandlerThread MessageQueue 排队 |
| 调度时延 | Ts | 等待 CPU/线程调度的时间 | select 超时、锁竞争、Binder 线程池 |
| 协议时延 | Tproto | 握手/重传/超时等待 | wait_for_ack 1500ms |

**端到端时延 = Σ(Tt + Tp + Tq + Ts + Tproto)**

### 8.2 关键路径时延拆解

#### 路径 A：MCU 上行帧 → App（按键/电压上报）

```
[MCU 发帧]
   │ Tt1: UART 传输（最大帧 2048 bytes @ 460800bps）
   │     = 2048 * 8 / 460800 ≈ 35.6 ms
   │     典型帧（电压 10 bytes）≈ 0.17 ms
   ▼
[/dev/ttyS7]
   │ Ts1: C reader_thread select() 等待
   │     = 0~50 ms（serial_read_some timeout=50ms）
   │     平均 ≈ 25 ms（帧随机到达）
   ▼
[C: live_reader_consume]
   │ Tp1: parser + decode + protocol state machine
   │     ≈ 0.01~0.1 ms（纯内存操作）
   │ Tp2: auto-ACK 构建 + serial_write_all
   │     Tt2: ACK 帧 7 bytes @ 460800 ≈ 0.12 ms
   │     Tp3: tcdrain 等待 UART FIFO 空
   │         ≈ 0.1~1 ms（取决于系统调度）
   ▼
[C++: jni_notify_cb]
   │ Ts2: AttachCurrentThread（仅首次）
   │     首次 ≈ 1~5 ms，后续 ≈ 0（已 attach）
   │ Tp4: NewByteArray + SetByteArrayRegion
   │     ≈ 0.01~0.5 ms（与 payload 大小相关）
   │ Tp5: CallStaticVoidMethod（JNI 到 Java）
   │     ≈ 0.05~0.2 ms
   │ Tp6: DeleteLocalRef
   │     ≈ 0.01 ms
   ▼
[Java: FrameRouter.onMcuFrame]
   │ Tp7: payload.clone()
   │     ≈ 0.01~0.5 ms（与 payload 大小相关）
   │ Tq1: HANDLER.post() 进入 MessageQueue
   │     ≈ 0（立即入队，无阻塞）
   ▼
[Java: FrameRouter.dispatch] (FrameRouter HandlerThread)
   │ Tq2: MessageQueue 排队等待（若前面有未处理帧）
   │     0~∞（取决于积压帧数）
   │ Tp8: DomainBus 分发 + Feature.onFrame
   │     ≈ 0.1~1 ms（遍历 CopyOnWriteArrayList + switch）
   │ Tp9: VehicleFeature 解析 + snapshot 更新
   │     ≈ 0.1~1 ms（synchronized + 字段赋值）
   │ Tp10: broadcastVehicle() → beginBroadcast + for 循环
   │     ≈ 0.5~5 ms（取决于 listener 数量和 Binder 对端响应）
   │     ⚠️ 如果 App listener 回调阻塞，此处会阻塞 FrameRouter 线程！
   ▼
[Binder IPC] → [App 进程]
   │ Ts3: Binder 线程池调度
   │     ≈ 1~5 ms（跨进程）
   │ Tp11: App 回调执行（onVoltage）
   │     ≈ 1~10 ms（取决于 App 业务逻辑）
   ▼
[App UI 更新]
```

**时延预算汇总（上行路径）**

| 环节 | 最小值 | 典型值 | 最大值 | 备注 |
|------|--------|--------|--------|------|
| UART 传输 Tt1 | 0.17ms | 0.17ms | 35.6ms | 小帧 vs 大帧 |
| select 等待 Ts1 | 0ms | 25ms | 50ms | 均匀分布 |
| C 层处理 Tp1~3 | 0.2ms | 0.5ms | 2ms | parser + ACK + tcdrain |
| JNI 回调 Tp4~6 | 0.1ms | 0.2ms | 6ms | 首次 Attach 最贵 |
| clone + post Tp7 + Tq1 | 0.01ms | 0.05ms | 1ms | - |
| Handler 排队 Tq2 | 0ms | 0ms | ∞ | **风险点：队列积压** |
| Feature 解析 Tp8~9 | 0.2ms | 0.5ms | 2ms | - |
| 广播 + Binder Tp10 + Ts3 | 2ms | 3ms | 20ms | listener 阻塞会恶化 |
| App 处理 Tp11 | 1ms | 3ms | 10ms | 不可控 |
| **端到端总计** | **~3.7ms** | **~32ms** | **~125ms+** | 大帧/排队时超标 |

**关键风险：**
1. 🔴 **FrameRouter HandlerThread 单线程瓶颈**：如果 MCU 以高频 burst 发帧（如 OTA 调试时大量日志帧），而 App 端 listener 处理慢，`broadcastTyped` 会阻塞 HandlerThread，后续帧在 MessageQueue 中排队，时延累积。
2. 🟡 **select 超时 50ms**：这是 C 层 `serial_read_some` 的硬编码超时。意味着 reader_thread 最多 50ms 才能发现一帧到达。对于 7 字节小帧（传输仅需 0.12ms），50ms 的 poll 间隔是主要瓶颈。
3. 🟡 **tcdrain 阻塞**：`serial_write_all` 每次写完都调 `tcdrain`，等待 UART 硬件 FIFO 排空。在 460800bps 下，FIFO 通常 16~64 字节深，排空时间 < 1ms，但如果内核调度延迟，可能达到数 ms。

#### 路径 B：App 命令 → MCU → ACK（setBand 等 AIDL 调用）

```
[App 调用 setBand]
   │ Ts4: Binder IPC（App → Service）
   │     ≈ 1~3 ms
   ▼
[McuBinder.setBandWithFreq] (Service Binder 线程池)
   │ Tp12: 参数校验 + isLinkAlive()
   │     ≈ 0.01~0.1 ms
   │ Tp13: Commands.Radio.setBand() 构建 payload
   │     ≈ 0.01 ms
   ▼
[McuLink.sendAndWaitAck]
   │ Tp14: 构建 byte[2+bodyLen]
   │     ≈ 0.01 ms
   ▼
[JNI: nativeMcuLinkSendAndWaitAck]
   │ Ts5: pthread_mutex_lock(&g_link_mutex)
   │     0~∞（如果有其他 JNI 调用持锁）
   │ Ts6: pthread_mutex_lock(&ctx->lock)
   │     0~∞（如果有 reader/heartbeat 线程持锁）
   │ Tp15: GetByteArrayElements (JNI 数组锁定)
   │     ≈ 0.01~0.1 ms
   │ Tp16: mcu_link_send_data_frame → live_write_frame_locked
   │     Tp17: mcu_build_frame_bytes
   │         ≈ 0.001 ms
   │     Tt3: serial_write_all → UART 传输
   │         典型命令帧 ~50 bytes → 0.87 ms
   │     Tp18: tcdrain
   │         ≈ 0.1~1 ms
   │ Tp19: ReleaseByteArrayElements
   │     ≈ 0.01 ms
   │     ⚠️ 如果 sent=false，不进入 wait，直接 rc=-2
   ▼
[C: mcu_link_wait_for_ack] (仅在 sent=true 时)
   │ Tproto1: 等待 MCU 处理 + ACK 回传
   │     MCU 处理时间：假设 5~20 ms
   │     ACK 传输 Tt4：7 bytes → 0.12 ms
   │     回程 select Ts1：0~50 ms（reader 线程 poll）
   │     ACK 解析 + auto-ACK 不写回（reader 发现是 ACK 不 auto-ACK）
   │     event_queue_push → event_queue_wait_pop
   │         使用 pthread_cond_timedwait，精确唤醒
   │         等待时间 ≈ MCU 处理 + 传输 + 调度（不含 select）
   │     超时阈值：1500ms（Java 传入）
   ▼
[JNI 返回 rc]
   │ Ts7: pthread_mutex_unlock
   │     ≈ 0.01 ms
   ▼
[Java: seq >= 0 ? OK : ERR_TIMEOUT]
   │ Ts8: Binder IPC（Service → App）
   │     ≈ 1~3 ms
   ▼
[App 收到结果]
```

**时延预算汇总（下行路径）**

| 环节 | 最小值 | 典型值 | 最大值 | 备注 |
|------|--------|--------|--------|------|
| Binder App→Service Ts4 | 0.5ms | 1.5ms | 5ms | - |
| Java 层处理 Tp12~14 | 0.01ms | 0.05ms | 0.5ms | - |
| JNI 锁竞争 Ts5~6 | 0ms | 0.1ms | 10ms | 高并发时恶化 |
| JNI 数组拷贝 Tp15,19 | 0.01ms | 0.05ms | 0.5ms | - |
| UART 写 Tt3 + Tp18 | 0.5ms | 1ms | 5ms | tcdrain 主导 |
| MCU 处理 Tproto1 | 5ms | 10ms | 50ms | MCU 固件行为 |
| ACK 回程 Tt4 + Ts1 | 0.1ms | 25ms | 50ms | select 50ms 上限 |
| cond_wait 唤醒 | 0.01ms | 0.05ms | 1ms | - |
| Binder Service→App Ts8 | 0.5ms | 1.5ms | 5ms | - |
| **端到端总计（成功）** | **~6ms** | **~39ms** | **~127ms** | 正常 < 1500ms |
| **端到端总计（超时）** | **1500ms** | **1500ms** | **1500ms** | 硬超时 |

**关键风险：**
1. 🟡 **g_link_mutex + ctx->lock 双重锁竞争**：如果 OTA 正在进行（`mcu_upgrade_run` 持有 `g_link_mutex` 并在 `wait_for_update_frame` 中阻塞），App 的 `setBand` 调用会在 `nativeMcuLinkSendAndWaitAck` 入口阻塞，直到 OTA 释放锁。如果 OTA 单包等待 5000ms，`setBand` 最多阻塞 5s+。
2. 🟡 **select 50ms 让 ACK 检测不精确**：即使 MCU 立刻回 ACK，reader 线程最多 50ms 后才读取到，导致 `wait_for_ack` 的时延比理论值多 0~50ms。
3. 🟢 **tcdrain 是隐藏瓶颈**：`serial_write_all` 中 `while(written < len)` 循环写完后调 `tcdrain`。如果 UART 驱动层有延迟（如 DMA 未完成），`tcdrain` 会阻塞调用线程（持有 `ctx->lock`），导致 reader 线程和 heartbeat 线程无法获取锁。

#### 路径 C：OTA 单包传输（数据量大、 pacing 控制）

```
[mcu_upgrade_run 主循环]
   │ 收到 UPDATE_IND_AQUIRE(index=N)
   │ Tp20: 计算 offset + chunk_len
   │     ≈ 0.001 ms
   │ Tp21: mcu_build_upgrade_data_payload
   │     ≈ 0.001 ms
   │ Tsleep1: MCU_UPGRADE_CHUNK_SETTLE_MS
   │     = 200 ms（固定等待，让 MCU 准备接收）
   ▼
[发送 UPDATE_DATA]
   │ JNI 锁竞争 + 数组拷贝（同路径 B）
   │ Tp22: mcu_link_send_data_frame
   │     注意：OTA 大帧走 serial_write_all_paced
   │     Tt5:  paced 写入 128 bytes / 1ms gap
   │         pack_len=1024 → 8 次 write + 7 次 sleep(1ms)
   │         = ~15 ms（含 syscall 开销）
   │     Tp23: tcdrain
   │         ≈ 1~5 ms
   ▼
[等待 ACK]
   │ Tproto2: mcu_link_wait_for_ack(timeout=2500ms)
   │     MCU 处理 + ACK 回程：通常 10~50 ms
   │     若失败，重试最多 3 次，间隔 30ms
   ▼
[进度回调 jni_upgrade_progress_cb]
   │ Tp24: CallStaticVoidMethod（12 个参数）
   │     ≈ 0.1~0.5 ms
   ▼
[Java: McuLink.onUpgradeProgress]
   │ Tp25: 调用 listener.onProgress()
   │     ≈ 0.1~1 ms
```

**时延预算汇总（OTA 单包）**

| 环节 | 最小值 | 典型值 | 最大值 | 备注 |
|------|--------|--------|--------|------|
| MCU settle 等待 Tsleep1 | 200ms | 200ms | 200ms | **硬编码，不可配置** |
| 构建 payload Tp20~21 | 0.001ms | 0.01ms | 0.1ms | - |
| JNI 锁 + 拷贝 | 0.1ms | 0.5ms | 10ms | - |
| paced UART 写 Tt5 | 10ms | 15ms | 30ms | pack_len 越大越慢 |
| tcdrain Tp23 | 0.5ms | 2ms | 10ms | - |
| MCU 处理 + ACK Tproto2 | 5ms | 20ms | 100ms | 取决于 MCU 擦写 Flash |
| 回调 Java Tp24~25 | 0.2ms | 0.5ms | 2ms | - |
| **单包总计（成功）** | **~216ms** | **~238ms** | **~372ms** | - |
| **单包重试代价** | +30ms | +30ms | +30ms | 每次重试间隔 |

**OTA 全量估算**：
- 文档说 211KB 图像，实测 ~65s，0 重试。
- 反推：65000ms / 211 ≈ 308 ms/包。说明 pack_len 较小（可能 256~512 bytes），或 MCU 处理较慢。
- 如果 pack_len=256：每包 paced 写 = 2次×128B + 1次×剩余，sleep 1次，约 5ms 写时间。
- 但 Tsleep1=200ms 是主导！211 包 × 200ms = 42.2s。加上其他开销，与 65s 吻合。

**📝 设计说明：OTA 200ms settle 是 MCU 固件约束**
- `MCU_UPGRADE_CHUNK_SETTLE_MS = 200ms` 在每次发 DATA 前硬编码 sleep，源自参考实现 `mcu_structured_test.c`。
- 注释明确说明：MCU RX 端使用 tight software loop 轮询 UART 字节。200ms 的间隙是为了确保 MCU 完成 UPDATE_IND 处理并准备好接收 DATA 帧。
- 该值是 MCU 固件侧的协议要求，**非 Service 代码缺陷**，因此不作为优化项。若后续 MCU 固件改为中断驱动接收，可由联调双方重新协商此值。

#### 路径 D：Service 启动与链路建立

```
[System 启动 QsMcuSystemService]
   │
   ├── onCreate()
   │   │ Tp26: 初始化 Feature + DomainBus + Binder
   │   │     ≈ 5~20 ms
   │   │ Tp27: startForeground(notification)
   │   │     ≈ 10~50 ms
   │   ▼
   ├── tryOpenSerial(attempt=0)
   │   │ Tproto3: McuLink.open() → nativeMcuLinkOpen
   │   │     serial_open_port: open + tcsetattr + fcntl
   │   │     ≈ 10~50 ms
   │   │     若失败 → postDelayed(1000ms) → 重试
   │   │     最大累积：1+2+4+8+16+30+60 = 121s（前7次）
   │   ▼
   ├── open 成功 → start() → doSetup()
   │   │ Tproto4: mcu_link_do_setup
   │   │     最多 3 次 attempt
   │   │     每次：send_setup + wait_for_setupack(3000ms)
   │   │     若 MCU 未就绪：3 × 3000ms = 9000ms
   │   ▼
   ├── scheduleInitProbe()
   │   │ Tproto5: 轮询 isLinkAlive()，每 200ms 一次，最多 30 次
   │   │     最大 6000ms
   │   │     成功后 sendNormal(0x01, 0x81)
   │   ▼
   ├── scheduleRepeatProbe()
   │   │ 每 10s 周期性探针（后台维护）
```

**时延预算汇总（Service 启动）**

| 环节 | 最小值 | 典型值 | 最大值 | 备注 |
|------|--------|--------|--------|------|
| onCreate 初始化 | 5ms | 15ms | 50ms | - |
| 串口打开 | 10ms | 20ms | 50ms | - |
| 串口重试累积 | 0ms | 0ms | 121s | 若 MCU 未上电 |
| doSetup | 10ms | 100ms | 9000ms | 3次×3s |
| initProbe | 200ms | 500ms | 6000ms | 轮询链路 |
| **启动总计** | **~225ms** | **~635ms** | **~15s+** | MCU 未就绪时超长 |

#### 路径 E：心跳维护

```
[heartbeat_thread 循环]
   │ Ts9: sleep(100ms) 轮询
   │     每 100ms 检查一次条件
   │
   ├── 条件：link_state==LINKED && current_ms >= last_rx_frame_ms + 2000ms
   │   └── 即 RX 静默 2000ms 后发 HB
   │
   ├── live_send_heartbeat_locked()
   │   │ Tt6: UART 写 7 bytes → 0.12 ms
   │   │ Tp28: tcdrain → 0.1~1 ms
   │
   └── last_hb_tx_ms 更新
```

**时延预算**

| 环节 | 值 | 备注 |
|------|-----|------|
| 轮询间隔 | 100ms | 硬编码 `mcu_sleep_ms(100)` |
| HB 触发阈值 | 2000ms | `MCU_HB_INTERVAL_MS` |
| HB 传输时间 | 0.12ms | 空帧 7 bytes |
| **HB 精度** | **2000ms ± 100ms** | sleep 轮询导致最大 100ms 抖动 |

**风险**：
- 🟢 精度要求不高，±100ms 可接受。
- 🟡 但 `heartbeat_thread` 在 `mcu_link_close` 时需要 `pthread_join`，最多等 100ms（当前 sleep 周期）。

### 8.3 瓶颈识别与优化建议

| 瓶颈 | 位置 | 影响 | 优化方向 |
|------|------|------|---------|
| **select 超时 50ms** | `serial_read_some` | 上行帧检测延迟 0~50ms | 降至 5~10ms，或改用 epoll/SIGIO |
| **OTA 200ms settle** | `MCU_UPGRADE_CHUNK_SETTLE_MS` | 占 OTA 总时间 60~80% | ~~联调实测 MCU 最小准备时间，动态调整~~ → **设计约束**（MCU tight loop 轮询需要，非 Service 缺陷） |
| **tcdrain 阻塞** | `serial_write_all` 末尾 | 阻塞锁持有者，影响 reader/HB | 移至锁外部，或取消 tcdrain |
| **FrameRouter 单线程** | `HANDLER.post(dispatch)` | App listener 阻塞导致排队 | listener 回调改为异步 post |
| **UART 波特率 460800** | 硬件配置 | 大帧传输 35.6ms | 升级至 921600bps 或更高 |

### 8.4 时延预算速查表

| 场景 | 预算目标 | 当前典型值 | 当前最大值 | 是否达标 |
|------|---------|-----------|-----------|---------|
| 上行小帧（按键/电压）→ App | < 50ms | ~32ms | ~125ms | ⚠️ 边界 |
| 上行大帧（日志/版本）→ App | < 100ms | ~67ms | ~160ms | ✅ |
| 下行命令（setBand）→ ACK | < 100ms | ~39ms | ~127ms | ✅ |
| OTA 单包传输 | < 300ms | ~238ms | ~372ms | ✅ |
| OTA 全量（211KB） | < 30s | ~65s | ~120s+ | ⚠️ 受 MCU 200ms settle 约束，属设计预期 |
| Service 冷启动 | < 1s | ~635ms | ~15s+ | ⚠️ 边界 |
| HB 精度 | ± 200ms | ± 100ms | ± 100ms | ✅ |

---

## 九、问题汇总总表

| 序号 | 问题描述 | 风险等级 | 维度 | 建议 |
|------|---------|---------|------|------|
| 1 | OTA 期间 `mcu_link_wait_for_update_frame` 丢弃 MCU 主动上报的非 UPDATE 帧（按键/传感器等）；若 OTA 为后台运行且需保持业务通道，事件会丢失。另 `McuBinder.sendCommand` 未在代码层拦截 OTA 期间的普通命令 | 🟡 **中** | 状态机/故障树 | 产品层保证 OTA 期间无交互即可；若需后台 OTA，建议在 Java 层增加 OTA 状态检查，拒绝非 OTA 命令，并评估 C 层 deferred 队列保留非 UPDATE 帧 |
| 2 | `mcu_link_wait_for_ack` 的 `deferred` 数组硬编码 128，超限时静默丢帧 | 🟡 **中** | 决策树/时序 | 增大数组或改为动态链表；超限加 `MCU_LOGW` |
| 3 | C reader 线程同步回调 Java（`jni_notify_cb`），若 JVM/GC 阻塞会卡住 UART 读取和 auto-ACK | 🟡 **中** | 数据流/时序 | 评估增加 C→Java 的无锁环形队列，解耦 reader 线程 |
| 4 | `KeyInjector.enabled` 无 `volatile`，多线程可见性问题 | 🟡 **中** | 时序 | `private volatile boolean enabled` |
| 5 | `KeyInjector.pendingKeyCode/downTime` 无同步，设计脆弱 | 🟢 **低** | 时序 | 加 `synchronized` 或确保只在单线程访问 |
| 6 | `UpgradeManager.startUpgrade` 的 `running` 检查和执行非原子，可能并发启动两次 OTA | 🟡 **中** | 状态机 | 将 `running` 的检查和 `worker.post` 封装在同步块内 |
| 7 | `McuBinder` 旧 API（如 `setBand`）对 `sendAndWaitAck` 返回 `-2`（写错误）和 `-1`（超时）统一返回 `ERR_TIMEOUT` | 🟢 **低** | 决策树 | 参考 `sendCommand` 的写法，细化错误码 |
| 8 | `injectKeyEvent` 每次反射获取 `getParameterCount()`，性能开销 | 🟢 **低** | 决策树/性能 | 在构造函数中判断并缓存参数数量/类型 |
| 9 | `mcu_protocol_on_frame` 收到重复 SETUPACK 会重置 `send_seq`，可能导致 seq 跳变 | 🟢 **低** | 状态机 | 加状态判断：`if (state->link_state == LINK_LINKED) ignore;` |
| 10 | `FrameRouter.onMcuFrame` 的注释说 "native buffer is only valid during the call"，但实际上 `payload` 已经是 `NewByteArray` 分配的 JVM 堆数组，生命周期由 JVM GC 管理 | 🟢 **低** | 数据流 | 注释可更新，避免误导 |
| 11 | `QsMcuSystemService` 的 `dump()` 调用 `McuLink.get().getStats()`，但 `getStats()` 被标记 `@Deprecated` 且返回 zeros | 🟢 **低** | 资源 | 移除或替换为实际统计 |
| 12 | `ReleaseByteArrayElements` 使用 `JNI_ABORT`，若未来代码修改写入了数组，数据不会同步回 Java | 🟢 **低** | 数据流/资源 | 当前是只读，安全。加注释说明 |
| 13 | JNI GlobalRef (`g_router_class`, `g_mculink_class`) 无 DeleteGlobalRef | 🟡 **中** | 资源所有权 | so 卸载时增加 DeleteGlobalRef；或加注释说明依赖进程生命周期 |
| 14 | UART 断开后 `isLinkAlive()` 仍返回 true（链路假活） | 🟢 **低** | 故障树 | **硬件已规避**：MCU-SoC 采用共复位设计，一方复位另一方立刻重启，Service 随进程重建，不存在持续假活窗口。C 层可增加 RX 静默检测作为防御性补充，但非必要 |
| 15 | MCU 重启后无法自动恢复（FT_SETUP 在 LINKED 状态下被忽略） | 🟡 **中** | 故障树 | LINKED 状态下收到 SETUP 自动重置状态机并重新 doSetup |
| 16 | Service `onDestroy` 可能被 Binder 调用阻塞 5s+，有 ANR 风险 | 🟡 **中** | 故障树 | onDestroy 中避免同步等待；或用超时机制强制关闭 |
| 17 | Java `sendCommand` 限制 `body.length > 2040`，C 层支持 `2041` | 🟡 **中** | 跨层契约 | 统一为 2040 或 2041 |
| 18 | `FRAME_TYPE_NOTIFY (0x07)` 与 `FT_TRANSFER (0x07)` 命名语义不一致 | 🟢 **低** | 跨层契约 | 统一命名，或加注释说明等价 |
| 19 | Java 层未预审 `projectId` 长度、`baudRate` 合法性 | 🟢 **低** | 跨层契约 | Java 层增加参数预审，提前返回 ERR_INVALID_ARG |
| 20 | 旧 API 错误码粗粒度（无法区分写失败 vs 超时） | 🟢 **低** | 跨层契约 | 统一使用 sendCommand 的细粒度错误码策略 |
| 21 | `McuLink.upgradeListener` 未在 OTA 结束后置 null，可能内存泄漏 | 🟢 **低** | 资源所有权 | OTA 结束或 shutdown 时显式 setUpgradeListener(null) |
| 22 | select 超时 50ms 是上行帧检测主要瓶颈 | 🟡 **中** | 性能 | 降至 5~10ms，或改用异步通知 |
| 23 | OTA 200ms settle 延迟占传输时间 80%+ | 🟢 **低** | 性能 | **设计约束**：MCU RX 使用 tight loop 轮询，参考实现已定义该值；非 Service 缺陷，无需优化 |
| 24 | tcdrain 阻塞锁持有者，影响并发 | 🟡 **中** | 性能 | 移至锁外部或取消 |
| 25 | FrameRouter HandlerThread 单线程，listener 阻塞会导致帧排队 | 🟡 **中** | 性能 | listener 回调改为异步 |
| 26 | C 层 `event_queue_wait_pop` 使用 `CLOCK_REALTIME`，系统时间跳变会导致 wait 永久阻塞或立即超时 | 🟡 **中** | C 语言陷阱/时序 | 改为 `CLOCK_MONOTONIC` |
| 27 | Java `QsMcuSystemService.sInstance` 非 `volatile`，多线程访问可能看到旧值或部分构造对象 | 🟡 **中** | Java 陷阱/时序 | `private volatile static QsMcuSystemService sInstance` |
| 28 | Java `QsMcuSystemService.onDestroy` 未移除 `mainHandler` 的 `repeatProbeRunnable` post，Service 被延迟引用导致泄漏 | 🟡 **中** | Java 陷阱/资源 | `onDestroy` 中调 `mainHandler.removeCallbacks(repeatProbeRunnable)` |
| 29 | C 层 `mcu_sleep_ms` 未处理 `nanosleep` 被信号中断，实际睡眠时间可能不足 | 🟢 **低** | C 语言陷阱 | 循环调用 `nanosleep` 直到剩余时间为 0 |
| 30 | C 层 `mcu_link_wait_for_ack` 的 `deferred[128]` 在栈上分配约 263KB，接近 pthread 默认栈上限 | 🟢 **低** | C 语言陷阱/资源 | 改为堆分配或减小容量 |
| 31 | Java `McuLink.sendBusinessFrame` 未检查 `body.length` 上限，超大数组导致 `NegativeArraySizeException` | 🟢 **低** | Java 陷阱/决策树 | 增加 `body.length > 2040` 前置检查 |

---

## 十、附录：关键文件路径

```
service/src/main/java/com/qsjnic/mcu/service/McuLink.java
service/src/main/java/com/qsjnic/mcu/service/FrameRouter.java
service/src/main/java/com/qsjnic/mcu/service/McuBinder.java
service/src/main/java/com/qsjnic/mcu/service/QsMcuSystemService.java
service/src/main/java/com/qsjnic/mcu/service/UpgradeManager.java
service/src/main/java/com/qsjnic/mcu/service/feature/DomainBus.java
service/src/main/java/com/qsjnic/mcu/service/feature/VehicleFeature.java
service/src/main/java/com/qsjnic/mcu/service/feature/KeyFeature.java
service/src/main/java/com/qsjnic/mcu/service/feature/KeyInjector.java
service/src/main/java/com/qsjnic/mcu/service/feature/RadioFeature.java
service/src/main/java/com/qsjnic/mcu/service/feature/UpgradeFeature.java
service/src/main/cpp/jni_bridge.cpp
service/src/main/cpp/mcu_protocol.h
service/src/main/cpp/mcu_protocol.c
service/src/main/cpp/mcu_transport.h
service/src/main/cpp/mcu_transport.c
service/src/main/cpp/mcu_upgrade.h
service/src/main/cpp/mcu_upgrade.c
```

---

*报告生成时间：2026-06-04*
