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 {
|
impl BragiDevice {
|
||||||
/// Findet ein Corsair-Gerät, öffnet es und führt die Init-Sequenz durch.
|
/// Findet ein Corsair-Gerät, öffnet es und führt die Init-Sequenz durch.
|
||||||
pub fn open() -> Result<Self> {
|
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()?;
|
let api = HidApi::new()?;
|
||||||
|
if verbose {
|
||||||
|
hid::log_device_info(&api);
|
||||||
|
}
|
||||||
let device = hid::find_and_open(&api)?;
|
let device = hid::find_and_open(&api)?;
|
||||||
|
if verbose {
|
||||||
|
eprintln!("[init] Gerät gefunden und geöffnet");
|
||||||
|
}
|
||||||
let bragi = Self { device };
|
let bragi = Self { device };
|
||||||
bragi.initialize()?;
|
bragi.initialize(verbose)?;
|
||||||
Ok(bragi)
|
Ok(bragi)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vollständige Bragi-Handshake-Sequenz.
|
/// Vollständige Bragi-Handshake-Sequenz.
|
||||||
fn initialize(&self) -> Result<()> {
|
fn initialize(&self, verbose: bool) -> Result<()> {
|
||||||
// Phase 1: Receiver initialisieren
|
// Phase 1: Receiver initialisieren
|
||||||
// Wake-Up: Firmware-Version lesen
|
// Wake-Up: Firmware-Version lesen
|
||||||
let packet = protocol::build_get_packet(ENDPOINT_RECEIVER, Property::AppFirmware.id());
|
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
|
// Receiver in Software-Modus
|
||||||
let packet = protocol::build_set_packet(
|
let packet = protocol::build_set_packet(
|
||||||
@ -39,11 +53,17 @@ impl BragiDevice {
|
|||||||
Property::Mode.id(),
|
Property::Mode.id(),
|
||||||
&[0x00, protocol::MODE_SOFTWARE],
|
&[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
|
// Heartbeat: PID abfragen
|
||||||
let packet = protocol::build_get_packet(ENDPOINT_RECEIVER, Property::ProductId.id());
|
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
|
// Phase 2: Headset initialisieren
|
||||||
// Headset in Software-Modus
|
// Headset in Software-Modus
|
||||||
@ -52,15 +72,24 @@ impl BragiDevice {
|
|||||||
Property::Mode.id(),
|
Property::Mode.id(),
|
||||||
&[0x00, protocol::MODE_SOFTWARE],
|
&[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
|
// Puffer leeren
|
||||||
hid::flush(&self.device)?;
|
hid::flush(&self.device)?;
|
||||||
|
if verbose {
|
||||||
|
eprintln!("[init] Buffer geflusht");
|
||||||
|
}
|
||||||
|
|
||||||
// Heartbeat: PID abfragen — keine Antwort = Headset offline
|
// Heartbeat: PID abfragen — keine Antwort = Headset offline
|
||||||
let packet = protocol::build_get_packet(ENDPOINT_HEADSET, Property::ProductId.id());
|
let packet = protocol::build_get_packet(ENDPOINT_HEADSET, Property::ProductId.id());
|
||||||
let response = hid::send_recv_optional(&self.device, &packet)?;
|
let resp = hid::send_recv_optional(&self.device, &packet)?;
|
||||||
if response.is_none() {
|
if verbose {
|
||||||
|
eprintln!("[init] Headset PID (Heartbeat): {}", format_resp(&resp));
|
||||||
|
}
|
||||||
|
if resp.is_none() {
|
||||||
return Err(CorsairError::HeadsetOffline);
|
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 {
|
impl Drop for BragiDevice {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.cleanup();
|
self.cleanup();
|
||||||
|
|||||||
@ -6,6 +6,10 @@ use clap::{Parser, Subcommand};
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "corsairctl", version, about = "CLI für Corsair Bragi-Geräte")]
|
#[command(name = "corsairctl", version, about = "CLI für Corsair Bragi-Geräte")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
|
/// Debug-Output auf stderr ausgeben
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
pub verbose: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Command,
|
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::bragi::protocol::{CORSAIR_VID, HID_INTERFACE, PACKET_SIZE, REPORT_SIZE};
|
||||||
use crate::error::{CorsairError, Result};
|
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> {
|
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()
|
.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)?;
|
.ok_or(CorsairError::DeviceNotFound)?;
|
||||||
|
|
||||||
let device = device_info.open_device(api)?;
|
let device = device_info.open_device(api)?;
|
||||||
Ok(device)
|
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.
|
/// Sendet ein Paket und liest die Antwort.
|
||||||
pub fn send_recv(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Result<Vec<u8>> {
|
pub fn send_recv(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Result<Vec<u8>> {
|
||||||
device.write(packet)?;
|
device.write(packet)?;
|
||||||
|
|||||||
@ -17,7 +17,7 @@ fn run() -> error::Result<()> {
|
|||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Battery => {
|
Command::Battery => {
|
||||||
let device = bragi::BragiDevice::open()?;
|
let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?;
|
||||||
let level = device.battery_level()?;
|
let level = device.battery_level()?;
|
||||||
let status = device.battery_status()?;
|
let status = device.battery_status()?;
|
||||||
println!("{}", output::format_battery(level, &status));
|
println!("{}", output::format_battery(level, &status));
|
||||||
@ -35,7 +35,7 @@ fn run() -> error::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Command::Led { brightness } => {
|
Command::Led { brightness } => {
|
||||||
let device = bragi::BragiDevice::open()?;
|
let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?;
|
||||||
if let Some(value) = brightness {
|
if let Some(value) = brightness {
|
||||||
let clamped = value.clamp(0, 1000);
|
let clamped = value.clamp(0, 1000);
|
||||||
device.set_brightness(clamped)?;
|
device.set_brightness(clamped)?;
|
||||||
@ -47,7 +47,7 @@ fn run() -> error::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Command::Info => {
|
Command::Info => {
|
||||||
let device = bragi::BragiDevice::open()?;
|
let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?;
|
||||||
let vid = device.vendor_id()?;
|
let vid = device.vendor_id()?;
|
||||||
let pid = device.product_id()?;
|
let pid = device.product_id()?;
|
||||||
let fw_app = device.firmware_app()?;
|
let fw_app = device.firmware_app()?;
|
||||||
@ -56,7 +56,7 @@ fn run() -> error::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Command::Json => {
|
Command::Json => {
|
||||||
let device = bragi::BragiDevice::open()?;
|
let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?;
|
||||||
let level = device.battery_level()?;
|
let level = device.battery_level()?;
|
||||||
let status = device.battery_status()?;
|
let status = device.battery_status()?;
|
||||||
println!("{}", output::format_waybar_json(level, &status));
|
println!("{}", output::format_waybar_json(level, &status));
|
||||||
|
|||||||
@ -5,24 +5,22 @@ use alsa::mixer::{Mixer, SelemChannelId, SelemId};
|
|||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
const CARD_NAME: &str = "Gaming";
|
const SIDETONE_CONTROL: &str = "Sidetone";
|
||||||
const SIDETONE_VOLUME: &str = "Sidetone Playback Volume";
|
|
||||||
const SIDETONE_SWITCH: &str = "Sidetone Playback Switch";
|
|
||||||
|
|
||||||
/// 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> {
|
fn find_card() -> Result<String> {
|
||||||
// ALSA-Cards durchiterieren
|
for card in alsa::card::Iter::new().flatten() {
|
||||||
for card_idx in 0..32 {
|
let name = format!("hw:{}", card.get_index());
|
||||||
let name = format!("hw:{card_idx}");
|
if let Ok(mixer) = Mixer::new(&name, false) {
|
||||||
if let Ok(ctl) = alsa::Ctl::new(&name, false) {
|
let selem_id = SelemId::new(SIDETONE_CONTROL, 0);
|
||||||
if let Ok(info) = ctl.card_info() {
|
if mixer.find_selem(&selem_id).is_some() {
|
||||||
let card_name = info.get_name().unwrap_or_default();
|
|
||||||
if card_name.contains(CARD_NAME) {
|
|
||||||
return Ok(name);
|
return Ok(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(crate::error::CorsairError::DeviceNotFound)
|
Err(crate::error::CorsairError::DeviceNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +29,7 @@ pub fn get_level() -> Result<i64> {
|
|||||||
let card = find_card()?;
|
let card = find_card()?;
|
||||||
let mixer = Mixer::new(&card, false)?;
|
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
|
let selem = mixer
|
||||||
.find_selem(&selem_id)
|
.find_selem(&selem_id)
|
||||||
.ok_or(crate::error::CorsairError::DeviceNotFound)?;
|
.ok_or(crate::error::CorsairError::DeviceNotFound)?;
|
||||||
@ -46,19 +44,12 @@ pub fn set_level(level: i64) -> Result<()> {
|
|||||||
let mixer = Mixer::new(&card, false)?;
|
let mixer = Mixer::new(&card, false)?;
|
||||||
|
|
||||||
// Volume setzen
|
// Volume setzen
|
||||||
let volume_id = SelemId::new(SIDETONE_VOLUME, 0);
|
let volume_id = SelemId::new(SIDETONE_CONTROL, 0);
|
||||||
let volume_elem = mixer
|
let volume_elem = mixer
|
||||||
.find_selem(&volume_id)
|
.find_selem(&volume_id)
|
||||||
.ok_or(crate::error::CorsairError::DeviceNotFound)?;
|
.ok_or(crate::error::CorsairError::DeviceNotFound)?;
|
||||||
|
|
||||||
volume_elem.set_playback_volume_all(level)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user