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    // POWERPLAY wireless charging system's embedded Lightspeed receiver.
74    ReceiverDescriptor::logitech(
75        0xc53a,
76        ReceiverBrand::Lightspeed,
77        ReceiverProtocol::Unifying,
78    ),
79    // Lightspeed nano receiver, verified with a G305 (WPID 0x4074).
80    ReceiverDescriptor::logitech(
81        0xc53f,
82        ReceiverBrand::Lightspeed,
83        ReceiverProtocol::Unifying,
84    ),
85    // Lightspeed receiver, verified with a G915 (WPID 0x407c).
86    ReceiverDescriptor::logitech(
87        0xc541,
88        ReceiverBrand::Lightspeed,
89        ReceiverProtocol::Unifying,
90    ),
91    // Lightspeed receiver, verified with a G915 (WPID 0x407c) and G502 X.
92    ReceiverDescriptor::logitech(
93        0xc547,
94        ReceiverBrand::Lightspeed,
95        ReceiverProtocol::Unifying,
96    ),
97    ReceiverDescriptor::logitech(0xc548, ReceiverBrand::Bolt, ReceiverProtocol::Bolt),
98    // Lightspeed receiver, verified with a PRO X SUPERLIGHT 2 DEX (WPID 0x40b8).
99    ReceiverDescriptor::logitech(
100        0xc54d,
101        ReceiverBrand::Lightspeed,
102        ReceiverProtocol::Unifying,
103    ),
104];
105
106/// Finds the descriptor for an exact USB vendor/product identity.
107#[must_use]
108pub fn find_receiver(vendor_id: u16, product_id: u16) -> Option<&'static ReceiverDescriptor> {
109    RECEIVERS
110        .iter()
111        .find(|receiver| receiver.vendor_id == vendor_id && receiver.product_id == product_id)
112}
113
114#[cfg(test)]
115mod tests {
116    use super::{RECEIVERS, ReceiverBrand, ReceiverProtocol, find_receiver};
117    use crate::LOGITECH_VENDOR_ID;
118
119    #[test]
120    fn receiver_identities_are_unique() {
121        for (index, receiver) in RECEIVERS.iter().enumerate() {
122            assert!(
123                RECEIVERS[..index].iter().all(|other| {
124                    (other.vendor_id, other.product_id) != (receiver.vendor_id, receiver.product_id)
125                }),
126                "duplicate receiver identity {:04x}:{:04x}",
127                receiver.vendor_id,
128                receiver.product_id
129            );
130        }
131    }
132
133    #[test]
134    fn powerplay_receiver_is_lightspeed_over_unifying_protocol() {
135        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc53a).expect("c53a receiver");
136
137        assert_eq!(receiver.brand, ReceiverBrand::Lightspeed);
138        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
139    }
140
141    #[test]
142    fn g915_receiver_is_lightspeed_over_unifying_protocol() {
143        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc541).expect("c541 receiver");
144
145        assert_eq!(receiver.brand, ReceiverBrand::Lightspeed);
146        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
147    }
148
149    #[test]
150    fn superlight_dex_receiver_is_lightspeed_over_unifying_protocol() {
151        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc54d).expect("c54d receiver");
152
153        assert_eq!(receiver.brand, ReceiverBrand::Lightspeed);
154        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
155    }
156
157    #[test]
158    fn mk270_mk295_nano_receiver_is_nano_over_unifying_protocol() {
159        let receiver = find_receiver(LOGITECH_VENDOR_ID, 0xc534).expect("c534 receiver");
160
161        assert_eq!(receiver.brand, ReceiverBrand::Nano);
162        assert_eq!(receiver.protocol, ReceiverProtocol::Unifying);
163    }
164
165    #[test]
166    fn lookup_requires_the_matching_vendor() {
167        assert!(find_receiver(0xffff, 0xc54d).is_none());
168    }
169}