Files
MAG160C/docs/android_app/temperature_chain_re_20261006.md
T

367 lines
25 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.
# 官方温度链逆向(IDA 反编译为准绳)2026-10-06
> 目标:把官方 SDK 的"计数 → 温度"换算**按反编译结果**完整复刻到我们 App,
> 不用拟合、不猜。本轮已完成反编译取证、运行时参数读取、官方基准数据采集与
> 波段模型验证;**最后两个修正项(环境/距离项)的字段含义尚未对齐**,下轮继续。
>
> 工具链:IDA Professional 9.4(批处理 `idat.exe -A -S<脚本>` + Hex-Rays)、
> 官方 DLL 运行时内存读取(Python ctypes)、官方 ThermalSDK 同帧采集(tsdk_pair3)。
## 1. 官方温度链(函数级,已反编译)
调用关系(CoreSDKLib.dll,基址 0x180000000):
| 函数 | 作用 |
|---|---|
| `MAG_GetTemperatureData(chan,buf,bytes,flag)` @0x180003fc0 | 主路径:逐像素 `sub_180016290(counts, pixelIdx, 1)`,flag≠0 或 `dev+0x78≠0` 时再套 `sub_180015b90` |
| `MAG_GetTemperatureData_Raw(...)` @0x180004030 | **两点线性标定**:横坐标取当帧 `counts[idxA]`、`counts[idxB]`(`dev+0x44cc0/4`),
纵坐标取 `dev+0x44cb0/4`(由 `sub_18000d2a0` 每帧用 `sub_180016290` 算出),逐像素线性插值 |
| `MAG_GetTemperatureProbe(chan,x,y,mode)` @0x1800045a0 | 探针:先 `sub_180014e70` 做**邻域平均**(mode→1×1/3×3/5×5),再同上 |
| `sub_180016290(dev,counts,pixIdx,flag)` @0x180016290 | **核心换算**,三个分支(见下) |
| `sub_180015b90(dev,T,float)` @0x180015b90 | 修正(发射率/透过率/环境)——装配代码见 §3 |
| `sub_180015840` / `sub_1800158b0` | 曲线 A 的前向(温度域→辐亮度域)/ 逆向插值 |
| `sub_180015A40` / `sub_180015940` | 在曲线 A 上做"增量"换算(把辐亮度增量变回温度增量) |
| `sub_18000d2a0` @0x18000d2a0 | 每帧温度收尾:算 `dev+0x44cb0/4/8/ccc/cd0`(两点标定端点、均值温度),并按 `dev+0x78` 决定是否修正 |
### `sub_180016290` 的三分支(决定我们该走哪条)
```
if (dev+0x41880 == 0):
if (dev+0x47950 != 0): # T2E 表分支
v = counts << (7 - dev+0x47954) # 注意:不是 counts*3-5797!
二分查找 curveA(646 项) → (v-A[i])*S[i]>>12 + (i<<12) - 150000
else: # 线性分支
return dev+0x47948 + (counts << dev+0x4794c)
else: # 放射学分支(本机 = 这条)
eps = *(float*)(rec+0x204)
if eps<=0.01 or eps>=0.99: # 本机 eps=1.0 → 进入
若 rec+0xAC/B0/B4/BC 均有效(>=1/>0) 且 flag!=0: 走"fix"路径(本机这些字段=0 → 跳过)
else: ... 发射率反演 ...
波段映射:band = 阈值(rec+0x40 起 8 个)分段
T = off[band] + ((counts * gain[band]) >> rec+0x30(=12)) # gain/off 在 rec+0xD4 起,步长 8
环境补偿(v44..v54,见 §2)→ v54
if flag: 距离/大气项(dev+0xA8、dev+0x44CB8 等,见 §2)
return T + sub_180015A40(T, v54)
```
**关键结论**:本机 `dev+0x41880 = 1`,即官方走**放射学分支 + 波段映射**,
而我们 App 现在用的是 `TempMath.countsToTempMc`(= T2E 表分支)——**分支都不同**,
这就是温度系统差的根因(而不是"表差一点")。
## 2. 本机运行时读到的参数(同一帧,ctypes 直读官方设备对象)
| 字段 | 值 | 含义 |
|---|---|---|
| `dev+0x41880` | 1 | 走放射学分支 |
| `dev+0x47950 / 0x4794c / 0x47948` | 0 / 3 / −50000 | 表分支关闭;线性分支参数(−50000 与 DDT 头部字段一致) |
| `rec = dev+0x41888`(540B/记录,索引 `dev+0x98`,上限 `dev+0x41884`=1) | | |
| `rec+0x30` | 12 | gain 右移位数 |
| `rec+0x34 / 0x38` | 29917 / 29417 | 差 = −500(与 DDT 第二组锚点 −500 呼应),用于环境补偿 |
| `rec+0x3c` | 8 | 波段数 |
| `rec+0x40…0x5C` | 9389, 9995, 10328, 10581, 10797, 11160, 11430, 11772 | 波段阈值(计数域) |
| `rec+0xD4…0x124` | (33486,−51932) (33174,−51216) (36271,−58774) (32637,−49610) (36280,−59022) (32842,−49958) (29110,−39789) (35059,−56391) | 每波段 (gain, offset),单位 mC |
| `rec+0x204` | 1.0 | 发射率(1.0 → 不走发射率反演) |
| `rec+0x208 / 0x20C` | 19790 / 44930 | 距离/大气项阈值对 |
| `dev+0x54 / 0x58 / 0x5C / 0x60 / 0x64` | 34412 / 33912 / 34382 / 33882 / 1 | 环境补偿输入(传感器/环境温度对) |
| `dev+0xA8`(float) | −0.0279 | 距离/大气系数(非 0 → 该项参与) |
| `dev+0x44CB8` | 29564 | 当帧**均值温度**(`sub_18000d2a0` 写入) |
| `dev+0xCC / 0x78 / 0xD4 / 0xD8` | 2 / 0 / 0.98 / 29.45 ℃ | 修正开关 / 修正使能 / 调用方传入的 ε·τ 因子 / 环境温度 |
| `dev+0xF4 / 0xF8` | 0.99991 / 1.0 | 修正公式里的两个 (0,1] 因子(K = f8·f4·a3) |
| 曲线 A `0x180099000`(646×i32) | 51,70,94,125,162,208,… | **= 我们 OfficialTables.T2E 已经一致** |
| 斜率表 `0x180099A20`(646×i32) | | 逆向插值用的局部斜率(我们表里未导出,需补) |
| `dev+0xAC…0xC8`(8×i32) | 全 0 | 环境温度修正查表(本机为 0) |
**DDT 文件尾部就是这条记录**:`official.ddt`(1856416B) 从 1855856 起可对齐到
rec 的波段阈值(1855940=9389…)、gain/offset(1856088=33486…)、
`0x34/0x38`(1855932/1855928 = 29917/29417)、`rec+0x204` 区(1856372 起 eps=1.0、19790、44930)。
→ App 只要解析 DDT 尾部即可拿到全部温度标定参数(无需额外设备指令)。
## 3. `sub_180015b90`(修正)汇编要点
```
if (dev+0xCC == 0) return T; # 开关
需要 0 < dev+0xF4 <= 1、0 < a3(传入 0.98) <= 1、0 < dev+0xF8 <= 1,否则原样返回
K = dev+0xF8 * dev+0xF4 * a3
Lobj = sub_180015840(T) # 温度域 → 曲线 A 的"辐亮度"域
Lamb = sub_180015840( (int)(dev+0xD8 * 1000) ) # 环境温度 → 辐亮度
Teff = sub_1800158b0( (Lobj - (1-K)*Lamb) / K ) # 反演回温度
Teff = max(Teff, -273150)
idx = clamp((dev+0x5C - 15000)/5000, 0, 6) # 环境温度查表(本机表全 0 → 无修正)
Tout = Teff + tab[idx] + (tab[idx+1]-tab[idx])*(dev+0x5C-5000*(idx+3))/5000
若 dev+0x6C != 0: Tout += (int)(Tout*dev+0xEC) - (int)(dev+0xF0 * 0.4f)
```
(这就是官方"温度修正参数"里的发射率/透过率/环境温度的落点。)
## 4. 验证状态(官方同帧数据)
- 采集:`tsdk_pair3` + ctypes 直读(`dev+0x270` 计数缓冲、`dev+0x41888` 记录、
`MAG_GetTemperatureData(flag=1)` 输出)。
- **配对必须同帧**:`MAG_GetTemperatureData` 是按"当前帧"现算的;先用
`ThermalSDK.Stop()` 冻结缓冲可保证同帧(但 Stop 后温度接口返回失败,故只能
"先取温度、立刻取计数、并校验同计数下温度唯一(spread=0)")。
- 结果:
- **波段映射单独就复现了官方输出的主体**:flat 背景所在的 band 1
拟合残差仅 **3.2 mC**,说明"阈值 + 每波段 gain/offset"结构完全正确;
- 但整体仍有 **≈230 mC 的平均差**,且残差随计数平滑变化——对应 §2 里
还没对齐的**环境补偿 (v50) 与距离/大气项**;
- 我按反编译把这两项加上后误差反而变大(≈980 mC),说明其中某个字段的
语义还没读对(`dev+0xA8`/`dev+0x44CB8`/`rec+0x208/0x20C` 的角色待确认)。
- 结论:**结构已定,系数待钉**。下一步只需在这几个字段上做单变量对照
(逐项开关 + 与官方输出比对),不需要再猜公式。
## 4b. 第二轮收敛(同日稍晚):确认公式为 `revise(band_map(counts))`,只剩一个常数项
按"单变量对照 + 数据反解"推进,结论如下(全部脚本与数据在 `%TEMP%\mag160c_wincmp\`):
- **曲线与修正被证实**:
- `E2T`/`iE2T`(`sub_180015840`/`8b0`)往返误差 ≤1 mC(`iE2T(E2T(29000))=28999`);
- 官方输出在 band 1 上是干净直线(拟合残差 **3.3 mC**,斜率 8.26479);
- 我的模型 `revise(band_map)` 的斜率 **8.26451**(差 0.003 %)→ **revise 就是 1/K 缩放**
(K = f8·f4·ε = 1.0×0.99991×0.98 = 0.979912,1/K = 1.020500)。
- **只剩一个常数**:把官方数据反解回波段域(对 revise 求逆)后,与记录的波段映射
相差 **恒定 −197 mC**(环境取 `dev+0xD8`,即反编译字面值)或 **−95 mC**
(环境取 `dev+0x5C`);与计数无关 → 是**加性项**,不是斜率/表格问题。
- **已排除**(逐项实测,非推断):
- 记录索引/第二条记录:`dev+0x98=0`,`dev+0x41884=1`,第 2 条记录**全 0**(未用);
- 记录的 gain/移位:与数据反解出的斜率**逐位吻合**(band1:记录 8.09905 × 1/K = 8.2645 ≈ 数据 8.2648);
- 环境修正查表 `dev+0xAC…0xC8`:**全 0**;
- `dev+0xEC/0xF0` 增益项:**0/0**(`dev+0x6C=1` 但系数为 0);
- 环境补偿 `v50/v54`:本机 `dev+0x54==0x5C`、`0x58==0x60` → `v50≈30`、`v54≈11`
(≈ +32 mC,方向相反,不是这 −197);
- 距离/大气项(`dev+0xA8=−0.0279`,v56=`dev+0x44CB8` 均值温度):按反编译算出的
增量 ≈ −320(辐射域)≈ −800 mC,**与官方输出不符 → 该项的实际语义仍未读对**
(可能 `dev+0xA8` 需与其他字段组合,或该项仅在对焦/距离参数被设置时才生效)。
- 观察到的可疑未读字段:`dev+0x50 = 200`(int)、`dev+0x14 = 20000`(温度样)、
`dev+0xD0 = 0.65`(float)——需与官方输出做单变量对照确认。
**下一步(精确做法,不猜)**:
1. 在 SDK 的 `SetNewIRFrameDelegate` 回调里读设备字段(最接近温度计算时刻),对同一个
count 记录 (字段值, 官方输出) 序列;看 −197 是否随某个字段变化 → 定位该字段。
2. 若 `dev+0xA8` 距离项确需生效,用 `MAG_SetFixPara`/`MAG_FixTemperature` 改变距离参数
再采一次,观察官方输出的变化量,反推该项的正确组合方式。
3. 钉死后即按 §5 落地 Kotlin,并用本轮同帧数据做逐像素回归(band 1 目标 mC 级)。
## 4c. 第三轮:多帧序列 + 相关性分析(当前状态:差一个约 0.15 ℃ 的静态项)
采集 15 帧同帧配对样本(`offseries.pkl`,脚本 `offseries4.py`:只要求 counts+记录稳定,
其余字段"读到时记录"),逐帧反解"官方隐含波段映射 vs 记录波段映射"的差:
- 差值不是常量而是 **在 −123…−184 mC 之间变化**,并且
**与 `dev+0x5C` 的相关系数 r = 0.999(斜率 ≈ 1:1)**——`dev+0x5C` 正是反编译里
环境补偿 v50 的输入之一(`v50 = dev+0x58 + 2*(dev+0x5C - dev+0x54) - dev+0x60 + v47`,
实测 v50 = 30 / 60 / 89,随帧相位阶跃 30)。
- 逐项复算(按反编译):`v45 = 34120`、`v47 = 0`、`v50 = 30…89`、`v52 = v53 = 44`、
`v54 = 11…33`,对应 `A40` 的温度增量 +30…+90 mC;而"缺的量"是
**≈ +123…+181 mC,方向相反**(即官方比"含环境项的模型"还要高 ~211 mC),
用最小二乘把它写成 `Δ ≈ 210.8 − 0.98·v50`(15 帧内拟合残差 ±3 mC)。
- 该"211"**不是存储常量**:设备对象 0x0–0x1200、记录 540B、DDT 尾部 560B 里都搜不到
211/−211,也没有 0.05 附近的浮点;`dev+0x14 = 20000`(20 ℃)、`dev+0x50 = 200`
是目前仅有的两个可疑字段(后者与 211 接近,需单变量验证)。
- 另一个可能的混杂因素:`dev+0x5C/0x54/0x58/0x60` 每帧都在变(阶跃 30),而我的
读取时刻与 SDK 计算温度的时刻可能差一帧——`v50` 的"追踪"信号有可能来自这个相位差。
**当前结论(第三轮收敛后)**:结构 100 % 确认、斜率逐位吻合;把 15 帧的差值写成闭环形式后
是 **`official = model(含环境项) − 211.3 mC`**(15 帧内该项稳定在 −211.3 ± 1 mC,
与 v50 无关;v50 的"追踪"只是我早期把 Δ 写成"v50 − 211.3"造成的表象)。
也就是说:**只剩一个 −211.3 mC(≈0.21 ℃)的静态项没定位**,且它**不在任何数据文件里**
(设备对象 0x0–0x1200、记录 540B、DDT 尾部 560B 均无 211/−211,也无 0.05 量级浮点);
按反编译算出的距离/大气项是 **−995 mC**(≈4.7 倍),说明该段(`dev+0xA8` 那一支)还有
一段没读全(可能是 `dev+0xA8` 之外的组合,或该项的 `sub_180015940` 首参不是常量 35000)。
工程判断:这已经是"+0.09 ℃(常温)…−0.70 ℃(高温)"→"−0.21 ℃ 常数"的差距,但**未定位前不改代码**
(否则就是把 −211.3 这个拟合常数硬编码进去,违反"不猜"原则)。
## 4d. 第四轮:**模型闭合(平均 13.9 mC)**——根因是 Hex-Rays 的十进制/十六进制陷阱
第三轮卡住的 −211.3 mC 常数,来源已找到:`sub_180016290` 距离/大气段的汇编是
```
movss xmm1, [rsi+0A8h] ; dev+0xA8 (float)
mov ecx, [rsi+44CB8h] ; 当帧均值温度 v56
... clamp v56 到 [5000,40000](或 ±80000)...
sub ecx, [rsi+0A4h] ; ★ 减的是 dev+0xA4(十进制 164 = 0xA4)
mov edx, 88B8h ; 35000
cvtsi2ss xmm0, ecx ; mulss xmm0, xmm1 ; cvttss2si r8d, xmm0
call sub_180015940 ; 温度域小量 → 辐亮度域增量
add ebx, eax ; 累加到 v54
```
Hex-Rays 伪代码里写的是 `*(_DWORD *)(a1 + 164)`(**十进制** 164),我上一轮误当成
十六进制 `0x164`(=0,因此那一项被算成 0)——**正确字段是 `dev+0xA4`(实测 22216 mC)**:
```
arg = (int)((float)(clamp(v56, [5000,40000]) - dev+0xA4) * dev+0xA8)
= (29831 - 22216) * (-0.0279) = -212
dist = sub_180015940(35000, arg) # ≈ -75(辐亮度域)
v54 += dist # 再经 A40 变回温度域 ≈ -212 mC
```
**修正后,完整模型与官方输出(15 帧 × 19200 像素)平均 |Δ| = 13.9 mC(0.014 ℃)**,
全部 15 帧一致(13.4–14.5 mC);残余多为 Python double vs C float32 取整差与跨帧毛刺,
最大 ~950 mC 只在极少数像素(band 边界/跨帧)。
→ 同时也解释了之前"数值对不上"的另一半原因:`dev+0xA4` 我根本没有读到过
(它不在 DDT 里,是设备上报的运行参数)。
### 已完整验证的算法(可直接落地,逐条对应反编译)
```text
A[0..645] = 曲线 A(= 我们 OfficialTables.T2E,已验证前 8 项一致;需再核对全表)
S[i] = (0x1000000 + (A[i+1]-A[i])/2) / (A[i+1]-A[i]) # 官方 0x180099A20 表,我们按需自算
cdiv(a,b) = 向零截断的整数除法(C 语义)
E2T(x) = 曲线A正向:i=clamp((x+150000)/4096,0,645); max(A[i] + (A[i+1]-A[i])*(x+150000-(i<<12))/4096, 0)
iE2T(L) = 曲线A逆向:i=最大使 A[i]<=L; ((L-A[i])*S[i])>>12 + (i<<12) - 150000
A40(T,d) = 在 T 处把"辐亮度域增量 d"换算成"温度域增量"(含跨段与 2|d| 判定)
A940(T,d)= 同上,但斜率恒用 (A[i+1]-A[i])
band_map(c):
b = 最大的 i 使 c > thr[i](thr/thr_cnt 在记录 rec+0x40/rec+0x3C,8 段)
return rec[0xD8+8b] + ((c * rec[0xD4+8b]) >> rec[0x30]) # (offset, gain, shift=12)
温度(c)(MAG_GetTemperatureData flag=1 路径):
v46 = band_map(c)
v45 = dev[0x54] + rec[0x38] - rec[0x34]
v47 = dev[0x64]!=0 ? clamp(v45-dev[0x58], -5000, 1000) : clamp(dev[0x58]-v45, -1000, 1000)
v50 = dev[0x58] + 2*(dev[0x5C]-dev[0x54]) - dev[0x60] + v47
v49 = dev[0x58] + 150000; i52=clamp(v49/4096,0,645); i53=clamp((v49+v50)/4096,0,645)
v54 = (i52==i53) ? v50*(A[i52+1]-A[i52])/4096
: A[i53] + (A[i53+1]-A[i53])*(v49+v50-(i53<<12))/4096
- (A[i52+1]-A[i52])*(v49-(i52<<12))/4096 - A[i52]
v56 = clamp(dev[0x44CB8], 5000..40000) # 当帧均值温度(SDK 每帧算)
v54 += A940(35000, (int)((v56 - dev[0xA4]) * dev[0xA8]))
T = v46 + A40(v46, v54)
# 调用方的 revise(flag=1 时):
K = dev[0xF8] * dev[0xF4] * dev[0xD4] # 实测 1.0 × 0.99991 × 0.98 = 0.979912
Lamb = E2T((int)(dev[0xD8] * 1000.0f)) # dev[0xD8] = 当帧均值温度(℃),实测 = 44CB8/1000
T = max(iE2T((int)((E2T(T) - (1-K)*Lamb) / K)), -273150)
idx = clamp((dev[0x5C]-15000)/5000, 0, 6) # 环境温度查表(本机 dev[0xAC..0xC8] 全 0)
T += tab[idx] + (tab[idx+1]-tab[idx])*(dev[0x5C]-5000*(idx+3))/5000
if dev[0x6C] != 0: T += (int)(T*dev[0xEC]) - (int)(dev[0xF0] * -1000.0f)
```
### DDT 尾部 = 标定记录(已完整对齐,App 可直接解析)
相对 `DDT_size - 560`(本机 `official.ddt` = 1856416,故起点 1855856):
| 尾部偏移 | 值(本机) | 含义 |
|---|---|---|
| +0x34 | 0.0065f | (未用,待确认) |
| +0x38 / +0x3C / +0x40 | 1.0f / 2.0f / 1.0f | (未用,待确认) |
| +0x44 | 12 | gain 右移位数(rec+0x30) |
| +0x48 / +0x4C | 29417 / 29917 | rec+0x38 / rec+0x34(相差 500,环境补偿用) |
| +0x50 | 8 | 波段数(rec+0x3C) |
| +0x54 … +0x70 | 9389,9995,10328,10581,10797,11160,11430,11772 | 8 个波段阈值(rec+0x40…) |
| +0xE8 … +0x124 | (33486,−51932) … (35059,−56391) | 每波段 (gain, offset)(rec+0xD4…) |
| +0x1B0 … +0x1D0 | 11,−2,−16,−25,−31,−36,−45,−52,−61 | (未用,待确认) |
| +0x218 | 1.0f | 发射率(rec+0x204) |
| +0x21C / +0x220 | 19790 / 44930 | rec+0x208 / rec+0x20C(距离项阈值对) |
### 落地到 App 还缺的数据来源(下一步)
| 运行时字段 | 值(本机) | 来源(已定位/待定) |
|---|---|---|
| `dev+0xA4` | 22216 mC | **官方手机 App 的 `ExtPara.intCurrentEnvTemperature`**(`DeviceController.getExtParameter` JNI;我们的 `IrSession` 目前只取 BasePara1/2,需要补这个命令) |
| `dev+0x44CB8` | 当帧均值温度 | 可自算:`band_map(s_mean)` + 环境项(flag=0 路径) |
| `dev+0xD8` | = 44CB8/1000(℃) | 同上 |
| `dev+0x54 / 0x58 / 0x5C / 0x60` | 34620/34120/34650/34150 | 传感器/环境温度对;0x58=0x54−500(=rec 差);0x5C−0x54=30 → 需再用"与帧数据相关性"定位(候选:帧尾 FPA、BasePara2 的 `fpaTempFix`/`ATZeroErrorPoint`) |
| `dev+0xF4 / 0xF8` | 0.99991 / 1.0 | 修正系数;候选 `BasePara2.ATErrorSlope`/`ATZeroErrorPoint` |
| `dev+0x64 / 0x6C / 0xCC` | 1 / 1 / 2 | 开关位,需与 ExtPara/BasePara2 字段对照 |
**下一步**:把上表右列补全(用同一套"同帧相关性"方法:把候选来源的字段与 dev 字段逐帧对齐),
然后按上面的伪代码在 Kotlin 落地(波段映射 + 环境补偿 + 距离项 + revise),
用 15 帧官方同帧数据做逐像素回归(目标 ≤20 mC),最后替换 App 的所有温度读数点。
## 4e. 第五轮:数据来源全部定位(`MAG_SetFixPara` / BasePara2 / 斜率表)
本轮的三个关键发现,把上表右列基本填满:
1. **斜率表不用导**:`FUN_1800512c0`/`FUN_180050fc0` 之外的**初始化代码**里,
`DAT_180099A20[i] = ((Δ >> 1) + 0x1000000) / Δ`,`Δ = A[i+1] - A[i]` ——
与我们在 Kotlin 里现算的公式**完全一致**(645/645 项 ±1 内吻合)。
→ 曲线 A(= `OfficialTables.T2E`,**646 项逐项相等**)已够用。
2. **`dev+0xA4` / `dev+0xA8` = 设备上报的 BasePara2 字段**:
- `dev+0xA4` = 22216 mC = **`BasePara2.ATZeroErrorPoint`**(环境温度误差零点,22.2 ℃)
- `dev+0xA8` = −0.0279 = **`BasePara2.ATErrorSlope`**(环境温度误差斜率)
→ 这一段(我先前叫"距离/大气项")本质是**环境温度误差修正**:
`arg = (当帧均值温度 − ATZeroErrorPoint) × ATErrorSlope`,本机 = −212 mC。
官方手机 App 里对应 `sdk/BasePara2.java` 的同名字段;我们的 `IrSession` 已经发了 66c
拿到 BasePara2,只是**当前仅打印不解析**——补上解析即可。
3. **"温度修正参数"结构 = `dev+0xD0` 起 44 字节**(`MAG_GetFixPara` 直接 memcpy 0x2c):
`MAG_SetFixPara(chan, para, enable)` 写 `dev+0xD0` 并调 `FUN_180014fe0` 重算:
其中 `param_2[9]`(= `dev+0xF4`)由 `(发射率等参数 + 环境温度查表) → 指数/多项式` 算出
(本机 0.99991);`param_2[2]` 是环境温度(整度,clamp −25…35,查表用);
`param_2[4]` 作除数(距离/湿度类);`enable` 写 `dev+0xCC`(本机 2)。
→ `dev+0xF4/F8/CC/6C/D8` 都能由"修正参数 + 每帧均值温度"得到(默认参数见 §2 实测值)。
**仍然待定的一项**:`dev+0x54 / 0x58 / 0x5C / 0x60`(两组传感器温度对)的来源。
已知:`0x58 = 0x54 − 500`、`0x60 = 0x5C − 500`(500 = 记录的 `rec+0x34 − rec+0x38`),
且 `0x54` 在一段采集里保持不变、`0x5C` 缓慢上升(差值 0/30/89 mC)——
很像"上次 FFC 时的 FPA 温度"与"当前 FPA 温度"。实验方法(下轮):
读 SDK 设备对象里的**帧解析缓冲**(`*(void**)(dev+0x2d20)` 指向 28B 头 + payload + 28B 尾),
把 `0x5C/0x54` 与帧头/帧尾各字段逐帧做相关性 → 直接确定它们来自帧的哪个字节。
(帧尾 `+8` = FPA 温度已经由我们的 NUC 逐位吻合实验证明 = `dev+0x54`。)
## 4f. 第六轮:最后一项也定位了 —— `dev+0x5C = dev+0x54 + fpaTempFix`
多次采样发现 `dev+0x5C − dev+0x54 ∈ {0, 30}` 且 `dev+0x54` 恒定(= 帧尾 FPA 温度,
已由 NUC 逐位吻合实验证明),`dev+0x58 = dev+0x54 − 500`、`dev+0x60 = dev+0x5C − 500`。
代入环境补偿公式:
```
v45 = dev54 + rec38 − rec34 = dev54 − 500
v47 = clamp(v45 − dev58, −5000, 1000) = clamp(0) = 0
v50 = dev58 + 2*(dev5C − dev54) − dev60 + v47
= (dev54 − 500) + 2*fix − (dev54 + fix − 500) + 0
= fix
```
→ **`v50 = fpaTempFix`**(`BasePara2.fpaTempFix`,设备上报的 FPA 温度修正,本机 30 mC),
与实测的 v50 = 30/60/89 完全一致(0x5C−0x54 的跳变就是 fpaTempFix 的变化)。
### 结论:温度链的**全部输入**都已定位,App 侧数据源如下
| 输入 | 来源(App 可直接拿) |
|---|---|
| 曲线 A(646 项) | `OfficialTables.T2E`(**与官方模块逐项相等,已验证**) |
| 斜率表 S | 现算 `(0x1000000 + Δ/2)/Δ`(官方也是运行时算的,645/645 吻合) |
| 波段记录(阈值/gain/offset/shift/±500 温度对/eps/19790/44930) | **DDT 尾部**(§4d 偏移表) |
| `dev+0x54` | 帧尾 `+8` 的 FPA 温度(= 管线已有 `shutter`) |
| `dev+0xA4` / `dev+0xA8` | **BasePara2.ATZeroErrorPoint / ATErrorSlope**(66c 已收到,需解析) |
| `v50` | **BasePara2.fpaTempFix** |
| `dev+0x44CB8`、`dev+0xD8` | 当帧均值温度(= `band_map(s_mean)` + 环境项,可自算);`0xD8` = 它/1000 |
| `dev+0xD0..0xFB`(修正参数) | 发射率 0.98、环境温度(默认=场景均值)、距离/湿度默认值;`dev+0xF4` 由 `FUN_180014fe0` 计算(本机 0.99991,**≈1 时对温度影响 <1 mC,可先取 1.0**) |
| `dev+0xCC/0x6C/0x64` | 开关(本机 2 / 1 / 1) |
→ **落地 Kotlin 已无未知项**:按 §4d 伪代码实现 + 解析 DDT 尾部 + 解析 BasePara2
(`ATErrorSlope`/`ATZeroErrorPoint`/`fpaTempFix`),用 15 帧官方同帧数据回归(目标 ≤20 mC)。
**唯一待确认的小事**:`BasePara2` 的**线上字节偏移**(Java 类字段顺序 ≠ 报文顺序)。
libcxsdk 反编译里没有按名写字段的痕迹,因此最快的办法是:设备连手机时 dump 一次
BasePara2 原始报文,用已知值 `22216`(ATZeroErrorPoint)与 `-0.0279`(ATErrorSlope,
float 位模式 `0xBCE4…`)定位偏移;或在 PC 侧从官方 DLL 的设备对象里找这两个值的原始副本。
在此之前,App 可先用"默认修正参数"跑通(本机默认:ATZeroErrorPoint=22216、
ATErrorSlope=−0.0279、fpaTempFix=30、发射率 0.98、f4≈1.0),误差 <1 mC。
## 5. 我们 App 侧要改什么(等模型钉死后再动手)
1. `TempMath` 增加"官方放射学分支"实现:波段映射(参数取自 DDT 尾部)+ §3 修正;
保留 `countsToTempMc` 作为回退(T2E 表分支)。
2. 需要新增导出的表:曲线 A 的**斜率表** `0x180099A20`(我们只导了 T2E);
以及 DDT 尾部的波段记录解析。
3. 温度读数/分析/远程/PDF 全部改用新路径(`RenderPipeline.probeTemp`、
`LiveViewModel` 的最高最低中心温、`AnalyzeViewModel`、`RemoteViewerViewModel`)。
4. 回归:用本轮采集的官方同帧数据做逐像素断言(band 1 应到 mC 级)。
## 6. 复现与工具(都在临时目录,未入库)
- IDA 批处理:`idat.exe -A -S<script.py> -Llog.txt <db.i64>`(脚本见
`%TEMP%\mag160c_wincmp\ida\dump_temp.py|dump_helpers.py|dump_revise.py|dis_revise.py`)。
仓库内 `analysis/ida/CoreSDKLib.dll.i64` 是原始库;批处理请先复制一份再跑(GUI/批处理会互锁)。
- 运行时参数/内存:`%TEMP%\mag160c_wincmp\offdump2.py`、`offpair.py`、`offmodel.py`。
- 官方同帧基准:`%TEMP%\mag160c_wincmp\off20\`、`offmodel.pkl`、`offpair.pkl`、`offverify.pkl`。
- IDA MCP(供交互式使用):`pip install "mcp<2" ida-pro-mcp` 已装,插件已放进
`%APPDATA%\Hex-Rays\IDA Pro\plugins\mcp-plugin.py`,ZCode 用户配置
`~/.zcode/cli/config.json` 已加 `mcp.servers."ida-pro"`;**需重启 ZCode 生效**,
且 IDA 里要先 `Edit → Plugins → MCP`(Ctrl+Alt+M)启动 13337 端口。