Files
MAG160C/docs/android_app/session_state.md
T
ZXCLI 4c940fe773 android: 照片改为竖屏方向 + 标注风格重做(去白底,准星+描边字)+ 录像 3x 编码
照片方向(推翻上一轮的"传感器朝向"决定,用户明确否掉):
把显示旋转(锁定 90° + 手动旋转)与手动翻转一起烘进 JPEG,竖屏持机拍出来就是
竖屏 720x960,顺序与实时渲染器一致(先镜像再旋转)。温度数据不跟着转——probes/
NUC/extremes 仍在原始传感器空间,旋转只存在于呈现层;BLOCK_RENDER 升到 version 2
多存 rotDeg,旧文件(8B v1 块)解析为 rot=0,符合它们确实是传感器朝向的事实。
分析页映射改为直接复用 PhotoSaver.sensorToPhoto(...,1,1),杜绝两处各算一套。

标注风格(参考 FLIR/Testo 的仪表做法):
- 图标改为细线方形准星 + 四短臂(原为圆环+圆点):方框限定测量区域、四臂指明确切
  像素、中心镂空不遮挡被测点。
- 文字白字+深色描边(先描边后填充),去掉白色底板——白底挡住被测画面且像消费级 App;
  描边让文字在黑冷端和白色热端都读得清。
- 极值改用 MAX/MIN 大写 + 引线把读数连到自己的准星;颜色仍取白色(彩色标点在铁虹
  橙黄区会糊掉,区分靠文字)。

标注尺寸统一:AnnotSpec 的 320 单位是图像长边,实时界面原用 viewport.width()(竖屏
时是短边)去除 320,使实时标点只有照片的 3/4 大——这正是用户早先"照片标点太大"的
由来。现四处统一 max(w,h)/320。

录像清晰度:编码尺寸改为传感器 x3(竖屏 720x960),码率随像素数放大;帧的翻转+旋转
用一个 Matrix 一次 drawBitmap 完成,标点经 sensorToPhoto 落在同一变换下(矩阵复合
结果与 sensorToPhoto 按坐标推导核对一致)。实测 tkhd 720x960 / avc1 / 96帧 0丢失。

分析页"每个标记出现两次":不是坐标错,而是标签避让与绘制顺序有关——拍摄按
probes→MAX→MIN、分析按 MAX→MIN→probes,避让把标签推到不同位置。改为同序后叠加
完全重合(真机裁剪对比确认)。

排查方法:验证"文件里标记位置是否正确"不靠肉眼看截图叠加,直接解析 MDT 算期望像素
再统计该处中性白色像素数——据此一次证伪"旋转没生效":rot=90 处 markerPixels=
204/219/296/719,rot=0 处全为 0。

103 项测试全绿(新增 mirror x rotation 往返、旋转角点、v1→v2 渲染块兼容)。
2026-09-12 14:34:54 +08:00

968 lines
70 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 16API 36)、现代 UI、横竖屏、
无流氓文件夹、干爽简洁 → **进行中**
3. 背景问题:专业版在用户 Android 16 手机上花屏(根因见 reverse_apk_features.md §6
## 已完成的里程碑
- [x] 工具链:jadx 1.5.1C:\Tools\jadx)、Ghidra 12.1.2C:\Tools)、IDA MCP 可用
- [x] 4 APK 解包 + jadx 全量反编译(%TEMP%\opencode\apkwork\jadx_*
- [x] 普通版/专业版/ThermoScope/demo 功能清单 → docs/android_app/reverse_apk_features.md
- [x] IDApro libcoresdk.so GetOutputImage/copyBitmap 位图格式证实(ARGB_8888
- [x] Ghidralibcxsdk.so 导出 C++ API 清单(CFunctions 族,与 ARM64 coresdk 同源)
- [x] 花屏根因分析(32 位专属库 + targetSdk25 legacy 渲染栈)
## 关键事实速查(开发时直接用)
- USBVID 0x833C PID 1EP 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.ddt1.8MBSN 160043865
- MDT 文件格式:reverse_apk_features.md §4152B Tail + 5 段布局 + ROI 256B 记录)
- 温度:毫度 intMDT 解码走 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 移植为 KotlinGradle 8.14.3 + SDK 已有组件
- [x] 阶段2:项目骨架(android/AGP 8.11.1 + Kotlin 2.2.0 + Compose BOM
compileSdk/targetSdk 36minSdk 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] 阶段2PC端参考工具 analysis/render_offline.cgcc 编译)+ 差分测试:
**Kotlin 输出与C参考逐字节一致**(60帧序列,首渲染帧32,NUC/LUT/灰度/输出全同)
- [x] 阶段2FrameStream 分块重组测试 + 温度换算单调性测试全过
## 移植陷阱记录(教训)
- C 无符号32位回绕:乘积/减法必须 u32() 掩码(cdf*denom、0xffc0000-iv7*0x40000、
(v-win_lo)*S 等)
- lutRebuild 的 u12 基址是常量 iv7*0x10+0x10**不是**链式 u21vu21v 只用于曲线钳制比较
- Kotlin ByteArray 存 255 = -1lut[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 = 全屏减顶栏/底栏insetAppRoot横屏时
导航栏宽度不再误算为底部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] **底部新增相机快门区**(仅实时页,位于底导航上方,白/红色调):
相册快捷入口(跳转媒体库页签)/ 大圆形快门=拍照 / 录像-停止(红圈,录像中
红圈内容变红色方块)。快门区高度并入渲染器底部 insetvm.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] 工具链在本机重装并重建 APK2026-09-09):JDK Temurin 21 `C:\Tools\jdk-21`
SDK `C:\Tools\android-sdk`platform 36 / build-tools 36.0.0 / platform-tools,许可已接受)、
Gradle 用工程 wrapper8.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/MAG160Ctext/plainAPI29+ 无需权限,
相册/文件管理器可见),逐行 flush;同时镜像到 logcattag `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)、LiveViewModelconnect/权限/首帧到达/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 秒启动延迟期间的若干次超时
不再杀死数据流。去掉异步 UsbRequestusbfs 超时对 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-10BREAKTHROUGH:逆向官方 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=FFC673=START
674=STOP676/677=省电参数(AutoPowerSave,非图像传输)。
- **官方安卓连接序列根本不发 66b/66c**66b 是 PC libmagcore 旧演示遗留),
也不发预启动 FFC;序列 = 66f →(缓存缺失)670+0x84 拉取 → 读线程 →
50ms → START。FFC 由帧驱动(pipeline onFfc)。
- 推断:我们一直发的 66b 把该固件带入旧式握手模式并卡死(66b 有响应、
之后全哑、看门狗 ~25s 重启)——与全部日志吻合。
- [x] **IrSession 按官方序列重写连接**66f1000ms 读,解析 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 常量全表)**
- P2D66b=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=BasePara1serial/hw+devType/sw/宽/高/fps/gain/flip/
interFrame/interLine/gfid/gsk14 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;未命中→启动 ThreadCaliRecv0x84、16KB 块、800ms/读、
无数据 5s 放弃)+ 发 670waitForCmdAck 读 ack)→ 收满后 startTransfer
5) startTransfernative startProcess(…, caliPath)= loadDdt 拉到的文件)
→ setEX/ExtPara/放大倍率(native,非 USB)→ 起线程 → 5ms → 673(读 ack)。
- **TIMEOUT=800ms 全线统一**(我们此前的 400ms 是响应缺失的直接嫌疑);
- 图像帧尾校验 drop=type 0/1FFC 触发=按 GetLifeTime 开机时间表(devType3
走 imageStableCounter 分支)+ 双击/消息触发,均为 672。
- [x] **IrSession 按上述逐行重写**(保留:会话互斥、CLEAR_HALT 恢复、DETACHED、
no_stream_data 提示;去掉:5s re-kick、延迟 66b)。BasePara1 解析出 identity
serial/devType/宽/高/fps+ lastInfo0BasePara2 存 lastInfo1MDT 用)。
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.so1290 函数);
- `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-10docs/android_app/execution_plan.md
- [x] Phase A2026-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 B2026-09-10):Mdt.parse 返回 MdtFilejpg 按 FFD9 裁尾、text 去 NUL
填充、framePixels/info0/info1 齐全);TempMath.tempMapFromPixels 毫度图;
分析页温度条(中心/最低/最高)+ 点击测温探针(白点+环+温度标签,再点清除);
新增 MdtTest/TempMathTest17 个单测全绿)。注:分析页图像为原生横向显示,
故探针映射用直接映射(不是实时页的 90° 逆映射)。APK 已更新。
- [x] Phase C2026-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%)→ 保留近似并注明。
新增 PalettesTest8 项);产物见 palette_extraction_findings.md。
APK 已更新。
- [x] Phase D2026-09-10):云模块脚手架(Retrofit 2.11.0 opt-in,默认关闭):
cloud/CloudApi.kt(接口+DATA classCloudClient.api() 在未开启时直接
check() 抛异常——绝不静默联网);AppSettings.cloudEnabled(默认 false
构造时同步 CloudClient);设置页"语言"与"关于"之间新增"云同步"行 +
说明对话框("当前版本仅预留接口,不会发起任何网络请求");
proguard-rules.pro 新建(-keep cloud.**;原文件缺失但被 build.gradle
引用)。新增 CloudClientTest4 项,锁"默认关闭/api() 拒绝"契约)。
debug + release(R8) 双构建通过,29 单测全绿。APK 已更新。
- [x] Phase E2026-09-10):可见光 PIP 融合:Manifest 加 CAMERA 权限 +
camera.any uses-featureorientation 未动);LiveState 加
pipOn/pipSizeIndex/pipXf/pipYf + togglePip/cyclePipSize/setPipPos
新增 ui/live/PipCameraView.ktCamera2 最小实现,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 F2026-09-10):网络互连远程预览。net/RemoteContract.kt(协议常量+
手写扁平 JSON 助手,**无 Android 依赖**便于 JVM 单测 + FramePacketReader
流式重组);net/RemoteHost.ktUDP 47510 每秒广播 + TCP 47511 单客户端;
**单一协程拥有 socket**,控制回复与帧记录不会交错;帧队列 DROP_OLDEST 不拖慢
相机);net/RemoteClient.ktdiscover 去重 3sconnect 行模式→收到
stream-start 后切帧模式,同一 chunk 内混合也能正确切分;10s 无数据判死);
IrSession 加 rawHook(与 recorderHook 并列);LiveViewModel 接 rawHook →
remoteHost.offerFrame + setRemoteHostEnabled(需活跃 USB 会话);
设置页新增"远程预览服务端"/"远程预览客户端"两行(前置校验弹"先连接热像仪");
ui/remote/ 三文件:RemoteClientListScreen56dp 标题栏/64dp 卡片/重新扫描/
手动添加/空态)、RemoteViewerViewModel(本地 RenderPipeline + 内置 DDT
渲染)、RemoteViewerScreen + RemoteRendererHost(与 LiveRenderer 同构图,
未改冻结文件)、AppRoot 加两个全屏目的地。Manifest 加 INTERNET。
单测 44 个全绿,含 **loopback 端到端测试**(真实 TCPhello→welcome→
start→stream-start→5 帧逐字节往返→ffc→stop→stream-stop)与粘包/截断/
坏长度/重同步用例。debug+release 双构建通过。真机双机步骤写入清单第 16-24 步。
- [x] Phase Z2026-09-10):收尾完成。docs 更新(本文件 + HANDOFF_DEVELOPMENT
§3 结构表加入 net/ cloud/ ui/remote/ 与 VendorPalettes/PipCameraView
§7 状态与待办重写 + 禁改清单);全量 `assembleDebug test`44 单测全绿)
+ `assembleRelease`R8)通过;APK 已覆盖提交
build-artifacts/mag160c-app-debug.apk12.66MBmd5 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-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.9Pt1 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.4336/8 相近,符合"8 档门槛更高、
只增强更强边缘")。该行为由 `theLevelRaisesTheContrastGateAndDropsWeakDetail` 锁定
window range=608 档 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 PIP16-24 双机远程预览),
每步标注预期 DebugLog 原文。
**已知未完成项(诚实记录)**
1. 红热调色板(索引 11)仍是近似——官方 APK 预览图是白热的占位副本,
生成的 case12/13 对任何预览覆盖率 ≤6%,不猜测;需真机抓帧反推。
2. 全部真机验证(温度绝对值、PIP 相机、双机远程、MDT 温度条)需用户配合;
本机无模拟器(SDK 无 emulator 组件)、无连接设备,只能保证编译与单测。
3. 云端点为占位,需账号与接口文档才能对接。
## 里程碑日志
- 阶段3b 完成(2026-09-06 晚): 拍照 MDT 容器(jpg+info块+raw帧+备注)存入
MediaStore DCIM/MAG160CMP4 录像 = Surface 编码 H.264 @320×240 15fps 2Mbps
- 阶段4 完成: 媒体库(MediaStore 扫描 + 内嵌 JPEG 缩略图 + MDT 尾部校验)
- 阶段5a 完成: 分析查看器(detectTransformGestures 缩放1-4x/平移、12 调色板
重渲染原始帧、自动窗口、备注编辑回写容器、温度探针近似)
- 阶段5b 完成: PDF 巡检报告(PdfDocument:标题/7×4信息表/热像图/结果/建议/测试员)
- 阶段6a 完成: 设置页(默认调色板/发射率/报警温度/关于,SharedPreferences 私有)
+ 任务巡检解析器 TaskParsersqlite task/region/device/part/phase + XML title/target
## 阶段3a 完成(2026-09-06 晚)
- [x] USB传输层 usb/UsbTransport.ktVID 0x833C 枚举/权限/claim/EP查找)
- [x] 命令协议 usb/MagProtocol.kt4B命令+FFC 8B0x5BB5B55B 信息块解析)
- [x] 直播会话 usb/IrSession.kt66b/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-androidKotlin + Compose M3 + NDK 移植 csdk 渲染/温度 C 代码 + Kotlin USB 层)
- 阶段 3:实时画面(USB 流+官方管线+OSD+ROI+调色板+FFC+增强+PIP 可见光+拍照 MDT+MP4 录像)
- 阶段 4:媒体库(MediaStoreDCIM/MAG160C,无流氓文件夹)
- 阶段 5:MDT 离线分析(缩放/ROI/参数/PDF 报告/备注)
- 阶段 6:设置/任务巡检/可选云(Retrofit opt-in
- 阶段 7Android 16 专项适配验收(16KB 对齐/横竖屏/深色/预测性返回)
- 阶段 8:打包安装到用户手机实测
## 心跳约定
- 每完成一个阶段:更新本文件勾选 + git commit(不 push)。
- 中断恢复:读本文件 → 按"下一步"继续 → 环境检查命令:
```powershell
Test-Path C:\Users\ZXC\AppData\Local\Android\Sdk\ndk\<ver>
Get-Command java; Get-Command gradle
```