367 lines
25 KiB
Markdown
367 lines
25 KiB
Markdown
# 官方温度链逆向(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 端口。
|