Files
MAG160C/docs/android_app/verification_guide.md

424 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 核查指南 —— Phase A→F 执行轮(交给核查 AI 的入口)
> **核查对象**commit `06c1f30..8e1312a`7 个提交,基线 `087e15c`),
> 位于 `C:\Project\MAG160C`,源码在 `android\`Gradle,包 `com.mag160c.thermal`)。
>
> **第一原则:不要相信执行模型的总结与文档叙述。** 所有结论必须来自
> ① git 提交内容 ② 仓库内的官方逆向产物(可独立复核)③ 亲自重跑的脚本/构建
> ④ 硬件实测。执行模型(下称"执行者")的文档只当"待验证的声明"处理。
>
> 本文件给出一条可直接粘贴的起始指令、阅读顺序、三道必过关口、
> 逐阶段核查方法、**执行者主动交底的高风险项**,以及本机无法验证的边界。
---
## 0. 可直接粘贴给核查 AI 的起始指令
```
你是本项目的独立核查者。项目根 C:\Project\MAG160Cgit 仓库),
核查范围是提交 06c1f30..8e1312a7 个提交,基线 087e15c),
任务是把 docs/android_app/execution_plan.md 中 Phase A→F 的每项声明
独立复核一遍并对每条给出 证实/证伪/无法判定 的结论。
铁律:
1. 不要采信执行者写的总结、session_state、checklist 的任何叙述,
它们只是待验证声明;结论必须来自代码、官方逆向产物、可复现命令。
2. 先读 docs/android_app/verification_guide.md(核查指南),按它的
三道关口顺序执行;第一道不通过就停止并报告。
3. 不要修改任何源码或提交;你是只读核查者。发现问题只报告,不修复。
4. 无法在本机验证的(需真机/双设备)必须显式标注"无法判定",不要默认通过。
5. 执行者的构建命令(Windows Git Bash):
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21" && \
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug test --no-daemon"
(本机无 python3、无模拟器、无连接设备;JDK/SDK 见 android/local.properties
最后按"阶段 → 声明 → 方法 → 结论 → 反例(若证伪)"输出报告。
```
---
## 1. 阅读顺序(先建立判据,再看实现)
1. `docs/android_app/execution_plan.md` —— **验收判据来源**。特别是 §0 全局约束
(禁改清单、构建命令、提交格式)与各阶段的"精确规格"。核查就是"拿计划对实现"。
2. `docs/android_app/verification_guide.md`(本文件)—— 核查路线。
3. `git log --oneline 087e15c..HEAD` 与每个提交的 `git show --stat` —— 改动边界。
4. `docs/android_app/session_state.md` 的"执行计划进度""本轮总结""待办"——
执行者的自述(**待验证**)。
5. `docs/android_app/real_device_checklist.md` —— 24 步真机清单(**待验证**,
见 §4.9:其中第 15 步的预期与实现可能不符)。
6. 官方逆向产物(判据,非执行者产出):
- `analysis/magcx_official_flow.md`(协议权威)
- `analysis/sdk_re/android_app/jadx_magcx/`(官方普通版 Java 源码)
- `analysis/sdk_re/android_app/libcxsdk_decomp.txt`Ghidra 伪代码)
- `analysis/sdk_re/android_app/palette_extraction_findings.md`Phase C 结论)
判据优先级:**官方产物 > 计划文档 > 执行者文档**。三者冲突时以官方产物为准,
并指出冲突本身。
---
## 2. 第一关:硬门槛(不通过则停止核查)
### 2.1 git 完整性
```bash
cd /c/Project/MAG160C
git status --short # 期望:无输出(工作树干净)
git log --oneline 087e15c..HEAD # 期望:恰好 7 条,见下表
git log --oneline -1 origin/main # 期望:087e15c**未 push**,符合用户指令)
```
期望的提交与消息(消息须与执行计划 §各阶段"提交信息"**逐字一致**):
| 序 | hash | 消息(计划要求) | 阶段 |
|---|---|---|---|
| 1 | 06c1f30 | `android: lifetime query + cali consistency check + real-device checklist` | A |
| 2 | 656d419 | `android: MDT temperature decode + analyzer probe UI` | B |
| 3 | b7a928e | `android: vendor palette extraction tooling (+exact tables if found)` | C |
| 4 | c34940e | `android: cloud module scaffold (retrofit, opt-in, disabled by default)` | D |
| 5 | f8b3200 | `android: visible-light PIP overlay (camera2, draggable, 3 sizes)` | E |
| 6 | 512508e | `android: LAN remote preview (UDP discovery + raw-frame TCP stream, client-side rendering)` | F |
| 7 | 8e1312a | `android: phase Z wrap-up (...)`(Z 无强制消息,内容须仅为文档/清单/APK) | Z |
**反例**:条数≠7;消息被改写;Z 里混入功能代码改动。
### 2.2 禁改清单(执行计划 §0 第 1 条)
逐条独立验证,不要相信"执行者说没改":
```bash
# ① 竖屏锁定:源码 + 已构建 APK 双重确认
git diff 087e15c..HEAD -- android/app/src/main/AndroidManifest.xml | grep -i orientation
# 期望:无 '-' 开头的行(只有新增注释/权限行)
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump xmltree \
build-artifacts/mag160c-app-debug.apk --file AndroidManifest.xml | grep -i screenOrientation
# 期望:screenOrientation(0x0101001e)=1 1 = portrait
# ② 命令字节序锁:MagProtocolTest 与 cmd4/cmd8 实现都不得变
git diff 087e15c..HEAD --stat -- android/app/src/test/kotlin/com/mag160c/thermal/usb/MagProtocolTest.kt
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt
# 期望:测试文件无 diffMagProtocol 仅新增 RSP_SEND_LIFETIME 常量
# ③ LiveRenderer 构图逻辑
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt
# 期望:空
# ④ analysis/ 只读参考:已存在文件不得被改(新增文件允许)
git diff 087e15c..HEAD --stat --diff-filter=M -- analysis/
# 期望:空
# ⑤ IrSession 握手序列(A 阶段按要求加了代码,需人工确认序列未被改)
git diff 087e15c..HEAD -- android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt
# 核查点:66b(GetParameter1)→66c→66f→[670]→673 顺序、800ms 超时、
# 前半段失败即 abort/回退 的行为均未被改动;删除行应仅有两处
# cache.readBytes() 改为先比 MD5stats 行末尾追加 lifetime=
```
**反例**APK 里 screenOrientation≠1MagProtocolTest 有改动;
LiveRenderer 有 diff`--diff-filter=M` 的 analysis/ 输出非空。
### 2.3 构建与测试(全绿是计划的硬性验收)
```bash
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21"
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug :app:assembleRelease test --no-daemon"
# 期望:BUILD SUCCESSFULdebug + release(R8) + 单测)
```
单测计数须可复现(执行者报告为 44/0/0):
```bash
cd /c/Project/MAG160C
grep -h -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' \
android/app/build/test-results/testDebugUnitTest/*.xml \
| awk -F'"' '{t+=$2;f+=$6;e+=$8} END {print "tests="t" failures="f" errors="e}'
```
期望的逐类分布(缺一类即说明有用例被静默跳过):
| 测试类 | 用例数 | 覆盖 |
|---|---|---|
| usb.MagProtocolTest | 3 | 命令字节序(既有,未改) |
| core.RenderPipelineTest | 3 | 与 C 参考逐字节一致(既有) |
| core.TempMathTest | 5 | Phase B 温度图 |
| core.PalettesTest | 8 | Phase C 调色板 |
| media.MdtTest | 6 | Phase B MDT 容器 |
| cloud.CloudClientTest | 4 | Phase D 默认关闭契约 |
| net.RemoteContractTest | 12 | Phase F 协议/粘包/截断 |
| net.RemoteLoopbackTest | 3 | Phase F **真实 TCP 回环端到端** |
**重要**`RenderPipelineTest.matchesCReferencePixelExact` 必须仍然通过——
它是整个渲染管线正确性的地基;若它挂了,后面所有阶段都无意义。
---
## 3. 第二关:逐阶段声明 → 核查方法
### Phase A06c1f30
| 声明 | 核查方法 | 通过判据 / 反例 |
|---|---|---|
| `RSP_SEND_LIFETIME = 0x5BB5B561` 等于官方 `D2P_SendLifeTime` | 读 `jadx_magcx/\cn\com\magnity\magnitycx\sdk\D2PCmd.java` 的十进制值,手算十六进制 | =1538635105;反例:数值不符 |
| `CMD_GET_LIFETIME` 未被改动 | 同上(P2DCmd.java)对照 | =1807136373=0x6BB6B675 |
| lifetime 失败不阻断连接 | 读 `IrSession.startInternal` 第 3b 步 | 失败仅 `DebugLog.log`,无 return/abort |
| cali 缓存 MD5 对照**不改变**返回值 | 读 `obtainCali` cache-hit 分支 | 仍 return cached;日志文案 `cali cache vs bundled DDT: identical/differ` |
| 心跳 stats 追加 lifetime | 读 `streamLoop` | 行尾 ` lifetime=${deviceLifetimeMs}` |
| checklist 的日志串与代码一致 | 跑 `analysis/tools/CheckStrings.java`(见下),再人工追溯它标出的 7 条 | 无凭空编造的日志行 |
**checklist 日志串机械核对**(执行者已自测,核查者须独立重跑):
```bash
cd /c/Project/MAG160C && \
"C:\Tools\jdk-21\bin\java" analysis/tools/CheckStrings.java \
docs/android_app/real_device_checklist.md android/app/src/main
# 执行者 2026-09-11 实测:exact=16, skeleton=30, needs-review=7
```
那 7 条**不是缺陷**,而是"整行都是实例数据"的情形(模板在一个字符串字面量里,
命令名由调用点传入),工具无法自动匹配,必须人工追溯。执行者已逐条追溯完毕:
| 标出的行 | 源码出处(已核对) |
|---|---|
| `[cmd] GetParameter1 write=4/4` | `"$name write=$n/${packet.size}"` (IrSession.kt:418) + 调用点 `"GetParameter1"` (:185) |
| `[cmd] GetParameter1 resp=0x5BB5B55B len=60 head=…` | `"$name resp=0x%08X len=%d head=%s"` (:433) + 同调用点 |
| `[cmd] GetParameter2 resp=0x5BB5B55C len=…` | 同上模板 + 66c 调用点 (:192) |
| `[cmd] BasePara1: serial=… devType=3 160x120 @15fps` | `"BasePara1: serial=$serial devType=$devType ${width}x$height @${fps}fps"` (:292) |
| `[usb] ep 0x81 fail#… halted=…` | `"ep 0x%02X fail#$failureCount get_status rc=%d halted=%d clear_halt rc=%d"` (:408) |
| `[cmd]`(空)与 `[crash]` | 正则产物,非清单条目 |
**核查者要做的是独立确认这张对应表**(尤其模板与调用点是否真能拼出清单示例),
而不是重跑得到同样的 7 条后就放行。
> A 的 `obtainCali` 无单测(需 `UsbDeviceConnection`)。核查者应显式标注
> "仅静态审查,无运行时证据"。
### Phase B656d419
| 声明 | 核查方法 | 通过判据 / 反例 |
|---|---|---|
| `Mdt.parse``compose` 互逆 | 读代码 + 跑 `MdtTest`;**另写独立用例**:构造含 EXIF 缩略图的 JPEG(内部含 `FF D9`)再 compose/parse | jpg 字节往返一致(`extractJpg` 反向搜索最后一个 FFD9,应取主图 EOI,不被缩略图干扰) |
| text 去 NUL 填充 | 读 parse | `trimEnd('\u0000')` |
| 温度图 = u16LE → `countsToTempMc` | 读 `TempMath.tempMapFromPixels`**独立复算**:用 `csdk/src/mag160c_temp.c` 或已提交的 C 参考对同一 counts 算温度 | 与 `TempMathTest.mapMatchesScalarConversionPerPixel` 同值 |
| 分析页探针映射为"直接映射"而非 90° 逆映射 | 读 `AnalyzeViewer`:画布 `aspectRatio(4/3)``drawImage` **无 canvas.rotate** | 判断成立则直接映射正确;反例:若实际绘制有旋转,则探针温度取错像素 |
| 温度条固定在未缩放 fitRect 顶部 | 读 `drawTemperatureOsd`(传 `imageRect(size,1f,Offset.Zero)`) | 属**规格歧义**(计划原文"随 fitRect 走,不随手势矩阵"),核查者需判定取舍是否可接受 |
### Phase Cb7a928e)——本轮最需要深度复核的一相
| 声明 | 核查方法 | 通过判据 / 反例 |
|---|---|---|
| `libcxsdk.so` 无静态调色板表 | 跑 `analysis/tools/PalScan.java`(**决定性**:正对照);python 版仅作弱旁证 | 三种编码(`B,G,R,0` / `B,G,R,0xFF` / 忽略第 4 字节的 `B,G,R`)全部 not found |
| 索引↔case 映射(0..10 | ①读 `libcxsdk_decomp.txt``CFunctions::SetColorPalette`,确认是 `switch(param_2)` 且 case 标签即调色板序号;②锚点:case 2 → `OfficialTables.PALETTE256_ARGB` 256/256;③预览图交叉验证 | ①②为结构性证据;③仅作旁证 |
| 索引 11 未解决 | 检查 `VendorPalettes.SOURCE_CASE[11] == -1``Palettes.buildAll()[11]` 为近似曲线 | 与 findings 文档一致 |
| 铁虹仍是官方表 | 读 `Palettes.officialIronbow()``PalettesTest.ironbowMatchesOfficialTables` | 返回 `OfficialTables.PALETTE256_ARGB` 本身 |
**否证复核 + 生成物可复现**(执行者已自测,核查者须独立重跑;完整命令见
`analysis/sdk_re/android_app/palette_extraction_findings.md` 的"可复现产物"节):
```bash
cd /c/Project/MAG160C
# 决定性:官方铁虹表是否真的不在库里(3 种编码都要 not found)
"C:\Tools\jdk-21\bin\java" analysis/tools/PalScan.java \
analysis/sdk_re/android_app/bin/libcxsdk.so \
csdk/src/mag160c_official_palette256.h
# 生成物可复现(两文件一起编译;重新生成后 diff 应为空)
# 实测:VendorPalettes.kt 与 palette_candidates.json diff 均为空,锚点 256/256
```
> ⚠️ **核查者必须避开的坑**:铁虹表在内存里的布局是 `(B, G, R, 0)`——**第 4 字节
> 是 0**,而 `OfficialTables.kt` 的 ARGB int 被生成脚本强制 `alpha=0xFF`。
> 所以"直接把 Kotlin 表当针去二进制里搜"**必然搜不到**,会得到假的"未找到"。
> 针必须从 `csdk/src/mag160c_official_palette256.h`(真实内存布局)构造,
> 或用已修正的 `PalScan.java`。执行者在第一版工具里就犯过这个错,已修正。
>
> 另:通用"256 项渐变"启发式扫描在本库上误报约 11% 的位置(全是指令/整数
> 数组),`PalScan.java` 的 [C] 段已明确标注**不可作为证据**,不要引用该计数。
**重新生成并比对**(Phase C 最强核查手段;执行者已自测通过,**核查者须独立重跑**):
> 注意:`PalExport2.java` **不是**单文件可直跑——它依赖同一目录
> `PalIdentify.java` 中的 `PalBody`(13 个生成体)。必须两文件一起编译。
> 另外 Windows 版 java 不认 Git Bash 的 `/tmp` 路径,必须用 `C:\...` 形式。
```bash
# ① 解包官方普通版的调色板预览图(交叉验证用)
mkdir -p /c/Users/zxc/AppData/Local/Temp/cxres && \
cd /c/Users/zxc/AppData/Local/Temp/cxres && \
"C:\Tools\jdk-21\bin\jar" xf "C:\Project\MAG160C\app\【普通版】MAG-Cx.apk" res/mipmap-hdpi-v4/
# ② 两文件编译
mkdir -p /c/Users/zxc/AppData/Local/Temp/regen_build && \
cd /c/Users/zxc/AppData/Local/Temp/regen_build && \
cp "C:\Project\MAG160C\analysis\tools\PalIdentify.java" . && \
cp "C:\Project\MAG160C\analysis\tools\PalExport2.java" . && \
"C:\Tools\jdk-21\bin\javac" -d out PalIdentify.java PalExport2.java
# ③ 重新生成(输出到临时目录,勿覆盖仓库)
"C:\Tools\jdk-21\bin\java" -cp out PalExport2 \
"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\OfficialTables.kt" \
"C:\Users\zxc\AppData\Local\Temp\cxres\res\mipmap-hdpi-v4" \
"C:\Users\zxc\AppData\Local\Temp\regen_core" \
"C:\Users\zxc\AppData\Local\Temp\regen_out"
# 期望最后一行:iron_bow vs OfficialTables anchor: 256/256
# ④ 与仓库文件逐字节比对(两条都必须为空)
diff "C:\Users\zxc\AppData\Local\Temp\regen_core\VendorPalettes.kt" \
"C:\Project\MAG160C\android\app\src\main\kotlin\com\mag160c\thermal\core\VendorPalettes.kt"
diff "C:\Users\zxc\AppData\Local\Temp\regen_out\palette_candidates.json" \
"C:\Project\MAG160C\analysis\sdk_re\android_app\palette_candidates.json"
```
**执行者自测结果(2026-09-11,供核查者对照)**:上述四条命令全部成功,
铁虹锚点 256/256,两份生成物 diff 均为空,即**从 .so 伪代码 → 生成体移植 →
Kotlin 表**这条链完全可复现。这证明的是"移植实现与提交物一致"**不**证明
"移植的语义与厂商二进制一致"(后者只有 case 2 锚点能证明)。
**执行者主动交底(务必复核)**index 4/8/9(琥珀/喷射/红饱和)的映射在
**两种独立度量下结论不一致**——像素加权的最近色距离倾向把"红饱和"的预览图
判给灰阶表(case 0),而颜色集合覆盖率倾向 case 9。原因是该预览图近乎无彩
(执行者测得 meanChroma≈9.7)。执行者最终采用了"结构性证据(`switch(param_2)`
的 case 标签即调色板序号)+ case 2 锚点"的论证,预览图仅作旁证。**核查者应
独立判定索引 9(以及 4、8)的映射是否成立**;更强证据需反解 ARM32 跳表
(文件偏移 0x26cd2 附近,执行者尝试未成功)或真机抓帧比对。
可复现的"红热预览图是占位副本"检查(支撑索引 11 未解决的结论):
```bash
# palette_red_hot.png 与 palette_white_hot.png11914 个不透明像素中 11777 个完全相同
# 可用任意图像工具逐像素比对(执行者用自写 Java 工具测得 meanAbsDiff=0.5/255
```
### Phase Dc34940e
| 声明 | 核查方法 | 通过判据 / 反例 |
|---|---|---|
| Retrofit 2.11.0 已接入 | 读 `gradle/libs.versions.toml``app/build.gradle.kts` | 两处均在 |
| 默认关闭 | 读 `AppSettings.cloudEnabled`(默认 false);跑 `CloudClientTest` | 4 项全绿 |
| **任何生产代码路径都不会静默联网** | `grep -rn "CloudClient" android/app/src/main` | 只应有 AppSettings 的 setEnabled 与声明;**反例**:任何 `api()` 调用点 |
| release 混淆不破 | 跑 `assembleRelease` | BUILD SUCCESSFUL |
| PROGUARD 规则存在 | 读 `app/proguard-rules.pro` | 含 `-keep class com.mag160c.thermal.cloud.**` |
### Phase Ef8b3200
| 声明 | 核查方法 | 通过判据 / 反例 |
|---|---|---|
| CAMERA 权限已加且**相机可选** | `aapt2 dump badging` 看 uses-feature | `camera`/`camera.any`/`autofocus` 均为 not-requiredusb.host 仍为 required |
| 顶栏第 5 项 + 图标 | 读 `LiveScreen``res/drawable/ic_pip.xml` | 双弧圆 + 右下实心矩形(符合计划图标约定) |
| 三档尺寸 96/128/160dp、高=宽×3/4 | 读 `LiveViewModel.PIP_WIDTHS_DP``PipOverlay` | 一致 |
| 右侧留色标条位 | `PIP_RIGHT_MARGIN_DP = 32` | 一致 |
| 异常不崩溃 | 读 `PipCameraView`:所有回调 try/catch + release | 无未捕获路径 |
| 相机释放(关闭/离页/ON_STOP) | 读 `PipOverlay.DisposableEffect``LiveScreen` 生命周期观察者 | 三处均有 release |
**执行者主动交底(务必真机验证)**`ON_STOP` 释放相机后,**是否有代码自动重新
打开未经证实**。实现中 `PipCameraEngine.released` 标志被置位后不再复位,重新
打开依赖 TextureView surface 重建路径(`onSurfaceTextureAvailable`)。
`real_device_checklist.md` 第 15 步写了"返回后若 PIP 仍为开启态,画面重新出现",
这是**推断而非实测**;若真机不符,应判定为该步预期有误(清单过强),
而非功能必须修复——请按实测修正文档结论。
**另一处待验证**PIP 浮层用了两个并列 `pointerInput`(一个拖拽、一个点按/双击)。
Compose 中多 detector 并存的行为需真机确认(单击换档、双击关闭、拖拽是否互不干扰)。
### Phase F512508e
| 声明 | 核查方法 | 通过判据 / 反例 |
|---|---|---|
| 端口/魔数/帧长与计划一致 | 读 `RemoteContract`47510/47511/0x1BB1B11B/38400/38412/3s/10s | 逐个对表 |
| 封包格式 | 读 `encodeFramePacket` + 跑 `RemoteContractTest` | `[magic][counter][len][38400B]` |
| 粘包/截断/坏长度/重同步 | 跑 `net.RemoteContractTest`12 项) | 全绿 |
| 端到端可用 | 跑 `net.RemoteLoopbackTest`(真实 TCP) | 全绿;核查者亦可**自写独立回环测试**(契约见 `RemoteContract` 注释) |
| 渲染在客户端本地 | 读 `RemoteViewerViewModel`:本地 `RenderPipeline`+内置 DDT`setPalette/setZoom` 只动本地 | 调色板/变倍无网络调用 |
| 主机侧不拖慢相机 | 读 `RemoteHost`:帧队列 DROP_OLDEST、单协程写 socket | 一致 |
| 默认不启动 | 读设置页与 `setRemoteHostEnabled` | 默认关闭;开启需活跃 USB 会话,否则返回 false 弹"先连接热像仪" |
| INTERNET 权限 | `aapt2 dump badging` | 已声明 |
**执行者主动交底(潜在缺陷,建议构造用例)**
`RemoteSession.send()` 每条命令 `launch` 一个新协程写同一 socket,**两条相邻命令
(如 `hello` 后紧跟 `startStream`)理论上可能交错写入**(小包在 TCP 上通常原子,
但无保证)。建议核查者写一个高频命令用例(如连续多次 `requestFfc()`)观察主机侧
是否出现解析不了的半行 JSON。若复现,属真实缺陷(应串行化写协程)。
**未验证边界**UDP `255.255.255.255` 广播在真机/真实 Wi-Fi 上是否可达
(部分网络/设备会过滤广播,且息屏策略可能影响收包)——仅回环测试通过。
---
## 4. 第三关:执行者主动交底的可疑点汇总(按风险排序)
| # | 事项 | 风险 | 建议动作 |
|---|---|---|---|
| 1 | Phase C 索引 4/8/9 的映射在两种度量下不一致 | 高(可能装错表) | 独立复验;必要时反解跳表/真机比对 |
| 2 | Phase C 静态扫描判据仅覆盖 alpha=0xFF | 中 | 补扫 alpha=0x00 / RGB888 / BGR 等编码 |
| 3 | PIP 从后台返回能否自动恢复相机 | 中 | 真机验证;不符则修正清单预期 |
| 4 | PIP 双 `pointerInput` 手势并存 | 中 | 真机验证三种手势 |
| 5 | Phase F 命令写入未串行化 | 中 | 高频命令用例压测 |
| 6 | Phase B 探针映射方向(直接 vs 旋转) | 中 | 真机点已知位置比对温度 |
| 7 | Phase B 温度条"随 fitRect"的规格歧义 | 低 | 判定取舍 |
| 8 | Phase A lifetime 负载接受两种布局的"对冲" | 低 | 判定是否应只留官方一种(官方 Java 为 `[magic][ms]`,共 8B |
| 9 | checklist 第 15 步预期与实现可能不符 | 低 | 见 #3 |
| 10 | `build-artifacts` 内 APK 非可复现字节(时间戳) | 低 | 以"重建并验证字符串/安装行为"替代字节比对 |
| 11 | 超出计划的一处主动改动:camera feature 显式设为 required=false | 低 | 判定是否可接受(不影响 USB 主机功能) |
---
## 5. 核查边界:本机无法验证的事项(须标注"无法判定")
本机(Windows`C:\Tools\android-sdk`)**没有模拟器组件、没有连接的真机**,
`adb devices` 为空。因此以下只能在用户真机上判定:
1. USB 出流与温度绝对值标定(`real_device_checklist.md` 第 1-12 步)。
2. 可见光 PIP 的全部运行时行为(第 13-15 步)。
3. 局域网远程预览双机行为(第 16-24 步)。
4. 分析页温度条/探针的真机显示与取温正确性(第 11 步)。
5. 任何"画面/像素"层面的确认(执行者无视觉能力,只能保证编译与单测)。
核查者不应把"未实测"一律记为失败;应记为**无法判定**,并保留执行者的
真机清单作为待用户执行的验证脚本(但清单本身的日志串与预期须先按 §3A/§4
机械核对一遍)。
---
## 6. 复现命令速查
```bash
# 构建 + 全量单测(Windows Git Bash
cd /c/Project/MAG160C/android && export JAVA_HOME="C:\\Tools\\jdk-21" && \
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:assembleDebug :app:assembleRelease test --no-daemon"
# 单类测试
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:testDebugUnitTest --tests 'com.mag160c.thermal.net.*' --no-daemon"
# 单测计数
grep -h -o 'tests="[0-9]*"' /c/Project/MAG160C/android/app/build/test-results/testDebugUnitTest/*.xml | ...
# APK 清单核验(竖屏 + 权限 + feature
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump badging build-artifacts/mag160c-app-debug.apk
"C:\Tools\android-sdk\build-tools\36.0.0\aapt2.exe" dump xmltree build-artifacts/mag160c-app-debug.apk --file AndroidManifest.xml
# dex 中文串(防乱码回归;无需脚本)
unzip -p build-artifacts/mag160c-app-debug.apk 'classes*.dex' | grep -ac '画中画'
unzip -p build-artifacts/mag160c-app-debug.apk 'classes*.dex' | grep -ac '正在扫描局域网主机'
# checklist 日志串核对(执行者已跑通:exact=16 skeleton=30 review=7
"C:\Tools\jdk-21\bin\java" analysis/tools/CheckStrings.java \
docs/android_app/real_device_checklist.md android/app/src/main
# Phase C 重新生成并比对(见 §3 Phase C 完整命令)
```
**可用工具**`C:\Tools\jdk-21`java/javac/jar)、
`C:\Tools\android-sdk\build-tools\36.0.0`aapt2)、`C:\Tools\jadx-1.5.1`
**不可用 / 易踩的坑**(执行者已实测,核查者省得重踩):
| 事项 | 实际 |
|---|---|
| python3 | **不可用**Microsoft Store 桩,退出码 49)。`extract_palettes.py` 需改用 JDK 复写或安装 python |
| node | 未安装 |
| 模拟器 / 连接设备 | 无(`adb devices` 为空,SDK 无 emulator 组件) |
| `analysis/tools/PalExport2.java` | **不能单文件直跑**:依赖 `PalIdentify.java``PalBody`,须两文件一起 `javac -d out` |
| Git Bash 的 `/tmp` 路径 | Windows 版 java **不认**,必须写 `C:\Users\zxc\AppData\Local\Temp\...` |
| `analysis/tools/CheckStrings.java` | 可单文件直跑(无外部依赖) |