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#[derive(Debug, Clone)]
18pub struct CalcExpression {
19 compiled: calc::CompiledExpr,
20}
21
22impl CalcExpression {
23 pub fn parse(expr: &str) -> Option<CalcExpression> {
27 calc::compile(expr)
28 .ok()
29 .map(|compiled| CalcExpression { compiled })
30 }
31
32 pub fn evaluate(&self, a: f64, b: f64) -> f64 {
35 let mut inputs = calc::NumericInputs::new();
36 inputs.vars[0] = a; inputs.vars[1] = b; calc::eval(&self.compiled, &mut inputs).unwrap_or(0.0)
39 }
40
41 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#[derive(Debug, Clone)]
52pub enum TriggerCondition {
53 AttributeThreshold { name: String, threshold: f64 },
55 External,
57 Calc {
62 attr_a: String,
63 attr_b: String,
64 expression: CalcExpression,
65 },
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum BufferStatus {
71 Idle,
72 BufferFilling,
73 Flushing,
74 AcquisitionCompleted,
75}
76
77#[derive(Debug, Clone, Copy)]
80pub struct TriggerValues {
81 pub a: f64,
83 pub b: f64,
85 pub calc: f64,
87}
88
89#[derive(Debug, Default, Clone, PartialEq, Eq)]
107pub struct FrameParams {
108 pub triggered: Option<i32>,
111 pub current_image: Option<i32>,
114 pub post_count: Option<i32>,
117 pub actual_trigger_count: Option<i32>,
120 pub soft_trigger: Option<i32>,
123 pub control: Option<i32>,
126 pub status: Option<&'static str>,
129}
130
131#[derive(Debug, Default)]
135pub struct PushResult {
136 pub forward: Vec<Arc<NDArray>>,
138 pub sequence_done: bool,
140 pub trigger_values: Option<TriggerValues>,
144 pub params: FrameParams,
146}
147
148pub struct CircularBuffer {
150 control: bool,
163 pub(crate) pre_count: usize,
164 pub(crate) post_count: usize,
165 buffer: VecDeque<Arc<NDArray>>,
166 pub(crate) trigger_condition: TriggerCondition,
167 triggered: bool,
168 post_done: usize,
170 pre_flushed: bool,
172 preset_trigger_count: usize,
174 trigger_count: usize,
178 flush_on_soft_trigger: i32,
184 pub(crate) status: BufferStatus,
186}
187
188impl CircularBuffer {
189 pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
190 Self {
191 control: false,
194 pre_count,
195 post_count,
196 buffer: VecDeque::with_capacity(pre_count + 1),
197 trigger_condition: condition,
198 triggered: false,
199 post_done: 0,
200 pre_flushed: false,
201 preset_trigger_count: 0,
202 trigger_count: 0,
203 flush_on_soft_trigger: 0,
204 status: BufferStatus::Idle,
205 }
206 }
207
208 pub fn set_preset_trigger_count(&mut self, count: usize) {
210 self.preset_trigger_count = count;
211 }
212
213 pub fn trigger_count(&self) -> usize {
217 self.trigger_count
218 }
219
220 pub fn status(&self) -> BufferStatus {
222 self.status
223 }
224
225 pub fn set_flush_on_soft_trigger(&mut self, flush_on: i32) {
228 self.flush_on_soft_trigger = flush_on;
229 }
230
231 pub fn flushes_on_soft_trigger(&self) -> bool {
236 self.flush_on_soft_trigger > 0
237 }
238
239 pub fn start(&mut self) {
243 self.reset();
244 self.control = true;
245 self.status = BufferStatus::BufferFilling;
246 }
247
248 pub fn stop(&mut self) {
252 self.control = false;
253 self.triggered = false;
254 self.status = BufferStatus::Idle;
255 }
256
257 pub fn is_running(&self) -> bool {
261 self.control
262 }
263
264 pub fn push(&mut self, array: Arc<NDArray>) -> PushResult {
272 let mut result = PushResult::default();
273
274 if !self.control {
282 return result;
283 }
284
285 if !self.triggered {
289 let fired = self.evaluate_trigger(&array, &mut result);
290 result.params.triggered = Some(i32::from(fired));
293 if fired {
294 self.trigger();
298 }
299 }
300
301 if !self.triggered {
302 self.buffer.push_back(array);
304 if self.buffer.len() > self.pre_count {
305 self.buffer.pop_front();
306 }
307 result.params.current_image = Some(self.buffer.len() as i32);
310 if self.buffer.len() == self.pre_count {
312 result.params.status = Some(if self.pre_count > 0 {
313 "Buffer Wrapping"
314 } else {
315 "Dropping frames"
316 });
317 }
318 } else {
319 result.params.status = Some("Flushing");
322 if !self.pre_flushed {
323 result.forward.extend(self.flush_pre_buffer());
324 }
325 result.forward.push(array);
326 self.post_done += 1;
327 result.params.post_count = Some(self.post_done as i32);
328 }
329
330 if self.post_done >= self.post_count {
338 self.complete_sequence(&mut result);
339 }
340
341 result
342 }
343
344 fn evaluate_trigger(&self, array: &NDArray, result: &mut PushResult) -> bool {
349 match &self.trigger_condition {
350 TriggerCondition::AttributeThreshold { name, threshold } => array
351 .attributes
352 .get(name)
353 .and_then(|a| a.value.as_f64())
354 .map(|v| v >= *threshold)
355 .unwrap_or(false),
356 TriggerCondition::External => false,
357 TriggerCondition::Calc {
358 attr_a,
359 attr_b,
360 expression,
361 } => {
362 let a = array
363 .attributes
364 .get(attr_a)
365 .and_then(|a| a.value.as_f64())
366 .unwrap_or(f64::NAN);
367 let b = array
368 .attributes
369 .get(attr_b)
370 .and_then(|a| a.value.as_f64())
371 .unwrap_or(f64::NAN);
372 let mut vars = [0.0f64; calc::CALC_NARGS];
375 vars[0] = a; vars[1] = b; vars[2] = self.pre_count as f64; vars[3] = self.post_count as f64; vars[4] = self.buffer.len() as f64; vars[5] = if self.triggered { 1.0 } else { 0.0 }; let calc = expression.evaluate_vars(&vars);
382 result.trigger_values = Some(TriggerValues { a, b, calc });
383 calc.is_finite() && calc != 0.0
389 }
390 }
391 }
392
393 fn flush_pre_buffer(&mut self) -> Vec<Arc<NDArray>> {
401 self.pre_flushed = true;
402 self.buffer.drain(..).collect()
403 }
404
405 fn complete_sequence(&mut self, result: &mut PushResult) {
409 self.triggered = false;
410 self.pre_flushed = false;
411 self.post_done = 0;
412 self.trigger_count += 1;
416 result.params.actual_trigger_count = Some(self.trigger_count as i32);
417 if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count {
418 self.control = false;
424 self.status = BufferStatus::AcquisitionCompleted;
425 result.params.triggered = Some(0);
426 result.params.control = Some(0);
427 result.params.status = Some("Acquisition Completed");
428 } else {
429 self.status = BufferStatus::BufferFilling;
432 result.params.control = Some(1);
433 result.params.soft_trigger = Some(0);
434 result.params.triggered = Some(0);
435 result.params.post_count = Some(0);
436 result.params.status = Some(if self.pre_count > 0 {
437 "Buffer filling"
438 } else {
439 "Dropping frames"
440 });
441 }
442 result.sequence_done = true;
443 }
444
445 pub fn trigger(&mut self) {
447 if !self.control {
452 return;
453 }
454
455 self.triggered = true;
456 self.post_done = 0;
457 self.pre_flushed = false;
460 self.status = BufferStatus::Flushing;
461 }
462
463 pub fn is_triggered(&self) -> bool {
464 self.triggered
465 }
466
467 pub fn pre_buffer_len(&self) -> usize {
468 self.buffer.len()
469 }
470
471 pub fn reset(&mut self) {
474 self.control = false;
475 self.buffer.clear();
476 self.triggered = false;
477 self.post_done = 0;
478 self.pre_flushed = false;
479 self.trigger_count = 0;
480 self.status = BufferStatus::Idle;
481 }
482}
483
484#[derive(Default)]
489struct CBParamIndices {
490 control: Option<usize>,
491 status: Option<usize>,
492 trigger_a: Option<usize>,
493 trigger_b: Option<usize>,
494 trigger_a_val: Option<usize>,
495 trigger_b_val: Option<usize>,
496 trigger_calc: Option<usize>,
497 trigger_calc_val: Option<usize>,
498 pre_trigger: Option<usize>,
499 post_trigger: Option<usize>,
500 current_image: Option<usize>,
501 post_count: Option<usize>,
502 soft_trigger: Option<usize>,
503 triggered: Option<usize>,
504 preset_trigger_count: Option<usize>,
505 actual_trigger_count: Option<usize>,
506 flush_on_soft_trigger: Option<usize>,
507}
508
509pub struct CircularBuffProcessor {
510 buffer: CircularBuffer,
511 params: CBParamIndices,
512 max_buffers: usize,
516 trigger_a_name: String,
518 trigger_b_name: String,
519 trigger_calc_expr: String,
520}
521
522impl CircularBuffProcessor {
523 pub fn new(
524 pre_count: usize,
525 post_count: usize,
526 condition: TriggerCondition,
527 max_buffers: usize,
528 ) -> Self {
529 Self {
530 buffer: CircularBuffer::new(pre_count, post_count, condition),
531 params: CBParamIndices::default(),
532 max_buffers,
533 trigger_a_name: String::new(),
534 trigger_b_name: String::new(),
535 trigger_calc_expr: String::new(),
536 }
537 }
538
539 pub fn trigger(&mut self) {
540 self.buffer.trigger();
541 }
542
543 pub fn start(&mut self) {
547 self.buffer.start();
548 }
549
550 pub fn stop(&mut self) {
552 self.buffer.stop();
553 }
554
555 pub fn buffer(&self) -> &CircularBuffer {
556 &self.buffer
557 }
558
559 fn rebuild_trigger_condition(&mut self) {
561 if !self.trigger_calc_expr.is_empty() {
562 if let Some(expr) = CalcExpression::parse(&self.trigger_calc_expr) {
563 self.buffer.trigger_condition = TriggerCondition::Calc {
564 attr_a: self.trigger_a_name.clone(),
565 attr_b: self.trigger_b_name.clone(),
566 expression: expr,
567 };
568 return;
569 }
570 }
571 if !self.trigger_a_name.is_empty() {
572 self.buffer.trigger_condition = TriggerCondition::AttributeThreshold {
573 name: self.trigger_a_name.clone(),
574 threshold: 0.5,
575 };
576 } else {
577 self.buffer.trigger_condition = TriggerCondition::External;
578 }
579 }
580}
581
582impl NDPluginProcess for CircularBuffProcessor {
583 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
584 use ad_core_rs::plugin::runtime::ParamUpdate;
585
586 let push_result = self.buffer.push(Arc::new(array.clone()));
587
588 let mut updates = Vec::new();
594 let p = &push_result.params;
595 if let (Some(idx), Some(s)) = (self.params.status, p.status) {
596 updates.push(ParamUpdate::octet(idx, s.to_string()));
598 }
599 for (index, value) in [
600 (self.params.triggered, p.triggered),
601 (self.params.current_image, p.current_image),
602 (self.params.post_count, p.post_count),
603 (self.params.actual_trigger_count, p.actual_trigger_count),
604 (self.params.soft_trigger, p.soft_trigger),
605 (self.params.control, p.control),
606 ] {
607 if let (Some(idx), Some(v)) = (index, value) {
608 updates.push(ParamUpdate::int32(idx, v));
609 }
610 }
611 if let Some(tv) = push_result.trigger_values {
614 if let Some(idx) = self.params.trigger_a_val {
615 updates.push(ParamUpdate::float64(idx, tv.a));
616 }
617 if let Some(idx) = self.params.trigger_b_val {
618 updates.push(ParamUpdate::float64(idx, tv.b));
619 }
620 if let Some(idx) = self.params.trigger_calc_val {
621 updates.push(ParamUpdate::float64(idx, tv.calc));
622 }
623 }
624
625 if push_result.forward.is_empty() {
629 ProcessResult::sink(updates)
630 } else {
631 let mut result = ProcessResult::arrays(push_result.forward);
632 result.param_updates = updates;
633 result
634 }
635 }
636
637 fn plugin_type(&self) -> &str {
638 "NDPluginCircularBuff"
639 }
640
641 fn register_params(
642 &mut self,
643 base: &mut asyn_rs::port::PortDriverBase,
644 ) -> asyn_rs::error::AsynResult<()> {
645 use asyn_rs::param::ParamType;
646 base.create_param("CIRC_BUFF_CONTROL", ParamType::Int32)?;
647 base.create_param("CIRC_BUFF_STATUS", ParamType::Octet)?;
650 base.create_param("CIRC_BUFF_TRIGGER_A", ParamType::Octet)?;
651 base.create_param("CIRC_BUFF_TRIGGER_B", ParamType::Octet)?;
652 base.create_param("CIRC_BUFF_TRIGGER_A_VAL", ParamType::Float64)?;
653 base.create_param("CIRC_BUFF_TRIGGER_B_VAL", ParamType::Float64)?;
654 base.create_param("CIRC_BUFF_TRIGGER_CALC", ParamType::Octet)?;
655 base.create_param("CIRC_BUFF_TRIGGER_CALC_VAL", ParamType::Float64)?;
656 base.create_param("CIRC_BUFF_PRE_TRIGGER", ParamType::Int32)?;
657 base.create_param("CIRC_BUFF_POST_TRIGGER", ParamType::Int32)?;
658 base.create_param("CIRC_BUFF_CURRENT_IMAGE", ParamType::Int32)?;
659 base.create_param("CIRC_BUFF_POST_COUNT", ParamType::Int32)?;
660 base.create_param("CIRC_BUFF_SOFT_TRIGGER", ParamType::Int32)?;
661 base.create_param("CIRC_BUFF_TRIGGERED", ParamType::Int32)?;
662 base.create_param("CIRC_BUFF_PRESET_TRIGGER_COUNT", ParamType::Int32)?;
663 base.create_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT", ParamType::Int32)?;
664 base.create_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER", ParamType::Int32)?;
665
666 self.params.control = base.find_param("CIRC_BUFF_CONTROL");
667 self.params.status = base.find_param("CIRC_BUFF_STATUS");
668 self.params.trigger_a = base.find_param("CIRC_BUFF_TRIGGER_A");
669 self.params.trigger_b = base.find_param("CIRC_BUFF_TRIGGER_B");
670 self.params.trigger_a_val = base.find_param("CIRC_BUFF_TRIGGER_A_VAL");
671 self.params.trigger_b_val = base.find_param("CIRC_BUFF_TRIGGER_B_VAL");
672 self.params.trigger_calc = base.find_param("CIRC_BUFF_TRIGGER_CALC");
673 self.params.trigger_calc_val = base.find_param("CIRC_BUFF_TRIGGER_CALC_VAL");
674 self.params.pre_trigger = base.find_param("CIRC_BUFF_PRE_TRIGGER");
675 self.params.post_trigger = base.find_param("CIRC_BUFF_POST_TRIGGER");
676 self.params.current_image = base.find_param("CIRC_BUFF_CURRENT_IMAGE");
677 self.params.post_count = base.find_param("CIRC_BUFF_POST_COUNT");
678 self.params.soft_trigger = base.find_param("CIRC_BUFF_SOFT_TRIGGER");
679 self.params.triggered = base.find_param("CIRC_BUFF_TRIGGERED");
680 self.params.preset_trigger_count = base.find_param("CIRC_BUFF_PRESET_TRIGGER_COUNT");
681 self.params.actual_trigger_count = base.find_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT");
682 self.params.flush_on_soft_trigger = base.find_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER");
683
684 if let Some(idx) = self.params.status {
687 base.set_string_param(idx, 0, "Idle".into())?;
688 }
689 Ok(())
690 }
691
692 fn on_param_change(
693 &mut self,
694 reason: usize,
695 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
696 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
697 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
698
699 let mut updates = Vec::new();
700 if Some(reason) == self.params.control {
701 let v = params.value.as_i32();
702 if v == 1 {
703 self.buffer.start();
708 for (index, value) in [
709 (self.params.soft_trigger, 0),
710 (self.params.triggered, 0),
711 (self.params.post_count, 0),
712 (self.params.actual_trigger_count, 0),
713 ] {
714 if let Some(idx) = index {
715 updates.push(ParamUpdate::int32(idx, value));
716 }
717 }
718 if let Some(idx) = self.params.status {
721 let s = if self.buffer.pre_count > 0 {
722 "Buffer filling"
723 } else {
724 "Dropping frames"
725 };
726 updates.push(ParamUpdate::octet(idx, s.to_string()));
727 }
728 } else {
729 self.buffer.stop();
734 for (index, value) in [
735 (self.params.soft_trigger, 0),
736 (self.params.triggered, 0),
737 (self.params.current_image, 0),
738 ] {
739 if let Some(idx) = index {
740 updates.push(ParamUpdate::int32(idx, value));
741 }
742 }
743 if let Some(idx) = self.params.status {
746 updates.push(ParamUpdate::octet(idx, "Acquisition Stopped".to_string()));
747 }
748 }
749 } else if Some(reason) == self.params.pre_trigger {
750 let value = params.value.as_i32();
756 let reject_msg = if self.buffer.is_running() {
759 Some("Stop acquisition to set pre-count")
760 } else if value > self.max_buffers as i32 - 1 {
761 Some("Pre-count too high")
763 } else if value < 0 {
764 Some("Invalid pre-count value")
765 } else {
766 None
767 };
768 if let Some(msg) = reject_msg {
769 if let Some(idx) = self.params.status {
770 updates.push(ParamUpdate::octet(idx, msg.to_string()));
771 }
772 if let Some(idx) = self.params.pre_trigger {
775 updates.push(ParamUpdate::int32(idx, self.buffer.pre_count as i32));
776 }
777 } else {
778 self.buffer.pre_count = value as usize;
779 }
780 } else if Some(reason) == self.params.post_trigger {
781 self.buffer.post_count = params.value.as_i32().max(0) as usize;
782 } else if Some(reason) == self.params.preset_trigger_count {
783 self.buffer
784 .set_preset_trigger_count(params.value.as_i32().max(0) as usize);
785 } else if Some(reason) == self.params.flush_on_soft_trigger {
786 self.buffer.set_flush_on_soft_trigger(params.value.as_i32());
787 } else if Some(reason) == self.params.soft_trigger {
788 if params.value.as_i32() != 0 {
812 self.buffer.trigger();
813 if let Some(idx) = self.params.triggered {
814 updates.push(ParamUpdate::int32(idx, 1));
815 }
816 if self.buffer.flushes_on_soft_trigger() {
820 let flushed = self.buffer.flush_pre_buffer();
821 if !flushed.is_empty() {
822 return ParamChangeResult::combined(flushed, updates);
823 }
824 }
825 }
826 } else if Some(reason) == self.params.trigger_a {
827 if let ParamChangeValue::Octet(s) = ¶ms.value {
828 self.trigger_a_name = s.clone();
829 self.rebuild_trigger_condition();
830 }
831 } else if Some(reason) == self.params.trigger_b {
832 if let ParamChangeValue::Octet(s) = ¶ms.value {
833 self.trigger_b_name = s.clone();
834 self.rebuild_trigger_condition();
835 }
836 } else if Some(reason) == self.params.trigger_calc {
837 if let ParamChangeValue::Octet(s) = ¶ms.value {
838 self.trigger_calc_expr = s.clone();
839 self.rebuild_trigger_condition();
840 }
841 }
842
843 ParamChangeResult::updates(updates)
844 }
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
851 use ad_core_rs::ndarray::{NDDataType, NDDimension};
852
853 fn make_array(id: i32) -> Arc<NDArray> {
854 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
855 arr.unique_id = id;
856 Arc::new(arr)
857 }
858
859 fn make_array_with_attr(id: i32, attr_val: f64) -> Arc<NDArray> {
860 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
861 arr.unique_id = id;
862 arr.attributes.add(NDAttribute::new_static(
863 "trigger",
864 "",
865 NDAttrSource::Driver,
866 NDAttrValue::Float64(attr_val),
867 ));
868 Arc::new(arr)
869 }
870
871 fn make_array_with_attrs(id: i32, a_val: f64, b_val: f64) -> Arc<NDArray> {
872 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
873 arr.unique_id = id;
874 arr.attributes.add(NDAttribute::new_static(
875 "attr_a",
876 "",
877 NDAttrSource::Driver,
878 NDAttrValue::Float64(a_val),
879 ));
880 arr.attributes.add(NDAttribute::new_static(
881 "attr_b",
882 "",
883 NDAttrSource::Driver,
884 NDAttrValue::Float64(b_val),
885 ));
886 Arc::new(arr)
887 }
888
889 #[test]
890 fn test_pre_trigger_buffering() {
891 let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
892 cb.start(); for i in 0..5 {
895 cb.push(make_array(i));
896 }
897 assert_eq!(cb.pre_buffer_len(), 3);
899 }
900
901 #[test]
908 fn a_completed_sequence_retains_no_forwarded_frames() {
909 let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
910 cb.start();
911
912 let pre = make_array(1);
913 cb.push(Arc::clone(&pre));
914 cb.trigger();
915
916 let post = make_array(2);
917 let r1 = cb.push(Arc::clone(&post));
918 let r2 = cb.push(make_array(3));
919 assert!(r2.sequence_done);
920
921 drop(r1);
923 drop(r2);
924
925 assert_eq!(
926 Arc::strong_count(&pre),
927 1,
928 "the flushed pre-trigger frame must not be retained after forwarding"
929 );
930 assert_eq!(
931 Arc::strong_count(&post),
932 1,
933 "the forwarded post-trigger frame must not be retained"
934 );
935 }
936
937 #[test]
938 fn test_external_trigger() {
939 let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
940 cb.start(); cb.push(make_array(1));
943 cb.push(make_array(2));
944 cb.push(make_array(3));
945 cb.trigger();
948 assert!(cb.is_triggered());
949
950 let r1 = cb.push(make_array(4));
952 assert!(!r1.sequence_done);
953 let ids1: Vec<_> = r1.forward.iter().map(|a| a.unique_id).collect();
954 assert_eq!(ids1, vec![2, 3, 4]); let r2 = cb.push(make_array(5));
958 assert!(r2.sequence_done);
959 let ids2: Vec<_> = r2.forward.iter().map(|a| a.unique_id).collect();
960 assert_eq!(ids2, vec![5]);
961 }
962
963 #[test]
964 fn test_post_count_zero_no_underflow() {
965 let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
968 cb.start(); cb.push(make_array(1));
970 cb.push(make_array(2));
971 cb.trigger();
972 assert!(cb.is_triggered());
973
974 let r = cb.push(make_array(3));
977 assert!(r.sequence_done);
978 let ids: Vec<_> = r.forward.iter().map(|a| a.unique_id).collect();
979 assert_eq!(ids, vec![1, 2, 3]);
980 assert!(!cb.is_triggered());
981 assert_eq!(cb.status(), BufferStatus::BufferFilling);
982
983 let r2 = cb.push(make_array(4));
991 assert!(r2.sequence_done);
992 assert!(r2.forward.is_empty());
993 }
994
995 #[test]
996 fn test_post_count_zero_completes_on_untriggered_frame() {
997 let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
1004 cb.start(); for (n, id) in (1..=3).enumerate() {
1007 let r = cb.push(make_array(id));
1008 assert!(r.forward.is_empty(), "frame {id} must not be forwarded");
1010 assert!(r.sequence_done, "frame {id} must complete a sequence");
1012 assert_eq!(r.params.actual_trigger_count, Some(n as i32 + 1));
1013 assert_eq!(cb.trigger_count(), n + 1);
1014 assert_eq!(r.params.control, Some(1));
1016 assert_eq!(r.params.soft_trigger, Some(0));
1017 assert_eq!(r.params.triggered, Some(0));
1018 assert_eq!(r.params.post_count, Some(0));
1019 assert_eq!(r.params.status, Some("Buffer filling"));
1020 }
1021 assert_eq!(cb.pre_buffer_len(), 2);
1024
1025 let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
1028 cb.start(); let r = cb.push(make_array(1));
1030 assert!(!r.sequence_done);
1031 assert_eq!(r.params.actual_trigger_count, None);
1032 assert_eq!(cb.trigger_count(), 0);
1033 }
1034
1035 #[test]
1036 fn test_post_count_zero_untriggered_frames_reach_preset_trigger_count() {
1037 let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
1042 cb.start(); cb.set_preset_trigger_count(2);
1044
1045 let r1 = cb.push(make_array(1));
1046 assert!(r1.sequence_done);
1047 assert_eq!(r1.params.actual_trigger_count, Some(1));
1048 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1049
1050 let r2 = cb.push(make_array(2));
1051 assert_eq!(r2.params.actual_trigger_count, Some(2));
1052 assert_eq!(r2.params.control, Some(0));
1054 assert_eq!(r2.params.status, Some("Acquisition Completed"));
1055 assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
1056 }
1057
1058 #[test]
1059 fn test_attribute_trigger_post_count_zero() {
1060 let mut cb = CircularBuffer::new(
1063 1,
1064 0,
1065 TriggerCondition::AttributeThreshold {
1066 name: "trigger".into(),
1067 threshold: 5.0,
1068 },
1069 );
1070 cb.start(); cb.push(make_array_with_attr(1, 1.0));
1072 let r = cb.push(make_array_with_attr(2, 9.0));
1073 assert!(r.sequence_done);
1074 let ids: Vec<_> = r.forward.iter().map(|a| a.unique_id).collect();
1075 assert_eq!(ids, vec![1, 2]); assert!(!cb.is_triggered());
1077 }
1078
1079 #[test]
1080 fn test_attribute_trigger() {
1081 let mut cb = CircularBuffer::new(
1082 1,
1083 2,
1084 TriggerCondition::AttributeThreshold {
1085 name: "trigger".into(),
1086 threshold: 5.0,
1087 },
1088 );
1089 cb.start(); cb.push(make_array_with_attr(1, 1.0));
1092 cb.push(make_array_with_attr(2, 2.0));
1093 assert!(!cb.is_triggered());
1094
1095 let r3 = cb.push(make_array_with_attr(3, 5.0));
1097 assert!(cb.is_triggered());
1098 let ids3: Vec<_> = r3.forward.iter().map(|a| a.unique_id).collect();
1100 assert_eq!(ids3, vec![2, 3]);
1101
1102 let r4 = cb.push(make_array(4));
1103 assert!(r4.sequence_done);
1104 let ids4: Vec<_> = r4.forward.iter().map(|a| a.unique_id).collect();
1105 assert_eq!(ids4, vec![4]);
1106 }
1107
1108 #[test]
1111 fn test_calc_trigger() {
1112 let expr = CalcExpression::parse("A>5").unwrap();
1114 let mut cb = CircularBuffer::new(
1115 1,
1116 2,
1117 TriggerCondition::Calc {
1118 attr_a: "attr_a".into(),
1119 attr_b: "attr_b".into(),
1120 expression: expr,
1121 },
1122 );
1123 cb.start(); let mut forwarded: Vec<i32> = Vec::new();
1126 let mut record = |r: PushResult| {
1127 forwarded.extend(r.forward.iter().map(|a| a.unique_id));
1128 r.sequence_done
1129 };
1130
1131 record(cb.push(make_array_with_attrs(1, 3.0, 0.0)));
1133 assert!(!cb.is_triggered());
1134
1135 record(cb.push(make_array_with_attrs(2, 6.0, 0.0)));
1137 assert!(cb.is_triggered());
1138
1139 assert!(record(cb.push(make_array(3))));
1140
1141 assert_eq!(forwarded, vec![1, 2, 3]);
1143 }
1144
1145 #[test]
1146 fn test_calc_trigger_values_surface() {
1147 let expr = CalcExpression::parse("A+B").unwrap();
1150 let mut cb = CircularBuffer::new(
1153 2,
1154 3,
1155 TriggerCondition::Calc {
1156 attr_a: "attr_a".into(),
1157 attr_b: "attr_b".into(),
1158 expression: expr,
1159 },
1160 );
1161 cb.start(); let r = cb.push(make_array_with_attrs(1, 3.0, 4.0));
1165 let tv = r.trigger_values.expect("calc path surfaces trigger values");
1166 assert_eq!(tv.a, 3.0);
1167 assert_eq!(tv.b, 4.0);
1168 assert_eq!(tv.calc, 7.0);
1169
1170 let r2 = cb.push(make_array(2));
1173 assert!(r2.trigger_values.is_none());
1174 }
1175
1176 #[test]
1177 fn test_calc_trigger_values_nan_when_attr_absent() {
1178 let expr = CalcExpression::parse("A").unwrap();
1181 let mut cb = CircularBuffer::new(
1182 2,
1183 1,
1184 TriggerCondition::Calc {
1185 attr_a: "missing_a".into(),
1186 attr_b: "missing_b".into(),
1187 expression: expr,
1188 },
1189 );
1190 cb.start(); let r = cb.push(make_array(1));
1192 let tv = r.trigger_values.expect("calc path surfaces trigger values");
1193 assert!(tv.a.is_nan());
1194 assert!(tv.b.is_nan());
1195 assert!(tv.calc.is_nan());
1196 }
1197
1198 #[test]
1199 fn test_calc_trigger_skips_nan_and_inf_results() {
1200 let push_calc = |val: f64| {
1207 let expr = CalcExpression::parse("A").unwrap();
1208 let mut cb = CircularBuffer::new(
1209 2,
1210 2,
1211 TriggerCondition::Calc {
1212 attr_a: "attr_a".into(),
1213 attr_b: "attr_b".into(),
1214 expression: expr,
1215 },
1216 );
1217 cb.start(); cb.push(make_array_with_attrs(1, val, 0.0));
1219 cb.is_triggered()
1220 };
1221 assert!(!push_calc(f64::NAN));
1223 assert!(!push_calc(f64::INFINITY));
1224 assert!(!push_calc(f64::NEG_INFINITY));
1225 assert!(push_calc(1.0));
1228 assert!(!push_calc(0.0));
1229 }
1230
1231 #[test]
1232 fn test_calc_expression_parse() {
1233 let expr = CalcExpression::parse("A>5").unwrap();
1235 assert_eq!(expr.evaluate(6.0, 0.0), 1.0);
1236 assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
1237 assert_eq!(expr.evaluate(5.0, 0.0), 0.0); let expr = CalcExpression::parse("A>=5").unwrap();
1241 assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
1242 assert_eq!(expr.evaluate(4.9, 0.0), 0.0);
1243
1244 let expr = CalcExpression::parse("A>3&&B<10").unwrap();
1246 assert_eq!(expr.evaluate(4.0, 5.0), 1.0);
1247 assert_eq!(expr.evaluate(2.0, 5.0), 0.0);
1248 assert_eq!(expr.evaluate(4.0, 15.0), 0.0);
1249
1250 let expr = CalcExpression::parse("(A>10)||(B>10)").unwrap();
1252 assert_eq!(expr.evaluate(11.0, 0.0), 1.0);
1253 assert_eq!(expr.evaluate(0.0, 11.0), 1.0);
1254 assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
1255
1256 let expr = CalcExpression::parse("A!=0").unwrap();
1258 assert_eq!(expr.evaluate(1.0, 0.0), 1.0);
1259 assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
1260
1261 let expr = CalcExpression::parse("A==B").unwrap();
1263 assert_eq!(expr.evaluate(5.0, 5.0), 1.0);
1264 assert_eq!(expr.evaluate(5.0, 6.0), 0.0);
1265
1266 let expr = CalcExpression::parse("!A").unwrap();
1268 assert_eq!(expr.evaluate(0.0, 0.0), 1.0);
1269 assert_eq!(expr.evaluate(1.0, 0.0), 0.0);
1270
1271 let expr = CalcExpression::parse("A=5").unwrap();
1274 assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
1275 assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
1276
1277 let expr = CalcExpression::parse("A&B").unwrap();
1278 assert_eq!(expr.evaluate(3.0, 1.0), 1.0);
1280
1281 let expr = CalcExpression::parse("ABS(A)").unwrap();
1283 assert_eq!(expr.evaluate(-5.0, 0.0), 5.0);
1284
1285 let expr = CalcExpression::parse("SQRT(A)").unwrap();
1286 assert!((expr.evaluate(9.0, 0.0) - 3.0).abs() < 1e-10);
1287
1288 let expr = CalcExpression::parse("A+B").unwrap();
1289 assert_eq!(expr.evaluate(3.0, 4.0), 7.0);
1290
1291 let expr = CalcExpression::parse("A-B").unwrap();
1292 assert_eq!(expr.evaluate(10.0, 3.0), 7.0);
1293
1294 let expr = CalcExpression::parse("A*B").unwrap();
1295 assert_eq!(expr.evaluate(3.0, 4.0), 12.0);
1296
1297 let expr = CalcExpression::parse("A/B").unwrap();
1298 assert_eq!(expr.evaluate(12.0, 4.0), 3.0);
1299
1300 let expr = CalcExpression::parse("A>5&&C>0").unwrap();
1302 let mut vars = [0.0f64; calc::CALC_NARGS];
1303 vars[0] = 6.0; vars[2] = 1.0; assert_eq!(expr.evaluate_vars(&vars), 1.0);
1306 vars[2] = 0.0; assert_eq!(expr.evaluate_vars(&vars), 0.0);
1308
1309 assert!(CalcExpression::parse("@@@").is_none());
1311 }
1312
1313 #[test]
1314 fn test_preset_trigger_count() {
1315 let mut cb = CircularBuffer::new(1, 1, TriggerCondition::External);
1316 cb.start(); cb.set_preset_trigger_count(2);
1318
1319 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1323
1324 cb.push(make_array(1));
1325 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1326
1327 cb.trigger();
1330 assert_eq!(cb.trigger_count(), 0);
1331 assert_eq!(cb.status(), BufferStatus::Flushing);
1332
1333 let done = cb.push(make_array(2));
1334 assert!(done.sequence_done);
1335 assert_eq!(cb.trigger_count(), 1); assert_eq!(cb.status(), BufferStatus::BufferFilling); cb.push(make_array(3));
1340
1341 cb.trigger();
1343 assert_eq!(cb.trigger_count(), 1);
1344 assert_eq!(cb.status(), BufferStatus::Flushing);
1345
1346 let done = cb.push(make_array(4));
1347 assert!(done.sequence_done);
1348 assert_eq!(cb.trigger_count(), 2);
1349 assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
1350
1351 let done = cb.push(make_array(5));
1353 assert!(!done.sequence_done);
1354 assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
1355
1356 cb.trigger();
1358 assert_eq!(cb.trigger_count(), 2); }
1360
1361 #[test]
1362 fn test_stop_resets_current_image_and_status() {
1363 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1366
1367 let mut processor = CircularBuffProcessor::new(2, 1, TriggerCondition::External, 100);
1368 processor.params.control = Some(10);
1369 processor.params.current_image = Some(11);
1370 processor.params.status = Some(12);
1371
1372 let snapshot = PluginParamSnapshot {
1373 enable_callbacks: true,
1374 reason: 10,
1375 addr: 0,
1376 value: ParamChangeValue::Int32(0), };
1378 let result = processor.on_param_change(10, &snapshot);
1379
1380 assert!(
1381 result.param_updates.iter().any(|u| matches!(
1382 u,
1383 ParamUpdate::Int32 {
1384 reason: 11,
1385 value: 0,
1386 ..
1387 }
1388 )),
1389 "stop must post CURRENT_IMAGE=0"
1390 );
1391 assert!(
1392 result.param_updates.iter().any(|u| matches!(
1393 u,
1394 ParamUpdate::Octet { reason: 12, value, .. } if value == "Acquisition Stopped"
1395 )),
1396 "stop must post STATUS=Acquisition Stopped"
1397 );
1398 }
1399
1400 #[test]
1401 fn test_pre_count_validation() {
1402 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1407
1408 let make_proc = || {
1409 let mut p = CircularBuffProcessor::new(3, 1, TriggerCondition::External, 10);
1410 p.params.pre_trigger = Some(20);
1411 p.params.status = Some(12);
1412 p
1413 };
1414 let write = |p: &mut CircularBuffProcessor, v: i32| {
1415 let snap = PluginParamSnapshot {
1416 enable_callbacks: true,
1417 reason: 20,
1418 addr: 0,
1419 value: ParamChangeValue::Int32(v),
1420 };
1421 p.on_param_change(20, &snap)
1422 };
1423
1424 let mut p = make_proc();
1426 p.buffer.start();
1427 let r = write(&mut p, 7);
1428 assert_eq!(
1429 p.buffer.pre_count, 3,
1430 "reject while running, value unchanged"
1431 );
1432 assert!(r.param_updates.iter().any(|u| matches!(
1433 u,
1434 ParamUpdate::Octet { reason: 12, value, .. } if value == "Stop acquisition to set pre-count"
1435 )));
1436 assert!(r.param_updates.iter().any(|u| matches!(
1437 u,
1438 ParamUpdate::Int32 {
1439 reason: 20,
1440 value: 3,
1441 ..
1442 }
1443 )));
1444
1445 let mut p = make_proc();
1447 p.buffer.stop();
1448 let r = write(&mut p, -1);
1449 assert_eq!(p.buffer.pre_count, 3, "negative rejected, value unchanged");
1450 assert!(r.param_updates.iter().any(|u| matches!(
1451 u,
1452 ParamUpdate::Octet { reason: 12, value, .. } if value == "Invalid pre-count value"
1453 )));
1454
1455 let mut p = make_proc();
1457 p.buffer.stop();
1458 let r = write(&mut p, 10);
1459 assert_eq!(p.buffer.pre_count, 3, "too-high rejected, value unchanged");
1460 assert!(r.param_updates.iter().any(|u| matches!(
1461 u,
1462 ParamUpdate::Octet { reason: 12, value, .. } if value == "Pre-count too high"
1463 )));
1464 assert!(r.param_updates.iter().any(|u| matches!(
1465 u,
1466 ParamUpdate::Int32 {
1467 reason: 20,
1468 value: 3,
1469 ..
1470 }
1471 )));
1472
1473 let mut p = make_proc();
1475 p.buffer.stop();
1476 write(&mut p, 9);
1477 assert_eq!(p.buffer.pre_count, 9, "valid pre-count committed");
1478 }
1479
1480 #[test]
1481 fn test_frame_status_strings() {
1482 let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
1486 cb.start(); assert_eq!(cb.push(make_array(1)).params.status, None);
1488 assert_eq!(
1491 cb.push(make_array(2)).params.status,
1492 Some("Buffer Wrapping")
1493 );
1494 assert_eq!(
1495 cb.push(make_array(3)).params.status,
1496 Some("Buffer Wrapping")
1497 );
1498 cb.trigger();
1500 assert_eq!(cb.push(make_array(4)).params.status, Some("Flushing"));
1501 assert_eq!(cb.push(make_array(5)).params.status, Some("Buffer filling"));
1503
1504 let mut cb = CircularBuffer::new(0, 1, TriggerCondition::External);
1507 cb.start(); assert_eq!(
1509 cb.push(make_array(1)).params.status,
1510 Some("Dropping frames")
1511 );
1512 cb.trigger();
1513 assert_eq!(
1514 cb.push(make_array(2)).params.status,
1515 Some("Dropping frames")
1516 );
1517
1518 let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
1520 cb.start(); cb.set_preset_trigger_count(1);
1522 cb.trigger();
1523 assert_eq!(
1524 cb.push(make_array(1)).params.status,
1525 Some("Acquisition Completed")
1526 );
1527 }
1528
1529 #[test]
1530 fn test_post_count_posted_per_flushed_frame() {
1531 let mut cb = CircularBuffer::new(2, 3, TriggerCondition::External);
1537 cb.start(); assert_eq!(cb.push(make_array(1)).params.post_count, None);
1540 assert_eq!(cb.push(make_array(2)).params.post_count, None);
1541
1542 cb.trigger();
1543 assert_eq!(cb.push(make_array(3)).params.post_count, Some(1));
1544 assert_eq!(cb.push(make_array(4)).params.post_count, Some(2));
1545 assert_eq!(cb.push(make_array(5)).params.post_count, Some(0));
1548
1549 cb.trigger();
1551 assert_eq!(cb.push(make_array(6)).params.post_count, Some(1));
1552 }
1553
1554 #[test]
1555 fn test_post_count_survives_acquisition_completed() {
1556 let mut cb = CircularBuffer::new(1, 2, TriggerCondition::External);
1560 cb.start(); cb.set_preset_trigger_count(1);
1562 cb.trigger();
1563 assert_eq!(cb.push(make_array(1)).params.post_count, Some(1));
1564 let done = cb.push(make_array(2));
1565 assert_eq!(done.params.post_count, Some(2), "final count, not reset");
1566 assert_eq!(done.params.status, Some("Acquisition Completed"));
1567 assert_eq!(done.params.control, Some(0), "C turns acquisition off");
1568 }
1569
1570 #[test]
1571 fn test_current_image_frozen_during_flush() {
1572 let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
1578 cb.start(); assert_eq!(cb.push(make_array(1)).params.current_image, Some(1));
1580 assert_eq!(cb.push(make_array(2)).params.current_image, Some(2));
1581
1582 cb.trigger();
1583 let r1 = cb.push(make_array(3));
1586 assert_eq!(cb.pre_buffer_len(), 0, "the flush drained the ring");
1587 assert_eq!(r1.params.current_image, None);
1588 assert_eq!(cb.push(make_array(4)).params.current_image, None);
1589
1590 assert_eq!(cb.push(make_array(5)).params.current_image, Some(1));
1592 }
1593
1594 #[test]
1595 fn test_actual_trigger_count_increments_at_sequence_completion() {
1596 let mut cb = CircularBuffer::new(1, 2, TriggerCondition::External);
1601 cb.start(); cb.push(make_array(1));
1603 assert_eq!(cb.trigger_count(), 0);
1604
1605 cb.trigger();
1606 assert_eq!(cb.trigger_count(), 0, "the trigger alone completes nothing");
1607
1608 let r1 = cb.push(make_array(2));
1610 assert_eq!(r1.params.actual_trigger_count, None);
1611 assert_eq!(cb.trigger_count(), 0);
1612
1613 let r2 = cb.push(make_array(3));
1616 assert!(r2.sequence_done);
1617 assert_eq!(r2.params.actual_trigger_count, Some(1));
1618 assert_eq!(cb.trigger_count(), 1);
1619 assert_eq!(r2.params.soft_trigger, Some(0), "C clears the soft latch");
1620 assert_eq!(r2.params.triggered, Some(0));
1621 assert_eq!(r2.params.control, Some(1), "still acquiring");
1622 }
1623
1624 #[test]
1625 fn test_processor_emits_the_frame_params() {
1626 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1629 use ad_core_rs::plugin::runtime::ParamUpdate;
1630
1631 let mut p = CircularBuffProcessor::new(2, 2, TriggerCondition::External, 100);
1632 p.buffer.start(); p.params.current_image = Some(11);
1634 p.params.post_count = Some(13);
1635 p.params.actual_trigger_count = Some(16);
1636 let pool = NDArrayPool::new(0);
1637 let frame = || NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1638 let int32s = |r: &ProcessResult| -> Vec<(usize, i32)> {
1639 r.param_updates
1640 .iter()
1641 .filter_map(|u| match u {
1642 ParamUpdate::Int32 { reason, value, .. } => Some((*reason, *value)),
1643 _ => None,
1644 })
1645 .collect()
1646 };
1647
1648 let r = p.process_array(&frame(), &pool);
1650 assert!(int32s(&r).contains(&(11, 1)));
1651 assert!(!int32s(&r).iter().any(|(reason, _)| *reason == 13));
1652
1653 p.trigger();
1656 let r = p.process_array(&frame(), &pool);
1657 assert!(int32s(&r).contains(&(13, 1)), "POST_COUNT posted per frame");
1658 assert!(
1659 !int32s(&r).iter().any(|(reason, _)| *reason == 11),
1660 "CURRENT_IMAGE frozen during the flush"
1661 );
1662 assert!(
1663 !int32s(&r).iter().any(|(reason, _)| *reason == 16),
1664 "ActualTriggerCount only moves at completion"
1665 );
1666
1667 let r = p.process_array(&frame(), &pool);
1669 assert!(int32s(&r).contains(&(16, 1)));
1670 assert!(int32s(&r).contains(&(13, 0)));
1671 }
1672
1673 #[test]
1685 fn test_soft_trigger_write_latches_only_for_a_nonzero_value() {
1686 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1687 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1688
1689 let soft_trigger_write = |p: &mut CircularBuffProcessor, value: i32| {
1690 let reason = p.params.soft_trigger.unwrap();
1691 p.on_param_change(
1692 reason,
1693 &PluginParamSnapshot {
1694 enable_callbacks: true,
1695 reason,
1696 addr: 0,
1697 value: ParamChangeValue::Int32(value),
1698 },
1699 )
1700 };
1701 let processor = |flush_on_soft_trig: i32| {
1702 let mut p = CircularBuffProcessor::new(3, 2, TriggerCondition::External, 100);
1703 p.buffer.start(); p.params.soft_trigger = Some(20);
1705 p.params.triggered = Some(21);
1706 p.buffer.set_flush_on_soft_trigger(flush_on_soft_trig);
1707 let pool = NDArrayPool::new(0);
1708 for id in 1..=2 {
1710 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1711 a.unique_id = id;
1712 p.process_array(&a, &pool);
1713 }
1714 assert_eq!(p.buffer().pre_buffer_len(), 2);
1715 p
1716 };
1717
1718 let latched = |r: &ad_core_rs::plugin::runtime::ParamChangeResult| {
1719 r.param_updates.iter().any(|u| {
1720 matches!(
1721 u,
1722 ParamUpdate::Int32 {
1723 reason: 21,
1724 value: 1,
1725 ..
1726 }
1727 )
1728 })
1729 };
1730
1731 let mut p = processor(1);
1734 let r = soft_trigger_write(&mut p, 0);
1735 assert!(!p.buffer().is_triggered(), "SoftTrigger 0 must not arm");
1736 assert!(!latched(&r), "SoftTrigger 0 must not post Triggered=1");
1737 assert!(r.output_arrays.is_empty(), "SoftTrigger 0 must not flush");
1738 assert_eq!(p.buffer().pre_buffer_len(), 2);
1739
1740 let mut p = processor(0);
1743 let r = soft_trigger_write(&mut p, 1);
1744 assert!(p.buffer().is_triggered());
1745 assert!(latched(&r));
1746 assert!(
1747 r.output_arrays.is_empty(),
1748 "no flush when FlushOnSoftTrig = 0"
1749 );
1750 assert_eq!(p.buffer().pre_buffer_len(), 2);
1751
1752 let mut p = processor(1);
1754 let r = soft_trigger_write(&mut p, 1);
1755 assert!(p.buffer().is_triggered());
1756 let ids: Vec<_> = r.output_arrays.iter().map(|a| a.unique_id).collect();
1757 assert_eq!(ids, vec![1, 2], "pre-buffer flushed from the write");
1758 assert_eq!(p.buffer().pre_buffer_len(), 0);
1759 assert!(latched(&r));
1760
1761 let pool = NDArrayPool::new(0);
1765 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1766 a.unique_id = 3;
1767 let r = p.process_array(&a, &pool);
1768 let ids: Vec<_> = r.output_arrays.iter().map(|a| a.unique_id).collect();
1769 assert_eq!(ids, vec![3], "pre-buffer already flushed, not re-emitted");
1770 }
1771
1772 #[test]
1778 fn r11_63_flush_on_soft_trig_requires_a_positive_value() {
1779 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1780 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
1781
1782 const FLUSH_ON: usize = 22;
1783 const SOFT_TRIG: usize = 20;
1784
1785 let write = |p: &mut CircularBuffProcessor, reason: usize, value: i32| {
1786 p.on_param_change(
1787 reason,
1788 &PluginParamSnapshot {
1789 enable_callbacks: true,
1790 reason,
1791 addr: 0,
1792 value: ParamChangeValue::Int32(value),
1793 },
1794 )
1795 };
1796
1797 for (flush_on, expect_flush) in [(-1, false), (0, false), (1, true)] {
1798 let mut p = CircularBuffProcessor::new(3, 2, TriggerCondition::External, 100);
1799 p.buffer.start();
1800 p.params.soft_trigger = Some(SOFT_TRIG);
1801 p.params.triggered = Some(21);
1802 p.params.flush_on_soft_trigger = Some(FLUSH_ON);
1803
1804 write(&mut p, FLUSH_ON, flush_on);
1805 assert_eq!(
1806 p.buffer().flushes_on_soft_trigger(),
1807 expect_flush,
1808 "FlushOnSoftTrig = {flush_on}: C flushes only when > 0"
1809 );
1810
1811 let pool = NDArrayPool::new(0);
1812 for id in 1..=2 {
1813 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1814 a.unique_id = id;
1815 p.process_array(&a, &pool);
1816 }
1817 assert_eq!(p.buffer().pre_buffer_len(), 2);
1818
1819 let r = write(&mut p, SOFT_TRIG, 1);
1820 assert!(p.buffer().is_triggered());
1821 if expect_flush {
1822 let ids: Vec<_> = r.output_arrays.iter().map(|a| a.unique_id).collect();
1823 assert_eq!(ids, vec![1, 2], "FlushOnSoftTrig = {flush_on}: flushed");
1824 assert_eq!(p.buffer().pre_buffer_len(), 0);
1825 } else {
1826 assert!(
1827 r.output_arrays.is_empty(),
1828 "FlushOnSoftTrig = {flush_on}: C does not flush from the write"
1829 );
1830 assert_eq!(p.buffer().pre_buffer_len(), 2);
1831 }
1832 }
1833 }
1834
1835 #[test]
1836 fn test_buffer_status_transitions() {
1837 let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
1838
1839 assert_eq!(cb.status(), BufferStatus::Idle);
1841
1842 cb.start();
1845 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1846
1847 cb.push(make_array(1));
1848 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1849
1850 cb.push(make_array(2));
1851 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1852
1853 cb.trigger();
1855 assert_eq!(cb.status(), BufferStatus::Flushing);
1856
1857 let done = cb.push(make_array(3));
1859 assert!(done.sequence_done);
1860 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1861
1862 cb.reset();
1864 assert_eq!(cb.status(), BufferStatus::Idle);
1865 assert_eq!(cb.trigger_count(), 0);
1866 }
1867}