respeaker 0.3.0

USB control library for Seeed ReSpeaker Mic Array v2.0 and XVF3800 (LEDs + DSP tuning), with reSpeaker Lite identification
Documentation
//! XVF3800 control commands from Seeed `python_control/xvf_host.py`.

use crate::error::{Error, Result};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Access {
    ReadWrite,
    ReadOnly,
    WriteOnly,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ValueType {
    U8,
    U16,
    U32,
    I32,
    Float,
    Radians,
    Char,
}

#[derive(Clone, Copy, Debug)]
pub struct Command {
    pub name: &'static str,
    pub resid: u16,
    pub cmdid: u16,
    /// Number of typed elements in the payload (not bytes).
    pub length: usize,
    pub access: Access,
    pub value_type: ValueType,
    pub info: &'static str,
}

/// Runtime value for an XVF3800 command.
#[derive(Clone, Debug, PartialEq)]
pub enum XvfValue {
    U8(Vec<u8>),
    U16(Vec<u16>),
    U32(Vec<u32>),
    I32(Vec<i32>),
    Float(Vec<f32>),
    Text(String),
}

impl std::fmt::Display for XvfValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::U8(v) => write!(f, "{v:?}"),
            Self::U16(v) => write!(f, "{v:?}"),
            Self::U32(v) => {
                let hex: Vec<_> = v.iter().map(|x| format!("0x{x:06X}")).collect();
                write!(f, "[{}]", hex.join(", "))
            }
            Self::I32(v) => write!(f, "{v:?}"),
            Self::Float(v) => {
                let s: Vec<_> = v.iter().map(|x| format!("{x:.5}")).collect();
                write!(f, "[{}]", s.join(", "))
            }
            Self::Text(v) => write!(f, "{v}"),
        }
    }
}

macro_rules! cmds {
    ($(($name:literal, $resid:expr, $cmdid:expr, $len:expr, $acc:ident, $ty:ident, $info:literal)),* $(,)?) => {
        pub static COMMANDS: &[Command] = &[
            $(
                Command {
                    name: $name,
                    resid: $resid,
                    cmdid: $cmdid,
                    length: $len,
                    access: Access::$acc,
                    value_type: ValueType::$ty,
                    info: $info,
                },
            )*
        ];
    };
}

cmds! {
    // APPLICATION_SERVICER_RESID
    ("VERSION", 48, 0, 3, ReadOnly, U8, "Firmware version MAJOR MINOR PATCH"),
    ("SAVE_CONFIGURATION", 48, 9, 1, WriteOnly, U8, "Save current configuration to flash"),
    ("CLEAR_CONFIGURATION", 48, 10, 1, WriteOnly, U8, "Clear configuration / revert to defaults"),
    ("REBOOT", 48, 7, 1, WriteOnly, U8, "Reboot the chip"),

    // AEC
    ("AEC_AZIMUTH_VALUES", 33, 75, 4, ReadOnly, Radians, "Azimuth radians: beam1 beam2 free-running auto-select"),
    ("AEC_SPENERGY_VALUES", 33, 80, 4, ReadOnly, Float, "Speech energy per beam"),
    ("AEC_ASROUTGAIN", 33, 36, 1, ReadWrite, Float, "Fixed gain applied to ASR output"),
    ("AEC_HPFONOFF", 33, 1, 1, ReadWrite, I32, "Mic HPF: 0=off 1=70 2=125 3=150 4=180"),
    ("AEC_AECPATHCHANGE", 33, 0, 1, ReadOnly, I32, "AEC path change detection"),
    ("AEC_RT60", 33, 9, 1, ReadOnly, Float, "Current RT60 estimate (seconds)"),

    // AUDIO_MGR
    ("AUDIO_MGR_MIC_GAIN", 35, 0, 1, ReadWrite, Float, "Pre-SHF microphone gain"),
    ("AUDIO_MGR_REF_GAIN", 35, 1, 1, ReadWrite, Float, "Pre-SHF reference gain"),
    ("AUDIO_MGR_SYS_DELAY", 35, 26, 1, ReadWrite, I32, "Reference delay in samples"),
    ("AUDIO_MGR_OP_L", 35, 15, 2, ReadWrite, U8, "Left output category/source"),
    ("AUDIO_MGR_OP_R", 35, 19, 2, ReadWrite, U8, "Right output category/source"),

    // GPO / LED / DOA (resource 20)
    ("GPO_READ_VALUES", 20, 0, 5, ReadOnly, U8, "GPO levels: X0D11 X0D30 X0D31 X0D33 X0D39"),
    ("GPO_WRITE_VALUE", 20, 1, 2, WriteOnly, U8, "Set GPO pin: pin_id level"),
    ("LED_EFFECT", 20, 12, 1, ReadWrite, U8, "0=off 1=breath 2=rainbow 3=mono 4=doa 5=ring"),
    ("LED_BRIGHTNESS", 20, 13, 1, ReadWrite, U8, "Brightness for breath/rainbow (0..=255)"),
    ("LED_GAMMIFY", 20, 14, 1, ReadWrite, U8, "Gamma correction 0/1"),
    ("LED_SPEED", 20, 15, 1, ReadWrite, U8, "Breath/rainbow speed"),
    ("LED_COLOR", 20, 16, 1, ReadWrite, U32, "Breath/mono color 0xRRGGBB"),
    ("LED_DOA_COLOR", 20, 17, 2, ReadWrite, U32, "DoA base + highlight colors"),
    ("DOA_VALUE", 20, 18, 2, ReadOnly, U16, "DOA degrees + speech detected flag"),
    // Firmware 2.0.x accepts writes but returns SERVICER_WRONG_COMMAND_ID (65) on read.
    ("LED_RING_COLOR", 20, 19, 12, WriteOnly, U32, "Per-LED colors for ring mode (write-only on current firmware)"),

    // Post-process
    ("PP_AGCONOFF", 17, 10, 1, ReadWrite, I32, "Automatic Gain Control on/off"),
    ("PP_AGCMAXGAIN", 17, 11, 1, ReadWrite, Float, "Maximum AGC gain factor"),
    ("PP_AGCGAIN", 17, 13, 1, ReadWrite, Float, "Current AGC gain factor"),
    ("PP_AGCDESIREDLEVEL", 17, 12, 1, ReadWrite, Float, "AGC target power level"),
    ("PP_MIN_NS", 17, 21, 1, ReadWrite, Float, "Stationary noise gain floor"),
    ("PP_MIN_NN", 17, 22, 1, ReadWrite, Float, "Non-stationary noise gain floor"),
    ("PP_ECHOONOFF", 17, 23, 1, ReadWrite, I32, "Echo suppression on/off"),
    ("PP_FMIN_SPEINDEX", 17, 30, 1, ReadWrite, Float, "Double-talk SPEINDEX frequency"),
    ("PP_DTSENSITIVE", 17, 31, 1, ReadWrite, I32, "Echo vs double-talk tradeoff"),
}

pub fn find_command(name: &str) -> Option<&'static Command> {
    COMMANDS
        .iter()
        .find(|c| c.name.eq_ignore_ascii_case(name))
}

impl Command {
    pub(crate) fn payload_bytes(&self) -> usize {
        match self.value_type {
            ValueType::U8 | ValueType::Char => self.length,
            ValueType::U16 => self.length * 2,
            ValueType::U32 | ValueType::I32 | ValueType::Float | ValueType::Radians => {
                self.length * 4
            }
        }
    }

    pub(crate) fn encode(&self, values: &[f64]) -> Result<Vec<u8>> {
        if values.len() != self.length {
            return Err(Error::InvalidValue(format!(
                "{} expects {} value(s), got {}",
                self.name,
                self.length,
                values.len()
            )));
        }
        let mut out = Vec::with_capacity(self.payload_bytes());
        match self.value_type {
            ValueType::U8 => {
                for v in values {
                    out.push(*v as u8);
                }
            }
            ValueType::U16 => {
                for v in values {
                    out.extend_from_slice(&(*v as u16).to_le_bytes());
                }
            }
            ValueType::U32 => {
                for v in values {
                    out.extend_from_slice(&(*v as u32).to_le_bytes());
                }
            }
            ValueType::I32 => {
                for v in values {
                    out.extend_from_slice(&(*v as i32).to_le_bytes());
                }
            }
            ValueType::Float | ValueType::Radians => {
                for v in values {
                    out.extend_from_slice(&(*v as f32).to_le_bytes());
                }
            }
            ValueType::Char => {
                return Err(Error::InvalidValue(
                    "char parameters are not writable via numeric values".into(),
                ));
            }
        }
        Ok(out)
    }

    pub(crate) fn decode(&self, payload: &[u8]) -> Result<XvfValue> {
        let need = self.payload_bytes();
        if payload.len() < need {
            return Err(Error::Message(format!(
                "{}: short payload ({} < {})",
                self.name,
                payload.len(),
                need
            )));
        }
        let p = &payload[..need];
        Ok(match self.value_type {
            ValueType::U8 => XvfValue::U8(p.to_vec()),
            ValueType::Char => {
                let s = String::from_utf8_lossy(p)
                    .trim_end_matches('\0')
                    .to_string();
                XvfValue::Text(s)
            }
            ValueType::U16 => {
                let mut v = Vec::with_capacity(self.length);
                for chunk in p.chunks_exact(2) {
                    v.push(u16::from_le_bytes([chunk[0], chunk[1]]));
                }
                XvfValue::U16(v)
            }
            ValueType::U32 => {
                let mut v = Vec::with_capacity(self.length);
                for chunk in p.chunks_exact(4) {
                    v.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
                }
                XvfValue::U32(v)
            }
            ValueType::I32 => {
                let mut v = Vec::with_capacity(self.length);
                for chunk in p.chunks_exact(4) {
                    v.push(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
                }
                XvfValue::I32(v)
            }
            ValueType::Float | ValueType::Radians => {
                let mut v = Vec::with_capacity(self.length);
                for chunk in p.chunks_exact(4) {
                    v.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
                }
                XvfValue::Float(v)
            }
        })
    }
}