Files
MAG160C/docs/android_app/HANDOFF_DEVELOPMENT.md
T

320 lines
23 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 — 完整开发交接文档(换模型用)
> 生成时间:2026-09-07。给接手模型读取本文件即可恢复全部上下文。
> 结合 `docs/android_app/session_state.md`(心跳)与 `docs/android_app/reverse_apk_features.md`(逆向总表)。
---
## 0. 给新模型的读取提示词
> 你是本项目的续任开发者。请先完整阅读 `C:\Project\MAG160C\docs\android_app\HANDOFF_DEVELOPMENT.md`
> 和 `C:\Project\MAG160C\docs\android_app\session_state.md`、`C:\Project\MAG160C\docs\android_app\reverse_apk_features.md`、
> `C:\Project\MAG160C\analysis\protocol_spec.md`,再继续开发。项目根目录 `C:\Project\MAG160C` 是 git 仓库,
> 源码在 `android\` 子目录(Gradle 工程,package `com.mag160c.thermal`)。当前会话状态与待办见 session_state.md。
---
## 1. 项目目标与背景
- 把 MAG160C 热像仪(160×120, 15fps, USB VID 0x833C)的官方 4 个安卓APP
(普通版 MAG-Cx / 专业版 MAG-Mx / ThermoScope 数据分析 / CoreSdkSample)逆向整合
为一款现代化安卓APP。
- 适配 **Android 16API 36**;用户反馈旧专业版在 Android 16 上花屏,重写根治。
- 逆向证据:`docs/android_app/reverse_apk_features.md`4 APP 功能总表)、
`analysis/protocol_spec.md`USB 协议/温度算法全逆向)。
- 用户要求:现代 UI、**构图钉死竖屏框架**(竖屏锁定,见 §4.3)、**不建立流氓文件夹**(媒体→MediaStore DCIM/MAG160C)、
无 32 位库(纯 Kotlin,无 NDK)。
## 2. 技术栈与工具链(已部署)
- **纯 Kotlin + Jetpack Compose (Material 3)**,无 NDK、无 .so、无 native。
- **工具链(2026-09-09 本机重装;交接文档旧机器路径 C:\Tools\gradle-8.14.3、
Zulu、C:\Users\ZXC\AppData\Local\Android\Sdk 均已失效)**
- JDK Temurin 21`C:\Tools\jdk-21`
- Android SDK`C:\Tools\android-sdk`cmdline-tools\latest、platforms/android-36、
build-tools 36.0.0、platform-tools/adb;许可已接受;无模拟器/AVD/system-images
- Gradle:用工程 wrapper `android\gradlew.bat`8.14.3 自动下载)
- `android\local.properties` 已写 `sdk.dir`(已 gitignore
- 构建需 `JAVA_HOME=C:\Tools\jdk-21`;国内下载慢可给 JVM 挂本机代理
Clash 7890`GRADLE_OPTS="-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=7890 ..."`
- 工程版本目录 `gradle\libs.versions.toml`
AGP 8.11.1 / Kotlin 2.2.0 / Compose BOM 2025.06.01 / lifecycle 2.9.1 / junit 4.13.2)。
- **磁盘轻量约定**:不装 NDK/CMake/zig(曾占用 2.4GB+,已卸载);全 Kotlin。
- 模拟器实测命令(无窗口):
```powershell
Start-Process "$env:LOCALAPPDATA\Android\Sdk\emulator\emulator.exe" -ArgumentList @('-avd','Medium_Phone_API_36.1','-no-window','-no-audio','-gpu','swiftshader_indirect') -WindowStyle Hidden
# 安装/启动/截图:
adb -s emulator-5554 install -r build-artifacts\mag160c-app-debug.apk
adb -s emulator-5554 shell am force-stop com.mag160c.thermal
adb -s emulator-5554 shell am start -n com.mag160c.thermal/.MainActivity
adb -s emulator-5554 exec-out screencap -p > analysis\preview\xxx.png # 注意:exec-out 才二进制安全
adb -s emulator-5554 emu rotate # 旋转模拟器(每次90°)
```
## 3. 源码结构与核心模块
```
android/app/src/main/kotlin/com/mag160c/thermal/
├─ MainActivity.kt 启动,enableEdgeToEdge + 隐藏状态栏(沉浸)
├─ ui/AppRoot.kt App壳:底部导航(实时/相册/分析/设置)恒贴竖屏底边(Activity 竖屏锁定)
├─ ui/DeviceOrientation.kt 加速度计→手机物理朝向(条栏/OSD 文字补偿旋转用)
├─ ui/UiInsets.kt 单例:底部导航高度 px(渲染器用它留白)
├─ ui/theme/Theme.kt M3 动态取色(Android12+
├─ ui/live/ ★实时画面(最核心)
│ ├─ LiveViewModel.kt 状态机/多点测温/追踪开关/拍照/录像/inset上报/PIP状态/
│ │ 远程服务端开关+rawHook→RemoteHost
│ ├─ LiveScreen.kt 稳定单分支相机布局:顶栏5控制项(FFC/变倍/追踪/调色板/画中画)
│ │ +底部快门区(相册/拍照/录像)+SurfaceView+PIP浮层
│ ├─ PipCameraView.kt 可见光PIP相机引擎(Camera2最小实现,异常只记日志)
│ ├─ ImageTransform.kt 方向/翻转/letterbox/缩放几何 + 传感器↔屏幕互逆映射(纯 Kotlin,单测覆盖)
│ └─ LiveRenderer.kt SurfaceView 软件渲染:图像锁定竖屏框架(90°,可按设置手工旋转/翻转)、
│ letterbox+变倍、色标条/圆环标记/中心温OSD(标签按旋转包围盒定位)
├─ ui/gallery/ 相册页(MediaStore DCIM/MAG160C 扫描 + 内嵌JPEG缩略图 + 运行时媒体权限)
├─ ui/analyze/ MDT 离线分析(缩放/调色板重渲染/温度条+点击测温/备注回写/PDF报告)
├─ ui/settings/ 设置页(默认调色板/发射率/报警温度/语言/云同步/远程预览两行/关于)
├─ ui/remote/ 远程预览客户端(Phase F)
│ ├─ RemoteClientListScreen.kt 主机列表(UDP发现/手动IP/重新扫描)
│ ├─ RemoteViewerViewModel.kt 本地管线渲染远端原始帧(调色板/变倍本地化)
│ ├─ RemoteViewerScreen.kt 查看页(与实时页同构图,快门区换成红色"断开")
│ └─ RemoteRendererHost.kt 与 LiveRenderer 同构图的渲染器(LiveRenderer 已冻结)
├─ net/ 网络互连(Phase F,纯 java.net,可 JVM 单测)
│ ├─ RemoteContract.kt 端口/魔数/JSON data/帧封包与流式重组 FramePacketReader
│ ├─ RemoteHost.kt 服务端:UDP 47510 广播 + TCP 47511 单客户端流
│ └─ RemoteClient.kt 客户端:发现(去重3s) + 连接/行模式→帧模式/10s判死
├─ cloud/CloudApi.kt 云接口占位(Retrofitopt-in 默认关闭,api() 未开启即抛异常)
├─ media/
│ ├─ Mdt.kt MDT 容器格式(JPG + DDT段[info块+原始帧] + 152B Tail+ parse
│ ├─ PhotoSaver.kt MediaStore 保存(DCIM/MAG160C,无权限也可写自有文件)
│ ├─ Mp4Recorder.kt MP4录像(Surface 输入 H.264MediaCodec+MediaMuxer
│ ├─ DebugLog.kt 现场调试日志(落盘 DCIM→Download→私有目录 + logcat 镜像)
│ └─ PdfReport.kt PDF 巡检报告(PdfDocument
├─ usb/
│ ├─ UsbTransport.kt UsbManager 枚举(VID 0x833C)/权限/claim/EP
│ ├─ MagProtocol.kt 命令码 0x6BB6B6xx(小端)/响应 0x5BB5B5xx
│ └─ IrSession.kt 连接/启动流/读线程→FrameStream→RenderPipeline→回调;
│ FFC回调;rawHook(远程预览)/recorderHook(录像);
│ deviceLifetimeMs675);cali缓存MD5对照
├─ core/
│ ├─ OfficialTables.kt 自动生成:PALETTE256_ARGB(官方铁虹)/T2E(646)/T2E274/E2T
│ ├─ VendorPalettes.kt 官方运行时时序生成器的移植结果(11/12 精确表,生成物)
│ ├─ Palettes.kt 12调色板(0..10=官方精确表,11 红热=近似)
│ ├─ RenderPipeline.kt ★官方渲染管线 Kotlin 移植(与C参考逐字节一致)
│ ├─ TempMath.kt 温度换算(counts→毫度,T2E+ tempMapFromPixelsMDT温度图)
│ └─ FrameStream.kt 流帧重组(0x1BB1B11B 搜索)
└─ task/TaskParser.kt 任务巡检 sqlite/xml 解析
assets/mag160c.ddt 官方 DDT 标定文件(1.8MB,渲染必需)
res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形)
```
## 4. 核心知识(必须知道)
### 4.1 渲染管线(已逐字节验证)
- `core/RenderPipeline.kt` 移植自 `csdk/src/mag160c_render.c`(官方管线,像素级验证)。
- 输入:USB 帧(0x38 头 + 38400B u16 小端 + 尾部);输出 320×240 ARGB。
- FFC 状态机:暖机20帧内暂停;第75帧强制FFCFFC(0)→5隐藏→FFC(1)→4参考帧→4隐藏→正常。
- **移植陷阱(已解决,改代码时务必保持)**:
1. C 的 32 位无符号回绕:所有 `u32()` 掩码必须保留(cdf*denom、0xffc0000-iv7*0x40000、(v-lo)*S 等)。
2. lutRebuild 里 `u12` 的基址是常量 `iv7*0x10+0x10`**不是**链式 u21v。
3. ByteArray 存 255=-1:判 0xFF 必须 `(b.toInt() and 0xFF)`。
4. 暖机窗口检查在 ffc_step **之前**(顺序不能改)。
- 验证方式:`android/app/src/test/.../RenderPipelineTest.kt`JVM 差分测试,参照 `analysis/render_offline.c` 的 C 输出,60帧序列与官方DDT)。
### 4.2 USB 协议(权威版:analysis/magcx_official_flow.md
- **2026-09-10 已反编译官方普通版全量源码**(jadx:`analysis/sdk_re/android_app/jadx_magcx/`
UsbCommunication.java 是 USB 层真身;Ghidralibcxsdk 全量伪代码
`analysis/sdk_re/android_app/libcxsdk_decomp.txt`1290 函数)。
- 协议要点:命令 4 字节**小端**;超时全线 800ms;序列
GetParameter1(66b)→GetParameter2(66c)→GetCaliInfo(66f)→[缓存缺失]
GetCaliFile(670)+EP 0x84 拉 16KB 块→StartTransferImg(673)FFC=672(帧驱动)。
- 官方响应格式:0x5BB5B55B BasePara160B,含 serial/devType/宽/高/fps)。
- 旧 analysis/protocol_spec.md 的 66f/670 描述("prepare/version query")不准,
以 magcx_official_flow.md 为准。
### 4.3 实时画面渲染(2026-09-11 定稿:图像锁定 + 官方同款手工修正)
- **Activity 锁定竖屏**manifest `screenOrientation="portrait"`):屏幕相对手机框架
永不旋转。顶栏、热像区域、底栏的**绝对位置**永远贴着手机竖屏的物理顶边(挖孔侧)、
中间、物理底边;手机怎么物理旋转,构图都不动。**不要改回 fullSensor。**
- **图像内容同样锁定**(用户 2026-09-11 明确)**,不随握持角旋转**:热像传感器
物理装在手机上、跟着手机一起转,所以锁定图像时场景与世界的相对方向自动保持
正确,**不需要**加速度计参与。这也正是官方 App 的屏上效果(官方窗口随传感器
转,其面板级图像旋转因此是恒定的 90°)。
- ⚠️ **曾经改错又改回**:2026-09-11 中途有一版把图像按握持角反向补偿
`rot = 90 - φ`),结果**双重补偿**,正是用户报的"右转画面往反方向转"。
**不要再引入握持角参与图像旋转。**
- `ImageTransform.params()` **不接受握持角参数**——这是编译期保证。
- **手工修正项**(官方"旋转USB画面/水平翻转/竖直翻转"同款,设置页):
`imageRotateDeg`(0/90/180/270) 叠加在锁定基准 90° 之上;两个翻转作用在
**传感器帧**上(与官方一致:翻转交给 native,旋转在显示矩阵上)。
持久化于 `AppSettings`。
- **数学等价关系(单测锁定)**:**"竖直翻转 + 旋转 90°" ≡ "水平翻转 + 旋转 270°"**
(逐像素比对验证)。用户实测"竖直翻转 + 旋转 90°"可把画面转到另一方向,
正是这条恒等式;两者只差一次水平镜像。
- **图标/文字按握持角补偿**`graphicsLayer rotationZ=-φ`Compose/
`canvas.rotate(-φ)`(Canvas),保证任何握持姿态下可读。**标签一律按旋转后
包围盒定位**`ImageTransform.rotatedBoxHalfExtents` + `drawGripText`),
不要再拿基线锚点摆位——色标条两端数字曾因此与色条错位。
- **几何单一来源**`ui/live/ImageTransform.kt`(纯 Kotlin,可单测)同时供
渲染器与点击/探针映射使用(`sensorToScreen` / `screenToSensor` 互为逆映射,
已对 0/90/180/270 × 翻转组合做往返测试)。渲染与取温映射**必须**用同一套参数,
否则标记会落到错误像素上。
- **加速度计符号约定(第七轮教训,勿改回)**:真机 TYPE_ACCELEROMETER 静止读数
指向世界上方(竖屏正持 y=+9.81);模拟器虚拟传感器是反的重力约定(y=-9.81)。
DeviceOrientation 映射按**真机约定**写,模拟器测试须用反号值驱动
(φ=0→`adb emu sensor set acceleration 0:9.81:0`)。
- **追踪开关控制最高+最低两个标记**(此前只关最高,最低永远画着)。
- 顶栏=5 个控制项(FFC / 变倍×N / 追踪·开 / 调色板+名称 / 画中画),恒在顶部并带
小字标签,`safeDrawing` 顶部 inset 适配挖孔屏。
- 底部(仅实时页)导航栏上方为**相机快门区**:相册快捷入口 / 大快门拍照 /
录像-停止;快门区高度并入 `vm.uiBottomPx`= 导航高+快门区高)。
- 顶栏高度经 `onSizeChanged`→`vm.uiTopPx`**必须挂在 safeDrawing inset 之前**
即包含挖孔安全区高度,否则真机热像顶端会插进顶栏背后);底导航高度经
`UiInsets.navPx`、快门区高度经其 `onSizeChanged`→`vm.uiBottomPx`= 导航+快门区),
渲染器据此留白。
- **演示模式已撤下**(2026-09-09 用户要求,接真机实测显示,勿加回来):无设备时
`connect()` 置 `status="no_device"`LiveScreen 显示占位文案"未检测到热像仪,
请插入MAG160C",渲染器无帧黑底;startDemo/合成帧代码已删除。
### 4.3.1 温度显示(2026-09-11 修复三个真机缺陷)
- **中心温度**`probeTemp()` 返回的**已经是毫度**,界面只许除以 1000 一次。
旧代码又调了一次 `countsToTempMc()`,真机显示 108.7℃(实际约 24℃)。
- **FFC 期间的温度跳变**:FFC 参考帧过去被解码进 `nuc`OSD 采样的同一缓冲区),
未补偿的原始 counts 直接参与显示 → 最高/最低短暂跳到 ~150℃。
现在参考帧走独立 `refScratch` 缓冲。
- **上电/未就绪**:首帧渲染前 `nuc` 全 0,而 `countsToTempMc(0) = -161.0℃`
(看着像合法读数)。`RenderPipeline.tempsReady()` / `IrSession.tempsReady()`
门控 UI 取温;远程页同样门控。
- 回归测试:`core/PipelineTemperatureStateTest.kt`。
### 4.4 温度
- 温度 = 毫度 int(÷1000 = ℃)。`counts_to_temp_mc`(NUC域→T2E逆映射)用于探针/OSD,
在真机上需按 DDT 标定核对绝对值(待办)。
- 离线 MDT 温度解码(ConvertResponse2Temperature + 标定参数)未实现(待真机文件对照)。
## 5. 构建 / 测试 / 提交
```powershell
# 构建 + 单元测试(无 --daemon 单次,约1分钟;首次在本机需下载依赖较久)
cd C:\Project\MAG160C\android
$env:JAVA_HOME="C:\Tools\jdk-21"
.\gradlew.bat :app:assembleDebug test --no-daemon
# 产物
Copy-Item app\build\outputs\apk\debug\app-debug.apk C:\Project\MAG160C\build-artifacts\mag160c-app-debug.apk
# 提交(每里程碑一个 commit,不 push
git -C C:\Project\MAG160C add -A
git -C C:\Project\MAG160C commit -m "android: ..."
```
## 6. 严重教训(反复踩坑,务必遵守)
1. **编码**:所有源码必须 UTF-8。本机默认编码是 GBK!
- **python 读写文件必须显式 `encoding='utf-8'`**`open(p,'w')` 裸写会变 GBK→编译出的中文全乱码)。
- 检测/修复脚本:`C:\Users\ZXC\AppData\Local\Temp\opencode\fix_encoding.py`、
`check_mojibake.py`。改完必须跑一遍确认全 OK,再构建。
- gradle.properties 已有 `-Dfile.encoding=UTF-8`gradle + kotlin daemon)。
2. **图标**:手写 pathData 的弧线易坏,一律用双弧圆 `M x,y a r,r 0 1,0 2r,0 a r,r 0 1,0 -2r,0z` + 直线/矩形。
3. **SurfaceView 与旋转**:不要在横竖屏间切换不同布局分支(SurfaceView 会被销毁且 surface 不重建)。
用"稳定单分支 + 覆盖层"布局(当前实现即如此)。
4. **写代码要谨慎**:本会话多次因急于成稿写出语法错误/残留死代码。每写一个文件后立即编译。
5. **ByteBuffer.putInt 默认大端**USB 命令必须小端(官方 intToByteArray)。
16 轮黑屏调试的根因就是它(66b 回文数掩盖了字节反转),MagProtocolTest 已锁死。
6. **官方 App 源码已入库,先读它再写协议代码**analysis/sdk_re/android_app/jadx_magcx/Java)、
analysis/sdk_re/android_app/libcxsdk_decomp.txtnative)、
analysis/magcx_official_flow.md(权威结论)。不要凭旧文档猜协议。
## 7. 当前状态与待办
**已完成**:核心管线移植(字节级验证)、USB 层、实时画面(相机式布局:
顶栏5控制项 FFC/变倍/追踪/调色板/画中画 + 底部快门区 相册/拍照/录像 + 底导航)、
图标文字按物理持机朝向补偿旋转(加速度计)、拍照 MDT 容器→MediaStore、
MP4 录像、媒体库、离线分析(含温度条+点击测温)、PDF 报告、任务解析、设置页、
Android16 沉浸(状态栏隐藏+竖屏锁定+挖孔安全区)、乱码多次修复。
(演示模式 2026-09-09 按用户要求撤下,见 §4.3。)
**执行计划 2026-09-10 七阶段全部落地**docs/android_app/execution_plan.md):
- Phase AGetLifeTime(675) 查询→`deviceLifetimeMs` + 心跳 stats 带 lifetime
cali 缓存与内置 DDT 的 MD5 一致性日志;真机自检清单
`docs/android_app/real_device_checklist.md`24 步,含预期日志行)。
- Phase B`Mdt.parse` 返回 `MdtFile`jpg 按 FFD9 裁尾、text 去 NUL 填充、
framePixels 齐全);`TempMath.tempMapFromPixels` 毫度图;分析页温度条
(中心/最低/最高)+ 点击测温探针。**注意:分析页图像按原生横向绘制,
探针映射是直接映射,不要照抄实时页的 90° 逆映射。**
- Phase C:厂商调色板——结论是 **libcxsdk.so 无静态表**,全部由
`CFunctions::SetColorPalette` 运行时算术生成;移植该函数后 11/12 个表
已达官方精确(0..10,铁虹与官方表 256/256 一致),索引 11 红热保留近似。
详见 `analysis/sdk_re/android_app/palette_extraction_findings.md`。
- Phase D:云模块占位(Retrofit 2.11.0`AppSettings.cloudEnabled` 默认 false
`CloudClient.api()` 未开启即 check 抛异常——**不允许静默联网**)。
- Phase E:可见光 PIPCamera2 最小实现,三档尺寸、可拖动、双击关闭;
相机任何异常只记 `[pip]` 日志并收窗,绝不影响热像主画面)。
- Phase F:局域网远程预览(UDP 47510 发现 + TCP 47511 控制与原始帧流,
**客户端本地管线渲染**,调色板/变倍不过网;单测含真实 TCP loopback 端到端)。
**布局约定(最终,勿回退)**
- Activity **锁定竖屏**`portrait`),构图钉死手机竖屏框架:顶栏/热像区域/快门区/底导航
绝对位置永不随手机物理旋转变化(屏显方向永远对应镜头方向)。
- 图标/文字持机朝向补偿:`ui/DeviceOrientation`(加速度计,真机约定 y=+9.81=0°;
模拟器是反的重力约定)→ `rotationZ=-φ`Compose/`canvas.rotate(-φ)`Canvas)。
- 顶栏 `uiTopPx` 必须挂在 `safeDrawing` inset **之前**(包含挖孔安全区,否则真机热像
顶端被顶栏盖住);`uiBottomPx` = 底导航高 + 快门区高。
**禁改清单**(详见 execution_plan.md §0,改前先读):Manifest 的
`screenOrientation="portrait"`、`MagProtocol` 命令字节序(MagProtocolTest 锁定)、
`IrSession` 握手序列、`LiveRenderer` 构图逻辑、`analysis/` 目录。
**待办**
1. 真机 USB 实测:温度绝对值标定、FFC/录像/MDT 保存端到端——照
`docs/android_app/real_device_checklist.md` 24 步走。
2. 红热调色板(索引 11)精确表:官方 APK 的预览图是占位副本,需真机抓帧反推。
3. 双机远程预览实测(清单第 16-24 步)。
4. 云服务真实接口对接(当前仅占位,需账号与接口文档)。
## 8. 文件路径速查
- 心跳/状态:`docs/android_app/session_state.md`
- 逆向功能总表:`docs/android_app/reverse_apk_features.md`
- USB 协议/温度算法:`analysis/protocol_spec.md`
- C 参考实现(Kotlin 管线的蓝本):`csdk/src/mag160c_render.c`、`mag160c_temp.c`、`mag160c_ir.c`
- PC 参考工具:`analysis/render_offline.c`、`analysis/gen_kotlin_tables.py`
- 预览截图:`analysis/preview/preview_*.png`
- APK 产物:`build-artifacts/mag160c-app-debug.apk`
## 9. 下一轮提示词(交付新模型直接用)
> 你是本项目的续任开发者。请先完整阅读 `C:\Project\MAG160C\docs\android_app\HANDOFF_DEVELOPMENT.md`
> 和 `C:\Project\MAG160C\docs\android_app\session_state.md`、`docs\android_app\reverse_apk_features.md`、
> `C:\Project\MAG160C\analysis\protocol_spec.md`,再继续开发。
> 项目根 `C:\Project\MAG160C` 是 git 仓库,源码在 `android\`Gradle 工程,package
> `com.mag160c.thermal`)。Android 16 已适配,目标是一个 MAG160C 热像仪(160×120, 15fps,
> USB VID 0x833C PID 1)统一 APP。
>
> 当前已完成到第 10 轮反馈修复:核心渲染管线 Kotlin 移植(逐字节验证)、USB 层、
> 相机式实时布局(顶栏4控制项+底部快门区+底导航)、图标文字持机朝向补偿旋转、
> 拍照MDT存MediaStore、MP4录像、媒体库、离线分析、PDF报告、任务解析、设置页、
> Android16沉浸(竖屏锁定+挖孔适配)。
> 第 10 轮(2026-09-09):按用户要求撤下演示模式合成热像图(无设备只显示
> "未检测到热像仪"占位),接真机+热像仪实测显示。
>
> 硬性约定(勿回退):① Activity 竖屏锁定 `portrait`,构图钉死手机竖屏框架,
> 屏显方向必须始终对应镜头方向;图标/文字用 `ui/DeviceOrientation`(加速度计,
> 真机约定 y=+9.81=0°)补偿旋转。② 源码全 UTF-8;python 读写必须显式
> `encoding='utf-8'`(本机默认 GBK,裸写会乱码)。③ 图标用双弧圆+直线/矩形。
>
> 下一步建议按 session_state.md「待办」推进:
> 1. 真机 USB 实测——温度绝对值标定(对 DDT 核对 counts→毫度);
> 2. 网络互连远程预览——UDP 广播自动发现 + 手动内网 IP,主机手机插热像仪、
> 局域网另一台手机远程实时预览(自定义轻量协议:控制通道 JSON + 图像流,
> 不走厂商 33596/33597);
> 3. 离线 MDT 温度解码(ConvertResponse2Temperature + 标定参数);
> 4. 厂商 12 调色板精确提取;5. 可见光 PIP 融合、云模块(Retrofit opt-in 预留)。
>
> 构建/提交:
> ```powershell
> cd C:\Project\MAG160C\android; $env:JAVA_HOME="C:\Tools\jdk-21"
> .\gradlew.bat :app:assembleDebug test --no-daemon
> git -C C:\Project\MAG160C add -A; git -C C:\Project\MAG160C commit -m "android: ..."
> ```
> 环境(2026-09-09 本机重装):JDK Temurin 21 `C:\Tools\jdk-21`、SDK `C:\Tools\android-sdk`
> (无模拟器/AVD)、Gradle 用工程 wrapper。注意:git 工作树里有大量与本任务无关的脏文件
> `.tools/``IR_Camera_SDK/``csdk/``analysis/` 等),**不要提交它们**,只提交你的改动。