autd3-rs 0.2.0

Core async client library for the AUTD3 phased-array kit.
Documentation
use crate::Velocity;
use crate::error::{Error, PayloadError};
use crate::mirror::FirmwareState;
use crate::params::{EMISSION_MAX_INDICES, MAX_FOCI_TOTAL, NUM_FOCI_MAX};
use crate::protocol::{Cmd, PAYLOAD_BYTES};
use crate::value::{LoopBehavior, PatternBank, SamplingConfig};

use super::{Distribution, Operation, silencer_constraint};

#[derive(Clone, Copy, Debug)]
pub struct ConfigPattern {
    pub bank: PatternBank,
    pub config: SamplingConfig,
    pub size: usize,
    pub loop_behavior: LoopBehavior,
}

#[derive(Clone, Copy, Debug)]
pub struct ConfigFociStm {
    pub bank: PatternBank,
    pub config: SamplingConfig,
    pub size: usize,
    pub num_foci: u8,
    pub sound_speed: Velocity,
    pub loop_behavior: LoopBehavior,
}

fn divider_of(config: SamplingConfig) -> Result<u16, Error> {
    config
        .divide()
        .map_err(|e| Error::InvalidPayload(PayloadError::from(e)))
}

fn reflect_pattern(
    config: SamplingConfig,
    bank: PatternBank,
    loop_behavior: LoopBehavior,
    device: usize,
    state: &mut FirmwareState,
) -> Result<(), Error> {
    let divider = divider_of(config)?;
    if let Err(v) = state.silencer.check_pattern_div(divider) {
        return Err(silencer_constraint(device, v));
    }
    state.silencer.note_pattern_div(bank.as_u8(), divider);
    state
        .transition
        .note_pattern_loop(bank.as_u8(), loop_behavior);
    Ok(())
}

impl Operation for ConfigPattern {
    fn frames(&self) -> usize {
        1
    }

    fn distribution(&self) -> Distribution {
        Distribution::Broadcast
    }

    fn encode(
        &self,
        _device: usize,
        _frame: usize,
        out: &mut [u8; PAYLOAD_BYTES],
    ) -> Result<Cmd, Error> {
        let divider = divider_of(self.config)?;
        if self.size == 0 {
            return Err(Error::InvalidPayload(PayloadError::PatternSizeZero));
        }
        if self.size > EMISSION_MAX_INDICES {
            return Err(Error::InvalidPayload(PayloadError::StmSizeOutOfRange {
                size: self.size,
                max: EMISSION_MAX_INDICES,
            }));
        }
        let size = u32::try_from(self.size).expect("bounded by capacity checks");
        out[0] = self.bank.as_u8();
        out[1] = 1;
        out[2..4].copy_from_slice(&divider.to_le_bytes());
        out[4..8].copy_from_slice(&size.to_le_bytes());
        out[8] = 0;
        out[10..12].copy_from_slice(&0u16.to_le_bytes());
        out[12..14].copy_from_slice(&self.loop_behavior.rep().to_le_bytes());
        Ok(Cmd::ConfigPattern)
    }

    fn reflect(&self, device: usize, state: &mut FirmwareState) -> Result<(), Error> {
        reflect_pattern(self.config, self.bank, self.loop_behavior, device, state)
    }
}

impl Operation for ConfigFociStm {
    fn frames(&self) -> usize {
        1
    }

    fn distribution(&self) -> Distribution {
        Distribution::Broadcast
    }

    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    fn encode(
        &self,
        _device: usize,
        _frame: usize,
        out: &mut [u8; PAYLOAD_BYTES],
    ) -> Result<Cmd, Error> {
        let divider = divider_of(self.config)?;
        if self.size == 0 {
            return Err(Error::InvalidPayload(PayloadError::PatternSizeZero));
        }
        if self.num_foci == 0 || self.num_foci > NUM_FOCI_MAX {
            return Err(Error::InvalidPayload(PayloadError::NumFociOutOfRange {
                num_foci: self.num_foci,
                max: NUM_FOCI_MAX,
            }));
        }
        if self.size > MAX_FOCI_TOTAL / usize::from(self.num_foci) {
            return Err(Error::InvalidPayload(PayloadError::StmFociExceedCapacity {
                size: self.size,
                num_foci: self.num_foci,
                capacity: MAX_FOCI_TOTAL,
            }));
        }
        let sound_speed = (self.sound_speed.m_s() * 64.0).round() as u16;
        if sound_speed == 0 {
            return Err(Error::InvalidPayload(PayloadError::SoundSpeedZero));
        }
        let size = u32::try_from(self.size).expect("bounded by capacity checks");
        out[0] = self.bank.as_u8();
        out[1] = 0;
        out[2..4].copy_from_slice(&divider.to_le_bytes());
        out[4..8].copy_from_slice(&size.to_le_bytes());
        out[8] = self.num_foci;
        out[10..12].copy_from_slice(&sound_speed.to_le_bytes());
        out[12..14].copy_from_slice(&self.loop_behavior.rep().to_le_bytes());
        Ok(Cmd::ConfigPattern)
    }

    fn reflect(&self, device: usize, state: &mut FirmwareState) -> Result<(), Error> {
        reflect_pattern(self.config, self.bank, self.loop_behavior, device, state)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use core::num::NonZeroU16;

    fn encode(op: impl Operation) -> Result<(Cmd, [u8; PAYLOAD_BYTES]), Error> {
        let mut out = [0u8; PAYLOAD_BYTES];
        let cmd = op.encode(0, 0, &mut out)?;
        Ok((cmd, out))
    }

    #[test]
    fn config_pattern_lays_out_raw_fields() {
        let (cmd, payload) = encode(ConfigPattern {
            bank: PatternBank::B0,
            config: SamplingConfig::new(NonZeroU16::new(2).unwrap()),
            size: 1024,
            loop_behavior: LoopBehavior::Finite(NonZeroU16::new(8).unwrap()),
        })
        .unwrap();

        assert_eq!(cmd, Cmd::ConfigPattern);
        assert_eq!(payload[0], 0);
        assert_eq!(payload[1], 1, "RawEmissions wire value");
        assert_eq!(&payload[2..4], &2u16.to_le_bytes());
        assert_eq!(&payload[4..8], &1024u32.to_le_bytes());
        assert_eq!(payload[8], 0);
        assert_eq!(&payload[10..12], &0u16.to_le_bytes());
        assert_eq!(&payload[12..14], &7u16.to_le_bytes());
    }

    #[test]
    fn config_foci_stm_lays_out_foci_fields() {
        let (cmd, payload) = encode(ConfigFociStm {
            bank: PatternBank::B1,
            config: SamplingConfig::new(NonZeroU16::MIN),
            size: 8192,
            num_foci: 8,
            sound_speed: Velocity::from_m_s(340.0),
            loop_behavior: LoopBehavior::Infinite,
        })
        .unwrap();

        assert_eq!(cmd, Cmd::ConfigPattern);
        assert_eq!(payload[0], 1);
        assert_eq!(payload[1], 0, "Foci wire value");
        assert_eq!(&payload[4..8], &8192u32.to_le_bytes());
        assert_eq!(payload[8], 8);
        assert_eq!(&payload[10..12], &21760u16.to_le_bytes(), "340 m/s * 64");
        assert_eq!(&payload[12..14], &0xFFFFu16.to_le_bytes(), "infinite rep");
    }

    #[test]
    fn config_pattern_rejects_invalid_raw_fields() {
        let raw = |size: usize| ConfigPattern {
            bank: PatternBank::B0,
            config: SamplingConfig::new(NonZeroU16::MIN),
            size,
            loop_behavior: LoopBehavior::Infinite,
        };
        assert!(matches!(encode(raw(0)), Err(Error::InvalidPayload(_))));
        assert!(
            matches!(
                encode(ConfigPattern {
                    config: SamplingConfig::new(core::time::Duration::from_nanos(1)),
                    ..raw(1)
                }),
                Err(Error::InvalidPayload(_))
            ),
            "an unrepresentable sampling config is rejected"
        );
        assert!(matches!(
            encode(raw(EMISSION_MAX_INDICES + 1)),
            Err(Error::InvalidPayload(_))
        ));
    }

    #[test]
    fn config_foci_stm_rejects_invalid_fields() {
        let foci = |size: usize, num_foci: u8, sound_speed: Velocity| ConfigFociStm {
            bank: PatternBank::B0,
            config: SamplingConfig::new(NonZeroU16::MIN),
            size,
            num_foci,
            sound_speed,
            loop_behavior: LoopBehavior::Infinite,
        };
        let v = Velocity::from_m_s(340.0);
        assert!(matches!(
            encode(foci(1, 0, v)),
            Err(Error::InvalidPayload(_))
        ));
        assert!(matches!(
            encode(foci(1, NUM_FOCI_MAX + 1, v)),
            Err(Error::InvalidPayload(_))
        ));
        assert!(matches!(
            encode(foci(MAX_FOCI_TOTAL / 8 + 1, 8, v)),
            Err(Error::InvalidPayload(_))
        ));
        assert!(matches!(
            encode(foci(1, 1, Velocity::from_m_s(0.0))),
            Err(Error::InvalidPayload(_))
        ));
        assert!(encode(foci(MAX_FOCI_TOTAL / 8, 8, v)).is_ok());
    }
}