OpenLogi

接收器

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:PIDOpenLogi 接收器协议
Logi Bolt046D:C548Bolt
Unifying046D:C52B、046D:C532Unifying
Nano046D:C534、046D:C537与 Unifying 兼容
Lightspeed046D: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 = 0xFFHID++ 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"。
唯一 IDget_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 的处理
80x000100WIRELESS:设备到达/移除(0x41)通知已建模。
110x000800SOFTWARE_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>
读取唯一 IDget_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 IDget_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-IDPayload 布局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>
读取唯一 IDget_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 IDget_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~3Wireless 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 的新配对请求可以打开找到的第一个受支持接收器。这是未解决的产品限制, 不是未确认的线格式结论。

无接收器

蓝牙直连和有线设备会作为独立库存设备出现。它们不使用接收器寄存器。参见 连接设备。

本页目录