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 U).
42    ///
43    /// `vars` is indexed 0=A, 1=B, 2=C, ... up to `CALC_NARGS - 1` = U.
44    pub fn evaluate_vars(&self, vars: &[f64; calc::CALC_NARGS]) -> 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/// Result of pushing a frame: which frames to forward downstream now, and
78/// whether a capture sequence completed on this push.
79#[derive(Debug, Default)]
80pub struct PushResult {
81    /// Frames to forward downstream immediately, in order.
82    pub forward: Vec<Arc<NDArray>>,
83    /// True if the post-trigger count was reached on this push.
84    pub sequence_done: bool,
85}
86
87/// Circular buffer state for pre/post-trigger capture.
88pub struct CircularBuffer {
89    pub(crate) pre_count: usize,
90    pub(crate) post_count: usize,
91    buffer: VecDeque<Arc<NDArray>>,
92    pub(crate) trigger_condition: TriggerCondition,
93    triggered: bool,
94    /// Number of post-trigger frames forwarded so far for the current trigger.
95    post_done: usize,
96    /// True once the pre-buffer has been flushed for the current trigger.
97    pre_flushed: bool,
98    /// Frames captured for the current sequence (pre + post), for callers
99    /// that want the batch via [`CircularBuffer::take_captured`].
100    captured: Vec<Arc<NDArray>>,
101    /// Maximum number of triggers before stopping (0 = unlimited).
102    preset_trigger_count: usize,
103    /// Number of triggers fired so far.
104    trigger_count: usize,
105    /// If true, flush buffer immediately on soft trigger.
106    flush_on_soft_trigger: bool,
107    /// Current buffer status.
108    pub(crate) status: BufferStatus,
109}
110
111impl CircularBuffer {
112    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
113        Self {
114            pre_count,
115            post_count,
116            buffer: VecDeque::with_capacity(pre_count + 1),
117            trigger_condition: condition,
118            triggered: false,
119            post_done: 0,
120            pre_flushed: false,
121            captured: Vec::new(),
122            preset_trigger_count: 0,
123            trigger_count: 0,
124            flush_on_soft_trigger: false,
125            status: BufferStatus::Idle,
126        }
127    }
128
129    /// Set the preset trigger count (0 = unlimited).
130    pub fn set_preset_trigger_count(&mut self, count: usize) {
131        self.preset_trigger_count = count;
132    }
133
134    /// Get the current trigger count.
135    pub fn trigger_count(&self) -> usize {
136        self.trigger_count
137    }
138
139    /// Get the current buffer status.
140    pub fn status(&self) -> BufferStatus {
141        self.status
142    }
143
144    /// Set flush_on_soft_trigger flag.
145    pub fn set_flush_on_soft_trigger(&mut self, flush: bool) {
146        self.flush_on_soft_trigger = flush;
147    }
148
149    /// Push an array into the circular buffer.
150    ///
151    /// Mirrors C++ `NDPluginCircularBuff::processCallbacks`: on the frame that
152    /// triggers, the pre-buffer is flushed immediately and the triggering
153    /// frame is forwarded as the first post-trigger frame; each subsequent
154    /// post-trigger frame is forwarded individually. The returned
155    /// [`PushResult::forward`] holds the frames to send downstream this call.
156    pub fn push(&mut self, array: Arc<NDArray>) -> PushResult {
157        let mut result = PushResult::default();
158
159        // If acquisition is completed, ignore new frames
160        if self.status == BufferStatus::AcquisitionCompleted {
161            return result;
162        }
163
164        // Transition from Idle to BufferFilling on first push
165        if self.status == BufferStatus::Idle {
166            self.status = BufferStatus::BufferFilling;
167        }
168
169        if self.triggered {
170            // Post-trigger capture (Flushing state).
171            // Flush the pre-buffer once, before the first post-trigger frame.
172            if !self.pre_flushed {
173                self.pre_flushed = true;
174                let pre: Vec<_> = self.buffer.drain(..).collect();
175                self.captured.extend(pre.iter().cloned());
176                result.forward.extend(pre);
177            }
178            // C++ increments currentPostCount, forwards the frame, then tests
179            // `currentPostCount >= postCount`.
180            self.captured.push(Arc::clone(&array));
181            result.forward.push(array);
182            self.post_done += 1;
183            if self.post_done >= self.post_count {
184                self.complete_sequence(&mut result);
185            }
186            return result;
187        }
188
189        // Check trigger condition BEFORE adding to pre-buffer,
190        // so the triggering frame becomes the first post-trigger frame.
191        let trigger = match &self.trigger_condition {
192            TriggerCondition::AttributeThreshold { name, threshold } => array
193                .attributes
194                .get(name)
195                .and_then(|a| a.value.as_f64())
196                .map(|v| v >= *threshold)
197                .unwrap_or(false),
198            TriggerCondition::External => false,
199            TriggerCondition::Calc {
200                attr_a,
201                attr_b,
202                expression,
203            } => {
204                let a = array
205                    .attributes
206                    .get(attr_a)
207                    .and_then(|a| a.value.as_f64())
208                    .unwrap_or(f64::NAN);
209                let b = array
210                    .attributes
211                    .get(attr_b)
212                    .and_then(|a| a.value.as_f64())
213                    .unwrap_or(f64::NAN);
214                // C++ passes: A=attrValueA, B=attrValueB, C=preTrigger,
215                // D=postTrigger, E=currentImage, F=triggered
216                let mut vars = [0.0f64; calc::CALC_NARGS];
217                vars[0] = a; // A
218                vars[1] = b; // B
219                vars[2] = self.pre_count as f64; // C
220                vars[3] = self.post_count as f64; // D
221                vars[4] = self.buffer.len() as f64; // E (currentImage)
222                vars[5] = if self.triggered { 1.0 } else { 0.0 }; // F
223                expression.evaluate_vars(&vars) != 0.0
224            }
225        };
226
227        if trigger {
228            // Trigger fires before adding this frame to the pre-buffer,
229            // so the triggering frame will be the first post-trigger frame.
230            self.trigger();
231            // Flush the pre-buffer immediately, then forward the triggering
232            // frame as the first post-trigger frame (C++ flushPreBuffer +
233            // doCallbacksGenericPointer of the trigger frame).
234            self.pre_flushed = true;
235            let pre: Vec<_> = self.buffer.drain(..).collect();
236            self.captured.extend(pre.iter().cloned());
237            result.forward.extend(pre);
238            self.captured.push(Arc::clone(&array));
239            result.forward.push(array);
240            self.post_done += 1;
241            if self.post_done >= self.post_count {
242                self.complete_sequence(&mut result);
243            }
244            return result;
245        }
246
247        // Maintain pre-trigger ring buffer
248        self.buffer.push_back(array);
249        if self.buffer.len() > self.pre_count {
250            self.buffer.pop_front();
251        }
252
253        result
254    }
255
256    /// Finalize a completed post-trigger sequence (C++
257    /// `currentPostCount >= postCount` branch): advance status / trigger
258    /// bookkeeping and signal completion.
259    fn complete_sequence(&mut self, result: &mut PushResult) {
260        self.triggered = false;
261        self.pre_flushed = false;
262        self.post_done = 0;
263        if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count {
264            self.status = BufferStatus::AcquisitionCompleted;
265        } else {
266            self.status = BufferStatus::BufferFilling;
267        }
268        result.sequence_done = true;
269    }
270
271    /// External trigger.
272    pub fn trigger(&mut self) {
273        // Don't trigger if acquisition already completed
274        if self.status == BufferStatus::AcquisitionCompleted {
275            return;
276        }
277
278        self.triggered = true;
279        self.post_done = 0;
280        self.pre_flushed = false;
281        self.trigger_count += 1;
282        self.status = BufferStatus::Flushing;
283        // The pre-buffer is flushed lazily on the first post-trigger push so
284        // the frames stream out in order with the post-trigger frames.
285        self.captured.clear();
286    }
287
288    /// Take the captured arrays (pre + post trigger).
289    pub fn take_captured(&mut self) -> Vec<Arc<NDArray>> {
290        std::mem::take(&mut self.captured)
291    }
292
293    pub fn is_triggered(&self) -> bool {
294        self.triggered
295    }
296
297    pub fn pre_buffer_len(&self) -> usize {
298        self.buffer.len()
299    }
300
301    pub fn reset(&mut self) {
302        self.buffer.clear();
303        self.captured.clear();
304        self.triggered = false;
305        self.post_done = 0;
306        self.pre_flushed = false;
307        self.trigger_count = 0;
308        self.status = BufferStatus::Idle;
309    }
310}
311
312// --- New CircularBuffProcessor (NDPluginProcess-based) ---
313
314/// CircularBuff processor: maintains ring buffer state, emits captured arrays on trigger.
315#[derive(Default)]
316struct CBParamIndices {
317    control: Option<usize>,
318    status: Option<usize>,
319    trigger_a: Option<usize>,
320    trigger_b: Option<usize>,
321    trigger_a_val: Option<usize>,
322    trigger_b_val: Option<usize>,
323    trigger_calc: Option<usize>,
324    trigger_calc_val: Option<usize>,
325    pre_trigger: Option<usize>,
326    post_trigger: Option<usize>,
327    current_image: Option<usize>,
328    post_count: Option<usize>,
329    soft_trigger: Option<usize>,
330    triggered: Option<usize>,
331    preset_trigger_count: Option<usize>,
332    actual_trigger_count: Option<usize>,
333    flush_on_soft_trigger: Option<usize>,
334}
335
336pub struct CircularBuffProcessor {
337    buffer: CircularBuffer,
338    params: CBParamIndices,
339    // cached trigger attribute names and calc expression
340    trigger_a_name: String,
341    trigger_b_name: String,
342    trigger_calc_expr: String,
343}
344
345impl CircularBuffProcessor {
346    pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
347        Self {
348            buffer: CircularBuffer::new(pre_count, post_count, condition),
349            params: CBParamIndices::default(),
350            trigger_a_name: String::new(),
351            trigger_b_name: String::new(),
352            trigger_calc_expr: String::new(),
353        }
354    }
355
356    pub fn trigger(&mut self) {
357        self.buffer.trigger();
358    }
359
360    pub fn buffer(&self) -> &CircularBuffer {
361        &self.buffer
362    }
363
364    /// Rebuild the trigger condition from cached attribute names and calc expression.
365    fn rebuild_trigger_condition(&mut self) {
366        if !self.trigger_calc_expr.is_empty() {
367            if let Some(expr) = CalcExpression::parse(&self.trigger_calc_expr) {
368                self.buffer.trigger_condition = TriggerCondition::Calc {
369                    attr_a: self.trigger_a_name.clone(),
370                    attr_b: self.trigger_b_name.clone(),
371                    expression: expr,
372                };
373                return;
374            }
375        }
376        if !self.trigger_a_name.is_empty() {
377            self.buffer.trigger_condition = TriggerCondition::AttributeThreshold {
378                name: self.trigger_a_name.clone(),
379                threshold: 0.5,
380            };
381        } else {
382            self.buffer.trigger_condition = TriggerCondition::External;
383        }
384    }
385}
386
387impl NDPluginProcess for CircularBuffProcessor {
388    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
389        use ad_core_rs::plugin::runtime::ParamUpdate;
390
391        let push_result = self.buffer.push(Arc::new(array.clone()));
392
393        let mut updates = Vec::new();
394        if let Some(idx) = self.params.status {
395            let status_val = match self.buffer.status() {
396                BufferStatus::Idle => 0,
397                BufferStatus::BufferFilling => 1,
398                BufferStatus::Flushing => 2,
399                BufferStatus::AcquisitionCompleted => 3,
400            };
401            updates.push(ParamUpdate::int32(idx, status_val));
402        }
403        if let Some(idx) = self.params.current_image {
404            updates.push(ParamUpdate::int32(idx, self.buffer.pre_buffer_len() as i32));
405        }
406        if let Some(idx) = self.params.triggered {
407            updates.push(ParamUpdate::int32(
408                idx,
409                if self.buffer.is_triggered() { 1 } else { 0 },
410            ));
411        }
412        if let Some(idx) = self.params.actual_trigger_count {
413            updates.push(ParamUpdate::int32(idx, self.buffer.trigger_count() as i32));
414        }
415
416        // Stream frames downstream as the C++ plugin does: pre-buffer frames
417        // are flushed at the trigger and each post-trigger frame is forwarded
418        // immediately, rather than being withheld until the sequence ends.
419        if push_result.forward.is_empty() {
420            ProcessResult::sink(updates)
421        } else {
422            let mut result = ProcessResult::arrays(push_result.forward);
423            result.param_updates = updates;
424            result
425        }
426    }
427
428    fn plugin_type(&self) -> &str {
429        "NDPluginCircularBuff"
430    }
431
432    fn register_params(
433        &mut self,
434        base: &mut asyn_rs::port::PortDriverBase,
435    ) -> asyn_rs::error::AsynResult<()> {
436        use asyn_rs::param::ParamType;
437        base.create_param("CIRC_BUFF_CONTROL", ParamType::Int32)?;
438        base.create_param("CIRC_BUFF_STATUS", ParamType::Int32)?;
439        base.create_param("CIRC_BUFF_TRIGGER_A", ParamType::Octet)?;
440        base.create_param("CIRC_BUFF_TRIGGER_B", ParamType::Octet)?;
441        base.create_param("CIRC_BUFF_TRIGGER_A_VAL", ParamType::Float64)?;
442        base.create_param("CIRC_BUFF_TRIGGER_B_VAL", ParamType::Float64)?;
443        base.create_param("CIRC_BUFF_TRIGGER_CALC", ParamType::Octet)?;
444        base.create_param("CIRC_BUFF_TRIGGER_CALC_VAL", ParamType::Float64)?;
445        base.create_param("CIRC_BUFF_PRE_TRIGGER", ParamType::Int32)?;
446        base.create_param("CIRC_BUFF_POST_TRIGGER", ParamType::Int32)?;
447        base.create_param("CIRC_BUFF_CURRENT_IMAGE", ParamType::Int32)?;
448        base.create_param("CIRC_BUFF_POST_COUNT", ParamType::Int32)?;
449        base.create_param("CIRC_BUFF_SOFT_TRIGGER", ParamType::Int32)?;
450        base.create_param("CIRC_BUFF_TRIGGERED", ParamType::Int32)?;
451        base.create_param("CIRC_BUFF_PRESET_TRIGGER_COUNT", ParamType::Int32)?;
452        base.create_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT", ParamType::Int32)?;
453        base.create_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER", ParamType::Int32)?;
454
455        self.params.control = base.find_param("CIRC_BUFF_CONTROL");
456        self.params.status = base.find_param("CIRC_BUFF_STATUS");
457        self.params.trigger_a = base.find_param("CIRC_BUFF_TRIGGER_A");
458        self.params.trigger_b = base.find_param("CIRC_BUFF_TRIGGER_B");
459        self.params.trigger_a_val = base.find_param("CIRC_BUFF_TRIGGER_A_VAL");
460        self.params.trigger_b_val = base.find_param("CIRC_BUFF_TRIGGER_B_VAL");
461        self.params.trigger_calc = base.find_param("CIRC_BUFF_TRIGGER_CALC");
462        self.params.trigger_calc_val = base.find_param("CIRC_BUFF_TRIGGER_CALC_VAL");
463        self.params.pre_trigger = base.find_param("CIRC_BUFF_PRE_TRIGGER");
464        self.params.post_trigger = base.find_param("CIRC_BUFF_POST_TRIGGER");
465        self.params.current_image = base.find_param("CIRC_BUFF_CURRENT_IMAGE");
466        self.params.post_count = base.find_param("CIRC_BUFF_POST_COUNT");
467        self.params.soft_trigger = base.find_param("CIRC_BUFF_SOFT_TRIGGER");
468        self.params.triggered = base.find_param("CIRC_BUFF_TRIGGERED");
469        self.params.preset_trigger_count = base.find_param("CIRC_BUFF_PRESET_TRIGGER_COUNT");
470        self.params.actual_trigger_count = base.find_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT");
471        self.params.flush_on_soft_trigger = base.find_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER");
472        Ok(())
473    }
474
475    fn on_param_change(
476        &mut self,
477        reason: usize,
478        params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
479    ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
480        use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue};
481
482        if Some(reason) == self.params.control {
483            let v = params.value.as_i32();
484            if v == 1 {
485                // Start
486                self.buffer.reset();
487                self.buffer.status = BufferStatus::BufferFilling;
488            } else {
489                // Stop
490                self.buffer.status = BufferStatus::Idle;
491            }
492        } else if Some(reason) == self.params.pre_trigger {
493            self.buffer.pre_count = params.value.as_i32().max(0) as usize;
494        } else if Some(reason) == self.params.post_trigger {
495            self.buffer.post_count = params.value.as_i32().max(0) as usize;
496        } else if Some(reason) == self.params.preset_trigger_count {
497            self.buffer
498                .set_preset_trigger_count(params.value.as_i32().max(0) as usize);
499        } else if Some(reason) == self.params.flush_on_soft_trigger {
500            self.buffer
501                .set_flush_on_soft_trigger(params.value.as_i32() != 0);
502        } else if Some(reason) == self.params.soft_trigger {
503            if params.value.as_i32() != 0 {
504                self.buffer.trigger();
505            }
506        } else if Some(reason) == self.params.trigger_a {
507            if let ParamChangeValue::Octet(s) = &params.value {
508                self.trigger_a_name = s.clone();
509                self.rebuild_trigger_condition();
510            }
511        } else if Some(reason) == self.params.trigger_b {
512            if let ParamChangeValue::Octet(s) = &params.value {
513                self.trigger_b_name = s.clone();
514                self.rebuild_trigger_condition();
515            }
516        } else if Some(reason) == self.params.trigger_calc {
517            if let ParamChangeValue::Octet(s) = &params.value {
518                self.trigger_calc_expr = s.clone();
519                self.rebuild_trigger_condition();
520            }
521        }
522
523        ParamChangeResult::updates(vec![])
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
531    use ad_core_rs::ndarray::{NDDataType, NDDimension};
532
533    fn make_array(id: i32) -> Arc<NDArray> {
534        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
535        arr.unique_id = id;
536        Arc::new(arr)
537    }
538
539    fn make_array_with_attr(id: i32, attr_val: f64) -> Arc<NDArray> {
540        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
541        arr.unique_id = id;
542        arr.attributes.add(NDAttribute::new_static(
543            "trigger",
544            "",
545            NDAttrSource::Driver,
546            NDAttrValue::Float64(attr_val),
547        ));
548        Arc::new(arr)
549    }
550
551    fn make_array_with_attrs(id: i32, a_val: f64, b_val: f64) -> Arc<NDArray> {
552        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
553        arr.unique_id = id;
554        arr.attributes.add(NDAttribute::new_static(
555            "attr_a",
556            "",
557            NDAttrSource::Driver,
558            NDAttrValue::Float64(a_val),
559        ));
560        arr.attributes.add(NDAttribute::new_static(
561            "attr_b",
562            "",
563            NDAttrSource::Driver,
564            NDAttrValue::Float64(b_val),
565        ));
566        Arc::new(arr)
567    }
568
569    #[test]
570    fn test_pre_trigger_buffering() {
571        let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
572
573        for i in 0..5 {
574            cb.push(make_array(i));
575        }
576        // Pre-buffer should hold last 3
577        assert_eq!(cb.pre_buffer_len(), 3);
578    }
579
580    #[test]
581    fn test_external_trigger() {
582        let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
583
584        cb.push(make_array(1));
585        cb.push(make_array(2));
586        cb.push(make_array(3));
587        // Pre-buffer: [2, 3]
588
589        cb.trigger();
590        assert!(cb.is_triggered());
591
592        // First post-trigger push flushes the pre-buffer and forwards frame 4.
593        let r1 = cb.push(make_array(4));
594        assert!(!r1.sequence_done);
595        let ids1: Vec<_> = r1.forward.iter().map(|a| a.unique_id).collect();
596        assert_eq!(ids1, vec![2, 3, 4]); // 2 pre + frame 4
597
598        // Second post-trigger push forwards frame 5 and completes.
599        let r2 = cb.push(make_array(5));
600        assert!(r2.sequence_done);
601        let ids2: Vec<_> = r2.forward.iter().map(|a| a.unique_id).collect();
602        assert_eq!(ids2, vec![5]);
603
604        let captured = cb.take_captured();
605        assert_eq!(captured.len(), 4); // 2 pre + 2 post
606        assert_eq!(captured[0].unique_id, 2);
607        assert_eq!(captured[1].unique_id, 3);
608        assert_eq!(captured[2].unique_id, 4);
609        assert_eq!(captured[3].unique_id, 5);
610    }
611
612    #[test]
613    fn test_post_count_zero_no_underflow() {
614        // Regression: post_count == 0 must complete the sequence on the first
615        // post-trigger frame instead of underflowing the post counter.
616        let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
617        cb.push(make_array(1));
618        cb.push(make_array(2));
619        cb.trigger();
620        assert!(cb.is_triggered());
621
622        // First frame after the trigger: pre-buffer flushed + this frame,
623        // and the sequence completes immediately (postCount == 0).
624        let r = cb.push(make_array(3));
625        assert!(r.sequence_done);
626        let ids: Vec<_> = r.forward.iter().map(|a| a.unique_id).collect();
627        assert_eq!(ids, vec![1, 2, 3]);
628        assert!(!cb.is_triggered());
629        assert_eq!(cb.status(), BufferStatus::BufferFilling);
630
631        // No panic / no 2^64 capture; further frames just fill the pre-buffer.
632        let r2 = cb.push(make_array(4));
633        assert!(!r2.sequence_done);
634        assert!(r2.forward.is_empty());
635    }
636
637    #[test]
638    fn test_attribute_trigger_post_count_zero() {
639        // post_count == 0 with an attribute trigger: the triggering frame is
640        // forwarded and the sequence completes on the same push.
641        let mut cb = CircularBuffer::new(
642            1,
643            0,
644            TriggerCondition::AttributeThreshold {
645                name: "trigger".into(),
646                threshold: 5.0,
647            },
648        );
649        cb.push(make_array_with_attr(1, 1.0));
650        let r = cb.push(make_array_with_attr(2, 9.0));
651        assert!(r.sequence_done);
652        let ids: Vec<_> = r.forward.iter().map(|a| a.unique_id).collect();
653        assert_eq!(ids, vec![1, 2]); // 1 pre + triggering frame
654        assert!(!cb.is_triggered());
655    }
656
657    #[test]
658    fn test_attribute_trigger() {
659        let mut cb = CircularBuffer::new(
660            1,
661            2,
662            TriggerCondition::AttributeThreshold {
663                name: "trigger".into(),
664                threshold: 5.0,
665            },
666        );
667
668        cb.push(make_array_with_attr(1, 1.0));
669        cb.push(make_array_with_attr(2, 2.0));
670        assert!(!cb.is_triggered());
671
672        // This should trigger (attr >= 5.0); triggering frame is first post-trigger
673        let r3 = cb.push(make_array_with_attr(3, 5.0));
674        assert!(cb.is_triggered());
675        // Pre-buffer (id=2) flushed + triggering frame (id=3) forwarded now.
676        let ids3: Vec<_> = r3.forward.iter().map(|a| a.unique_id).collect();
677        assert_eq!(ids3, vec![2, 3]);
678
679        let r4 = cb.push(make_array(4));
680        assert!(r4.sequence_done);
681
682        let captured = cb.take_captured();
683        // 1 pre (id=2) + 2 post (id=3 triggering frame + id=4)
684        assert_eq!(captured.len(), 3);
685        assert_eq!(captured[0].unique_id, 2);
686        assert_eq!(captured[1].unique_id, 3);
687        assert_eq!(captured[2].unique_id, 4);
688    }
689
690    // --- New tests ---
691
692    #[test]
693    fn test_calc_trigger() {
694        // Expression: "A>5" — trigger when attribute A exceeds 5
695        let expr = CalcExpression::parse("A>5").unwrap();
696        let mut cb = CircularBuffer::new(
697            1,
698            2,
699            TriggerCondition::Calc {
700                attr_a: "attr_a".into(),
701                attr_b: "attr_b".into(),
702                expression: expr,
703            },
704        );
705
706        // A=3, should not trigger
707        cb.push(make_array_with_attrs(1, 3.0, 0.0));
708        assert!(!cb.is_triggered());
709
710        // A=6, should trigger; triggering frame is first post-trigger
711        cb.push(make_array_with_attrs(2, 6.0, 0.0));
712        assert!(cb.is_triggered());
713
714        let done = cb.push(make_array(3));
715        assert!(done.sequence_done);
716
717        let captured = cb.take_captured();
718        // 1 pre (id=1) + 2 post (id=2 triggering frame + id=3)
719        assert_eq!(captured.len(), 3);
720        assert_eq!(captured[0].unique_id, 1);
721        assert_eq!(captured[1].unique_id, 2);
722        assert_eq!(captured[2].unique_id, 3);
723    }
724
725    #[test]
726    fn test_calc_expression_parse() {
727        // Simple comparison
728        let expr = CalcExpression::parse("A>5").unwrap();
729        assert_eq!(expr.evaluate(6.0, 0.0), 1.0);
730        assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
731        assert_eq!(expr.evaluate(5.0, 0.0), 0.0); // not >=
732
733        // Greater-or-equal
734        let expr = CalcExpression::parse("A>=5").unwrap();
735        assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
736        assert_eq!(expr.evaluate(4.9, 0.0), 0.0);
737
738        // Logical AND with two variables
739        let expr = CalcExpression::parse("A>3&&B<10").unwrap();
740        assert_eq!(expr.evaluate(4.0, 5.0), 1.0);
741        assert_eq!(expr.evaluate(2.0, 5.0), 0.0);
742        assert_eq!(expr.evaluate(4.0, 15.0), 0.0);
743
744        // Parenthesized OR
745        let expr = CalcExpression::parse("(A>10)||(B>10)").unwrap();
746        assert_eq!(expr.evaluate(11.0, 0.0), 1.0);
747        assert_eq!(expr.evaluate(0.0, 11.0), 1.0);
748        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
749
750        // Not-equal
751        let expr = CalcExpression::parse("A!=0").unwrap();
752        assert_eq!(expr.evaluate(1.0, 0.0), 1.0);
753        assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
754
755        // Equality
756        let expr = CalcExpression::parse("A==B").unwrap();
757        assert_eq!(expr.evaluate(5.0, 5.0), 1.0);
758        assert_eq!(expr.evaluate(5.0, 6.0), 0.0);
759
760        // Not operator
761        let expr = CalcExpression::parse("!A").unwrap();
762        assert_eq!(expr.evaluate(0.0, 0.0), 1.0);
763        assert_eq!(expr.evaluate(1.0, 0.0), 0.0);
764
765        // The full EPICS calc engine treats single '=' as equality (like '==')
766        // and single '&' as bitwise AND, so both are valid expressions.
767        let expr = CalcExpression::parse("A=5").unwrap();
768        assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
769        assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
770
771        let expr = CalcExpression::parse("A&B").unwrap();
772        // 3 & 1 = 1 (bitwise AND)
773        assert_eq!(expr.evaluate(3.0, 1.0), 1.0);
774
775        // Test math functions supported by the full calc engine
776        let expr = CalcExpression::parse("ABS(A)").unwrap();
777        assert_eq!(expr.evaluate(-5.0, 0.0), 5.0);
778
779        let expr = CalcExpression::parse("SQRT(A)").unwrap();
780        assert!((expr.evaluate(9.0, 0.0) - 3.0).abs() < 1e-10);
781
782        let expr = CalcExpression::parse("A+B").unwrap();
783        assert_eq!(expr.evaluate(3.0, 4.0), 7.0);
784
785        let expr = CalcExpression::parse("A-B").unwrap();
786        assert_eq!(expr.evaluate(10.0, 3.0), 7.0);
787
788        let expr = CalcExpression::parse("A*B").unwrap();
789        assert_eq!(expr.evaluate(3.0, 4.0), 12.0);
790
791        let expr = CalcExpression::parse("A/B").unwrap();
792        assert_eq!(expr.evaluate(12.0, 4.0), 3.0);
793
794        // Test variables C through F using evaluate_vars
795        let expr = CalcExpression::parse("A>5&&C>0").unwrap();
796        let mut vars = [0.0f64; calc::CALC_NARGS];
797        vars[0] = 6.0; // A
798        vars[2] = 1.0; // C
799        assert_eq!(expr.evaluate_vars(&vars), 1.0);
800        vars[2] = 0.0; // C=0 should fail the condition
801        assert_eq!(expr.evaluate_vars(&vars), 0.0);
802
803        // Invalid expression returns None
804        assert!(CalcExpression::parse("@@@").is_none());
805    }
806
807    #[test]
808    fn test_preset_trigger_count() {
809        let mut cb = CircularBuffer::new(1, 1, TriggerCondition::External);
810        cb.set_preset_trigger_count(2);
811
812        assert_eq!(cb.status(), BufferStatus::Idle);
813
814        // First push transitions to BufferFilling
815        cb.push(make_array(1));
816        assert_eq!(cb.status(), BufferStatus::BufferFilling);
817
818        // First trigger
819        cb.trigger();
820        assert_eq!(cb.trigger_count(), 1);
821        assert_eq!(cb.status(), BufferStatus::Flushing);
822
823        let done = cb.push(make_array(2));
824        assert!(done.sequence_done);
825        assert_eq!(cb.status(), BufferStatus::BufferFilling); // back to filling after first capture
826
827        cb.take_captured();
828
829        // Refill buffer
830        cb.push(make_array(3));
831
832        // Second trigger — should reach preset count
833        cb.trigger();
834        assert_eq!(cb.trigger_count(), 2);
835        assert_eq!(cb.status(), BufferStatus::Flushing);
836
837        let done = cb.push(make_array(4));
838        assert!(done.sequence_done);
839        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
840
841        cb.take_captured();
842
843        // Further frames should be ignored
844        let done = cb.push(make_array(5));
845        assert!(!done.sequence_done);
846        assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
847
848        // Further triggers should be ignored
849        cb.trigger();
850        assert_eq!(cb.trigger_count(), 2); // unchanged
851    }
852
853    #[test]
854    fn test_buffer_status_transitions() {
855        let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
856
857        // Initial state
858        assert_eq!(cb.status(), BufferStatus::Idle);
859
860        // First push -> BufferFilling
861        cb.push(make_array(1));
862        assert_eq!(cb.status(), BufferStatus::BufferFilling);
863
864        cb.push(make_array(2));
865        assert_eq!(cb.status(), BufferStatus::BufferFilling);
866
867        // Trigger -> Flushing
868        cb.trigger();
869        assert_eq!(cb.status(), BufferStatus::Flushing);
870
871        // Post-trigger capture completes -> back to BufferFilling
872        let done = cb.push(make_array(3));
873        assert!(done.sequence_done);
874        assert_eq!(cb.status(), BufferStatus::BufferFilling);
875
876        // Reset -> Idle
877        cb.reset();
878        assert_eq!(cb.status(), BufferStatus::Idle);
879        assert_eq!(cb.trigger_count(), 0);
880    }
881}