indina853/NariMeter
GitHub: indina853/NariMeter
一款轻量级 Windows 托盘工具,通过逆向 USB 协议绕过 Razer Synapse 直接显示 Nari 无线耳机的实时电池电量。
Stars: 0 | Forks: 0
# NariMeter
一个轻量级的 Windows 系统托盘应用程序,用于显示 Razer Nari 无线耳机的**实时电池电量**——完全通过 USB 协议逆向工程构建,零依赖 Razer 软件。




## Razer Synapse 的问题
Razer Nari 是一款性能出色的无线耳机。然而,它的配套软件 Razer Synapse 却并非如此。
Synapse 会安装一系列后台服务——`RazerNahimic`、`RazerGameScannerService`、`RazerCentralService` 等——这些服务在空闲状态下会占用大约 300 到 600 MB 的 RAM,并且在未经询问的情况下自动注册开机启动,同时与 Razer 的遥测基础架构保持持久的网络连接。这意味着,作为获取电池百分比的先决条件,您的耳机使用习惯、音频设置和硬件标识符等数据正被持续传输到外部服务器。
其软件界面本身也将电池指示器隐藏在一个庞大且加载缓慢的覆盖层中,需要多次点击才能查看。为了获取一条简单的信息——*我的耳机还有多少电量?*——这带来的摩擦是显而易见的。
NariMeter 通过一个一目了然的托盘图标、约 345 KB 的磁盘占用以及完全零网络活动,轻松回答了这个问题。
## 功能
- 电池百分比直接显示在 Windows 系统托盘中
- 颜色编码的图标,一眼即可反映电量
- 连接 USB 线缆时显示专属的充电图标
- 当连接线缆且电池电量为 100% 时,显示充满电指示器
- 耳机关机或断开连接时显示耳机图标
- 悬停工具提示当前状态
- **低电量通知** — 可配置的预警和严重阈值
- **充满电通知** — 当耳机达到 100% 时发出提醒
- 右键菜单中的**开机自启**开关(通过 Windows 注册表实现,无需安装程序)
- 完全便携——单个 `.exe` 文件,无需安装,除了可选的自启项外不产生任何注册表垃圾
## 托盘图标参考
| 图标 | 状态 |
|---|---|
| 🟢 绿色电池 | 已连接且正在放电 — 电量 > 50% |
| 🟡 黄色电池 | 已连接且正在放电 — 电量 > 20% |
| 🔴 红色电池 | 已连接且正在放电 — 电量 ≤ 20% |
| ⚡ 充电中的电池 | USB 线缆已连接,正在积极充电 |
| 🟢 绿色电池 | USB 线缆已连接,100% 充满 |
| 🎧 耳机 | 耳机已关机或接收器已断开连接 |
## 系统要求
- Windows 10 或更高版本 (x64)
- [.NET 8 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime) — 必需,免费,一键安装
- 带有已连接 USB 接收器的 Razer Nari 无线耳机
- 在接收器的 Interface 5 上安装 WinUSB 驱动程序(参见下方的设置说明)
## 设置
### 1. 安装 .NET 8 Runtime
从 [dotnet.microsoft.com](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime) 下载并运行安装程序。许多用户可能已经安装了该运行环境。
### 2. 安装 WinUSB 驱动程序
NariMeter 在 USB 协议层与 Nari 接收器进行通信,完全绕过了 Razer 的驱动程序栈。为实现这一点,必须将通用的 WinUSB 驱动程序绑定到接收器的 **Interface 5** 上。
1. 下载并运行 [Zadig](https://zadig.akeo.ie/)
2. 在菜单中,选择 **Options → List All Devices**
3. 在下拉菜单中找到 `Razer Nari` — 选择与 **Interface 5** 对应的条目
4. 将目标驱动程序设置为 **WinUSB**
5. 点击 **Replace Driver**
### 3. 运行 NariMeter
从 [Releases](../../releases) 页面下载 `NariMeter.exe` 并运行。无需安装。在接收器被识别后的几秒钟内,托盘图标就会出现。
## 工作原理 — 协议逆向工程
NariMeter 是在无法获取任何 Razer 文档的情况下构建的。其电池报告协议完全是通过 USB 流量分析发现的。
### 第一步 — 识别设备
Razer Nari 接收器作为具有多个接口的 USB 复合设备向 Windows 展示自身。使用 USBPcap 和 Wireshark,捕获了正常运行期间 Razer Synapse 与接收器之间的所有 HID 流量。
设备标识符为:
```
Vendor ID: 0x1532 (Razer Inc.)
Product ID: 0x051C (Nari dongle)
Interface: 5
```
Interface 5 是负责报告设备状态的 HID 接口,与用于按钮输入的音频和标准 HID 接口是分开的。
### 第二步 — 捕获握手过程
Synapse 通过 USB HID 控制传输与 Interface 5 通信——即 `SET_REPORT` 后跟 `GET_REPORT`——这是一种用于查询设备状态的标准 HID 模式。
捕获到的能够触发电池响应的 `SET_REPORT` payload 为:
```
FF 0A 00 FD 04 12 F1 02 05 00 00 ... (64 bytes total)
```
它作为 Class 请求发送到该接口:
```
bmRequestType: 0x21 (Host → Device, Class, Interface)
bRequest: 0x09 (SET_REPORT)
wValue: 0x03FF
wIndex: 5 (Interface number)
wLength: 64
```
### 第三步 — 读取响应
随后的 `GET_REPORT` 请求从设备检索 64 字节的响应:
```
bmRequestType: 0xA1 (Device → Host, Class, Interface)
bRequest: 0x01 (GET_REPORT)
wValue: 0x03FF
wIndex: 5
wLength: 64
```
在响应缓冲区中,相关字段如下:
| 字节偏移量 | 值 | 含义 |
|---|---|---|
| `[1]` | `0x01` | 设备空闲 / 耳机未激活 |
| `[2]` | `0x00` | 确认空闲状态 |
| `[9]` | `0x05` | USB 线缆已连接(充电中) |
| `[9]` | `0x06` | USB 线缆已连接,电池已充满 |
| `[9]` | `0x03` | USB 线缆已断开(放电中) |
| `[12:13]` | uint16 big-endian | 以毫伏为单位的电池电压——计算百分比的主要来源 |
| `[14]` | uint8 | 设备固件报告的电池百分比——仅用作完整性检查 |
### 第四步 — 电池百分比计算
固件值 `response[14]` **并不作为主要的电池百分比**。实证测试表明,Nari 固件更新此值时存在大幅度的离散跳跃——例如,在仅变化约 8 mV 的情况下,该值会在 80% 维持几个小时,然后直接降至 50%。这是固件的校准限制,并不是实际充电水平的真实反映。
NariMeter 改为根据 `response[12:13]` 处的原始毫伏读数推导电池百分比,并在设备观测到的最低和最高电压之间使用线性插值进行计算:
```
percent = (mv - minMv) / (maxMv - minMv) × 100
```
电压边界(`minMv`、`maxMv`)是**按设备自适应校准的**——它们以保守的默认值开始,随着应用在正常使用过程中观察到接近低和高极端值的读数而自动优化。校准后的值会跨会话持久化保存在 `NariMeter.state.json` 中。
固件值 `response[14]` 被保留用作**完整性检查**:如果基于电压推导的百分比与固件报告的百分比相差超过 40 个百分点,则该读数被视为异常,系统将转而保留最后一个已知的正常值。
电池百分比以 **5% 的步长**显示并进行平滑过渡——无论底层数据的变化有多大,显示的值在每个轮询周期内最多变动 5%,从而防止托盘图标出现突然的跳变。
### 第五步 — 充电状态检测
设备通过 `response[9]` 原生报告充电状态:
```
isCharging = response[9] == 0x05;
isFullyCharged = response[9] == 0x06;
```
`isFullyCharged` 在 `BatteryReader` 中会短路所有的稳定状态和百分比计算路径——它会被优先检查并被视为权威状态。托盘图标和工具提示也会将 `Charging && BatteryPercent >= 100` 视为“已充满”,以覆盖固件字节发生转换之前的涓流充电窗口:在电池达到满容量且电压稳定在 4200 mV 时,固件可能仍会在几分钟内保持在 `0x05` 状态。
### 第六步 — 状态机和去抖动
原始的 USB 读数存在噪声,尤其是在启动时,此时主机控制器和设备仍在进行协商。NariMeter 在 `UsbDevice.cs` 中实现了独立的去抖动阈值:
- **开机状态**仅在**连续 4 次活动读数**(4 × 2秒 = 8 秒)后确认
- **关机状态**在**连续 4 次空闲读数**后确认
在活动状态下,电池电量每 **30 秒**轮询一次,鉴于电池放电速度较慢,这样的频率已经足够。
### 第七步 — 架构概述
```
Program.cs
└── TrayApp.cs — ApplicationContext, timer orchestration, tray icon management
├── BatteryReader.cs — Charge state logic, stabilization, state persistence
├── UsbDevice.cs — USB HID control transfers, static buffers, debounce state machine
├── HeadsetState.cs — Immutable state record, ChargeStatus enum, tooltip formatting
├── StateStore.cs — JSON persistence of last known percentage and calibrated mV bounds
└── StartupManager.cs — Windows registry autostart toggle (HKCU\...\Run)
```
## 通知
NariMeter 支持可选的 Windows 气球通知,可通过右键菜单中的 **Show Notifications** 进行切换:
- **低电量预警** — 在可配置的阈值(默认:20%)触发
- **严重电量警告** — 在可配置的阈值(默认:10%)触发
- **已充满电** — 当连接线缆的耳机达到 100% 时触发
预警和严重阈值可通过右键菜单独立配置,并跨会话持久保存。
## 从源代码构建
**前提条件:** .NET 8 SDK
```
git clone https://github.com/indina853/NariMeter.git
cd NariMeter
dotnet build
```
**发布便携版可执行文件:**
```
dotnet publish -c Release
```
输出:`bin\Release\net8.0-windows\win-x64\publish\NariMeter.exe`
## 性能
| 指标 | 值 |
|---|---|
| 可执行文件大小 | ~345 KB |
| 内存占用(稳定状态) | ~15 MB |
| CPU 使用率 | < 0.1% |
| 网络活动 | 无 |
| 磁盘写入 | 仅在电池百分比改变和更新设置时 |
| 轮询间隔 — 状态 | 2 秒 |
| 轮询间隔 — 电池 | 30 秒 |
## 项目结构
```
NariMeter/
├── BatteryReader.cs — Battery charge state logic
├── HeadsetState.cs — State record and charge status definitions
├── Program.cs — Entry point
├── StartupManager.cs — Run at startup via Windows registry
├── StateStore.cs — Battery % and settings persistence (JSON)
├── TrayApp.cs — Tray icon, timers, notifications, menu
├── UsbDevice.cs — USB HID communication layer
├── App.ico — Application icon (task manager, Explorer)
├── Headphone.ico — Tray: powered off / disconnected state
├── BatteryGreen.ico — Tray: battery > 50% or fully charged
├── BatteryYellow.ico — Tray: battery > 20%
├── BatteryRed.ico — Tray: battery ≤ 20%
├── BatteryCharging.ico — Tray: charging
├── NariMeter.csproj
└── RunTimeConfig.template.json
```
## 贡献
欢迎提交 Pull request。如果您拥有不同的 Razer 无线耳机,并希望调查相同的协议是否适用,切入点是使用 [USBPcap](https://desowin.org/usbpcap/) + [Wireshark](https://www.wireshark.org/) 捕获 Synapse 查询您的设备时的 USB 流量,然后将 `SET_REPORT` payload 和响应的字节布局与上面记录的 Nari 协议进行比较。
## 许可证
MIT — 查看 [LICENSE](LICENSE)
标签:.NET 8, USB协议, 硬件状态监测, 端点可见性, 系统托盘工具, 驱动替代工具