openseries-gg 1.0.0

Control supported SteelSeries headsets and mice from the command line or Rust
Documentation
use super::*;

pub(crate) static DEFINITION: DeviceDefinition = DeviceDefinition {
    slug: "sensei-ten",
    product_ids: &[0x1832, 0x1834],
    rule: MatchRule::ReportSizes {
        output: 15,
        feature: 36,
    },
    connect: |identity, transport| {
        Device::Mouse(Mouse::new(Box::new(SenseiTen::new(identity, transport))))
    },
};

pub(crate) struct SenseiTen {
    base: DeviceContext,
}
impl SenseiTen {
    pub(crate) fn new(identity: Identity, transport: HidTransport) -> Self {
        Self {
            base: DeviceContext::new(identity, transport),
        }
    }
}
impl DeviceProtocol for SenseiTen {
    fn id(&self) -> &str {
        &self.base.identity.id
    }
    fn name(&self) -> &str {
        if self.base.identity.product_id == 0x1834 {
            "SteelSeries Sensei Ten CS:GO Neon Rider Edition"
        } else {
            "SteelSeries Sensei Ten"
        }
    }
    fn product_id(&self) -> u16 {
        self.base.identity.product_id
    }
    fn supported_features(&self) -> Capabilities {
        Capabilities::MOUSE_SENSITIVITY | Capabilities::POLLING_RATE | Capabilities::ILLUMINATION
    }
}
impl SenseiTen {
    fn output(&mut self, command: &[u8]) -> Result<()> {
        self.base.transport.write_output(command, 1, 0)
    }
    fn output_and_save(&mut self, command: &[u8]) -> Result<()> {
        self.output(command)?;
        thread::sleep(Duration::from_millis(50));
        self.output(&[0x59, 0])
    }
}
impl MouseProtocol for SenseiTen {
    fn sensitivity_info(&self) -> Result<MouseSensitivityInfo> {
        Ok(MouseSensitivityInfo {
            minimum: 50,
            maximum: 18_000,
            step: 50,
            maximum_preset_count: 5,
        })
    }
    fn supported_polling_rates(&self) -> Result<&'static [u16]> {
        Ok(&POLLING_RATES)
    }
    fn supported_illumination_zones(&self) -> Result<&'static [MouseZone]> {
        Ok(&SENSEI_ZONES)
    }
    fn set_sensitivity(&mut self, values: &[u16]) -> Result<()> {
        if !(1..=5).contains(&values.len()) {
            return Err(invalid("Between one and five DPI presets are required."));
        }
        let mut command = vec![0x55, 0, (1_u8 << values.len()) - 1, 1];
        for (index, dpi) in values.iter().copied().enumerate() {
            if !(50..=18_000).contains(&dpi) || !dpi.is_multiple_of(50) {
                return Err(invalid(format!(
                    "DPI preset {} must be from 50 to 18000 in steps of 50.",
                    index + 1
                )));
            }
            command.extend((dpi / 50).to_le_bytes());
        }
        self.output_and_save(&command)
    }
    fn set_polling_rate(&mut self, rate: u16) -> Result<()> {
        let encoded = match rate {
            125 => 4,
            250 => 3,
            500 => 2,
            1000 => 1,
            _ => return Err(invalid("Polling rate must be 125, 250, 500, or 1000 Hz.")),
        };
        self.output_and_save(&[0x54, 0, encoded])
    }
    fn set_illumination(
        &mut self,
        zone: MouseZone,
        color: RgbColor,
        persistence: Persistence,
    ) -> Result<()> {
        let led = match zone {
            MouseZone::Logo => 0,
            MouseZone::Wheel => 1,
            _ => return Err(invalid("Sensei Ten lighting zone must be logo or wheel.")),
        };
        let mut command = [0_u8; 35];
        command[0] = 0x5b;
        command[2] = led;
        command[3] = 0xe8;
        command[4] = 3;
        command[19] = 1;
        command[27] = 1;
        command[28..31].copy_from_slice(&[color.red, color.green, color.blue]);
        command[31..34].copy_from_slice(&[color.red, color.green, color.blue]);
        self.base.transport.write_feature(&command, 1, 36)?;
        if persistence == Persistence::Save {
            thread::sleep(Duration::from_millis(50));
            self.output(&[0x59, 0])
        } else {
            Ok(())
        }
    }
}