# QSJNIC SystemService 实施计划

> 本文档是项目执行的**主线计划**,把架构设计落到 16 个可独立验证的步骤。
> 每一步执行完都能在设备上**眼见为实**(log/dumpsys/界面/测试报告),确认通过再进下一步。
> 状态约定:⏳ 未开始 / 🔄 进行中 / ✅ 完成 / ⏸ 受阻(标注阻塞原因)。

---

## 阶段总览

```
┌─────────────────────────────────────────────────────────────────────┐
│ Phase A: 工程骨架(2 步)             ─→ 多模块构建跑得起来         │
│ Phase B: Service 内部能力(3 步)     ─→ Service 自己跟 MCU 通讯    │
│ Phase C: 跨进程契约(3 步)           ─→ SDK 经 Binder 调到 Service │
│ Phase D: 业务域对接(3 步)           ─→ 三个 demo 跑起来           │
│ Phase E: 验收测试(4 步)             ─→ 4 项验收指标全部达标       │
│ Phase F: 交付打包(1 步)             ─→ release 产物 + 文档定稿    │
└─────────────────────────────────────────────────────────────────────┘
                                 16 步
```

| # | 步骤 | 状态 | 关键交付 | 验证手段 |
|---|---|---|---|---|
| **A1** | 多模块 Gradle 骨架 | ✅ | settings/build.gradle.kts、空 module | `./gradlew tasks` 列出三模块 |
| **A2** | Service 空壳可运行 | ✅ | qsmw-service.apk 最小版 | logcat 看启动 log |
| **B1** | Service 持有 /dev/ttyS7 | ✅ | libmcujni.so + JNI 桥 | logcat + lsof |
| **B2** | Service 与 MCU 建链 | ✅ | Setup/Heartbeat 收发 | logcat 看 ">>> Link Established" |
| **B3** | Service 完整 RX 路由 | ✅ | FrameRouter Java | logcat 看 MCU Version 字符串 |
| **C1** | Contract 模块 AIDL 骨架 | ✅ | IMcuService 最小版 | `:contract:assembleDebug` 成功 |
| **C2** | Service 暴露 Binder | ✅ | onBind / Stub 实现 | `service list` / `service call` |
| **C3** | SDK 最小入口 | ✅ | McuClient.init() | 装一个 hello App 调通 |
| **D1** | Vehicle 域 + vehicle-demo | ✅ | 7 个 cmd + Listener + 独立 AS demo | demo 显示电压/ACC |
| **D2** | Radio 域 + radio-demo | ✅ | 8 个 cmd + Listener + demo | demo 切台听到广播 |
| **D3** | Upgrade 域 + ota-demo | ✅⏸ | 状态机 + demo,**仅到 IND_AQUIRE** | demo 显示协议握手前 4 步 PASS,DATA 因 MCU bug 阻塞 |
| **E1** | 多进程隔离测试 | ✅ | TC-IS-01/02/03 PASS | 三 App 同时运行,Listener 独立,kill 后 auto-clean |
| **E2** | 重连自愈测试 | ✅ | TC-RC-01/02 PASS | kill 后 2s 重启,Listener 自动恢复 |
| **E3** | 延迟测试 + dumpsys | ⚠️ | TC-LT-01/02 + dumpsys 接口 | dumpsys 输出完整,P95 机制正确,样本不足待 MCU 恢复后补测 |
| **E4** | 权限测试 | ✅ | TC-PM-01/02 PASS | 无权限 bind false,同签名+有权限 bind true |
| **F1** | 交付打包 | ✅ | release/、demos zip、文档定稿 | `./gradlew packRelease` 产出全部 5 个文件 + sha256 |

---

## Phase A:工程骨架

### A1. 多模块 Gradle 骨架

**目标**:仓库根能跑 Gradle 多模块构建,三个模块都是空骨架。

**做什么**

- 写 `settings.gradle.kts`(`include(":contract", ":service", ":sdk")`)
- 写 `build.gradle.kts`(根 + 各模块)
- 写 `gradle/libs.versions.toml`(AGP / Kotlin / minSdk / targetSdk 统一管理)
- 三个模块各自有空 `src/main/AndroidManifest.xml`(对 service)和空 `build.gradle.kts`
- `.gitignore` 加 `build/`、`.idea/`、`local.properties`、`release/`

**不做什么**

- 不写任何业务代码
- 不写任何 AIDL
- 不创建 demos/(等 D 阶段)

**验证**

```bash
./gradlew tasks --all | grep -E "(contract|service|sdk)"
# 期望:三个模块的 assembleDebug / assembleRelease 等任务都列出

./gradlew assembleDebug
# 期望:BUILD SUCCESSFUL,三模块都跑过 lint
```

**完成后状态**

- `release/` 此时还没有产物(没编 release 配置)
- 仓库根 `./gradlew build` 干净通过

---

### A2. Service 空壳可运行

**目标**:把 A1 的空 Service 写成最小可运行版,装到车机能看到启动 log。

**做什么**

- `service/src/main/AndroidManifest.xml`:
  - package=`com.qsjnic.mcu.service`
  - `application` 加 `persistent="true"`(尝试,实际权限取决于签名)
  - 一个 `BootCompletedReceiver` + `QsMcuSystemService` 类
  - service 加 `exported=false`(B 阶段先不暴露 Binder)
- `QsMcuSystemService.java`(继承 `Service`):
  - `onCreate()` 打 log `"QsMcuSystemService started, PID=" + Process.myPid()`
  - `onStartCommand()` 返回 `START_STICKY`
- `BootCompletedReceiver`:监听 `BOOT_COMPLETED` 后 `startService` 自身

**验证**

```bash
./gradlew :service:assembleDebug
adb install -r service/build/outputs/apk/debug/service-debug.apk
adb shell am startservice -n com.qsjnic.mcu.service/.QsMcuSystemService
adb shell logcat -s QsMcuSystemService:*
# 期望:看到 "QsMcuSystemService started, PID=..."

adb shell dumpsys activity services com.qsjnic.mcu.service
# 期望:显示 RUNNING
```

**完成后状态**

- Service 不做任何业务,只证明能启动
- 此时 SDK 层、AIDL 都还没写,任何应用都还调不到 Service

---

## Phase B:Service 内部能力(纯 native + 内部线程,不涉及 IPC)

### B1. Service 持有 `/dev/ttyS7`

**目标**:Service 能成功打开串口,JNI 桥跑通。

**做什么**

- `service/src/main/cpp/CMakeLists.txt`:include `test_Serial/serial_io.c` 与 `protocol.c`
- `service/src/main/cpp/jni_bridge.cpp`:实现 `nativeOpenPort(String)` / `nativeClosePort(int)`
- `service/src/main/java/.../McuLink.java`:Java 层封装,`open()` 调 native 拿 fd
- `QsMcuSystemService.onCreate()` 调 `McuLink.getInstance().open("/dev/ttyS7")`

**验证**

```bash
adb root && adb shell setenforce 0    # 开发期临时关 SELinux 简化验证
adb install -r ...
adb shell logcat -s QsMcuSystemService:* McuLink:*
# 期望:看到 "serial fd=N opened successfully"

adb shell lsof /dev/ttyS7
# 期望:com.qsjnic.mcu.service 进程持有该 fd
```

**完成后状态**

- 串口已被 Service 持有(`test_Serial/qsjnic_test` 此时跑不动 = 正常,fd 冲突)
- 还没收发任何帧

---

### B2. Service 与 MCU 建链

**目标**:把 `test_Serial/main_test.c` 的链路状态机搬到 Service 内部,完成 Setup/Heartbeat 握手。

**做什么**

- 拆出 `service/src/main/cpp/link_state_machine.c`:封装 `current_seq`、`is_linked` 进 `McuLink` struct
- JNI 暴露:`nativeStartLink()` / `nativeStopLink()` / `nativeIsLinkAlive()`
- 内部启 Tx 线程(2s 心跳)和 Rx 线程(读串口、解帧)
- Java 层 `McuLink.start()` 调 native 启动两个线程

**验证**

```bash
adb shell logcat -s QsMcuSystemService:* McuLink:*
# 期望(对应 test_Serial 行为):
#   "TX [Setup] len=7, hex: FF AA 00 07 01 01 F6"
#   "RX [Frame] len=7, hex: FF AA 00 07 01 03 F4"
#   ">>> Link Established <<<"
#   每 2 秒一次 "TX [Heartbeat]"

# 用 SystemService 的 dumpsys(下一步要做但可先简易实现一个 "echo" 版)
adb shell dumpsys activity services com.qsjnic.mcu.service
# 自定义打印一段 "linkAlive=true heartbeatRxCount=N"
```

**完成后状态**

- Service 与 MCU 建链稳定,行为与 `test_Serial` 一致
- 还没分发任何 RX 数据给上层(B3 才做)

---

### B3. Service 完整 RX 路由

**目标**:Rx 线程把 MCU 上报的 Public/Radio/Update 帧解析并打到 logcat,验证业务数据流通。

**做什么**

- `service/src/main/cpp/frame_dispatcher.c`:按 (msg_type, cmd) 分发,通过 JNI callback 投到 Java
- `service/src/main/java/.../FrameRouter.java`:Java 端 Looper 线程接收 native 回调,解析 payload
- 暂时只在 Java log 里打:`"MCU Version: <str>"`、`"VOLTAGE: N mV"`、`"LINE_DETECT: bitmap=0xNN"`

**验证**

```bash
adb shell logcat -s FrameRouter:*
# 期望(MCU 自然上报):
#   "MCU Version: E112C069XZA0120015"
#   每 100ms 一条 "VOLTAGE: 12655 mV"
#   "LINE_DETECT: ACC=1 IG=0 ..."
```

**完成后状态**

- Service 内部已能"看见"全部 MCU 上报数据
- 还没向跨进程暴露任何东西(C 阶段才做)

---

## Phase C:跨进程契约

### C1. Contract 模块 AIDL 骨架

**目标**:`:contract` 模块写最小的 AIDL,能编译出 stub。

**做什么**

- `contract/src/main/aidl/com/qsjnic/mcu/contract/IMcuService.aidl`:
  ```aidl
  package com.qsjnic.mcu.contract;
  interface IMcuService {
      int getAidlVersion();
      boolean isLinkAlive();
  }
  ```
- 暂不写 Listener AIDL,不写 Parcelable
- 在 `contract/build.gradle.kts` 配 `buildFeatures { aidl = true }`

**验证**

```bash
./gradlew :contract:assembleDebug
# 期望:BUILD SUCCESSFUL
ls contract/build/generated/aidl_source_output_dir/
# 期望:看到生成的 IMcuService.java (含 Stub 内部类)
```

**完成后状态**

- 契约模块产出可被 service / sdk 依赖
- 实现部分还没写

---

### C2. Service 暴露 Binder

**目标**:Service 实现 IMcuService.Stub,通过 onBind 返回。

**做什么**

- `service/build.gradle.kts` 加 `implementation(project(":contract"))`
- `service/src/main/AndroidManifest.xml` Service 改 `exported="true"`,加 intent-filter `cn.qsjnic.IMcuService`
- 加自定义权限 `cn.qsjnic.permission.MCU_ACCESS`(暂用 normal 级,等 E4 改回 signature|privileged)
- `QsMcuSystemService.onBind()` 返回 `IMcuService.Stub` 实现:
  - `getAidlVersion() = 1`
  - `isLinkAlive() = mcuLink.isAlive()`

**验证**

```bash
# 用 adb shell + service call(原生 binder 调用工具)
adb shell service list | grep mcu
# (此版不注册到 ServiceManager,所以这里看不到。下面用 am 间接验证)

# 或临时写一个最小 hello 客户端 apk(也可放到 C3)
# 直接验证途径:dumpsys
adb shell dumpsys activity services com.qsjnic.mcu.service
# 期望:bind 状态可见,clientCount=0
```

**完成后状态**

- Service 已可被 bindService
- 但还没有任何客户端

---

### C3. SDK 最小入口能调通

**目标**:`:sdk` 模块写最小 McuClient,能 bindService 并调通 getAidlVersion / isLinkAlive。

**做什么**

- `sdk/build.gradle.kts` 加 `implementation(project(":contract"))`
- `sdk/src/main/java/.../McuClient.java`:单例,`init(Context)` 内部 bindService,Connected 后保存 IMcuService
- 暴露 `getAidlVersion()` / `isLinkAlive()`(同步等 bind,简单实现即可)
- 在 `:sdk` 模块同时建一个 `:sdk-helloapp`(临时 instrumented test 或最小 demo App,放在 sdk/src/androidTest 或独立子模块都可)
  - 一个 Activity,`onResume` 调 SDK 两个方法,`Toast` 显示结果

**验证**

```bash
adb install -r ...service-debug.apk
adb install -r ...sdk-helloapp-debug.apk
adb shell am start -n com.qsjnic.helloapp/.MainActivity
# 期望:Toast 显示 "AIDL ver=1, link=true"
adb shell logcat -s McuClient:*
# 期望:看到 "Service connected, version=1"
```

**完成后状态**

- 跨进程链路通了,后续业务命令往这里加即可
- helloapp 完成使命可删除或保留作内部 smoke test

---

## Phase D:业务域对接

### D1. Vehicle 域 + vehicle-demo

**目标**:Vehicle 域全部命令上线,demos/vehicle-demo 能装到设备显示数据。

**做什么**

- `:contract` 加:
  - `IMcuVehicleListener.aidl`
  - Parcelable: `LineDetectInfo`、`VehicleSnapshot`、`McuVersion`
  - `IMcuService` 加 vehicle 相关方法(7 个 setter / getter)
- `:service` 实现:
  - 把 RxRouter 的 vehicle 帧路由到 `RemoteCallbackList<IMcuVehicleListener>` 分发
  - 实现各 setter:序列化为帧 → 入 Tx 队列
- `:sdk` 加 `McuVehicleClient`(domain Facade):
  - 包装 AIDL 调用
  - Listener 转 Java Listener
- 新建独立 AS 工程 `demos/vehicle-demo/`:
  - 一个 Activity,显示电压(实时刷新)、ACC/IG 状态、MCU Version
  - 内嵌 `qsmw-sdk.aar` 到 `app/libs/`

**验证**

```bash
# Service / SDK 已装好的前提下:
cd demos/vehicle-demo && ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.qsjnic.demo.vehicle/.MainActivity
# 期望:界面显示
#   "MCU Version: E112C069XZA0120015"
#   "Voltage: 12655 mV" (每 100ms 刷新)
#   "ACC: 1, IG: 0, ..."

# 命令验证:demo 里点"50% 背光"按钮
# 期望:logcat 看到 0x84 帧发出,实际车机背光变化
```

**完成后状态**

- 一个完整业务域端到端跑通
- 后两个域(Radio/Upgrade)按相同模式复制

---

### D2. Radio 域 + radio-demo

**做什么**:同 D1,域换成 Radio。新建 `demos/radio-demo/`。

**验证**:demo 切到 87.5MHz / 触发 SEEK,实际能听到收音机声音(或在没天线环境下至少看到 FREQ 回调返回正确值)。

---

### D3. Upgrade 域 + ota-demo(受 MCU bug 阻塞)

**做什么**

- `:contract` 加 `IMcuUpgradeListener.aidl`、`UpgradeRequest`、`UpgradeProgress`
- `:service` 实现升级状态机(沿用 `test_Serial/main_test.c` 的 `test_update_real()` 逻辑)
  - 进入 UPGRADING 状态,排他写
  - DATA loop 接收 IND_AQUIRE 驱动
- `:sdk` 加 `McuUpgradeClient`
- 新建 `demos/ota-demo/`:
  - 选择 bin 文件,显示进度条
  - 显示当前阶段:REQ → BEGIN → LEN+IND → DATA(0/N)→ ...

**验证**

由于已知 MCU bug,**期望验证仅至 IND_AQUIRE 阶段成功**:

```
[demo 界面]
  阶段:REQ ✓
  阶段:BEGIN ✓
  阶段:UPDATE_LEN=1024 ✓
  阶段:IND_AQUIRE idx=0 ✓
  阶段:DATA idx=0 [发送中...] ⏸  ← 预期卡在这里(MCU bug)
  错误:DATA 帧 MCU 未响应,这是已知 MCU 侧问题,详见 doc/ota_evidence/
```

**状态**:本步标 ⏸ 部分完成,等 MCU 厂商修复后再验证 DATA 之后流程。

---

## Phase E:验收测试

### E1. 多进程隔离测试

**做什么**

- 三个 demo 都装在设备上,同时运行
- 写 instrumented test 或手动跑步骤
- 填写 `doc/验收测试报告模板.md` 的 TC-IS-01/02/03

**验证**:三个用例 PASS,无数据串台,client 死亡自动清理。

---

### E2. 重连自愈测试

**做什么**:`kill -9` Service 后观察 SDK 自动重连。
**验证**:TC-RC-01/02 PASS,重连耗时 < 10s。

---

### E3. 延迟测试 + dumpsys

**做什么**

- Service 加 `dumpsys` 接口实现,输出最近 100 次命令的延迟(P50/P95/P99)
- demo 触发 100 次 setBacklight,采集数据
- 填 TC-LT-01/02

**验证**

```bash
adb shell dumpsys mcu_service
# 期望:输出延迟统计、Tx/Rx 计数、链路状态
```

P95 ≤ 50ms,电压回调丢失率 < 10%。

---

### E4. 权限测试

**做什么**

- 把 Service `MCU_ACCESS` 权限 `protectionLevel` 改为 `signature|privileged`
- 写一个**不签名 / 第三方签名**的测试 App 调用 SDK
- 写一个**平台签名**的 demo App 调用 SDK
- 填 TC-PM-01/02

**验证**:无权限 App 调用返回 `ERR_PERMISSION (-5)`,有权限 App 正常工作。

---

## Phase F:交付打包

### F1. 交付打包

**做什么**

- 写 `:release` Gradle 任务:把 `service-release.apk` / `sdk-release.aar` 改名拷贝到 `release/`
- 三个 demo 工程各自的 `app/libs/qsmw-sdk.aar` 同步最新版
- 把 `demos/` 整目录打 zip(`qsmw-demos-vYYYYMMDD.zip`)
- 文档定稿:`SystemService集成说明.md` 加版本号、变更说明
- 验收报告填写完毕,签字

**验证**:`doc/工程结构与交付清单.md` 第 4 节交付边界对照表,逐项打勾。

---

## 阶段间依赖图

```
A1 → A2 → B1 → B2 → B3
                     │
                     ▼
                    C1 → C2 → C3
                              │
                ┌─────────────┼─────────────┐
                ▼             ▼             ▼
               D1            D2            D3 ⏸(等 MCU 修)
                │             │             │
                └─────────────┼─────────────┘
                              ▼
                       E1 → E2 → E3 → E4
                              │
                              ▼
                              F1
```

D1/D2/D3 之间技术上**可并行**,但建议串行做(D1 最先,模式跑通后 D2/D3 复制更快)。
E 阶段必须等 D 阶段三个 demo 都到位后再开始。

---

## 开发期工作流(减少重启)

`/system/priv-app/` 的部署路径每次重启会重置 `/dev` 节点权限和 SELinux,这给 dev 迭代带来痛苦的"改一行代码 → 重启 2 分钟"循环。本节固化我们的减负措施:

### 一次性 boot 配置(已就位,设备本地)

`/vendor/etc/ueventd.rc`(末尾追加,生效于 ueventd 创建 /dev 节点时):

```
/dev/ttyS7           0666   root       root
```

`/vendor/etc/init/qsmw-dev.rc`(boot 完成后自动 setenforce + chcon):

```
service qsmw_dev_perms /system/bin/sh -c 'chcon u:object_r:tty_device:s0 /dev/ttyS7; setenforce 0'
    user root
    seclabel u:r:su:s0
    oneshot

on property:sys.boot_completed=1
    start qsmw_dev_perms
```

**这两份文件只放一次,之后所有 dev 重启都自动应用**,不再手动 chmod/chcon/setenforce。
量产前删除 `qsmw-dev.rc` 并把 ueventd.rc 改回 `0660 system system`(整车需求规范)。

### APK 迭代不重启

priv-app 一旦在位:

```bash
# 普通代码改动:用 adb install 覆盖到 /data/app(保留 priv-app 特权,但代码用新版)
./gradlew :service:assembleDebug
adb install -r service/build/outputs/apk/debug/service-debug.apk

# 触发 Service(避开 BG 启动限制)
adb shell am start -n com.qsjnic.mcu.service/.DevTriggerActivity
```

仅以下情况需要重启:

| 情景 | 必须重启? | 备注 |
|---|---|---|
| 改 Java 业务代码 | ❌ | `adb install -r` 即可 |
| 改 cpp / native | ❌ | 同上(libmcujni.so 已在 priv-app 解压目录,新 APK 内的 lib 也会被覆盖到 /data) |
| 改 AndroidManifest | ✅ | 加 component / permission / exported 等都需 PMS 重扫 |
| 改 contract AIDL | ❌ | 重新 build sdk/service,install -r 即可 |

---

## 文档同步约定

每完成一步,**对应文档同步更新**:

| 步骤 | 同步更新的文档 |
|---|---|
| A1-A2 | 本文档(状态)、`README.md`(项目概览) |
| B1-B3 | `SystemService架构与接口设计.md` 第 10 节(Native 复用细节) |
| C1-C3 | `SystemService架构与接口设计.md` 第 4 节(AIDL 实际签名) |
| D1-D3 | demos 各自的 README;`SystemService架构与接口设计.md` 第 5 节(SDK API) |
| E1-E4 | `验收测试报告模板.md`(填实际结果);出 PASS/FAIL 用例附件 |
| F1 | `工程结构与交付清单.md` 第 9 节(全部状态打勾) |

每步完成后我会:
1. 推动相应代码 / 文档变更
2. 更新本计划的状态列(⏳ → ✅)
3. 给出验证命令与预期输出,你在设备上确认 PASS 后再启动下一步

---

## 当前位置

**已完成的前置工作**(不在 16 步内,但已就位):

- ✅ `test_Serial/` C raw 测试工具已与 MCU 联调通过(Public 9 PASS,Radio 8 PASS)
- ✅ `doc/PRD.md`、`doc/QSJNIC_SDK架构与技术白皮书.md`(项目级文档)
- ✅ `doc/SystemService架构与接口设计.md`(技术设计)
- ✅ `doc/工程结构与交付清单.md`(工程规范)
- ✅ `doc/SystemService集成说明.md`(给甲方)
- ✅ `doc/验收测试报告模板.md`(验收用)
- ✅ `doc/ota_evidence/`(MCU OTA bug 证据包)

**下一步行动**:启动 **A1**。

---

## 风险与权变

| 风险 | 应对 |
|---|---|
| MCU OTA DATA 帧 bug 长期不修 | D3 / E 仅做协议握手前 4 步,不阻塞主流程交付;最终 OTA 完整验证转入维保期 |
| 平台签名密钥不齐 | E4 之前用 `signature` 级 + 测试密钥;甲方密钥就位后切换 |
| `/dev/ttyS7` SELinux 权限受限 | B1 开发期 setenforce 0;E 阶段补 SELinux 策略并验证 Enforcing 通过 |
| 多进程并发延迟超标 | E3 提前度量,若 P95 接近上限,优化 Tx 队列锁粒度 / 减少同步 transact |
| AIDL 接口后续变更 | 加版本号 `getAidlVersion()`,新增方法不破坏旧 SDK;主版本变更需文档说明 |
