Skip to main content

ad_plugins/
circular_buff.rs

1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use ad_core::ndarray::NDArray;
5use ad_core::ndarray_pool::NDArrayPool;
6use ad_core::plugin::runtime::{NDPluginProcess, ProcessResult};
7
8/// Operations supported by CalcExpression.
9#[derive(Debug, Clone, Copy)]
10enum CalcOp {
11    Gt,
12    Lt,
13    Ge,
14    Le,
15    Eq,
16    Ne,
17    And,
18    Or,
19    Not,
20}
21
22/// Token in a parsed CalcExpression (RPN).
23#[derive(Debug, Clone)]
24enum CalcToken {
25    Num(f64),
26    VarA,
27    VarB,
28    Op(CalcOp),
29}
30
31/// Raw token used during parsing (before conversion to RPN).
32#[derive(Debug, Clone)]
33enum RawToken {
34    Num(f64),
35    VarA,
36    VarB,
37    Op(CalcOp),
38    LParen,
39    RParen,
40}
41
42/// A simple expression evaluator supporting variables A and B,
43/// numeric literals, comparison and logical operators.
44///
45/// Parsed into reverse-polish notation (RPN) using shunting-yard.
46#[derive(Debug, Clone)]
47pub struct CalcExpression {
48    tokens: Vec<CalcToken>,
49}
50
51impl CalcExpression {
52    /// Parse an infix expression into RPN.
53    ///
54    /// Supports: A, B (variables), numeric literals (including decimals and negatives
55    /// at start or after open paren), >, <, >=, <=, ==, !=, &&, ||, !, parentheses.
56    pub fn parse(expr: &str) -> Option<CalcExpression> {
57        let raw_tokens = Self::tokenize(expr)?;
58        let rpn = Self::shunting_yard(raw_tokens)?;
59        Some(CalcExpression { tokens: rpn })
60    }
61
62    /// Evaluate the expression with the given variable values.
63    /// Returns the numeric result; nonzero means true for trigger purposes.
64    pub fn evaluate(&self, a: f64, b: f64) -> f64 {
65        let mut stack: Vec<f64> = Vec::new();
66        for tok in &self.tokens {
67            match tok {
68                CalcToken::Num(n) => stack.push(*n),
69                CalcToken::VarA => stack.push(a),
70                CalcToken::VarB => stack.push(b),
71                CalcToken::Op(op) => {
72                    match op {
73                        CalcOp::Not => {
74                            let v = stack.pop().unwrap_or(0.0);
75                            stack.push(if v == 0.0 { 1.0 } else { 0.0 });
76                        }
77                        _ => {
78                            let rhs = stack.pop().unwrap_or(0.0);
79                            let lhs = stack.pop().unwrap_or(0.0);
80                            let result = match op {
81                                CalcOp::Gt => if lhs > rhs { 1.0 } else { 0.0 },
82                                CalcOp::Lt => if lhs < rhs { 1.0 } else { 0.0 },
83                                CalcOp::Ge => if lhs >= rhs { 1.0 } else { 0.0 },
84                                CalcOp::Le => if lhs <= rhs { 1.0 } else { 0.0 },
85                                CalcOp::Eq => if (lhs - rhs).abs() < f64::EPSILON { 1.0 } else { 0.0 },
86                                CalcOp::Ne => if (lhs - rhs).abs() >= f64::EPSILON { 1.0 } else { 0.0 },
87                                CalcOp::And => if lhs != 0.0 && rhs != 0.0 { 1.0 } else { 0.0 },
88                                CalcOp::Or => if lhs != 0.0 || rhs != 0.0 { 1.0 } else { 0.0 },
89                                CalcOp::Not => unreachable!(),
90                            };
91                            stack.push(result);
92                        }
93                    }
94                }
95            }
96        }
97        stack.pop().unwrap_or(0.0)
98    }
99
100    fn precedence(op: &CalcOp) -> u8 {
101        match op {
102            CalcOp::Or => 1,
103            CalcOp::And => 2,
104            CalcOp::Eq | CalcOp::Ne => 3,
105            CalcOp::Gt | CalcOp::Lt | CalcOp::Ge | CalcOp::Le => 4,
106            CalcOp::Not => 5,
107        }
108    }
109
110    fn is_right_assoc(op: &CalcOp) -> bool {
111        matches!(op, CalcOp::Not)
112    }
113
114    fn tokenize(expr: &str) -> Option<Vec<RawToken>> {
115        use RawToken as RT;
116        let chars: Vec<char> = expr.chars().collect();
117        let mut tokens = Vec::new();
118        let mut i = 0;
119
120        while i < chars.len() {
121            match chars[i] {
122                ' ' | '\t' => { i += 1; }
123                '(' => { tokens.push(RT::LParen); i += 1; }
124                ')' => { tokens.push(RT::RParen); i += 1; }
125                'A' | 'a' => { tokens.push(RT::VarA); i += 1; }
126                'B' | 'b' => { tokens.push(RT::VarB); i += 1; }
127                '>' => {
128                    if i + 1 < chars.len() && chars[i + 1] == '=' {
129                        tokens.push(RT::Op(CalcOp::Ge));
130                        i += 2;
131                    } else {
132                        tokens.push(RT::Op(CalcOp::Gt));
133                        i += 1;
134                    }
135                }
136                '<' => {
137                    if i + 1 < chars.len() && chars[i + 1] == '=' {
138                        tokens.push(RT::Op(CalcOp::Le));
139                        i += 2;
140                    } else {
141                        tokens.push(RT::Op(CalcOp::Lt));
142                        i += 1;
143                    }
144                }
145                '=' => {
146                    if i + 1 < chars.len() && chars[i + 1] == '=' {
147                        tokens.push(RT::Op(CalcOp::Eq));
148                        i += 2;
149                    } else {
150                        return None; // Single '=' not supported
151                    }
152                }
153                '!' => {
154                    if i + 1 < chars.len() && chars[i + 1] == '=' {
155                        tokens.push(RT::Op(CalcOp::Ne));
156                        i += 2;
157                    } else {
158                        tokens.push(RT::Op(CalcOp::Not));
159                        i += 1;
160                    }
161                }
162                '&' => {
163                    if i + 1 < chars.len() && chars[i + 1] == '&' {
164                        tokens.push(RT::Op(CalcOp::And));
165                        i += 2;
166                    } else {
167                        return None;
168                    }
169                }
170                '|' => {
171                    if i + 1 < chars.len() && chars[i + 1] == '|' {
172                        tokens.push(RT::Op(CalcOp::Or));
173                        i += 2;
174                    } else {
175                        return None;
176                    }
177                }
178                c if c.is_ascii_digit() || c == '.' => {
179                    let start = i;
180                    while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
181                        i += 1;
182                    }
183                    let num_str: String = chars[start..i].iter().collect();
184                    let num: f64 = num_str.parse().ok()?;
185                    tokens.push(RT::Num(num));
186                }
187                '-' => {
188                    // Negative number: at start, or after '(' or after an operator
189                    let is_unary_minus = tokens.is_empty()
190                        || matches!(tokens.last(), Some(RT::LParen) | Some(RT::Op(_)));
191                    if is_unary_minus && i + 1 < chars.len() && (chars[i + 1].is_ascii_digit() || chars[i + 1] == '.') {
192                        i += 1; // skip '-'
193                        let start = i;
194                        while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
195                            i += 1;
196                        }
197                        let num_str: String = chars[start..i].iter().collect();
198                        let num: f64 = num_str.parse().ok()?;
199                        tokens.push(RT::Num(-num));
200                    } else {
201                        return None; // Subtraction not supported
202                    }
203                }
204                _ => return None,
205            }
206        }
207
208        Some(tokens)
209    }
210
211    fn shunting_yard(raw: Vec<RawToken>) -> Option<Vec<CalcToken>> {
212        use RawToken as RT;
213        let mut output: Vec<CalcToken> = Vec::new();
214        let mut op_stack: Vec<RawToken> = Vec::new();
215
216        for tok in raw {
217            match tok {
218                RT::Num(n) => output.push(CalcToken::Num(n)),
219                RT::VarA => output.push(CalcToken::VarA),
220                RT::VarB => output.push(CalcToken::VarB),
221                RT::Op(ref op) => {
222                    while let Some(RT::Op(top_op)) = op_stack.last() {
223                        let top_prec = Self::precedence(top_op);
224                        let cur_prec = Self::precedence(op);
225                        if (!Self::is_right_assoc(op) && cur_prec <= top_prec)
226                            || (Self::is_right_assoc(op) && cur_prec < top_prec)
227                        {
228                            if let Some(RT::Op(o)) = op_stack.pop() {
229                                output.push(CalcToken::Op(o));
230                            }
231                        } else {
232                            break;
233                        }
234                    }
235                    op_stack.push(tok);
236                }
237                RT::LParen => op_stack.push(tok),
238                RT::RParen => {
239                    loop {
240                        match op_stack.pop() {
241                            Some(RT::LParen) => break,
242                            Some(RT::Op(o)) => output.push(CalcToken::Op(o)),
243                            _ => return None, // Mismatched parens
244                        }
245                    }
246                }
247            }
248        }
249
250        // Pop remaining operators
251        while let Some(tok) = op_stack.pop() {
252            match tok {
253                RT::Op(o) => output.push(CalcToken::Op(o)),
254                RT::LParen => return None, // Mismatched parens
255                _ => return None,
256            }
257        }
258
259        Some(output)
260    }
261}
262
263/// Trigger condition for circular buffer.
264#[derive(Debug, Clone)]
265pub enum TriggerCondition {
266    /// Trigger on an attribute value exceeding threshold.
267    AttributeThreshold { name: String, threshold: f64 },
268    /// External trigger (manual).
269    External,
270    /// Calculated trigger based on two attribute values and an expression.
271    Calc {
272        attr_a: String,
273        attr_b: String,
274        expression: CalcExpression,
275    },
276}
277
278/// Status of the circular buffer.
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum BufferStatus {
281    Idle,
282    BufferFilling,
283    Flushing,
284    AcquisitionCompleted,
285}
286
287/// Circular buffer state for pre/post-trigger capture.
288pub struct CircularBuffer {
289    pre_count: usize,
290    post_count: usize,
291    buffer: VecDeque<Arc<NDArray>>,
292    trigger_condition: TriggerCondition,
293    triggered: bool,
294    post_remaining: usize,
295    captured: Vec<Arc<NDArray>>,
296    /// Maximum number of triggers before stopping (0 = unlimited).
297    preset_trigger_count: usize,
298    /// Number of triggers fired so far.
299    trigger_count: usize,
300    /// If true, flush buffer immediately on soft trigger.
301    flush_on_soft_trigger: bool,
302    /// Current buffer status.
303    status: BufferStatus,
304}
305
306impl CircularBuffer {
307    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
308        Self {
309            pre_count,
310            post_count,
311            buffer: VecDeque::with_capacity(pre_count + 1),
312            trigger_condition: condition,
313            triggered: false,
314            post_remaining: 0,
315            captured: Vec::new(),
316            preset_trigger_count: 0,
317            trigger_count: 0,
318            flush_on_soft_trigger: false,
319            status: BufferStatus::Idle,
320        }
321    }
322
323    /// Set the preset trigger count (0 = unlimited).
324    pub fn set_preset_trigger_count(&mut self, count: usize) {
325        self.preset_trigger_count = count;
326    }
327
328    /// Get the current trigger count.
329    pub fn trigger_count(&self) -> usize {
330        self.trigger_count
331    }
332
333    /// Get the current buffer status.
334    pub fn status(&self) -> BufferStatus {
335        self.status
336    }
337
338    /// Set flush_on_soft_trigger flag.
339    pub fn set_flush_on_soft_trigger(&mut self, flush: bool) {
340        self.flush_on_soft_trigger = flush;
341    }
342
343    /// Push an array into the circular buffer.
344    /// Returns true if a complete capture sequence is ready.
345    pub fn push(&mut self, array: Arc<NDArray>) -> bool {
346        // If acquisition is completed, ignore new frames
347        if self.status == BufferStatus::AcquisitionCompleted {
348            return false;
349        }
350
351        // Transition from Idle to BufferFilling on first push
352        if self.status == BufferStatus::Idle {
353            self.status = BufferStatus::BufferFilling;
354        }
355
356        if self.triggered {
357            // Post-trigger capture (Flushing state)
358            self.captured.push(array);
359            self.post_remaining -= 1;
360            if self.post_remaining == 0 {
361                self.triggered = false;
362                // Check if we've reached the preset trigger count
363                if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count {
364                    self.status = BufferStatus::AcquisitionCompleted;
365                } else {
366                    self.status = BufferStatus::BufferFilling;
367                }
368                return true;
369            }
370            return false;
371        }
372
373        // Check trigger condition
374        let trigger = match &self.trigger_condition {
375            TriggerCondition::AttributeThreshold { name, threshold } => {
376                array.attributes.get(name)
377                    .and_then(|a| a.value.as_f64())
378                    .map(|v| v >= *threshold)
379                    .unwrap_or(false)
380            }
381            TriggerCondition::External => false,
382            TriggerCondition::Calc { attr_a, attr_b, expression } => {
383                let a = array.attributes.get(attr_a)
384                    .and_then(|a| a.value.as_f64()).unwrap_or(0.0);
385                let b = array.attributes.get(attr_b)
386                    .and_then(|a| a.value.as_f64()).unwrap_or(0.0);
387                expression.evaluate(a, b) != 0.0
388            }
389        };
390
391        // Maintain pre-trigger ring buffer
392        self.buffer.push_back(array);
393        if self.buffer.len() > self.pre_count {
394            self.buffer.pop_front();
395        }
396
397        if trigger {
398            self.trigger();
399        }
400
401        false
402    }
403
404    /// External trigger.
405    pub fn trigger(&mut self) {
406        // Don't trigger if acquisition already completed
407        if self.status == BufferStatus::AcquisitionCompleted {
408            return;
409        }
410
411        self.triggered = true;
412        self.post_remaining = self.post_count;
413        self.trigger_count += 1;
414        self.status = BufferStatus::Flushing;
415        // Flush pre-trigger buffer to captured
416        self.captured.clear();
417        self.captured.extend(self.buffer.drain(..));
418    }
419
420    /// Take the captured arrays (pre + post trigger).
421    pub fn take_captured(&mut self) -> Vec<Arc<NDArray>> {
422        std::mem::take(&mut self.captured)
423    }
424
425    pub fn is_triggered(&self) -> bool {
426        self.triggered
427    }
428
429    pub fn pre_buffer_len(&self) -> usize {
430        self.buffer.len()
431    }
432
433    pub fn reset(&mut self) {
434        self.buffer.clear();
435        self.captured.clear();
436        self.triggered = false;
437        self.post_remaining = 0;
438        self.trigger_count = 0;
439        self.status = BufferStatus::Idle;
440    }
441}
442
443// --- New CircularBuffProcessor (NDPluginProcess-based) ---
444
445/// CircularBuff processor: maintains ring buffer state, emits captured arrays on trigger.
446pub struct CircularBuffProcessor {
447    buffer: CircularBuffer,
448}
449
450impl CircularBuffProcessor {
451    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
452        Self {
453            buffer: CircularBuffer::new(pre_count, post_count, condition),
454        }
455    }
456
457    pub fn trigger(&mut self) {
458        self.buffer.trigger();
459    }
460
461    pub fn buffer(&self) -> &CircularBuffer {
462        &self.buffer
463    }
464}
465
466impl NDPluginProcess for CircularBuffProcessor {
467    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
468        let done = self.buffer.push(Arc::new(array.clone()));
469        if done {
470            ProcessResult::arrays(self.buffer.take_captured())
471        } else {
472            ProcessResult::empty()
473        }
474    }
475
476    fn plugin_type(&self) -> &str {
477        "NDPluginCircularBuff"
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use ad_core::ndarray::{NDDataType, NDDimension};
485    use ad_core::attributes::{NDAttribute, NDAttrSource, NDAttrValue};
486
487    fn make_array(id: i32) -> Arc<NDArray> {
488        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
489        arr.unique_id = id;
490        Arc::new(arr)
491    }
492
493    fn make_array_with_attr(id: i32, attr_val: f64) -> Arc<NDArray> {
494        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
495        arr.unique_id = id;
496        arr.attributes.add(NDAttribute {
497            name: "trigger".into(),
498            description: "".into(),
499            source: NDAttrSource::Driver,
500            value: NDAttrValue::Float64(attr_val),
501        });
502        Arc::new(arr)
503    }
504
505    fn make_array_with_attrs(id: i32, a_val: f64, b_val: f64) -> Arc<NDArray> {
506        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
507        arr.unique_id = id;
508        arr.attributes.add(NDAttribute {
509            name: "attr_a".into(),
510            description: "".into(),
511            source: NDAttrSource::Driver,
512            value: NDAttrValue::Float64(a_val),
513        });
514        arr.attributes.add(NDAttribute {
515            name: "attr_b".into(),
516            description: "".into(),
517            source: NDAttrSource::Driver,
518            value: NDAttrValue::Float64(b_val),
519        });
520        Arc::new(arr)
521    }
522
523    #[test]
524    fn test_pre_trigger_buffering() {
525        let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
526
527        for i in 0..5 {
528            cb.push(make_array(i));
529        }
530        // Pre-buffer should hold last 3
531        assert_eq!(cb.pre_buffer_len(), 3);
532    }
533
534    #[test]
535    fn test_external_trigger() {
536        let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
537
538        cb.push(make_array(1));
539        cb.push(make_array(2));
540        cb.push(make_array(3));
541        // Pre-buffer: [2, 3]
542
543        cb.trigger();
544        assert!(cb.is_triggered());
545
546        cb.push(make_array(4));
547        let done = cb.push(make_array(5));
548        assert!(done);
549
550        let captured = cb.take_captured();
551        assert_eq!(captured.len(), 4); // 2 pre + 2 post
552        assert_eq!(captured[0].unique_id, 2);
553        assert_eq!(captured[1].unique_id, 3);
554        assert_eq!(captured[2].unique_id, 4);
555        assert_eq!(captured[3].unique_id, 5);
556    }
557
558    #[test]
559    fn test_attribute_trigger() {
560        let mut cb = CircularBuffer::new(1, 1, TriggerCondition::AttributeThreshold {
561            name: "trigger".into(),
562            threshold: 5.0,
563        });
564
565        cb.push(make_array_with_attr(1, 1.0));
566        cb.push(make_array_with_attr(2, 2.0));
567        assert!(!cb.is_triggered());
568
569        // This should trigger (attr >= 5.0)
570        cb.push(make_array_with_attr(3, 5.0));
571        assert!(cb.is_triggered());
572
573        let done = cb.push(make_array(4));
574        assert!(done);
575
576        let captured = cb.take_captured();
577        assert_eq!(captured.len(), 2); // 1 pre + 1 post
578    }
579
580    // --- New tests ---
581
582    #[test]
583    fn test_calc_trigger() {
584        // Expression: "A>5" — trigger when attribute A exceeds 5
585        let expr = CalcExpression::parse("A>5").unwrap();
586        let mut cb = CircularBuffer::new(1, 1, TriggerCondition::Calc {
587            attr_a: "attr_a".into(),
588            attr_b: "attr_b".into(),
589            expression: expr,
590        });
591
592        // A=3, should not trigger
593        cb.push(make_array_with_attrs(1, 3.0, 0.0));
594        assert!(!cb.is_triggered());
595
596        // A=6, should trigger
597        cb.push(make_array_with_attrs(2, 6.0, 0.0));
598        assert!(cb.is_triggered());
599
600        let done = cb.push(make_array(3));
601        assert!(done);
602
603        let captured = cb.take_captured();
604        assert_eq!(captured.len(), 2); // 1 pre + 1 post
605    }
606
607    #[test]
608    fn test_calc_expression_parse() {
609        // Simple comparison
610        let expr = CalcExpression::parse("A>5").unwrap();
611        assert_eq!(expr.evaluate(6.0, 0.0), 1.0);
612        assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
613        assert_eq!(expr.evaluate(5.0, 0.0), 0.0); // not >=
614
615        // Greater-or-equal
616        let expr = CalcExpression::parse("A>=5").unwrap();
617        assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
618        assert_eq!(expr.evaluate(4.9, 0.0), 0.0);
619
620        // Logical AND with two variables
621        let expr = CalcExpression::parse("A>3&&B<10").unwrap();
622        assert_eq!(expr.evaluate(4.0, 5.0), 1.0);
623        assert_eq!(expr.evaluate(2.0, 5.0), 0.0);
624        assert_eq!(expr.evaluate(4.0, 15.0), 0.0);
625
626        // Parenthesized OR
627        let expr = CalcExpression::parse("(A>10)||(B>10)").unwrap();
628        assert_eq!(expr.evaluate(11.0, 0.0), 1.0);
629        assert_eq!(expr.evaluate(0.0, 11.0), 1.0);
630        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
631
632        // Not-equal
633        let expr = CalcExpression::parse("A!=0").unwrap();
634        assert_eq!(expr.evaluate(1.0, 0.0), 1.0);
635        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
636
637        // Equality
638        let expr = CalcExpression::parse("A==B").unwrap();
639        assert_eq!(expr.evaluate(5.0, 5.0), 1.0);
640        assert_eq!(expr.evaluate(5.0, 6.0), 0.0);
641
642        // Not operator
643        let expr = CalcExpression::parse("!A").unwrap();
644        assert_eq!(expr.evaluate(0.0, 0.0), 1.0);
645        assert_eq!(expr.evaluate(1.0, 0.0), 0.0);
646
647        // Invalid expression returns None
648        assert!(CalcExpression::parse("A=5").is_none()); // single = not supported
649        assert!(CalcExpression::parse("A&B").is_none());  // single & not supported
650    }
651
652    #[test]
653    fn test_preset_trigger_count() {
654        let mut cb = CircularBuffer::new(1, 1, TriggerCondition::External);
655        cb.set_preset_trigger_count(2);
656
657        assert_eq!(cb.status(), BufferStatus::Idle);
658
659        // First push transitions to BufferFilling
660        cb.push(make_array(1));
661        assert_eq!(cb.status(), BufferStatus::BufferFilling);
662
663        // First trigger
664        cb.trigger();
665        assert_eq!(cb.trigger_count(), 1);
666        assert_eq!(cb.status(), BufferStatus::Flushing);
667
668        let done = cb.push(make_array(2));
669        assert!(done);
670        assert_eq!(cb.status(), BufferStatus::BufferFilling); // back to filling after first capture
671
672        cb.take_captured();
673
674        // Refill buffer
675        cb.push(make_array(3));
676
677        // Second trigger — should reach preset count
678        cb.trigger();
679        assert_eq!(cb.trigger_count(), 2);
680        assert_eq!(cb.status(), BufferStatus::Flushing);
681
682        let done = cb.push(make_array(4));
683        assert!(done);
684        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
685
686        cb.take_captured();
687
688        // Further frames should be ignored
689        let done = cb.push(make_array(5));
690        assert!(!done);
691        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
692
693        // Further triggers should be ignored
694        cb.trigger();
695        assert_eq!(cb.trigger_count(), 2); // unchanged
696    }
697
698    #[test]
699    fn test_buffer_status_transitions() {
700        let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
701
702        // Initial state
703        assert_eq!(cb.status(), BufferStatus::Idle);
704
705        // First push -> BufferFilling
706        cb.push(make_array(1));
707        assert_eq!(cb.status(), BufferStatus::BufferFilling);
708
709        cb.push(make_array(2));
710        assert_eq!(cb.status(), BufferStatus::BufferFilling);
711
712        // Trigger -> Flushing
713        cb.trigger();
714        assert_eq!(cb.status(), BufferStatus::Flushing);
715
716        // Post-trigger capture completes -> back to BufferFilling
717        let done = cb.push(make_array(3));
718        assert!(done);
719        assert_eq!(cb.status(), BufferStatus::BufferFilling);
720
721        // Reset -> Idle
722        cb.reset();
723        assert_eq!(cb.status(), BufferStatus::Idle);
724        assert_eq!(cb.trigger_count(), 0);
725    }
726}