autd3-rs 0.5.0

Core async client library for the AUTD3 phased-array kit.
Documentation
use autd3_cpu_wire::params::{EMISSION_TYPE_FOCI, EMISSION_TYPE_RAW};
use autd3_cpu_wire::payload::ConfigPatternPayload;
use zerocopy::FromBytes;
use zerocopy::little_endian::{U16, U32};

use crate::Velocity;
use crate::error::{Error, PayloadError};
use crate::geometry::Device;
use crate::mirror::FirmwareState;
use crate::params::{BUFFER_SIZE_MIN, 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, check_index_advance};

#[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 reflect_pattern(
    config: SamplingConfig,
    bank: PatternBank,
    loop_behavior: LoopBehavior,
    device: usize,
    state: &mut FirmwareState,
) -> Result<(), Error> {
    let divider = config.divide()?;
    state.silencer.check_pattern_div(device, divider)?;
    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 distribution(&self) -> Distribution {
        Distribution::Broadcast
    }

    fn encode(&self, _device: &Device, out: &mut [u8; PAYLOAD_BYTES]) -> Result<Cmd, Error> {
        let divider = self.config.divide()?;
        if self.size == 0 || self.size > EMISSION_MAX_INDICES {
            return Err(PayloadError::StmSizeOutOfRange {
                size: self.size,
                min: 1,
                max: EMISSION_MAX_INDICES,
            }
            .into());
        }
        check_index_advance(self.size, self.loop_behavior)?;
        let size = u32::try_from(self.size).expect("bounded by capacity checks");
        let (p, _) = ConfigPatternPayload::mut_from_prefix(&mut out[..]).unwrap();
        *p = ConfigPatternPayload {
            bank: self.bank.as_u8(),
            emission_type: EMISSION_TYPE_RAW,
            divider: U16::new(divider),
            size: U32::new(size),
            num_foci: 0,
            reserved: 0,
            sound_speed: U16::new(0),
            rep: U16::new(self.loop_behavior.rep()),
        };
        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 distribution(&self) -> Distribution {
        Distribution::Broadcast
    }

    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    fn encode(&self, _device: &Device, out: &mut [u8; PAYLOAD_BYTES]) -> Result<Cmd, Error> {
        let divider = self.config.divide()?;
        if self.size < BUFFER_SIZE_MIN {
            return Err(PayloadError::PatternSizeTooSmall {
                size: self.size,
                min: BUFFER_SIZE_MIN,
            }
            .into());
        }
        if self.num_foci == 0 || self.num_foci > NUM_FOCI_MAX {
            return Err(PayloadError::NumFociOutOfRange {
                num_foci: self.num_foci,
                max: NUM_FOCI_MAX,
            }
            .into());
        }
        if self.size > MAX_FOCI_TOTAL / usize::from(self.num_foci) {
            return Err(PayloadError::StmFociExceedCapacity {
                size: self.size,
                num_foci: self.num_foci,
                capacity: MAX_FOCI_TOTAL,
            }
            .into());
        }
        let sound_speed = (self.sound_speed.m_s() * 64.0).round() as u16;
        if sound_speed == 0 {
            return Err(PayloadError::SoundSpeedZero.into());
        }
        let size = u32::try_from(self.size).expect("bounded by capacity checks");
        let (p, _) = ConfigPatternPayload::mut_from_prefix(&mut out[..]).unwrap();
        *p = ConfigPatternPayload {
            bank: self.bank.as_u8(),
            emission_type: EMISSION_TYPE_FOCI,
            divider: U16::new(divider),
            size: U32::new(size),
            num_foci: self.num_foci,
            reserved: 0,
            sound_speed: U16::new(sound_speed),
            rep: U16::new(self.loop_behavior.rep()),
        };
        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 crate::test_utils::test_device;
    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(&test_device(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_pattern_allows_a_single_index_only_for_an_infinite_loop() {
        let raw = |size: usize, loop_behavior: LoopBehavior| ConfigPattern {
            bank: PatternBank::B0,
            config: SamplingConfig::new(NonZeroU16::MIN),
            size,
            loop_behavior,
        };
        let finite = LoopBehavior::Finite(NonZeroU16::new(4).unwrap());

        assert!(
            encode(&raw(1, LoopBehavior::Infinite)).is_ok(),
            "a static pattern is a single index"
        );
        assert!(
            matches!(encode(&raw(1, finite)), Err(Error::InvalidPayload(_))),
            "a single index never advances, so a finite loop would never end"
        );
        assert!(encode(&raw(2, finite)).is_ok());
    }

    #[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, 1, v)), Err(Error::InvalidPayload(_))),
            "a single-sample STM never advances its index"
        );
        assert!(matches!(
            encode(&foci(2, 0, v)),
            Err(Error::InvalidPayload(_))
        ));
        assert!(matches!(
            encode(&foci(2, 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(2, 1, Velocity::from_m_s(0.0))),
            Err(Error::InvalidPayload(_))
        ));
        assert!(encode(&foci(MAX_FOCI_TOTAL / 8, 8, v)).is_ok());
    }
}