respeaker 0.3.0

USB control library for Seeed ReSpeaker Mic Array v2.0 and XVF3800 (LEDs + DSP tuning), with reSpeaker Lite identification
Documentation
//! USB control for Seeed ReSpeaker microphone arrays.
//!
//! Supported models:
//! - [`Model::MicArrayV2`] — Mic Array v2.0 / XVF-3000 (`2886:0018`)
//! - [`Model::Xvf3800`] — reSpeaker XVF3800 (`2886:001a`)
//! - [`Model::Lite`] — reSpeaker Lite / XU316 (`2886:0019`), identification
//!   only: its USB firmware is a UAC2 sound card plus DFU and answers no
//!   vendor control transfers (see [`lite`])
//!
//! ```no_run
//! use respeaker::ReSpeaker;
//!
//! let dev = ReSpeaker::open()?;
//! println!("model: {}", dev.model());
//! dev.led().off()?;
//! println!("DOA: {}°", dev.tuning().doa_angle()?);
//! # Ok::<(), respeaker::Error>(())
//! ```

mod color;
mod error;
mod led;
mod model;
mod tuning;

pub mod lite;
pub mod v2;
pub mod xvf3800;

pub use color::Rgb;
pub use error::{Error, Result};
pub use led::Led;
pub use lite::{FirmwareVersion, Lite};
pub use model::Model;
pub use tuning::Tuning;
pub use v2::{
    find_parameter, Access as V2Access, ParamKind, ParamValue, Parameter, PixelRing, VadLed,
    PARAMETERS as V2_PARAMETERS,
};
pub use xvf3800::{
    find_command, Access as XvfAccess, Command as XvfCommand, LedEffect, ValueType, Xvf3800,
    Xvf3800Led, XvfValue, COMMANDS as XVF_COMMANDS,
};

// Back-compat aliases used by existing callers.
pub use v2::Access;
pub use v2::PARAMETERS;

use rusb::{Context, DeviceHandle, Direction, Recipient, RequestType, UsbContext};

/// Seeed Technology vendor id.
pub const VENDOR_ID: u16 = 0x2886;
/// ReSpeaker Mic Array v2.0 (UAC1.0) product id.
pub const PRODUCT_ID_V2: u16 = 0x0018;
/// reSpeaker XVF3800 USB 4-Mic Array product id.
pub const PRODUCT_ID_XVF3800: u16 = 0x001A;
/// reSpeaker Lite (XU316) product id.
pub const PRODUCT_ID_LITE: u16 = 0x0019;
/// Alias kept for Mic Array v2 callers.
pub const PRODUCT_ID: u16 = PRODUCT_ID_V2;

pub(crate) const LED_INDEX: u16 = 0x1C;
pub(crate) const LED_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(8000);
pub(crate) const TUNING_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(5000);

pub(crate) const VENDOR_OUT: u8 =
    rusb::request_type(Direction::Out, RequestType::Vendor, Recipient::Device);
pub(crate) const VENDOR_IN: u8 =
    rusb::request_type(Direction::In, RequestType::Vendor, Recipient::Device);

/// Open handle to a supported ReSpeaker device.
pub struct ReSpeaker<T: UsbContext = Context> {
    handle: DeviceHandle<T>,
    model: Model,
}

impl ReSpeaker<Context> {
    /// Find and open the first supported ReSpeaker (v2, XVF3800 or Lite).
    pub fn open() -> Result<Self> {
        let context = Context::new()?;
        Self::open_with_context(&context)
    }

    /// Open a specific model.
    pub fn open_model(model: Model) -> Result<Self> {
        let context = Context::new()?;
        Self::open_model_with_context(&context, model)
    }
}

impl<T: UsbContext> ReSpeaker<T> {
    /// Find and open the first supported device using an existing USB context.
    pub fn open_with_context(context: &T) -> Result<Self> {
        let devices = context.devices()?;
        let mut matched = None;
        for device in devices.iter() {
            let Ok(desc) = device.device_descriptor() else {
                continue;
            };
            if desc.vendor_id() != VENDOR_ID {
                continue;
            }
            let model = match desc.product_id() {
                PRODUCT_ID_V2 => Model::MicArrayV2,
                PRODUCT_ID_XVF3800 => Model::Xvf3800,
                PRODUCT_ID_LITE => Model::Lite,
                _ => continue,
            };
            matched = Some((device, model));
            break;
        }

        let (device, model) = matched.ok_or(Error::NotFound {
            vid: VENDOR_ID,
            pids: format!(
                "{PRODUCT_ID_V2:04x}/{PRODUCT_ID_XVF3800:04x}/{PRODUCT_ID_LITE:04x}"
            ),
        })?;

        let handle = match device.open() {
            Ok(h) => h,
            Err(rusb::Error::Access) => return Err(Error::Access),
            Err(e) => return Err(Error::Usb(e)),
        };

        Ok(Self { handle, model })
    }

    pub fn open_model_with_context(context: &T, model: Model) -> Result<Self> {
        let devices = context.devices()?;
        let pid = model.product_id();
        let mut matched = None;
        for device in devices.iter() {
            let Ok(desc) = device.device_descriptor() else {
                continue;
            };
            if desc.vendor_id() == VENDOR_ID && desc.product_id() == pid {
                matched = Some(device);
                break;
            }
        }
        let device = matched.ok_or(Error::NotFound {
            vid: VENDOR_ID,
            pids: format!("{pid:04x}"),
        })?;
        let handle = match device.open() {
            Ok(h) => h,
            Err(rusb::Error::Access) => return Err(Error::Access),
            Err(e) => return Err(Error::Usb(e)),
        };
        Ok(Self { handle, model })
    }

    pub fn from_handle(handle: DeviceHandle<T>, model: Model) -> Self {
        Self { handle, model }
    }

    pub fn model(&self) -> Model {
        self.model
    }

    pub fn led(&self) -> Led<'_, T> {
        Led::new(self.model, &self.handle)
    }

    pub fn tuning(&self) -> Tuning<'_, T> {
        Tuning::new(self.model, &self.handle)
    }

    /// Mic Array v2-only LED API.
    pub fn pixel_ring(&self) -> Result<PixelRing<'_, T>> {
        match self.model {
            Model::MicArrayV2 => Ok(PixelRing::new(&self.handle)),
            Model::Xvf3800 => Err(Error::Unsupported("pixel_ring is Mic Array v2 only")),
            Model::Lite => Err(Error::Unsupported(lite::NO_USB_CONTROL)),
        }
    }

    /// XVF3800-only control API.
    pub fn xvf3800(&self) -> Result<Xvf3800<'_, T>> {
        match self.model {
            Model::Xvf3800 => Ok(Xvf3800::new(&self.handle)),
            Model::MicArrayV2 => Err(Error::Unsupported("xvf3800() is XVF3800 only")),
            Model::Lite => Err(Error::Unsupported(lite::NO_USB_CONTROL)),
        }
    }

    /// reSpeaker Lite-only identification API.
    pub fn lite(&self) -> Result<Lite<'_, T>> {
        match self.model {
            Model::Lite => Ok(Lite::new(&self.handle)),
            _ => Err(Error::Unsupported("lite() is reSpeaker Lite only")),
        }
    }

    pub fn handle(&self) -> &DeviceHandle<T> {
        &self.handle
    }
}