fix: Device-Discovery und Sidetone gegen echte Hardware korrigiert
- PID-Filter in find_and_open(), damit nicht die Harpoon-Maus statt dem HS80 geöffnet wird - ALSA Sidetone: Card-Suche über Sidetone-Control statt Kartenname (war abgeschnitten zu "Gamin") - Verbose-Flag (-v) für Debug-Output der Init-Sequenz
This commit is contained in:
parent
c5f8625345
commit
812d14b81a
@ -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> {
|
||||
Self::open_with_verbose(false)
|
||||
}
|
||||
|
||||
/// Wie `open()`, aber mit optionalem Debug-Output auf stderr.
|
||||
pub fn open_with_verbose(verbose: bool) -> Result<Self> {
|
||||
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<Vec<u8>>) -> String {
|
||||
match resp {
|
||||
Some(data) => {
|
||||
let hex: Vec<String> = 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();
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
44
src/hid.rs
44
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<HidDevice> {
|
||||
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<Vec<u8>> {
|
||||
device.write(packet)?;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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<String> {
|
||||
// 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<i64> {
|
||||
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(())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user