diff --git a/src/bragi/device.rs b/src/bragi/device.rs index 908b4b4..3243fbf 100644 --- a/src/bragi/device.rs +++ b/src/bragi/device.rs @@ -19,19 +19,33 @@ pub struct BragiDevice { impl BragiDevice { /// Findet ein Corsair-Gerät, öffnet es und führt die Init-Sequenz durch. pub fn open() -> Result { + Self::open_with_verbose(false) + } + + /// Wie `open()`, aber mit optionalem Debug-Output auf stderr. + pub fn open_with_verbose(verbose: bool) -> Result { let api = HidApi::new()?; + if verbose { + hid::log_device_info(&api); + } let device = hid::find_and_open(&api)?; + if verbose { + eprintln!("[init] Gerät gefunden und geöffnet"); + } let bragi = Self { device }; - bragi.initialize()?; + bragi.initialize(verbose)?; Ok(bragi) } /// Vollständige Bragi-Handshake-Sequenz. - fn initialize(&self) -> Result<()> { + fn initialize(&self, verbose: bool) -> Result<()> { // Phase 1: Receiver initialisieren // Wake-Up: Firmware-Version lesen let packet = protocol::build_get_packet(ENDPOINT_RECEIVER, Property::AppFirmware.id()); - hid::send_recv_optional(&self.device, &packet)?; + let resp = hid::send_recv_optional(&self.device, &packet)?; + if verbose { + eprintln!("[init] Receiver Wake-Up: {}", format_resp(&resp)); + } // Receiver in Software-Modus let packet = protocol::build_set_packet( @@ -39,11 +53,17 @@ impl BragiDevice { Property::Mode.id(), &[0x00, protocol::MODE_SOFTWARE], ); - hid::send_recv_optional(&self.device, &packet)?; + let resp = hid::send_recv_optional(&self.device, &packet)?; + if verbose { + eprintln!("[init] Receiver -> Software-Mode: {}", format_resp(&resp)); + } // Heartbeat: PID abfragen let packet = protocol::build_get_packet(ENDPOINT_RECEIVER, Property::ProductId.id()); - hid::send_recv_optional(&self.device, &packet)?; + let resp = hid::send_recv_optional(&self.device, &packet)?; + if verbose { + eprintln!("[init] Receiver PID (Heartbeat): {}", format_resp(&resp)); + } // Phase 2: Headset initialisieren // Headset in Software-Modus @@ -52,15 +72,24 @@ impl BragiDevice { Property::Mode.id(), &[0x00, protocol::MODE_SOFTWARE], ); - hid::send_recv_optional(&self.device, &packet)?; + let resp = hid::send_recv_optional(&self.device, &packet)?; + if verbose { + eprintln!("[init] Headset -> Software-Mode: {}", format_resp(&resp)); + } // Puffer leeren hid::flush(&self.device)?; + if verbose { + eprintln!("[init] Buffer geflusht"); + } // Heartbeat: PID abfragen — keine Antwort = Headset offline let packet = protocol::build_get_packet(ENDPOINT_HEADSET, Property::ProductId.id()); - let response = hid::send_recv_optional(&self.device, &packet)?; - if response.is_none() { + let resp = hid::send_recv_optional(&self.device, &packet)?; + if verbose { + eprintln!("[init] Headset PID (Heartbeat): {}", format_resp(&resp)); + } + if resp.is_none() { return Err(CorsairError::HeadsetOffline); } @@ -151,6 +180,18 @@ impl BragiDevice { } } +/// Formatiert eine optionale Response für Debug-Output. +fn format_resp(resp: &Option>) -> String { + match resp { + Some(data) => { + let hex: Vec = data.iter().take(20).map(|b| format!("{b:02X}")).collect(); + let suffix = if data.len() > 20 { "..." } else { "" }; + format!("{}{}", hex.join(" "), suffix) + } + None => "keine Antwort".to_string(), + } +} + impl Drop for BragiDevice { fn drop(&mut self) { self.cleanup(); diff --git a/src/cli.rs b/src/cli.rs index b8ead13..ab06462 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,10 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name = "corsairctl", version, about = "CLI für Corsair Bragi-Geräte")] pub struct Cli { + /// Debug-Output auf stderr ausgeben + #[arg(short, long, global = true)] + pub verbose: bool, + #[command(subcommand)] pub command: Command, } diff --git a/src/hid.rs b/src/hid.rs index 1c34a6e..4f1a47c 100644 --- a/src/hid.rs +++ b/src/hid.rs @@ -8,17 +8,55 @@ use hidapi::{HidApi, HidDevice}; use crate::bragi::protocol::{CORSAIR_VID, HID_INTERFACE, PACKET_SIZE, REPORT_SIZE}; use crate::error::{CorsairError, Result}; -/// Findet ein Corsair-Gerät auf Interface 3 und öffnet es. +/// Findet ein Corsair Bragi-Gerät auf Interface 3 und öffnet es. +/// +/// Sucht gezielt nach bekannten Bragi-PIDs, um Verwechslungen mit +/// anderen Corsair-Geräten (z.B. Mäuse) zu vermeiden. pub fn find_and_open(api: &HidApi) -> Result { - let device_info = api + // Alle Corsair-Geräte auf Interface 3 sammeln + let candidates: Vec<_> = api .device_list() - .find(|d| d.vendor_id() == CORSAIR_VID && d.interface_number() == HID_INTERFACE) + .filter(|d| d.vendor_id() == CORSAIR_VID && d.interface_number() == HID_INTERFACE) + .collect(); + + if candidates.is_empty() { + return Err(CorsairError::DeviceNotFound); + } + + // Bekannte Bragi-PIDs bevorzugen (HS80 = 0x0A6B) + let device_info = candidates + .iter() + .find(|d| KNOWN_BRAGI_PIDS.contains(&d.product_id())) + .or(candidates.first()) .ok_or(CorsairError::DeviceNotFound)?; let device = device_info.open_device(api)?; Ok(device) } +/// Gibt Informationen über das gefundene Gerät aus (für Debug). +pub fn log_device_info(api: &HidApi) { + let candidates: Vec<_> = api + .device_list() + .filter(|d| d.vendor_id() == CORSAIR_VID && d.interface_number() == HID_INTERFACE) + .collect(); + + for d in &candidates { + let selected = if KNOWN_BRAGI_PIDS.contains(&d.product_id()) { " ← selected" } else { "" }; + eprintln!( + "[hid] Gefunden: VID=0x{:04X} PID=0x{:04X} Interface={}{selected}", + d.vendor_id(), + d.product_id(), + d.interface_number(), + ); + } +} + +/// Bekannte Product IDs für Corsair Bragi-Geräte. +const KNOWN_BRAGI_PIDS: &[u16] = &[ + 0x0A6B, // HS80 RGB Wireless +]; + /// Sendet ein Paket und liest die Antwort. pub fn send_recv(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Result> { device.write(packet)?; diff --git a/src/main.rs b/src/main.rs index 02df391..29770eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ fn run() -> error::Result<()> { match cli.command { Command::Battery => { - let device = bragi::BragiDevice::open()?; + let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?; let level = device.battery_level()?; let status = device.battery_status()?; println!("{}", output::format_battery(level, &status)); @@ -35,7 +35,7 @@ fn run() -> error::Result<()> { } Command::Led { brightness } => { - let device = bragi::BragiDevice::open()?; + let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?; if let Some(value) = brightness { let clamped = value.clamp(0, 1000); device.set_brightness(clamped)?; @@ -47,7 +47,7 @@ fn run() -> error::Result<()> { } Command::Info => { - let device = bragi::BragiDevice::open()?; + let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?; let vid = device.vendor_id()?; let pid = device.product_id()?; let fw_app = device.firmware_app()?; @@ -56,7 +56,7 @@ fn run() -> error::Result<()> { } Command::Json => { - let device = bragi::BragiDevice::open()?; + let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?; let level = device.battery_level()?; let status = device.battery_status()?; println!("{}", output::format_waybar_json(level, &status)); diff --git a/src/sidetone.rs b/src/sidetone.rs index b9fc037..a6d61f8 100644 --- a/src/sidetone.rs +++ b/src/sidetone.rs @@ -5,21 +5,19 @@ use alsa::mixer::{Mixer, SelemChannelId, SelemId}; use crate::error::Result; -const CARD_NAME: &str = "Gaming"; -const SIDETONE_VOLUME: &str = "Sidetone Playback Volume"; -const SIDETONE_SWITCH: &str = "Sidetone Playback Switch"; +const SIDETONE_CONTROL: &str = "Sidetone"; -/// Findet die ALSA-Card-Nummer für die Corsair Gaming Soundkarte. +/// Findet die ALSA-Card mit einem Sidetone-Control. +/// +/// Sucht nach der Karte, die ein "Sidetone" Control hat, +/// statt sich auf den Kartennamen zu verlassen (der kann abgeschnitten sein). fn find_card() -> Result { - // ALSA-Cards durchiterieren - for card_idx in 0..32 { - let name = format!("hw:{card_idx}"); - if let Ok(ctl) = alsa::Ctl::new(&name, false) { - if let Ok(info) = ctl.card_info() { - let card_name = info.get_name().unwrap_or_default(); - if card_name.contains(CARD_NAME) { - return Ok(name); - } + for card in alsa::card::Iter::new().flatten() { + let name = format!("hw:{}", card.get_index()); + if let Ok(mixer) = Mixer::new(&name, false) { + let selem_id = SelemId::new(SIDETONE_CONTROL, 0); + if mixer.find_selem(&selem_id).is_some() { + return Ok(name); } } } @@ -31,7 +29,7 @@ pub fn get_level() -> Result { let card = find_card()?; let mixer = Mixer::new(&card, false)?; - let selem_id = SelemId::new(SIDETONE_VOLUME, 0); + let selem_id = SelemId::new(SIDETONE_CONTROL, 0); let selem = mixer .find_selem(&selem_id) .ok_or(crate::error::CorsairError::DeviceNotFound)?; @@ -46,19 +44,12 @@ pub fn set_level(level: i64) -> Result<()> { let mixer = Mixer::new(&card, false)?; // Volume setzen - let volume_id = SelemId::new(SIDETONE_VOLUME, 0); + let volume_id = SelemId::new(SIDETONE_CONTROL, 0); let volume_elem = mixer .find_selem(&volume_id) .ok_or(crate::error::CorsairError::DeviceNotFound)?; volume_elem.set_playback_volume_all(level)?; - // Switch aktivieren falls Level > 0 - let switch_id = SelemId::new(SIDETONE_SWITCH, 0); - if let Some(switch_elem) = mixer.find_selem(&switch_id) { - let enabled = if level > 0 { 1 } else { 0 }; - let _ = switch_elem.set_playback_switch_all(enabled); - } - Ok(()) }