# QSJNIC 完整项目编程实施计划

> 目的:把"架构方案 + 实施计划"落到**每一行代码、每一个文件、每一条验证命令**
> 起点:cpp 重构完成(commit 90449d4)
> 输出形态:可顺序执行的编码手册,每步含「文件 / 写什么 / 怎么验证」三段

---

## 0. 工程准备(0.5h)

### 0.1 分支与提交策略
```bash
git checkout -b feature/full-build
# 之后每个 Phase 单独 commit,Phase 内多个 sub-step 可压成一条
```

### 0.2 本地工具就绪
- Android Studio 已能编 `service:assembleRelease`
- `adb` 可达车机,`/data/local/tmp/` 可写
- 备好 `mcu_app.bin`(参考 `mcu_structured_test/mcu_app.bin`)和正确 project 号(默认 10040)

### 0.3 验证基线
```bash
./gradlew clean
./gradlew :service:assembleRelease :sdk:assembleRelease :contract:assembleRelease
# 三个 module 当前能编通 = 基线 OK
```

---

## Phase A — 联机验证 cpp 重构(0.5d)

**目标**:把 commit 90449d4 跑一次端到端,确认新协议栈在车机上不出问题。

### A-1. 编 + 装 Service
```bash
./gradlew :service:assembleRelease
adb root && adb remount
adb push service/build/outputs/apk/release/service-release.apk /system/priv-app/QsmwService/QsmwService.apk
adb shell sync
adb reboot
```

### A-2. 跑 selftest(Java 触发 native)
临时在 `QsMcuSystemService.onCreate` 末尾加一行:
```java
Log.i(TAG, "selftest mask = " + McuLink.get().runSelftests());
```
启动 Service,看 logcat:
```
McuSelftest: [PASS] proto.checksum.setup
... (12 条)
McuSelftest: Self-tests: total=12 failed=0 (mask=0x00000000)
McuLink:     selftest mask = 0
```
**Exit**:mask == 0。完成后**回滚**那行临时 Log。

### A-3. 跑一次 OTA(通过 native API)
临时在 `DevTriggerActivity` 加一个按钮调:
```java
new Thread(() -> {
    int rc = McuLink.get().runUpgrade("/data/local/tmp/mcu_app.bin", "10040");
    Log.i("DevTrigger", "runUpgrade rc=" + rc);
}).start();
```
push 一份 mcu_app.bin 到 `/data/local/tmp/`,点按钮,期望 logcat:
```
McuUpgrade: [UPGRADE] image=... size=215904 crc16=0x1677
McuUpgrade: [UPGRADE] packets=211 pack_len=1024
...
McuUpgrade: [PASS] upgrade.complete ack=SUCCESS
DevTrigger: runUpgrade rc=0
```
**Exit**:rc == 0,全程 ≤ 90s。

### A-4. 触发 5 个业务命令
依然在 DevTrigger 加按钮,各发一条:
- `McuLink.get().sendAndWaitAck(0x01, 0x81, null, 2500)` → REQ_INIT_INFO
- `McuLink.get().sendAndWaitAck(0x01, 0x82, new byte[]{0}, 2500)` → MUTE off
- `McuLink.get().sendAndWaitAck(0x01, 0x84, new byte[]{1,80,80}, 2500)` → backlight 80/80
- `McuLink.get().sendAndWaitAck(0x05, 0x83, new byte[]{0,0x22,0x2E}, 2500)` → SET_BAND FM 87.5
- `McuLink.get().sendAndWaitAck(0x05, 0x84, new byte[]{0x22,0x2E}, 2500)` → SET_FREQ 87.5

每条返回 seq ≥ 0(非 -1/-2)即成功。

### A-5. 产出验证报告
```
docs/phase_a_validation_20260516.md
  - MCU 固件版本(从 logcat MCU_VERSION 抓)
  - selftest 结果
  - OTA 时间 / 速率 / 重传数
  - 5 业务命令的 ack_seq
```

---

## Phase B — `:domain` 模块 + 删 Java OTA 死代码(3d)

### B-1. 新建 `:domain` 模块(0.5h)

**`settings.gradle.kts`** 加:
```kotlin
include(":domain")
```

**`domain/build.gradle.kts`**:
```kotlin
plugins { id("java-library") }
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
    testImplementation("junit:junit:4.13.2")
}
```

**`domain/src/main/java/com/qsjnic/mcu/domain/package-info.java`** —— 占位

**验证**:`./gradlew :domain:build` 通过(空模块也算)。

### B-2. 命令枚举常量(0.5h)
**`domain/src/main/java/com/qsjnic/mcu/domain/MsgType.java`**
```java
public final class MsgType {
    public static final int PUBLIC = 0x01;
    public static final int RADIO  = 0x05;
    public static final int UPDATE = 0x07;
    private MsgType() {}
}
```

**`Cmds.java`** —— 每个 module 一个常量类:
```java
public final class Cmds {
    public static final class Public {
        public static final int REQ_INIT          = 0x81;
        public static final int REQ_MUTE          = 0x82;
        public static final int TIME_INFO         = 0x83;
        public static final int TFT_DUTY          = 0x84;
        public static final int REQ_SLEEP         = 0x85;
        public static final int REQ_TYPE          = 0x86;
        // ... (照搬 contract/protocol.h 里 PUB_A2M_* 与现在 QsMcuSystemService 用到的全部)
    }
    public static final class Radio  { /* ... */ }
    public static final class Update { /* ... */ }
}
```

### B-3. 命令编码器 `Commands.Public`(0.5d)
**`domain/src/main/java/com/qsjnic/mcu/domain/Commands.java`**

对照现在 `QsMcuSystemService` 60 处 byte[] 拼装,逐一搬过来。每个方法 ≤ 10 行,纯函数。例:
```java
public final class Commands {
    public static final class Public {
        public static byte[] reqInit() { return EMPTY; }

        public static byte[] reqMute(boolean on) {
            return new byte[]{ (byte)(on ? 1 : 0) };
        }

        public static byte[] tftDuty(int d1, int d2) {
            require(d1 >= 0 && d1 <= 100, "duty1 out of range");
            require(d2 >= 0 && d2 <= 100, "duty2 out of range");
            return new byte[]{ 0x01, (byte) d1, (byte) d2 };
        }

        public static byte[] timeInfo(int year, int mo, int d, int h, int m, int s) {
            require(year >= 2000 && year <= 2099, "year out of range");
            require(mo >= 1 && mo <= 12, "month out of range");
            // ...
            return new byte[]{ 0x01, (byte)(year - 2000), (byte) mo, (byte) d,
                               (byte) h, (byte) m, (byte) s };
        }

        // ... 写满 ~30 个 Public A2M 方法
    }
    // ... Radio / Update 同样
    private static final byte[] EMPTY = new byte[0];
    private static void require(boolean cond, String msg) {
        if (!cond) throw new IllegalArgumentException(msg);
    }
}
```

**逐项对照 IMcuService.aidl 里的方法**,把 byte[] 编码补全。

### B-4. 命令编码单元测试(0.5d)
**`domain/src/test/java/com/qsjnic/mcu/domain/CommandsTest.java`**

每个 Commands 方法至少 1 个 happy path + 1 个边界:
```java
public class CommandsTest {
    @Test public void reqMute_off()  { assertArrayEquals(new byte[]{0}, Commands.Public.reqMute(false)); }
    @Test public void reqMute_on()   { assertArrayEquals(new byte[]{1}, Commands.Public.reqMute(true));  }

    @Test public void tftDuty_min()  { assertArrayEquals(new byte[]{1,0,0},     Commands.Public.tftDuty(0, 0));   }
    @Test public void tftDuty_max()  { assertArrayEquals(new byte[]{1,100,100}, Commands.Public.tftDuty(100,100));}
    @Test(expected = IllegalArgumentException.class)
    public void tftDuty_negative()   { Commands.Public.tftDuty(-1, 0); }
    @Test(expected = IllegalArgumentException.class)
    public void tftDuty_over100()    { Commands.Public.tftDuty(0, 101); }

    @Test public void timeInfo_basic() {
        assertArrayEquals(new byte[]{1, 26, 5, 16, 14, 30, 0},
                          Commands.Public.timeInfo(2026, 5, 16, 14, 30, 0));
    }
    // ... 目标 ≥ 80 个测试
}
```

**`./gradlew :domain:test`** 全 PASS。

### B-5. 帧解码器 `Decoders`(0.5d)
**`domain/src/main/java/com/qsjnic/mcu/domain/Decoders.java`**

把 `FrameRouter.handlePublic/handleRadio` 里的解码逻辑搬过来,每个解码方法返回 plain object:
```java
public final class Decoders {
    public static int parseVoltage(byte[] body) {
        if (body == null || body.length < 2) return -1;
        return ((body[0] & 0xFF) << 8) | (body[1] & 0xFF);
    }

    public static LineDetectInfo parseLineDetect(byte[] body) {
        if (body == null || body.length < 1) return new LineDetectInfo();
        int b = body[0] & 0xFF;
        LineDetectInfo o = new LineDetectInfo();
        o.ig     = (b      ) & 1;
        o.acc    = (b >> 1) & 1;
        // ...
        return o;
    }

    public static RadioFreq parseRadioFreq(byte[] body) {
        if (body == null || body.length < 7) return null;
        RadioFreq f = new RadioFreq();
        f.freq   = ((body[0] & 0xFF) << 8) | (body[1] & 0xFF);
        f.band   = body[2] & 0x0F;
        f.signal = body[6] & 0xFF;
        return f;
    }
    // ...
}
```

`LineDetectInfo` 等 plain DTO 也放在 `:domain`(与 `contract` 里的 Parcelable 同名但纯 POJO)。

测试同样在 `DecodersTest.java`:对每条已知的 MCU 推送字节,断言解出来的字段。

### B-6. `:contract` 依赖 `:domain`(0.2h)
**`contract/build.gradle.kts`** 加:
```kotlin
dependencies {
    api(project(":domain"))   // 用 api,因为 Parcelable DTO 可能引 domain POJO
    implementation(libs.androidx.annotation)
}
```

### B-7. `QsMcuSystemService` 切到 Commands(0.5d)
逐一替换 60 个 inline byte[]。例:
```java
// before
@Override public int setBacklight(int duty1, int duty2) {
    if (duty1<0||duty1>100||duty2<0||duty2>100) return ERR_INVALID_ARG;
    if (!McuLink.get().isLinkAlive()) return ERR_LINK_DOWN;
    byte[] payload = new byte[]{0x01, (byte)duty1, (byte)duty2};
    return McuLink.get().sendNormal(0x01, 0x84, payload) ? OK : ERR_LINK_DOWN;
}

// after
@Override public int setBacklight(int duty1, int duty2) {
    try {
        byte[] payload = Commands.Public.tftDuty(duty1, duty2);
        if (!McuLink.get().isLinkAlive()) return ERR_LINK_DOWN;
        return McuLink.get().sendNormal(MsgType.PUBLIC, Cmds.Public.TFT_DUTY, payload)
                ? OK : ERR_LINK_DOWN;
    } catch (IllegalArgumentException e) {
        return ERR_INVALID_ARG;
    }
}
```

**验证**:
- `./gradlew :service:assembleRelease` 通过
- 上车跑 A-4 的 5 条业务命令仍工作

### B-8. 切 native OTA(0.5d)
**`UpgradeManager.java`**

砍掉 OTA 状态机相关代码(`doStart` / `handleUpdateLen` / `handleIndAcquire` / `sendData` / `sendBegin` / `sendEnd` / `binBuf` / `packetSize` 等),改为:
```java
public int startUpgrade(@Nullable UpgradeRequest req) {
    if (req == null || req.binFilePath == null) return ERR_INVALID_ARG;
    synchronized (stateLock) {
        if (running) return ERR_BUSY;
        running = true;
        abortRequested = false;
        resetProgress();
    }
    McuLink.get().setUpgradeListener(this::onNativeProgress);
    worker.post(() -> {
        int rc = McuLink.get().runUpgrade(req.binFilePath, req.projectId != null ? req.projectId : "10040");
        synchronized (stateLock) {
            running = false;
            progress.state = (rc == 0) ? UpgradeProgress.STATE_DONE : UpgradeProgress.STATE_FAIL;
            progress.errCode = rc;
        }
        broadcastProgress();
    });
    return ERR_OK;
}

private void onNativeProgress(int phase, int chunkIndex, int totalChunks, int packLen,
                              long bytesDone, long bytesTotal, int retriesThisChunk,
                              int totalRetries, int ackSeq, long elapsedMs, long rateBps,
                              int ackCode) {
    synchronized (stateLock) {
        progress.phase = phase;
        progress.currentPacket = chunkIndex + 1;
        progress.totalPackets = totalChunks;
        progress.packetSize = packLen;
        progress.bytesDone = bytesDone;
        progress.bytesTotal = bytesTotal;
        progress.totalRetries = totalRetries;
        progress.elapsedMs = elapsedMs;
        progress.rateBps = rateBps;
    }
    broadcastProgress();
}
```

`onUpdateFrame` 可以保留作为 debug log,但不再驱动状态机。

**验证**:上车触发 `startUpgrade`,与 A-3 行为一致。

### B-9. 删除 Java OTA 死代码(0.2d)
确认 B-8 跑通后,把 `UpgradeManager.java` 里:
- `UPD_M2A_REQ/BEGIN/DATA/END` 等常量
- `doStart` / `handleUpdateLen` / `handleIndAcquire` / `sendReq` / `sendBegin` / `sendData` / `sendEnd`
- `binBuf` / `packetSize` / `totalPackets` / `projectIdOffset`

全删,只保留:`startUpgrade` / `stopUpgrade` / `getProgress` / `register/unregister` / `onNativeProgress` / `broadcastProgress`。

目标:`UpgradeManager.java` 行数 556 → ≤ 150。

### B-10. Phase B 提交
```bash
git add domain/ contract/build.gradle.kts service/ settings.gradle.kts
git commit -m "feat(domain): 抽取命令编码器到 :domain,切 native OTA,删 Java OTA 死代码"
```

---

## Phase C — AIDL evolution(2d)

### C-1. 新建 `IMcuFrameListener.aidl`(0.3d)
**`contract/src/main/aidl/com/qsjnic/mcu/contract/IMcuFrameListener.aidl`**
```aidl
package com.qsjnic.mcu.contract;

import com.qsjnic.mcu.contract.VehicleSnapshot;
import com.qsjnic.mcu.contract.RadioState;
import com.qsjnic.mcu.contract.UpgradeProgress;

interface IMcuFrameListener {
    /** Raw frame from MCU. body 已剥 (msgType,cmd) 前 2 字节。 */
    oneway void onMcuFrame(int frameType, int msgType, int cmd, in byte[] body);

    /** 解码后的高层状态变化(decoded)。 */
    oneway void onVehicleStateChanged(in VehicleSnapshot snapshot);
    oneway void onRadioStateChanged(in RadioState state);
    oneway void onUpgradeProgress(in UpgradeProgress progress);
}
```

### C-2. `IMcuService.aidl` 扩展(0.3d)
在文件末尾加(不动现有方法,保持二进制兼容):
```aidl
    // ===== AIDL v2: generic command channel =====

    /** Send a command and wait for matching ACK. Returns ackSeq (>=0) or
     *  negative McuErrorCode on error. timeoutMs > 0. */
    int sendCommand(int msgType, int cmd, in byte[] body, int timeoutMs);

    /** Fire-and-forget command (no wait). */
    oneway void postCommand(int msgType, int cmd, in byte[] body);

    /** Unified frame listener — replaces 3 typed listeners eventually. */
    void registerFrameListener(in IMcuFrameListener cb);
    void unregisterFrameListener(in IMcuFrameListener cb);
```

`McuServiceManager.AIDL_VERSION = 2;`

### C-3. Service 实现新方法(0.5d)
**`QsMcuSystemService.binder`**:
```java
@Override
public int sendCommand(int msgType, int cmd, byte[] body, int timeoutMs) {
    if (!McuLink.get().isLinkAlive()) return ERR_LINK_DOWN;
    if (body != null && body.length > 2040) return ERR_INVALID_ARG;
    int seq = McuLink.get().sendAndWaitAck(msgType, cmd, body, timeoutMs);
    return seq >= 0 ? OK : (seq == -1 ? ERR_TIMEOUT : ERR_LINK_DOWN);
}

@Override
public void postCommand(int msgType, int cmd, byte[] body) throws RemoteException {
    if (McuLink.get().isLinkAlive()) {
        McuLink.get().sendBusinessFrame(msgType, cmd, body);
    }
}

@Override
public void registerFrameListener(IMcuFrameListener cb) throws RemoteException {
    frameListeners.register(cb);
}

@Override
public void unregisterFrameListener(IMcuFrameListener cb) throws RemoteException {
    frameListeners.unregister(cb);
}
```

**`McuErrorCode.java`** 加 `ERR_TIMEOUT = -7;`(如未定义)。

### C-4. FrameRouter / Managers 广播 frame listener(0.3d)
`FrameRouter.onMcuFrame` 里加一行:
```java
QsMcuSystemService.broadcastRawFrame(frameType, msgType, cmd, payload);
```
Service 内加静态方法:
```java
public static void broadcastRawFrame(int ft, int mt, int cmd, byte[] body) {
    int n = sFrameListeners.beginBroadcast();
    for (int i = 0; i < n; i++) {
        try { sFrameListeners.getBroadcastItem(i).onMcuFrame(ft, mt, cmd, body); }
        catch (RemoteException ignored) {}
    }
    sFrameListeners.finishBroadcast();
}
```

Vehicle/Radio/Upgrade Manager 在 broadcast state 时也走 `IMcuFrameListener` 的 typed cb。

### C-5. 标 @Deprecated(0.2d)
`IMcuService.aidl` 老 50 个 typed 方法上方加注释:
```aidl
    /** @deprecated since v2, use sendCommand(MsgType.PUBLIC, Cmds.Public.REQ_MUTE, Commands.Public.reqMute(on), 2500). */
    int setMute(boolean on);
```
(AIDL 不支持 `@Deprecated` 注解,只能用注释。SDK Facade 用 Java `@Deprecated`。)

### C-6. SDK 顶层加 sendCommand/post(0.3d)
**`McuClient.java`**:
```java
public int sendCommand(int msgType, int cmd, byte[] body, int timeoutMs) {
    IMcuService s = getServiceUnchecked();
    if (s == null) return McuErrorCode.ERR_SERVICE;
    try { return s.sendCommand(msgType, cmd, body, timeoutMs); }
    catch (RemoteException e) { return McuErrorCode.ERR_LINK_DOWN; }
}

public void postCommand(int msgType, int cmd, byte[] body) {
    IMcuService s = getServiceUnchecked();
    if (s == null) return;
    try { s.postCommand(msgType, cmd, body); } catch (RemoteException ignored) {}
}

public void addFrameListener(IMcuFrameListener cb) { ... }
```

### C-7. Phase C 提交
```bash
git add contract/ service/ sdk/
git commit -m "feat(contract): AIDL v2 — sendCommand/postCommand + IMcuFrameListener"
```

---

## Phase D — Service 内部重构:DomainBus + Features(3d)

### D-1. 新建 `service/feature/` 包(0.2d)
```
service/src/main/java/com/qsjnic/mcu/service/feature/
├── Feature.java                  (interface)
├── DomainBus.java
├── VehicleFeature.java
├── RadioFeature.java
└── UpgradeFeature.java
```

**`Feature.java`**:
```java
public interface Feature {
    /** True if this feature wants the frame. */
    boolean accepts(int msgType);
    /** Process a decoded frame. Must be cheap; broadcast async. */
    void onFrame(int frameType, int msgType, int cmd, byte[] body);
}
```

### D-2. `DomainBus`(0.3d)
```java
public final class DomainBus {
    private final List<Feature> features = new ArrayList<>();
    public void register(Feature f) { features.add(f); }
    public void onFrame(int ft, int mt, int cmd, byte[] body) {
        for (Feature f : features) {
            if (f.accepts(mt)) f.onFrame(ft, mt, cmd, body);
        }
    }
}
```

### D-3. `VehicleFeature`(0.8d)
搬 `VehicleManager` + `FrameRouter.handlePublic` 解码,改成:
```java
public final class VehicleFeature implements Feature {
    private final VehicleSnapshot snapshot = new VehicleSnapshot();
    private final RemoteCallbackList<IMcuVehicleListener> typedListeners = new RemoteCallbackList<>();
    private final RemoteCallbackList<IMcuFrameListener> frameListeners;  // 共用

    public VehicleFeature(RemoteCallbackList<IMcuFrameListener> shared) {
        this.frameListeners = shared;
    }

    @Override public boolean accepts(int msgType) { return msgType == MsgType.PUBLIC; }

    @Override public void onFrame(int ft, int mt, int cmd, byte[] body) {
        switch (cmd) {
            case Cmds.Public.M2A_LINE_DETECT:
                snapshot.lineDetect = Decoders.parseLineDetect(body);
                broadcast();
                break;
            case Cmds.Public.M2A_VOLTAGE:
                snapshot.voltageMv = Decoders.parseVoltage(body);
                broadcast();
                break;
            // ... 等等
        }
    }

    private void broadcast() {
        // 1) typed listener
        int n = typedListeners.beginBroadcast();
        for (int i = 0; i < n; i++) {
            try { typedListeners.getBroadcastItem(i).onVoltage(snapshot.voltageMv); }
            catch (RemoteException ignored) {}
        }
        typedListeners.finishBroadcast();

        // 2) unified state cb
        int m = frameListeners.beginBroadcast();
        for (int i = 0; i < m; i++) {
            try { frameListeners.getBroadcastItem(i).onVehicleStateChanged(snapshot); }
            catch (RemoteException ignored) {}
        }
        frameListeners.finishBroadcast();
    }

    public VehicleSnapshot snapshot() { return cloneOf(snapshot); }
    public void register(IMcuVehicleListener cb)   { typedListeners.register(cb); }
    public void unregister(IMcuVehicleListener cb) { typedListeners.unregister(cb); }
}
```

### D-4. `RadioFeature`(0.5d)
同上,处理 `MsgType.RADIO` 的 5 种 M→A 推送。

### D-5. `UpgradeFeature`(0.4d)
本质就是 B-8 之后的 `UpgradeManager`,改名为 Feature,持 RemoteCallbackList。

### D-6. `QsMcuSystemService` 接入(0.3d)
```java
private DomainBus bus;
private VehicleFeature vehicle;
private RadioFeature radio;
private UpgradeFeature upgrade;
private final RemoteCallbackList<IMcuFrameListener> frameListeners = new RemoteCallbackList<>();

@Override public void onCreate() {
    super.onCreate();
    // ... open + start link as before ...
    vehicle = new VehicleFeature(frameListeners);
    radio   = new RadioFeature(frameListeners);
    upgrade = new UpgradeFeature(frameListeners);
    bus = new DomainBus();
    bus.register(vehicle);
    bus.register(radio);
    bus.register(upgrade);
}
```

`FrameRouter.onMcuFrame` 改为只调 `bus.onFrame(...)`,删原来 3 个 Manager 的入口。

### D-7. 删除 `FrameRouter` / `*Manager`(0.3d)
确认 D-3..D-6 跑通后,删:
- `service/.../FrameRouter.java`
- `service/.../VehicleManager.java`
- `service/.../RadioManager.java`
- `service/.../UpgradeManager.java`

binder 实现里所有 `VehicleManager.getInstance()` 改为 `vehicle.snapshot()` 等。

### D-8. Feature 单元测试(0.5d)
**`service/src/test/java/com/qsjnic/mcu/service/feature/VehicleFeatureTest.java`**

mock `IMcuVehicleListener.Stub`,注入 byte[] frame,断言 snapshot:
```java
@Test public void voltageFrame_updatesSnapshot() {
    VehicleFeature f = new VehicleFeature(new RemoteCallbackList<>());
    f.onFrame(0x06, MsgType.PUBLIC, Cmds.Public.M2A_VOLTAGE, new byte[]{0x30, 0xD4});
    assertEquals(0x30D4, f.snapshot().voltageMv);
}
```

### D-9. 整机回归(0.3d)
跑 A-3 / A-4 / `vehicle-demo` + `radio-demo` smoketest 全过。

### D-10. Phase D 提交
```bash
git commit -m "refactor(service): DomainBus + Vehicle/Radio/Upgrade Features 替代 Managers"
```

---

## Phase E — SDK 重构 + Demos 打磨(3d)

### E-1. SDK Facade 内部切 sendCommand(0.8d)
**`McuVehicleClient.java`** 每个方法改为:
```java
public int setMute(boolean on) {
    return parent.sendCommand(MsgType.PUBLIC, Cmds.Public.REQ_MUTE,
                              Commands.Public.reqMute(on), 2500);
}
public int setBacklight(int d1, int d2) {
    try {
        return parent.sendCommand(MsgType.PUBLIC, Cmds.Public.TFT_DUTY,
                                  Commands.Public.tftDuty(d1, d2), 2500);
    } catch (IllegalArgumentException e) {
        return McuErrorCode.ERR_INVALID_ARG;
    }
}
```
SDK 直接依赖 `:domain`(`sdk/build.gradle.kts` 加 `implementation(project(":domain"))`)。

### E-2. SDK 注册统一 frame listener,内部分发(0.5d)
**`McuClient.java`** 提供:
```java
public void addVehicleListener(VehicleListener l) { ... }
public void addRadioListener(RadioListener l)     { ... }
public void addUpgradeListener(UpgradeListener l) { ... }
```
内部维护 3 个 `CopyOnWriteArrayList`,通过单个 `IMcuFrameListener` 接收所有 frame,按 msgType demux。

旧 `addListener` 走 typed AIDL listener,保留兼容。

### E-3. 重连重注册(0.3d)
`McuClient.onServiceConnected` 内,把 3 个 list 里的所有 listener 重新 register:
```java
private void onConnected() {
    try {
        if (anyVehicleListener) service.registerFrameListener(frameListenerStub);
        ...
    } catch (RemoteException e) { /* schedule retry */ }
}
```

### E-4. vehicle-demo UI(0.5d)
**`demos/vehicle-demo/app/src/main/java/com/qsjnic/demo/vehicle/MainActivity.java`**

实时显示:
- IG/ACC/Back/Aux/Brake/Turn 各位(LineDetect bitmap)
- 电压(mV → V,1 位小数)
- MCU 版本字符串
- 当前 TimeInfo

按钮:`setMute on/off` / `setBacklight 30/60/90` / `setTftOnOff` / `requestInitInfo`。

Layout 用 LinearLayout 即可,无需 XML 资源(参考现有 demo)。

### E-5. radio-demo UI(0.5d)
现有 demo 已经覆盖基本场景,补:
- AM/FM 切换 + 当前 band 显示
- 频率手动输入 + Tune
- Seek 前进/后退 + 停止
- PS / PTY / RT 持续刷新区
- 信号强度条

### E-6. ota-demo UI(0.5d)
- 文件选择按钮(SAF: `ACTION_OPEN_DOCUMENT`,把 URI 复制到 `cacheDir` 拿到 path)
- 进度条 + 百分比 + chunk/total + 速率 + ETA
- Phase 名称展示("REQ → BEGIN → LEN → DATA → END → SUCCESS")
- Start / Stop 按钮
- 详细 phase 日志区(滚动 TextView)

### E-7. Demos "Service 未连接" 提示(0.2d)
3 demo 统一在 `onResume` 时:
```java
if (!McuClient.getInstance().isReady()) {
    showToast("Service 未连接,5s 后重试");
    ui.postDelayed(this::onResume, 5000);
}
```

### E-8. Demos 文案统一(0.2d)
全中文 or 全英文一致(目前混杂),用 `strings.xml` 替代 inline 文本。

### E-9. Phase E 提交
```bash
git commit -m "refactor(sdk): facades 走 sendCommand,3 demos UI 完整化"
```

---

## Phase F — Release 工程化(2d)

### F-1. 生产 keystore(0.2d)
```bash
keytool -genkey -v -keystore keystore/qsjnic_release.jks \
    -alias qsjnic_release -keyalg RSA -keysize 2048 -validity 10000 \
    -storepass <STRONG_PASS> -keypass <STRONG_PASS>
```
密码进 `keystore/release.properties`(已在 `.gitignore`):
```
RELEASE_STORE_FILE=../keystore/qsjnic_release.jks
RELEASE_STORE_PASSWORD=...
RELEASE_KEY_ALIAS=qsjnic_release
RELEASE_KEY_PASSWORD=...
```

### F-2. `service/build.gradle.kts` 区分 debug/release 签名(0.2d)
```kotlin
val releaseProps = Properties().apply {
    rootProject.file("keystore/release.properties").takeIf { it.exists() }
        ?.inputStream()?.use { load(it) }
}

signingConfigs {
    create("sharedDebug") { /* 现有 */ }
    if (releaseProps.isNotEmpty()) {
        create("releaseSign") {
            storeFile = rootProject.file(releaseProps["RELEASE_STORE_FILE"] as String)
            storePassword = releaseProps["RELEASE_STORE_PASSWORD"] as String
            keyAlias = releaseProps["RELEASE_KEY_ALIAS"] as String
            keyPassword = releaseProps["RELEASE_KEY_PASSWORD"] as String
        }
    }
}

buildTypes {
    release {
        signingConfig = signingConfigs.findByName("releaseSign")
            ?: signingConfigs.getByName("sharedDebug")
        ...
    }
}
```

### F-3. AndroidManifest 生产权限(0.2d)
**`service/src/main/AndroidManifest.xml`**:
```xml
<permission
    android:name="cn.qsjnic.permission.MCU_ACCESS"
    android:protectionLevel="signature|privileged" />

<service
    android:name=".QsMcuSystemService"
    android:exported="true"
    android:permission="cn.qsjnic.permission.MCU_ACCESS"
    ...>
    ...
</service>
```
注释保留 debug 版的 `normal` 备用片段。

### F-4. composite-build 引入 demos(0.3d)
**`settings.gradle.kts`** 加:
```kotlin
includeBuild("demos/vehicle-demo")
includeBuild("demos/radio-demo")
includeBuild("demos/ota-demo")
```

每个 demo 的 `settings.gradle.kts` 加:
```kotlin
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
```

确认 `./gradlew :vehicle-demo:app:assembleDebug` 在主仓库 root 跑得通。

### F-5. CI 脚本(0.5d)
**`scripts/release_check.sh`**:
```bash
#!/bin/bash
set -e
./gradlew clean packRelease
ls -la release/
adb shell rm -rf /system/priv-app/QsmwService/
adb push release/qsmw-service.apk /system/priv-app/QsmwService/QsmwService.apk
adb reboot && sleep 30
adb logcat -d -s McuSelftest | grep "failed=0" || (echo "SELFTEST FAILED"; exit 1)
echo "Release check PASS"
```

### F-6. `packRelease` 产物校对(0.3d)
确认 `release/` 下:
- `qsmw-service.apk`(arm64-v8a only,~3-5MB)
- `qsmw-sdk.aar`、`qsmw-contract.aar`
- `qsmw-demos.zip`(含 3 demo + 各自 libs/*.aar)
- `RELEASE_INFO.txt`(version、sha256、build time)

### F-7. `release-template/` 准备(0.3d)
**`release-template/README.md`**(随 zip 一起):
```
QSJNIC SystemService 交付包 v1.0.0
=================================

1. 把 qsmw-service.apk 预置到车机 /system/priv-app/QsmwService/
2. 三方 App 在 app/libs/ 放 qsmw-sdk.aar + qsmw-contract.aar
3. 集成步骤见 INTEGRATION_GUIDE.md
4. API 列表见 API_REFERENCE.md
5. 参考 qsmw-demos.zip 里的三个 demo 工程
```

### F-8. Phase F 提交
```bash
git commit -m "chore(release): 生产签名 + signature|privileged 权限 + composite-build demos + CI 脚本"
```

---

## Phase G — 文档 + 客户验收(2d)

### G-1. `release-template/INTEGRATION_GUIDE.md`(0.5d)
内容:
- 系统要求(Android 8.0+,arm64-v8a)
- 权限授予(`signature|privileged` / 或 dev 用 `normal`)
- App `build.gradle` 依赖配置
- `AndroidManifest.xml` 模板(`<uses-permission android:name="cn.qsjnic.permission.MCU_ACCESS"/>`)
- `McuClient.init(context)` 调用时机
- 监听器注册套路
- 混淆规则(`proguard-rules.pro` 片段)
- 常见错误码

### G-2. `release-template/API_REFERENCE.md`(0.5d)
列出:
- `McuClient`:`init / release / isReady / addReadyListener / vehicle / radio / upgrade / sendCommand / postCommand / addFrameListener`
- `VehicleFacade`:30+ 方法签名 + 返回码
- `RadioFacade`:10+ 方法签名
- `UpgradeFacade`:`startUpgrade / stopUpgrade / addListener`
- 所有 DTO(VehicleSnapshot / RadioState / UpgradeProgress / McuVersion 字段)
- `McuErrorCode` 完整枚举

### G-3. `CHANGELOG.md`(0.2d)
```
# v1.0.0 — 2026-05-xx
- cpp 协议栈基于 mcu_structured_test.c 重写,OTA 经实测验证(commit 90449d4)
- AIDL v2:新增 sendCommand/postCommand 通用通道
- 新增 :domain 模块,命令编码集中
- Service 重构:DomainBus + Vehicle/Radio/Upgrade Features
- 3 个 demo 完整化:UI + 文案 + Service 未连接重试
- 生产签名 / signature|privileged 权限隔离
- composite-build 把 demos 纳入主仓库 CI
```

### G-4. 客户验收脚本(0.5d)
**`scripts/customer_acceptance.sh`**:
```bash
#!/bin/bash
set -e
echo "=== Step 1: 安装 Service ==="
adb root && adb remount
adb push release/qsmw-service.apk /system/priv-app/QsmwService/QsmwService.apk
adb reboot && sleep 40

echo "=== Step 2: 检查 selftest ==="
adb logcat -d -s McuSelftest | grep "Self-tests: total=12 failed=0" || { echo "FAIL"; exit 1; }

echo "=== Step 3: 装 vehicle-demo 并验证 push ==="
adb install -r demos/vehicle-demo/app/build/outputs/apk/release/app-release.apk
adb shell am start -n com.qsjnic.demo.vehicle/.MainActivity
sleep 10
adb logcat -d -s VehicleDemo | grep "onVoltage" || { echo "no voltage push"; exit 1; }

echo "=== Step 4: 装 radio-demo 并验证 tune ==="
# ...

echo "=== Step 5: 装 ota-demo 并跑一次 OTA ==="
adb push files/mcu_app.bin /sdcard/Download/mcu_app.bin
adb install -r demos/ota-demo/app/build/outputs/apk/release/app-release.apk
adb shell am start -n com.qsjnic.demo.ota/.MainActivity
# 等候 70s
sleep 70
adb logcat -d -s OtaDemo | grep "STATE_DONE" || { echo "OTA failed"; exit 1; }

echo "ALL ACCEPTANCE CHECKS PASSED"
```

### G-5. Phase G 提交 + Tag
```bash
git commit -m "docs: integration guide + API reference + customer acceptance script"
git tag v1.0.0
git push --tags
```

---

## 最终验收清单

实施完 Phase A–G 后,以下应全部 ✅:

| # | 验收项 | 验证方式 |
|---|---|---|
| 1 | cpp self-test 12/12 PASS | logcat 抓 `Self-tests: total=12 failed=0` |
| 2 | OTA 端到端成功(211 chunk) | 时间 ≤ 90s,无 [FAIL],ack=SUCCESS |
| 3 | `:domain` 单测 ≥ 80 个全 PASS | `./gradlew :domain:test` |
| 4 | Feature 单测 ≥ 15 个全 PASS | `./gradlew :service:test` |
| 5 | `QsMcuSystemService` ≤ 200 行 | `wc -l` |
| 6 | `UpgradeManager` ≤ 150 行 | `wc -l` |
| 7 | AIDL v2 `sendCommand` 可用,旧 typed 方法仍工作 | 老 SDK 连新 Service 烟测 |
| 8 | 3 demo 自动 smoke 通过(无人工) | `customer_acceptance.sh` |
| 9 | release apk 用生产 keystore 签名 | `apksigner verify --print-certs` |
| 10 | `./gradlew packRelease` 在干净仓库一次成功 | clean clone + build |
| 11 | 主仓库 `./gradlew build` 含 demos | composite-build 触发 |
| 12 | 客户拿到 zip 30 分钟内能跑通 | 找无背景知识工程师试一次 |

---

## 时间总表

| Phase | 工作量 | 累计 | 里程碑 |
|---|---|---|---|
| A | 0.5d | 0.5d | cpp 联机 OK |
| B | 3.0d | 3.5d | :domain + native OTA |
| C | 2.0d | 5.5d | **M1**: AIDL v2 |
| D | 3.0d | 8.5d | **M2**: Service 重构完 |
| E | 3.0d | 11.5d | SDK + demos |
| F | 2.0d | 13.5d | release 工程化 |
| G | 2.0d | 15.5d | **M3**: 客户可交付 |

---

## 风险与回退

| 风险 | 触发条件 | 回退动作 |
|---|---|---|
| Phase B 切 native OTA 失败 | 实机 OTA 出错 | 保留 `McuLink.runUpgrade` 调用,Java OTA 暂回滚 |
| Phase D Feature 单元测试无法 mock RemoteCallbackList | Android 框架限制 | 抽小接口,Feature 持 `Broadcaster` 接口,测试用 fake |
| Phase F 客户车机不支持 signature\|privileged | 装 APK 不生效 | 文档保留 normal 备选,提供两种 manifest |
| composite-build 导致主仓库 build 慢 | gradle build > 5min | demos 不进默认 task,只在 packRelease 时拉 |

---

## 立即开始

```bash
# 第一步,今天
git checkout -b feature/full-build
# Phase A:编 + 装 + 跑 selftest + OTA + 5 业务命令
./gradlew :service:assembleRelease
# ... 按 A-1 ~ A-5 走
# 产出 docs/phase_a_validation_20260516.md
git add docs/phase_a_validation_20260516.md
git commit -m "chore: phase A — cpp refactor live validation report"
```

完成后立刻进 Phase B,新建 `:domain` 模块。
