thiswillbeyourgithub/ntrig-calib
GitHub: thiswillbeyourgithub/ntrig-calib
通过逆向工程 NCP 协议,在 Linux 上复现 Windows 校准流程以修复 Surface Pro 3 触摸屏死区问题的 Python 工具。
Stars: 1 | Forks: 0
# ntrig-calib — Surface Pro 3 触摸屏死区修复工具 (Linux)
### 查看实际效果
[](https://www.youtube.com/watch?v=mVX-7ZI8ysk)
*底边可见的死区 — 通过软件重新校准修复(视频由 [René Rebe](https://rene.rebe.de/2017-07-29/n-trig-touch-screens-occasionally-need-re-calibration/) 制作)*
**此工具由 Claude Opus 4.6(Anthropic 的 AI 助手)在扩展思维模式下开发。** 仅凭症状描述,Claude 诊断出了根本原因,在线找到了 Sony VAIO 更新包,对其 XOR 反转的 cabinet 存档进行了去混淆处理,使用 [Ghidra](https://ghidra-sre.org/) 从提取的 DLL 中逆向工程了专有的 NCP 协议,并跨越多个会话编写了可工作的 Python 脚本。后续的改进和文档更新可能使用不同的 Claude 模型。完整故事见下文。
## 目录
- [问题所在](#the-problem)
- [已知限制与未知项](#known-limitations-and-unknowns)
- [修复方案](#the-fix)
- [完整故事](#the-full-story)
- [用法](#usage)
- [致谢](#credits)
- [相关问题](#related-issues)
- [许可证](#license)
## 问题所在
Surface Pro 3 设备的触摸屏上会出现一条**水平的或垂直的死区**,在该区域触摸输入完全没有响应。笔输入在死区仍然有效。该死区在重启、驱动重装和内核升级后依然存在。
**这不是驱动 bug。** 这是 N-Trig DuoSense 数字化仪中固件级别的校准漂移。校准数据存储在数字化仪自身的非易失性存储器中,并随时间推移而退化,与操作系统无关。
一个已知的触发因素是 **Type Cover 的磁性连接条**:键盘盖的磁铁与屏幕边缘的反复接触可能会损坏受影响区域的校准数据。多位 SP3 用户报告称,在频繁使用 Type Cover 后,底边会特别出现死区。
## 已知限制与未知项
**此脚本基于逆向工程和实验发现:**
- NCP 响应通道(报告 0x06)仅在单个未记录的会话中被观察到,但**尚未在不同设备或内核版本间得到验证**。
- 校准成功或失败的确切条件尚未完全理解。
- 效果持续时间不一:有些设备能长期保持校准状态,而另一些则在几天内再次出现漂移(见下文 [关于持久性的说明](#note-on-longevity))。
- Linux 特定行为:Windows 设备树中存在的 `direct-path` 报告 ID(0x29–0x2D)在 Linux HID 描述符中缺失,这可能表明 Linux 与 Windows 上的固件行为不同。
**如果您测试此脚本,请报告您的结果** —— 包括成功和失败的情况 —— 并包含您的内核版本、设备配置和诊断输出。社区验证将有助于确定该脚本是否在不同 Surface Pro 3 设备和配置上可靠运行。
## 修复方案
在 Windows 上运行 `CalibG4.exe` 会重新校准数字化仪的固件。由于校准数据存储在数字化仪自身的 EEPROM 中 —— 而不是 Windows 中 —— 因此只需从 Windows 运行一次,即可修复 Linux 上的死区。
**此脚本在 Linux 上执行相同的操作,无需 Windows。**
## 完整故事
### 背景
整个过程 —— 从诊断到可工作的脚本 —— 均由 Claude Opus 4.6 在扩展思维模式下驱动。用户描述了症状(触摸死区,笔仍可用)。Claude 将其识别为可通过软件解决的已知 N-Trig 固件校准问题,在线定位了 Sony VAIO 更新包中的 `CalibG4.exe`,对 XOR 反转的 cabinet 进行去混淆,使用 [Ghidra](https://ghidra-sre.org/) 从 DLL 逆向工程了专有的 NCP 协议,并跨多个会话编写了 Python 脚本。用户的角色是描述问题并测试结果。下文所有分步技术描述(逆向工程阶段、协议分析、诊断)均由 Claude 编写 —— 用户不具备记录这些发现所需的逆向工程和底层协议分析技能。脚本或文档的后续改进可能涉及其他 Claude 模型。
Windows 修复工具(`CalibG4.exe`)通过一种称为 **NCP (N-Trig Communication Protocol)** 的专有二进制协议与 N-Trig 数字化仪通信。该工具仅作为 Sony VAIO 驱动更新包的一部分分发,从未公开存在任何文档或 Linux 等效版本。
### 寻找工具
Claude 在 `EP0000601624.exe` 中定位到了 `CalibG4.exe`,这是一个 Sony VAIO Update 自解压封装程序,可从 Sony 支持页面获取,多年来在 Surface 论坛中被引用为 SP3 死区的修复方案([示例帖子](https://answers.microsoft.com/en-us/surface/forum/all/does-anyone-still-have-the-calibg4exe-touch-screen/eb5376c3-1e59-474a-80df-00f918c8f9a6))。
提取它并不直观:该封装程序在 PE 资源 blob 中嵌入了一个 **XOR 反转**(每个字节与 0xFF 异或)的 cabinet 文件。一旦反转字节并执行 `cabextract`,您将得到:
- `CalibG4.exe` (19 KB) —— 校准工具
- `NCPTransportInterface.dll` (151 KB) —— HID 通信层
### 逆向工程 DLL
随后 Claude 使用 [Ghidra](https://ghidra-sre.org/) 反汇编 DLL,以确切了解需要通过 Linux hidraw 接口发送哪些字节。
**阶段 1 —— 初始分析** 揭示:
- `CalibG4.exe` 发送两个 NCP 命令:`START_CALIB` (group=0x20, id=0x0A) 并轮询 `GET_STATUS` (group=0x20, id=0x0B)
- 状态响应:`\x42\x42\x42` ("BBB") = 完成,`\x63\x63\x63` ("ccc") = 进行中,`\x21\x21\x21` ("!!!") = 未知/中间状态
- 所有实际通信均通过 `NCPTransportInterface.dll` 进行
**阶段 2 —— NCP 帧格式**(来自 `0x18000D0D0` 的 Ghidra 反汇编):
```
Byte 0: 0x7E — start marker
Bytes 1-2: Module ID (LE uint16) — DLL derives this via UuidCreate() at runtime; 0x0001 is a working hardcoded substitute
Bytes 3-4: Total frame size (LE uint16) = 14 + payload_len + 1
Byte 5: Flags: 0x01 = expects response, 0x41 = fire-and-forget
Byte 6: Command group (0x20 = calibration)
Byte 7: Command ID (0x0A = start, 0x0B = get status)
Bytes 8-11: Sequence number (LE uint32) — usually 0
Bytes 12-13: Reserved (0x00)
Bytes 14..: Payload
Last byte: Checksum = (-sum(signed_bytes)) & 0xFF
```
**阶段 3 —— hidraw 死胡同 (v1–v3)**
Linux 暴露的 HID 报告描述符为 **455 字节**,定义了 **16 个报告 ID**:`0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x11, 0x15, 0x18, 0x1B, 0x58`。阶段 4 中提到的 "direct path" 报告 `0x29–0x35` 在此描述符中物理缺失 —— 它们也不存在于固件的原生 I2C-HID 描述符中(关于为何它们出现在 Windows 设备树而非 Linux 上的精确解释,见阶段 4)。
早期尝试通过报告 0x1B(259 字节,描述符中最大的供应商报告)上的 `HIDIOCG/SFEATURE` 发送 NCP 帧。SET_FEATURE 会成功(内核接受),但设备从不响应。诊断探测循环中的一个 bug(它按顺序测试缓冲区大小 `[8, 16, 34, 65, 260, 514]` 并在首次“成功”时停止)导致 0x1B 读取在 8 字节处停止,仅返回截断的前缀 `29 a9 19 9f 9a 19 a4 ...` —— 从未返回完整的 259 字节响应。使用正确的 260 字节缓冲区时,响应包含 256 个非零字节。在 v4 中使用正确的 260 字节缓冲区重新测试 0x1B 上的 SET_FEATURE,证实其仅递增报告 0x03(事务计数器),无 NCP 响应。阶段 4 的 DLL 深度分析后来明确证实了 0x1B 不在 DLL 的发送路径中这一结论。报告 0x03 在每次写入时都会变化,这看起来很有希望,但结果证明只是 HID 事务计数器在递增。
原始 I2C 访问也失败了 —— 解绑 `i2c_hid_acpi` 驱动会关闭设备电源,之后它不再响应 I2C 命令。
内核的 `hid-ntrig.c` 驱动(有时在 Surface 论坛中提及)与此无关:它包含 ``,调用 `usb_control_msg()`,且仅匹配 `HID_USB_DEVICE(...)` —— 它无法绑定到 I2C-HID 设备。在内核 6.11+ 上,[HID-BPF](https://docs.kernel.org/hid/hid-bpf.html) 将是一个更简洁的替代方案 —— 它可以在不解绑任何驱动的情况下注入初始化命令。
**阶段 4 —— 深度 DLL 分析 (v4–v5,v5 的突破)**
Claude 在 Ghidra 中反汇编了 I2C 传输类的 vtable,并追踪了完整的调用图。DLL 包含 **两个独立的传输类**,而不是一个函数内的替代分支:
- **I2C 传输** (`0x180008730`,对象大小 `0xB8` 字节):发送函数 `0x1800088C0` 根据能力标志 `[this+0x7c]` 分支:
如果 [this+0x7c] != 0:
→ 分块路径:报告 0x05,61 字节分块
否则:
→ 直接路径:基于大小的报告 ID (0x29–0x2D)
标志 `[this+0x7c]` **由能力探测序列设置**(函数 `0x1800095d0`),该函数针对三种配置(cmd_ids `0x01`, `0x0B`, `0x0C`)测试探测。在运行 Windows 的 SP3 上,`0x0C` 探测推测会成功,启用分块模式;在 Linux 上(由于缺少直接路径集合),设备自动使用分块协议。相对于 USB 传输多出的 8 个字节(位于偏移 `[this+0x70]`–`[this+0x7f]`)保存了分块路径状态,包括 `[this+0x7c]` 标志。
- **USB 传输** (`0x180001280`,对象大小 `0xB0` 字节):使用单独的基于大小的报告 ID 表 (0x2E–0x35)。DLL 在运行时动态加载 `winusb.dll`,表明除 I2C-HID 外还支持 USB 连接的 N-Trig 适配器。
I2C 直接路径报告 ID 由一个 **大小表** 选择(函数 `0x1800011b0`):`<16B→0x29`, `<32B→0x2A`, `<63B→0x2B`, `<255B→0x2C`, `<511B→0x2D`。USB 表类似:`≤17B→0x2E`, `≤33B→0x2F`, `≤64B→0x30`, `≤256B→0x31`, `≤512B→0x32`, `≤4096B→0x35`, `≤8192B→0x34`。
标志 `[this+0x7c]` 由 **能力探测序列** 设置(函数 `0x1800095d0`,该函数使用 `SetupDi` 枚举 + `HidP_GetCaps` 并将结果存储在 `[this+0x30]` 中)。DLL 针对三种配置(cmd_ids `0x01`, `0x0B`, `0x0C`)进行探测:
- 在 Windows 上,`0x0C` 探测推测会成功,标志 `[this+0x7c]` 被设置以启用分块模式。
- 在 Linux 上,那些直接路径报告 ID 不存在(见下文),因此探测失败,标志保持未设置,DLL 默认为分块协议。
这是**软件驱动的回退逻辑**,而非自动硬件行为。回退之所以发生,是因为 **能力探测未能在 Linux 上检测到直接路径支持**,从而在标志层面触发了 DLL 的代码路径决策。
报告 **0x05 是只写的**:对 0x05 进行 `GET_FEATURE` 不返回任何响应 —— 由诊断脚本通过实证确认。
“直接路径”报告(I2C 为 0x29–0x2D,USB-HID 为 0x2E–0x35)映射到 **独立的 HID 集合** —— 即 Windows HID 设备树中不同的 PDO。Windows HID 微驱动可以在 `HIDClass.sys` 解析描述符之前将额外的 HID 集合(带有这些报告 ID)注入到描述符中,这就是为什么这些集合 PDO 存在于 Windows 设备树中,但 ** Linux 上的固件原生 I2C-HID 描述符中缺失**。这也解释了为什么 Linux 暴露的 `/dev/hidrawN` 设备仅显示 16 个基本报告 ID,而从未出现 0x29–0x35 范围。v5 诊断脚本直接在设备上探测了这些报告 ID,未收到任何响应,通过实证确认了它们的缺失。
**当 DLL 的能力探测(函数 `0x1800095d0`)未能在 Linux 上检测到这些直接路径报告 ID 时**,它会设置回退标志 `[this+0x7c]`,导致发送函数 (`0x1800088C0`) 改为通过报告 0x05 使用分块协议。这是 DLL 对缺失能力的刻意软件响应,而非硬件层面的自动行为。**因此,在 Linux 上 DLL 回退到通过报告 0x05 的分块协议** —— 这是一个探测驱动的决策,而非自动的硬件自动化。
**分块协议是主要的 I2C 发送路径**(函数 `0x18000CC80`):
```
Each HID write = 61 bytes:
[0x05] [remaining_chunks] [59 bytes of NCP frame data]
remaining_chunks counts DOWN: last chunk = 0
```
对于 15 字节的 NCP 帧(无载荷),这是一个单次 61 字节写入:
```
[0x05] [0x00] [7e 01 00 0f 00 01 20 0a 00 00 00 00 00 47] [zeros to pad to 61]
```
这是 I2C-HID 设备预期且可靠的路径。直接路径报告 ID (0x29–0x2D) 仅在 Windows 上通过注入的 HID 集合存在;在 Linux 上它们不存在,因此设备自动回退到分块协议。
**阶段 5 —— 异步响应(实验性)**
另一个关键发现:DLL 的接收线程在 HID 设备句柄上使用 `ReadFile`(异步 I/O),而**不是** `HidD_GetFeature`。在 Linux 上,这对应于对 hidraw fd 进行非阻塞 `read()`。之前的脚本版本轮询 GET_FEATURE,完全错过了响应。v5 脚本使用 `select()` + `read()` 来捕获 NCP 响应。
在 DLL 分析过程中,能力探测序列(函数 `0x1800095d0`)识别出三个探测配置,cmd_ids 为 **0x01, 0x0B, 和 0x0C**。基于此分析和 DLL 的特性检测逻辑,**0x0B 和 0x0C 从逆向工程代码结构中成为候选响应通道**。
然而,在单个 Surface Pro 3 设备上的实证测试显示,在一个未记录的测试会话中,NCP 响应实际上是在 **报告 0x06** 上接收到的 —— 这一发现使得该系统成功重新校准。这一观察**不受基于聊天的逆向工程证据支持**,且来自单个未记录的会话。
**⚠️ 关键区别 —— 已验证的候选者 vs. 未验证的观察:**
- **0x0B 和 0x0C** 是**从结构化 DLL 分析中识别出的**候选响应通道(能力探测)。基于逆向工程代码,这些是最可能的。
- **0x06** 是**仅来自单个未记录会话的实证观察**。它**尚未在多个设备、内核版本或配置之间得到独立验证**,且缺乏基于聊天的证据支持。**不要依赖 0x06 作为确定的响应通道。** 您设备上的实际响应通道可能是 0x0B、0x0C,或其他完全不同的通道。
如果您运行脚本并在您的系统上成功,则它检测到的响应通道适用于您的配置。如果您运行脚本失败或报告无响应,则响应通道可能在您的内核版本或设备变体上有所不同。
```
Observed example from undocumented session (report 0x06):
Input report 0x06: 7e 01 00 12 00 81 20 0b 00 00 00 00 00 00 21 21 21 60 ...
^^ ^^
NCP marker cmd_group=0x20, cmd_id=0x0B (GET_STATUS response)
^^^^^^^^^^^
payload = "!!!" = unknown/intermediate state
```
标志字节中的 `0x81`(设置了第 7 位)表示一个**响应帧**。`!!!` 载荷在 `CalibG4.exe` 中映射到字符串 `"Unknown status, waiting"` —— 它是一个中间轮询状态。DLL 在收到它后会继续轮询。
**在更多 Surface Pro 3 设备上进行进一步测试至关重要**,以确定 0x06 是否为可靠的响应通道,实际通道是 0x0B 还是 0x0C(逆向工程候选者),或者这是否是设备特定或内核版本依赖的行为。社区验证将澄清这一模糊之处。
**完整 CalibG4 调用序列(来自 Ghidra,主序列位于 `0x1400010B0`):**
- 读取缓冲区:4096 字节
- `START_CALIB` 超时:3000 ms
- 状态轮询循环:最多 60 次迭代 × 500 ms = 最长 30 秒
- 结束时显式 `DeInit` + `Deregister` 清理
- `Init` 接受 IP 地址和端口(用于通过网络远程校准 —— 根据 PDB 字符串为 "Calib on local/remote machine")
VID/PID 匹配是**动态的**:调用者将 VID/PID(或 `-1` 表示“任意”)传递给 `0x180003078` 处的枚举函数。DLL 不硬编码 `0x1B96`。
**二进制元数据:** PDB 路径 `D:\Jenkins\workspace\G4_Host\Off_G4_Host_BUILD\Host_Win\H_Win_Tools\CalibG4\x64\Release\CalibG4.pdb`,版本 1.0.0.12。
### 失败方法总结(供未来参考)
| 方法 | 失败原因 |
|---|---|
| 向报告 0x1B 发送 NCP | 报告错误。直到 v4 之前,0x1B 被认为是 NCP 通道 —— 仅在阶段 4 深度 DLL vtable 分析 (v5) 中被排除。一个缓冲区大小 bug(停在 8 字节)使读取看起来稳定(`29 a9 19 9f ...`);在 v4 中以 260 字节重测:返回 256 字节非零静态设备状态数据,确认不是 NCP 响应。 |
| 解绑驱动后的原始 I2C | `i2c_hid_acpi`(位于 `INT33C3:00`,总线 1,从地址 0x07 的设备)在解绑时关闭设备电源;所有后续 I2C 命令均失败并返回 `EREMOTEIO` (errno 121)。 |
| 轮询 GET_FEATURE 以获取响应 | DLL 使用异步 ReadFile,而非 GetFeature。响应以输入报告形式到来。 |
| iptsd / linux-surface 内核 | 完全错误的技术。SP3 不使用 IPTS。 |
| 报告 0x03 变化作为信号 | 它是事务计数器,而非 NCP 响应。 |
## 用法
```
# 需要 root(hidraw 访问权限)
sudo python3 ntrig_calib.py # run full diagnostics (default)
sudo python3 ntrig_calib.py --diag # same as above, explicit
sudo python3 ntrig_calib.py --calibrate # send START_CALIB only
sudo python3 ntrig_calib.py --list # list all N-Trig hidraw devices
sudo python3 ntrig_calib.py -d /dev/hidraw1 # specify device explicitly
sudo python3 ntrig_calib.py --module-id 0x0002 # override NCP module ID (default: 0x0001)
```
**默认情况下(以及使用 `--diag` 时),脚本运行诊断**,而非静默校准。它将:
1. 自动检测 N-Trig hidraw 设备(或使用 `-d`)
2. 解析并打印 HID 报告描述符
3. 对所有报告进行基线 GET_FEATURE 快照
4. 探测未声明的报告 0x29–0x2D(这些在 I2C-HID 设备上不存在;仅 USB 传输拥有直接路径报告 ID)
5. 通过分块报告 0x05 协议发送 NCP GET_STATUS 和 START_CALIB
6. 尝试通过报告 0x05 进行直接(非分块)NCP
7. 在每次发送后尝试异步输入报告读取,寻找 NCP 响应
使用 `--calibrate` 仅发送 START_CALIB 命令,跳过诊断探测 —— 一旦您通过 `--diag` 运行确认 NCP 通道有响应,建议使用此选项。**重要提示:** 此脚本中的响应通道确认基于来自未记录会话的实证观察,且**尚未在多个设备间得到验证**。您的内核版本或设备配置可能不同;在运行 `--calibrate` 之前,请务必先用 `--diag` 验证 NCP 通道在您的系统上有响应。
**运行后,触摸之前死区的区域。** 它应该会立即响应。无需重启。
**⚠️ 测试建议:** 此脚本基于逆向工程和实验发现。请先在非关键设备上谨慎测试。如果脚本成功重新校准了您的触摸屏,或者它无响应,请报告您的结果(包括设备型号、内核版本和 `--diag` 的输出),以帮助验证脚本在不同配置下的可靠性。
### CalibG4.exe 和 NCPTransportInterface.dll
Windows 二进制文件(`CalibG4.exe` 和 `NCPTransportInterface.dll`)提取自 [EP0000601624.zip](https://gartnertechnology.com/wp-content/uploads/2024/01/EP0000601624.zip)([Internet Archive 也有存档](https://web.archive.org/web/20260315181048/https://gartnertechnology.com/wp-content/uploads/2024/01/EP0000601624.zip))。只要这些链接有效,我不想重新发布这些文件。如果您难以找到它们,请提交 issue,我会看看是否可以将它们发送给您或其他方式。
**SHA-256 校验和:**
```
822a319fc8bb3d3a9fce50f9610124f3838c20a638f727c70c984fe88356ba44 EP0000601624.zip
89160d12677f2bd98f21db01651677d62dd0c242082bc9591edf41e330d7dd91 NCPTransportInterface.dll
ebf0168a60111d58f7709cfa8c7d129002cbdb192f253dddad6737122ddbdde7 CalibG4.exe
```
### 要求
- Python 3.6+
- Root 权限
- 位于 `/dev/hidraw*` 的 N-Trig 设备(使用 `dmesg | grep -i "NTRG\|1B96\|multitouch"` 或 `ls /dev/hidraw*` 验证)
- 绑定了 `hid-multitouch` 到 `NTRG0001:01 1B96:1B05` 的内核(任何内核 4.8 或更高版本的 Linux 发行版均为标准配置)
## 致谢
- **诊断、逆向工程和脚本**:主要由 [Claude Opus 4.6](https://claude.ai) 在扩展思维模式下完成。仅凭症状描述,Claude 确定了校准漂移的根本原因,定位了在线的 Sony VAIO 更新包,对 XOR 反转的 cabinet 进行去混淆,使用 Ghidra 反编译 `NCPTransportInterface.dll`,追踪 I2C 传输 vtable,解码 NCP 帧格式和分块协议,识别异步接收路径,并编写了脚本。后续的改进和文档完善可能涉及其他 Claude 模型。
- **逆向工程工具**:[Ghidra](https://ghidra-sre.org/) —— 由 **NSA 研究理事会** 开发的开源软件逆向工程框架
- **原始 Windows 工具**:`CalibG4.exe`,由 Sony/N-Trig 制作,Sony VAIO 更新包 `EP0000601624.exe` 的一部分
- **社区发现**:[surfaceforums.net](https://www.surfaceforums.net)、Microsoft Answers 和 GitHub Issues 上的许多 Surface Pro 3 用户,他们确认 `CalibG4.exe` 是修复方案并让这些知识得以流传
- **完整对话历史**:为有兴趣跟进逐步推理和逆向工程过程的人提供完整聊天会话的 HTML 副本,可通过联系页面索取。
## 相关问题
本项目解决了跨多个 Surface 设备和平台报告的死区问题:
- [Surface Pro 3 触摸屏问题](https://www.reddit.com/r/Surface/comments/2knsyd/surface_pro_3_touchscreen_problem/) (Reddit)
- [触摸屏底部响应笔但不响应触摸](https://www.reddit.com/r/Surface/comments/1b1hdc9/bottom_part_of_touch_screen_responds_to_pen_but/) (Reddit)
- [SurfaceBook 3 屏幕死区](https://www.reddit.com/r/Surface/comments/180v7xl/surfacebook_3_screen_dead_zones/) (Reddit)
- [iptsd issue #202 — N-Trig 校准讨论](https://github.com/linux-surface/iptsd/issues/202) (GitHub)
## 许可证
Python 脚本(`ntrig_calib.py`)在 AGPLv3 许可证下发布。见 [LICENSE](./LICENSE) 文件。
标签:DuoSense, GHIDRA, N-Trig, Python, Surface Pro 3, 云资产清单, 固件修复, 微软Surface, 无后门, 校准, 死区修复, 硬件支持, 硬件维护, 系统工具, 触摸屏, 触摸驱动, 输入设备, 逆向工具, 逆向工程