Files
MAG160C/docs/android_app/verification_report.md
T

455 lines
30 KiB
Markdown
Raw 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 撰写,不采信执行者(下称"执行者")的
> 总结/session_state/checklist 叙述,全部结论来自 ① git 提交内容
> ② 仓库内官方逆向产物 ③ 核查者亲自重跑的命令与亲自编写的独立工具
> ④ 明确标注"无法判定"的真机项。
>
> **核查范围**`06c1f30..8e1312a`7 个提交),基线 `087e15c`
> 核查时 HEAD = `0919f5e`(含核查指南提交)。
> **本轮核查未修改任何源码、未提交、未 push**;本报告文件是唯一新增物
> (未提交,`git status` 会显示为 untracked)。
>
> **总体结论**:第一关硬门槛(git 完整性 / 禁改清单 / debug+release 构建 /
> 44 项单测)**全部通过**。逐阶段核查发现 **2 项证伪**(Phase F 命令写入
> 未串行化、Phase F busy 未实现)、**1 项 Phase Z 越界**Manifest 改动)、
> **2 项低风险偏差**,其余声明证实;需真机/双设备的项一律"无法判定"。
> Phase C 最高风险项(索引 4/8/9 映射)经独立复核**证实**,且本次找到了比
> 执行者更强的决定性证据。
---
## 0. 修复任务清单(给修复模型)
按优先级排列。每条含:位置、现象、根因、建议改法、改完怎么验、不修的风险。
**修复时仍须遵守 `execution_plan.md` §0 的禁改清单**(竖屏锁定、命令字节序、
握手序列、LiveRenderer 构图、analysis/ 只读)。
### FIX-1(高)`RemoteSession.send()` 并发写同一 socket 导致命令乱序
- **位置**`android/app/src/main/kotlin/com/mag160c/thermal/net/RemoteClient.kt:256-269`
- **现象(已复现)**`hello()` 紧接 `startStream()` 时,主机可能先收到 `start`
主机随即回 `stream-start`,客户端 reader 切到帧模式
`RemoteClient.kt:194`),后到的 `welcome` 文本被 `FramePacketReader` 当垃圾丢弃。
- **实测数据**:裸 socket 抓字节,5 次运行中 1 次观测到 `hello`/`start` 顺序颠倒;
面向真实 `RemoteHost` 的生产序列压测 **120 次中 8 次丢失 `welcome` 行(≈6.7%**
帧本身始终正常(30/30 各轮均收到)。
- **根因**`send()` 每条命令 `scope.launch(Dispatchers.IO)` 新协程写同一个
`socket.getOutputStream()`。多协程竞争同一流,**顺序无法保证**。
- **建议改法****不要用 Mutex**Mutex 只保证互斥,不保证"launch 顺序=写入顺序"
本缺陷是乱序而非字节交错,加锁修不掉)。改为单写协程 + 队列:
```kotlin
// RemoteSession 内新增
private val outQueue = kotlinx.coroutines.channels.Channel<String>(
kotlinx.coroutines.channels.Channel.UNLIMITED,
)
private val writerStarted = java.util.concurrent.atomic.AtomicBoolean(false)
private fun ensureWriter() {
if (!writerStarted.compareAndSet(false, true)) return
scope.launch(Dispatchers.IO) {
val out = socket.getOutputStream()
try {
for (line in outQueue) {
out.write((line + "\n").toByteArray(Charsets.UTF_8))
out.flush()
}
} catch (e: Exception) {
if (!closed.get()) DebugLog.log("remote", "writer ended: ${e.javaClass.simpleName}")
}
}
}
private fun send(cmd: String) {
if (closed.get()) return
ensureWriter()
outQueue.trySend(cmd) // 调用方(UI 协程)顺序入队 → 顺序落盘
}
fun close() {
if (!closed.getAndSet(true)) {
runCatching { outQueue.close() }
runCatching { socket.close() }
}
}
```
要点:`trySend` 在**调用方线程**顺序入队,写 socket 只发生在唯一协程里。
- **可选补充(不替代上面的修复)**:客户端在帧模式下遇到非帧数据时,
可先尝试按 JSON 行解析再丢弃,作为对旧主机的兼容容错。属于加固,不解决根因。
- **验证方法**:复跑"高频命令"用例——`hello()`+`startStream()` 连续 3 对,
再接 400 次 `requestFfc()`,断言收到的行数、内容与**顺序**完全一致、
无嵌套花括号行。(核查者的独立 harness 已实现该用例,见 §6。)
- **不修的风险**:目前 `welcome` 只被打印(`RemoteViewerViewModel.kt:103`),
用户可见影响小;但 `hello→welcome` 契约已被破坏。同类乱序也可能发生在
快速 stop→start、连续 FFC 等序列上,属真实缺陷。**建议修**。
### FIX-2(中)"后来者拒绝写 busy" 未实现
- **位置**`android/app/src/main/kotlin/com/mag160c/thermal/net/RemoteHost.kt:130-151`acceptLoop/serve
- **现象**`serve(socket)` 在 accept 循环内**同步**执行,第二个客户端只会在内核
backlog 里排队等待第一个断开;主机**从不写 `busy`**。
`RemoteContract.isBusyLine()``RemoteContract.kt:147`)全仓库**无调用点**,是死代码。
- **与计划冲突**`execution_plan.md` F2 明确要求"单客户端,后来者拒绝写 busy"。
- **建议改法(二选一)**
- **(a) 实现**accept 循环不阻塞在 serve 上,用标志位拒绝后来者:
```kotlin
private val serving = java.util.concurrent.atomic.AtomicBoolean(false)
// acceptLoop 的 while(running) 内:
val socket = server.accept()
if (!serving.compareAndSet(false, true)) {
runCatching {
val o = socket.getOutputStream()
o.write((RemoteContract.busyLine() + "\n").toByteArray(Charsets.UTF_8))
o.flush()
}
runCatching { socket.close() }
continue
}
scope?.launch { try { serve(socket) } finally { serving.set(false) } }
```
需在 `RemoteContract` 补 `fun busyLine(): String = "{\"type\":\"busy\"}"`
客户端侧建议在 `lines` 收集处对 `busy` 记一条日志(否则用户只看到连不上)。
- **(b) 如不实现**:同步修改 `execution_plan.md` F2 与相关文档,如实写明
"第二客户端排队等待,不写 busy",并把 `isBusyLine()` 删除或标注保留用途。
- **验证方法**:单测/回环:client1 连上后 client2 连接,断言 client2 在 1s 内
收到 `{"type":"busy"}` 或被立即关闭,且 client1 的帧流不受影响。
- **不修的风险**:单客户端约束事实成立,风险低;但"计划声明了、代码没有、
解析器留了死代码"三者不一致,会误导后续维护者。
### FIX-3(低)lifetime 负载接受两种布局(超出官方规格的对冲)
- **位置**`android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt:226-231`
- **现象**:实现同时接受 ①`[magic][magic][ms]`12B,计划代码片段的字面写法,
与计划自己的"共 8 字节"描述矛盾)②`[magic][ms]`(8B,官方 Java 唯一实现)。
- **官方权威**`jadx_magcx/.../UsbCommunication.java:190-205` —
`bb.getInt()==1538635105` 后再 `getInt()`,即 **8 字节 `[magic][ms]`**。
另注意 `IrSession.readResp` 已剥掉前 4 字节 magic`r.third` 是**去掉** magic 的负载),
故官方布局下 `r.first==8`、`r.third.size==4`、ms = `u32(r.third,0)`。
- **建议改法**(收紧为官方唯一布局):
```kotlin
readResp(conn, epResp, "GetLifeTime")?.let { r ->
// readResp 已剥离 4B magic;官方帧共 8B → 余 4B 即 i32 ms
if (r.second == MagProtocol.RSP_SEND_LIFETIME && r.third.size >= 4) {
deviceLifetimeMs = MagProtocol.u32(r.third, 0).toLong() and 0xFFFFFFFFL
}
}
```
- **验证方法**:真机清单第 3 步出现 `[session] device lifetime=<ms>ms` 且非 -1。
- **不修的风险**:不会误判为连接失败,仅接受一个官方不发送的布局;低。
### FIX-4(低)多余权限 `ACCESS_NETWORK_STATE`
- **位置**`android/app/src/main/AndroidManifest.xml:15`
- **现象**:全仓库 grep 无 `ConnectivityManager`/`NetworkCapabilities`/权限检查,
该权限从未被使用(计划 F9 只要求 INTERNET)。
- **建议改法**:删除该行;若保留,需在提交信息/文档中说明用途。
- **验证方法**`aapt2 dump badging build-artifacts/mag160c-app-debug.apk` 不再列出。
### FIX-5(低)Phase Z 越界:`8e1312a` 含 Manifest 功能性改动
- **位置**`8e1312a` 修改了 `android/app/src/main/AndroidManifest.xml`+3 条
`uses-feature``camera`、`camera.autofocus` 显式 `required=false`,并改写注释)。
- **性质**:改动本身**合理**(防止 CAMERA 权限隐式要求相机硬件,保证无相机
设备仍可运行热像主功能),执行者已主动交底;但违反"Z 内容须仅为文档/清单/APK"。
- **建议**:不需要回滚功能,**只需在 `session_state.md` / `execution_plan.md`
如实记录该改动发生在 Z 提交**(承认越界),避免后续核查再次对不上账。
### FIX-6(低)文档口径修正
1. `docs/android_app/real_device_checklist.md:29`(第 15 步):预期"返回后 PIP 画面
重新出现"是**推断而非实测**。代码里 `PipCameraEngine.released` 置位后不复位,
只能依赖 `onSurfaceTextureAvailable` 再次触发。**先按真机实测结果决定改文档
还是改代码**(指南 §3E 明确:若真机不符,应判定为清单预期有误)。
2. `analysis/sdk_re/android_app/palette_extraction_findings.md:71`:红热占位图的
分母表述混用了两类像素。实测(ImageIO 逐像素):
**全部像素 11777/11914 相同****不透明像素 8536/8673 相同**meanAbsDiff 2.18/765)。
原文"11914 个**不透明**像素中 11777 个相同"应改为上述两种口径之一。
3. `session_state.md` / `execution_plan.md`:补记本报告 §3-F 的两项证伪
FIX-1 乱序、FIX-2 busy 未实现),使文档不再高于实现。
### FIX-7(可选,测试加固)
1. `MdtTest`:补一条**含 EXIF 缩略图(内部含 `FF D9`)的 JPEG** 往返用例。
核查者已独立验证当前实现能正确取主图 EOI(不会被缩略图干扰),
但仓库测试缺失该场景,属回归风险。
2. `VendorPalettes``PalettesTest.vendorTablesCoverIndicesZeroThroughTen`
`PalettesTest.kt:47-63`)只证明 `Palettes.buildAll()` 用了 `VendorPalettes`
的对应槽位,**不能**证明 `VendorPalettes` 里 case N 的内容确实来自 case N
(全库仅 case 2 有官方锚点)。建议为 11 张表各加一条"金标"断言
(如 256 项 CRC32/采样点),把本次已外部验证过的映射冻结住。
3. `RemoteLoopbackTest`:在 FIX-1 完成后,补"高频命令顺序"用例(见 FIX-1 验证方法)。
4. 清理死代码:`Palettes.kt:77/89/98/110` 的 `rainbow()`/`highContrast()`/
`hotMetal()`/`jet()` 已无调用点(Phase C 接入精确表后遗留)。
### 判定为"可接受、无需修改"的项
- **Phase B 温度条固定未缩放 fitRect**:与计划字面一致(`imageRect(size,1f,Offset.Zero)`),
取舍合理。
- **Phase B 探针标签随缩放/平移**(`AnalyzeViewer.kt:266` 用 `imageRect(size,zoom,pan)`):
与计划"标签随 fitRect 走"字面不符,但**实现明显更正确**(标签须跟随像素),
判定为实现优于字面规格,建议保留(可在文档注明)。
- **Phase B 分析页直接映射(无 90° 旋转)**:`AnalyzeViewer` 画布 `aspectRatio(4f/3f)`
且 `drawImage` 无 `canvas.rotate`,位图 `Bitmap.createBitmap(160,120)` 为原始朝向,
直接映射正确。
---
## 1. 第一关:硬门槛(全部通过,故继续逐阶段核查)
| 声明 | 核查方法 | 结论 |
|---|---|---|
| 工作树干净 | `git status --short` 无输出 | 证实 |
| 范围内恰 7 个提交、消息与计划逐字一致 | `git log --oneline 087e15c..HEAD`,逐条比对计划"提交信息" | 证实 |
| 未 push | `git log --oneline -1 origin/main` = `087e15c` | 证实 |
| 竖屏锁定未改 | 源码 diff 无 `orientation` 删改;`aapt2 dump xmltree` 得 `screenOrientation(0x0101001e)=1` | 证实 |
| `MagProtocolTest` 未变 | `git diff --stat` 为空 | 证实 |
| `MagProtocol.kt` 仅新增常量 | diff 仅 `RSP_SEND_LIFETIME` 三行 | 证实 |
| `LiveRenderer.kt` 未变 | `git diff` 为空 | 证实 |
| `analysis/` 既有文件未被改 | `--diff-filter=M` 为空;仅 8 个新增文件 | 证实 |
| `IrSession` 握手序列未改 | 人工读 diff66b→66c→66f→[670]→673、800ms 超时、前半段失败即 abort 全部保留;删除行仅两处(cache 读后加 MD5、stats 行追加 lifetime | 证实 |
| `assembleDebug + assembleRelease + test` 全绿 | 亲自重跑:`BUILD SUCCESSFUL`release 走完 R8 | 证实 |
| 44 个单测、逐类分布 3/3/5/8/6/4/12/3 | `--rerun-tasks` 强制重跑,逐 XML 汇总 = 44/0/0;每类用例名与指南表格一一对应 | 证实 |
| `RenderPipelineTest.matchesCReferencePixelExact` 仍通过 | 重跑后该用例在列且失败数为 0 | 证实 |
---
## 2. 逐阶段核查明细
### Phase A`06c1f30`
| 声明 | 方法 | 结论 |
|---|---|---|
| `RSP_SEND_LIFETIME = 0x5BB5B561` = 官方 `D2P_SendLifeTime` | `jadx_magcx/.../D2PCmd.java:10` = 1538635105;手算 `0x5BB5B561` = 1538635105 | 证实 |
| `CMD_GET_LIFETIME` 未改 = 官方 `P2D_GetLifeTime` | `P2DCmd.java:8` = 1807136373 = `0x6BB6B675` | 证实 |
| lifetime 失败不阻断连接 | `IrSession.kt:219-235` 失败仅 log,无 return/abort;官方 `getDevLifeTime` 亦仅 `Logging.error` | 证实(仅静态审查,无运行时证据) |
| cali 缓存 MD5 对照不改变返回值 | cache-hit 分支先算 `cached`log `identical/differ` 后仍 `return cached` | 证实 |
| 心跳 stats 追加 lifetime | `IrSession.kt:559` `" lifetime=${deviceLifetimeMs}"` | 证实 |
| checklist 日志串与代码一致 | 独立重跑 `CheckStrings.java`exact=16/skeleton=30/review=7;并逐条人工追溯 7 条到模板与调用点(`"$name write=$n/${packet.size}"` 418 行、`"$name resp=0x%08X len=%d head=%s"` 433 行、`"BasePara1: serial=... @${fps}fps"` 292 行、`"ep 0x%02X fail#..."` 408 行;调用点 `"GetParameter1"` 185、`"GetParameter2"` 201)。另抽查 `hb: state=`、`first reads`、`first rendered frame`、`first run on this host`、`requesting permission`、`permission result`、`[crash]`DebugLog 崩溃钩子)均存在 | 证实 |
| 唯一偏差 | lifetime 负载接受两种布局(见 FIX-3) | 偏差(低) |
### Phase B`656d419`
核查者用 Gradle 缓存内的 Kotlin 编译器(`kotlin-compiler-embeddable-2.2.0.jar`
在仓库外搭建独立 harness,直接编译并驱动**仓库真实源码**
`Mdt.kt`/`TempMath.kt`/`OfficialTables.kt`),不依赖执行者的任何测试代码。
| 声明 | 方法 | 结论 |
|---|---|---|
| `Mdt.parse` 与 `compose` 互逆 | 独立构造**含 EXIF 缩略图(内部含 `FF D9`)**、长度 1081(非 4 对齐)的 JPEG,往返比对各段 | 证实:jpg/info0/info1/frame/text 全部字节一致,裁剪点为主图 EOI |
| 坏尾/截断返回 null | 翻转尾部 1 字节、截断至 100B | 证实 |
| text 去 NUL 填充 | `Mdt.kt:123` `trimEnd('\u0000')`;独立用例短文本往返 | 证实 |
| 温度图 = u16LE → `countsToTempMc` | **独立复算**:从 `csdk/src/mag160c_official_t2e.h` 解析 646 项真表(非 Kotlin 表),照 `mag160c_render.c:64` 重写 C 版算法,对 19200 像素全量比对 | 证实:19200/19200 完全一致 |
| 探针为"直接映射" | `AnalyzeViewer.kt``aspectRatio(4f/3f)`、`drawImage` 无 `canvas.rotate`、`Bitmap.createBitmap(160,120)` | 证实(静态) |
| 温度条固定未缩放 fitRect | `drawTemperatureOsd` 传 `imageRect(size,1f,Offset.Zero)` | 证实 |
| 探针标签 | 用 `imageRect(size,zoom,pan)`,与计划字面"随 fitRect"不同 | 偏差(实现更优,建议保留,见 §0) |
### Phase C`b7a928e`)—— 本轮最需深挖的一相
| 声明 | 方法 | 结论 |
|---|---|---|
| `libcxsdk.so` 无静态调色板表 | 独立重跑 `PalScan.java`:三种编码(`B,G,R,0`/`B,G,R,0xFF`/忽略第 4 字节)**全部 not found**;[A] 段自校准证明扫描非盲 | 证实 |
| 索引↔case 映射 0..10(case 标签即调色板序号) | ① `libcxsdk_decomp.txt:456-1180``switch(param_2)`case 0..10 各一份生成体(另有 `case 0xc`/`case 0xd`);② **核查者新找到的决定性证据**:`jadx_magcx/.../DialogFragmentPalette.java` 的 `mapIndex2Id_` 把 UI 索引 0..11 依次对应白热…红热,点击时 `DeviceController.setColorPalette(index)` 把 **UI 索引原样传给 native** → case 标签 = UI 索引;③ case 2 → `OfficialTables.PALETTE256_ARGB` 256/256(重跑生成链复现) | 证实 |
| 索引 4/8/9 映射成立(执行者交底的不一致项) | **自写独立工具**(JDK `ImageIO` 解码官方预览图,不用执行者的手写 PNG 解码器),从 `PalBody` 重算全部 case 颜色集合,对 12 张预览图做完整覆盖矩阵 | 证实:case 0..10 **各自都是自身预览图的最佳解释者**;红饱和 case9 26.7% vs case0 22.1%,其 meanChroma=9.7 解释了"像素加权最近色距离"为何误判 |
| 索引 11 未解决、`SOURCE_CASE[11]==-1`、保留近似曲线 | `VendorPalettes.kt:24`、`Palettes.kt:47` | 证实 |
| 铁虹仍是官方表 | `officialIronbow()` 直接返回 `OfficialTables.PALETTE256_ARGB``PalettesTest.ironbowMatchesOfficialTables` 通过 | 证实 |
| 生成物可复现 | 两文件一起编译 → 重新生成:锚点 `iron_bow vs OfficialTables anchor: 256/256``VendorPalettes.kt`、`palette_candidates.json`、`palette_match_report.txt` **diff 全空** | 证实 |
| 红热预览图为占位副本 | ImageIO 逐像素:全像素 11777/11914 相同;不透明像素 8536/8673 | 证实(文档分母表述需修正,见 FIX-6) |
| `extract_palettes.py` | 本机无 python3,无法运行 | 无法判定(弱旁证) |
### Phase D`c34940e`
| 声明 | 方法 | 结论 |
|---|---|---|
| Retrofit 2.11.0 已接入 | `libs.versions.toml:9,24,25``app/build.gradle.kts:52,53` | 证实 |
| 默认关闭 | `AppSettings.kt:29-38``CloudClientTest` 4 项全绿 | 证实 |
| 无生产路径静默联网 | `grep -rn CloudClient android/app/src/main` 仅命中声明与 `AppSettings.setEnabled`main 无 `api()` 调用点;`api()` 未开启即 `check()` 抛异常 | 证实 |
| release 混淆不破 | 亲自跑 `assembleRelease`R8)成功 | 证实 |
| PROGUARD 规则 | `proguard-rules.pro:5` `-keep class com.mag160c.thermal.cloud.** { *; }` | 证实 |
### Phase E`f8b3200`
| 声明 | 方法 | 结论 |
|---|---|---|
| CAMERA 权限 + 相机可选;usb.host 仍 required | `aapt2 dump badging`camera/camera.any/autofocus 均 not-required`usb.host`/`screen.portrait` required | 证实 |
| 顶栏第 5 项 + 图标 | `LiveScreen.kt` 5 个 `weight(1f)` 项,第 5 项 `ic_pip`+`画中画``ic_pip.xml` 双弧圆描边 + 右下实心矩形 | 证实 |
| 三档 96/128/160dp、高=宽×3/4 | `PIP_WIDTHS_DP``hDp = wDp * 3 / 4`72/96/120,整除无误差) | 证实 |
| 右侧留色标条位 | `PIP_RIGHT_MARGIN_DP = 32`,初始 `pipXf=1f,pipYf=0f` | 证实 |
| 异常不崩溃 | `PipCameraView.kt` 全部回调 try/catch,失败路径收敛到 `release()` | 证实(静态) |
| 相机释放三处 | `PipOverlay` 的 `DisposableEffect onDispose`、离页同路径、`LiveScreen` 的 `LifecycleEventObserver(ON_STOP)` | 证实(静态) |
| ON_STOP 后能否自动重开 | 代码侧 `released` 置位后不复位,重开依赖 `onSurfaceTextureAvailable`;本机无设备 | **无法判定** |
| 双 `pointerInput` 手势并存 | Compose 运行时行为无法在本机验证 | 无法判定 |
### Phase F`512508e`)—— 2 项证伪
核查者用独立 harness 在 JVM 上驱动**真实的** `RemoteHost`/`RemoteSession`
(仅对 `DebugLog` 做最小桩),不打桩网络层。
| 声明 | 方法 | 结论 |
|---|---|---|
| 端口/魔数/帧长/超时与计划一致 | `RemoteContract`47510/47511/`0x1BB1B11B`/38400/38412/3000ms/10000ms 逐项比对 | 证实 |
| 封包格式小端 | 独立断言字段偏移、字节序(`pkt[0..3]={1B,B1,B1,1B}`)、payload 位于 12 | 证实 |
| 粘包/截断/坏长度/重同步 | 3 帧一次读入→3 payload;按 1000B 任意切分→恰好重组 1 次;截断 20000B→0 输出且 `pending()==20000`;坏 length→下一 magic 恢复 | 证实 |
| 端到端可用 | 真实回环:connect→welcome→stream-start→5 帧(payload 逐字节)→ffc 到达主机回调→stream-stop`RemoteLoopbackTest` 3 项亦全绿 | 证实 |
| 渲染在客户端本地 | `RemoteViewerViewModel`:本地 `RenderPipeline(160,120)`+assets `mag160c.ddt``setPalette` 只调本地管线;`setZoom` 只改状态 | 证实 |
| 主机侧不拖慢相机 | `Channel(capacity=8, DROP_OLDEST)`+`trySend`USB 读线程非阻塞)+单一 `serve()` 协程写 socket | 证实 |
| 默认不启动;开启需活跃 USB 会话 | `setRemoteHostEnabled` 先 `if (!session.isStreaming()) return false`UI 弹"先连接热像仪" | 证实 |
| INTERNET 权限 | badging 已声明 | 证实 |
| 单客户端,后来者拒绝写 busy | `acceptLoop` 内同步 `serve()`,第二连接排队等待,从不写 busy;`isBusyLine` 死代码 | **证伪**(见 FIX-2 |
| 命令写入不交错 | 见 FIX-1:120 次生产序列中 8 次丢 `welcome`;裸 socket 抓到 `hello`/`start` 颠倒 | **证伪**(见 FIX-1 |
| UDP 广播真机可达性 | 仅回环证据 | **无法判定** |
| `ACCESS_NETWORK_STATE` | 全代码无使用点 | 偏差(见 FIX-4) |
### Phase Z`8e1312a`
| 声明 | 方法 | 结论 |
|---|---|---|
| 消息无强制格式 | `git log -1 --format=%B` | 证实 |
| 内容仅为文档/清单/APK | `git show --stat`3 份文档 + APK**外加 Manifest+3 条 uses-feature** | **证伪**(见 FIX-5 |
| 文档更新到位 | HANDOFF §3 含 `ui/remote/`、`net/`、`cloud/CloudApi.kt`session_state 勾选 AZ 并追加总结;execution_plan 顶部加执行状态 | 证实 |
| 全量构建 + 提交 APK,且不 push | 重跑 debug+release+test 成功;`origin/main` 仍 `087e15c` | 证实 |
| 提交的 APK 与 HEAD 一致 | 对已提交 APK 核验:`screenOrientation=1`、camera 系列 not-required、INTERNET/CAMERA 已声明、dex 内含 `画中画`/`正在扫描局域网主机`/`云同步`(E/D/F 代码确在包内)。APK 字节不可复现(时间戳),故以内容核验替代字节比对 | 证实(以内容为准) |
---
## 3. 核查边界:本机无法判定(须真机裁决,未默认通过)
本机(Windows)无模拟器、无连接设备、`adb devices` 为空。以下必须真机判定:
1. USB 出流与温度绝对值标定(清单 1–12 步)。
2. 可见光 PIP 全部运行时行为(13–15 步):**含"返回后画面是否重新出现"**、
单击换档/双击关闭/拖拽三种手势是否互不干扰。
3. 局域网远程预览双机行为(16–24 步):**含 `255.255.255.255` 广播在真实
Wi-Fi 上是否可达**(部分路由/AP 会过滤广播;息屏策略也可能影响收包)。
4. 分析页温度条/探针的实际显示与取温正确性(11 步)。
5. 任何"画面/像素"层面的确认。
清单本身已按指南机械核对:16 条字面命中、30 条骨架命中、7 条人工追溯全部
落实到真实模板与调用点,未发现凭空编造。清单可继续作为真机验证脚本。
---
## 4. 真机测试时请重点确认(与修复决策挂钩)
| 要确认的事 | 怎么看 | 影响哪个修复 |
|---|---|---|
| 远程预览:连上后是否每次都看到"welcome"日志 | B 端日志 `[remote] line: {"type":"welcome"...}` 是否出现;连续重连 10 次统计 | FIX-1(当前约 6.7% 丢失) |
| 快速 stop→start、连点 FFC 是否偶发失灵 | 反复操作,看 B 端画面是否与按钮状态不一致 | FIX-1 |
| PIP:Home 返回后小窗是否重新出现 | 清单 15 步 | FIX-6.1(决定改文档还是改代码) |
| PIP:单击换档 / 双击关闭 / 拖拽是否互不干扰 | 清单 14 步,各做 5 次 | 若互相干扰则需改手势实现(本机无法判定) |
| 分析页:点已知温度位置,读数是否合理 | 清单 11 步 | 探针直接映射的运行时确认 |
| 远程预览:局域网是否能在 10s 内发现主机 | 清单 19 步 | UDP 广播可达性 |
| lifetime 是否非 -1 | 清单第 3 步 `[session] device lifetime=<ms>ms` | FIX-3 |
---
## 5. 核查者使用的判据来源(可独立复核)
| 判据 | 路径 |
|---|---|
| 协议权威(官方 Java | `analysis/sdk_re/android_app/jadx_magcx/cn/com/magnity/magnitycx/sdk/{D2PCmd,P2DCmd,UsbCommunication}.java` |
| 官方调色板 UI 顺序与 native 调用点(**本次新用** | `analysis/sdk_re/android_app/jadx_magcx/cn/com/magnity/magnitycx/DialogFragmentPalette.java` |
| native 伪代码(12+2 个生成体) | `analysis/sdk_re/android_app/libcxsdk_decomp.txt``SetColorPalette @00026c70`case 0..10、0xc、0xd |
| 铁虹真表(真实内存布局 `B,G,R,0` | `csdk/src/mag160c_official_palette256.h` |
| 温度 C 参考实现 + T2E 真表 | `csdk/src/mag160c_render.c:64`、`csdk/src/mag160c_official_t2e.h` |
| 帧布局权威 | `csdk/src/mag160c_frame.c:11-13`(像素自 `+0x1c`,总长 `0x38+len` |
| 官方预览图 | `app/【普通版】MAG-Cx.apk` → `res/mipmap-hdpi-v4/palette_*.png`12 张) |
---
## 6. 复现命令(修复模型可直接照跑)
### 6.1 构建 + 全量单测(Windows Git Bash
```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"
# 强制重跑(否则 UP-TO-DATE 不产生新结果)
cmd //c "C:\Project\MAG160C\android\gradlew.bat :app:testDebugUnitTest --rerun-tasks --no-daemon"
# 单测计数
grep -h -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' \
/c/Project/MAG160C/android/app/build/test-results/testDebugUnitTest/*.xml
```
### 6.2 Phase C 否证复核 + 生成物可复现
```bash
cd /c/Project/MAG160C
"C:\Tools\jdk-21\bin\java" analysis/tools/PalScan.java \
analysis/sdk_re/android_app/bin/libcxsdk.so csdk/src/mag160c_official_palette256.h
# 期望:三种编码全部 not found[A] 段自校准 detected 1 candidate
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"
# 期望:两条 diff 均为空
```
### 6.3 独立 Kotlin harness(仓库外编译仓库真实源码)
无 gcc / 无 kotlinc,用 Gradle 缓存内的编译器:
```bash
KC=/c/Users/zxc/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin
KX=/c/Users/zxc/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx
STDLIB=$KC/kotlin-stdlib/2.2.0/fdfc65fbc42fda253a26f61dac3c0aca335fae96/kotlin-stdlib-2.2.0.jar
COMPILER=$KC/kotlin-compiler-embeddable/2.2.0/8cfa2b049a4006d94474296df4abd9b50f288821/kotlin-compiler-embeddable-2.2.0.jar
SCRIPT=$KC/kotlin-script-runtime/2.2.0/87c92e866fcd68680966a3005a2992e1ab8ec6ad/kotlin-script-runtime-2.2.0.jar
CORO=$KX/kotlinx-coroutines-core-jvm/1.8.0/ac1dc37a30a93150b704022f8d895ee1bd3a36b3/kotlinx-coroutines-core-jvm-1.8.0.jar
ANNOT=/c/Users/zxc/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/23.0.0/8cc20c07506ec18e0834947b84a864bfc094484e/annotations-23.0.0.jar
# 编译(示例:Phase FPhase B 同理,只换成 core/media 源码 + 自己的断言)
java -cp "$COMPILER;$STDLIB;$SCRIPT;$CORO;$ANNOT" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler \
-no-stdlib -cp "$STDLIB;$CORO" -d out \
<DebugLog 桩>.kt \
android/app/src/main/kotlin/com/mag160c/thermal/net/RemoteContract.kt \
android/app/src/main/kotlin/com/mag160c/thermal/net/RemoteClient.kt \
android/app/src/main/kotlin/com/mag160c/thermal/net/RemoteHost.kt \
<自己的断言>.kt
java -cp "out;$STDLIB;$CORO" <主类>Kt
```
要点:
- `DebugLog` 依赖 Android 类型,需自写同包同名的 JVM 桩
`package com.mag160c.thermal.media; object DebugLog { fun log(t: String, m: String) {} }`),
网络类即可在纯 JVM 上运行。
- Windows 版 java **不认** Git Bash 的 `/tmp`,路径必须写 `C:\...`。
### 6.4 其它
```bash
# 清单日志串核对(期望 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
# APK 清单核验
"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
```
---
## 7. 结论一句话
**Phase A/B/C/D/E 的声明全部证实(C 的索引 4/8/9 映射经独立复核成立;
B 的温度换算经独立 C 移植全量比对一致);Phase F 有两处真实缺陷
(命令乱序导致约 6.7% 丢 welcome、busy 未实现),Phase Z 有一处越界
(Manifest 改动);其余为低风险偏差。真机相关项无法判定,等实测。**