Files
MAG160C/docs/android_app/temperature_chain_re_20261006.md
T

20 KiB
Raw Blame History

官方温度链逆向(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 里,是设备上报的运行参数)。

已完整验证的算法(可直接落地,逐条对应反编译)

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 的所有温度读数点。

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 端口。