Skip to main content

openlogi_device_registry/
receiver.rs

1//! Logitech receiver identities.
2
3use crate::LOGITECH_VENDOR_ID;
4
5/// Logitech receiver product family, independent of its wire protocol.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum ReceiverBrand {
8    /// Logi Bolt receiver.
9    Bolt,
10    /// Logitech Unifying receiver.
11    Unifying,
12    /// Logitech Nano receiver.
13    Nano,
14    /// Logitech Lightspeed receiver.
15    Lightspeed,
16}
17
18/// Receiver protocol implementation used to enumerate and address devices.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum ReceiverProtocol {
21    /// Logi Bolt receiver registers and pairing flow.
22    Bolt,
23    /// HID++ 1.0 Unifying-compatible receiver registers and pairing flow.
24    Unifying,
25}
26
27/// A known receiver's USB identity and the behavior OpenLogi assigns to it.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct ReceiverDescriptor {
30    /// USB vendor ID.
31    pub vendor_id: u16,
32    /// USB product ID.
33    pub product_id: u16,
34    /// Receiver's marketed product family.
35    pub brand: ReceiverBrand,
36    /// Protocol implementation used for receiver operations.
37    pub protocol: ReceiverProtocol,
38}
39
40impl ReceiverDescriptor {
41    const fn logitech(product_id: u16, brand: ReceiverBrand, protocol: ReceiverProtocol) -> Self {
42        Self {
43            vendor_id: LOGITECH_VENDOR_ID,
44            product_id,
45            brand,
46            protocol,
47        }
48    }
49}
50
51/// All receiver identities supported by OpenLogi.
52///
53/// Each entry is a protocol claim and must be backed by hardware verification
54/// or an established reference implementation. Marketing families and protocol
55/// families are recorded separately: Nano and Lightspeed currently use
56/// [`ReceiverProtocol::Unifying`].
57pub const RECEIVERS: &[ReceiverDescriptor] = &[
58    ReceiverDescriptor::logitech(0xc52b, ReceiverBrand::Unifying, ReceiverProtocol::Unifying),
59    ReceiverDescriptor::logitech(0xc532, ReceiverBrand::Unifying, ReceiverProtocol::Unifying),
60    // Nano receiver bundled with budget wireless combos (e.g. MK270, MK295),
61    // limited to 2 paired devices — cross-checked against Solaar's
62    // NANO_RECEIVER_C534.
63    ReceiverDescriptor::logitech(0xc534, ReceiverBrand::Nano, ReceiverProtocol::Unifying),
64    // Nano receiver bundled with the G602.
65    ReceiverDescriptor::logitech(0xc537, ReceiverBrand::Nano, ReceiverProtocol::Unifying),
66    // Lightspeed gaming receiver used by the G502, G Pro Wireless, G604, and
67    // other G-series devices.
68    ReceiverDescriptor::logitech(
69        0xc539,
70        ReceiverBrand::Lightspeed,
71        ReceiverProtocol::Unifying,
72    ),
73    // Lightspeed nano receiver, verified with a G305 (WPID 0x4074).
74    ReceiverDescriptor::logitech(
75        0xc53f,
76        ReceiverBrand::Lightspeed,
77        ReceiverProtocol::Unifying,
78    ),
79    // Lightspeed receiver, verified with a G915 (WPID 0x407c).
80    ReceiverDescriptor::logitech(
81        0xc541,
82        ReceiverBrand::Lightspeed,
83        ReceiverProtocol::Unifying,
84    ),
85    // Lightspeed receiver, verified with a G915 (WPID 0x407c) and G502 X.
86    ReceiverDescriptor::logitech(
87        0xc547,
88        ReceiverBrand::Lightspeed,
89        ReceiverProtocol::Unifying,
90    ),
91    ReceiverDescriptor::logitech(0xc548, ReceiverBrand::Bolt, ReceiverProtocol::Bolt),
92    // Lightspeed receiver, verified with a PRO X SUPERLIGHT 2 DEX (WPID 0x40b8).
93    ReceiverDescriptor::logitech(
94        0xc54d,
95        ReceiverBrand::Lightspeed,
96        ReceiverProtocol::Unifying,
97    ),
98];
99
100/// Finds the descriptor for an exact USB vendor/product identity.
101#[must_use]
102pub fn find_receiver(vendor_id: u16, product_id: u16) -> Option<&'static ReceiverDescriptor> {
103    RECEIVERS
104        .iter()
105        .find(|receiver| receiver.vendor_id == vendor_id && receiver.product_id == product_id)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::{RECEIVERS, ReceiverBrand, ReceiverProtocol, find_receiver};
111    use crate::LOGITECH_VENDOR_ID;
112
113    #[test]
114    fn receiver_identities_are_unique() {
115        for (index, receiver) in RECEIVERS.iter().enumerate() {
116            assert!(
117                RECEIVERS[..index].iter().all(|other| {
118                    (other.vendor_id, other.product_id) != (receiver.vendor_id, receiver.product_id)
119                }),
120                "duplicate receiver identity {:04x}:{:04x}",
121                receiver.vendor_id,
122                receiver.product_id
123            );
124        }
125    }
126
127    #[test]
128    fn g915_receiver_is_lightspeed_over_unifying_protocol() {
129        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc541).expect("c541 receiver");
130
131        assert_eq!(receiver.brand, ReceiverBrand::Lightspeed);
132        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
133    }
134
135    #[test]
136    fn superlight_dex_receiver_is_lightspeed_over_unifying_protocol() {
137        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc54d).expect("c54d receiver");
138
139        assert_eq!(receiver.brand, ReceiverBrand::Lightspeed);
140        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
141    }
142
143    #[test]
144    fn mk270_mk295_nano_receiver_is_nano_over_unifying_protocol() {
145        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc534).expect("c534 receiver");
146
147        assert_eq!(receiver.brand, ReceiverBrand::Nano);
148        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
149    }
150
151    #[test]
152    fn lookup_requires_the_matching_vendor() {
153        assert!(find_receiver(0xffff, 0xc54d).is_none());
154    }
155}