autd3-rs 0.6.1

Core async client library for the AUTD3 phased-array kit.
Documentation
use super::{Command, WriteModulationBuffer};
use crate::commands::operation::{ChangeModulationBank, ConfigModulation, WriteModulationFused};
use crate::datagram::DatagramBuilder;
use crate::value::{LoopBehavior, ModulationBank, SamplingConfig, TransitionMode};

#[derive(Clone, Copy, Debug)]
pub struct Modulation<'a> {
    pub bank: ModulationBank,
    pub config: SamplingConfig,
    pub data: &'a [u8],
    pub loop_behavior: LoopBehavior,
    pub transition_mode: TransitionMode,
}

impl<'a> Modulation<'a> {
    #[must_use]
    pub fn new(config: SamplingConfig, data: &'a [u8]) -> Self {
        Self::with_bank(ModulationBank::B0, config, data)
    }

    #[must_use]
    pub fn with_bank(bank: ModulationBank, config: SamplingConfig, data: &'a [u8]) -> Self {
        Self {
            bank,
            config,
            data,
            loop_behavior: LoopBehavior::Infinite,
            transition_mode: TransitionMode::Immediate,
        }
    }
}

impl<'a> Command<'a> for Modulation<'a> {
    fn expand(self, builder: &mut DatagramBuilder<'a>) {
        let size = self.data.len();
        if self.transition_mode.is_later() {
            builder
                .push(WriteModulationBuffer {
                    bank: self.bank,
                    offset: 0,
                    data: self.data,
                })
                .push(ConfigModulation {
                    bank: self.bank,
                    config: self.config,
                    size,
                    loop_behavior: self.loop_behavior,
                });
            return;
        }
        if WriteModulationFused::fits_single_frame(size) {
            builder.push(WriteModulationFused {
                bank: self.bank,
                data: self.data,
                config: self.config,
                loop_behavior: self.loop_behavior,
                transition_mode: self.transition_mode,
            });
            return;
        }
        builder
            .push(WriteModulationBuffer {
                bank: self.bank,
                offset: 0,
                data: self.data,
            })
            .push(ConfigModulation {
                bank: self.bank,
                config: self.config,
                size,
                loop_behavior: self.loop_behavior,
            })
            .push(ChangeModulationBank {
                bank: self.bank,
                transition_mode: self.transition_mode,
            });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::Cmd;
    use crate::test_utils::test_geometry_arc;

    use crate::commands::operation::MOD_FUSED_MAX_DATA_LEN;

    fn fused_payload(m: Modulation<'_>) -> [u8; crate::protocol::PAYLOAD_BYTES] {
        let mut b = DatagramBuilder::new(test_geometry_arc(1));
        b.push(m);
        let datagrams = b.build().unwrap();
        assert_eq!(datagrams.len(), 1, "short modulation fuses into 1 frame");
        let f = datagrams.frame(0).unwrap();
        assert_eq!(f.datagrams()[0].cmd, Cmd::WriteModulationFused);
        f.datagrams()[0].payload
    }

    #[test]
    fn modulation_expands_with_size_from_data() {
        let data = vec![0x80u8; 20];
        let payload = fused_payload(Modulation::with_bank(
            ModulationBank::B1,
            SamplingConfig::FREQ_4K,
            &data,
        ));

        assert_eq!(payload[0], 1, "bank B1");
        assert_eq!(payload[1], 0xFF, "IMMEDIATE");
        assert_eq!(&payload[4..8], &20u32.to_le_bytes(), "size");
        assert_eq!(&payload[10..12], &20u16.to_le_bytes(), "data_len");
    }

    #[test]
    fn modulation_defaults_to_infinite_loop() {
        let data = vec![0x80u8; 4];
        let payload = fused_payload(Modulation::new(SamplingConfig::FREQ_4K, &data));
        assert_eq!(&payload[8..10], &0xFFFFu16.to_le_bytes());
    }

    #[test]
    fn modulation_defaults_to_immediate_transition() {
        let data = vec![0x80u8; 4];
        let payload = fused_payload(Modulation::new(SamplingConfig::FREQ_4K, &data));
        assert_eq!(payload[1], 0xFF, "IMMEDIATE");
        assert_eq!(&payload[12..20], &0u64.to_le_bytes());
    }

    #[test]
    fn modulation_transition_mode_encodes_into_fused_frame() {
        use crate::value::{DcSysTime, TransitionMode};

        let data = vec![0x80u8; 4];
        let payload = fused_payload(Modulation {
            transition_mode: TransitionMode::SysTime {
                time: DcSysTime::from_nanos(0xDEAD_BEEF),
                margin: None,
            },
            ..Modulation::new(SamplingConfig::FREQ_4K, &data)
        });

        assert_eq!(payload[1], 0x01, "SYS_TIME");
        assert_eq!(&payload[12..20], &0xDEAD_BEEFu64.to_le_bytes());
    }

    #[test]
    fn modulation_finite_loop_encodes_rep() {
        use crate::value::LoopBehavior;
        use core::num::NonZeroU16;

        let data = vec![0x80u8; 4];
        let payload = fused_payload(Modulation {
            loop_behavior: LoopBehavior::Finite(NonZeroU16::new(10).unwrap()),
            ..Modulation::new(SamplingConfig::FREQ_4K, &data)
        });
        assert_eq!(&payload[8..10], &9u16.to_le_bytes());
    }

    #[test]
    fn long_modulation_falls_back_to_the_three_frame_path() {
        let data = vec![0x80u8; MOD_FUSED_MAX_DATA_LEN + 1];
        let mut b = DatagramBuilder::new(test_geometry_arc(1));
        b.push(Modulation::with_bank(
            ModulationBank::B1,
            SamplingConfig::FREQ_4K,
            &data,
        ));
        let datagrams = b.build().unwrap();

        assert_eq!(datagrams.len(), 3, "write + config + change");
        assert_eq!(
            datagrams.frame(0).unwrap().datagrams()[0].cmd,
            Cmd::WriteModulationBuffer
        );
        let cfg = datagrams.frame(1).unwrap();
        assert_eq!(cfg.datagrams()[0].cmd, Cmd::ConfigModulation);
        let size = u32::try_from(data.len()).unwrap();
        assert_eq!(&cfg.datagrams()[0].payload[4..8], &size.to_le_bytes());
        assert_eq!(
            datagrams.frame(2).unwrap().datagrams()[0].cmd,
            Cmd::ChangeModulationBank
        );
    }

    #[test]
    fn later_writes_the_bank_without_changing_it() {
        let data = vec![0x80u8; 4];
        let mut b = DatagramBuilder::new(test_geometry_arc(1));
        b.push(Modulation {
            bank: ModulationBank::B1,
            transition_mode: TransitionMode::Later,
            ..Modulation::new(SamplingConfig::FREQ_4K, &data)
        });
        let datagrams = b.build().unwrap();

        assert_eq!(
            datagrams.len(),
            2,
            "write + config, no fusion and no change"
        );
        assert_eq!(
            datagrams.frame(0).unwrap().datagrams()[0].cmd,
            Cmd::WriteModulationBuffer
        );
        let cfg = datagrams.frame(1).unwrap();
        assert_eq!(cfg.datagrams()[0].cmd, Cmd::ConfigModulation);
        assert_eq!(cfg.datagrams()[0].payload[0], 1, "bank B1");
        assert_eq!(&cfg.datagrams()[0].payload[4..8], &4u32.to_le_bytes());
    }

    #[test]
    fn later_skips_the_loop_behavior_constraint() {
        use crate::value::LoopBehavior;
        use core::num::NonZeroU16;

        let data = vec![0x80u8; 4];
        let mut b = DatagramBuilder::new(test_geometry_arc(1));
        b.push(Modulation {
            bank: ModulationBank::B1,
            loop_behavior: LoopBehavior::Finite(NonZeroU16::new(10).unwrap()),
            transition_mode: TransitionMode::Later,
            ..Modulation::new(SamplingConfig::FREQ_4K, &data)
        });
        assert!(b.build().is_ok(), "no transition, so no constraint applies");
    }
}