use clap::{Parser, Subcommand, ValueEnum};
use respeaker::{
Access as V2Access, Model, ParamKind, ParamValue, ReSpeaker, Rgb, VadLed, XvfAccess,
PARAMETERS as V2_PARAMETERS, XVF_COMMANDS,
};
use std::process::ExitCode;
use std::thread;
use std::time::Duration;
#[derive(Parser, Debug)]
#[command(
name = "respeaker-cli",
about = "Exercise ReSpeaker Mic Array v2.0 and XVF3800 LED + DSP APIs"
)]
struct Opts {
#[arg(long, global = true, default_value_t = 800)]
delay_ms: u64,
#[arg(long, global = true, value_enum)]
model: Option<ModelCli>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum ModelCli {
V2,
Xvf3800,
Lite,
}
impl From<ModelCli> for Model {
fn from(v: ModelCli) -> Self {
match v {
ModelCli::V2 => Model::MicArrayV2,
ModelCli::Xvf3800 => Model::Xvf3800,
ModelCli::Lite => Model::Lite,
}
}
}
#[derive(Subcommand, Debug)]
enum Cmd {
Model,
TestAll,
Led {
#[command(subcommand)]
cmd: LedCmd,
},
Param {
#[command(subcommand)]
cmd: ParamCmd,
},
Doa,
Vad,
Version,
SaveConfig,
ClearConfig,
}
#[derive(Subcommand, Debug)]
enum LedCmd {
Off,
Trace,
Listen,
Speak,
Think,
Spin,
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,
},
Customize,
Brightness { level: u8 },
Volume { level: u8 },
VadLed {
#[arg(value_enum)]
state: VadLedCli,
},
Palette { a: String, b: String },
Effect {
#[arg(value_enum)]
mode: EffectCli,
},
Speed { level: u8 },
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum EffectCli {
Off,
Breath,
Rainbow,
Mono,
Doa,
Ring,
}
impl From<EffectCli> for respeaker::LedEffect {
fn from(v: EffectCli) -> Self {
match v {
EffectCli::Off => Self::Off,
EffectCli::Breath => Self::Breath,
EffectCli::Rainbow => Self::Rainbow,
EffectCli::Mono => Self::Mono,
EffectCli::Doa => Self::Doa,
EffectCli::Ring => Self::Ring,
}
}
}
#[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,
Get { name: String },
Set {
name: String,
values: Vec<String>,
},
ReadAll,
}
fn main() -> ExitCode {
let opts = Opts::parse();
match run(opts) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
fn open_device(model: Option<ModelCli>) -> respeaker::Result<ReSpeaker> {
match model {
Some(m) => ReSpeaker::open_model(m.into()),
None => ReSpeaker::open(),
}
}
fn run(opts: Opts) -> respeaker::Result<()> {
let delay = Duration::from_millis(opts.delay_ms);
if matches!(opts.cmd, Cmd::Param { cmd: ParamCmd::List }) {
if let Some(m) = opts.model {
print_param_list(m.into());
return Ok(());
}
}
let dev = open_device(opts.model)?;
match opts.cmd {
Cmd::Model => {
let pid = dev.model().product_id();
println!("{} (2886:{pid:04x})", dev.model());
if let Ok(lite) = dev.lite() {
println!("product: {}", lite.product_name()?);
println!("firmware: {}", lite.firmware_version()?);
println!("serial: {}", lite.serial_number()?);
}
if !dev.model().has_usb_control() {
println!("USB control protocol: none (audio + DFU only)");
}
}
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!(
"voice: {}",
if dev.tuning().is_voice()? { 1 } else { 0 }
),
Cmd::Version => println!("version: {}", dev.tuning().version_string()?),
Cmd::SaveConfig => {
dev.tuning().save_configuration()?;
println!("configuration saved to flash");
}
Cmd::ClearConfig => {
dev.tuning().clear_configuration()?;
println!("configuration cleared (reboot the device)");
}
}
Ok(())
}
fn test_all(dev: &ReSpeaker, delay: Duration) -> respeaker::Result<()> {
let led = dev.led();
let tuning = dev.tuning();
println!("=== Device ===");
println!("model: {}", dev.model());
println!("version: {}", tuning.version_string()?);
if let Ok(lite) = dev.lite() {
println!("product: {}", lite.product_name()?);
println!("serial: {}", lite.serial_number()?);
println!();
println!("reSpeaker Lite has no USB control protocol — LEDs, DoA/VAD and");
println!("DSP parameters are reachable only over I2C (address 0x{:02x}),", respeaker::lite::I2C_ADDRESS);
println!("from an I2C master such as a Raspberry Pi or XIAO ESP32S3.");
return Ok(());
}
println!("DOA: {}°", tuning.doa_angle()?);
println!("voice: {}", tuning.is_voice()?);
println!();
println!("=== LED modes ===");
match dev.model() {
Model::MicArrayV2 => {
step("brightness 31", delay, || led.set_brightness(31))?;
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("customize rainbow", delay, || led.customize(&rainbow()))?;
for level in [0u8, 4, 8, 12] {
step(&format!("volume {level}"), delay, || led.set_volume(level))?;
}
step("off", delay, || led.off())?;
}
Model::Xvf3800 => {
step("brightness 255", delay, || led.set_brightness(255))?;
step("speed 1", delay, || led.set_speed(1))?;
step("doa colors", delay, || {
led.set_color_palette(Rgb::from_u32(0x202020), Rgb::GREEN)
})?;
step("effect doa", delay, || {
led.set_effect(respeaker::LedEffect::Doa)
})?;
step("effect breath", delay, || {
led.set_effect(respeaker::LedEffect::Breath)
})?;
step("effect rainbow", delay, || {
led.set_effect(respeaker::LedEffect::Rainbow)
})?;
step("mono orange", delay, || led.mono(Rgb::from_u32(0xff8800)))?;
step("ring rainbow", delay, || led.customize(&rainbow()))?;
step("listen/doa", delay, || led.listen())?;
step("off", delay, || led.off())?;
}
Model::Lite => unreachable!("handled above"),
}
println!();
println!("=== Parameters (read-all) ===");
read_all_params(dev)?;
println!();
println!("=== Convenience helpers ===");
println!("doa_angle() = {}°", tuning.doa_angle()?);
println!("is_voice() = {}", tuning.is_voice()?);
match dev.model() {
Model::MicArrayV2 => {
println!("AGCONOFF = {}", tuning.read("AGCONOFF")?);
println!("GAMMAVAD_SR = {}", tuning.read("GAMMAVAD_SR")?);
}
Model::Xvf3800 => {
println!("DOA_VALUE = {}", tuning.read_xvf("DOA_VALUE")?);
println!(
"PP_AGCONOFF = {}",
tuning.read_xvf("PP_AGCONOFF")?
);
println!(
"AUDIO_MGR_MIC_GAIN = {}",
tuning.read_xvf("AUDIO_MGR_MIC_GAIN")?
);
}
Model::Lite => unreachable!("handled above"),
}
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] {
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/doa");
}
LedCmd::Listen => {
led.listen()?;
println!("LED mode: listen/doa");
}
LedCmd::Speak => {
led.speak()?;
println!("LED mode: speak/breath");
}
LedCmd::Think => {
led.think()?;
println!("LED mode: think/breath");
}
LedCmd::Spin => {
led.spin()?;
println!("LED mode: spin/rainbow");
}
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/ring: 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
);
}
LedCmd::Effect { mode } => {
led.set_effect(mode.into())?;
println!("LED effect: {mode:?}");
}
LedCmd::Speed { level } => {
led.set_speed(level)?;
println!("LED speed {level}");
}
}
Ok(())
}
fn run_param(dev: &ReSpeaker, cmd: ParamCmd) -> respeaker::Result<()> {
let tuning = dev.tuning();
match cmd {
ParamCmd::List => {
print_param_list(dev.model());
}
ParamCmd::Get { name } => match dev.model() {
Model::MicArrayV2 => println!("{name}: {}", tuning.read(&name)?),
Model::Xvf3800 => println!("{name}: {}", tuning.read_xvf(&name)?),
Model::Lite => return Err(lite_no_params()),
},
ParamCmd::Set { name, values } => {
if values.is_empty() {
return Err(respeaker::Error::InvalidValue(
"param set requires at least one value".into(),
));
}
let refs: Vec<&str> = values.iter().map(String::as_str).collect();
tuning.write_strs(&name, &refs)?;
match dev.model() {
Model::MicArrayV2 => println!("{name}: {}", tuning.read(&name)?),
Model::Xvf3800 => match tuning.read_xvf(&name) {
Ok(v) => println!("{name}: {v}"),
Err(respeaker::Error::WriteOnly(_)) => {
println!("{name}: written ({})", refs.join(" "));
}
Err(e) => return Err(e),
},
Model::Lite => return Err(lite_no_params()),
}
}
ParamCmd::ReadAll => read_all_params(dev)?,
}
Ok(())
}
fn read_all_params(dev: &ReSpeaker) -> respeaker::Result<()> {
let tuning = dev.tuning();
println!("{:<28} {}", "name", "value");
println!("{}", "-".repeat(50));
match dev.model() {
Model::MicArrayV2 => {
for p in V2_PARAMETERS {
match tuning.read(p.name) {
Ok(ParamValue::Int(v)) => println!("{:<28} {v}", p.name),
Ok(ParamValue::Float(v)) => println!("{:<28} {v}", p.name),
Err(e) => println!("{:<28} <error: {e}>", p.name),
}
}
}
Model::Xvf3800 => {
for c in XVF_COMMANDS {
if matches!(c.access, XvfAccess::WriteOnly) {
continue;
}
match tuning.read_xvf(c.name) {
Ok(v) => println!("{:<28} {v}", c.name),
Err(e) => println!("{:<28} <error: {e}>", c.name),
}
}
}
Model::Lite => return Err(lite_no_params()),
}
Ok(())
}
fn lite_no_params() -> respeaker::Error {
respeaker::Error::Unsupported(
"reSpeaker Lite exposes no parameters over USB; DSP control is I2C-only",
)
}
fn print_param_list(model: Model) {
match model {
Model::MicArrayV2 => {
println!("# Mic Array v2.0 parameters");
println!(
"{:<22} {:<6} {:>8} {:>8} {:<4} {}",
"name", "type", "max", "min", "r/w", "info"
);
println!("{}", "-".repeat(80));
for p in V2_PARAMETERS {
let kind = match p.kind {
ParamKind::Int => "int",
ParamKind::Float => "float",
};
let access = match p.access {
V2Access::ReadWrite => "rw",
V2Access::ReadOnly => "ro",
};
println!(
"{:<22} {:<6} {:>8} {:>8} {:<4} {}",
p.name, kind, p.max, p.min, access, p.info
);
}
}
Model::Xvf3800 => {
println!("# XVF3800 commands");
println!(
"{:<28} {:<6} {:<6} {:<6} {:<8} {:<8} {}",
"name", "resid", "cmdid", "len", "access", "type", "info"
);
println!("{}", "-".repeat(100));
for c in XVF_COMMANDS {
let access = match c.access {
XvfAccess::ReadWrite => "rw",
XvfAccess::ReadOnly => "ro",
XvfAccess::WriteOnly => "wo",
};
let ty = format!("{:?}", c.value_type);
println!(
"{:<28} {:<6} {:<6} {:<6} {:<8} {:<8} {}",
c.name, c.resid, c.cmdid, c.length, access, ty, c.info
);
}
}
Model::Lite => {
println!("# reSpeaker Lite");
println!("No parameters are reachable over USB: the USB firmware is a UAC2");
println!("sound card plus a DFU interface and answers no vendor control");
println!("transfers. DSP parameters live behind the XMOS command transport");
println!(
"over I2C (address 0x{:02x}, resource id {} for configuration).",
respeaker::lite::I2C_ADDRESS,
respeaker::lite::I2C_RESID_CONFIG
);
}
}
}