respeaker 0.1.0

USB control library for Seeed ReSpeaker Mic Array v2.0 (LEDs + XVF-3000 DSP tuning)
Documentation
//! Interactive CLI to exercise every ReSpeaker API surface.
//!
//! ```bash
//! cargo run --example cli -- --help
//! cargo run --example cli -- test-all
//! cargo run --example cli -- led mono --color 00ff00
//! cargo run --example cli -- param get DOAANGLE
//! ```

use clap::{Parser, Subcommand, ValueEnum};
use respeaker::{Access, ParamKind, ParamValue, ReSpeaker, Rgb, VadLed, PARAMETERS};
use std::process::ExitCode;
use std::thread;
use std::time::Duration;

#[derive(Parser, Debug)]
#[command(
    name = "respeaker-cli",
    about = "Exercise all ReSpeaker Mic Array v2.0 LED + DSP tuning APIs"
)]
struct Opts {
    /// Pause between steps in `test-all` (milliseconds)
    #[arg(long, global = true, default_value_t = 800)]
    delay_ms: u64,

    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand, Debug)]
enum Cmd {
    /// Run through every LED mode and read all DSP parameters
    TestAll,

    /// LED ring commands
    Led {
        #[command(subcommand)]
        cmd: LedCmd,
    },

    /// DSP parameter commands (XVF-3000 tuning)
    Param {
        #[command(subcommand)]
        cmd: ParamCmd,
    },

    /// Print direction of arrival in degrees
    Doa,

    /// Print whether voice activity is currently detected
    Vad,

    /// Print device protocol version byte
    Version,
}

#[derive(Subcommand, Debug)]
enum LedCmd {
    Off,
    Trace,
    Listen,
    Speak,
    Think,
    Spin,
    /// Solid color (`--color RRGGBB` or `--r/--g/--b`)
    Mono {
        #[arg(long)]
        color: Option<String>,
        #[arg(long, default_value_t = 0)]
        r: u8,
        #[arg(long, default_value_t = 0)]
        g: u8,
        #[arg(long, default_value_t = 0)]
        b: u8,
    },
    /// Per-LED custom pattern (rainbow over 12 LEDs)
    Customize,
    /// Brightness 0..=31
    Brightness { level: u8 },
    /// Volume bar 0..=12
    Volume { level: u8 },
    /// Center VAD LED: off | on | auto
    VadLed {
        #[arg(value_enum)]
        state: VadLedCli,
    },
    /// Two-color palette (hex RRGGBB)
    Palette { a: String, b: String },
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum VadLedCli {
    Off,
    On,
    Auto,
}

impl From<VadLedCli> for VadLed {
    fn from(v: VadLedCli) -> Self {
        match v {
            VadLedCli::Off => VadLed::Off,
            VadLedCli::On => VadLed::On,
            VadLedCli::Auto => VadLed::Auto,
        }
    }
}

#[derive(Subcommand, Debug)]
enum ParamCmd {
    /// List known parameter names and metadata
    List,
    Get { name: String },
    Set { name: String, value: String },
    ReadAll,
}

fn main() -> ExitCode {
    let opts = Opts::parse();
    match run(opts) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("error: {e}");
            ExitCode::FAILURE
        }
    }
}

fn run(opts: Opts) -> respeaker::Result<()> {
    if matches!(opts.cmd, Cmd::Param { cmd: ParamCmd::List }) {
        print_param_list();
        return Ok(());
    }

    let dev = ReSpeaker::open()?;
    let delay = Duration::from_millis(opts.delay_ms);

    match opts.cmd {
        Cmd::TestAll => test_all(&dev, delay)?,
        Cmd::Led { cmd } => run_led(&dev, cmd)?,
        Cmd::Param { cmd } => run_param(&dev, cmd)?,
        Cmd::Doa => println!("DOA: {}°", dev.tuning().doa_angle()?),
        Cmd::Vad => println!(
            "VOICEACTIVITY: {}",
            if dev.tuning().is_voice()? { 1 } else { 0 }
        ),
        Cmd::Version => println!("version: {}", dev.tuning().version()?),
    }

    Ok(())
}

fn test_all(dev: &ReSpeaker, delay: Duration) -> respeaker::Result<()> {
    let led = dev.led();
    let tuning = dev.tuning();

    println!("=== Device ===");
    println!("protocol version: {}", tuning.version()?);
    println!("DOA: {}°", tuning.doa_angle()?);
    println!("voice: {}", tuning.is_voice()?);
    println!();

    println!("=== LED modes ===");
    step("brightness 0x1F", delay, || led.set_brightness(0x1F))?;
    step("palette red/green", delay, || {
        led.set_color_palette(Rgb::RED, Rgb::GREEN)
    })?;
    step("vad-led auto", delay, || led.set_vad_led(VadLed::Auto))?;
    step("trace", delay, || led.trace())?;
    step("listen", delay, || led.listen())?;
    step("speak", delay, || led.speak())?;
    step("think", delay, || led.think())?;
    step("spin", delay, || led.spin())?;
    step("mono red", delay, || led.mono(Rgb::RED))?;
    step("mono green", delay, || led.mono(Rgb::GREEN))?;
    step("mono blue", delay, || led.mono(Rgb::BLUE))?;
    step("mono white", delay, || led.mono(Rgb::WHITE))?;
    step("customize rainbow", delay, || led.customize(&rainbow()))?;
    for level in [0u8, 4, 8, 12] {
        step(&format!("volume {level}"), delay, || led.set_volume(level))?;
    }
    for level in [0u8, 8, 16, 31] {
        step(&format!("brightness {level}"), delay, || {
            led.set_brightness(level)
        })?;
    }
    step("vad-led off", delay, || led.set_vad_led(VadLed::Off))?;
    step("vad-led on", delay, || led.set_vad_led(VadLed::On))?;
    step("off", delay, || led.off())?;

    println!();
    println!("=== DSP parameters (read-all) ===");
    println!("{:<24} {}", "name", "value");
    println!("{}", "-".repeat(40));
    for p in PARAMETERS {
        match tuning.read(p.name) {
            Ok(ParamValue::Int(v)) => println!("{:<24} {v}", p.name),
            Ok(ParamValue::Float(v)) => println!("{:<24} {v}", p.name),
            Err(e) => println!("{:<24} <error: {e}>", p.name),
        }
    }

    println!();
    println!("=== Convenience helpers ===");
    println!("doa_angle()     = {}°", tuning.doa_angle()?);
    println!("is_voice()      = {}", tuning.is_voice()?);
    // Read-only round-trip of a safe rw int (don't change device config in test-all).
    let agc = tuning.read("AGCONOFF")?;
    println!("read(AGCONOFF)  = {agc}");
    let gamma = tuning.read("GAMMAVAD_SR")?;
    println!("read(GAMMAVAD_SR) = {gamma}");

    println!();
    println!("test-all finished (LEDs left off)");
    Ok(())
}

fn step(label: &str, delay: Duration, f: impl FnOnce() -> respeaker::Result<()>) -> respeaker::Result<()> {
    print!("  {label} ... ");
    f()?;
    println!("ok");
    thread::sleep(delay);
    Ok(())
}

fn rainbow() -> [Rgb; 12] {
    // Rough HSV wheel across the 12 ring LEDs.
    let mut colors = [Rgb::BLACK; 12];
    for (i, c) in colors.iter_mut().enumerate() {
        let hue = (i as f32) / 12.0;
        *c = hsv_to_rgb(hue, 1.0, 1.0);
    }
    colors
}

fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Rgb {
    let i = (h * 6.0).floor();
    let f = h * 6.0 - i;
    let p = v * (1.0 - s);
    let q = v * (1.0 - f * s);
    let t = v * (1.0 - (1.0 - f) * s);
    let (r, g, b) = match (i as i32).rem_euclid(6) {
        0 => (v, t, p),
        1 => (q, v, p),
        2 => (p, v, t),
        3 => (p, q, v),
        4 => (t, p, v),
        _ => (v, p, q),
    };
    Rgb::new(
        (r * 255.0).round() as u8,
        (g * 255.0).round() as u8,
        (b * 255.0).round() as u8,
    )
}

fn run_led(dev: &ReSpeaker, cmd: LedCmd) -> respeaker::Result<()> {
    let led = dev.led();
    match cmd {
        LedCmd::Off => {
            led.off()?;
            println!("LEDs off");
        }
        LedCmd::Trace => {
            led.trace()?;
            println!("LED mode: trace");
        }
        LedCmd::Listen => {
            led.listen()?;
            println!("LED mode: listen");
        }
        LedCmd::Speak => {
            led.speak()?;
            println!("LED mode: speak");
        }
        LedCmd::Think => {
            led.think()?;
            println!("LED mode: think");
        }
        LedCmd::Spin => {
            led.spin()?;
            println!("LED mode: spin");
        }
        LedCmd::Mono { color, r, g, b } => {
            let rgb = if let Some(c) = color {
                Rgb::from_hex(&c)?
            } else {
                Rgb::new(r, g, b)
            };
            led.mono(rgb)?;
            println!("LED mono #{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b);
        }
        LedCmd::Customize => {
            led.customize(&rainbow())?;
            println!("LED customize: rainbow");
        }
        LedCmd::Brightness { level } => {
            led.set_brightness(level)?;
            println!("LED brightness {level}");
        }
        LedCmd::Volume { level } => {
            led.set_volume(level)?;
            println!("LED volume {level}");
        }
        LedCmd::VadLed { state } => {
            led.set_vad_led(state.into())?;
            println!("LED VAD center: {state:?}");
        }
        LedCmd::Palette { a, b } => {
            let a = Rgb::from_hex(&a)?;
            let b = Rgb::from_hex(&b)?;
            led.set_color_palette(a, b)?;
            println!(
                "LED palette #{:02x}{:02x}{:02x} / #{:02x}{:02x}{:02x}",
                a.r, a.g, a.b, b.r, b.g, b.b
            );
        }
    }
    Ok(())
}

fn run_param(dev: &ReSpeaker, cmd: ParamCmd) -> respeaker::Result<()> {
    let tuning = dev.tuning();
    match cmd {
        ParamCmd::List => unreachable!("handled before open"),
        ParamCmd::Get { name } => {
            println!("{name}: {}", tuning.read(&name)?);
        }
        ParamCmd::Set { name, value } => {
            tuning.write_str(&name, &value)?;
            println!("{name}: {}", tuning.read(&name)?);
        }
        ParamCmd::ReadAll => {
            println!("{:<24} {}", "name", "value");
            println!("{}", "-".repeat(40));
            for p in PARAMETERS {
                match tuning.read(p.name) {
                    Ok(ParamValue::Int(v)) => println!("{:<24} {v}", p.name),
                    Ok(ParamValue::Float(v)) => println!("{:<24} {v}", p.name),
                    Err(e) => println!("{:<24} <error: {e}>", p.name),
                }
            }
        }
    }
    Ok(())
}

fn print_param_list() {
    println!(
        "{:<22} {:<6} {:>8} {:>8} {:<4} {}",
        "name", "type", "max", "min", "r/w", "info"
    );
    println!("{}", "-".repeat(80));
    for p in PARAMETERS {
        let kind = match p.kind {
            ParamKind::Int => "int",
            ParamKind::Float => "float",
        };
        let access = match p.access {
            Access::ReadWrite => "rw",
            Access::ReadOnly => "ro",
        };
        println!(
            "{:<22} {:<6} {:>8} {:>8} {:<4} {}",
            p.name, kind, p.max, p.min, access, p.info
        );
    }
}