feat: initiale Implementierung von corsairctl
Rust CLI-Tool für Corsair Bragi-Geräte (HS80 RGB Wireless, etc.). Implementiert Protokoll-Kern, HID-Kommunikation, BragiDevice mit RAII-Lifecycle, CLI-Subcommands (battery, sidetone, led, info, json, udev), ALSA-Sidetone-Steuerung und Waybar-JSON-Output. 24 Unit-Tests für Packet-Bau, Response-Parsing und Property-Enums.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// ABOUTME: Unit-Tests für Bragi Property-Enums und Battery-Level-Konvertierung.
|
||||
// ABOUTME: Testet BatteryStatus-Konvertierung und Promille-zu-Prozent-Umrechnung.
|
||||
|
||||
use corsairctl::bragi::properties::{battery_promille_to_percent, BatteryStatus, Property};
|
||||
|
||||
#[test]
|
||||
fn battery_status_from_byte_discharging() {
|
||||
assert_eq!(BatteryStatus::from_byte(0x01), BatteryStatus::Discharging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_status_from_byte_charging_both_values() {
|
||||
// Sowohl 0x03 als auch 0x04 bedeuten "Charging"
|
||||
assert_eq!(BatteryStatus::from_byte(0x03), BatteryStatus::Charging);
|
||||
assert_eq!(BatteryStatus::from_byte(0x04), BatteryStatus::Charging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_status_from_byte_all_known_values() {
|
||||
assert_eq!(BatteryStatus::from_byte(0x00), BatteryStatus::Offline);
|
||||
assert_eq!(BatteryStatus::from_byte(0x01), BatteryStatus::Discharging);
|
||||
assert_eq!(BatteryStatus::from_byte(0x02), BatteryStatus::Low);
|
||||
assert_eq!(BatteryStatus::from_byte(0x03), BatteryStatus::Charging);
|
||||
assert_eq!(BatteryStatus::from_byte(0x04), BatteryStatus::Charging);
|
||||
assert_eq!(BatteryStatus::from_byte(0x05), BatteryStatus::FullyCharged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_status_unknown_value() {
|
||||
let status = BatteryStatus::from_byte(0xFF);
|
||||
assert!(matches!(status, BatteryStatus::Unknown(0xFF)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_status_labels() {
|
||||
assert_eq!(BatteryStatus::Offline.label(), "Offline");
|
||||
assert_eq!(BatteryStatus::Discharging.label(), "Discharging");
|
||||
assert_eq!(BatteryStatus::Low.label(), "Low");
|
||||
assert_eq!(BatteryStatus::Charging.label(), "Charging");
|
||||
assert_eq!(BatteryStatus::FullyCharged.label(), "Full");
|
||||
assert_eq!(BatteryStatus::Unknown(0xFF).label(), "Unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_status_is_charging() {
|
||||
assert!(BatteryStatus::Charging.is_charging());
|
||||
assert!(!BatteryStatus::Discharging.is_charging());
|
||||
assert!(!BatteryStatus::FullyCharged.is_charging());
|
||||
assert!(!BatteryStatus::Offline.is_charging());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_promille_to_percent_full() {
|
||||
assert!((battery_promille_to_percent(1000) - 100.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_promille_to_percent_half() {
|
||||
assert!((battery_promille_to_percent(500) - 50.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_promille_to_percent_empty() {
|
||||
assert!((battery_promille_to_percent(0) - 0.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_promille_to_percent_fractional() {
|
||||
// 735 Promille = 73.5%
|
||||
assert!((battery_promille_to_percent(735) - 73.5).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_ids_match_protocol() {
|
||||
assert_eq!(Property::BatteryLevel.id(), 0x0F);
|
||||
assert_eq!(Property::BatteryStatus.id(), 0x10);
|
||||
assert_eq!(Property::Brightness.id(), 0x02);
|
||||
assert_eq!(Property::Mode.id(), 0x03);
|
||||
assert_eq!(Property::VendorId.id(), 0x11);
|
||||
assert_eq!(Property::ProductId.id(), 0x12);
|
||||
assert_eq!(Property::AppFirmware.id(), 0x13);
|
||||
assert_eq!(Property::BuildFirmware.id(), 0x14);
|
||||
assert_eq!(Property::Sidetone.id(), 0x09);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// ABOUTME: Unit-Tests für Bragi-Protokoll Packet-Bau und Response-Parsing.
|
||||
// ABOUTME: Testet build_packet, parse_response und BragiResponse-Methoden.
|
||||
|
||||
use corsairctl::bragi::protocol::*;
|
||||
use corsairctl::error::CorsairError;
|
||||
|
||||
#[test]
|
||||
fn build_get_packet_has_correct_header() {
|
||||
let packet = build_get_packet(ENDPOINT_HEADSET, 0x0F);
|
||||
|
||||
assert_eq!(packet[0], 0x00, "Report ID");
|
||||
assert_eq!(packet[1], PROTOCOL_MARKER, "Protokoll-Marker");
|
||||
assert_eq!(packet[2], ENDPOINT_HEADSET, "Endpoint");
|
||||
assert_eq!(packet[3], CMD_GET, "Command");
|
||||
assert_eq!(packet[4], 0x0F, "Property");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_get_packet_is_zero_padded() {
|
||||
let packet = build_get_packet(ENDPOINT_RECEIVER, 0x13);
|
||||
|
||||
assert_eq!(packet.len(), PACKET_SIZE);
|
||||
// Alle Bytes nach der Property sollten 0 sein
|
||||
for &byte in &packet[5..] {
|
||||
assert_eq!(byte, 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_set_packet_includes_data() {
|
||||
let packet = build_set_packet(ENDPOINT_HEADSET, 0x03, &[0x00, 0x02]);
|
||||
|
||||
assert_eq!(packet[3], CMD_SET, "Command");
|
||||
assert_eq!(packet[4], 0x03, "Property");
|
||||
assert_eq!(packet[5], 0x00, "Data byte 0");
|
||||
assert_eq!(packet[6], 0x02, "Data byte 1");
|
||||
// Rest sollte 0 sein
|
||||
assert_eq!(packet[7], 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_packet_total_size() {
|
||||
let packet = build_packet(0x08, 0x01, 0x03, &[0x00, 0x02]);
|
||||
assert_eq!(packet.len(), 65);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_extracts_fields() {
|
||||
// Simulierte Response: [marker, status, endpoint, command, data0, data1, ...]
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
|
||||
assert_eq!(resp.status, STATUS_OK);
|
||||
assert_eq!(resp.endpoint, ENDPOINT_HEADSET);
|
||||
assert_eq!(resp.command, CMD_GET);
|
||||
assert_eq!(resp.data[0], 0xE8);
|
||||
assert_eq!(resp.data[1], 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_value_u16_little_endian() {
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
let value = resp.value_u16().unwrap();
|
||||
|
||||
// 0xE8 | (0x03 << 8) = 232 + 768 = 1000
|
||||
assert_eq!(value, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_value_u8() {
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
assert_eq!(resp.value_u8().unwrap(), 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_error_f0() {
|
||||
let raw = vec![0x02, 0xF0, 0x09, 0x02];
|
||||
|
||||
let result = parse_response(&raw);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::PropertyNotSupported { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_error_f1() {
|
||||
let raw = vec![0x02, 0xF1, 0x09, 0x02];
|
||||
|
||||
let result = parse_response(&raw);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::PropertyNotSupported { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_too_short() {
|
||||
let raw = vec![0x02, 0x00];
|
||||
|
||||
let result = parse_response(&raw);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::ResponseTooShort { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_u16_on_empty_data_returns_error() {
|
||||
let raw = vec![0x02, 0x00, 0x09, 0x02]; // Keine Daten-Bytes
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
|
||||
let result = resp.value_u16();
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::ResponseTooShort { expected: 2, got: 0 }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_level_full_is_1000_promille() {
|
||||
// Battery Level = 1000 (voll) → 0xE8, 0x03 LE
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
let promille = resp.value_u16().unwrap();
|
||||
assert_eq!(promille, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_level_half_is_500_promille() {
|
||||
// Battery Level = 500 (50%) → 0xF4, 0x01 LE
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xF4, 0x01];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
let promille = resp.value_u16().unwrap();
|
||||
assert_eq!(promille, 500);
|
||||
}
|
||||
Reference in New Issue
Block a user