docs/android_app/app_manual.md(新,223 行): - 安装与首次连接(含"重启后 USB 授权失效需重新确认") - 界面总览图、实时界面各控件、测温点增删 - 标记怎么读(准星/描边字/MAX-MIN/引线的含义) - 相册、分析(离线测温)逐项操作 - 设置逐项说明,并标出**是否真正生效**:发射率/报警温度/语言目前仅保存未接入, 云同步为占位——避免文档承诺 App 做不到的事 - 方向规则(文件跟随握持、预览锁定、录像方向在按下瞬间确定) - 文件位置与 MDT 容器格式、远程预览双机用法、常见问题、已知限制 (相册不列录像、红热调色板为近似、PDF/巡检未接入界面) README.md:阅读顺序加入操作手册;仓库结构表更新;里程碑补记 2026-09-11~12 真机多轮联调的三个根因级修复(launchMode / SET_CONFIGURATION / 软件画布卡顿, 6.5→15.1fps)与同期完成项。 session_state.md 第二十四轮条目补记本轮新增文档。
1013 lines
73 KiB
Markdown
1013 lines
73 KiB
Markdown
# 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-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
|
||
```
|