respeaker 0.3.0

USB control library for Seeed ReSpeaker Mic Array v2.0 and XVF3800 (LEDs + DSP tuning), with reSpeaker Lite identification
Documentation
//! Low-level XVF3800 vendor control transfers (Seeed `xvf_host.py`).

use crate::error::{Error, Result};
use crate::xvf3800::params::{find_command, Access, Command, XvfValue};
use crate::{TUNING_TIMEOUT, VENDOR_IN, VENDOR_OUT};
use rusb::{DeviceHandle, UsbContext};
use std::thread;
use std::time::Duration;

const CONTROL_SUCCESS: u8 = 0;
/// Seeed/`lib_device_control`: retry the GET until the servicer finishes.
const SERVICER_COMMAND_RETRY: u8 = 64;
const READ_RETRIES: usize = 100;
const RETRY_SLEEP: Duration = Duration::from_millis(2);

fn control_status_name(code: u8) -> &'static str {
    // Numeric values from XMOS `control_ret_t` (SERVICER_COMMAND_RETRY fixed at 0x40).
    match code {
        0 => "CONTROL_SUCCESS",
        1 => "CONTROL_REGISTRATION_FAILED",
        2 => "CONTROL_BAD_COMMAND",
        3 => "CONTROL_DATA_LENGTH_ERROR",
        4 => "CONTROL_OTHER_TRANSPORT_ERROR",
        5 => "CONTROL_BAD_RESOURCE",
        6 => "CONTROL_MALFORMED_PACKET",
        7 => "CONTROL_COMMAND_IGNORED_IN_DEVICE",
        8 => "CONTROL_ERROR",
        64 => "SERVICER_COMMAND_RETRY",
        65 => "SERVICER_WRONG_COMMAND_ID",
        66 => "SERVICER_WRONG_COMMAND_LEN",
        67 => "SERVICER_WRONG_PAYLOAD",
        68 => "SERVICER_QUEUE_FULL",
        69 => "SERVICER_SPECIAL_COMMAND_ALREADY_ONGOING",
        70 => "SERVICER_SPECIAL_COMMAND_BUFFER_OVERFLOW",
        71 => "SERVICER_RESOURCE_ERROR",
        72 => "SERVICER_SPECIAL_COMMAND_WRONG_ORDER",
        73 => "SERVICER_SPECIAL_COMMAND_BUF_SIZE_ERROR",
        _ => "unknown",
    }
}

/// Borrowed XVF3800 control interface.
pub struct Xvf3800<'a, T: UsbContext> {
    handle: &'a DeviceHandle<T>,
}

impl<'a, T: UsbContext> Xvf3800<'a, T> {
    pub(crate) fn new(handle: &'a DeviceHandle<T>) -> Self {
        Self { handle }
    }

    pub fn commands(&self) -> &'static [Command] {
        crate::xvf3800::params::COMMANDS
    }

    pub fn read_cmd(&self, name: &str) -> Result<XvfValue> {
        let cmd = find_command(name)
            .ok_or_else(|| Error::UnknownParameter(name.to_string()))?;
        if cmd.access == Access::WriteOnly {
            return Err(Error::WriteOnly(cmd.name.to_string()));
        }
        let payload = self.read_raw(cmd)?;
        cmd.decode(&payload)
    }

    pub fn write_cmd(&self, name: &str, values: &[f64]) -> Result<()> {
        let cmd = find_command(name)
            .ok_or_else(|| Error::UnknownParameter(name.to_string()))?;
        if cmd.access == Access::ReadOnly {
            return Err(Error::ReadOnly(cmd.name.to_string()));
        }
        let payload = cmd.encode(values)?;
        self.write_raw(cmd, &payload)
    }

    pub fn write_str(&self, name: &str, texts: &[&str]) -> Result<()> {
        let mut values = Vec::with_capacity(texts.len());
        for t in texts {
            values.push(parse_number(t)?);
        }
        self.write_cmd(name, &values)
    }

    pub fn version_string(&self) -> Result<String> {
        match self.read_cmd("VERSION")? {
            XvfValue::U8(v) => Ok(v
                .iter()
                .map(|b| b.to_string())
                .collect::<Vec<_>>()
                .join(".")),
            other => Ok(other.to_string()),
        }
    }

    pub fn doa_angle(&self) -> Result<i32> {
        match self.read_cmd("DOA_VALUE")? {
            XvfValue::U16(v) if !v.is_empty() => Ok(i32::from(v[0])),
            other => Err(Error::Message(format!("unexpected DOA_VALUE: {other}"))),
        }
    }

    pub fn is_voice(&self) -> Result<bool> {
        match self.read_cmd("DOA_VALUE")? {
            XvfValue::U16(v) if v.len() >= 2 => Ok(v[1] != 0),
            other => Err(Error::Message(format!("unexpected DOA_VALUE: {other}"))),
        }
    }

    pub fn save_configuration(&self) -> Result<()> {
        self.write_cmd("SAVE_CONFIGURATION", &[1.0])
    }

    pub fn clear_configuration(&self) -> Result<()> {
        self.write_cmd("CLEAR_CONFIGURATION", &[1.0])
    }

    fn read_raw(&self, cmd: &Command) -> Result<Vec<u8>> {
        let wvalue = 0x80 | cmd.cmdid;
        let length = cmd.payload_bytes() + 1; // leading status byte
        let mut last_status = 0xffu8;
        for _ in 0..READ_RETRIES {
            let mut buf = vec![0u8; length];
            let n = self.handle.read_control(
                VENDOR_IN,
                0,
                wvalue,
                cmd.resid,
                &mut buf,
                TUNING_TIMEOUT,
            )?;
            if n == 0 {
                return Err(Error::Message("empty USB control response".into()));
            }
            last_status = buf[0];
            if last_status == CONTROL_SUCCESS {
                return Ok(buf[1..n].to_vec());
            }
            if last_status != SERVICER_COMMAND_RETRY {
                return Err(Error::ControlStatus {
                    code: last_status,
                    name: control_status_name(last_status),
                });
            }
            thread::sleep(RETRY_SLEEP);
        }
        Err(Error::ControlStatus {
            code: last_status,
            name: "SERVICER_COMMAND_RETRY exhausted",
        })
    }

    fn write_raw(&self, cmd: &Command, payload: &[u8]) -> Result<()> {
        self.handle.write_control(
            VENDOR_OUT,
            0,
            cmd.cmdid,
            cmd.resid,
            payload,
            TUNING_TIMEOUT,
        )?;
        Ok(())
    }
}

fn parse_number(text: &str) -> Result<f64> {
    let t = text.trim();
    if let Some(hex) = t
        .strip_prefix("0x")
        .or_else(|| t.strip_prefix("0X"))
        .or_else(|| t.strip_prefix('$'))
    {
        let v = u64::from_str_radix(hex, 16)
            .map_err(|e| Error::InvalidValue(format!("invalid hex `{t}`: {e}")))?;
        return Ok(v as f64);
    }
    t.parse::<f64>()
        .map_err(|e| Error::InvalidValue(format!("expected number, got `{t}`: {e}")))
}