From eab7e89fa3af3d01ce564c3a6e088022f718fdac Mon Sep 17 00:00:00 2001 From: Ghoul Date: Wed, 21 Jan 2026 20:00:03 +0500 Subject: [PATCH] feat: add anime-led-scan tool Usage: cargo run --example anime-led-scan --- .gitignore | 5 +- asusctl/examples/anime-led-scan.rs | 547 +++++++++++++++++++++++++++++ rog-dbus/src/zbus_anime.rs | 2 +- 3 files changed, 551 insertions(+), 3 deletions(-) create mode 100644 asusctl/examples/anime-led-scan.rs diff --git a/.gitignore b/.gitignore index 161718aa..65253858 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ desktop-extensions/gnome*/node_modules desktop-extensions/gnome*/schemas/gschemas.compiled desktop-extensions/gnome*/*.zip -# agents +# agents and reference CLAUDE.md -AGENT.md +AGENTS.md +/reference diff --git a/asusctl/examples/anime-led-scan.rs b/asusctl/examples/anime-led-scan.rs new file mode 100644 index 00000000..c4ff9b94 --- /dev/null +++ b/asusctl/examples/anime-led-scan.rs @@ -0,0 +1,547 @@ +//! LED scanning tool for discovering AniMe Matrix buffer-to-LED mappings. +//! +//! This tool lights up one buffer index at a time, allowing you to observe +//! which physical LED corresponds to each buffer position. This is essential +//! for mapping new device types like G835L where the exact layout is unknown. +//! +//! You might want to use it slowly, as it sometimes doesn't work properly. +//! Maybe there's better ways to make this reliable but for now it works for my use case. +//! +//! # Usage +//! ``` +//! cargo run --example anime-led-scan -- [options] +//! ``` +//! +//! # Controls +//! - `n` or `Enter`: Next index +//! - `p` or `Backspace`: Previous index +//! - `j` followed by number: Jump to specific index +//! - `+` / `-`: Adjust step size (default 1) +//! - `s`: Save current index to notes file +//! - `r`: Mark current index as row start +//! - `q` or `Ctrl+C`: Quit +//! +//! # Output +//! Creates a `led-scan-notes.txt` file with recorded observations. + +use std::env; +use std::fs::OpenOptions; +use std::io::{self, BufRead, Write}; + +use rog_anime::usb::{get_anime_type, Brightness}; +use rog_anime::{AnimeDataBuffer, AnimeType}; +use rog_dbus::zbus_anime::AnimeProxyBlocking; +use zbus::blocking::Connection; + +/// Saved device state for restoration on exit +struct SavedState { + builtins_enabled: bool, + brightness: Brightness, + display_enabled: bool, +} + +fn print_help(scan_len: usize, buffer_len: usize) { + println!("\n=== LED Scan Tool ==="); + println!( + "Scan range: 0-{} (buffer size: {})", + scan_len - 1, + buffer_len + ); + println!("Commands:"); + println!(" n, Enter - Next index"); + println!(" p, Backspace - Previous index"); + println!(" j - Jump to index"); + println!(" + / - - Increase/decrease step size"); + println!(" s - Save note for current index"); + println!(" r - Mark as row start"); + println!(" a - Auto-scan (runs through all indices)"); + println!(" f - Fill all buffer bytes"); + println!(" f - Fill range (inclusive)"); + println!(" p1/p2/p3 - Fill pane 1/2/3 only (each is 627 bytes)"); + println!(" hold - Hold current LED (press Enter to release)"); + println!(" hold - Hold range (press Enter to release)"); + println!(" c - Clear display"); + println!(" row - Step through rows (G835L, provisional)"); + println!(" row - Show specific row (G835L, provisional)"); + println!(" allrows - Light all rows sequentially (G835L)"); + println!(" rowmap - Print the full row mapping (G835L)"); + println!(" h - Show this help"); + println!(" q - Quit and restore state"); + println!(); +} + +fn save_note(index: usize, note: &str) -> io::Result<()> { + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open("led-scan-notes.txt")?; + writeln!(file, "Index {}: {}", index, note)?; + Ok(()) +} + +fn write_single_led( + proxy: &AnimeProxyBlocking, + anime_type: AnimeType, + index: usize, + brightness: u8, +) { + let mut buffer = AnimeDataBuffer::new(anime_type); + let data = buffer.data_mut(); + if index < data.len() { + data[index] = brightness; + } + if let Err(e) = proxy.write(buffer) { + eprintln!("Error writing to device: {}", e); + } +} + +fn clear_display(proxy: &AnimeProxyBlocking, anime_type: AnimeType) { + let buffer = AnimeDataBuffer::new(anime_type); + let _ = proxy.write(buffer); +} + +fn fill_display(proxy: &AnimeProxyBlocking, anime_type: AnimeType, brightness: u8) { + let mut buffer = AnimeDataBuffer::new(anime_type); + let data = buffer.data_mut(); + for byte in data.iter_mut() { + *byte = brightness; + } + if let Err(e) = proxy.write(buffer) { + eprintln!("Error writing to device: {}", e); + } +} + +/// Fill a range of LEDs. Both start and end are INCLUSIVE. +fn fill_range( + proxy: &AnimeProxyBlocking, + anime_type: AnimeType, + start: usize, + end: usize, + brightness: u8, +) { + let mut buffer = AnimeDataBuffer::new(anime_type); + let data = buffer.data_mut(); + for i in start..=end.min(data.len().saturating_sub(1)) { + data[i] = brightness; + } + if let Err(e) = proxy.write(buffer) { + eprintln!("Error writing to device: {}", e); + } +} + +fn fill_pane(proxy: &AnimeProxyBlocking, anime_type: AnimeType, pane: usize, brightness: u8) { + const PANE_LEN: usize = 627; + let start = pane * PANE_LEN; + let end = start + PANE_LEN - 1; + fill_range(proxy, anime_type, start, end, brightness); +} + +/// G835L row pattern (PROVISIONAL - needs hardware verification): +/// - Rows 0-1: 1 LED each +/// - Rows 2-3: 2 LEDs each +/// - ... (pairs of rows with same length) +/// - Rows 26-27: 14 LEDs each +/// - Rows 28+: 15 LEDs each (constant) +/// +/// Returns (start_index, end_index_inclusive, row_length) +fn g835l_row_bounds(row: usize) -> (usize, usize, usize) { + let triangle_rows = 28; + let triangle_leds = 210; + + if row < triangle_rows { + let length = row / 2 + 1; + let mut start = 0usize; + for r in 0..row { + start += r / 2 + 1; + } + (start, start + length - 1, length) + } else { + let rows_after_triangle = row - triangle_rows; + let start = triangle_leds + rows_after_triangle * 15; + (start, start + 14, 15) + } +} + +fn g835l_total_rows() -> usize { + 28 + 40 +} + +fn save_state(proxy: &AnimeProxyBlocking) -> SavedState { + SavedState { + builtins_enabled: proxy.builtins_enabled().unwrap_or(false), + brightness: proxy.brightness().unwrap_or(Brightness::Med), + display_enabled: proxy.enable_display().unwrap_or(true), + } +} + +fn restore_state(proxy: &AnimeProxyBlocking, state: &SavedState) { + let _ = proxy.set_builtins_enabled(state.builtins_enabled); + let _ = proxy.set_brightness(state.brightness); + let _ = proxy.set_enable_display(state.display_enabled); + let _ = proxy.run_main_loop(true); +} + +fn main() { + let args: Vec = env::args().collect(); + + let mut start_index = 0usize; + let mut brightness = 200u8; + let mut scan_limit: Option = None; + + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--start" | "-s" => { + if i + 1 < args.len() { + start_index = args[i + 1].parse().unwrap_or(0); + i += 1; + } + } + "--brightness" | "-b" => { + if i + 1 < args.len() { + brightness = args[i + 1].parse().unwrap_or(200); + i += 1; + } + } + "--limit" | "-l" => { + if i + 1 < args.len() { + scan_limit = args[i + 1].parse().ok(); + i += 1; + } + } + "--help" | "-h" => { + println!("LED Scan Tool for AniMe Matrix"); + println!(); + println!("Usage: anime-led-scan [options]"); + println!(); + println!("Options:"); + println!(" -s, --start Start at index N (default: 0)"); + println!(" -b, --brightness LED brightness 0-255 (default: 200)"); + println!(" -l, --limit Cap scan range to N indices (e.g. 810 for G835L)"); + println!(" -h, --help Show this help"); + return; + } + _ => {} + } + i += 1; + } + + let conn = match Connection::system() { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to connect to D-Bus: {}", e); + eprintln!("Make sure asusd is running."); + return; + } + }; + + let proxy = match AnimeProxyBlocking::new(&conn) { + Ok(p) => p, + Err(e) => { + eprintln!("Failed to create Anime proxy: {}", e); + eprintln!("Make sure asusd supports your device."); + return; + } + }; + + let anime_type = get_anime_type(); + let buffer_len = anime_type.data_length(); + let scan_len = scan_limit.unwrap_or(buffer_len).min(buffer_len); + + println!("=== LED Scan Tool ==="); + println!("Device type: {:?}", anime_type); + println!("Buffer length: {} bytes", buffer_len); + println!("Scan range: 0-{}", scan_len - 1); + println!("Brightness: {}", brightness); + println!(); + + // Save current state for restoration + let saved_state = save_state(&proxy); + println!("Saved device state for restoration on exit."); + + // Stop system animations + if let Err(e) = proxy.run_main_loop(false) { + eprintln!("Warning: Could not stop main loop: {}", e); + } + println!("Stopped system animations."); + + print_help(scan_len, buffer_len); + + let mut current_index = start_index.min(scan_len - 1); + let mut step = 1usize; + + write_single_led(&proxy, anime_type, current_index, brightness); + println!(">>> Index: {} (step: {})", current_index, step); + + let stdin = io::stdin(); + let mut input = String::new(); + + loop { + input.clear(); + print!("> "); + io::stdout().flush().unwrap(); + + if stdin.lock().read_line(&mut input).is_err() { + break; + } + + let cmd = input.trim(); + + match cmd { + "q" | "quit" | "exit" => { + clear_display(&proxy, anime_type); + restore_state(&proxy, &saved_state); + println!("Restored device state. Goodbye!"); + break; + } + "n" | "" => { + current_index = (current_index + step).min(scan_len - 1); + write_single_led(&proxy, anime_type, current_index, brightness); + println!(">>> Index: {} (step: {})", current_index, step); + } + "p" => { + current_index = current_index.saturating_sub(step); + write_single_led(&proxy, anime_type, current_index, brightness); + println!(">>> Index: {} (step: {})", current_index, step); + } + "+" => { + step = step.saturating_mul(2).max(1); + println!("Step size: {}", step); + } + "-" => { + step = step.saturating_div(2).max(1); + println!("Step size: {}", step); + } + "r" => { + if let Err(e) = save_note(current_index, "ROW START") { + eprintln!("Error saving note: {}", e); + } else { + println!("Saved: Index {} marked as ROW START", current_index); + } + } + "h" | "help" | "?" => { + print_help(scan_len, buffer_len); + } + cmd if cmd.starts_with('j') => { + let num_str = cmd.trim_start_matches('j').trim(); + if let Ok(idx) = num_str.parse::() { + if idx < scan_len { + current_index = idx; + write_single_led(&proxy, anime_type, current_index, brightness); + println!(">>> Index: {} (step: {})", current_index, step); + } else { + println!("Index {} out of range (max: {})", idx, scan_len - 1); + } + } else { + println!("Usage: j "); + } + } + cmd if cmd.starts_with('s') && !cmd.starts_with("show") => { + let note = cmd.trim_start_matches('s').trim(); + let note = if note.is_empty() { "observed" } else { note }; + if let Err(e) = save_note(current_index, note) { + eprintln!("Error saving note: {}", e); + } else { + println!("Saved note for index {}", current_index); + } + } + "a" => { + println!("Auto-scan mode (0 to {})...", scan_len - 1); + let delay = std::time::Duration::from_millis(10); + for idx in current_index..scan_len { + write_single_led(&proxy, anime_type, idx, brightness); + print!("\rIndex: {} / {} ", idx, scan_len - 1); + io::stdout().flush().unwrap(); + std::thread::sleep(delay); + current_index = idx; + } + println!(); + println!("Auto-scan complete. Current index: {}", current_index); + } + "c" => { + clear_display(&proxy, anime_type); + println!("Display cleared"); + } + "f" => { + fill_display(&proxy, anime_type, brightness); + println!("All buffer bytes filled at brightness {}", brightness); + } + "p1" => { + fill_pane(&proxy, anime_type, 0, brightness); + println!("Pane 1 (indices 0-626) filled"); + } + "p2" => { + fill_pane(&proxy, anime_type, 1, brightness); + println!("Pane 2 (indices 627-1253) filled"); + } + "p3" => { + fill_pane(&proxy, anime_type, 2, brightness); + println!("Pane 3 (indices 1254-1880) filled"); + } + cmd if cmd.starts_with("f ") => { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + if parts.len() == 3 { + if let (Ok(start), Ok(end)) = + (parts[1].parse::(), parts[2].parse::()) + { + fill_range(&proxy, anime_type, start, end, brightness); + println!("Filled indices {} to {}", start, end); + } else { + println!("Usage: f "); + } + } else { + println!("Usage: f "); + } + } + "show" => { + write_single_led(&proxy, anime_type, current_index, brightness); + println!(">>> Index: {} (step: {})", current_index, step); + } + "row" => { + if anime_type != AnimeType::G835L { + println!("Warning: Row commands use G835L mapping (provisional). You can add to this code to support other types. `examples/anime-led-scan.rs[402:425]`"); + } + println!("Row stepping mode. Press Enter for next row, 'q' to quit."); + let total = g835l_total_rows(); + for row_num in 0..total { + let (start, end, len) = g835l_row_bounds(row_num); + if end >= scan_len { + println!("Row {} exceeds scan limit, stopping.", row_num); + break; + } + println!("Row {}: indices {}-{} ({} LEDs)", row_num, start, end, len); + fill_range(&proxy, anime_type, start, end, brightness); + input.clear(); + print!("(Enter=next, q=quit) > "); + io::stdout().flush().unwrap(); + if stdin.lock().read_line(&mut input).is_err() { + break; + } + if input.trim() == "q" { + break; + } + clear_display(&proxy, anime_type); + } + println!("Row stepping done."); + } + cmd if cmd.starts_with("row ") => { + if anime_type != AnimeType::G835L { + println!("Warning: Row commands use G835L mapping (provisional)."); + } + let row_str = cmd.trim_start_matches("row ").trim(); + if let Ok(row_num) = row_str.parse::() { + let total = g835l_total_rows(); + if row_num < total { + let (start, end, len) = g835l_row_bounds(row_num); + if end < scan_len { + println!("Row {}: indices {}-{} ({} LEDs)", row_num, start, end, len); + fill_range(&proxy, anime_type, start, end, brightness); + } else { + println!("Row {} exceeds scan limit", row_num); + } + } else { + println!("Row {} out of range (max: {})", row_num, total - 1); + } + } else { + println!("Usage: row "); + } + } + "allrows" => { + if anime_type != AnimeType::G835L { + println!("Warning: Row commands use G835L mapping (provisional)."); + } + println!("Lighting all rows sequentially (200ms each)..."); + let total = g835l_total_rows(); + let delay = std::time::Duration::from_millis(200); + for row_num in 0..total { + let (start, end, len) = g835l_row_bounds(row_num); + if end >= scan_len { + println!("\nRow {} exceeds scan limit, stopping.", row_num); + break; + } + print!( + "\rRow {}/{}: indices {}-{} ({} LEDs) ", + row_num, + total - 1, + start, + end, + len + ); + io::stdout().flush().unwrap(); + fill_range(&proxy, anime_type, start, end, brightness); + std::thread::sleep(delay); + clear_display(&proxy, anime_type); + } + println!("\nDone."); + } + "rowmap" => { + if anime_type != AnimeType::G835L { + println!("Warning: Row map is for G835L (provisional)."); + } + println!("G835L Row Map:"); + let total = g835l_total_rows(); + for row_num in 0..total { + let (start, end, len) = g835l_row_bounds(row_num); + let marker = if end >= scan_len { + " (exceeds limit)" + } else { + "" + }; + println!( + " Row {:2}: indices {:4}-{:4} ({:2} LEDs){}", + row_num, start, end, len, marker + ); + } + } + "hold" => { + // Single write, wait for Enter to release + println!("Holding index {}. Press Enter to release...", current_index); + write_single_led(&proxy, anime_type, current_index, brightness); + input.clear(); + let _ = stdin.lock().read_line(&mut input); + clear_display(&proxy, anime_type); + println!("Released."); + } + cmd if cmd.starts_with("hold ") => { + let arg = cmd.trim_start_matches("hold ").trim(); + let (start, end): (usize, usize) = match arg { + "p1" | "1" => (0, 626), + "p2" | "2" => (627, 1253), + _ => { + let parts: Vec<&str> = arg.split_whitespace().collect(); + if parts.len() == 2 { + if let (Ok(s), Ok(e)) = (parts[0].parse(), parts[1].parse()) { + (s, e) + } else { + println!("Usage: hold p1, hold p2, or hold "); + continue; + } + } else { + println!("Usage: hold p1, hold p2, or hold "); + continue; + } + } + }; + println!("Holding range {}-{}. Press Enter to release...", start, end); + fill_range(&proxy, anime_type, start, end, brightness); + input.clear(); + let _ = stdin.lock().read_line(&mut input); + clear_display(&proxy, anime_type); + println!("Released."); + } + _ => { + if let Ok(idx) = cmd.parse::() { + if idx < scan_len { + current_index = idx; + write_single_led(&proxy, anime_type, current_index, brightness); + println!(">>> Index: {} (step: {})", current_index, step); + } else { + println!("Index {} out of range (max: {})", idx, scan_len - 1); + } + } else { + println!("Unknown command: '{}'. Type 'h' for help.", cmd); + } + } + } + } +} diff --git a/rog-dbus/src/zbus_anime.rs b/rog-dbus/src/zbus_anime.rs index ed0df009..2fdec4b4 100644 --- a/rog-dbus/src/zbus_anime.rs +++ b/rog-dbus/src/zbus_anime.rs @@ -5,7 +5,7 @@ use zbus::proxy; #[proxy( interface = "xyz.ljones.Anime", default_service = "xyz.ljones.Asusd", - default_path = "/xyz/ljones" + default_path = "/xyz/ljones/aura/anime" )] pub trait Anime { /// DeviceState method