respeaker 0.1.0

USB control library for Seeed ReSpeaker Mic Array v2.0 (LEDs + XVF-3000 DSP tuning)
Documentation
//! LED ring control — protocol from `pixel_ring/usb_pixel_ring_v2.py`.

use crate::error::{Error, Result};
use crate::{LED_INDEX, LED_TIMEOUT, VENDOR_OUT};
use rusb::{DeviceHandle, UsbContext};

/// RGB color (0–255 per channel).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Rgb {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

impl Rgb {
    pub const BLACK: Self = Self { r: 0, g: 0, b: 0 };
    pub const RED: Self = Self { r: 255, g: 0, b: 0 };
    pub const GREEN: Self = Self { r: 0, g: 255, b: 0 };
    pub const BLUE: Self = Self { r: 0, g: 0, b: 255 };
    pub const WHITE: Self = Self {
        r: 255,
        g: 255,
        b: 255,
    };

    pub const fn new(r: u8, g: u8, b: u8) -> Self {
        Self { r, g, b }
    }

    /// Parse `RRGGBB` or `0xRRGGBB` hex.
    pub fn from_hex(s: &str) -> Result<Self> {
        let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
        if s.len() != 6 {
            return Err(Error::InvalidValue(format!(
                "expected RRGGBB hex color, got `{s}`"
            )));
        }
        let v = u32::from_str_radix(s, 16)
            .map_err(|e| Error::InvalidValue(format!("invalid hex color `{s}`: {e}")))?;
        Ok(Self {
            r: ((v >> 16) & 0xff) as u8,
            g: ((v >> 8) & 0xff) as u8,
            b: (v & 0xff) as u8,
        })
    }

    fn mono_payload(self) -> [u8; 4] {
        [self.r, self.g, self.b, 0]
    }
}

/// Center VAD LED policy (`set_vad_led`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum VadLed {
    Off = 0,
    On = 1,
    /// Follow voice-activity detection.
    Auto = 2,
}

/// Borrowed LED-ring controller for an open ReSpeaker handle.
pub struct PixelRing<'a, T: UsbContext> {
    handle: &'a DeviceHandle<T>,
}

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

    /// Trace mode — LEDs follow VAD / DOA.
    pub fn trace(&self) -> Result<()> {
        self.write(0, &[0])
    }

    /// Solid color on all LEDs.
    pub fn mono(&self, color: Rgb) -> Result<()> {
        self.write(1, &color.mono_payload())
    }

    /// Turn all LEDs off.
    pub fn off(&self) -> Result<()> {
        self.mono(Rgb::BLACK)
    }

    /// Listen / wakeup mode (LEDs active).
    pub fn listen(&self) -> Result<()> {
        self.write(2, &[0])
    }

    /// Alias for [`Self::listen`].
    pub fn wakeup(&self) -> Result<()> {
        self.listen()
    }

    /// Wait / speak mode (command 3 in pixel_ring).
    pub fn speak(&self) -> Result<()> {
        self.write(3, &[0])
    }

    /// Think / wait mode (command 4 in pixel_ring).
    pub fn think(&self) -> Result<()> {
        self.write(4, &[0])
    }

    /// Alias for [`Self::think`].
    pub fn wait(&self) -> Result<()> {
        self.think()
    }

    /// Spin animation.
    pub fn spin(&self) -> Result<()> {
        self.write(5, &[0])
    }

    /// Set each of the 12 LEDs individually.
    pub fn customize(&self, colors: &[Rgb; 12]) -> Result<()> {
        let mut data = [0u8; 48];
        for (i, c) in colors.iter().enumerate() {
            let base = i * 4;
            data[base] = c.r;
            data[base + 1] = c.g;
            data[base + 2] = c.b;
        }
        self.write(6, &data)
    }

    /// Brightness `0..=0x1F`.
    pub fn set_brightness(&self, brightness: u8) -> Result<()> {
        if brightness > 0x1f {
            return Err(Error::InvalidValue(
                "brightness must be in 0..=31 (0x1F)".into(),
            ));
        }
        self.write(0x20, &[brightness])
    }

    /// Two-color palette used by think/speak patterns.
    pub fn set_color_palette(&self, a: Rgb, b: Rgb) -> Result<()> {
        let data = [
            a.r, a.g, a.b, 0, //
            b.r, b.g, b.b, 0,
        ];
        self.write(0x21, &data)
    }

    /// Center LED: off / on / VAD-auto.
    pub fn set_vad_led(&self, state: VadLed) -> Result<()> {
        self.write(0x22, &[state as u8])
    }

    /// Show a volume level `0..=12`.
    pub fn set_volume(&self, volume: u8) -> Result<()> {
        if volume > 12 {
            return Err(Error::InvalidValue("volume must be in 0..=12".into()));
        }
        self.write(0x23, &[volume])
    }

    fn write(&self, command: u16, data: &[u8]) -> Result<()> {
        self.handle
            .write_control(VENDOR_OUT, 0, command, LED_INDEX, data, LED_TIMEOUT)?;
        Ok(())
    }
}