一个轻量的 Windows 11 鼠标电量监控小工具,用来替代体积庞大、需要常驻后台的罗技官方驱动。
它直接通过 HID++ 协议与罗技接收器/鼠标通信读取电量,不依赖罗技 G HUB / Options+,
也不需要管理员权限、不需要联网,可以和官方驱动同时存在、互不干扰。
主要适配 罗技 G304 Lightspeed Wireless Gaming Mouse,同时兼容绝大多数
Lightspeed、有线、2.4G 无线的罗技鼠标(协议细节见下文)。
| 浅色 | 深色 |
|---|---|
| 浅色主题 | 深色主题 |
上面两张是接上真实 G304 X LIGHTSPEED 之后截的图 —— 电量、充电状态、
数据源都是实际从鼠标读回来的。
其他状态:
| 低电量警告 | 未检测到设备 |
|---|---|
| 低电量 | 未检测到设备 |
| 功能 | 说明 |
|---|---|
| 自动设备检测 | 启动即扫描所有罗技 HID 设备,自动定位承载 HID++ 的通道与设备索引 |
| 电量显示 | 电池百分比(整数)+ 充电状态(充电中 / 放电中 / 已充满) |
| 低电量警告 | 低于 15% 时圆环变红并弹出提示条 |
| 自动重扫 | 检测到设备断开或连续读取失败时自动重新枚举设备。不是即时热插拔,拔插后最长约 60 秒恢复,详见「已知限制」 |
| 刷新间隔 | 15 秒 / 30 秒 / 1 分钟 / 5 分钟,可随时切换 |
| 深浅色主题 | 跟随系统 / 强制浅色 / 强制深色,实时切换 |
| 开机自启 | 写 HKCU 注册表,不需要管理员权限 |
| 完全离线 | 没有任何网络请求,所有数据都在本机处理 |
界面是苹果风格的卡片式设计:一张圆角卡片、一个电池圆环、三个设置项,没有托盘图标、没有悬浮窗、没有系统通知。
- 运行:Windows 10 1809 及以上 / Windows 11,x64
- 不需要安装 .NET 运行时 —— 发布的 exe 是自包含的
- 不需要管理员权限
- 编译:.NET SDK 8.0 或更高
- 取
dist/LogiBattery.exe(约 60–70 MB,单文件,已内嵌 .NET 运行时)。 - 双击运行。首次启动会扫描设备,通常一两秒内就能看到电量。
- 需要开机自启时,打开卡片下方的「开机自启」开关。
想先看看界面长什么样、但手边没有罗技鼠标,可以带上预览参数运行:
dist/LogiBattery.exe预览模式不会访问任何硬件,只是在几个示例状态之间轮播。另外还有一个出图/排障用的参数:
.\LogiBattery.exe --shot=C:\temp\preview它会把「真机当前状态」和各个示例状态分别渲染成 PNG 后退出 —— 其中
00-real-device-*.png 反映的是这台机器上真实读到的数据,反馈问题时把它带上最直观。
(只有确实读到在线设备时才会生成这两张;鼠标休眠时它会跳过,并在日志里说明。)
| 界面元素 | 说明 |
|---|---|
| 中间圆环 | 当前电量百分比。读不到时显示 -- |
| 圆环颜色 | 充电中为蓝色;≥30% 绿色;<30% 橙色;<15% 红色 |
| 状态胶囊 | 「充电中」/「放电中」/「已充满」/「电池异常」 |
| 设备名下方 | HID++ 版本 + 本次数据来自哪个特性 + 是否经接收器连接 |
| 小字备注 | 数据是估算值时,这里会说明估算方式 |
| 刷新间隔 | 切换后立即生效并马上刷新一次 |
| 重新扫描 | 丢弃当前连接、重新枚举设备(设备没被自动认出来时手动点一下) |
| 开机自启 | 写入 HKCU\Software\Microsoft\Windows\CurrentVersion\Run |
窗口右上角的 ✕ 直接退出程序。
dotnet --version # 需要 8.0 及以上dotnet build src/LogiBattery/LogiBattery.csproj -c Debug
dotnet run --project src/LogiBattery/LogiBattery.csprojdotnet publish src/LogiBattery/LogiBattery.csproj `
-c Release -r win-x64 --self-contained true `
-o dist或者直接跑仓库里的脚本:
.\build.ps1单文件、自包含等发布参数已经写在 LogiBattery.csproj 里,所以只要指定
-r win-x64 --self-contained true,dotnet publish 就会产出单个 exe。
说明:这里选 WPF 而不是 WinUI 3,就是因为 Windows App SDK 官方不支持单文件发布
(原生依赖必须散落在目录里)。WPF 能做到真正的一个 exe,界面观感完全一致。
Windows 会为 HID 设备的每个顶层集合生成一条独立的设备接口路径(形如
\\?\hid#vid_046d&pid_c531&mi_02&col01#...)。程序用 SetupAPI 枚举
GUID_DEVINTERFACE_HID,再用 hid.dll 过滤出罗技设备(VID 0x046D)。
罗技接收器用来承载 HID++ 报文的是一个厂商自定义集合:
UsagePage = 0xFF00(Vendor-Defined)Usage = 0x0001(或0x0002)
这类集合会被排到最前面优先尝试;其余罗技集合作为备选。
所有 HID++ 2.0 报文都遵循同样的头结构(第一个字节是 HID 报告 ID):
[0] 报告 ID 0x10 = 短报文(整包 7 字节) 0x11 = 长报文(整包 20 字节)
[1] 设备索引 0xFF = 接收器自身 / 有线直连设备;0x01..0x06 = 接收器下挂设备
[2] 特性索引 该特性在「本次会话」中的索引,由 ROOT 特性查询得到
[3] 功能号|软件ID 高 4 位 = 功能号,低 4 位 = 软件 ID(应答会原样回填)
[4..] 参数
读写都用「重叠 I/O + 事件」实现,因此每个请求都能带超时;收到不匹配的报文
(设备主动推送的通知等)会被跳过,继续读到超时为止。
罗技用 16 位特性 ID 标识功能,但设备实际寻址用的是 8 位特性索引。
ROOT 特性(ID 0x0000)永远位于索引 0x00,用它的功能 0 做转换:
请求 [ID高位, ID低位, 0x00] → 应答 [特性索引, 类型, 版本]
返回索引 0 表示设备不支持该特性。探活用 ROOT 功能 1(ping):请求
[0x00, 0x00, 0x5A],应答 [HID++主版本, 次版本, 回显的 0x5A],
只有真实存在的设备才会回显。
程序按下面的顺序依次尝试,第一个能读出有效数值的就用它:
| 顺序 | 特性 ID | 名称 | 请求 / 应答 | 说明 |
|---|---|---|---|---|
| 1 | 0x1004 |
UNIFIED_BATTERY | 功能 1 → [百分比, 电量档位, 充电状态] |
新一代设备,最准;先查功能 0 的能力位确认它真的会上报百分比 |
| 2 | 0x1000 |
BATTERY_STATUS | 功能 0 → [放电百分比, 下一档位, 充电状态] |
G304 走的就是这条 |
| 3 | 0x1001 |
BATTERY_VOLTAGE | 功能 0 → [电压高字节, 电压低字节, 充电状态] |
只有电压,百分比按放电曲线估算 |
| 4 | — | HID++ 1.0 寄存器 | 寄存器 0x0D / 0x07 |
没有特性表的老式 2.4G 设备,只能给粗略档位 |
充电状态的枚举值含义:
0x1000:0放电 /1充电中 /2充电收尾 /3已充满 /4慢充 /5–7异常0x1001:0放电 /1有线充电 /2无线充电 /3已充满0x1004:0放电 /1充电 /2慢充 /3已充满 /4错误
这几个坑都是在真实 G304 上调试时踩出来的,写在这里免得重蹈覆辙。
① 别用「接收器自身的索引 0xFF」当作整条通道的准入条件。
一开始的实现是:先 ping 设备索引 0xFF(接收器自身),不应答就判定这条 HID 通道
不跑 HID++,直接跳过。结果在实测的这台 G304 上完全探测不到设备 ——
这个接收器根本不对 0xFF 应答,只对已配对设备的索引 0x01 应答。
正确做法是「0xFF 和下挂的 1..6 都试一遍」,只要有一个应答就继续。
(0xFF 只在「有线直连、没有接收器」的场景下才是设备本身。)
② 一次读失败 ≠ 设备不支持。
无线设备省电时对 HID++ 请求的响应很不稳定,实测中同一个 G304 会出现
「第一次扫描全部超时、第二次扫描立刻成功」的情况。所以探测分成两步:
先只查特性表确认设备有没有电量功能,再带重试地读值。
有功能但没读出来 → 保留连接、几秒后重试;完全没有电量功能 → 才提示「不支持」。
③ 充电时百分比报 0。
部分固件在接着电源时会把「放电百分比」字段填 0。直接显示 0% 会让用户以为没电,
所以程序在 rawLevel == 0 且正在充电 时,按充电状态推算一个合理值
(充电中 80 / 收尾 90 / 已充满 100 / 慢充 50),并在界面上标注「推算值」。
Linux 内核的 hid-logitech-hidpp 也是这么处理的。
④ 长报文还是短报文。
Windows 的 HID 要求写入缓冲区长度与「该报告 ID 在描述符里声明的长度」一致,
所以短报文必须正好 7 字节、长报文正好 20 字节,不能随便补齐。
程序默认用长报文,只有在 UsagePage=0xFF00 的专用集合上失败时才退回短报文。
另外,超时的重叠 I/O 在取消之后不能无限等待回收:实测中发现
CancelIoEx 有时会返回失败(I/O 恰好已完成),此时再调
GetOverlappedResult(bWait: true) 会让整个探测线程永久卡住。
现在改成有上限的短轮询,最多等 500 ms 就放弃。
显示鼠标电量/
├─ README.md 本文档
├─ build.ps1 一键发布脚本(-Clean / -Configuration / -RuntimeIdentifier)
├─ dist/
│ └─ LogiBattery.exe 发布产物:单文件、自包含,双击即用
├─ docs/ README 用的界面截图
└─ src/LogiBattery/
├─ LogiBattery.csproj 项目与发布配置(单文件 + 自包含)
├─ app.manifest 声明 asInvoker(免管理员)、PerMonitorV2 DPI
├─ App.xaml / App.xaml.cs 入口、单实例互斥、全局异常兜底
├─ MainWindow.xaml / .cs 界面、设备变化消息钩子、状态渲染
├─ Interop/
│ └─ NativeMethods.cs setupapi / hid / kernel32 的 P/Invoke 声明
├─ Devices/
│ ├─ HidDeviceInfo.cs 一个 HID 顶层集合的快照
│ └─ HidDeviceEnumerator.cs 枚举罗技 HID 设备并读取能力
├─ Hidpp/
│ ├─ HidppConstants.cs 特性 ID、寄存器地址、错误码
│ ├─ HidppTransport.cs 报文收发(带超时的重叠 I/O)
│ ├─ HidppProtocol.cs 协议实现 + 电量回退链
│ ├─ HidppConnection.cs 探测设备、建立连接
│ ├─ BatteryReading.cs 电量结果模型
│ └─ BatteryVoltageCurve.cs 电压 → 百分比的估算曲线
├─ Controls/
│ └─ BatteryRing.xaml(.cs) 电池圆环控件
├─ Services/
│ ├─ BatteryMonitor.cs 后台轮询、静默重试、断开后重扫
│ ├─ AutoStartService.cs 注册表开机自启
│ ├─ AppSettings.cs 本地设置读写
│ ├─ ThemeManager.cs 深浅色调色板
│ └─ Log.cs 文件日志
└─ Themes/
├─ Palette.xaml 调色板默认值
└─ Controls.xaml 按钮 / 分段控件 / iOS 风格开关等样式
命令行参数(都只影响界面,不影响正常运行):
| 参数 | 作用 |
|---|---|
--demo |
用示例数据轮播各种状态,不访问硬件 |
--shot=<目录> |
把真机状态与示例状态渲染成 PNG 后退出 |
都放在 %AppData%\LogiBattery\(即 C:\Users\<用户名>\AppData\Roaming\LogiBattery\):
| 文件 | 内容 |
|---|---|
settings.ini |
刷新间隔与主题设置 |
logs\app-yyyyMMdd.log |
运行日志,单文件超过 512 KB 自动截断,只保留最近 7 天 |
排障时看日志就够了 —— 每次连接、每次读取失败、每次重新枚举设备都会记录。
开发过程中在一台装有 罗技 G304 X LIGHTSPEED(接收器 PID 0xC547)的 Windows 机器上
做了完整的实机验证:
| 验证项 | 结果 |
|---|---|
| 通道枚举 | 正确识别接收器的两个 HID++ 集合(col01 短报文 / col02 长报文) |
| 设备探测 | 在设备索引 0x01 找到 G304 X,HID++ 协议版本 4.2 |
| 电量读取 | 0x1004 读出 67%(放电中);也实际走到过回退链的 0x1001 分支 |
| 名称读取 | 特性 0x0005 正确返回 G304 X LIGHTSPEED |
| 界面渲染 | 深浅主题、低电量警告、未检测到设备等状态均正常 |
仓库里的 dist/LogiBattery.exe 就是这次验证用的构建产物。
- 拔插设备后不是立刻刷新,最长要等约 60 秒。 程序目前没有注册
RegisterDeviceNotification,WM_DEVICECHANGE处理里也没有判断DBT_DEVNODES_CHANGED (0x0007)—— 而 Windows 对 HID 接口的拔插默认只广播0x0007, 所以窗口消息这条路实际不会触发。真正起作用的是兜底逻辑:连续两次读取失败后重新枚举设备。 急着看结果就点界面上的「重新扫描」。根因与修法已定位,计划在下一个版本修复。 - 只支持 Windows x64。 用到的是 SetupAPI / hid.dll 这套 Windows 专有接口。
- G304 之外的设备未必百分百准确。 程序按 HID++ 标准实现,但不同固件对电量字段的
填充方式不完全统一。数据显示为估算值时,界面上会有小字提示。 - 电压换百分比是估算。 罗技官方软件用的是一条私有放电曲线,本程序用分段折线近似。
实测中同一个 G304 用0x1001电压估算得到 80%、用0x1004直接读到 67%,
可以看出估算确实偏乐观 —— 所以程序只有在0x1004/0x1000都不可用时才会走到这条路径。 - 蓝牙连接的罗技设备未经验证。 枚举按 VID
0x046D过滤,蓝牙设备理论上也会被扫到,
但没有实机验证过,可能出现「此设备不支持电量读取」。 - 多个罗技鼠标时只显示一个。 程序会选第一个能读出电量的设备;键盘等没有电池的
设备会被自动跳过。 - 不显示历史曲线、寿命预测。 需求里没有要求,保持轻量。
- 不做 G HUB 那类按键/DPI 配置。 这个工具只干一件事:把电量显示出来。
Q:会不会和罗技 G HUB / Options+ 冲突?
不会。双方都是各自打开自己的 HID 句柄只读查询,互不干扰,可以同时运行。
Q:为什么一定要插接收器?鼠标开着有线模式也能读到吗?
有线直连时鼠标本身就是 HID++ 设备,程序会用设备索引 0xFF 直接和它通信,同样能读。
Q:显示「未检测到罗技设备」怎么办?
- 确认接收器插好、鼠标已开机并和接收器配对;
- 点一下「重新扫描」;
- 还是不行就看
%AppData%\LogiBattery\logs\里当天的日志,里面有每个 HID 通道的探测结果。
Q:显示「此设备不支持电量读取」?
说明识别到了罗技设备,但这台设备的 HID++ 特性表里没有 0x1004 / 0x1000 / 0x1001,
寄存器也读不出电池 —— 通常是有线键盘之类的非电池设备。
Q:刚打开时短暂显示「正在读取电量…」甚至「未检测到罗技设备」,过一会儿又好了?
正常现象。无线鼠标休眠后不会立刻响应 HID++ 请求,接收器那一侧会表现为「写入失败」或
「等待应答超时」。实测中 G304 在休眠状态下第一轮探测就会这样,程序在几秒后自动重扫即可连上
(未连接时走 5 秒快速重试)。动一下鼠标能显著加快这个过程。
Q:为什么 exe 有 60 多 MB?
里面内嵌了完整的 .NET 运行时,换来的是「拷贝过去双击就能跑」,不需要目标机器装任何东西。
实现过程中参考了以下开源项目与文档中的协议细节:
- Solaar —— 尤其
hidpp10.py中的
HID++ 1.0 电池寄存器解析与档位映射 - LGSTrayBattery —— C# / .NET 下直接与
罗技设备通信、热插拔处理的思路 - OpenLogi HID++ 文档 —— 各特性的报文布局
- Linux 内核
hid-logitech-hidpp.c
—— 充电状态下百分比推算的处理方式 - Logitech HID++ 2.0 规范文档
本项目为个人自用工具,与罗技(Logitech)公司无任何关联。
MIT License © 2026 XianCong Chen
可以自由使用、修改、分发,包括商业用途,只需保留版权声明与许可证原文。 软件按「原样」提供,不附带任何担保。