Skip to main content

ad_plugins_rs/
circular_buff.rs

1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use ad_core_rs::ndarray::NDArray;
5use ad_core_rs::ndarray_pool::NDArrayPool;
6use ad_core_rs::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) => match op {
72                    CalcOp::Not => {
73                        let v = stack.pop().unwrap_or(0.0);
74                        stack.push(if v == 0.0 { 1.0 } else { 0.0 });
75                    }
76                    _ => {
77                        let rhs = stack.pop().unwrap_or(0.0);
78                        let lhs = stack.pop().unwrap_or(0.0);
79                        let result = match op {
80                            CalcOp::Gt => {
81                                if lhs > rhs {
82                                    1.0
83                                } else {
84                                    0.0
85                                }
86                            }
87                            CalcOp::Lt => {
88                                if lhs < rhs {
89                                    1.0
90                                } else {
91                                    0.0
92                                }
93                            }
94                            CalcOp::Ge => {
95                                if lhs >= rhs {
96                                    1.0
97                                } else {
98                                    0.0
99                                }
100                            }
101                            CalcOp::Le => {
102                                if lhs <= rhs {
103                                    1.0
104                                } else {
105                                    0.0
106                                }
107                            }
108                            CalcOp::Eq => {
109                                if (lhs - rhs).abs() < f64::EPSILON {
110                                    1.0
111                                } else {
112                                    0.0
113                                }
114                            }
115                            CalcOp::Ne => {
116                                if (lhs - rhs).abs() >= f64::EPSILON {
117                                    1.0
118                                } else {
119                                    0.0
120                                }
121                            }
122                            CalcOp::And => {
123                                if lhs != 0.0 && rhs != 0.0 {
124                                    1.0
125                                } else {
126                                    0.0
127                                }
128                            }
129                            CalcOp::Or => {
130                                if lhs != 0.0 || rhs != 0.0 {
131                                    1.0
132                                } else {
133                                    0.0
134                                }
135                            }
136                            CalcOp::Not => unreachable!(),
137                        };
138                        stack.push(result);
139                    }
140                },
141            }
142        }
143        stack.pop().unwrap_or(0.0)
144    }
145
146    fn precedence(op: &CalcOp) -> u8 {
147        match op {
148            CalcOp::Or => 1,
149            CalcOp::And => 2,
150            CalcOp::Eq | CalcOp::Ne => 3,
151            CalcOp::Gt | CalcOp::Lt | CalcOp::Ge | CalcOp::Le => 4,
152            CalcOp::Not => 5,
153        }
154    }
155
156    fn is_right_assoc(op: &CalcOp) -> bool {
157        matches!(op, CalcOp::Not)
158    }
159
160    fn tokenize(expr: &str) -> Option<Vec<RawToken>> {
161        use RawToken as RT;
162        let chars: Vec<char> = expr.chars().collect();
163        let mut tokens = Vec::new();
164        let mut i = 0;
165
166        while i < chars.len() {
167            match chars[i] {
168                ' ' | '\t' => {
169                    i += 1;
170                }
171                '(' => {
172                    tokens.push(RT::LParen);
173                    i += 1;
174                }
175                ')' => {
176                    tokens.push(RT::RParen);
177                    i += 1;
178                }
179                'A' | 'a' => {
180                    tokens.push(RT::VarA);
181                    i += 1;
182                }
183                'B' | 'b' => {
184                    tokens.push(RT::VarB);
185                    i += 1;
186                }
187                '>' => {
188                    if i + 1 < chars.len() && chars[i + 1] == '=' {
189                        tokens.push(RT::Op(CalcOp::Ge));
190                        i += 2;
191                    } else {
192                        tokens.push(RT::Op(CalcOp::Gt));
193                        i += 1;
194                    }
195                }
196                '<' => {
197                    if i + 1 < chars.len() && chars[i + 1] == '=' {
198                        tokens.push(RT::Op(CalcOp::Le));
199                        i += 2;
200                    } else {
201                        tokens.push(RT::Op(CalcOp::Lt));
202                        i += 1;
203                    }
204                }
205                '=' => {
206                    if i + 1 < chars.len() && chars[i + 1] == '=' {
207                        tokens.push(RT::Op(CalcOp::Eq));
208                        i += 2;
209                    } else {
210                        return None; // Single '=' not supported
211                    }
212                }
213                '!' => {
214                    if i + 1 < chars.len() && chars[i + 1] == '=' {
215                        tokens.push(RT::Op(CalcOp::Ne));
216                        i += 2;
217                    } else {
218                        tokens.push(RT::Op(CalcOp::Not));
219                        i += 1;
220                    }
221                }
222                '&' => {
223                    if i + 1 < chars.len() && chars[i + 1] == '&' {
224                        tokens.push(RT::Op(CalcOp::And));
225                        i += 2;
226                    } else {
227                        return None;
228                    }
229                }
230                '|' => {
231                    if i + 1 < chars.len() && chars[i + 1] == '|' {
232                        tokens.push(RT::Op(CalcOp::Or));
233                        i += 2;
234                    } else {
235                        return None;
236                    }
237                }
238                c if c.is_ascii_digit() || c == '.' => {
239                    let start = i;
240                    while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
241                        i += 1;
242                    }
243                    let num_str: String = chars[start..i].iter().collect();
244                    let num: f64 = num_str.parse().ok()?;
245                    tokens.push(RT::Num(num));
246                }
247                '-' => {
248                    // Negative number: at start, or after '(' or after an operator
249                    let is_unary_minus = tokens.is_empty()
250                        || matches!(tokens.last(), Some(RT::LParen) | Some(RT::Op(_)));
251                    if is_unary_minus
252                        && i + 1 < chars.len()
253                        && (chars[i + 1].is_ascii_digit() || chars[i + 1] == '.')
254                    {
255                        i += 1; // skip '-'
256                        let start = i;
257                        while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
258                            i += 1;
259                        }
260                        let num_str: String = chars[start..i].iter().collect();
261                        let num: f64 = num_str.parse().ok()?;
262                        tokens.push(RT::Num(-num));
263                    } else {
264                        return None; // Subtraction not supported
265                    }
266                }
267                _ => return None,
268            }
269        }
270
271        Some(tokens)
272    }
273
274    fn shunting_yard(raw: Vec<RawToken>) -> Option<Vec<CalcToken>> {
275        use RawToken as RT;
276        let mut output: Vec<CalcToken> = Vec::new();
277        let mut op_stack: Vec<RawToken> = Vec::new();
278
279        for tok in raw {
280            match tok {
281                RT::Num(n) => output.push(CalcToken::Num(n)),
282                RT::VarA => output.push(CalcToken::VarA),
283                RT::VarB => output.push(CalcToken::VarB),
284                RT::Op(ref op) => {
285                    while let Some(RT::Op(top_op)) = op_stack.last() {
286                        let top_prec = Self::precedence(top_op);
287                        let cur_prec = Self::precedence(op);
288                        if (!Self::is_right_assoc(op) && cur_prec <= top_prec)
289                            || (Self::is_right_assoc(op) && cur_prec < top_prec)
290                        {
291                            if let Some(RT::Op(o)) = op_stack.pop() {
292                                output.push(CalcToken::Op(o));
293                            }
294                        } else {
295                            break;
296                        }
297                    }
298                    op_stack.push(tok);
299                }
300                RT::LParen => op_stack.push(tok),
301                RT::RParen => {
302                    loop {
303                        match op_stack.pop() {
304                            Some(RT::LParen) => break,
305                            Some(RT::Op(o)) => output.push(CalcToken::Op(o)),
306                            _ => return None, // Mismatched parens
307                        }
308                    }
309                }
310            }
311        }
312
313        // Pop remaining operators
314        while let Some(tok) = op_stack.pop() {
315            match tok {
316                RT::Op(o) => output.push(CalcToken::Op(o)),
317                RT::LParen => return None, // Mismatched parens
318                _ => return None,
319            }
320        }
321
322        Some(output)
323    }
324}
325
326/// Trigger condition for circular buffer.
327#[derive(Debug, Clone)]
328pub enum TriggerCondition {
329    /// Trigger on an attribute value exceeding threshold.
330    AttributeThreshold { name: String, threshold: f64 },
331    /// External trigger (manual).
332    External,
333    /// Calculated trigger based on two attribute values and an expression.
334    Calc {
335        attr_a: String,
336        attr_b: String,
337        expression: CalcExpression,
338    },
339}
340
341/// Status of the circular buffer.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum BufferStatus {
344    Idle,
345    BufferFilling,
346    Flushing,
347    AcquisitionCompleted,
348}
349
350/// Circular buffer state for pre/post-trigger capture.
351pub struct CircularBuffer {
352    pub(crate) pre_count: usize,
353    pub(crate) post_count: usize,
354    buffer: VecDeque<Arc<NDArray>>,
355    pub(crate) trigger_condition: TriggerCondition,
356    triggered: bool,
357    post_remaining: usize,
358    captured: Vec<Arc<NDArray>>,
359    /// Maximum number of triggers before stopping (0 = unlimited).
360    preset_trigger_count: usize,
361    /// Number of triggers fired so far.
362    trigger_count: usize,
363    /// If true, flush buffer immediately on soft trigger.
364    flush_on_soft_trigger: bool,
365    /// Current buffer status.
366    pub(crate) status: BufferStatus,
367}
368
369impl CircularBuffer {
370    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
371        Self {
372            pre_count,
373            post_count,
374            buffer: VecDeque::with_capacity(pre_count + 1),
375            trigger_condition: condition,
376            triggered: false,
377            post_remaining: 0,
378            captured: Vec::new(),
379            preset_trigger_count: 0,
380            trigger_count: 0,
381            flush_on_soft_trigger: false,
382            status: BufferStatus::Idle,
383        }
384    }
385
386    /// Set the preset trigger count (0 = unlimited).
387    pub fn set_preset_trigger_count(&mut self, count: usize) {
388        self.preset_trigger_count = count;
389    }
390
391    /// Get the current trigger count.
392    pub fn trigger_count(&self) -> usize {
393        self.trigger_count
394    }
395
396    /// Get the current buffer status.
397    pub fn status(&self) -> BufferStatus {
398        self.status
399    }
400
401    /// Set flush_on_soft_trigger flag.
402    pub fn set_flush_on_soft_trigger(&mut self, flush: bool) {
403        self.flush_on_soft_trigger = flush;
404    }
405
406    /// Push an array into the circular buffer.
407    /// Returns true if a complete capture sequence is ready.
408    pub fn push(&mut self, array: Arc<NDArray>) -> bool {
409        // If acquisition is completed, ignore new frames
410        if self.status == BufferStatus::AcquisitionCompleted {
411            return false;
412        }
413
414        // Transition from Idle to BufferFilling on first push
415        if self.status == BufferStatus::Idle {
416            self.status = BufferStatus::BufferFilling;
417        }
418
419        if self.triggered {
420            // Post-trigger capture (Flushing state)
421            self.captured.push(array);
422            self.post_remaining -= 1;
423            if self.post_remaining == 0 {
424                self.triggered = false;
425                // Check if we've reached the preset trigger count
426                if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count
427                {
428                    self.status = BufferStatus::AcquisitionCompleted;
429                } else {
430                    self.status = BufferStatus::BufferFilling;
431                }
432                return true;
433            }
434            return false;
435        }
436
437        // Check trigger condition
438        let trigger = match &self.trigger_condition {
439            TriggerCondition::AttributeThreshold { name, threshold } => array
440                .attributes
441                .get(name)
442                .and_then(|a| a.value.as_f64())
443                .map(|v| v >= *threshold)
444                .unwrap_or(false),
445            TriggerCondition::External => false,
446            TriggerCondition::Calc {
447                attr_a,
448                attr_b,
449                expression,
450            } => {
451                let a = array
452                    .attributes
453                    .get(attr_a)
454                    .and_then(|a| a.value.as_f64())
455                    .unwrap_or(0.0);
456                let b = array
457                    .attributes
458                    .get(attr_b)
459                    .and_then(|a| a.value.as_f64())
460                    .unwrap_or(0.0);
461                expression.evaluate(a, b) != 0.0
462            }
463        };
464
465        // Maintain pre-trigger ring buffer
466        self.buffer.push_back(array);
467        if self.buffer.len() > self.pre_count {
468            self.buffer.pop_front();
469        }
470
471        if trigger {
472            self.trigger();
473        }
474
475        false
476    }
477
478    /// External trigger.
479    pub fn trigger(&mut self) {
480        // Don't trigger if acquisition already completed
481        if self.status == BufferStatus::AcquisitionCompleted {
482            return;
483        }
484
485        self.triggered = true;
486        self.post_remaining = self.post_count;
487        self.trigger_count += 1;
488        self.status = BufferStatus::Flushing;
489        // Flush pre-trigger buffer to captured
490        self.captured.clear();
491        self.captured.extend(self.buffer.drain(..));
492    }
493
494    /// Take the captured arrays (pre + post trigger).
495    pub fn take_captured(&mut self) -> Vec<Arc<NDArray>> {
496        std::mem::take(&mut self.captured)
497    }
498
499    pub fn is_triggered(&self) -> bool {
500        self.triggered
501    }
502
503    pub fn pre_buffer_len(&self) -> usize {
504        self.buffer.len()
505    }
506
507    pub fn reset(&mut self) {
508        self.buffer.clear();
509        self.captured.clear();
510        self.triggered = false;
511        self.post_remaining = 0;
512        self.trigger_count = 0;
513        self.status = BufferStatus::Idle;
514    }
515}
516
517// --- New CircularBuffProcessor (NDPluginProcess-based) ---
518
519/// CircularBuff processor: maintains ring buffer state, emits captured arrays on trigger.
520#[derive(Default)]
521struct CBParamIndices {
522    control: Option<usize>,
523    status: Option<usize>,
524    trigger_a: Option<usize>,
525    trigger_b: Option<usize>,
526    trigger_a_val: Option<usize>,
527    trigger_b_val: Option<usize>,
528    trigger_calc: Option<usize>,
529    trigger_calc_val: Option<usize>,
530    pre_trigger: Option<usize>,
531    post_trigger: Option<usize>,
532    current_image: Option<usize>,
533    post_count: Option<usize>,
534    soft_trigger: Option<usize>,
535    triggered: Option<usize>,
536    preset_trigger_count: Option<usize>,
537    actual_trigger_count: Option<usize>,
538    flush_on_soft_trigger: Option<usize>,
539}
540
541pub struct CircularBuffProcessor {
542    buffer: CircularBuffer,
543    params: CBParamIndices,
544    // cached trigger attribute names and calc expression
545    trigger_a_name: String,
546    trigger_b_name: String,
547    trigger_calc_expr: String,
548}
549
550impl CircularBuffProcessor {
551    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
552        Self {
553            buffer: CircularBuffer::new(pre_count, post_count, condition),
554            params: CBParamIndices::default(),
555            trigger_a_name: String::new(),
556            trigger_b_name: String::new(),
557            trigger_calc_expr: String::new(),
558        }
559    }
560
561    pub fn trigger(&mut self) {
562        self.buffer.trigger();
563    }
564
565    pub fn buffer(&self) -> &CircularBuffer {
566        &self.buffer
567    }
568
569    /// Rebuild the trigger condition from cached attribute names and calc expression.
570    fn rebuild_trigger_condition(&mut self) {
571        if !self.trigger_calc_expr.is_empty() {
572            if let Some(expr) = CalcExpression::parse(&self.trigger_calc_expr) {
573                self.buffer.trigger_condition = TriggerCondition::Calc {
574                    attr_a: self.trigger_a_name.clone(),
575                    attr_b: self.trigger_b_name.clone(),
576                    expression: expr,
577                };
578                return;
579            }
580        }
581        if !self.trigger_a_name.is_empty() {
582            self.buffer.trigger_condition = TriggerCondition::AttributeThreshold {
583                name: self.trigger_a_name.clone(),
584                threshold: 0.5,
585            };
586        } else {
587            self.buffer.trigger_condition = TriggerCondition::External;
588        }
589    }
590}
591
592impl NDPluginProcess for CircularBuffProcessor {
593    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
594        use ad_core_rs::plugin::runtime::ParamUpdate;
595
596        let done = self.buffer.push(Arc::new(array.clone()));
597
598        let mut updates = Vec::new();
599        if let Some(idx) = self.params.status {
600            let status_val = match self.buffer.status() {
601                BufferStatus::Idle => 0,
602                BufferStatus::BufferFilling => 1,
603                BufferStatus::Flushing => 2,
604                BufferStatus::AcquisitionCompleted => 3,
605            };
606            updates.push(ParamUpdate::int32(idx, status_val));
607        }
608        if let Some(idx) = self.params.current_image {
609            updates.push(ParamUpdate::int32(idx, self.buffer.pre_buffer_len() as i32));
610        }
611        if let Some(idx) = self.params.triggered {
612            updates.push(ParamUpdate::int32(
613                idx,
614                if self.buffer.is_triggered() { 1 } else { 0 },
615            ));
616        }
617        if let Some(idx) = self.params.actual_trigger_count {
618            updates.push(ParamUpdate::int32(idx, self.buffer.trigger_count() as i32));
619        }
620
621        if done {
622            let mut result = ProcessResult::arrays(self.buffer.take_captured());
623            result.param_updates = updates;
624            result
625        } else {
626            ProcessResult::sink(updates)
627        }
628    }
629
630    fn plugin_type(&self) -> &str {
631        "NDPluginCircularBuff"
632    }
633
634    fn register_params(
635        &mut self,
636        base: &mut asyn_rs::port::PortDriverBase,
637    ) -> asyn_rs::error::AsynResult<()> {
638        use asyn_rs::param::ParamType;
639        base.create_param("CIRC_BUFF_CONTROL", ParamType::Int32)?;
640        base.create_param("CIRC_BUFF_STATUS", ParamType::Int32)?;
641        base.create_param("CIRC_BUFF_TRIGGER_A", ParamType::Octet)?;
642        base.create_param("CIRC_BUFF_TRIGGER_B", ParamType::Octet)?;
643        base.create_param("CIRC_BUFF_TRIGGER_A_VAL", ParamType::Float64)?;
644        base.create_param("CIRC_BUFF_TRIGGER_B_VAL", ParamType::Float64)?;
645        base.create_param("CIRC_BUFF_TRIGGER_CALC", ParamType::Octet)?;
646        base.create_param("CIRC_BUFF_TRIGGER_CALC_VAL", ParamType::Float64)?;
647        base.create_param("CIRC_BUFF_PRE_TRIGGER", ParamType::Int32)?;
648        base.create_param("CIRC_BUFF_POST_TRIGGER", ParamType::Int32)?;
649        base.create_param("CIRC_BUFF_CURRENT_IMAGE", ParamType::Int32)?;
650        base.create_param("CIRC_BUFF_POST_COUNT", ParamType::Int32)?;
651        base.create_param("CIRC_BUFF_SOFT_TRIGGER", ParamType::Int32)?;
652        base.create_param("CIRC_BUFF_TRIGGERED", ParamType::Int32)?;
653        base.create_param("CIRC_BUFF_PRESET_TRIGGER_COUNT", ParamType::Int32)?;
654        base.create_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT", ParamType::Int32)?;
655        base.create_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER", ParamType::Int32)?;
656
657        self.params.control = base.find_param("CIRC_BUFF_CONTROL");
658        self.params.status = base.find_param("CIRC_BUFF_STATUS");
659        self.params.trigger_a = base.find_param("CIRC_BUFF_TRIGGER_A");
660        self.params.trigger_b = base.find_param("CIRC_BUFF_TRIGGER_B");
661        self.params.trigger_a_val = base.find_param("CIRC_BUFF_TRIGGER_A_VAL");
662        self.params.trigger_b_val = base.find_param("CIRC_BUFF_TRIGGER_B_VAL");
663        self.params.trigger_calc = base.find_param("CIRC_BUFF_TRIGGER_CALC");
664        self.params.trigger_calc_val = base.find_param("CIRC_BUFF_TRIGGER_CALC_VAL");
665        self.params.pre_trigger = base.find_param("CIRC_BUFF_PRE_TRIGGER");
666        self.params.post_trigger = base.find_param("CIRC_BUFF_POST_TRIGGER");
667        self.params.current_image = base.find_param("CIRC_BUFF_CURRENT_IMAGE");
668        self.params.post_count = base.find_param("CIRC_BUFF_POST_COUNT");
669        self.params.soft_trigger = base.find_param("CIRC_BUFF_SOFT_TRIGGER");
670        self.params.triggered = base.find_param("CIRC_BUFF_TRIGGERED");
671        self.params.preset_trigger_count = base.find_param("CIRC_BUFF_PRESET_TRIGGER_COUNT");
672        self.params.actual_trigger_count = base.find_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT");
673        self.params.flush_on_soft_trigger = base.find_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER");
674        Ok(())
675    }
676
677    fn on_param_change(
678        &mut self,
679        reason: usize,
680        params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
681    ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
682        use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue};
683
684        if Some(reason) == self.params.control {
685            let v = params.value.as_i32();
686            if v == 1 {
687                // Start
688                self.buffer.reset();
689                self.buffer.status = BufferStatus::BufferFilling;
690            } else {
691                // Stop
692                self.buffer.status = BufferStatus::Idle;
693            }
694        } else if Some(reason) == self.params.pre_trigger {
695            self.buffer.pre_count = params.value.as_i32().max(0) as usize;
696        } else if Some(reason) == self.params.post_trigger {
697            self.buffer.post_count = params.value.as_i32().max(0) as usize;
698        } else if Some(reason) == self.params.preset_trigger_count {
699            self.buffer
700                .set_preset_trigger_count(params.value.as_i32().max(0) as usize);
701        } else if Some(reason) == self.params.flush_on_soft_trigger {
702            self.buffer
703                .set_flush_on_soft_trigger(params.value.as_i32() != 0);
704        } else if Some(reason) == self.params.soft_trigger {
705            if params.value.as_i32() != 0 {
706                self.buffer.trigger();
707            }
708        } else if Some(reason) == self.params.trigger_a {
709            if let ParamChangeValue::Octet(s) = &params.value {
710                self.trigger_a_name = s.clone();
711                self.rebuild_trigger_condition();
712            }
713        } else if Some(reason) == self.params.trigger_b {
714            if let ParamChangeValue::Octet(s) = &params.value {
715                self.trigger_b_name = s.clone();
716                self.rebuild_trigger_condition();
717            }
718        } else if Some(reason) == self.params.trigger_calc {
719            if let ParamChangeValue::Octet(s) = &params.value {
720                self.trigger_calc_expr = s.clone();
721                self.rebuild_trigger_condition();
722            }
723        }
724
725        ParamChangeResult::updates(vec![])
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
733    use ad_core_rs::ndarray::{NDDataType, NDDimension};
734
735    fn make_array(id: i32) -> Arc<NDArray> {
736        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
737        arr.unique_id = id;
738        Arc::new(arr)
739    }
740
741    fn make_array_with_attr(id: i32, attr_val: f64) -> Arc<NDArray> {
742        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
743        arr.unique_id = id;
744        arr.attributes.add(NDAttribute {
745            name: "trigger".into(),
746            description: "".into(),
747            source: NDAttrSource::Driver,
748            value: NDAttrValue::Float64(attr_val),
749        });
750        Arc::new(arr)
751    }
752
753    fn make_array_with_attrs(id: i32, a_val: f64, b_val: f64) -> Arc<NDArray> {
754        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
755        arr.unique_id = id;
756        arr.attributes.add(NDAttribute {
757            name: "attr_a".into(),
758            description: "".into(),
759            source: NDAttrSource::Driver,
760            value: NDAttrValue::Float64(a_val),
761        });
762        arr.attributes.add(NDAttribute {
763            name: "attr_b".into(),
764            description: "".into(),
765            source: NDAttrSource::Driver,
766            value: NDAttrValue::Float64(b_val),
767        });
768        Arc::new(arr)
769    }
770
771    #[test]
772    fn test_pre_trigger_buffering() {
773        let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
774
775        for i in 0..5 {
776            cb.push(make_array(i));
777        }
778        // Pre-buffer should hold last 3
779        assert_eq!(cb.pre_buffer_len(), 3);
780    }
781
782    #[test]
783    fn test_external_trigger() {
784        let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
785
786        cb.push(make_array(1));
787        cb.push(make_array(2));
788        cb.push(make_array(3));
789        // Pre-buffer: [2, 3]
790
791        cb.trigger();
792        assert!(cb.is_triggered());
793
794        cb.push(make_array(4));
795        let done = cb.push(make_array(5));
796        assert!(done);
797
798        let captured = cb.take_captured();
799        assert_eq!(captured.len(), 4); // 2 pre + 2 post
800        assert_eq!(captured[0].unique_id, 2);
801        assert_eq!(captured[1].unique_id, 3);
802        assert_eq!(captured[2].unique_id, 4);
803        assert_eq!(captured[3].unique_id, 5);
804    }
805
806    #[test]
807    fn test_attribute_trigger() {
808        let mut cb = CircularBuffer::new(
809            1,
810            1,
811            TriggerCondition::AttributeThreshold {
812                name: "trigger".into(),
813                threshold: 5.0,
814            },
815        );
816
817        cb.push(make_array_with_attr(1, 1.0));
818        cb.push(make_array_with_attr(2, 2.0));
819        assert!(!cb.is_triggered());
820
821        // This should trigger (attr >= 5.0)
822        cb.push(make_array_with_attr(3, 5.0));
823        assert!(cb.is_triggered());
824
825        let done = cb.push(make_array(4));
826        assert!(done);
827
828        let captured = cb.take_captured();
829        assert_eq!(captured.len(), 2); // 1 pre + 1 post
830    }
831
832    // --- New tests ---
833
834    #[test]
835    fn test_calc_trigger() {
836        // Expression: "A>5" — trigger when attribute A exceeds 5
837        let expr = CalcExpression::parse("A>5").unwrap();
838        let mut cb = CircularBuffer::new(
839            1,
840            1,
841            TriggerCondition::Calc {
842                attr_a: "attr_a".into(),
843                attr_b: "attr_b".into(),
844                expression: expr,
845            },
846        );
847
848        // A=3, should not trigger
849        cb.push(make_array_with_attrs(1, 3.0, 0.0));
850        assert!(!cb.is_triggered());
851
852        // A=6, should trigger
853        cb.push(make_array_with_attrs(2, 6.0, 0.0));
854        assert!(cb.is_triggered());
855
856        let done = cb.push(make_array(3));
857        assert!(done);
858
859        let captured = cb.take_captured();
860        assert_eq!(captured.len(), 2); // 1 pre + 1 post
861    }
862
863    #[test]
864    fn test_calc_expression_parse() {
865        // Simple comparison
866        let expr = CalcExpression::parse("A>5").unwrap();
867        assert_eq!(expr.evaluate(6.0, 0.0), 1.0);
868        assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
869        assert_eq!(expr.evaluate(5.0, 0.0), 0.0); // not >=
870
871        // Greater-or-equal
872        let expr = CalcExpression::parse("A>=5").unwrap();
873        assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
874        assert_eq!(expr.evaluate(4.9, 0.0), 0.0);
875
876        // Logical AND with two variables
877        let expr = CalcExpression::parse("A>3&&B<10").unwrap();
878        assert_eq!(expr.evaluate(4.0, 5.0), 1.0);
879        assert_eq!(expr.evaluate(2.0, 5.0), 0.0);
880        assert_eq!(expr.evaluate(4.0, 15.0), 0.0);
881
882        // Parenthesized OR
883        let expr = CalcExpression::parse("(A>10)||(B>10)").unwrap();
884        assert_eq!(expr.evaluate(11.0, 0.0), 1.0);
885        assert_eq!(expr.evaluate(0.0, 11.0), 1.0);
886        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
887
888        // Not-equal
889        let expr = CalcExpression::parse("A!=0").unwrap();
890        assert_eq!(expr.evaluate(1.0, 0.0), 1.0);
891        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
892
893        // Equality
894        let expr = CalcExpression::parse("A==B").unwrap();
895        assert_eq!(expr.evaluate(5.0, 5.0), 1.0);
896        assert_eq!(expr.evaluate(5.0, 6.0), 0.0);
897
898        // Not operator
899        let expr = CalcExpression::parse("!A").unwrap();
900        assert_eq!(expr.evaluate(0.0, 0.0), 1.0);
901        assert_eq!(expr.evaluate(1.0, 0.0), 0.0);
902
903        // Invalid expression returns None
904        assert!(CalcExpression::parse("A=5").is_none()); // single = not supported
905        assert!(CalcExpression::parse("A&B").is_none()); // single & not supported
906    }
907
908    #[test]
909    fn test_preset_trigger_count() {
910        let mut cb = CircularBuffer::new(1, 1, TriggerCondition::External);
911        cb.set_preset_trigger_count(2);
912
913        assert_eq!(cb.status(), BufferStatus::Idle);
914
915        // First push transitions to BufferFilling
916        cb.push(make_array(1));
917        assert_eq!(cb.status(), BufferStatus::BufferFilling);
918
919        // First trigger
920        cb.trigger();
921        assert_eq!(cb.trigger_count(), 1);
922        assert_eq!(cb.status(), BufferStatus::Flushing);
923
924        let done = cb.push(make_array(2));
925        assert!(done);
926        assert_eq!(cb.status(), BufferStatus::BufferFilling); // back to filling after first capture
927
928        cb.take_captured();
929
930        // Refill buffer
931        cb.push(make_array(3));
932
933        // Second trigger — should reach preset count
934        cb.trigger();
935        assert_eq!(cb.trigger_count(), 2);
936        assert_eq!(cb.status(), BufferStatus::Flushing);
937
938        let done = cb.push(make_array(4));
939        assert!(done);
940        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
941
942        cb.take_captured();
943
944        // Further frames should be ignored
945        let done = cb.push(make_array(5));
946        assert!(!done);
947        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
948
949        // Further triggers should be ignored
950        cb.trigger();
951        assert_eq!(cb.trigger_count(), 2); // unchanged
952    }
953
954    #[test]
955    fn test_buffer_status_transitions() {
956        let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
957
958        // Initial state
959        assert_eq!(cb.status(), BufferStatus::Idle);
960
961        // First push -> BufferFilling
962        cb.push(make_array(1));
963        assert_eq!(cb.status(), BufferStatus::BufferFilling);
964
965        cb.push(make_array(2));
966        assert_eq!(cb.status(), BufferStatus::BufferFilling);
967
968        // Trigger -> Flushing
969        cb.trigger();
970        assert_eq!(cb.status(), BufferStatus::Flushing);
971
972        // Post-trigger capture completes -> back to BufferFilling
973        let done = cb.push(make_array(3));
974        assert!(done);
975        assert_eq!(cb.status(), BufferStatus::BufferFilling);
976
977        // Reset -> Idle
978        cb.reset();
979        assert_eq!(cb.status(), BufferStatus::Idle);
980        assert_eq!(cb.trigger_count(), 0);
981    }
982}