Files
MAG160C/docs/android_app/session_state.md
T

1580 lines
109 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.
# MAG160C 安卓统一 APP — 会话状态与心跳锚点
> 本文件是本任务("整合 4 个官方 APP 重写为 Android 16 现代化 APP")的心跳锚点。
> 每完成一个里程碑更新勾选;会话中断先读本文件 + `reverse_apk_features.md`,
> 再按"下一步"继续。约定:不弹询问窗,按计划自主推进,重大分歧点才停下来问。
## 任务目标
1. 逆向 C:\Project\MAG160C\app 4 个官方 APP(普通版/专业版/ThermoScope/demo)→ 已完成
2. 整合全部功能开发一款安卓 APP:适配 Android 16(API 36)、现代 UI、横竖屏、
无流氓文件夹、干爽简洁 → **进行中**
3. 背景问题:专业版在用户 Android 16 手机上花屏(根因见 reverse_apk_features.md §6)
## 已完成的里程碑
- [x] 工具链:jadx 1.5.1(C:\Tools\jadx)、Ghidra 12.1.2(C:\Tools)、IDA MCP 可用
- [x] 4 APK 解包 + jadx 全量反编译(%TEMP%\opencode\apkwork\jadx_*)
- [x] 普通版/专业版/ThermoScope/demo 功能清单 → docs/android_app/reverse_apk_features.md
- [x] IDA:pro libcoresdk.so GetOutputImage/copyBitmap 位图格式证实(ARGB_8888)
- [x] Ghidra:libcxsdk.so 导出 C++ API 清单(CFunctions 族,与 ARM64 coresdk 同源)
- [x] 花屏根因分析(32 位专属库 + targetSdk25 legacy 渲染栈)
## 关键事实速查(开发时直接用)
- USB:VID 0x833C PID 1,EP 0x03/0x82/0x81/0x84,帧 28B 头+38400B+尾,15fps
- 协议/渲染/温度算法全部已逆向:analysis/protocol_spec.md + csdk/(逐像素验证过的官方管线)
- csdk 的 mag160c_render 无平台依赖:USB 帧 → 320×240 RGB24 + 探针测温(毫度)
- DDT 标定文件:可从设备经 0x6BB6B66D/E 下载(官方 prepareProcessImage 同款),仓库有
build-artifacts/mag160c_official.ddt(1.8MB,SN 160043865)
- MDT 文件格式:reverse_apk_features.md §4(152B Tail + 5 段布局 + ROI 256B 记录)
- 温度:毫度 int;MDT 解码走 ConvertResponse2Temperature + T2E(Revise) + CorrectTemperature
## 用户已确认的范围(2026-09-06)
- 云功能:预留 Retrofit 接口模块,默认关闭
- 扫码打标 + 任务巡检(sqlite/xml) + 红外基准图叠加:v1 全部实现
- **网络互连(用户新增需求)**:主机手机插 USB 热像仪,局域网内另一台手机
远程实时预览——UDP 广播自动发现 + 手动添加内网 IP;自定义轻量协议
(控制通道 JSON + 图像流),不走厂商 33596/33597 协议
## 当前阶段
**阶段 2/8 完成:纯Kotlin核心管线移植并逐字节验证(2026-09-06)**
## 已完成里程碑(本任务)
- [x] 阶段1:轻量工具链定案——NDK/CMake/zig 全部卸载(-2.4GB),纯 Kotlin 方案:
渲染管线+温度算法+帧解析从 C 移植为 Kotlin,Gradle 8.14.3 + SDK 已有组件
- [x] 阶段2:项目骨架(android/,AGP 8.11.1 + Kotlin 2.2.0 + Compose BOM,
compileSdk/targetSdk 36,minSdk 26),最小APK可构建
- [x] 阶段2:表数据自动生成 analysis/gen_kotlin_tables.py → OfficialTables.kt
(palette256 ARGB / T2E 646 / T2E274 / E2T_ACC_Q10)
- [x] 阶段2:核心移植 FrameStream / RenderPipeline / TempMath(毫度测温)
- [x] 阶段2:PC端参考工具 analysis/render_offline.c(gcc 编译)+ 差分测试:
**Kotlin 输出与C参考逐字节一致**(60帧序列,首渲染帧32,NUC/LUT/灰度/输出全同)
- [x] 阶段2:FrameStream 分块重组测试 + 温度换算单调性测试全过
## 移植陷阱记录(教训)
- C 无符号32位回绕:乘积/减法必须 u32() 掩码(cdf*denom、0xffc0000-iv7*0x40000、
(v-win_lo)*S 等)
- lutRebuild 的 u12 基址是常量 iv7*0x10+0x10,**不是**链式 u21v;u21v 只用于曲线钳制比较
- Kotlin ByteArray 存 255 = -1:lut[bfmax] != 0xFF 判断必须 (b.toInt() and 0xFF)
- 暖机窗口内 FFC 状态机暂停(warm check 在 ffc_step 之前)——移植时保持顺序
## 用户反馈修复(2026-09-07)
- [x] **中文乱码根因**:部分源码曾被python以GBK重写(AppRoot.kt),编译出的字符串
mojibake。已写 fix_encoding.py 全量转UTF-8 + kotlin.daemon.jvmargs 锁UTF-8 +
dex字节级验证(classes5/9.dex 中"媒体库"UTF-8正确)
- [x] **权限弹窗**:Manifest 加 USB_DEVICE_ATTACHED intent-filter + device_filter
(插热像仪系统自动弹权限/启动);相册页运行时申请 READ_MEDIA_IMAGES
(模拟器实测弹窗正常)
- [x] **图标**:全部重绘为可验证几何图形(双弧圆/直线/矩形),横竖屏截图确认渲染正确
- [x] **专业版三大功能补齐**:多点测温(点击加Pt1..PtN、再点删除)、右侧色标条
(渐变+最高/最低温标注)、最高温追踪开关(追踪·开/关)
- [x] **演示模式**:无设备时用真实管线+DDT渲染合成帧(热块场景),全UI可探索;
demo温度用局部线性显示映射(真机温度走管线数学)
- [x] Android 16.1 模拟器(Medium_Phone AVD)实机验证:中文/图标/布局/横竖屏正常,
预览图在 analysis/preview/preview_*.png
## 用户反馈修复 第二轮(2026-09-07)
- [x] 竖屏时热像图旋转90°竖着显示(填满屏宽);横屏时保持传感器原生4:3方向
填满屏高——两个方向下显示区域都最大化(用户要求的官方行为)
- [x] 横屏:控制栏移到屏幕右侧(竖列),竖屏保持底部横排6项等分——切换时
不再跳位
- [x] 追踪/测温点标记改为大号"圆环+实心点"图标,字高随密度缩放(更大)
- [x] 色标条放大(宽20dp、高0.8视口)+更大刻度字号
- [x] ScreenOrientation=fullSensor:跟随传感器旋转(系统旋转锁定时也转,
与官方普通版一致)
- [x] **SurfaceView旋转存活**:AppRoot/LiveScreen改为稳定单分支布局(导航/控制
栏做成overlay),SurfaceView不再因横竖屏切换被销毁重建(此前横屏花屏空
白的根因)
- [x] 模拟器实测:竖屏/横屏截图确认全部生效(analysis/preview/)
## 用户反馈修复 第三轮(2026-09-07)
- [x] **布局重构**:控制项(FFC/变倍/追踪/调色板/拍照/录像/调色板名)移到**顶部横栏**,
横竖屏都显示;底栏导航(实时/相册/分析/设置)保持原位置(竖屏底部栏/横屏左侧栏)
- [x] **图像内容世界固定**:绘制角度 = (90 - 显示器rotation×90) mod 360,
四向旋转(竖屏/左横/右横/倒竖屏)图像内容都不随手机转,只转图标文字
(修复"向右横屏图像颠倒"问题)
- [x] **状态栏沉浸**:insetsController 隐藏状态栏(下滑临时显示),底栏导航仍可用
- [x] **inset感知渲染**:渲染器 viewport = 全屏减顶栏/底栏inset(AppRoot横屏时
导航栏宽度不再误算为底部inset)
- [x] 顶栏通过 onSizeChanged 上报 vm.uiTopPx;导航 overlay 高度经 UiInsets 单例
同步 vm.uiBottomPx
- [x] **(重要教训)python 裸写文件会用 GBK**:所有 python 编辑必须显式
encoding='utf-8';本次 LiveScreen.kt 双重乱码已重写恢复,dex 验证 CJK 正常
- [x] 模拟器验证:顶栏/左导航/图像/色标/标记布局正确,中文正常,状态栏隐藏生效
## 用户反馈修复 第四轮(2026-09-07,最终布局约定)
- [x] **顶栏/底栏位置固定**:顶栏=控制项(FFC/变倍/追踪/调色板/拍照/录像)恒在顶部;
底栏=导航恒在底部。横竖屏都不移动(**取消横屏左栏**),图标文字跟随屏幕方向(恒可读)
- [x] **图像区域永不旋转**:图像恒 90°CW 绘制为 3:4 竖向,填满顶栏~底栏之间可用区,
手机怎么转都不动;只转标记文字(文字恒屏幕水平,位置经 probeToScreen 固定90°映射)
- [x] **顶栏适配挖孔屏**:`WindowInsets.safeDrawing.only(Top)` 顶部安全区 padding
- [x] 去掉 display-rotation 依赖(uiRotation/drawRotationDeg 已删,tapImage/probeToScreen
用固定 90° 映射)
- [x] 完整交接文档:docs/android_app/HANDOFF_DEVELOPMENT.md(含给新模型的读取提示词)
## 用户反馈修复 第五轮(2026-09-07,布局约定最终定稿:构图钉死竖屏框架)
- [x] **用户澄清**:"顶栏底栏位置不变"指**绝对位置**——永远贴着手机竖屏的物理顶边
(挖孔侧)与物理底边;热像区域也**完全不动**。不是"横屏后显示区域跟着屏幕转":
那样屏显方向会和镜头实际方向对不上。
- [x] **根因与修复**:此前 fullSensor 下渲染"相对当前屏幕固定 90°CW",手机横过来时
屏幕本身转 90°,整个构图相对手机框架也转 90° → 与镜头方向脱节。修复 =
**Activity 锁定竖屏**(manifest `screenOrientation="portrait"`):屏幕相对手机框架
永不旋转,构图(顶栏/热像区/底栏)永远钉在竖屏框架上;手机怎么物理旋转都一样。
- [x] 顺带清理:GalleryScreen 去掉 isLandscape() 死逻辑(恒 3 列);
AppRoot/LiveScreen/LiveRenderer/UiInsets 注释同步更新。
- [x] 模拟器四方向实测(0/90/180/270°,`adb emu rotate`):**四个方向截图逐字节一致**
(MD5 相同)——构图绝对固定;演示画面/顶栏/底栏/色标/标记/中文均正常
(analysis/preview/preview_rot*_v5.png)。
- [x] 单元测试全过;APK 已更新 build-artifacts/mag160c-app-debug.apk。
- 注:不要再改回 fullSensor/sensor 横屏;横竖屏适配类需求一律以"竖屏构图恒定"为准。
## 用户反馈修复 第六轮(2026-09-07,横屏持机图标文字可读)
- [x] **用户反馈**:竖屏锁定后,横过来拿手机时顶栏/底栏里的图标和文字不旋转(侧着)。
约定补全:**构图钉死竖屏框架不变**(条栏绝对位置+热像区域不动),但条栏内容
(图标+文字)与 OSD 文字要按**物理持机朝向**补偿旋转,保持可读。
- [x] 新增 `ui/DeviceOrientation.kt`:加速度计→手机相对竖屏的顺时针物理转角
φ∈{0,90,180,270}(主轴判定+2.5m/s² 滞回;竖屏锁定下 Display.rotation 恒 0 不可用)。
- [x] 顶栏 7 控制项/底部导航 4 项:`graphicsLayer rotationZ=-φ` 原位预旋转(布局不动)。
- [x] 渲染器 OSD 文字(中心温/探针标注/色标最高最低数字)`canvas.rotate(-φ)` 绕锚点
旋转;标记圆点、色标条几何仍钉死在图像上。
- [x] 模拟器实测(`adb emu sensor set acceleration` 驱动 4 姿态):四姿态下热像区域
完全一致,图标/文字按姿态正确补偿(analysis/preview/preview_pose*_v6.png);
姿态复位 0° 后输出与第五轮逐字节一致(MD5 相同)。
- [x] 单元测试全过;APK 已更新。
- 注:对话框(调色板等)与其他页签内容未做补偿旋转(保持竖屏可读),如需再加。
## 用户反馈修复 第七轮(2026-09-07,真机朝向全反修复)
- [x] **用户反馈**:真机正持竖屏时屏幕内容倒立,其他方向也全反。
- [x] **根因**:第六轮把加速度计符号约定写反。Android 真机 TYPE_ACCELEROMETER
静止读数 = "加速度减重力",指向世界上方向(平放屏幕朝上 z=+9.81、竖屏正持
y=+9.81);模拟器虚拟传感器却是重力向量约定(正持 y=-9.81),故模拟器测试
"通过"而真机全反(所有姿态差 180°)。
- [x] **修复**:DeviceOrientation 映射改为 (0,+g)=0、(-g,0)=90、(0,-g)=180、(+g,0)=270;
代码注释明确标注"勿按模拟器默认值改回"。模拟器用反号值复测:姿态 0 恢复正常
正持显示,90/180/270 补偿几何不变(preview_fix_pose*_v7.png)。
- [x] 单元测试全过;APK 已更新。
## 用户反馈修复 第八轮(2026-09-07,相机式按键布局)
- [x] **顶栏精简为 4 个相机控制项**(左→右,带小字标签,随持机朝向补偿旋转):
FFC 校正 / 数码变倍(显示当前 1×/2×/4×,点击循环)/ 最高温追踪开关(追踪·开,
高亮+新 ic_target 准星图标)/ 调色板(显示当前调色板名,点击弹出选择)。
- [x] **底部新增相机快门区**(仅实时页,位于底导航上方,白/红色调):
相册快捷入口(跳转媒体库页签)/ 大圆形快门=拍照 / 录像-停止(红圈,录像中
红圈内容变红色方块)。快门区高度并入渲染器底部 inset(vm.uiBottomPx =
导航高+快门区高),测温点点击映射同步排除该区域。
- [x] 底部导航(实时/相册/分析/设置)不变;快门区内容同样按持机朝向补偿旋转。
- [x] 模拟器实测:0°/90° 姿态布局、旋转补偿、图像区域均正确
(preview_ui_v8.png / preview_ui90_v8.png);单元测试全过;APK 已更新。
## 用户反馈修复 第九轮(2026-09-07,真机热像顶端被顶栏遮挡)
- [x] **用户反馈**:真机上快门区把热像显示区顶满后,热像最顶端被顶栏盖住看不见。
- [x] **根因**:顶栏 `onSizeChanged` 挂在 `safeDrawing` 挖孔 inset 内侧,`uiTopPx`
不含挖孔安全区高度;模拟器无挖孔看不出来。快门区加入后剩余高度变小、热像
进入"高度受限"铺满视口状态,顶端误差直接表现为热像顶端插入顶栏背后。
- [x] **修复**:`onSizeChanged` 移到 `windowInsetsPadding` 之前(`uiTopPx` = 挖孔
安全区 + 顶栏全高,渲染器视口完整跳过顶栏);同时按要求缩小快门区
(相册/录像 42dp、快门 60dp、纵向 padding 6dp、间距 44dp)。
- [x] 模拟器复测布局正常,单元测试全过;APK 已更新。
## 用户反馈修复 第十轮(2026-09-09,撤下演示热像图)
- [x] **用户要求**:撤下用于测试的合成热像图(演示模式),准备接真机+热像仪实测显示。
- [x] LiveViewModel:`connect()` 无设备时不再启动演示渲染,改为 `status="no_device"`
(LiveScreen 既有占位文案"未检测到热像仪,请插入MAG160C",渲染器无帧即黑底);
删除 startDemo/buildDemoFrames/demoPipeline/demoNuc 与 refreshTemps 演示分支
(连带消除"演示循环与真机流同时写 latestFrame、演示温度覆盖真机温度"的隐患)。
勿把演示模式加回来。
- [x] 工具链在本机重装并重建 APK(2026-09-09):JDK Temurin 21 `C:\Tools\jdk-21`、
SDK `C:\Tools\android-sdk`(platform 36 / build-tools 36.0.0 / platform-tools,许可已接受)、
Gradle 用工程 wrapper(8.14.3);交接文档旧路径 C:\Tools\gradle-8.14.3 / Zulu /
AppData SDK 属另一台开发机已失效(HANDOFF §2/§5 已改为本机路径)。
assembleDebug+test 全过;dex 抽查中文 UTF-8 正常、无 startDemo 残留;
build-artifacts/mag160c-app-debug.apk 已更新(11.8MB),可直接装真机。
- 待办不变(真机 USB 实测温度标定、网络互连远程预览、离线MDT温度解码、
调色板精确提取、PIP/云模块)。
## 用户反馈修复 第十一轮(2026-09-09,真机黑屏:日志落盘 + 修复缺失 START)
- [x] **用户反馈**:真机插热像仪有 USB 弹窗,但实时画面黑屏;要求把 debug 日志
保存到热成像相册目录,运行后把文件发回分析。
- [x] **黑屏根因(代码审查定位)**:`IrSession.start` 移植时漏掉了硬件已验证的
启动序列(csdk `mag160c_ir.c`):读循环前必须 FFC(0)×2 → 300ms → START(73);
旧代码只发 66b/66c/66f 就进读循环,相机从未出流 → EP 0x81 静默 → 黑屏。
已按 C 参考补上(50ms → FFC(0)×2 → 300ms → START)。
- [x] **新增 `media/DebugLog.kt`**:每次 connect() 在 MediaStore 建一个
`debug_yyyyMMdd_HHmmss.log`(DCIM/MAG160C,text/plain,API29+ 无需权限,
相册/文件管理器可见),逐行 flush;同时镜像到 logcat(tag `MAG160C/*`);
MainActivity 装崩溃钩子(栈回写文件后再交前 handler)。
- [x] **全链路插桩**:UsbTransport(设备列表/权限结果/openDevice/claim/端点表)、
IrSession(命令交换 write/read/响应 magic+头16字节、首3次读头24字节、
前5帧 type/shutter、每2s 心跳 stats reads/frames/rendered/timeouts/fps/
renderState/ref、STOP)、LiveViewModel(connect/权限/首帧到达/5s 心跳)。
- [x] 状态文案补全:open_fail/no_endpoints/exception:* 在 LiveScreen 显示
具体提示(原先一律"连接中…",掩盖故障)。
- [x] 构建+单测全过;dex 抽查中文/日志器/启动序列字符串正常;APK 已更新
(11.87MB)。调试日志要点:若仍黑屏,看文件里 `stats: reads=?` ——
reads=0 → START 被无视(查 cmd 日志);frames>0 rendered=0 →
warm/FFC/ref 窗口(ref=false 持续 → 66c 响应异常);rendered>0 仍黑屏 →
UI/渲染层问题(hb: uiFrames=?)。
- 注:调试日志文件由 MediaStore Files(非 Images)写入,部分相册 app 不显示
text/plain,可用系统"文件"应用或 PC 复制 DCIM/MAG160C/debug_*.log。
## 用户反馈修复 第十二轮(2026-09-10,打开即闪退:日志落点修复)
- [x] **用户反馈**:第 11 轮 APK 装好后直接打开就闪退。
- [x] **根因**:`DebugLog.startFile` 用 `MediaStore.Files` 往 DCIM/MAG160C 插
text/plain 文件;scoped storage 规定 DCIM 只收图片/视频,非媒体文件被拒,
`insert()` 抛 IllegalArgumentException,而该调用在 `LaunchedEffect →
connect()` 协程里无 try/catch → 启动即崩。
- [x] **修复**:DebugLog 全链路 try/catch 永不抛异常;落点三级回退
DCIM/MAG160C → Download/MAG160C(非媒体允许目录)→ 应用私有外部目录;
文件头写实际落点 `sink=`;日志 4MB 封顶。`startFile` 提前到
MainActivity.onCreate(启动崩溃也有记录);`connect()`/权限回调整体
try/catch,失败置 `connect_fail`(statusText 显示"连接流程异常")。
- [x] 构建+单测全过,dex 抽查通过;APK 已更新(11.87MB)。
- 注:取日志时 DCIM/MAG160C 和 Download/MAG160C 都看一眼(文件头 sink= 注明
实际位置);若在前者失败会自动落后者。
## 用户反馈修复 第十三轮(2026-09-10,真机日志分析:端点静默 + 重枚举)
- [x] **用户回传 debug 日志**(vivo V2509A, Android16):相机识别/DDT/66b 响应全正常
(SN 160043865),但 66c/66f 响应超时、FFC/START 第一轮写失败(write=-1,
端点疑似 halt)、流端点 19 秒 0 字节;00:51:10 相机重新枚举(权限再次弹出)
——相机在会话中途疑似断电/复位;且新旧两个 Activity 的会话同时在抢同一设备
(claimInterface 互相夺走)。
- [x] **IrSession 加固**:流端点改 **UsbRequest 异步**(API30+,position=字节数;
低版本回退同步 bulkTransfer);任何 transfer 失败先 GET_STATUS 诊断 +
**CLEAR_FEATURE(HALT)** 再重试一次;66b/66c/66f 响应读取缩短为 400ms 且
失败不阻断(与 C 参考一致);**会话互斥**(companion active,新会话先停旧
会话);stop() 时序修复(STOP 在 close 之前发,由 streamLoop 收尾)。
- [x] **LiveViewModel**:注册 USB **DETACHED** 接收器(VID 匹配→停会话+no_device);
connect() 800ms 去抖;流 10 秒 0 数据 notify `no_stream_data`
(LiveScreen 屏显"已连接但无数据流")。
- [x] 构建+单测全过;APK 已更新(11.87MB)。
- ⚠️ **下一步排查(若仍无流)**:相机中途重枚举强烈怀疑 **OTG 供电不足**
(vivo 口限流 → 相机 MCU 帧处理起来后掉电复位)。请用户:① 换一根好点的
短线/带供电的 OTG 转接器再试;② 在同一台手机装官方普通版 MAG-Cx 对照
(若官方也掉,就是供电/兼容问题,与我们的代码无关);③ 新日志看
`usb detached` 与 `get_status halted=` 行。
## 用户反馈修复 第十四轮(2026-09-10,端点 halt 真凶确认:usbfs 超时遗留)
- [x] **用户回传第二轮日志 + 关键信息"官方软件正常"**:每次 clear_halt 都成功
(rc=0)但下一次读仍 -1 且 halted=1 → 不是相机 STALL,而是 **Linux/Android
usbfs 在 bulkTransfer 超时后把端点标记 halted,后续传输全部瞬间失败**。
流端点首次 500ms 读超时(相机 1~2s 才开始出流)→ 0x81 被标 halted →
之后 19 秒的读全是瞬间失败,帧全被错过。66c/66f 响应读不到同机理
(C 参考在 PC 上也忽略这些失败,完全吻合)。官方 App 正常 → 排除供电问题。
- [x] **修复(核心一行)**:流循环每次 -1 后**无条件 GET_STATUS + CLEAR_HALT**
(节流日志),端点永远保持可用;相机 1~2 秒启动延迟期间的若干次超时
不再杀死数据流。去掉异步 UsbRequest(usbfs 超时对 async 同样留 halt,
无收益);5 秒零数据自动重发一次 START 兜底;流内 FFC 响应读缩短为
400ms(不再阻塞渲染线程 2 秒)。
- [x] 第 13 轮的会话互斥/DETACHED 恢复/去抖已验证生效(日志见 "stopping
stale previous session")。
- [x] 构建+单测全过;APK 已更新(11.87MB)。
- 预期:本轮装上后,日志应出现 `first reads [0] n=...` 与递增的 frames/rendered,
画面出图。若 timeouts 持续上涨且 reads=0,再看 5s re-kick 与 halted 状态。
## 用户反馈修复 第十五轮(2026-09-10,BREAKTHROUGH:逆向官方 libcoresdk 找到缺失握手)
- [x] **用户回传第三轮日志**:clear_halt 修复已生效(0x81 每 500ms 健康轮询 25s
无遗漏),但相机确实 0 字节;66b 仅在相机上电后响应一次,随后对一切命令
沉默,~27s 后相机掉线重启。**官方 App 在同一手机正常** → 排除供电/硬件。
- [x] **逆向 analysis/ida/export 的 libcoresdk(arm64) 反编译(官方在用的库)**:
- `CNetComm` 命令层全貌:66f=ReadCaliInfo(响应 0x5BB5B55E pair={u64 标定
文件大小, u64 版本});**670=GetCaliFile**(响应带大小 → 从 **EP 0x84**
按 ≤512KB 块、60s 超时读回标定文件;日志串 "First running on new host,
it will cost some time for initializing...");672=FFC;673=START;
674=STOP;676/677=省电参数(AutoPowerSave,非图像传输)。
- **官方安卓连接序列根本不发 66b/66c**(66b 是 PC libmagcore 旧演示遗留),
也不发预启动 FFC;序列 = 66f →(缓存缺失)670+0x84 拉取 → 读线程 →
50ms → START。FFC 由帧驱动(pipeline onFfc)。
- 推断:我们一直发的 66b 把该固件带入旧式握手模式并卡死(66b 有响应、
之后全哑、看门狗 ~25s 重启)——与全部日志吻合。
- [x] **IrSession 按官方序列重写连接**:66f(1000ms 读,解析 pair)→ 缓存
`files/cali/magcore.cali.<version>` 命中则直接用 → 否则 670 + EP 0x84
拉取(60s/块,进度日志,成功后写缓存)→ loadDdt(拉取文件优先,失败回退
内置 DDT)→ 50ms → START。删除 66b/66c/预启动 FFC×2;首帧后补发一次
66b 拿 info 块(MDT 用)。UsbTransport 加 endpointByAddress()。
- [x] 构建+单测全过;APK 已更新(11.87MB)。
- 预期日志特征:`66f cali-info ok` + `cali pair: size=1856416...` →
`first run on this host: fetching cali file...` → `cali fetched+cached` →
`first reads [0] n=...` → frames 递增 → 出图。
## 用户反馈修复 第十六轮(2026-09-10,反编译官方 MAG-Cx Java 源码,按其逐行重写 USB 层)
- [x] **jadx 反编译官方普通版**(C:\Tools\jadx-1.5.1,产物 C:\Tools\jadx-out):
`sdk/UsbCommunication.java` 就是能在这台手机上跑通的完整 USB 协议 Java 实现。
- [x] **官方真实协议(P2DCmd/D2PCmd 常量全表)**:
- P2D:66b=GetParameter1、66c=GetParameter2、66f=GetCaliInfo、670=GetCaliFile、
671=SendCaliFile、672=SetShutterState(FFC)、673=StartTransferImg、
674=StopTransferImg、675=GetLifeTime、676=SetLaserState、677=PowerSave、
679=SetFrameRate;全部 4 字节小端(intToByteArray LE,排除字节序假设)。
- D2P 响应:0x5BB5B55B=BasePara1(serial/hw+devType/sw/宽/高/fps/gain/flip/
interFrame/interLine/gfid/gsk,14 int=56B+4=60B,与日志完全吻合)、
0x5BB5B55C=BasePara2、0x5BB5B55D=SendCaliFile ack、0x5BB5B55E={i32 size,
i32 reserved, i64 date}、0x5BB5B561=lifetime。
- [x] **官方连接序列(UsbCommunication.connect + startTransfer)**:
1) 66b(**不读 ack**,waitAck 对 GetParameter1/2/GetCaliInfo 是 default=true)
→ recvCmd 64B/800ms → BasePara1;
2) 66c → 64B/800ms → BasePara2;
3) 66f → 64B/800ms → CaliInfo{size,reserved,date};
4) 缓存 `{caliDir}/{productType}.{serial}.{date}`(devType3=core160)命中→
直接 startTransfer;未命中→启动 ThreadCaliRecv(0x84、16KB 块、800ms/读、
无数据 5s 放弃)+ 发 670(waitForCmdAck 读 ack)→ 收满后 startTransfer;
5) startTransfer:native startProcess(…, caliPath)(= loadDdt 拉到的文件)
→ setEX/ExtPara/放大倍率(native,非 USB)→ 起线程 → 5ms → 673(读 ack)。
- **TIMEOUT=800ms 全线统一**(我们此前的 400ms 是响应缺失的直接嫌疑);
- 图像帧尾校验 drop=type 0/1;FFC 触发=按 GetLifeTime 开机时间表(devType3
走 imageStableCounter 分支)+ 双击/消息触发,均为 672。
- [x] **IrSession 按上述逐行重写**(保留:会话互斥、CLEAR_HALT 恢复、DETACHED、
no_stream_data 提示;去掉:5s re-kick、延迟 66b)。BasePara1 解析出 identity
(serial/devType/宽/高/fps)+ lastInfo0;BasePara2 存 lastInfo1(MDT 用)。
no_handshake 状态屏显"相机无应答,请拔插重试"。
- [x] 构建+单测全过;APK 已更新(11.87MB)。
- 预期日志:GetParameter1 resp=0x5BB5B55B len=60 → GetParameter2 → GetCaliInfo
(cali size=…date=…)→ 下载(首次)或 cache hit → frames 递增。
## 用户反馈修复 第十七轮(2026-09-10,根因确认:命令字节序反转)
- [x] **第 16 轮日志**:官方序列下 GetParameter1 仍唯一有响应(60B BasePara1 完整
解析成功:serial=160043865 devType=3=core160 160x120@15fps),66c/66f/673 全部
800ms 超时 → 与"字节内容"相关而非流程顺序。
- [x] **根因**:`MagProtocol.cmd4/cmd8` 用 `ByteBuffer.putInt` —— **默认大端**!
官方 `GlobalFunc.intToByteArray` 是小端。66b=0x6BB6B66B 是回文数,大小端
字节相同,所以 16 轮调试里它是唯一能被相机识别的命令,完美掩盖了 bug;
其余命令(66c/66f/670/672/673/674)发出去的字节全是反的 → 相机不应答、
收到垃圾触发看门狗重启。PC 的 C 参考(mag160c_ir.c)是手工小端打包,所以
PC 上一直正常。
- [x] **修复**:cmd4 改手工小端打包(与官方 intToByteArray 逐字节一致);
cmd8 = cmd4(magic)+cmd4(param)。新增 MagProtocolTest 锁死
GetParameter2/StartTransferImg/SetShutterState 的线缆字节序。
- [x] 构建+单测全过;APK 已更新。提交 623d62b。
- [x] **按用户要求完成全量解包复核**(产物入库 analysis/):
- `analysis/sdk_re/android_app/jadx_magcx/`:官方普通版全量 Java 源码(64 文件);
- `analysis/sdk_re/android_app/libcxsdk_decomp.txt`:Ghidra 11.3.2 全量
反编译 libcxsdk.so(1290 函数);
- `analysis/magcx_official_flow.md`:权威协议结论(命令表/BasePara/CaliInfo/
连接序列/FFC 逻辑/工具重跑命令)。
- native 复核:`Controller::StartProcess` 必须 `LoadCalibrationTable(caliPath)`
(官方 startProcess 失败会删缓存文件重下)→ 我们的 loadDdt 等价正确;
`Controller::PushFrame` 输入=整帧缓冲(28B头+像素+28B尾),与本管线一致。
- 预期:66b/66c/66f 全部有响应 → cali 下载(首次)→ 流出帧。
## 待办(2026-09-10 更新:执行计划 A→F 已全部完成)
- **真机实测**(唯一未完成的主线):照 `docs/android_app/real_device_checklist.md`
24 步走一遍——1-12 单机(USB 出流/FFC/拍照/录像/媒体库/分析页温度条/PDF)、
13-15 可见光 PIP、16-24 双机远程预览。每步都有预期 DebugLog 原文可对照。
- 红热调色板(索引 11)精确表:官方 APK 预览图为占位副本,需真机抓帧反推。
- 云端点对接:当前为占位接口,需账号与接口文档。
- 任务巡检 UI(TaskParser 已就绪,尚无界面)——沿自早期计划,未列入本轮范围。
- 旧的"网络互连/离线MDT解码/调色板提取/PIP/云模块"五项**已在本轮全部实现**
(见下方阶段表),此处不再重复。
## 执行计划进度(2026-09-10,docs/android_app/execution_plan.md)
- [x] Phase A(2026-09-10):GetLifeTime(675→0x5BB5B561) 查询 + deviceLifetimeMs;
cali 缓存与内置 DDT MD5 一致性日志;新增 real_device_checklist.md;
gradlew test 全绿,APK 已更新。commit: "android: lifetime query + cali consistency check + real-device checklist"
- [x] Phase B(2026-09-10):Mdt.parse 返回 MdtFile(jpg 按 FFD9 裁尾、text 去 NUL
填充、framePixels/info0/info1 齐全);TempMath.tempMapFromPixels 毫度图;
分析页温度条(中心/最低/最高)+ 点击测温探针(白点+环+温度标签,再点清除);
新增 MdtTest/TempMathTest(17 个单测全绿)。注:分析页图像为原生横向显示,
故探针映射用直接映射(不是实时页的 90° 逆映射)。APK 已更新。
- [x] Phase C(2026-09-10):**结论:libcxsdk.so 无静态调色板表**,12 调色板由
CFunctions::SetColorPalette 运行时算术生成(全文件扫描 alpha=0xFF 的
256-run 命中 0)。改走"移植生成函数"路线:case 2 与 OfficialTables
PALETTE256_ARGB **256/256 逐字节一致**(锚点锁死算术+字节序),
每 case 恰好写满 256 槽;官方 APK 预览图覆盖率自身恒为最高。
**UI 索引 0..10 共 11 个调色板已换成官方精确表**(VendorPalettes.kt,
Palettes.kt 接线);索引 11 红热未解决(官方预览图是白热的占位副本、
case12/13 对任何预览覆盖率≤6%)→ 保留近似并注明。
新增 PalettesTest(8 项);产物见 palette_extraction_findings.md。
APK 已更新。
- [x] Phase D(2026-09-10):云模块脚手架(Retrofit 2.11.0 opt-in,默认关闭):
cloud/CloudApi.kt(接口+DATA class,CloudClient.api() 在未开启时直接
check() 抛异常——绝不静默联网);AppSettings.cloudEnabled(默认 false,
构造时同步 CloudClient);设置页"语言"与"关于"之间新增"云同步"行 +
说明对话框("当前版本仅预留接口,不会发起任何网络请求");
proguard-rules.pro 新建(-keep cloud.**;原文件缺失但被 build.gradle
引用)。新增 CloudClientTest(4 项,锁"默认关闭/api() 拒绝"契约)。
debug + release(R8) 双构建通过,29 单测全绿。APK 已更新。
- [x] Phase E(2026-09-10):可见光 PIP 融合:Manifest 加 CAMERA 权限 +
camera.any uses-feature(orientation 未动);LiveState 加
pipOn/pipSizeIndex/pipXf/pipYf + togglePip/cyclePipSize/setPipPos;
新增 ui/live/PipCameraView.kt(Camera2 最小实现,TextureView 预览,
全部异常只记 DebugLog("pip",…) 并 release,绝不崩溃);顶栏第 5 项
"画中画"+ ic_pip.xml(双弧圆+右下实心矩形);浮层三档 96/128/160dp
(高=宽×3/4),位置相对热像视口、右边缘额外留 32dp 色标条位,
拖动/单击换档/双击关闭;PIP 关闭、离页、ON_STOP 三处释放相机;
运行时权限用 rememberLauncherForActivityResult。真机项已写入
real_device_checklist.md(第 13-15 步 + 失败表现)。APK 已更新。
- [x] Phase F(2026-09-10):网络互连远程预览。net/RemoteContract.kt(协议常量+
手写扁平 JSON 助手,**无 Android 依赖**便于 JVM 单测 + FramePacketReader
流式重组);net/RemoteHost.kt(UDP 47510 每秒广播 + TCP 47511 单客户端;
**单一协程拥有 socket**,控制回复与帧记录不会交错;帧队列 DROP_OLDEST 不拖慢
相机);net/RemoteClient.kt(discover 去重 3s;connect 行模式→收到
stream-start 后切帧模式,同一 chunk 内混合也能正确切分;10s 无数据判死);
IrSession 加 rawHook(与 recorderHook 并列);LiveViewModel 接 rawHook →
remoteHost.offerFrame + setRemoteHostEnabled(需活跃 USB 会话);
设置页新增"远程预览服务端"/"远程预览客户端"两行(前置校验弹"先连接热像仪");
ui/remote/ 三文件:RemoteClientListScreen(56dp 标题栏/64dp 卡片/重新扫描/
手动添加/空态)、RemoteViewerViewModel(本地 RenderPipeline + 内置 DDT
渲染)、RemoteViewerScreen + RemoteRendererHost(与 LiveRenderer 同构图,
未改冻结文件)、AppRoot 加两个全屏目的地。Manifest 加 INTERNET。
单测 44 个全绿,含 **loopback 端到端测试**(真实 TCP:hello→welcome→
start→stream-start→5 帧逐字节往返→ffc→stop→stream-stop)与粘包/截断/
坏长度/重同步用例。debug+release 双构建通过。真机双机步骤写入清单第 16-24 步。
- [x] Phase Z(2026-09-10):收尾完成。docs 更新(本文件 + HANDOFF_DEVELOPMENT
§3 结构表加入 net/ cloud/ ui/remote/ 与 VendorPalettes/PipCameraView,
§7 状态与待办重写 + 禁改清单);全量 `assembleDebug test`(44 单测全绿)
+ `assembleRelease`(R8)通过;APK 已覆盖提交
(build-artifacts/mag160c-app-debug.apk,12.66MB,md5 76ff4e12…);
dex 抽查 10 个新增中文串 UTF-8 正确、无 GBK 乱码;APK 校验:
screenOrientation=portrait 保持、camera/camera.any 均为 required=false
(CAMERA 权限原本会让相机变必需,已显式声明为可选)。
**未 push**(按用户指令)。
## 用户反馈修复 第十九轮(2026-09-11,第二轮真机:相册/录像/分析/设置持久化)
用户第二轮实机测试报出以下问题,本轮全部处理:
- [x] **录像停止即闪退**(严重)。两处根因:
① `Mp4Recorder.offerFrame` 先取 `inputSurface` 再判断,而 `stop()` 会 release
它——采集线程随后 `lockCanvas` 在已释放 Surface 上抛异常,**该异常发生在
USB 读线程且无人捕获 → 进程崩溃**。现改为所有 surface/encoder 访问同锁,
且 `offerFrame` 整体 try/catch 吞掉编码层异常(丢帧优于崩溃)。
② **录完的文件从未保存**(只改了状态文案,相册里什么都没有)。现新增
`MediaStore.Video` 保存(`PhotoSaver.saveVideo`)+ 临时文件清理,
状态区分 `rec_done`/`rec_save_fail`/`rec_fail`。
- [x] **相册点照片打不开**:`AnalyzeViewer` 被放在 `fillMaxSize()` 的 Column
**之后**,布局到屏幕外,所以点击像"没反应"。改为 `Box` 内**覆盖层**,
并加 `BackHandler` 让返回键关闭查看器。
- [x] **远程预览列表按返回键直接退出软件**:缺 `BackHandler`。列表页与查看页
均补上(列表→返回设置;查看页→断开并回列表)。
- [x] **设置不生效/不持久**(用户:"每次重开设置就变回默认"):根因是
**默认调色板/追踪模式从未被实时页读取**(只有方向设置接了)。
`AppSettings` 全部相关 setter 现在都 publish 到 `ImageOrientationSettings`,
`init` 用持久化值播种;实时页 collect 后即时应用调色板+追踪模式。
(`defaultEmissivityPercent`/`alarmTempC` 仍未被管线使用——见"诚实记录"。)
- [x] **照片要按设置竖直翻转/旋转后再保存**:新增
`PhotoSaver.encodeRendered(frame, orientation, probes)`,拍照时按**与屏幕
一致**的方向(翻转→旋转)生成 JPEG,不再保存传感器原始朝向。
- [x] **照片上烧录测温点+温度**:拍照时把探针(含 `Pt* 温度` 标签)绘到 JPEG 上。
- [x] **MDT 新增探针数据块**(`0x5BB5B55F`,UTF-8 文本行 `x,y,label,tempMc`),
分析页可**重新载入**原有测温点并可编辑。
- [x] **分析页改造**(按用户要求):只显示保存的原始渲染图(不再按调色板重渲染,
避免"改调色板看起来照片被改了");点击图像添加测温点、再点删除;
"保存"生成**新照片**(标注烧录、探针入库),原文件不动;
最高/最低/中心温度与测温点列表显示在**侧边栏**。
- [x] **追踪模式设置项**:设置页新增"追踪"(最高温/最低温/最高+最低/关闭),
顶栏按钮显示当前模式(追高/追低/追高·低/追踪),录入 `TraceMode`。
- [x] **操作反馈**:拍照/录像完成后在实时页显示 2.5 秒提示(此前这些状态只在
"未连接"分支显示,正常出图时用户看不到任何反馈)。
- [x] 单测 66 → **76 项全绿**;debug + release(R8) 双构建通过;APK 已更新。
## 用户反馈修复 第三十九轮(2026-10-07 凌晨,"官方背景条纹更不明显"结案:GPU 画布假设被真机 A/B 推翻,真因 = 我们开着官方从不开启的 7×7 细节增强)
按 38 轮交接清单执行(首验软件画布 → 正式修复 → A/B 验收 → NUC 域收尾)。
完整数据与口径见 `rendering_diff_phone_20261006.md` §17.7。测量脚本
`%TEMP%\mag160c_re\final39*.py`(38 轮存档数字全部复现:官方 FULL 0.10/0.28、
暗区 0.15、照片 JPEG 0.25——口径互锁)。
- [x] **首验(软件画布)否定 GPU 画布假设**:`useHardwareCanvas=false` 实测
fps 11.6-12.4(avg 84.6 ms,15 fps 不达标,33 轮历史重演);同场景 A/B
暗区 ROW 软件 0.49/0.28/0.19/0.33 vs 硬件 0.46/0.31/0.32/0.36——**画布
类型无显示差**。38 轮"暗区 ROW 2.07"无脚本存档、所有口径均无法复现
(≤0.47),随方向性结论一起作废。
- [x] **备选 a(Skia 720×960 预放大 + 硬件整数 2× blit)无收益**:fps 达标
(15.1 paints,avg 31.9 ms)但暗区 ROW 与单级无差——已回退。SF 采样比
假设同时否定。
- [x] **App 内位图 dump 定位(决定性)**:同帧三方——管线 RGB 输出 DARK ROW
**0.35**、Skia 6× 后 0.21、同秒截屏 0.27、离线复刻 0.16。合成/截屏不添加;
差在管线输出(vs 复刻)。真机 settings 实测 **enhanceLevel=4**(7×7 增强
开着,strength=32);官方 App **从不调用 SetDetailEnhancement**(jadx 全量
无此符号)。38 轮两个未解子问题("显示链添加 +0.146"与"NUC 域 1.6×")
被同一混淆变量解释:**复刻链无增强、真机链有**——均结案。
- [x] **修复 + 验收(同场景 A/B,唯一变量增强 4→0)**:暗区 ROW 0.31-0.46 →
**0.14-0.23**(官方 0.15,nomask 0.18 达成 §16.3 的 ≤0.2);全幅 ROW
0.29 → **0.15**(官方 0.10)、COL 0.54 → **0.34**(官方 0.28)。fps 15.1。
- [x] **代码落点**:LiveRenderer 维持 nativeOut 单级(试验代码回退,类注释记录
定案);DebugLog 改回 false;设置页"图像增强"去掉"4 级(推荐)"标记 +
注明会放大暗场条纹/官方从不开启;**用户设备 enhanceLevel 已恢复 0**;
csdk/README.md 38 轮段同步修正。DetailEnhance 移植保留(官方 SDK 功能、
单测锁定,用户可选)。
- [x] 123 单测全绿;csdk 无代码变化。
- 现场状态:我们 App 前台出流(最终版 APK = 仓库提交代码);rotation 仍锁
竖屏(**待用户恢复**);增强已关(用户如需锐度可在设置里重开,代价是暗场
条纹加重);`%TEMP%\mag160c_re\` 下 39 轮脚本与截图待清理。
## 用户反馈修复 第三十八轮(2026-10-07 凌晨,原始帧录制 + 逐级重放全链路定案 + 显示链与官方对齐)
按第三十七轮交接清单执行。用户中途解锁手机并配合真机操作。完整数据表与
排除清单见 `docs/android_app/rendering_diff_phone_20261006.md` §17。
- [x] **原始帧录制调试开关**:`usb/RawFrameRecorder`(单文件,帧头 magic/seq/
phase/shutter/flags + raw 38456B,渲染帧附 NUC u16 块)+ `IrSession.rawFrameSink`
(读线程帧入口)+ `LiveViewModel` 的 `files/rawcap.enable` 标志轮询(400ms,
置位即录并触发手动 FFC,300 帧自动停写 `rawcap.state`)。零 Manifest/协议改动。
新增 `RawFrameRecorderTest`;123 单测全绿。
- [x] **逐级重放**:Python 整数语义复刻(i32/移位/截断同 Kotlin)与真机 NUC dump
互锁(corr 0.9996)。行交替 596.9(raw)→ 5.20(d2)→ 4.25(NUC):**参考减除
消 98.4%,残差即条纹来源**。A/B 排除:参考窗相位/平均长度/表插值温度/参考
老化(10.3 s 平坦)/跨 FFC 随机——残差=快门态与开放态行 offset FPN 图样差
(设备固有 ~1.7%)。
- [x] **官方 SDK 二次核对**(本轮新读 13 函数:CalibrationMain_RTV、PushBaselineRaw、
CAccumulator 全家、Raw2Temperature、CFunctions::PushFrame、TempRangeStat、
BlindCompensation、Interpolate*、SelectCaliCurve、DoSomething、GenerateVolatileLens、
Controller::PushFrame/StartProcess + jadx Java 帧路径):**与我们逐字段一致,
官方无任何额外处理能解释残差差**;GenerateVolatileLens 只进温度链。
- [x] **显示域 A/B 定案**:同场景同铁虹(§16.2 口径)。发现并修正两大测量混淆:
①我们竖屏 UI 的中心温度/MAX 标注/色标条画在热像区内,贡献与 FPN 同量级
(mask 后 1.41→0.76);②§16.2 的 8–17× 含横屏/竖屏几何错位。真实 NUC 域
差距 **1.6×**(同一份数据走官方显示链复刻 0.19 vs 我们链 0.32,官方截图 0.12)。
- [x] **修复 A(nativeOut)**:`RenderPipeline` 去掉 PC 版遗留的内部 2× 插值级
(官方安卓无此级),输出原生 160×120;IrSession/LiveRenderer/录像(6×)/拍照
全链适配,`NativeOutTest` 锁 parity(偶数采样=旧路径)。
- [x] **修复 B(640×480 中间级)**:`LiveRenderer` 按官方 viewer 补两级显示
(160×120 →FILTER_BITMAP 4×→640×480→屏幕)。复刻证明该中间级对逐行 FPN
有 ~0.6× 低通——同数据走官方两级链=0.12 vs 我们单级=0.32,**"官方背景
条纹更不明显"的主导机制是显示链**,现已逐级等价。
- [ ] **遗留(下一轮)**:UI mask 后我们 0.76 vs 官方 0.12——NUC 域 1.6×(设备
状态嫌疑,官方同算法同承担)+ 真机 GPU 显示链与 PIL 复刻间 ~2× 未解释差;
验收目标"逐行 std ≤ 0.2"未达成,从真机显示链逐像素对照入手。
- 现场状态:rotation 仍锁竖屏(原值 1,**待恢复**);我们 App 前台出流(新版
APK);官方 App 已 force-stop;DebugLog 已改回默认关。MIUI 会把授权后焦点
拉回我们 App:切官方前先 force-stop 我们,USB 弹窗用 `TAB×2+ENTER` 过
(横屏 input tap 坐标不可靠)。
## 用户反馈修复 第三十七轮(2026-10-06 深夜,"官方背景条纹更不明显"——逆向排除全部候选,定位为逐行固定偏移;交接轮)
用户:"很明显啊,官方背景里面的条纹就是要更不明显,你看看逆向出来的代码,是不是还漏掉了什么"。
本轮**只做了调查与测量,无代码改动**。完整证据与数据表见
`docs/android_app/rendering_diff_phone_20261006.md` §16,交接清单也在那里(16.3)。
- 逆向:官方每帧链路(CalibrationMain_RTV + BlindCompensation + TempRangeStat +
GrayEnlarge/HistDeRedundance)、内参(0x3808=1、0x380c=1、0x3804=baseLineAcc=4、
0x3810=5、EX=0、增强=0)、FFC 时序(关5→采4→第9帧开)、表只在 FFC 窗口重建、
setFrameRate 仅 devType1/2 —— 全部与我们一致,**没有遗漏的阶段**。
- 测量(调色板弧长反演 + 真机连拍):对比度一致;高频噪声我们 ~2×;**主导项是
"逐传感器行固定偏移":我们 std 1.2~1.84(max 12.1)vs 官方 0.11~0.15(max 0.57)**。
- 下一步(按序):① app 加原始帧录制(含 FFC 窗口)→ ② 离线逐级重放找引入行偏移的级
(参考帧污染/表插值温度/盲元邻域)→ ③ 修复后用 §16.2 指标验收(逐行 std ≤ 0.2)。
- 现场状态:热像仪已被用户拔下充电;手机 rotation 被我锁成竖屏
(`su -c 'settings put system accelerometer_rotation 0'`,原值 1,**测完要恢复**);
官方 App 已 force-stop,我们的 App 在前台(USB 权限对话框"确定"在 (1193,1897),
勾选框未勾,"一律打开"关联仍指向官方)。
- 工具痕迹:`%TEMP%\mag160c_re\`(IDA 批处理脚本、libcsxx 反编译、analyzer.py、
截图与 npy 中间件)——按约定属临时件,可删。
## 用户反馈修复 第三十六轮(2026-10-06 夜,"我们颗粒更大"→ 照片改单级放大,实时回退插值)
用户:"我们的app图片看起来像素颗粒更大啊,特别是高低温分界的边沿,是不是啥滤波没开。"
### 定位
14 节的"最近邻 2×"把**相邻两个传感器像素复制成同一个值** → 有效分辨率砍半 →
边沿变成 2 像素台阶(用户看到的"颗粒更大")。再核对官方源码:`ImageViewer` 还有
`PaintFlagsDrawFilter pfd_`(**加** FILTER_BITMAP)→ 官方是从 160×120 **单级滤波放大**。
### 修法
- 照片:`PhotoSaver.SENSOR_W/H = 160×120`、`RENDER_SCALE = 6`(输出仍 960×720),
调用方交入偶数采样的精确 160×120 帧 → **单级 6× 双线性**(无交错、无块状);
- 实时:`upscaleNearest = false`(回退插值 2×,颗粒回到 1 传感器像素);
- csdk:两个 demo 的 `upscale_nearest` 回退 0,选项保留并写明教训。
### 实测(同场景 17:54 vs 17:56,对齐 r=0.994)
分位 18/47/78/92/224 vs 官方 21/48/76/90/224;冷区 `|dx|` **3.29 vs 4.32(0.76)**——
修好后我们比官方更平滑。单测 119 全绿(`PhotoNucMappingTest` 已按新策略更新)。
## 用户反馈修复 第三十五轮(2026-10-06 夜,"低温区条纹更多":定位为我们自造的 2× 交错并修复)
用户:"你依据反汇编出来的东西进行修复"。
### 依据(官方安卓路径没有插值级)
`ImageViewer`:`Bitmap.createBitmap(fpaWidth,fpaHeight)` = **160×120** 原生尺寸,
`new Paint(1)`(仅抗锯齿、**无 FILTER_BITMAP**)+ 离屏 640×480 → **最近邻**放大。
我们的 `upscale2x`(PC SDK 移植)是双线性(偶列原值、奇列邻均值)→ 在传感器 Nyquist 上
自造"全幅/半幅"交错细竖纹,恰好在暗/低温区最显眼。
### 实测(同场景 A/B,冷区=最冷 25%)
细尺度列条纹比(我们/官方):**1.16 / 1.29(双线性)→ 1.00 / 0.93(最近邻)** ✔;
宽尺度(真实 FPN)0.87/0.90 → 0.72/0.82(我们本来更轻 ✔);列剖面相关性 0.815→0.832。
`|dx|` HF 比 1.27→1.67 是**旧交错充当了列平滑**所致(假图案被去掉后露出真实噪声),
不是变差;逐像素噪声差异的排查见 §12/§13。
### 落地
`RenderPipeline.upscaleNearest`(实时 true,默认 false 保 C 参考字节不变);
csdk 增加 `upscale_nearest`(两个 demo 默认 1);新增 `UpscaleParityTest`;**119 单测全绿**。
## 用户反馈修复 第三十四轮(2026-10-06 晚,"低温区条纹更多":冷区噪声超预算 1.65×,定位中)
用户:"不对吧,还是有差别,我换了个场景,背景的低温区域在我们的app里面明显条纹更多"。
### 量化(同场景 A/B + 12 s 漂移标定)
- 漂移标定:官方两张相隔 12 s,分位差 ~1 级、逐像素 |Δ|=1.20 → 跨 App 分位差不是漂移;
- 冷区(最冷 25%):**逐计数映射差 ≤1.7 级 ✔**、列条纹 2.28 vs 2.12(1.08 ✔)、
**细纹理 HF 2.17 vs 1.71(1.27 ✗)**;实时截图冷区 HF 1.01 vs 1.06 ✔(无 JPEG 干扰);
- JPEG 只解释 1/3(重压 640×480/q98 后 1.96)。
### 预算对比(关键)
整帧预算吻合(0.559σ_n=12.3 ✔,§12.1),但**冷区实测 20.4 counts = 1.65× 超预算** ——
只在**大负 d**(d=(raw-ref)>>1)出现 → 与用户观察一致。
### 已排除
阈值/增益 16 位解析与符号(厂商侧也是 signed 比较)、band 选择(映射一致)、快门参考、
JPEG、显示链(实时冷区反而更低)。
### 下一步(一次带插桩实机)
在大负 d 分层打印 d 分布/命中 band/插值后 gain-off/σ_n,核 DDT band0 原值;首查
`fpaTempFix` 是否不该作用在图像链(出两版对比冷区 HF)。
## 用户反馈修复 第三十三轮(2026-10-06 傍晚,"计数噪声偏大":实测预算证伪 + 修两处真问题)
用户:"把'计数'噪声偏大修一下,没问题就合并到csdk里面,各部分readme文档更新,然后推送"。
### 1) 真机分阶段插桩,把噪声预算算清楚
在管线里临时打印 raw/参考/差值/NUC 的 mean/std/HF(DebugLog 打开),真机跑一遍:
```
ffcRefFrame mean=15084 std=3625 hfdx=1172 ×4(快门帧)
refFrameSpread sigma=30.2/27.5/27.2 ← 逐帧像素噪声
stage.raw mean=11442 std=3466 hfdx=1168
stage.ref mean=15083 std=3624 hfdx=1172
stage.diff mean=-1821 std=326 hfdx=29.1
stage.nuc mean=9731 std=245 hfdx=24.2
```
- 快门帧是真快门(均值差 3600、4 帧一致到 0.1%)✓;
- σ_n ≈ 21–24 counts → 差值噪声理论值 0.559σ_n = 12.3 ✓ 与实测吻合;
- FPN 抵消到 2.4%(1168→29)✓。
→ **我们的参考采集与 NUC 链路完全符合物理预算,"计数噪声偏大"在数据链路上不成立**
(上轮跨 App 指标推断的结论没有证据支持)。
### 2) 1.2–1.5× 跨 App 差的测量混淆项(已逐条列出)
我们的照片 960×720/q95(0.29 B/px)vs 官方 640×480(0.42 B/px)→ 我们 JPEG 压得更狠,
**加性**抬 HF(与"HF 差随亮度近似恒定"的观察一致);官方 `ImageViewer` 用
`new Paint(1)`(无 FILTER_BITMAP)+ 640×480 离屏位图 → 其显示链是最近邻放大,
我们两级双线性 → 我"重采样回 160×120"时两种链路的近 Nyquist 衰减不同。均写在 §12.2。
### 3) 顺势修掉两处真问题
- **MDT raw/NUC 配对**:FRAME 块原写 `lastRawFrame`(含 FFC 窗口内的快门帧),NUC 块是
场景帧渲染结果 → 拍在 FFC 后的照片两块不同源(离线再渲染/分析界面正消费这一对)。
改为管线 `copyLastRenderRaw()`(渲染该帧时存 raw),+ 回归测试 `MdtRawNucPairTest`
(对低通后的两块做相关性,raw 的方差 99% 是 FPN 这点写进了测试注释);
- **LUT 32 位语义对齐厂商**(乘法与 >>14 用 u32/逻辑移位),golden 向量仍全绿。
### 4) csdk 合并(用户要求)
`mag160c_render.h/.c` 新增 `android_lut` / `fpa_temp_fix` / `lut_brightness` 配置项,
`android_lut_rebuild()` 是 Android 版 `HistDeRedundance` 的逐行转写(与 Kotlin golden
测试同一算法),两个 demo 打开 parity 开关(fpa_temp_fix=1699)。**本机没有 C 编译器**
(gcc/clang/NDK 均无),所以 C 侧未编译,README 里已明确标注。
### 5) 单测
118 全绿(新增 `MdtRawNucPairTest` + `AndroidLutPipelineTest` 4 个端到端用例)。
## 用户反馈修复 第三十二轮(2026-10-06 下午,手机解锁后真机验证:渲染已对齐,剩噪声)
用户:"手机我开了,你进行测试"。
### 1) 先修掉实时全黑(我上一轮引入的 bug)
`System.arraycopy(IntArray → ByteArray)` 每帧抛 `ArrayStoreException` → 实时画面全黑(SurfaceView
仍刷、logcat 无异常)。黄金向量测试只覆盖纯函数,没覆盖实例路径。已改逐元素拷贝 + 新增
`AndroidLutPipelineTest`(4 个端到端用例)。单测 **117 全绿**。
### 2) 同场景 A/B:映射对齐
官方/我们前后 2 分钟各拍一张(同一静止场景),调色板索引空间、同一 160×120 网格:
- 分位:官方 [82,89,**96**,117,133] vs 我们 [85,92,**98**,114,134];
- 逐计数中位索引差:**中位 2.0 级**;全图平均 |Δ| = **4.47/255**,99.2% ≤16。
对比旧 LUT(低端压死,我们 8/17 vs 官方 45/52,系统偏差 ≈35 级)→ **"官方对比度更高/更细腻"
的映射差异已消除**。
### 3) 步进常数 k 复核:反汇编公式即最优(旧结论作废)
用照片里存的 NUC 计数对官方照片做模型比对(新的一对,span=1241):
| k | 平均误差 | ≤8 占比 |
|---|---|---|
| **16·span/range(反汇编,=19)** | **4.64** | **82.9%** |
| 16(上轮"拟合最优") | 5.76 | 73.6% |
| 24 / 12 | 4.92 / 9.16 | 80.8% / 49.2% |
→ 代码不动,§10.4 的遗留项关闭(上轮"偏好 16"是当时那对照片的产物)。
### 4) 剩余问题已定位到"计数噪声"域
同 LUT 下我们的 NUC 计数渲染出的 HF(4.65/4.25)明显高于官方成品(照片 2.46/2.22),
即**映射一致、噪声不一致**;FFC 后立刻截图几乎不改善(帧间 2.44→2.34)→ 不是参考老化;
实时帧率 15.0 fps、单帧 6-9 ms(无回归)。→ 下一步查**实时 FFC 参考采集**与离线/PC
路径(PC 端 NUC 逐字节一致)的差别。
### 5) 运维踩坑(已写进渲染文档 §11.5)
推流中 force-stop 会把相机卡死(需 USB 重新枚举 `/sys/bus/usb/devices/2-1/authorized`);
切换 App 先按 HOME 正常停流;MIUI 每次重装/重枚举都要重新授权,勾"一律打开"要点**复选框本体**。
## 用户反馈修复 第三十一轮(2026-10-06,按真机 libcxsdk 反汇编定案:显示 LUT 才是差异)
用户:"先从逆向文件里面找灵感,尽可能多读一些伪代码,然后推测可能缺失的东西,再补上验证"。
### 1) 读伪代码的对象换了:**手机上的官方库本体**
从官方 APK 的 `lib/arm/libcxsdk.so` 建 IDB,按 vtable 指针段解析真实函数体,逐条看反汇编
(Hex-Rays 会吞掉 `__aeabi_uidiv` 的隐式除数,必须对指令)。顺带把上一轮 dump 的
"`CFunctions` 全量伪代码" 对照核清。
### 2) 三个"缺失"候选,两个被否掉、一个命中
- **`CAccumulator`**:`mode>=4` 是**恰好 mode 帧的块平均**(不是 IIR),`Controller::PushFrame`
对 A+1..A+E(E=baseLineAcc=4)各推一次并 `Restart` 一次 → 官方参考 = 4 帧平均 = **我们一样**。
§9.3"官方参考更干净"的机制**不成立**;§9.2 的"官方空间噪声低 2.3×"是**分辨率偏差**
(官方预览是 160×120 输出、我们预览是 320×240,逐屏像素 HF 不可比)。
- **`CSmoother` / 显示侧累积 / `setEX`**:官方分别由 `0x380c=1`(需>1 才生效)、`0x3808=1`、
`imageEX=0` 关闭 → 与我们一致。
- **命中:灰度 LUT 是 PC 版形状,官方是 Android 版形状**。另有:App 传给 SDK 的温度是
`帧尾 FPA + BasePara2.fpaTempFix(真机 1699)`(我们少加了),而 7×7 细节增强虽然每帧被
调用(`MapTemperature` 把门槛 `0x3864` 置 1),但强度源 `CFunctions+0x28` 全库未写 = 0
→ **官方那一步是空操作**(我们默认关 ✔ 一致)。
### 3) 定量:把官方照片与我们的 NUC 对齐
用我们照片里存的 NUC 计数块与官方照片逐像素对齐(垂直翻转后 **r=0.99**)→ **计数一致,
差异只在"计数→索引"映射**:官方 ≈ `0.84×我们的LUT + 21`(背景落在索引 45–55 的平台,
我们的旧 LUT 把它压到 0–20)。反汇编核对确认 Android 版 `HistDeRedundance` 与 PC 版
**结构不同**:固定基值 `16*(448+447*b/200)`、步进上限 `max(1,(0x1800+4j)·k>>13)`
(`k=min(0x200,16·span/range)`)、反向 CDF 从 `hist[pivot]` 起、`hist[1023]=hist[1022]`、
pivot=`max(center_idx,(meanBelow+meanAbove)/2)`(**pivot 官方是开着的**,`_96_4_` 构造=1
且全库无第二处写 → §8 的"关掉 pivot"是误判,已回退)。
拟合(同一 NUC、逐像素):旧 PC 版 ≈35,round-30 的"pivot 关" ≈14,
**Android 版逐指令移植 ≈12.5**,把步进常数取 16(拟合最优)≈5.9,官方实测 p5/p25=45/52。
### 4) 本轮改动
- `RenderPipeline.androidLutOf()`:Android `HistDeRedundance` 逐指令移植(+`AndroidLutTest`
golden 向量锁定),实时会话 `androidLut = true`;PC 路径保留,C 参考黄金测试不动;
- `RenderPipeline.fpaTempFix` → `rebuildTables(shutter + fpaTempFix)`,`IrSession` 从 BasePara2 注入;
- 单测 113 全绿。
### 5) 下一步(需要机主解锁手机:当前被 PIN 锁屏,无法做实时 A/B)
1. 同刻官方/我们实时画面 + 我们的 NUC,复核"`+fpaTempFix` 是否让计数分布与官方一致",
再定 §10.4 里那个 k(21 vs 16);
2. 7×7 细节增强按官方默认(2040)打开并回归(离线拟合我们的移植反而略差,需先核移植细节);
3. 同步 csdk(Android 版 LUT + fpaTempFix + 温度链)。
## 用户反馈修复 第三十轮(2026-10-06,"官方还是更细腻"→ 定位到参考帧累积)
用户:"不对吧,官方的画面看着还是要更细腻,你是不是还是遗漏了什么东西"。
### 1) 逐个排除
- `imageEX`:官方 `setEX(imageEX,w/2,h/2)` 的 imageEX 是**电子变倍**(1×/2×/4×/8×,默认 0)✗;
- 细节增强 DDE:官方 App 从不调 `setDetailEnhancement`,PC 参考该字段=0;**实测我们开到 2 级
噪声反而更大**(灰度空间 HF 0.76→0.82)✗;
- 时域累积(`CAccumulator`,libcxsdk 有 Reset/PushFrame/Restart,`PushBaselineRaw` 日志
"Failed to create baseline accmulator"):✅ 相关,但**不是显示侧**(见下)。
### 2) 实测判定(灰度域,调色板反解,消除色带斜率干扰)
| 配置 | 灰度空间 HF | 灰度帧间噪声 |
|---|---|---|
| 官方 App | **0.34 / 0.26** | **4.12 / 1.80** |
| 我们(现状) | 0.76 / 0.80 | 4.02 / 3.95 |
| 我们 + 显示侧累积 `>>2` | 0.18(过平滑 ✗) | 0.14(冻结 ✗,且静态横条纹被"暴露"更明显) |
| 我们 + 显示侧累积 `>>1` | 0.77 | 3.69 / 5.67 |
→ 官方是"**空间噪声低 2.3×、帧间噪声与我们相同**";显示侧平滑会同时压低两者 ✗。
**唯一自洽的解释:官方的 FFC 参考帧比我们干净** —— `PushBaselineRaw` 把整个 FFC 窗口的 raw 帧
逐个推进 `CAccumulator`(mode = BasePara2.baseLineAcc = 4,`>>2` IIR,每次 FFC Restart),
而我们的 `refPush` 只有 **4 帧等权平均**。
### 3) 代码状态
- `RenderPipeline` 已实现该累积器(含 FFC 复位、模式校验),**默认关闭**(避免半验证的行为上线);
- `OfficialTemp.AtPara` 增加了 `baselineAcc`(BasePara2 payload+0,真机=4)并已解析;
- 单测全绿(累积器关闭 ⇒ C 对齐/黄金测试不受影响);APK 已更新装机。
### 4) 下一步
1. 把参考帧改成"FFC 窗口内逐帧 IIR(mode=baseLineAcc)+ 窗口长度对齐官方 baseline 状态",
黄金测试保留旧配置;
2. 用同一套灰度指标回归(目标空间 HF ≈0.34、帧间噪声不变);
3. 再同步 csdk(pivot + 温度链 + 参考累积)。
## 用户反馈修复 第二十九轮(2026-10-06,渲染差异定位并修复:灰度 CDF 的 75% pivot)
用户:"我说的就是渲染的差异……官方的对比度似乎更高,而且画面里面的条纹看起来也更少。"
### 1) 逐一排除(都有反编译或实测证据)
- **窗口**:Android `GrayEnlarge`/`CalContrastOffsetedAutoEnlargeRange` 与我们的 `statsWindow`
在对比度 c=0 时逐字节一致(官方 App 也只传 `setAutoEnlarge(enlargeRange=8)`);
- **灰度 LUT/CDF 算法**:Android `HistDeRedundance` 与我们 `lutRebuild` 逐行一致
(1024 桶直方图、双向 CDF、`total>>12`、`0x100/0x200` 步进);
- **调色板**:官方照片与我们照片都完美拟合 2 号铁虹(中位色差 0.67 / 1.00);
`VendorPalettes` 已记录 case 2 与我们的表 256/256 一致(§7 的"调色板不同"是裁剪污染误判,已纠正);
- **条纹本身**:我们手机 NUC 的 column-HF 7.20(0.63%)vs 官方 7.81(0.63%),渲染平坦块列抖动
更低(0.34–0.94 vs 0.81–1.44)→ 数据/降噪不差于官方。
### 2) 定案:**`cdfPivot75`(SDK 里由 `+0x60` 参数控制的 CDF 75% 分位支路)**
测量方法:关闭两边对话框后同刻各截实时画面,用**调色板反解灰度**(99%+ 像素可唯一反解,
排除显示路径/色域影响):
| | p25 | p50 | p75 | mean |
|---|---|---|---|---|
| 官方 App | 48 | **61** | 129 | 82.2 |
| 我们(pivot 开,原状) | 23 | **33** | 82 | 60.2 |
| 我们(pivot 关) | 31–34 | **50–54** | 132–135 | **79.8–81.2** ✔ |
→ 关掉 pivot 后与官方分布重合。修复:`IrSession` 构造实时管线时显式
`cdfPivot75 = false`(类默认仍为 true,C 参考的黄金测试不受影响)。已装机复验(视觉一致)。
这同时解释"条纹更少"的观感:偏暗/低对比时噪声与残留 FPN 更显眼。
### 3) csdk 同步
- 工具已改为 `cdf_pivot_75 = 0`(与官方手机 App 一致,库保留开关;`render_offline.c`
生成黄金夹具仍用 1);
- 温度链移植(放射学分支)规格已写进 `csdk/README.md`,**待有 C 编译器时落地**
(本机当前 gcc/clang 均缺失,无法编译验证)。
## 用户反馈修复 第二十八轮(2026-10-06,温度链进 App + 真机对比:渲染差异收敛到 LUT-CDF)
用户:把温度修改同步进 App、真机测试没问题后改入 csdk;用官方 App 拍照对比,有差异就
Android SDK 与 Linux SDK 交叉验证。
### 1) 温度链进 App(已装机运行)
- 新增 `core/OfficialTemp.kt`:官方放射学链(波段映射 + AT 环境误差修正 + 发射率 revise),
曲线 A 直接用现成 `OfficialTables.T2E`(646 项与官方逐项相等),斜率表现算(公式一致)。
- `IrSession` 解析 **BasePara2**(真机报文验证的偏移:payload 不含 magic,`+14 short fpaTempFix`、
`+40 int ATZeroErrorPoint`、`+44 float ATErrorSlope`)与 **DDT 尾部波段记录**;
`RenderPipeline.officialTemps()/probeTemp()` 与 `LiveViewModel` 已切到官方链(缺失参数自动回退旧曲线)。
- 单测(全绿,含既有 100+ 项):官方同帧夹具 400 点(均值 17.5 mC)+ 真机 BasePara2 报文
+ DDT 尾部记录与设备内存记录逐字段一致 + 曲线往返 + 波段单调性。
- 真机:`zero=22216 slope=-0.0279`,radRecord `bands=8 shift=12 eps=1.0`;实时读数
中心 27.8–28.5 ℃ / MAX 34.7 ℃。
- **新发现**:设备上报的 `fpaTempFix=1699` ≠ PC 上实测的补偿量 `v50`(=dev+0x5C−dev+0x54,
0…89 mC)→ App 侧 `sensorDeltaMc` 暂取 0(误差 ≤0.09 ℃),来源待定位。
### 2) 手机端拍照对比(详见 `rendering_diff_phone_20261006.md`)
- 官方 App 照片是**纯 JPEG(无任何数据块)**;我们的是完整 MDT。
- 同场景亮度:官方 mean≈84 / p50≈49,我们 mean≈53 / p50≈19(偏暗);实时画面同样复现
(mean 53–72 vs 20–25),确认是**显示映射差异**而非时刻/场景差异。
- **三 SDK 交叉验证**:窗口公式 `CalContrastOffsetedAutoEnlargeRange`(Android)与我们移植的
`statsWindow` 在对比度 c=0 时**逐字节一致**(官方 App 也只传 `setAutoEnlarge(enlargeRange=8)`,
未设对比度;Win/Linux 侧同名 API 参数一致);直方图骨架也一致(`0xFFC00000/span`、`>>20 & 0xffc`)。
- → 差异收敛到 **灰度 LUT 的 CDF 细节**:Android 版 `HistDeRedundance` 有**双向 CDF + 步长量化**
(`local_1024 = total>>12`、`iVar3 = 0x10000/local_1024`、前后向各建一张 0x10cc/0x10d0 表),
我们的 `lutRebuild`(PC 版语义)没有这套量化。
### 3) 下一步
1. 按 Android SDK `HistDeRedundance` 重做 `lutRebuild`(含 `+0x60` pivot 分支与双向 CDF 量化),
用真机截图/照片定量回归(目标:luma 直方图与官方 App 吻合)。
2. 补 `getExtParameter`(ExtPara:`intContrastOffset/intBrightOffset/intCurrentEnvTemperature`),
把设备的对比度/亮度/环境温度接进管线;温度读数接 App 的发射率设置。
3. App 确认无误后,按用户要求同步进 **csdk**(C 参考实现 + csdk 测试)。
## 用户反馈修复 第二十七轮(2026-10-06,官方温度链反编译:按 IDA 结果重建,系数待钉)
用户要求:"修复这些的时候要和 ida 或者 Ghidra 反编译的结果对的上,不要自己瞎猜,并且是
全局代码对的上;机器上已经有 ida 了,你自己装个 mcp 就能用"。
详细报告:`docs/android_app/temperature_chain_re_20261006.md`。
### 1) 用 IDA 建立了地面真值(不是猜)
- IDA Professional 9.4 批处理(`idat.exe -A -S`)+ 运行时 ctypes 直读官方 DLL 内存,
反编译并核对:`sub_180016290`(核心换算三分支)、`sub_180014e70`(探针,含邻域平均)、
`sub_18000d2a0`(每帧两点标定端点)、`sub_180015b90`(修正,含 `K=f8·f4·ε` 与
环境辐亮度反演)、`sub_180015840/8b0/A40/940`(曲线 A 前后向插值)。
- **根因确定**:本机 `dev+0x41880=1` → 官方走**放射学分支 + 每波段线性映射**,
而 App 用的是 `countsToTempMc`(T2E 表分支)——**分支都不同**,不是"表差一点"。
- 拿到全部运行时参数:8 个波段阈值(9389/9995/10328/10581/10797/11160/11430/11772)、
每波段 (gain,offset)、`shift=12`、发射率 1.0、环境温度 29.45℃、曲线 A 与斜率表地址等;
并确认**这些参数就在 DDT 文件尾部**(App 解析 DDT 即可获得,无需新指令)。
### 2) 验证到什么程度
- 官方同帧数据(`MAG_GetTemperatureData` + `dev+0x270` 计数 + 记录快照)逐像素比对:
**波段映射结构正确**——flat 背景所在 band 的拟合残差仅 **3.2 mC**;
但整体仍差 **≈230 mC**,对应尚未对齐的**环境补偿/距离大气修正项**
(这两项按反编译加上去反而变差 ≈980 mC,说明字段语义还有一处没读对)。
- 结论:**结构已定、系数待钉**;下一步只做这两项的单变量对照,不再猜公式。
### 3) 工具链落地
- IDA MCP:`pip install "mcp<2" ida-pro-mcp` + 插件(`%APPDATA%\Hex-Rays\IDA Pro\plugins\mcp-plugin.py`),
ZCode 用户配置 `~/.zcode/cli/config.json` 已加 `mcp.servers."ida-pro"`;
**需重启 ZCode 才出现工具**,IDA 侧要 `Edit→Plugins→MCP`(Ctrl+Alt+M) 起 13337 端口。
- 批处理脚本与运行时读数脚本、官方同帧基准数据都在 `%TEMP%\mag160c_wincmp\`,未入库。
### 4) 下一步
1. 用单变量对照钉死环境补偿 (v50) 与距离/大气项(`dev+0xA8`、`dev+0x44CB8`、
`rec+0x208/0x20C`)。
2. Kotlin 落地官方放射学分支(波段映射 + 修正),并补导曲线 A 的斜率表与 DDT 尾部解析。
3. 用本轮官方同帧数据做回归断言(band 1 应到 mC 级),再替换 App 各温度读数点。
## 用户反馈修复 第二十六轮(2026-10-06,官方 PC 软件同帧对比:渲染逐像素一致,温度曲线有系统差)
用户把热像仪接上 PC,要求"调用项目里官方给的 win 软件拍图,看和逆向出来的 sdk 的图
差在哪些细节上"。详细报告:`docs/android_app/pc_official_compare_20261006.md`,
证据图 `docs/android_app/pc_vs_ours_20261006.png`。
### 1) 官方 Win 侧选谁(侦察结论)
- `app/【PC分析软件】…exe` = 内部名 **ThermoX 2.5.3.6**(巨哥电子),是**网络码流**软件
(指定IP/DHCP Server/传输混合·纯视频·纯温度码流),扫不到 USB 机芯 → 不适用于本机芯。
- `IR_Camera_SDK-1.0.1/.../EloThermal.exe`(官方 SDK 演示)能出图但**无存图功能**(全源码核实)。
- 采用 `tsdk_pair3.exe`(调用官方 `ThermalSDK.dll`/`CoreSDKLib.dll`)做基准采集:
每帧 19 个文件(官方渲染 rgb/cbrgb、灰度、NUC 输出、输入帧、ref、t32 温度、pal、
LUT、盲元、DDT),官方 DDT 一并拿到。
### 2) 同帧逐像素对比结果(官方 vs 我们的 Kotlin 管线)
- **NUC 域逐位相等**:抽检 4 帧 19200/19200 像素完全一致(max diff = 0)→ 端点插值、
增益/阈值表、盲元补偿、ref 处理全部正确。
- **调色板 256/256 完全一致**;官方彩色图 = palette(官方灰度)(max diff 0)→ 官方这份
渲染**没开** 7×7 细节增强,与我们默认增强等级 0 对齐。
- **灰度平均差 +1.3 级、最大 10 级**;彩色 61–72 % 字节相同(平均 |Δ| 1.4–1.9/255)。
- **窗口下界规则不同**:官方 20/20 帧 `winHi = 数据max`、12/20 帧 `winLo = 数据min`
(其余低 1–7 计数);我们 `winLo = mean−500`,本例比数据 min 低 188 计数
→ 就是那 1.3 级偏差的来源。
- **温度曲线系统差**:官方 t32 是 NUC 计数的精确直线(k = 8.539 mC/count,残差 ≤0.5 mC,
无逐像素差异,即 `ConvertResponse2Temperature` 一类);我们固定 T2E 表斜率 8.26→7.67
递减 → **27 ℃ 处 +0.1 ℃,38 ℃ 处 −0.62 ℃**(最大 −0.70 ℃)。
### 3) 新解码的官方内部语义(供后续复用)
帧输入 = `dev+0x41f20`、ref = `dev+0x41ef0`、**FPA 温度 = `dev+0x54`**(喂进我们管线后
NUC 与官方逐位相等,据此锁定帧尾 FPA 字段)、NUC 输出 = `dev+0x270`(`raw` 缓冲)。
### 4) 环境坑(重要)
- 本机原本没装相机驱动(`CM_PROB_FAILED_INSTALL`)。厂商 INF `ThermoCube_stream.inf`
是 libusb-win32 1.2.6.0 + libwdi 自签 cat → 需先把证书导入"受信任的根/发布者"才能
`pnputil /add-driver /install`;装好后设备 OK、`libusb0` 服务 RUNNING。
- **libusb-1.0 1.0.19 在该绑定下不可用**:`claim_interface` 内部 Win32 错误 87
(`LIBUSB_ERROR_ACCESS`),故 csdk 的 `demo3.exe`/`render_offline` 在本机跑不起来;
官方 SDK(libusb0.dll API)正常。要用 C 工具需换 libusb-1.0 版本或把设备改绑
WinUSB/libusbK。
### 5) 待办(下一轮)
1. 温度改用设备响应换算(线性系数;先定位 DDT/BasePara 中的标定字段)。
2. `statsWindow()` 下界对齐官方行为(≈数据 min)。
3. 手机侧受控对照(官方手机 App 的 MDT 里存的就是 NUC 后帧,可喂我们的显示阶段;
需要给管线加"只跑显示阶段"的离线入口)。
临时文件(官方 20 帧基准数据、差异图、harness)都在 `%TEMP%\mag160c_wincmp\`,仓库内
只保留结论文档 + 一张证据图;本轮临时单测已删除。
## 用户反馈修复 第二十五轮(2026-09-12,关闭 debug 日志 + 带图操作手册)
用户三点要求:把 App 的 debug 日志关掉;做一个带图的手册;push 上去。
### 1) debug 日志默认关闭
`DebugLog` 增加总开关 `enabled`,**默认 false**:`log()` 直接返回(不写 logcat 也不写文件),
`init()` 不安装崩溃钩子,`startFile()` 不建文件。要在设备上排查时把该常量改成 true 重新
构建即可,其它代码一行不用动。
- 关掉后崩溃仍按安卓常规方式可见(logcat `AndroidRuntime` + 系统"应用停止运行"弹窗),
只是不再有那份落在相册目录旁的 `debug_*.log`
- 真机烟雾测试(小米 22041211AC):出流正常、拍照成功(相册 19 → 20 张,
新增 `MAG160C_20260912_153522.jpg`)、崩溃数 0、**App 自身日志行数 0**、
`Download/MAG160C` 无新日志文件
- 注意:`adb logcat | grep MAG160C` 会匹配到系统 MediaProvider 打印的**文件路径**,
不是本 App 的日志。判断 App 是否还在打日志要按 tag 过滤:
`grep -E "MAG160C/(vm|usb|session|stream|render|rec|analyze)"`
### 2) 带图操作手册
`docs/android_app/app_manual.md` 升级为插图版,图在 `manual_images/`:
| 图 | 内容 | 来源 |
|---|---|---|
| `01_live_schematic.png` | 实时界面布局与 ①~⑥ 控件编号 | **绘制**(见下) |
| `02_markers.png` | 标记结构解剖:准星/读数/引线/描边字 | 真机照片放大 3 倍 |
| `03/04_photo_*.png` | 横屏 960×720 与竖屏 720×960 的真实照片 | 真机拍摄 |
| `05_settings.png` | 设置页 | 真机截图 |
| `06_analysis.png` | 分析页(含底部测量面板) | 真机截图 |
| `07_gallery.png` | 相册 | 真机截图 |
**为什么实时界面用绘制图而不是截图**:实时界面的 OSD 文字会按握持角度预旋转
(这是用户要的可读性设计),手册拍摄时手机是横放的,截图里全部文字都是侧躺的、
顶部/底部栏也一样,放进手册无法阅读。因此实时页改为自绘示意图(控件位置与真实
布局一致),其余界面文字本来就是水平的,直接用真机截图。
图由 `manual_images/MakeFigures.java` 生成(截图裁剪缩放、标记解剖放大与标注、
示意图绘制),界面改动后重跑即可,避免手册里的图与实物不一致。原始截图存为
JPEG(`*_raw.jpg`,PNG 转 JPEG 省下约 8MB)作为可再生的输入。
### 3) README 同步
阅读顺序加入操作手册,仓库结构表更新,里程碑补记 2026-09-11~12 的三个根因级修复
(launchMode / SET_CONFIGURATION / 软件画布卡顿 6.5→15.1fps)与同期完成项。
105 项测试全绿;本轮提交后 push 到 origin(用户明确要求)。
## 用户反馈修复 第二十四轮(2026-09-12,横屏拍摄方向:文件跟随握持角度)
用户反馈:"切到横屏拍照片和视频方向又不对了"。
**根因**:上一轮把照片旋转定为**显示旋转**(锁定 90° + 手动旋转),但这个值与手机的
物理姿态无关。相机模组是**固定在手机上的**,手机转 90°,场景就在传感器画面里转 90°,
所以文件必须跟着转——否则横屏拍出来就是"竖幅文件里装着横躺的场景"。
**修法**(普通相机的做法):
```
文件旋转 = 显示旋转 + 握持角度 (PhotoSaver.captureRotation)
```
握持角度取自 `DeviceOrientation.deg`(0 竖直/90 顺时针/180 倒置/270 逆时针)。
推论:竖直与倒置 → 竖幅文件(720×960);两个横屏姿态 → 横幅文件(960×720),
且两者相差 180°(场景在两种横屏姿态下本来就上下颠倒)。录像在**开始录制时**采样一次
(编码尺寸固定,中途换手不旋转——所有相机都如此)。
注意这**不影响预览**:预览仍是锁定的(用户此前明确要求"热成像画面还是要锁定")。
所以横屏下拍出来的文件不是预览的截图,而是预览**再转一个握持角**——正是这一步让
场景在文件里是正的。
**真机验证(关键证据)**:测试时设备恰好停在 270° 横屏姿态(`dumpsys sensorservice`
读数 gx=9.88, gy=-1.02 → deg=270),于是直接复现了用户的问题:
- 修复前:720×960 **竖幅**,内容相对竖直姿态拍的照片转了 90°
- 修复后:960×720 **横幅**;把修复前的文件转 **-90°** 与新文件逐像素比对,
平均亮度差 **5.06**,而转 +90° 的差是 **40.94** → 方向正确、只差这一转
- 录像:`tkhd 960 x 720`、avc1、97 帧 0 丢弃
- 分析页读该横幅照片(rot=0):MIN/MAX 各一个、与烧录标记重合,
面板 41.4 / 21.7 / 27.0 与照片一致
新增测试 `savedFileRotationFollowsTheGrip`(四种握持角度 + 手动旋转叠加)与
`landscapeGripsProduceLandscapeFiles`(形状规则 + 两个横屏姿态相差 180°)。
105 项测试全绿。
**教训**:文件方向是"世界坐标"问题,预览方向是"屏幕坐标"问题——两者可以不同,
混为一谈就会在某个握持姿态下出错。固件级/相机类功能的"方向正确"必须用
**同一场景在不同姿态下的可复现拍摄**来验证,不能只靠推导。
**本轮新增文档**:`docs/android_app/app_manual.md`(App 操作手册:界面、标注含义、
设置逐项说明与"是否真正生效"、方向规则、文件位置与格式、远程预览、常见问题、
已知限制),`README.md` 里程碑与阅读顺序已同步更新。
## 用户反馈修复 第二十三轮(2026-09-12,照片竖屏 / 标注风格重做 / 录像清晰度)
用户第五轮反馈(三点):竖屏拍照出来的照片不是竖屏;录像里的温度标点糊;
温度标点不要白底,参考大牌热成像重做一套、要有工业感。
### 1) 照片改为按"用户看到的方向"保存(推翻上一轮的决定)
上一轮把照片定为**传感器朝向**(4:3 横),用户明确否掉了。现在把**显示旋转**
(锁定 90° + 设置里的手动旋转)连同手动翻转一起烘进 JPEG:竖屏持机拍出来就是
竖屏(720×960)。顺序与实时渲染器完全一致——**先镜像、再旋转**。
- 温度数据**不跟着转**:probes / NUC / extremes 仍留在原始传感器空间,旋转只存在于
呈现层;`BLOCK_RENDER` 升级到 **version 2**,多存一个 `rotDeg`。旧文件(8 字节
v1 块)解析为 rot=0,正好符合它们确实是传感器朝向的事实——不会被静默转错。
- `sensorToPhoto` / `photoToSensor` 增加旋转参数(正向 (u,v)→(1-v,u),逆向对应),
并有 16 组 mirror×rotation 的往返单测。
- 分析页的映射改为**直接复用** `PhotoSaver.sensorToPhoto(..., 1, 1)`,让"分析显示的
位置"和"烧进照片的位置"不可能各算一套。
### 2) 标注风格重做(去白底,工业感)
按 FLIR/Testo 那类仪表的做法重做(`AnnotSpec` + `MarkerPainter`):
- **图标**:细线方形准星 + 四根短臂(原来是不带臂的圆环+圆点)。方框限定测量区域、
四臂指明确切像素、中间镂空不遮挡被测点。
- **文字**:白字 + 深色描边(先描边后填充),**不再有白色底板**。描边是为了在黑冷的
和白色的两端都读得清——白底会挡住被测画面,而且看着像消费级 App。
- 极值标签改用 **MAX / MIN** 大写,并加一条**引线**把读数与自己的准星连起来。
- 极值颜色与测点一样是白色:这是参考仪表的做法,彩色标点在铁虹的橙黄区会糊掉,
区分靠 MAX/MIN 文字。
### 3) 标注尺寸统一(顺带修掉的不一致)
AnnotSpec 的 320 单位是**图像长边**。实时界面原来用 `viewport.width()`(竖屏时是短边)
去除 320,导致实时标点只有照片标点的 3/4 大——正是用户早先"照片标点太大"的由来。
现在实时/照片/录像/分析**统一用 `max(w,h)/320`**,同一画面里标点相对图像的尺寸完全一致。
### 4) 录像清晰度
原来按原生 320×240 编码,文字只有 9.5px、放大后必然糊。现在:
- 编码尺寸 = 传感器 × **3**(竖屏 720×960,横屏 960×720),码率随像素数自动放大
(`w*h*10`,下限 2Mbps)。
- 帧的翻转+旋转用**一个 Matrix** 在 reader 线程上一次 drawBitmap 完成;
标点经 `sensorToPhoto` 落在同一变换下。矩阵与映射的一致性我按坐标推导核对过:
矩阵复合结果 = `sensorToPhoto`(rot90 时 u'=1-v、v'=u),两者不会各转各的。
- 真机实测:`tkhd 720 x 960`、avc1、96 帧 / 0 丢弃 / 7.3MB。
### 5) 分析页"每个标记出现两次"
照片里已经烧录了标记,分析页再叠一层,两层**标签位置不同**(准星位置是对的)。
根因不是坐标错误,而是**标签避让与顺序有关**:拍摄时按 probes→MAX→MIN 的顺序绘制,
分析页按 MAX→MIN→probes,碰撞避让把标签推到不同位置。已改为与分析页一致的顺序,
叠加后完全重合(真机裁剪对比确认)。
**排查方法记录**:判断"文件里的标记位置对不对"不要靠肉眼看截图叠加,直接**解析 MDT**
(`analysis` 里已有的块格式)算出每个标记的期望像素,再统计该处的中性白色像素数。
本轮据此一次性证伪了"旋转没生效"的猜测:rot=90 处 markerPixels=204/219/296/719,
rot=0 处全为 0。
**本轮真机验证(小米 22041211AC / Android 12 / MIUI)**:
- [x] 照片为 **720×960 竖屏**,方向与竖屏实时画面一致
- [x] MDT 内 `flags=2 flipV=true rot=90`,四个标记在旋转后的期望位置均有标记像素
- [x] 照片标注为新风格(准星+描边字、无白底),MAX/MIN 大写带引线,标签互不重叠
- [x] 分析页叠加后与烧录标记**完全重合**(每个标记只有一套)
- [x] 面板数值与照片一致(40.3 / 21.3 / 24.9,Pt1 25.0 / Pt2 23.2)
- [x] 录像 720×960 竖屏、96 帧 0 丢弃、首帧日志确认极值标记烧入
- [x] 103 项测试全绿
## 用户反馈修复 第二十二轮(2026-09-12,7×7 细节增强 + 标注统一 + 卡顿根因)
用户第四轮实机反馈(四点):移植 7×7 局部细节增强;分析界面标点没对齐;
实时界面与照片的标注风格必须**完全一致**("就像直接从实时界面截图"),
最高最低用 `min`/`max` 标注;实时画面还是卡。
### 1) 卡顿根因(本轮最有价值的发现,已量化)
先前只靠 `dumpsys gfxinfo`(显示 7.26% janky)猜原因,但**它根本看不到热像画面**:
热像走 `SurfaceView.lockCanvas()` 软件画布,gfxinfo 只统计 Compose 层
(实测 5 秒内只记到 28 帧,而流本身在 15fps)。于是给渲染线程加了自测量日志
(`MAG160C/render: paints=N avg=... worst=...`),一测即见真相:
| 阶段 | 绘制帧率 | 单帧绘制耗时 |
|------|---------|------------|
| 修复前 | **6.5 帧/秒** | **134 ms** |
| 加硬件画布后 | **15.1 帧/秒** | **6.4 ms** |
根因:`SurfaceHolder.lockCanvas()` 返回**软件**画布,于是每帧都在 CPU 上做
320×240 → 约 810×1080 的双线性放大 + 手工清整屏(约 10MB)。
**改用 `lockHardwareCanvas()`(API 29+,旧系统自动回退软件路径)**,放大与清屏
交给 GPU,CPU 只剩 76800 像素的拷贝。相机 15fps 因此第一次能完整到达屏幕。
配套修掉的每帧垃圾(卡顿的次要来源,也是 GC 停顿的来源):
- `setFramePixels` 每帧 `IntArray(320*240)`(4.6MB/s 垃圾)→ 复用 `flipped`
- 色条每帧 new 96 个 `Paint` + 96 次 `drawRect` → 预渲染 1×96 位图,仅换色板时重建
- 标记列表每帧 new ArrayList → 复用
- 温度点名 `refreshTemps()` 原本在**主线程**(LaunchedEffect 驱动):
400ms 一次分配 19200 整型数组 + 全屏扫描 + `copyNuc`(持渲染线程的锁)
→ 移到后台 `Dispatchers.Default` 循环,结果经 `Dispatchers.Main` 发布
### 2) 标注统一(照片 = 实时截图)
- 新增 `core/AnnotSpec.kt`(唯一几何定义)+ `media/MarkerPainter.kt`(唯一绘制例程),
实时/分析/照片/视频四处共用。此前各画各的,所以"看起来不一样"。
- 极端值标签由 高/低 改为 **`max` / `min`**(用户指定)。
- 照片以**传感器朝向**保存(4:3 横),文字在该帧内水平,3 倍分辨率(文字清晰)。
### 3) 分析界面标点对齐(两个真实 bug)
1. **极端值没有走坐标映射**:probes 经 `sensorToPhoto` 转换,extremes 却被
**原样加入**(`marks.addAll(extremes)`)。传感器坐标是 0..159/0..119,在
960×720 的照片上就落在**左上角几十像素内**——真机照片实测两个 min/max
全挤在左上角。已改为同一映射。
2. **`annotateJpeg` 忽略照片镜像**:从分析界面另存时,标记按未镜像坐标烧录,
于是**镜像到另一侧**。已加 `mirror` 参数并走 `sensorToPhoto`。
### 4) 标签互相压字(真机照片可见)
`min 21.9℃` 与 `max 32.6℃` 印在一起成一团。`placeLabel` 只判边界不判重叠,
现已支持"已放置盒"列表:默认右置 → 被占则翻到左侧 → 仍冲突则下移让位,
并保证始终在画面内。`MarkerPainter` 一次调用内跟踪,分析界面改为把 probes 与
extremes **合并成一次 draw**(原来分两次调用,彼此看不见)。
### 5) 重复的 min 标记(分析页 vs 照片)
分析页重新扫描 NUC 推导极值,与拍摄时 live 扫描的结果**不可能逐位一致**
(传感器在拍摄与重载之间会漂移,平坦区 argmin 极易移位)——真机出现
两个相距几像素、22.0/22.1℃ 的 min。已在容器新增
`BLOCK_EXTREMES (0x5BB5B562)`:拍摄时把 minPos/maxPos/minMc/maxMc 一并存盘,
分析页优先采用,从而与照片上烧录的标记**完全相同**。另存新照片也继续携带。
### 6) 分析页测量面板被导航栏遮住
`UiInsets.navPx` 是普通 `var` 且初值 0,全屏覆盖层(分析查看器)读它时
拿到的是首帧值 0,于是底部面板落在导航栏之下(截图可见数值不可见)。
已改为 `mutableStateOf` 并让分析查看器 `padding(bottom = navPx)`。
### 7) 7×7 局部细节增强(官方同款)
按反编译的 `CFunctions::FilterDetailEnhancement_Simple` + `LocalMap7x7_Simple`
移植(`core/DetailEnhance.kt`):7×7 窗口按 4×4 抽样(x/y 步长 2),
`mean = sum >> 4`,`if (strength <= (max-min)*32)` 时
`detail = (0x8000/divisor)*(center-mean)`,最后 `gray += (strength*detail) >> 15`。
管线位置与官方一致:`grayMap → 细节增强 → upscale2x → 调色板`。
**两个 strength 不可互换(本轮抓到的实现 bug)**:官方把**增益缩放后**的
`k = strength * gain * 2 >> 8` 传给 map(决定阈值与 divisor 下限),
但**混叠系数用的是原始 strength**。本移植最初两处都用 `k`,等于把细节
再乘一遍增益——真机 level 2 画面满屏噪点。修正后 HF 能量与关闭档几乎相同
(0.47~0.51 对 0.46~0.47),边缘仍被增强。
数值由 `blendUsesTheRawStrengthNotTheGainScaledOne` 黄金值测试锁定
(推导写在测试注释里;把 bug 改回去该测试立刻失败,已验证)。
**档位换算经官方 SDK 核对**:`MAG_SetDetailEnhancement` 把档位钳到 **0..32**,
`SetEX` 与之无关(那是数码变焦 ROI)。**官方 App 本身不调用
SetDetailEnhancement**(jadx 全量搜索无此符号),所以官方实时画面用的是
SDK 内部默认档——无法据此对齐具体档位,故本应用把它做成用户可见设置
(0/1/2/3/4/6/8 档),默认**关闭**(关闭时 `RenderPipelineTest` 逐位基线不受影响)。
**档位到底控制什么(推导 + 真机 HF 实测,曾一度写反)**:
`amplitude = strength*(center-mean)/divisor`,`divisor = max(max-mean, mean-min, mapStrength)`,
`mapStrength = strength*gain*2>>8`(gain=1000 → 每档 62.5)。由此:
- 弱对比区域:divisor 被 mapStrength 兜住 → 幅度化简为 `128/gain`,**与档位无关**;
- 强边缘:divisor 由局部对比度主导 → 幅度**随档位线性增长**;
- 门槛 `mapStrength <= range*32` → 档位越高,**弱对比区域被整个排除**(噪声所在处)。
所以高档位是"更锐但更挑",不是"连噪声一起放大"(设置页文案曾写反,已改)。
真机 HF 能量实测:关 0.365、6 档 0.438、8 档 0.433(6/8 相近,符合"8 档门槛更高、
只增强更强边缘")。该行为由 `theLevelRaisesTheContrastGateAndDropsWeakDetail` 锁定
(window range=60:8 档 mapStrength 1500 通过、16 档 3000 被拒)。
**另一个一致性修复**:分析页原先把测温点拿去**重新测量** NUC,导致照片上烧录的
`Pt2 26.1℃` 与面板显示的 25.9℃ 不一致(同极值那类漂移)。现在优先采用文件里
记录的 `tempMc`(照片上印的就是它),仅在缺失时才测量。
实机实测:级别 2 下仍 15.1fps,单帧绘制 11.3ms(滤波器约 +5ms,帧预算 66ms 内);
6/8 档 9.9~10.3ms,同样不掉帧。
### 8) 其他顺带修复(本轮真机复现)
- **`no_device` 死锁**:首次 connect 若赶上相机正在重枚举(`findDevice()` 返回
null)就停在"无相机",而 200ms 后到达的 ATTACHED 广播又落在 800ms 防抖窗口里
被丢弃 → 永久无画面,必须手动重启 App。现在防抖与退避都改为**延后重试而非丢弃**,
且 `no_device` 本身也会安排一次 1.5s 重试(`scheduleRetry` 单槽位、最新请求优先)。
- **平放时 OSD 文字整体转 90°**:手机平放在桌面时加速度计 x/y 都接近 0,
迟滞逻辑保留了上一次的姿态角。现在 tilt < 4 m/s²(约 24°)判定为"平放",
姿态角归零。
- **视频录制缺少 max/min 标记**:实时画面显示的两个极值没有烧进录像帧,
现在 `probesAsMarks()` 一并返回(录像因此与实时画面一致)。
- **屏幕熄灭时的行为(实测)**:渲染线程随 SurfaceView 停止绘制(无空转),
USB 读取继续,整机 CPU 占用 3.7%——后台不烧电、不崩溃。
**本轮真机验证(小米 22041211AC / Android 12 / MIUI)**:
- [x] 渲染帧率 6.5 → **15.1 帧/秒**,单帧 134ms → **6.4ms**(开增强 9~11ms)
- [x] 拍照:`capture: nuc=yes probes=2 mirror(h=false,v=true) extremes=2 saved=true`
- [x] 照片上标记风格与实时界面一致(同一 MarkerPainter),`max` 在热区、
`min` 在冷区(贴近右边缘时标签自动翻到左侧)、Pt1/Pt2 带白底标签,互不压字
- [x] 分析界面:标点与照片烧录位置对齐,**只有一个 min、一个 max**
- [x] 照片 / 分析面板 / 底部面板三方数值完全一致
(真机同帧:照片 max 36.0 min 20.4 Pt1 21.7 Pt2 26.1;面板 36.0/20.4/24.5 + 21.7/26.1)
- [x] 图像增强:6 档与 8 档生效、修正后不再放大噪声、15.1fps 不掉帧
- [x] 设置持久化跨冷启动(`mag160c_settings.xml`:enhanceLevel / imageFlipV)
- [x] 录像:99 帧 / 0 丢弃 / 1.6MB,首帧日志证明极值标记已烧入
(`annotation: 2 markers ... trace=BOTH minPos=159 maxPos=16269`)
- [x] **重启后冷启动**(USB 权限被系统清空后重新授权):一次成功、15.1fps、
无重枚举、无重连风暴
- [x] 屏幕熄灭时无空转(CPU 3.7%)、USB 读取继续、不崩溃
- [x] 长时间运行 60s+:15.1fps 稳定、无丢帧、无崩溃
**排查方法记录(本轮新增)**:
- 判断"自绘画面"的帧率/耗时**不能用 `dumpsys gfxinfo`**(它看不到 SurfaceView 的
lockCanvas 绘制,实测只记到 28 帧而流在 15fps)。本应用已内置 `MAG160C/render`
日志:每 5 秒打印 `paints=N avg=x ms worst=y ms`,这是唯一能看到真实绘制性能的途径。
- 录像标注是否齐全可从 `MAG160C/rec` 首帧日志直接读出(含 trace 模式与极值输入)。
**教训记录**:
- `dumpsys gfxinfo` 不统计 SurfaceView 的 lockCanvas 绘制。判断自绘画面性能
必须自己打点(本轮加的 `MAG160C/render` 日志就是为此,已保留)。
- 断言"某处卡"之前先量出**每帧耗时**和**实际帧率**:本轮原以为是温度扫描
(主线程 400ms 扫描)导致,量化后才发现是软件画布放大,量级差 20 倍。
## 用户反馈修复 第二十一轮(2026-09-12,无线 adb 真机调试:根因是 launchMode)
**本轮最大的发现**:前几轮反复出现的"连接风暴/重连循环/相机每 2 秒重枚举"
(设备号 060→093 一路涨、CPU 136%、状态在 streaming/no_device 间抖动)
**根因是 `MainActivity` 少了 `android:launchMode="singleTask"`**。
- Manifest 给 MainActivity 声明了 `USB_DEVICE_ATTACHED` intent-filter,而
**MIUI 在相机插着时会持续重复广播该事件**。默认 `standard` 启动模式下,
每次广播都创建一个**新的 MainActivity 实例**——真机 `dumpsys` 实测同时存在
**2 个实例**,各自持有自己的 ViewModel、IrSession、广播接收器,全部争抢同一台
相机;谁被挤掉谁就重新握手,于是相机被反复复位→重枚举。
- 官方 App 的 manifest 正是 `launchMode=2 (singleTask)`(aapt 实测)。
- 加上 `singleTask` 后实测:Activity 实例 1 个、**设备号 65 恒定 40 秒不变**、
状态稳定 streaming、fps 15.1、CPU 归零。
- 排查过程中曾按症状加过多层防御(connect 互斥锁、代际守卫、attach 去重、
失败退避+自动重试、stream 循环退避)。这些**已一并保留**:它们各自修掉了
真实存在的小问题(见下),但都不是那个根因;根因只有 launchMode 一处。
**本轮真机(小米 22041211AC / Android 12 / MIUI)实测通过的功能**:
- [x] 出流:冷启动一次成功,15fps、ref=true、timeouts=0
- [x] **拍照**:`capture: nuc=yes rot=90 saved=true`;文件入 `DCIM/MAG160C/`
- [x] **照片含正确温度数据**:NUC 块 153600B / 76800 样本(照片分辨率 1:1)、
零空洞、温度 22.6~32.9℃(均值 25.9℃)——不再有 -161℃/145℃
- [x] **录像**:开始→停止**不闪退**,MP4 2.2MB / 134 帧,`moov` box 完整可播
- [x] **分析页温度正确**:`min=22.606 max=32.877 center=24.683`,
与文件真值**逐位吻合**
- [x] **分析页加测温点 + 另存新照片**:生成 `_edit.jpg`,含 PROBES 块
(`116,102,Pt1,24674` / `150,169,Pt2,24838`,数值合理)+ NUC 块保留
- [x] **相册**:列出 4 张 → 点开全屏查看器(返回/删除)→ 返回键回列表 →
删除确认对话框 → 删除后文件 4→3、列表同步
- [x] **分析 tab 只列可测温照片**(旧的无 NUC 照片被过滤,符合设计)
**本轮顺带修掉的真实缺陷**(都在真机上复现过):
1. `UsbTransport.open()` 在 `claimInterface` 之后发 **SET_CONFIGURATION**——
这是设备级复位,会让相机立刻重新枚举。官方代码从不发(仅 claimInterface)。
**已删除**。(实测:app 停止时相机 30 秒稳定不动,启动后设备号立刻开始爬升。)
2. 分析页 `_minTempC.value = mn / 1000f`——`mn` 是 NUC **counts** 却被当温度,
显示 9.2/10.4℃(centre 走了正确路径所以是对的)。已改为过温度曲线。
3. `Mdt.compose` 只接受 `nucPixels.size == 38400`,而真实块是照片分辨率
(153600)→ **静默丢弃**,而 capture 日志仍打 `nuc=yes` 掩盖了它。
已改为按尺寸下限校验。
4. 三个全屏查看器(相册查看器、分析查看器)**都没有 BackHandler**——
返回键直接退出 app。已补。
5. USB attach 广播未校验 VID,任何 USB 事件都会触发 `connect()`;MIUI 重复
广播时把健康会话的状态覆盖成 `no_device`(画面在跑却显示"未检测到热像仪")。
已改为只认 0x833C 且已有会话时忽略。
6. `streamLoop` 在端点 halt 后 `bulkTransfer` **立即返回失败**(不等超时),
循环以约 1000 次/秒空转、每次两次控制传输——实测 28000 条失败日志、
**CPU 136%**。已加三级退避(0/20/250ms)+ 日志静默 + 死链约 1 分钟后收尾。
7. `connect()` 失败后无人重试(backoff 只拦截、不重发),UI 会永远停在
`no_handshake`。已加单实例自动重试。
8. `Mp4Recorder.stop()` 的收尾 drain 异常现在被记录说明(`file kept`),
不再让人误以为文件没生成;实测该异常下 MP4 仍完整可播。
**调试方法记录**(供后续排查):
- 设备端日志:`/sdcard/Download/MAG160C/debug_*.log.txt`(DCIM 被 MIUI 拒收
text/plain,自动回退到 Download),可 `adb pull` 取回
- MIUI 禁止 adb 注入输入(`INJECT_EVENTS`),但设备有 **Magisk root**:
`adb shell su -c 'input tap X Y'` 可以,`uiautomator dump` 配合读控件坐标
- 相机是否在重枚举:`dumpsys usb | grep -oE 'bus/usb/002/[0-9]+'` 连续采样看
设备号是否变化——这是判断"是硬件问题还是我们代码问题"的最快手段
(app 停止时稳定、启动后爬升 = 我们的代码在复位设备)
- Activity 实例数:`dumpsys activity activities | grep 'Activities=\['`
出现两个 MainActivity = launchMode 问题
## 用户反馈修复 第二十轮(2026-09-11,第三轮真机:分析测温/标注/相册/界面)
用户第三轮实机测试(含截图)报出以下问题,本轮全部处理:
- [x] **分析温度全错**(截图显示最高 145.1℃ / 最低 -161.0℃ / 中心 76.5℃)。
双重根因:
① **数据源错了**:实时读数用的是 `copyNuc()` 的**NUC 补偿后 counts**,
而照片里存的 `BLOCK_FRAME` 是**传感器原始响应**,标定表对它无效 →
直接换算就是垃圾值。新增 **`BLOCK_NUC` (0x5BB5B560)**:存拍照当刻的
**NUC counts**,分析端用它测量(与实时屏幕同一数据源)。
② **我自己引入的索引 bug**:第一版把 160×120 的样本"散布"进 320×240 的
照片空间,76800 个槽位只填了 19200,其余全 0 → `countsToTempMc(0)`
= **-161.0℃**(正是截图里的最低温)。改为
`buildPhotoOrderedCounts()`:**每个照片像素一个样本**,查表就是
`counts[iy*photoW+ix]`,不做任何旋转/翻转/缩放换算。
- [x] **测温点位置标错**:探针原先按**传感器坐标**存储,显示时却当**照片像素**用。
现在拍照时即转换到照片像素(`sensorToImage`),并且新增
`photoToSensor()` 反变换用于测温;两者互逆性有单测覆盖(4 旋转 × 2 翻转)。
- [x] **测温点太大**:标注尺寸原先用**屏幕密度**(4.5×density 的点、9×density 的环)
画进 320×240 的位图 → 环直径约 36px / 320px 图宽,视觉上盖住画面。
新增 `MarkStyle`:尺寸按**图像宽度**比例(点 2.6、环 5.5、字 9 @320px),
与实时屏幕观感一致。实时页/分析页/保存照片三处统一。
- [x] **分析页布局**:数据面板从**右侧竖栏**改为**底部横条**(用户要求),
三个数值(最高/最低/中心)等分排布 + 测温点横向滚动列表,标题栏收窄。
- [x] **分析页不显示极值标记**:现在在图上用小环标出最高/最低位置(带"高/低"字样)。
- [x] **相册与分析界面一样**:分析 tab 原先**直接渲染相册的网格**(同一个
`GalleryScreen`)。拆分为:
- `GalleryScreen`(相册 tab):像正常相册一样,点开进入**全屏查看器**
(双指缩放 1–8×、拖动平移、双击复位、**删除**含确认对话框);
- `AnalyzeScreen`(分析 tab):只列出**带温度数据**的照片
(`MdtProbe.isMeasurable`),点开进入测温分析页。
- [x] **旧照片优雅降级**:没有 `BLOCK_NUC` 的照片(本轮之前拍的)在分析页显示
"无温度数据",**不再编造温度**;新拍照片都带该数据块。
- [x] 单测 76 → **83 项全绿**(新增 `PhotoNucMappingTest`:密集性/中心对齐/
探针往返/字节往返/无 NUC 判定);debug + release(R8) 双构建通过;APK 已更新。
**已知取舍(诚实记录)**:
- `BLOCK_NUC` 是**照片分辨率**(76800 样本 = 153KB,未压缩),照片文件因此增大
约 150KB。选择它的理由:分析端查表无需任何坐标换算,从根上消除"索引对不上"
这一类缺陷(本轮的两个温度 bug 都源于此)。若日后要压缩,需保证查找端使用
同一套映射函数。
- 默认发射率/报警温度仍未接入管线(同第十八轮记录)。
**诚实记录(第十八轮未做)**:
- `defaultEmissivityPercent`(默认发射率)与 `alarmTempC`(报警温度)**仍未被
测温管线使用**:发射率需要官方 `CorrectTemperature` 的完整浮点公式(已从
libcxsdk 伪代码定位到 `@000298f0`,但牵涉 T2E/环境温度/`Energe2Temp` 多处
状态,属独立议题),报警温度需要超温提示 UI。当前这两项**只保存与显示**,
不声称已生效。
## 用户反馈修复 第十八轮(2026-09-11,真机实测:温度/朝向/远程三类缺陷)
用户实机安装测试(含两台手机远程预览)报出以下问题,本轮全部处理:
- [x] **中心温度测量错误**:`probeTemp()` 返回的**已是毫度**,`updateTemps` 又调了
一次 `countsToTempMc()` → 真机显示 108.7℃(实际约 24℃)。
远程页同一 bug(显示 -161℃)。两处都改为只除 1000 一次,并在参数注释里
写明单位契约。
- [x] **FFC 时最高/最低温跳到 ~150℃**:FFC 参考帧被解码进 `nuc`(OSD 采样的同一
缓冲区),未补偿的原始 counts 被当成温度。改为独立 `refScratch` 缓冲;
另加 `tempsReady()` 门控(首帧渲染前 `nuc` 全 0,而 `countsToTempMc(0)`
= **-161.0℃**,正是远程截图显示的读数)。
新增回归测试 `PipelineTemperatureStateTest`(3 项)。
- [x] **追踪开关只关最高温**:最低温标记未受开关控制(`LiveRenderer.drawOsd`)。
现在开=最高+最低都画("高"/"低"),关=都不画。
- [x] **画面转向反了 / 横屏应翻 180°**:先按"图像内容随握持角补偿(`rot = 90 - φ`)"
改了一版,**用户实测后指正:图像应保持锁定**——传感器装在手机上跟着一起转,
锁定图像时场景相对世界方向自动正确,加速度计参与反而双重补偿(这正是
"右转画面往反方向转"的原因)。**已改回锁定**:`ImageTransform.params()`
不再接受握持角参数(编译期保证),只保留官方同款手工修正。
用户还实测发现"**竖直翻转 + 旋转 90°**"能把画面转到另一方向——已用单测
证明它等价于"水平翻转 + 旋转 270°"(逐像素比对),两者只差一次水平镜像。
- [x] **新增官方同款方向设置**:设置页"旋转USB画面"(0/90/180/270,叠加在锁定
基准 90° 上)、"水平翻转"、"竖直翻转"(作用在传感器帧上,与官方一致),
持久化于 `AppSettings`。
- [x] **色标条两端最高/最低温与色条错位**:标签原先按未旋转的基线锚点定位。
改为**按旋转后包围盒**定位(`ImageTransform.rotatedBoxHalfExtents` +
`drawGripText`),任意握持角都贴着色条两端。
- [x] **几何单一来源**:新增 `ui/live/ImageTransform.kt`(纯 Kotlin),渲染器与
点击/探针映射共用同一套参数与互逆映射;新增 `ImageTransformTest`(6 项,
覆盖 0/90/180/270 × 两种翻转的往返一致性)+ `ImageTransformOrientationTest`
(4 项,锁官方映射与"转身时图像反向")。
- [x] **远程预览画面不正确**(用户截图:-161℃、满屏噪声):根因是协议只传像素,
客户端独立跑 FFC 状态机且**拿不到帧的相机温度**,而 NUC 表要按快门温度插值
→ counts 饱和、参考帧缺失。协议改为传元数据:
`[magic][counter][38400][flags][ffcPhase][shutter][像素]`(头 24B,记录 38424B),
客户端用 `RenderPipeline.frameRemote(frame, phase, shutter, out)` 复刻主机状态。
(用户建议"传原始数据本地渲染"正是此方向;此前的错误在于只传了数据的一半。)
- [x] **顺带修核查报告的两项证伪**:`RemoteSession.send()` 改为**单写协程 + 队列**
(原先每条命令各起协程写同一 socket,`hello`/`start` 可能乱序,~6.7% 丢
`welcome`;新增 `commandOrderIsPreservedUnderRapidSends` 回归);
主机实现"后来者写 busy"(accept 循环不再阻塞在 serve 上,第二客户端立即
收到 `{"type":"busy"}`;新增 `secondClientIsRejectedWithBusy`)。
另删除未使用的 `ACCESS_NETWORK_STATE` 权限。
- [x] **设置页翻不动 + 死行**(用户报"设置页选项翻不动"):
① 设置页 `Column` **缺 `verticalScroll`**,11 行内容一屏放不下,下方几行
完全够不到 → 已加滚动;
② "语言"行点了没反应:`when(dialog)` 里根本没有 `"language"` 分支,
点击后落进 `else -> {}`;且该项**没有任何代码读取、也没有 i18n 资源**
(全部界面为中文字面量、无 strings.xml)→ 补上对话框并**如实说明"当前仅中文,
选择会被记录待后续翻译"**,不再假装能切语言;
③ "关于"行同样设 `dialog = null` 属死行 → 补上说明对话框;
④ **设置变更即时生效**:原先实时/远程页每 400ms 轮询 `AppSettings`,而该轮询
只在实时页处于组合状态时运行(在设置页改动后要切回实时页才生效)→ 新增
`ImageOrientationSettings`(进程级 StateFlow),`AppSettings` 三个方向
setter 写入后立即 publish,实时/远程页 collect 后即时应用。
新增 `ImageOrientationSettingsTest`(4 项);单测 62 → **66 项全绿**。
- [x] 单测 62 → **66 项全绿**;debug + release(R8) 双构建通过;APK 已更新
(12.66MB)。`screenOrientation=portrait` 复核保持、camera 系列仍
not-required、多余权限已消失。
⚠️ **规格说明(用户裁定)**:图像**锁定**在竖屏框架(第四/五轮约定维持不变);
新增的仅是官方同款三项手工方向修正。中途曾按握持角补偿图像,用户实测指正后
已改回,`ImageTransform.params()` 不再接受握持角参数以防回退。
## 本轮(2026-09-10 执行计划 A→F)总结
七个阶段全部落地,每阶段一次 commit:
| 阶段 | commit | 内容 |
|---|---|---|
| A | 06c1f30 | lifetime 查询 + cali MD5 对照 + 真机自检清单 |
| B | 656d419 | MDT 温度解码 + 分析页温度条/测温探针 |
| C | b7a928e | 厂商调色板提取(11/12 精确表) |
| D | c34940e | 云模块脚手架(opt-in 默认关) |
| E | f8b3200 | 可见光 PIP 叠加(Camera2,三档可拖) |
| F | 512508e | 局域网远程预览(UDP 发现 + 原始帧 TCP,客户端渲染) |
测试:29(D 结束)→ 44(F 结束),含真实 TCP loopback 端到端用例。
**留给用户的实测**:`docs/android_app/real_device_checklist.md` 共 24 步
(1-12 单机 USB/拍照/录像/分析;13-15 PIP;16-24 双机远程预览),
每步标注预期 DebugLog 原文。
**已知未完成项(诚实记录)**:
1. 红热调色板(索引 11)仍是近似——官方 APK 预览图是白热的占位副本,
生成的 case12/13 对任何预览覆盖率 ≤6%,不猜测;需真机抓帧反推。
2. 全部真机验证(温度绝对值、PIP 相机、双机远程、MDT 温度条)需用户配合;
本机无模拟器(SDK 无 emulator 组件)、无连接设备,只能保证编译与单测。
3. 云端点为占位,需账号与接口文档才能对接。
## 里程碑日志
- 阶段3b 完成(2026-09-06 晚): 拍照 MDT 容器(jpg+info块+raw帧+备注)存入
MediaStore DCIM/MAG160C;MP4 录像 = Surface 编码 H.264 @320×240 15fps 2Mbps
- 阶段4 完成: 媒体库(MediaStore 扫描 + 内嵌 JPEG 缩略图 + MDT 尾部校验)
- 阶段5a 完成: 分析查看器(detectTransformGestures 缩放1-4x/平移、12 调色板
重渲染原始帧、自动窗口、备注编辑回写容器、温度探针近似)
- 阶段5b 完成: PDF 巡检报告(PdfDocument:标题/7×4信息表/热像图/结果/建议/测试员)
- 阶段6a 完成: 设置页(默认调色板/发射率/报警温度/关于,SharedPreferences 私有)
+ 任务巡检解析器 TaskParser(sqlite task/region/device/part/phase + XML title/target)
## 阶段3a 完成(2026-09-06 晚)
- [x] USB传输层 usb/UsbTransport.kt(VID 0x833C 枚举/权限/claim/EP查找)
- [x] 命令协议 usb/MagProtocol.kt(4B命令+FFC 8B;0x5BB5B55B 信息块解析)
- [x] 直播会话 usb/IrSession.kt(66b/66c/66f → FFC(0)x2 → 300ms → START(73),
读线程→FrameStream→RenderPipeline→帧回调;STOP(74);手动FFC)
- [x] 官方12调色板 core/Palettes.kt(铁虹=官方提取表,其余为标准曲线近似)
- [x] UI骨架:底部导航4区(实时/相册/分析/设置,横屏自动切侧栏 NavigationRail)
- [x] 实时画面 ui/live/:SurfaceView+软件Canvas渲染(letterbox+数码变倍+OSD
中心温/最高最低温标记),ViewModel驱动,慢速测温定时器
- [x] DDT标定文件打包进 assets/mag160c.ddt
- [x] 全APK assembleDebug 成功(11.7MB,无material-icons膨胀)
- [ ] 待实机验证USB流(需插设备)
## 开发计划(待确认后逐阶段执行)
- 阶段 1:环境(cmdline-tools/NDK r27+/gradle 8.x + 16KB 对齐配置 + AGP 8.x)
- 阶段 2:新仓库 app-android(Kotlin + Compose M3 + NDK 移植 csdk 渲染/温度 C 代码 + Kotlin USB 层)
- 阶段 3:实时画面(USB 流+官方管线+OSD+ROI+调色板+FFC+增强+PIP 可见光+拍照 MDT+MP4 录像)
- 阶段 4:媒体库(MediaStore,DCIM/MAG160C,无流氓文件夹)
- 阶段 5:MDT 离线分析(缩放/ROI/参数/PDF 报告/备注)
- 阶段 6:设置/任务巡检/可选云(Retrofit opt-in)
- 阶段 7:Android 16 专项适配验收(16KB 对齐/横竖屏/深色/预测性返回)
- 阶段 8:打包安装到用户手机实测
## 心跳约定
- 每完成一个阶段:更新本文件勾选 + git commit(不 push)。
- 中断恢复:读本文件 → 按"下一步"继续 → 环境检查命令:
```powershell
Test-Path C:\Users\ZXC\AppData\Local\Android\Sdk\ndk\<ver>
Get-Command java; Get-Command gradle
```