Files
MAG160C/docs/android_app/verification_report.md

30 KiB
Raw Permalink Blame History

Phase A→F 独立核查报告(可交给修复模型)

核查者立场:本报告由独立核查 AI 撰写,不采信执行者(下称"执行者")的 总结/session_state/checklist 叙述,全部结论来自 ① git 提交内容 ② 仓库内官方逆向产物 ③ 核查者亲自重跑的命令与亲自编写的独立工具 ④ 明确标注"无法判定"的真机项。

核查范围06c1f30..8e1312a7 个提交),基线 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 顺序=写入顺序", 本缺陷是乱序而非字节交错,加锁修不掉)。改为单写协程 + 队列:

    // 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-151acceptLoop/serve
  • 现象serve(socket) 在 accept 循环内同步执行,第二个客户端只会在内核 backlog 里排队等待第一个断开;主机从不写 busyRemoteContract.isBusyLine()RemoteContract.kt:147)全仓库无调用点,是死代码。
  • 与计划冲突execution_plan.md F2 明确要求"单客户端,后来者拒绝写 busy"。
  • 建议改法(二选一)
    • (a) 实现accept 循环不阻塞在 serve 上,用标志位拒绝后来者:

      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) } }
      

      需在 RemoteContractfun 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-205bb.getInt()==1538635105 后再 getInt(),即 8 字节 [magic][ms]。 另注意 IrSession.readResp 已剥掉前 4 字节 magicr.third去掉 magic 的负载), 故官方布局下 r.first==8r.third.size==4、ms = u32(r.third,0)

  • 建议改法(收紧为官方唯一布局):

    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-featurecameracamera.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. VendorPalettesPalettesTest.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/110rainbow()/highContrast()/ hotMetal()/jet() 已无调用点(Phase C 接入精确表后遗留)。

判定为"可接受、无需修改"的项

  • Phase B 温度条固定未缩放 fitRect:与计划字面一致(imageRect(size,1f,Offset.Zero)), 取舍合理。
  • Phase B 探针标签随缩放/平移AnalyzeViewer.kt:266imageRect(size,zoom,pan)): 与计划"标签随 fitRect 走"字面不符,但实现明显更正确(标签须跟随像素), 判定为实现优于字面规格,建议保留(可在文档注明)。
  • Phase B 分析页直接映射(无 90° 旋转)AnalyzeViewer 画布 aspectRatio(4f/3f)drawImagecanvas.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 xmltreescreenOrientation(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 SUCCESSFULrelease 走完 R8 证实
44 个单测、逐类分布 3/3/5/8/6/4/12/3 --rerun-tasks 强制重跑,逐 XML 汇总 = 44/0/0;每类用例名与指南表格一一对应 证实
RenderPipelineTest.matchesCReferencePixelExact 仍通过 重跑后该用例在列且失败数为 0 证实

2. 逐阶段核查明细

Phase A06c1f30

声明 方法 结论
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 分支先算 cachedlog identical/differ 后仍 return cached 证实
心跳 stats 追加 lifetime IrSession.kt:559 " lifetime=${deviceLifetimeMs}" 证实
checklist 日志串与代码一致 独立重跑 CheckStrings.javaexact=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 readsfirst rendered framefirst run on this hostrequesting permissionpermission result[crash]DebugLog 崩溃钩子)均存在 证实
唯一偏差 lifetime 负载接受两种布局(见 FIX-3) 偏差(低)

Phase B656d419

核查者用 Gradle 缓存内的 Kotlin 编译器(kotlin-compiler-embeddable-2.2.0.jar) 在仓库外搭建独立 harness,直接编译并驱动仓库真实源码 Mdt.kt/TempMath.kt/OfficialTables.kt),不依赖执行者的任何测试代码。

声明 方法 结论
Mdt.parsecompose 互逆 独立构造含 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.ktaspectRatio(4f/3f)drawImagecanvas.rotateBitmap.createBitmap(160,120) 证实(静态)
温度条固定未缩放 fitRect drawTemperatureOsdimageRect(size,1f,Offset.Zero) 证实
探针标签 imageRect(size,zoom,pan),与计划字面"随 fitRect"不同 偏差(实现更优,建议保留,见 §0

Phase Cb7a928e)—— 本轮最需深挖的一相

声明 方法 结论
libcxsdk.so 无静态调色板表 独立重跑 PalScan.java:三种编码(B,G,R,0/B,G,R,0xFF/忽略第 4 字节)全部 not found[A] 段自校准证明扫描非盲 证实
索引↔case 映射 0..10(case 标签即调色板序号) libcxsdk_decomp.txt:456-1180switch(param_2),case 0..10 各一份生成体(另有 case 0xc/case 0xd);② 核查者新找到的决定性证据jadx_magcx/.../DialogFragmentPalette.javamapIndex2Id_ 把 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:24Palettes.kt:47 证实
铁虹仍是官方表 officialIronbow() 直接返回 OfficialTables.PALETTE256_ARGBPalettesTest.ironbowMatchesOfficialTables 通过 证实
生成物可复现 两文件一起编译 → 重新生成:锚点 iron_bow vs OfficialTables anchor: 256/256VendorPalettes.ktpalette_candidates.jsonpalette_match_report.txt diff 全空 证实
红热预览图为占位副本 ImageIO 逐像素:全像素 11777/11914 相同;不透明像素 8536/8673 证实(文档分母表述需修正,见 FIX-6)
extract_palettes.py 本机无 python3,无法运行 无法判定(弱旁证)

Phase Dc34940e

声明 方法 结论
Retrofit 2.11.0 已接入 libs.versions.toml:9,24,25app/build.gradle.kts:52,53 证实
默认关闭 AppSettings.kt:29-38CloudClientTest 4 项全绿 证实
无生产路径静默联网 grep -rn CloudClient android/app/src/main 仅命中声明与 AppSettings.setEnabledmain 无 api() 调用点;api() 未开启即 check() 抛异常 证实
release 混淆不破 亲自跑 assembleReleaseR8)成功 证实
PROGUARD 规则 proguard-rules.pro:5 -keep class com.mag160c.thermal.cloud.** { *; } 证实

Phase Ef8b3200

声明 方法 结论
CAMERA 权限 + 相机可选;usb.host 仍 required aapt2 dump badgingcamera/camera.any/autofocus 均 not-requiredusb.host/screen.portrait required 证实
顶栏第 5 项 + 图标 LiveScreen.kt 5 个 weight(1f) 项,第 5 项 ic_pip+画中画ic_pip.xml 双弧圆描边 + 右下实心矩形 证实
三档 96/128/160dp、高=宽×3/4 PIP_WIDTHS_DPhDp = wDp * 3 / 472/96/120,整除无误差) 证实
右侧留色标条位 PIP_RIGHT_MARGIN_DP = 32,初始 pipXf=1f,pipYf=0f 证实
异常不崩溃 PipCameraView.kt 全部回调 try/catch,失败路径收敛到 release() 证实(静态)
相机释放三处 PipOverlayDisposableEffect onDispose、离页同路径、LiveScreenLifecycleEventObserver(ON_STOP) 证实(静态)
ON_STOP 后能否自动重开 代码侧 released 置位后不复位,重开依赖 onSurfaceTextureAvailable;本机无设备 无法判定
pointerInput 手势并存 Compose 运行时行为无法在本机验证 无法判定

Phase F512508e)—— 2 项证伪

核查者用独立 harness 在 JVM 上驱动真实的 RemoteHost/RemoteSession (仅对 DebugLog 做最小桩),不打桩网络层。

声明 方法 结论
端口/魔数/帧长/超时与计划一致 RemoteContract47510/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-stopRemoteLoopbackTest 3 项亦全绿 证实
渲染在客户端本地 RemoteViewerViewModel:本地 RenderPipeline(160,120)+assets mag160c.ddtsetPalette 只调本地管线;setZoom 只改状态 证实
主机侧不拖慢相机 Channel(capacity=8, DROP_OLDEST)+trySendUSB 读线程非阻塞)+单一 serve() 协程写 socket 证实
默认不启动;开启需活跃 USB 会话 setRemoteHostEnabledif (!session.isStreaming()) return falseUI 弹"先连接热像仪" 证实
INTERNET 权限 badging 已声明 证实
单客户端,后来者拒绝写 busy acceptLoop 内同步 serve(),第二连接排队等待,从不写 busyisBusyLine 死代码 证伪(见 FIX-2
命令写入不交错 见 FIX-1:120 次生产序列中 8 次丢 welcome;裸 socket 抓到 hello/start 颠倒 证伪(见 FIX-1
UDP 广播真机可达性 仅回环证据 无法判定
ACCESS_NETWORK_STATE 全代码无使用点 偏差(见 FIX-4

Phase Z8e1312a

声明 方法 结论
消息无强制格式 git log -1 --format=%B 证实
内容仅为文档/清单/APK git show --stat3 份文档 + APK外加 Manifest+3 条 uses-feature 证伪(见 FIX-5
文档更新到位 HANDOFF §3 含 ui/remote/net/cloud/CloudApi.ktsession_state 勾选 AZ 并追加总结;execution_plan 顶部加执行状态 证实
全量构建 + 提交 APK,且不 push 重跑 debug+release+test 成功;origin/main087e15c 证实
提交的 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.txtSetColorPalette @00026c70case 0..10、0xc、0xd
铁虹真表(真实内存布局 B,G,R,0 csdk/src/mag160c_official_palette256.h
温度 C 参考实现 + T2E 真表 csdk/src/mag160c_render.c:64csdk/src/mag160c_official_t2e.h
帧布局权威 csdk/src/mag160c_frame.c:11-13(像素自 +0x1c,总长 0x38+len
官方预览图 app/【普通版】MAG-Cx.apkres/mipmap-hdpi-v4/palette_*.png12 张)

6. 复现命令(修复模型可直接照跑)

6.1 构建 + 全量单测(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"
# 强制重跑(否则 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 否证复核 + 生成物可复现

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 缓存内的编译器:

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 其它

# 清单日志串核对(期望 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 改动);其余为低风险偏差。真机相关项无法判定,等实测。