接收器
Logitech 无线接收器通过 HID++ 1.0 寄存器完成检测、已配对设备枚举、通知和配对。
接收器(receiver)是无线 Logitech 设备使用的 USB 适配器。OpenLogi 按 USB
VID/PID 检测已知适配器。OpenLogi 通过 HID++ 1.0 设备索引
0xFF(RECEIVER_DEVICE_INDEX)控制该适配器。OpenLogi 使用已配对设备的槽位索引
寻址该设备。
**规格:**Logitech 未公开此规格。布局由 Logi Options+ 2.8(静态分析)逆向得出。**交叉核对:**Solaar。用于:连接设备和支持的设备。
检测
receiver::detect(chan: Arc<HidppChannel>) -> Option<Receiver> 会匹配精确的 USB
VID/PID。该函数会对未列出的适配器返回 None。表中的所有设备都使用 Logitech
VID 046D。
| 市场系列 | VID:PID | OpenLogi 接收器协议 |
|---|---|---|
| Logi Bolt | 046D:C548 | Bolt |
| Unifying | 046D:C52B、046D:C532 | Unifying |
| Nano | 046D:C534、046D:C537 | 与 Unifying 兼容 |
| Lightspeed | 046D:C539、046D:C53A、046D:C53F、046D:C541、046D:C547、046D:C54D | 与 Unifying 兼容 |
Receiver 只有 Bolt 和 Unifying 两种协议变体。因此,Nano 和 Lightspeed
适配器会返回 Receiver::Unifying(_)。Receiver::name() 也会为这些适配器返回
"Unifying Receiver"。
通用 API
Receiver 持有具体接收器的监听器。其接收器级操作使用设备索引 0xFF。其方法不公开
适配器的市场系列。
| 项目 | 签名或值 | 说明 |
|---|---|---|
| 接收器地址 | RECEIVER_DEVICE_INDEX = 0xFF | HID++ 1.0 接收器设备索引。 |
| 检测 | detect(Arc<HidppChannel>) -> Option<Receiver> | 对已知 VID/PID 分类。 |
| 接收器类型 | Receiver::Bolt(bolt::Receiver) / Receiver::Unifying(unifying::Receiver) | 选择协议包装器。 |
| 显示名称 | name(&self) -> String | 返回 "Logi Bolt Receiver" 或 "Unifying Receiver"。 |
| 唯一 ID | get_unique_id(&self) -> Result<String, ReceiverError> | 返回特定协议系列的接收器标识符。 |
| 读取通知标志 | read_notification_flags(&HidppChannel) -> Result<NotificationFlags, Hidpp10Error> | 读取寄存器 0x00。 |
| 写入通知标志 | write_notification_flags(&HidppChannel, NotificationFlags) -> Result<(), Hidpp10Error> | 写入寄存器 0x00。 |
ReceiverError 为 UnknownReceiver 或 Protocol(Hidpp10Error)。非穷尽的
Receiver 和错误枚举可以在后续版本增加变体。
寄存器 0x00:通知标志
寄存器 0x00 是完整的 24 位 BE 字。NotificationFlags 从
read_notification_flags() 获得时,会保留未命名的位。调用方必须修改读取结果,才能调用
write_notification_flags()。
| 字位 | 掩码 | 含义 | OpenLogi 的处理 |
|---|---|---|---|
| 8 | 0x000100 | WIRELESS:设备到达/移除(0x41)通知 | 已建模。 |
| 11 | 0x000800 | SOFTWARE_PRESENT:主机软件存在 | 已建模。配对需要此位和 WIRELESS。 |
| 所有其他位 | 其他所有位 | 接收器或软件特有的通知 | 从读取值保留,但没有名称。 |
write_notification_flags() 会写入所提供值的全部 3 个字节。该函数不能恢复调用方遗漏的位。
unifying::Receiver::set_wireless_notifications() 会读取该字,只修改
WIRELESS,并跳过相同的写入。
Logi Bolt
Bolt 是 Bluetooth Low Energy 接收器。其包装器支持最多 6 台已配对设备、发现、 passkey 事件、配对、取消配对和已配对设备读取。Logitech 未公开其寄存器布局规格。
API
| 方法 | 签名 | 返回值 |
|---|---|---|
| 构造 | new(Arc<HidppChannel>) | Result<bolt::Receiver, ReceiverError> |
| 订阅 | listen(&self) | async_channel::Receiver<bolt::Event> |
| 读取通知标志 | get_notification_flags(&self) | Result<NotificationFlags, ReceiverError> |
| 写入通知标志 | set_notification_flags(&self, flags: NotificationFlags) | Result<(), ReceiverError> |
| 统计配对数 | count_pairings(&self) | Result<u8, ReceiverError> |
| 触发到达通知 | trigger_device_arrival(&self) | Result<(), ReceiverError> |
| 收集已配对设备 | collect_paired_devices(&self) | Result<Vec<bolt::DeviceConnection>, ReceiverError> |
| 读取唯一 ID | get_unique_id(&self) | Result<String, ReceiverError> |
| 读取配对信息 | get_device_pairing_information(&self, device_index: u8) | Result<bolt::DevicePairingInformation, ReceiverError> |
| 读取代号 | get_device_codename(&self, device_index: u8) | Result<String, ReceiverError> |
| 取消配对 | unpair_device(&self, device_index: u8) | Result<(), ReceiverError> |
| 配对 | pair_device(&self, slot: u8, address: [u8; 6], authentication: u8, entropy: u8) | Result<(), ReceiverError> |
| 开始发现 | discover_devices(&self, timeout: Option<u8>) | Result<(), ReceiverError> |
| 取消发现 | cancel_device_discovery(&self) | Result<(), ReceiverError> |
bolt::DeviceConnection 有 index、kind、encrypted、online 和 wpid。
bolt::DevicePairingInformation 有 wpid、kind、encrypted、online 和
unit_id: [u8; 4]。
bolt::DeviceKind 建模 Keyboard、Mouse、Numpad、Presenter、Remote、
Trackball、Touchpad、Tablet、Gamepad、Joystick、Headset 和 Unknown。
PairingPasskeyPressType 建模 Initialization(0x00)、Keypress(0x01)、
Submit(0x04)和 Other(u8)。
寄存器和配对消息
以下所有条目都使用接收器索引 0xFF。N 是已配对设备槽位的低 4 位。多字节 WPID
使用 LE。
| 寄存器或 sub-ID | 请求或 payload 布局 | OpenLogi 的处理 |
|---|---|---|
0x02 读取 | 短读取 payload 为 [00, 00, 00];响应字节 1 是配对数量 | count_pairings() 读取字节 1。 |
0x02 写入 | [02, 00, 00] | trigger_device_arrival() 为每个已配对槽位请求一条 0x41 报告。 |
0xB5 / 0x5N | 长子寄存器读取,请求为 [00, 00];响应字节 1 是设备信息,字节 2~3 是 WPID LE,字节 4~7 是 unit ID | get_device_pairing_information() 建模这些字段。 |
0xB5 / 0x6N | 长子寄存器读取,请求为 [01, 00];响应字节 2 是名称长度,字节 3 起为 UTF-8 字节 | get_device_codename() 最多返回一个响应中的 13 个字节。该函数会拒绝无效 UTF-8。 |
0xFB | 长寄存器读取,请求为 [00, 00, 00] | get_unique_id() 将完整响应解析为 UTF-8。该值不是 Bolt 序列号。 |
0xC0 发现 | 开始为 [timeout, 01, 00];取消为 [00, 02, 00] | None 会写入超时值 0。接收器最大超时为 60 秒。 |
0xC1 配对 | 长 payload:[01, slot, address[0..6], authentication, entropy, 00 × 6] | pair_device() 发送提供的槽位。设备层使用槽位 0 自动分配。 |
0xC1 取消或取消配对 | 取消为 [02, 00 × 15];取消配对为 [03, slot, 00 × 14] | 包装器和设备层使用这些固定操作值。 |
包装器会发出以下未匹配的长通知。偏移从 HID++ 1.0 的两个字节头之后开始。除了 0x41
以外,所有通知都必须使用接收器索引 0xFF。
| Sub-ID | Payload 布局 | bolt::Event 结果 |
|---|---|---|
0x4D passkey 请求 | 字节 1~6:NUL 填充的 ASCII 数字。字节 7~12:设备地址。 | PairingPasskeyRequest { device_address, passkey } |
0x4E passkey 按键 | 字节 0:按键类型。字节 1~6:设备地址。 | PairingPasskeyPressed { device_address, press_type } |
0x4F 发现详情 | 字节 0~1:计数器 LE。字节 2:0。字节 4:设备类型。字节 5~6:WPID LE。字节 7~12:地址。字节 15:认证。 | DeviceDiscoveryDeviceDetails |
0x4F 发现名称 | 字节 0~1:计数器 LE。字节 2:1。字节 3:UTF-8 长度。字节 4 起:名称。 | DeviceDiscoveryDeviceName |
0x53 发现状态 | 字节 0 为 0 表示发现已启用。 | DeviceDiscoveryStatus { discovery_enabled } |
公共包装器会忽略这些布局中的未使用字节。包装器会丢弃格式错误的名称和 passkey 文本。
设备配对服务还会读取 0x54:字节 1 是接收器错误;字节 0 为 0x02 时,字节 8 是分配的槽位。
Unifying
Unifying 使用 2.4 GHz eQuad 无线协议。其包装器支持最多 6 台已配对设备、接收器信息、
已配对设备读取和 0x41 事件。该包装器不公开直接的发现、配对、取消配对或代号方法。
API
| 方法 | 签名 | 返回值 |
|---|---|---|
| 构造 | new(Arc<HidppChannel>) | Result<unifying::Receiver, ReceiverError> |
| 订阅 | listen(&self) | async_channel::Receiver<unifying::Event> |
| 统计配对数 | count_pairings(&self) | Result<u8, ReceiverError> |
| 设置无线通知 | set_wireless_notifications(&self, enabled: bool) | Result<(), ReceiverError> |
| 触发到达通知 | trigger_device_arrival(&self) | Result<(), ReceiverError> |
| 读取接收器信息 | get_receiver_info(&self) | Result<unifying::ReceiverInfo, ReceiverError> |
| 读取配对信息 | get_device_pairing_information(&self, device_index: u8) | Result<unifying::DevicePairingInformation, ReceiverError> |
| 读取唯一 ID | get_unique_id(&self) | Result<String, ReceiverError> |
unifying::ReceiverInfo 有 serial_number 和 pairing_slots。
unifying::DevicePairingInformation 有 wpid、kind、encrypted、online 和
unit_id: [u8; 4]。unifying::Event 只有
DeviceConnection(DeviceConnection)。
Unifying 的设备类型表从 Keyboard 到 Presenter 与 Bolt 相同。Unifying 使用
Remote = 0x05、Trackball = 0x06 和 Touchpad = 0x07。Bolt 对这 3 种设备分别使用
0x07、0x08 和 0x09。两个包装器都会将未知设备类型转为 Unknown。
寄存器和配对锁
| 寄存器或 sub-ID | 请求或 payload 布局 | OpenLogi 的处理 |
|---|---|---|
0x02 读取 | 短读取 payload 为 [00, 00, 00];响应字节 1 是配对数量 | count_pairings() 读取字节 1。 |
0x02 写入 | [02, 00, 00] | trigger_device_arrival() 为在线和离线的已配对槽位请求报告。 |
0xB5 / 0x03 | 长子寄存器读取,请求为 [00, 00];响应字节 1~4 组成接收器序列号,字节 6 是槽位数量 | get_receiver_info() 将字节 1~4 显示为大写十六进制。 |
0xB5 / 0x5N | 长子寄存器读取,请求为 [00, 00];字节 1 是设备信息,字节 2~3 是 WPID LE,字节 4~7 是 unit ID | get_device_pairing_information() 建模这些字段。 |
0xB2 配对锁 | 打开为 [01, 00, 30];关闭为 [02, 00, 00];取消配对为 [03, slot, 00] | 设备配对服务控制此寄存器。 |
0x4A 锁状态 | 字节 0 的位 0:锁打开。字节 1:非零接收器错误。 | 配对服务会先确认锁已打开,再报告 Searching。 |
InfoSubRegister::DeviceCodename 以 0x60 存在,但 Unifying 包装器没有代号方法。
OpenLogi 的库存路径使用单独的 Unifying 专用读取来获取代号。
0x41 连接通知
两个接收器包装器都解析 sub-ID 0x41。头部中的设备索引是从 1 开始的已配对设备槽位。
触发请求会为每个已配对槽位生成一条报告,其中包括离线槽位。因此,0x41 报告是槽位报告,
不能证明设备在线。
下表描述 HID++ 1.0 头部之后的通知 payload。
| 字节或位 | 线上的含义 | OpenLogi 的处理 |
|---|---|---|
| 字节 0 | 协议字节 | 0x10 会使 encrypted 为 true。其他值不会公开。Options+ 2.8 和 Solaar 证据将此值定义为 Bolt 加密链路哨兵。 |
| 字节 1,位 0~3 | 设备类型 | 使用特定协议系列的设备类型表建模。未知值转为 Unknown。 |
字节 1,位 4(0x10) | Software present | 事件解码器会忽略此位。此位反映寄存器 0x00 的 SOFTWARE_PRESENT,不是加密。 |
字节 1,位 5(0x20) | 链路已加密 | 建模为 encrypted。 |
字节 1,位 6(0x40) | 链路未建立 | 建模为 online = false。该位清除表示 online = true。 |
字节 1,位 7(0x80) | 后面有 payload 数据包 | 事件解码器会忽略此位。 |
| 字节 2~3 | Wireless product ID | 建模为 wpid,使用 LE。 |
OpenLogi 从字节 1 的位 5 或字节 0 的 0x10 计算 encrypted。事件中只建模
index、kind、encrypted、online 和 wpid。DeviceConnection 不保留字节 0、
位 4 或位 7。
枚举已配对设备
触发到达通知前,必须启用无线通知。Unifying 辅助方法会对寄存器 0x00 作读-改-写。
下面的 Bolt 示例同样从读取的标志派生写入值。
use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
receiver::{self, unifying, NotificationFlags, Receiver, ReceiverError},
};
async fn enumerate_receiver(chan: Arc<HidppChannel>) -> Result<(), ReceiverError> {
let Some(receiver) = receiver::detect(Arc::clone(&chan)) else {
return Ok(());
};
println!("{} — {}", receiver.name(), receiver.get_unique_id().await?);
match receiver {
Receiver::Bolt(bolt) => {
let flags = bolt.get_notification_flags().await?;
bolt.set_notification_flags(flags | NotificationFlags::WIRELESS)
.await?;
for device in bolt.collect_paired_devices().await? {
println!("slot {}: {:04X}", device.index, device.wpid);
}
}
Receiver::Unifying(unifying_receiver) => {
unifying_receiver.set_wireless_notifications(true).await?;
let events = unifying_receiver.listen();
unifying_receiver.trigger_device_arrival().await?;
while let Ok(unifying::Event::DeviceConnection(device)) = events.recv().await {
println!("slot {}: {:04X}", device.index, device.wpid);
}
}
_ => {}
}
Ok(())
}Bolt 辅助方法会在触发写入完成后停止收集。Unifying 包装器会发出单独事件,因此调用方选择 自己的收集边界。
OpenLogi 的用法
设备注册表会在库存和配对使用协议包装器之前,对 USB 接收器分类。与 Unifying 兼容的 Nano
和 Lightspeed 适配器使用 Unifying 路径。库存会以读-改-写启用 WIRELESS,然后触发
0x41 报告来枚举已配对槽位。
配对服务在完整会话期间持有一个接收器寄存器阶段。该服务读取完整的 0x00 字,添加
WIRELESS 和 SOFTWARE_PRESENT,并在监听器关闭后恢复原始字。从 v0.8.14 开始,
该流程会保留未知通知位,不再清除寄存器。
Bolt 会写入发现寄存器 0xC0,并在写入确认后报告 Searching。Unifying 会写入
0xB2 锁打开请求,然后最多等待 2 秒来接收确认位 0 的 0x4A。只有收到确认后,
该流程才会报告 Searching。错误、取消和超时会发送对应阶段的关闭或取消请求。
公共配对选择器只有 First 和 BoltUid(String)。服务可以通过路由保存的接收器 UID
匹配任一协议系列来取消配对。服务不能在开始新配对会话时按 UID 选择 Unifying 接收器。
未确认的细节
Logitech 未公开这些接收器寄存器布局的规格。本页描述 Options+ 2.8 的静态证据和 Solaar 交叉核对。本页不将静态证据表述为硬件观察结果。
Options+ 在 Unifying 0xB2 锁打开请求的第 3 个字节中写入 0。Solaar 支持 30 秒超时。
OpenLogi 写入 30。静态证据没有表明该值错误。硬件跟踪必须确认特定接收器的时序语义。
Options+ 会保留 0x41 的字节 0。Solaar 将字节 0 的 0x10 视为已加密的 Bolt 链路。
OpenLogi 在共享解码器中应用该哨兵,但不公开协议字节。该哨兵对未来 Unifying 协议值的含义
尚未确认。
OpenLogi 可以读取 Unifying 唯一 ID,但 ReceiverSelector 没有 UnifyingUid 变体。
因此,使用 First 的新配对请求可以打开找到的第一个受支持接收器。这是未解决的产品限制,
不是未确认的线格式结论。
无接收器
蓝牙直连和有线设备会作为独立库存设备出现。它们不使用接收器寄存器。参见 连接设备。