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};
7use epics_base_rs::calc;
8
9/// Compiled EPICS calc expression wrapper.
10///
11/// Uses the full epics-base-rs calc engine which supports variables A-L (indices 0-11)
12/// plus arithmetic, math functions (ABS, SQRT, LOG, LN, EXP, SIN, COS, MIN, MAX, etc.),
13/// comparison, logical, and bitwise operators -- matching the C++ EPICS calc engine.
14///
15/// For trigger calculations the C++ passes:
16///   A=attrValueA, B=attrValueB, C=preTrigger, D=postTrigger, E=currentImage, F=triggered
17#[derive(Debug, Clone)]
18pub struct CalcExpression {
19    compiled: calc::CompiledExpr,
20}
21
22impl CalcExpression {
23    /// Compile an infix expression string.
24    ///
25    /// Returns `None` if the expression is invalid.
26    pub fn parse(expr: &str) -> Option<CalcExpression> {
27        calc::compile(expr)
28            .ok()
29            .map(|compiled| CalcExpression { compiled })
30    }
31
32    /// Evaluate with variables A and B only (legacy 2-variable interface).
33    /// Returns the numeric result; nonzero means true for trigger purposes.
34    pub fn evaluate(&self, a: f64, b: f64) -> f64 {
35        let mut inputs = calc::NumericInputs::new();
36        inputs.vars[0] = a; // A
37        inputs.vars[1] = b; // B
38        calc::eval(&self.compiled, &mut inputs).unwrap_or(0.0)
39    }
40
41    /// Evaluate with the full variable set (A through L and beyond).
42    ///
43    /// `vars` is indexed 0=A, 1=B, 2=C, ... 11=L, up to 15=P.
44    pub fn evaluate_vars(&self, vars: &[f64; 16]) -> f64 {
45        let mut inputs = calc::NumericInputs::with_vars(*vars);
46        calc::eval(&self.compiled, &mut inputs).unwrap_or(0.0)
47    }
48}
49
50/// Trigger condition for circular buffer.
51#[derive(Debug, Clone)]
52pub enum TriggerCondition {
53    /// Trigger on an attribute value exceeding threshold.
54    AttributeThreshold { name: String, threshold: f64 },
55    /// External trigger (manual).
56    External,
57    /// Calculated trigger based on attribute values and an expression.
58    ///
59    /// The C++ calc engine passes: A=attrValueA, B=attrValueB, C=preTrigger,
60    /// D=postTrigger, E=currentImage, F=triggered.
61    Calc {
62        attr_a: String,
63        attr_b: String,
64        expression: CalcExpression,
65    },
66}
67
68/// Status of the circular buffer.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum BufferStatus {
71    Idle,
72    BufferFilling,
73    Flushing,
74    AcquisitionCompleted,
75}
76
77/// Circular buffer state for pre/post-trigger capture.
78pub struct CircularBuffer {
79    pub(crate) pre_count: usize,
80    pub(crate) post_count: usize,
81    buffer: VecDeque<Arc<NDArray>>,
82    pub(crate) trigger_condition: TriggerCondition,
83    triggered: bool,
84    post_remaining: usize,
85    captured: Vec<Arc<NDArray>>,
86    /// Maximum number of triggers before stopping (0 = unlimited).
87    preset_trigger_count: usize,
88    /// Number of triggers fired so far.
89    trigger_count: usize,
90    /// If true, flush buffer immediately on soft trigger.
91    flush_on_soft_trigger: bool,
92    /// Current buffer status.
93    pub(crate) status: BufferStatus,
94}
95
96impl CircularBuffer {
97    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
98        Self {
99            pre_count,
100            post_count,
101            buffer: VecDeque::with_capacity(pre_count + 1),
102            trigger_condition: condition,
103            triggered: false,
104            post_remaining: 0,
105            captured: Vec::new(),
106            preset_trigger_count: 0,
107            trigger_count: 0,
108            flush_on_soft_trigger: false,
109            status: BufferStatus::Idle,
110        }
111    }
112
113    /// Set the preset trigger count (0 = unlimited).
114    pub fn set_preset_trigger_count(&mut self, count: usize) {
115        self.preset_trigger_count = count;
116    }
117
118    /// Get the current trigger count.
119    pub fn trigger_count(&self) -> usize {
120        self.trigger_count
121    }
122
123    /// Get the current buffer status.
124    pub fn status(&self) -> BufferStatus {
125        self.status
126    }
127
128    /// Set flush_on_soft_trigger flag.
129    pub fn set_flush_on_soft_trigger(&mut self, flush: bool) {
130        self.flush_on_soft_trigger = flush;
131    }
132
133    /// Push an array into the circular buffer.
134    /// Returns true if a complete capture sequence is ready.
135    pub fn push(&mut self, array: Arc<NDArray>) -> bool {
136        // If acquisition is completed, ignore new frames
137        if self.status == BufferStatus::AcquisitionCompleted {
138            return false;
139        }
140
141        // Transition from Idle to BufferFilling on first push
142        if self.status == BufferStatus::Idle {
143            self.status = BufferStatus::BufferFilling;
144        }
145
146        if self.triggered {
147            // Post-trigger capture (Flushing state)
148            self.captured.push(array);
149            self.post_remaining -= 1;
150            if self.post_remaining == 0 {
151                self.triggered = false;
152                // Check if we've reached the preset trigger count
153                if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count
154                {
155                    self.status = BufferStatus::AcquisitionCompleted;
156                } else {
157                    self.status = BufferStatus::BufferFilling;
158                }
159                return true;
160            }
161            return false;
162        }
163
164        // Check trigger condition BEFORE adding to pre-buffer,
165        // so the triggering frame becomes the first post-trigger frame.
166        let trigger = match &self.trigger_condition {
167            TriggerCondition::AttributeThreshold { name, threshold } => array
168                .attributes
169                .get(name)
170                .and_then(|a| a.value.as_f64())
171                .map(|v| v >= *threshold)
172                .unwrap_or(false),
173            TriggerCondition::External => false,
174            TriggerCondition::Calc {
175                attr_a,
176                attr_b,
177                expression,
178            } => {
179                let a = array
180                    .attributes
181                    .get(attr_a)
182                    .and_then(|a| a.value.as_f64())
183                    .unwrap_or(f64::NAN);
184                let b = array
185                    .attributes
186                    .get(attr_b)
187                    .and_then(|a| a.value.as_f64())
188                    .unwrap_or(f64::NAN);
189                // C++ passes: A=attrValueA, B=attrValueB, C=preTrigger,
190                // D=postTrigger, E=currentImage, F=triggered
191                let mut vars = [0.0f64; 16];
192                vars[0] = a; // A
193                vars[1] = b; // B
194                vars[2] = self.pre_count as f64; // C
195                vars[3] = self.post_count as f64; // D
196                vars[4] = self.buffer.len() as f64; // E (currentImage)
197                vars[5] = if self.triggered { 1.0 } else { 0.0 }; // F
198                expression.evaluate_vars(&vars) != 0.0
199            }
200        };
201
202        if trigger {
203            // Trigger fires before adding this frame to the pre-buffer,
204            // so the triggering frame will be the first post-trigger frame.
205            self.trigger();
206            // The triggering frame is the first post-trigger capture.
207            self.captured.push(array);
208            self.post_remaining -= 1;
209            if self.post_remaining == 0 {
210                self.triggered = false;
211                if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count
212                {
213                    self.status = BufferStatus::AcquisitionCompleted;
214                } else {
215                    self.status = BufferStatus::BufferFilling;
216                }
217                return true;
218            }
219            return false;
220        }
221
222        // Maintain pre-trigger ring buffer
223        self.buffer.push_back(array);
224        if self.buffer.len() > self.pre_count {
225            self.buffer.pop_front();
226        }
227
228        false
229    }
230
231    /// External trigger.
232    pub fn trigger(&mut self) {
233        // Don't trigger if acquisition already completed
234        if self.status == BufferStatus::AcquisitionCompleted {
235            return;
236        }
237
238        self.triggered = true;
239        self.post_remaining = self.post_count;
240        self.trigger_count += 1;
241        self.status = BufferStatus::Flushing;
242        // Flush pre-trigger buffer to captured
243        self.captured.clear();
244        self.captured.extend(self.buffer.drain(..));
245    }
246
247    /// Take the captured arrays (pre + post trigger).
248    pub fn take_captured(&mut self) -> Vec<Arc<NDArray>> {
249        std::mem::take(&mut self.captured)
250    }
251
252    pub fn is_triggered(&self) -> bool {
253        self.triggered
254    }
255
256    pub fn pre_buffer_len(&self) -> usize {
257        self.buffer.len()
258    }
259
260    pub fn reset(&mut self) {
261        self.buffer.clear();
262        self.captured.clear();
263        self.triggered = false;
264        self.post_remaining = 0;
265        self.trigger_count = 0;
266        self.status = BufferStatus::Idle;
267    }
268}
269
270// --- New CircularBuffProcessor (NDPluginProcess-based) ---
271
272/// CircularBuff processor: maintains ring buffer state, emits captured arrays on trigger.
273#[derive(Default)]
274struct CBParamIndices {
275    control: Option<usize>,
276    status: Option<usize>,
277    trigger_a: Option<usize>,
278    trigger_b: Option<usize>,
279    trigger_a_val: Option<usize>,
280    trigger_b_val: Option<usize>,
281    trigger_calc: Option<usize>,
282    trigger_calc_val: Option<usize>,
283    pre_trigger: Option<usize>,
284    post_trigger: Option<usize>,
285    current_image: Option<usize>,
286    post_count: Option<usize>,
287    soft_trigger: Option<usize>,
288    triggered: Option<usize>,
289    preset_trigger_count: Option<usize>,
290    actual_trigger_count: Option<usize>,
291    flush_on_soft_trigger: Option<usize>,
292}
293
294pub struct CircularBuffProcessor {
295    buffer: CircularBuffer,
296    params: CBParamIndices,
297    // cached trigger attribute names and calc expression
298    trigger_a_name: String,
299    trigger_b_name: String,
300    trigger_calc_expr: String,
301}
302
303impl CircularBuffProcessor {
304    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
305        Self {
306            buffer: CircularBuffer::new(pre_count, post_count, condition),
307            params: CBParamIndices::default(),
308            trigger_a_name: String::new(),
309            trigger_b_name: String::new(),
310            trigger_calc_expr: String::new(),
311        }
312    }
313
314    pub fn trigger(&mut self) {
315        self.buffer.trigger();
316    }
317
318    pub fn buffer(&self) -> &CircularBuffer {
319        &self.buffer
320    }
321
322    /// Rebuild the trigger condition from cached attribute names and calc expression.
323    fn rebuild_trigger_condition(&mut self) {
324        if !self.trigger_calc_expr.is_empty() {
325            if let Some(expr) = CalcExpression::parse(&self.trigger_calc_expr) {
326                self.buffer.trigger_condition = TriggerCondition::Calc {
327                    attr_a: self.trigger_a_name.clone(),
328                    attr_b: self.trigger_b_name.clone(),
329                    expression: expr,
330                };
331                return;
332            }
333        }
334        if !self.trigger_a_name.is_empty() {
335            self.buffer.trigger_condition = TriggerCondition::AttributeThreshold {
336                name: self.trigger_a_name.clone(),
337                threshold: 0.5,
338            };
339        } else {
340            self.buffer.trigger_condition = TriggerCondition::External;
341        }
342    }
343}
344
345impl NDPluginProcess for CircularBuffProcessor {
346    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
347        use ad_core_rs::plugin::runtime::ParamUpdate;
348
349        let done = self.buffer.push(Arc::new(array.clone()));
350
351        let mut updates = Vec::new();
352        if let Some(idx) = self.params.status {
353            let status_val = match self.buffer.status() {
354                BufferStatus::Idle => 0,
355                BufferStatus::BufferFilling => 1,
356                BufferStatus::Flushing => 2,
357                BufferStatus::AcquisitionCompleted => 3,
358            };
359            updates.push(ParamUpdate::int32(idx, status_val));
360        }
361        if let Some(idx) = self.params.current_image {
362            updates.push(ParamUpdate::int32(idx, self.buffer.pre_buffer_len() as i32));
363        }
364        if let Some(idx) = self.params.triggered {
365            updates.push(ParamUpdate::int32(
366                idx,
367                if self.buffer.is_triggered() { 1 } else { 0 },
368            ));
369        }
370        if let Some(idx) = self.params.actual_trigger_count {
371            updates.push(ParamUpdate::int32(idx, self.buffer.trigger_count() as i32));
372        }
373
374        if done {
375            let mut result = ProcessResult::arrays(self.buffer.take_captured());
376            result.param_updates = updates;
377            result
378        } else {
379            ProcessResult::sink(updates)
380        }
381    }
382
383    fn plugin_type(&self) -> &str {
384        "NDPluginCircularBuff"
385    }
386
387    fn register_params(
388        &mut self,
389        base: &mut asyn_rs::port::PortDriverBase,
390    ) -> asyn_rs::error::AsynResult<()> {
391        use asyn_rs::param::ParamType;
392        base.create_param("CIRC_BUFF_CONTROL", ParamType::Int32)?;
393        base.create_param("CIRC_BUFF_STATUS", ParamType::Int32)?;
394        base.create_param("CIRC_BUFF_TRIGGER_A", ParamType::Octet)?;
395        base.create_param("CIRC_BUFF_TRIGGER_B", ParamType::Octet)?;
396        base.create_param("CIRC_BUFF_TRIGGER_A_VAL", ParamType::Float64)?;
397        base.create_param("CIRC_BUFF_TRIGGER_B_VAL", ParamType::Float64)?;
398        base.create_param("CIRC_BUFF_TRIGGER_CALC", ParamType::Octet)?;
399        base.create_param("CIRC_BUFF_TRIGGER_CALC_VAL", ParamType::Float64)?;
400        base.create_param("CIRC_BUFF_PRE_TRIGGER", ParamType::Int32)?;
401        base.create_param("CIRC_BUFF_POST_TRIGGER", ParamType::Int32)?;
402        base.create_param("CIRC_BUFF_CURRENT_IMAGE", ParamType::Int32)?;
403        base.create_param("CIRC_BUFF_POST_COUNT", ParamType::Int32)?;
404        base.create_param("CIRC_BUFF_SOFT_TRIGGER", ParamType::Int32)?;
405        base.create_param("CIRC_BUFF_TRIGGERED", ParamType::Int32)?;
406        base.create_param("CIRC_BUFF_PRESET_TRIGGER_COUNT", ParamType::Int32)?;
407        base.create_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT", ParamType::Int32)?;
408        base.create_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER", ParamType::Int32)?;
409
410        self.params.control = base.find_param("CIRC_BUFF_CONTROL");
411        self.params.status = base.find_param("CIRC_BUFF_STATUS");
412        self.params.trigger_a = base.find_param("CIRC_BUFF_TRIGGER_A");
413        self.params.trigger_b = base.find_param("CIRC_BUFF_TRIGGER_B");
414        self.params.trigger_a_val = base.find_param("CIRC_BUFF_TRIGGER_A_VAL");
415        self.params.trigger_b_val = base.find_param("CIRC_BUFF_TRIGGER_B_VAL");
416        self.params.trigger_calc = base.find_param("CIRC_BUFF_TRIGGER_CALC");
417        self.params.trigger_calc_val = base.find_param("CIRC_BUFF_TRIGGER_CALC_VAL");
418        self.params.pre_trigger = base.find_param("CIRC_BUFF_PRE_TRIGGER");
419        self.params.post_trigger = base.find_param("CIRC_BUFF_POST_TRIGGER");
420        self.params.current_image = base.find_param("CIRC_BUFF_CURRENT_IMAGE");
421        self.params.post_count = base.find_param("CIRC_BUFF_POST_COUNT");
422        self.params.soft_trigger = base.find_param("CIRC_BUFF_SOFT_TRIGGER");
423        self.params.triggered = base.find_param("CIRC_BUFF_TRIGGERED");
424        self.params.preset_trigger_count = base.find_param("CIRC_BUFF_PRESET_TRIGGER_COUNT");
425        self.params.actual_trigger_count = base.find_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT");
426        self.params.flush_on_soft_trigger = base.find_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER");
427        Ok(())
428    }
429
430    fn on_param_change(
431        &mut self,
432        reason: usize,
433        params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
434    ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
435        use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue};
436
437        if Some(reason) == self.params.control {
438            let v = params.value.as_i32();
439            if v == 1 {
440                // Start
441                self.buffer.reset();
442                self.buffer.status = BufferStatus::BufferFilling;
443            } else {
444                // Stop
445                self.buffer.status = BufferStatus::Idle;
446            }
447        } else if Some(reason) == self.params.pre_trigger {
448            self.buffer.pre_count = params.value.as_i32().max(0) as usize;
449        } else if Some(reason) == self.params.post_trigger {
450            self.buffer.post_count = params.value.as_i32().max(0) as usize;
451        } else if Some(reason) == self.params.preset_trigger_count {
452            self.buffer
453                .set_preset_trigger_count(params.value.as_i32().max(0) as usize);
454        } else if Some(reason) == self.params.flush_on_soft_trigger {
455            self.buffer
456                .set_flush_on_soft_trigger(params.value.as_i32() != 0);
457        } else if Some(reason) == self.params.soft_trigger {
458            if params.value.as_i32() != 0 {
459                self.buffer.trigger();
460            }
461        } else if Some(reason) == self.params.trigger_a {
462            if let ParamChangeValue::Octet(s) = &params.value {
463                self.trigger_a_name = s.clone();
464                self.rebuild_trigger_condition();
465            }
466        } else if Some(reason) == self.params.trigger_b {
467            if let ParamChangeValue::Octet(s) = &params.value {
468                self.trigger_b_name = s.clone();
469                self.rebuild_trigger_condition();
470            }
471        } else if Some(reason) == self.params.trigger_calc {
472            if let ParamChangeValue::Octet(s) = &params.value {
473                self.trigger_calc_expr = s.clone();
474                self.rebuild_trigger_condition();
475            }
476        }
477
478        ParamChangeResult::updates(vec![])
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
486    use ad_core_rs::ndarray::{NDDataType, NDDimension};
487
488    fn make_array(id: i32) -> Arc<NDArray> {
489        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
490        arr.unique_id = id;
491        Arc::new(arr)
492    }
493
494    fn make_array_with_attr(id: i32, attr_val: f64) -> Arc<NDArray> {
495        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
496        arr.unique_id = id;
497        arr.attributes.add(NDAttribute {
498            name: "trigger".into(),
499            description: "".into(),
500            source: NDAttrSource::Driver,
501            value: NDAttrValue::Float64(attr_val),
502        });
503        Arc::new(arr)
504    }
505
506    fn make_array_with_attrs(id: i32, a_val: f64, b_val: f64) -> Arc<NDArray> {
507        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
508        arr.unique_id = id;
509        arr.attributes.add(NDAttribute {
510            name: "attr_a".into(),
511            description: "".into(),
512            source: NDAttrSource::Driver,
513            value: NDAttrValue::Float64(a_val),
514        });
515        arr.attributes.add(NDAttribute {
516            name: "attr_b".into(),
517            description: "".into(),
518            source: NDAttrSource::Driver,
519            value: NDAttrValue::Float64(b_val),
520        });
521        Arc::new(arr)
522    }
523
524    #[test]
525    fn test_pre_trigger_buffering() {
526        let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
527
528        for i in 0..5 {
529            cb.push(make_array(i));
530        }
531        // Pre-buffer should hold last 3
532        assert_eq!(cb.pre_buffer_len(), 3);
533    }
534
535    #[test]
536    fn test_external_trigger() {
537        let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
538
539        cb.push(make_array(1));
540        cb.push(make_array(2));
541        cb.push(make_array(3));
542        // Pre-buffer: [2, 3]
543
544        cb.trigger();
545        assert!(cb.is_triggered());
546
547        cb.push(make_array(4));
548        let done = cb.push(make_array(5));
549        assert!(done);
550
551        let captured = cb.take_captured();
552        assert_eq!(captured.len(), 4); // 2 pre + 2 post
553        assert_eq!(captured[0].unique_id, 2);
554        assert_eq!(captured[1].unique_id, 3);
555        assert_eq!(captured[2].unique_id, 4);
556        assert_eq!(captured[3].unique_id, 5);
557    }
558
559    #[test]
560    fn test_attribute_trigger() {
561        let mut cb = CircularBuffer::new(
562            1,
563            2,
564            TriggerCondition::AttributeThreshold {
565                name: "trigger".into(),
566                threshold: 5.0,
567            },
568        );
569
570        cb.push(make_array_with_attr(1, 1.0));
571        cb.push(make_array_with_attr(2, 2.0));
572        assert!(!cb.is_triggered());
573
574        // This should trigger (attr >= 5.0); triggering frame is first post-trigger
575        cb.push(make_array_with_attr(3, 5.0));
576        assert!(cb.is_triggered());
577
578        let done = cb.push(make_array(4));
579        assert!(done);
580
581        let captured = cb.take_captured();
582        // 1 pre (id=2) + 2 post (id=3 triggering frame + id=4)
583        assert_eq!(captured.len(), 3);
584        assert_eq!(captured[0].unique_id, 2);
585        assert_eq!(captured[1].unique_id, 3);
586        assert_eq!(captured[2].unique_id, 4);
587    }
588
589    // --- New tests ---
590
591    #[test]
592    fn test_calc_trigger() {
593        // Expression: "A>5" — trigger when attribute A exceeds 5
594        let expr = CalcExpression::parse("A>5").unwrap();
595        let mut cb = CircularBuffer::new(
596            1,
597            2,
598            TriggerCondition::Calc {
599                attr_a: "attr_a".into(),
600                attr_b: "attr_b".into(),
601                expression: expr,
602            },
603        );
604
605        // A=3, should not trigger
606        cb.push(make_array_with_attrs(1, 3.0, 0.0));
607        assert!(!cb.is_triggered());
608
609        // A=6, should trigger; triggering frame is first post-trigger
610        cb.push(make_array_with_attrs(2, 6.0, 0.0));
611        assert!(cb.is_triggered());
612
613        let done = cb.push(make_array(3));
614        assert!(done);
615
616        let captured = cb.take_captured();
617        // 1 pre (id=1) + 2 post (id=2 triggering frame + id=3)
618        assert_eq!(captured.len(), 3);
619        assert_eq!(captured[0].unique_id, 1);
620        assert_eq!(captured[1].unique_id, 2);
621        assert_eq!(captured[2].unique_id, 3);
622    }
623
624    #[test]
625    fn test_calc_expression_parse() {
626        // Simple comparison
627        let expr = CalcExpression::parse("A>5").unwrap();
628        assert_eq!(expr.evaluate(6.0, 0.0), 1.0);
629        assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
630        assert_eq!(expr.evaluate(5.0, 0.0), 0.0); // not >=
631
632        // Greater-or-equal
633        let expr = CalcExpression::parse("A>=5").unwrap();
634        assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
635        assert_eq!(expr.evaluate(4.9, 0.0), 0.0);
636
637        // Logical AND with two variables
638        let expr = CalcExpression::parse("A>3&&B<10").unwrap();
639        assert_eq!(expr.evaluate(4.0, 5.0), 1.0);
640        assert_eq!(expr.evaluate(2.0, 5.0), 0.0);
641        assert_eq!(expr.evaluate(4.0, 15.0), 0.0);
642
643        // Parenthesized OR
644        let expr = CalcExpression::parse("(A>10)||(B>10)").unwrap();
645        assert_eq!(expr.evaluate(11.0, 0.0), 1.0);
646        assert_eq!(expr.evaluate(0.0, 11.0), 1.0);
647        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
648
649        // Not-equal
650        let expr = CalcExpression::parse("A!=0").unwrap();
651        assert_eq!(expr.evaluate(1.0, 0.0), 1.0);
652        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
653
654        // Equality
655        let expr = CalcExpression::parse("A==B").unwrap();
656        assert_eq!(expr.evaluate(5.0, 5.0), 1.0);
657        assert_eq!(expr.evaluate(5.0, 6.0), 0.0);
658
659        // Not operator
660        let expr = CalcExpression::parse("!A").unwrap();
661        assert_eq!(expr.evaluate(0.0, 0.0), 1.0);
662        assert_eq!(expr.evaluate(1.0, 0.0), 0.0);
663
664        // The full EPICS calc engine treats single '=' as equality (like '==')
665        // and single '&' as bitwise AND, so both are valid expressions.
666        let expr = CalcExpression::parse("A=5").unwrap();
667        assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
668        assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
669
670        let expr = CalcExpression::parse("A&B").unwrap();
671        // 3 & 1 = 1 (bitwise AND)
672        assert_eq!(expr.evaluate(3.0, 1.0), 1.0);
673
674        // Test math functions supported by the full calc engine
675        let expr = CalcExpression::parse("ABS(A)").unwrap();
676        assert_eq!(expr.evaluate(-5.0, 0.0), 5.0);
677
678        let expr = CalcExpression::parse("SQRT(A)").unwrap();
679        assert!((expr.evaluate(9.0, 0.0) - 3.0).abs() < 1e-10);
680
681        let expr = CalcExpression::parse("A+B").unwrap();
682        assert_eq!(expr.evaluate(3.0, 4.0), 7.0);
683
684        let expr = CalcExpression::parse("A-B").unwrap();
685        assert_eq!(expr.evaluate(10.0, 3.0), 7.0);
686
687        let expr = CalcExpression::parse("A*B").unwrap();
688        assert_eq!(expr.evaluate(3.0, 4.0), 12.0);
689
690        let expr = CalcExpression::parse("A/B").unwrap();
691        assert_eq!(expr.evaluate(12.0, 4.0), 3.0);
692
693        // Test variables C through F using evaluate_vars
694        let expr = CalcExpression::parse("A>5&&C>0").unwrap();
695        let mut vars = [0.0f64; 16];
696        vars[0] = 6.0; // A
697        vars[2] = 1.0; // C
698        assert_eq!(expr.evaluate_vars(&vars), 1.0);
699        vars[2] = 0.0; // C=0 should fail the condition
700        assert_eq!(expr.evaluate_vars(&vars), 0.0);
701
702        // Invalid expression returns None
703        assert!(CalcExpression::parse("@@@").is_none());
704    }
705
706    #[test]
707    fn test_preset_trigger_count() {
708        let mut cb = CircularBuffer::new(1, 1, TriggerCondition::External);
709        cb.set_preset_trigger_count(2);
710
711        assert_eq!(cb.status(), BufferStatus::Idle);
712
713        // First push transitions to BufferFilling
714        cb.push(make_array(1));
715        assert_eq!(cb.status(), BufferStatus::BufferFilling);
716
717        // First trigger
718        cb.trigger();
719        assert_eq!(cb.trigger_count(), 1);
720        assert_eq!(cb.status(), BufferStatus::Flushing);
721
722        let done = cb.push(make_array(2));
723        assert!(done);
724        assert_eq!(cb.status(), BufferStatus::BufferFilling); // back to filling after first capture
725
726        cb.take_captured();
727
728        // Refill buffer
729        cb.push(make_array(3));
730
731        // Second trigger — should reach preset count
732        cb.trigger();
733        assert_eq!(cb.trigger_count(), 2);
734        assert_eq!(cb.status(), BufferStatus::Flushing);
735
736        let done = cb.push(make_array(4));
737        assert!(done);
738        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
739
740        cb.take_captured();
741
742        // Further frames should be ignored
743        let done = cb.push(make_array(5));
744        assert!(!done);
745        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
746
747        // Further triggers should be ignored
748        cb.trigger();
749        assert_eq!(cb.trigger_count(), 2); // unchanged
750    }
751
752    #[test]
753    fn test_buffer_status_transitions() {
754        let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
755
756        // Initial state
757        assert_eq!(cb.status(), BufferStatus::Idle);
758
759        // First push -> BufferFilling
760        cb.push(make_array(1));
761        assert_eq!(cb.status(), BufferStatus::BufferFilling);
762
763        cb.push(make_array(2));
764        assert_eq!(cb.status(), BufferStatus::BufferFilling);
765
766        // Trigger -> Flushing
767        cb.trigger();
768        assert_eq!(cb.status(), BufferStatus::Flushing);
769
770        // Post-trigger capture completes -> back to BufferFilling
771        let done = cb.push(make_array(3));
772        assert!(done);
773        assert_eq!(cb.status(), BufferStatus::BufferFilling);
774
775        // Reset -> Idle
776        cb.reset();
777        assert_eq!(cb.status(), BufferStatus::Idle);
778        assert_eq!(cb.trigger_count(), 0);
779    }
780}