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 captured: Vec<Arc<NDArray>>,
175 preset_trigger_count: usize,
177 trigger_count: usize,
181 flush_on_soft_trigger: i32,
187 pub(crate) status: BufferStatus,
189}
190
191impl CircularBuffer {
192 pub fn new(pre_count: usize, post_count: usize, condition: TriggerCondition) -> Self {
193 Self {
194 control: false,
197 pre_count,
198 post_count,
199 buffer: VecDeque::with_capacity(pre_count + 1),
200 trigger_condition: condition,
201 triggered: false,
202 post_done: 0,
203 pre_flushed: false,
204 captured: Vec::new(),
205 preset_trigger_count: 0,
206 trigger_count: 0,
207 flush_on_soft_trigger: 0,
208 status: BufferStatus::Idle,
209 }
210 }
211
212 pub fn set_preset_trigger_count(&mut self, count: usize) {
214 self.preset_trigger_count = count;
215 }
216
217 pub fn trigger_count(&self) -> usize {
221 self.trigger_count
222 }
223
224 pub fn status(&self) -> BufferStatus {
226 self.status
227 }
228
229 pub fn set_flush_on_soft_trigger(&mut self, flush_on: i32) {
232 self.flush_on_soft_trigger = flush_on;
233 }
234
235 pub fn flushes_on_soft_trigger(&self) -> bool {
240 self.flush_on_soft_trigger > 0
241 }
242
243 pub fn start(&mut self) {
247 self.reset();
248 self.control = true;
249 self.status = BufferStatus::BufferFilling;
250 }
251
252 pub fn stop(&mut self) {
256 self.control = false;
257 self.triggered = false;
258 self.status = BufferStatus::Idle;
259 }
260
261 pub fn is_running(&self) -> bool {
265 self.control
266 }
267
268 pub fn push(&mut self, array: Arc<NDArray>) -> PushResult {
276 let mut result = PushResult::default();
277
278 if !self.control {
286 return result;
287 }
288
289 if !self.triggered {
293 let fired = self.evaluate_trigger(&array, &mut result);
294 result.params.triggered = Some(i32::from(fired));
297 if fired {
298 self.trigger();
302 }
303 }
304
305 if !self.triggered {
306 self.buffer.push_back(array);
308 if self.buffer.len() > self.pre_count {
309 self.buffer.pop_front();
310 }
311 result.params.current_image = Some(self.buffer.len() as i32);
314 if self.buffer.len() == self.pre_count {
316 result.params.status = Some(if self.pre_count > 0 {
317 "Buffer Wrapping"
318 } else {
319 "Dropping frames"
320 });
321 }
322 } else {
323 result.params.status = Some("Flushing");
326 if !self.pre_flushed {
327 result.forward.extend(self.flush_pre_buffer());
328 }
329 self.captured.push(Arc::clone(&array));
330 result.forward.push(array);
331 self.post_done += 1;
332 result.params.post_count = Some(self.post_done as i32);
333 }
334
335 if self.post_done >= self.post_count {
343 self.complete_sequence(&mut result);
344 }
345
346 result
347 }
348
349 fn evaluate_trigger(&self, array: &NDArray, result: &mut PushResult) -> bool {
354 match &self.trigger_condition {
355 TriggerCondition::AttributeThreshold { name, threshold } => array
356 .attributes
357 .get(name)
358 .and_then(|a| a.value.as_f64())
359 .map(|v| v >= *threshold)
360 .unwrap_or(false),
361 TriggerCondition::External => false,
362 TriggerCondition::Calc {
363 attr_a,
364 attr_b,
365 expression,
366 } => {
367 let a = array
368 .attributes
369 .get(attr_a)
370 .and_then(|a| a.value.as_f64())
371 .unwrap_or(f64::NAN);
372 let b = array
373 .attributes
374 .get(attr_b)
375 .and_then(|a| a.value.as_f64())
376 .unwrap_or(f64::NAN);
377 let mut vars = [0.0f64; calc::CALC_NARGS];
380 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);
387 result.trigger_values = Some(TriggerValues { a, b, calc });
388 calc.is_finite() && calc != 0.0
394 }
395 }
396 }
397
398 fn flush_pre_buffer(&mut self) -> Vec<Arc<NDArray>> {
406 self.pre_flushed = true;
407 let pre: Vec<Arc<NDArray>> = self.buffer.drain(..).collect();
408 self.captured.extend(pre.iter().cloned());
409 pre
410 }
411
412 fn complete_sequence(&mut self, result: &mut PushResult) {
416 self.triggered = false;
417 self.pre_flushed = false;
418 self.post_done = 0;
419 self.trigger_count += 1;
423 result.params.actual_trigger_count = Some(self.trigger_count as i32);
424 if self.preset_trigger_count > 0 && self.trigger_count >= self.preset_trigger_count {
425 self.control = false;
431 self.status = BufferStatus::AcquisitionCompleted;
432 result.params.triggered = Some(0);
433 result.params.control = Some(0);
434 result.params.status = Some("Acquisition Completed");
435 } else {
436 self.status = BufferStatus::BufferFilling;
439 result.params.control = Some(1);
440 result.params.soft_trigger = Some(0);
441 result.params.triggered = Some(0);
442 result.params.post_count = Some(0);
443 result.params.status = Some(if self.pre_count > 0 {
444 "Buffer filling"
445 } else {
446 "Dropping frames"
447 });
448 }
449 result.sequence_done = true;
450 }
451
452 pub fn trigger(&mut self) {
454 if !self.control {
459 return;
460 }
461
462 self.triggered = true;
463 self.post_done = 0;
464 self.pre_flushed = false;
465 self.status = BufferStatus::Flushing;
466 self.captured.clear();
469 }
470
471 pub fn take_captured(&mut self) -> Vec<Arc<NDArray>> {
473 std::mem::take(&mut self.captured)
474 }
475
476 pub fn is_triggered(&self) -> bool {
477 self.triggered
478 }
479
480 pub fn pre_buffer_len(&self) -> usize {
481 self.buffer.len()
482 }
483
484 pub fn reset(&mut self) {
487 self.control = false;
488 self.buffer.clear();
489 self.captured.clear();
490 self.triggered = false;
491 self.post_done = 0;
492 self.pre_flushed = false;
493 self.trigger_count = 0;
494 self.status = BufferStatus::Idle;
495 }
496}
497
498#[derive(Default)]
502struct CBParamIndices {
503 control: Option<usize>,
504 status: Option<usize>,
505 trigger_a: Option<usize>,
506 trigger_b: Option<usize>,
507 trigger_a_val: Option<usize>,
508 trigger_b_val: Option<usize>,
509 trigger_calc: Option<usize>,
510 trigger_calc_val: Option<usize>,
511 pre_trigger: Option<usize>,
512 post_trigger: Option<usize>,
513 current_image: Option<usize>,
514 post_count: Option<usize>,
515 soft_trigger: Option<usize>,
516 triggered: Option<usize>,
517 preset_trigger_count: Option<usize>,
518 actual_trigger_count: Option<usize>,
519 flush_on_soft_trigger: Option<usize>,
520}
521
522pub struct CircularBuffProcessor {
523 buffer: CircularBuffer,
524 params: CBParamIndices,
525 max_buffers: usize,
529 trigger_a_name: String,
531 trigger_b_name: String,
532 trigger_calc_expr: String,
533}
534
535impl CircularBuffProcessor {
536 pub fn new(
537 pre_count: usize,
538 post_count: usize,
539 condition: TriggerCondition,
540 max_buffers: usize,
541 ) -> Self {
542 Self {
543 buffer: CircularBuffer::new(pre_count, post_count, condition),
544 params: CBParamIndices::default(),
545 max_buffers,
546 trigger_a_name: String::new(),
547 trigger_b_name: String::new(),
548 trigger_calc_expr: String::new(),
549 }
550 }
551
552 pub fn trigger(&mut self) {
553 self.buffer.trigger();
554 }
555
556 pub fn start(&mut self) {
560 self.buffer.start();
561 }
562
563 pub fn stop(&mut self) {
565 self.buffer.stop();
566 }
567
568 pub fn buffer(&self) -> &CircularBuffer {
569 &self.buffer
570 }
571
572 fn rebuild_trigger_condition(&mut self) {
574 if !self.trigger_calc_expr.is_empty() {
575 if let Some(expr) = CalcExpression::parse(&self.trigger_calc_expr) {
576 self.buffer.trigger_condition = TriggerCondition::Calc {
577 attr_a: self.trigger_a_name.clone(),
578 attr_b: self.trigger_b_name.clone(),
579 expression: expr,
580 };
581 return;
582 }
583 }
584 if !self.trigger_a_name.is_empty() {
585 self.buffer.trigger_condition = TriggerCondition::AttributeThreshold {
586 name: self.trigger_a_name.clone(),
587 threshold: 0.5,
588 };
589 } else {
590 self.buffer.trigger_condition = TriggerCondition::External;
591 }
592 }
593}
594
595impl NDPluginProcess for CircularBuffProcessor {
596 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
597 use ad_core_rs::plugin::runtime::ParamUpdate;
598
599 let push_result = self.buffer.push(Arc::new(array.clone()));
600
601 let mut updates = Vec::new();
607 let p = &push_result.params;
608 if let (Some(idx), Some(s)) = (self.params.status, p.status) {
609 updates.push(ParamUpdate::octet(idx, s.to_string()));
611 }
612 for (index, value) in [
613 (self.params.triggered, p.triggered),
614 (self.params.current_image, p.current_image),
615 (self.params.post_count, p.post_count),
616 (self.params.actual_trigger_count, p.actual_trigger_count),
617 (self.params.soft_trigger, p.soft_trigger),
618 (self.params.control, p.control),
619 ] {
620 if let (Some(idx), Some(v)) = (index, value) {
621 updates.push(ParamUpdate::int32(idx, v));
622 }
623 }
624 if let Some(tv) = push_result.trigger_values {
627 if let Some(idx) = self.params.trigger_a_val {
628 updates.push(ParamUpdate::float64(idx, tv.a));
629 }
630 if let Some(idx) = self.params.trigger_b_val {
631 updates.push(ParamUpdate::float64(idx, tv.b));
632 }
633 if let Some(idx) = self.params.trigger_calc_val {
634 updates.push(ParamUpdate::float64(idx, tv.calc));
635 }
636 }
637
638 if push_result.forward.is_empty() {
642 ProcessResult::sink(updates)
643 } else {
644 let mut result = ProcessResult::arrays(push_result.forward);
645 result.param_updates = updates;
646 result
647 }
648 }
649
650 fn plugin_type(&self) -> &str {
651 "NDPluginCircularBuff"
652 }
653
654 fn register_params(
655 &mut self,
656 base: &mut asyn_rs::port::PortDriverBase,
657 ) -> asyn_rs::error::AsynResult<()> {
658 use asyn_rs::param::ParamType;
659 base.create_param("CIRC_BUFF_CONTROL", ParamType::Int32)?;
660 base.create_param("CIRC_BUFF_STATUS", ParamType::Octet)?;
663 base.create_param("CIRC_BUFF_TRIGGER_A", ParamType::Octet)?;
664 base.create_param("CIRC_BUFF_TRIGGER_B", ParamType::Octet)?;
665 base.create_param("CIRC_BUFF_TRIGGER_A_VAL", ParamType::Float64)?;
666 base.create_param("CIRC_BUFF_TRIGGER_B_VAL", ParamType::Float64)?;
667 base.create_param("CIRC_BUFF_TRIGGER_CALC", ParamType::Octet)?;
668 base.create_param("CIRC_BUFF_TRIGGER_CALC_VAL", ParamType::Float64)?;
669 base.create_param("CIRC_BUFF_PRE_TRIGGER", ParamType::Int32)?;
670 base.create_param("CIRC_BUFF_POST_TRIGGER", ParamType::Int32)?;
671 base.create_param("CIRC_BUFF_CURRENT_IMAGE", ParamType::Int32)?;
672 base.create_param("CIRC_BUFF_POST_COUNT", ParamType::Int32)?;
673 base.create_param("CIRC_BUFF_SOFT_TRIGGER", ParamType::Int32)?;
674 base.create_param("CIRC_BUFF_TRIGGERED", ParamType::Int32)?;
675 base.create_param("CIRC_BUFF_PRESET_TRIGGER_COUNT", ParamType::Int32)?;
676 base.create_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT", ParamType::Int32)?;
677 base.create_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER", ParamType::Int32)?;
678
679 self.params.control = base.find_param("CIRC_BUFF_CONTROL");
680 self.params.status = base.find_param("CIRC_BUFF_STATUS");
681 self.params.trigger_a = base.find_param("CIRC_BUFF_TRIGGER_A");
682 self.params.trigger_b = base.find_param("CIRC_BUFF_TRIGGER_B");
683 self.params.trigger_a_val = base.find_param("CIRC_BUFF_TRIGGER_A_VAL");
684 self.params.trigger_b_val = base.find_param("CIRC_BUFF_TRIGGER_B_VAL");
685 self.params.trigger_calc = base.find_param("CIRC_BUFF_TRIGGER_CALC");
686 self.params.trigger_calc_val = base.find_param("CIRC_BUFF_TRIGGER_CALC_VAL");
687 self.params.pre_trigger = base.find_param("CIRC_BUFF_PRE_TRIGGER");
688 self.params.post_trigger = base.find_param("CIRC_BUFF_POST_TRIGGER");
689 self.params.current_image = base.find_param("CIRC_BUFF_CURRENT_IMAGE");
690 self.params.post_count = base.find_param("CIRC_BUFF_POST_COUNT");
691 self.params.soft_trigger = base.find_param("CIRC_BUFF_SOFT_TRIGGER");
692 self.params.triggered = base.find_param("CIRC_BUFF_TRIGGERED");
693 self.params.preset_trigger_count = base.find_param("CIRC_BUFF_PRESET_TRIGGER_COUNT");
694 self.params.actual_trigger_count = base.find_param("CIRC_BUFF_ACTUAL_TRIGGER_COUNT");
695 self.params.flush_on_soft_trigger = base.find_param("CIRC_BUFF_FLUSH_ON_SOFTTRIGGER");
696
697 if let Some(idx) = self.params.status {
700 base.set_string_param(idx, 0, "Idle".into())?;
701 }
702 Ok(())
703 }
704
705 fn on_param_change(
706 &mut self,
707 reason: usize,
708 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
709 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
710 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
711
712 let mut updates = Vec::new();
713 if Some(reason) == self.params.control {
714 let v = params.value.as_i32();
715 if v == 1 {
716 self.buffer.start();
721 for (index, value) in [
722 (self.params.soft_trigger, 0),
723 (self.params.triggered, 0),
724 (self.params.post_count, 0),
725 (self.params.actual_trigger_count, 0),
726 ] {
727 if let Some(idx) = index {
728 updates.push(ParamUpdate::int32(idx, value));
729 }
730 }
731 if let Some(idx) = self.params.status {
734 let s = if self.buffer.pre_count > 0 {
735 "Buffer filling"
736 } else {
737 "Dropping frames"
738 };
739 updates.push(ParamUpdate::octet(idx, s.to_string()));
740 }
741 } else {
742 self.buffer.stop();
747 for (index, value) in [
748 (self.params.soft_trigger, 0),
749 (self.params.triggered, 0),
750 (self.params.current_image, 0),
751 ] {
752 if let Some(idx) = index {
753 updates.push(ParamUpdate::int32(idx, value));
754 }
755 }
756 if let Some(idx) = self.params.status {
759 updates.push(ParamUpdate::octet(idx, "Acquisition Stopped".to_string()));
760 }
761 }
762 } else if Some(reason) == self.params.pre_trigger {
763 let value = params.value.as_i32();
769 let reject_msg = if self.buffer.is_running() {
772 Some("Stop acquisition to set pre-count")
773 } else if value > self.max_buffers as i32 - 1 {
774 Some("Pre-count too high")
776 } else if value < 0 {
777 Some("Invalid pre-count value")
778 } else {
779 None
780 };
781 if let Some(msg) = reject_msg {
782 if let Some(idx) = self.params.status {
783 updates.push(ParamUpdate::octet(idx, msg.to_string()));
784 }
785 if let Some(idx) = self.params.pre_trigger {
788 updates.push(ParamUpdate::int32(idx, self.buffer.pre_count as i32));
789 }
790 } else {
791 self.buffer.pre_count = value as usize;
792 }
793 } else if Some(reason) == self.params.post_trigger {
794 self.buffer.post_count = params.value.as_i32().max(0) as usize;
795 } else if Some(reason) == self.params.preset_trigger_count {
796 self.buffer
797 .set_preset_trigger_count(params.value.as_i32().max(0) as usize);
798 } else if Some(reason) == self.params.flush_on_soft_trigger {
799 self.buffer.set_flush_on_soft_trigger(params.value.as_i32());
800 } else if Some(reason) == self.params.soft_trigger {
801 if params.value.as_i32() != 0 {
825 self.buffer.trigger();
826 if let Some(idx) = self.params.triggered {
827 updates.push(ParamUpdate::int32(idx, 1));
828 }
829 if self.buffer.flushes_on_soft_trigger() {
833 let flushed = self.buffer.flush_pre_buffer();
834 if !flushed.is_empty() {
835 return ParamChangeResult::combined(flushed, updates);
836 }
837 }
838 }
839 } else if Some(reason) == self.params.trigger_a {
840 if let ParamChangeValue::Octet(s) = ¶ms.value {
841 self.trigger_a_name = s.clone();
842 self.rebuild_trigger_condition();
843 }
844 } else if Some(reason) == self.params.trigger_b {
845 if let ParamChangeValue::Octet(s) = ¶ms.value {
846 self.trigger_b_name = s.clone();
847 self.rebuild_trigger_condition();
848 }
849 } else if Some(reason) == self.params.trigger_calc {
850 if let ParamChangeValue::Octet(s) = ¶ms.value {
851 self.trigger_calc_expr = s.clone();
852 self.rebuild_trigger_condition();
853 }
854 }
855
856 ParamChangeResult::updates(updates)
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
864 use ad_core_rs::ndarray::{NDDataType, NDDimension};
865
866 fn make_array(id: i32) -> Arc<NDArray> {
867 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
868 arr.unique_id = id;
869 Arc::new(arr)
870 }
871
872 fn make_array_with_attr(id: i32, attr_val: f64) -> Arc<NDArray> {
873 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
874 arr.unique_id = id;
875 arr.attributes.add(NDAttribute::new_static(
876 "trigger",
877 "",
878 NDAttrSource::Driver,
879 NDAttrValue::Float64(attr_val),
880 ));
881 Arc::new(arr)
882 }
883
884 fn make_array_with_attrs(id: i32, a_val: f64, b_val: f64) -> Arc<NDArray> {
885 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
886 arr.unique_id = id;
887 arr.attributes.add(NDAttribute::new_static(
888 "attr_a",
889 "",
890 NDAttrSource::Driver,
891 NDAttrValue::Float64(a_val),
892 ));
893 arr.attributes.add(NDAttribute::new_static(
894 "attr_b",
895 "",
896 NDAttrSource::Driver,
897 NDAttrValue::Float64(b_val),
898 ));
899 Arc::new(arr)
900 }
901
902 #[test]
903 fn test_pre_trigger_buffering() {
904 let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
905 cb.start(); for i in 0..5 {
908 cb.push(make_array(i));
909 }
910 assert_eq!(cb.pre_buffer_len(), 3);
912 }
913
914 #[test]
915 fn test_external_trigger() {
916 let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
917 cb.start(); cb.push(make_array(1));
920 cb.push(make_array(2));
921 cb.push(make_array(3));
922 cb.trigger();
925 assert!(cb.is_triggered());
926
927 let r1 = cb.push(make_array(4));
929 assert!(!r1.sequence_done);
930 let ids1: Vec<_> = r1.forward.iter().map(|a| a.unique_id).collect();
931 assert_eq!(ids1, vec![2, 3, 4]); let r2 = cb.push(make_array(5));
935 assert!(r2.sequence_done);
936 let ids2: Vec<_> = r2.forward.iter().map(|a| a.unique_id).collect();
937 assert_eq!(ids2, vec![5]);
938
939 let captured = cb.take_captured();
940 assert_eq!(captured.len(), 4); assert_eq!(captured[0].unique_id, 2);
942 assert_eq!(captured[1].unique_id, 3);
943 assert_eq!(captured[2].unique_id, 4);
944 assert_eq!(captured[3].unique_id, 5);
945 }
946
947 #[test]
948 fn test_post_count_zero_no_underflow() {
949 let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
952 cb.start(); cb.push(make_array(1));
954 cb.push(make_array(2));
955 cb.trigger();
956 assert!(cb.is_triggered());
957
958 let r = cb.push(make_array(3));
961 assert!(r.sequence_done);
962 let ids: Vec<_> = r.forward.iter().map(|a| a.unique_id).collect();
963 assert_eq!(ids, vec![1, 2, 3]);
964 assert!(!cb.is_triggered());
965 assert_eq!(cb.status(), BufferStatus::BufferFilling);
966
967 let r2 = cb.push(make_array(4));
975 assert!(r2.sequence_done);
976 assert!(r2.forward.is_empty());
977 }
978
979 #[test]
980 fn test_post_count_zero_completes_on_untriggered_frame() {
981 let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
988 cb.start(); for (n, id) in (1..=3).enumerate() {
991 let r = cb.push(make_array(id));
992 assert!(r.forward.is_empty(), "frame {id} must not be forwarded");
994 assert!(r.sequence_done, "frame {id} must complete a sequence");
996 assert_eq!(r.params.actual_trigger_count, Some(n as i32 + 1));
997 assert_eq!(cb.trigger_count(), n + 1);
998 assert_eq!(r.params.control, Some(1));
1000 assert_eq!(r.params.soft_trigger, Some(0));
1001 assert_eq!(r.params.triggered, Some(0));
1002 assert_eq!(r.params.post_count, Some(0));
1003 assert_eq!(r.params.status, Some("Buffer filling"));
1004 }
1005 assert_eq!(cb.pre_buffer_len(), 2);
1008
1009 let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
1012 cb.start(); let r = cb.push(make_array(1));
1014 assert!(!r.sequence_done);
1015 assert_eq!(r.params.actual_trigger_count, None);
1016 assert_eq!(cb.trigger_count(), 0);
1017 }
1018
1019 #[test]
1020 fn test_post_count_zero_untriggered_frames_reach_preset_trigger_count() {
1021 let mut cb = CircularBuffer::new(2, 0, TriggerCondition::External);
1026 cb.start(); cb.set_preset_trigger_count(2);
1028
1029 let r1 = cb.push(make_array(1));
1030 assert!(r1.sequence_done);
1031 assert_eq!(r1.params.actual_trigger_count, Some(1));
1032 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1033
1034 let r2 = cb.push(make_array(2));
1035 assert_eq!(r2.params.actual_trigger_count, Some(2));
1036 assert_eq!(r2.params.control, Some(0));
1038 assert_eq!(r2.params.status, Some("Acquisition Completed"));
1039 assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
1040 }
1041
1042 #[test]
1043 fn test_attribute_trigger_post_count_zero() {
1044 let mut cb = CircularBuffer::new(
1047 1,
1048 0,
1049 TriggerCondition::AttributeThreshold {
1050 name: "trigger".into(),
1051 threshold: 5.0,
1052 },
1053 );
1054 cb.start(); cb.push(make_array_with_attr(1, 1.0));
1056 let r = cb.push(make_array_with_attr(2, 9.0));
1057 assert!(r.sequence_done);
1058 let ids: Vec<_> = r.forward.iter().map(|a| a.unique_id).collect();
1059 assert_eq!(ids, vec![1, 2]); assert!(!cb.is_triggered());
1061 }
1062
1063 #[test]
1064 fn test_attribute_trigger() {
1065 let mut cb = CircularBuffer::new(
1066 1,
1067 2,
1068 TriggerCondition::AttributeThreshold {
1069 name: "trigger".into(),
1070 threshold: 5.0,
1071 },
1072 );
1073 cb.start(); cb.push(make_array_with_attr(1, 1.0));
1076 cb.push(make_array_with_attr(2, 2.0));
1077 assert!(!cb.is_triggered());
1078
1079 let r3 = cb.push(make_array_with_attr(3, 5.0));
1081 assert!(cb.is_triggered());
1082 let ids3: Vec<_> = r3.forward.iter().map(|a| a.unique_id).collect();
1084 assert_eq!(ids3, vec![2, 3]);
1085
1086 let r4 = cb.push(make_array(4));
1087 assert!(r4.sequence_done);
1088
1089 let captured = cb.take_captured();
1090 assert_eq!(captured.len(), 3);
1092 assert_eq!(captured[0].unique_id, 2);
1093 assert_eq!(captured[1].unique_id, 3);
1094 assert_eq!(captured[2].unique_id, 4);
1095 }
1096
1097 #[test]
1100 fn test_calc_trigger() {
1101 let expr = CalcExpression::parse("A>5").unwrap();
1103 let mut cb = CircularBuffer::new(
1104 1,
1105 2,
1106 TriggerCondition::Calc {
1107 attr_a: "attr_a".into(),
1108 attr_b: "attr_b".into(),
1109 expression: expr,
1110 },
1111 );
1112 cb.start(); cb.push(make_array_with_attrs(1, 3.0, 0.0));
1116 assert!(!cb.is_triggered());
1117
1118 cb.push(make_array_with_attrs(2, 6.0, 0.0));
1120 assert!(cb.is_triggered());
1121
1122 let done = cb.push(make_array(3));
1123 assert!(done.sequence_done);
1124
1125 let captured = cb.take_captured();
1126 assert_eq!(captured.len(), 3);
1128 assert_eq!(captured[0].unique_id, 1);
1129 assert_eq!(captured[1].unique_id, 2);
1130 assert_eq!(captured[2].unique_id, 3);
1131 }
1132
1133 #[test]
1134 fn test_calc_trigger_values_surface() {
1135 let expr = CalcExpression::parse("A+B").unwrap();
1138 let mut cb = CircularBuffer::new(
1141 2,
1142 3,
1143 TriggerCondition::Calc {
1144 attr_a: "attr_a".into(),
1145 attr_b: "attr_b".into(),
1146 expression: expr,
1147 },
1148 );
1149 cb.start(); let r = cb.push(make_array_with_attrs(1, 3.0, 4.0));
1153 let tv = r.trigger_values.expect("calc path surfaces trigger values");
1154 assert_eq!(tv.a, 3.0);
1155 assert_eq!(tv.b, 4.0);
1156 assert_eq!(tv.calc, 7.0);
1157
1158 let r2 = cb.push(make_array(2));
1161 assert!(r2.trigger_values.is_none());
1162 }
1163
1164 #[test]
1165 fn test_calc_trigger_values_nan_when_attr_absent() {
1166 let expr = CalcExpression::parse("A").unwrap();
1169 let mut cb = CircularBuffer::new(
1170 2,
1171 1,
1172 TriggerCondition::Calc {
1173 attr_a: "missing_a".into(),
1174 attr_b: "missing_b".into(),
1175 expression: expr,
1176 },
1177 );
1178 cb.start(); let r = cb.push(make_array(1));
1180 let tv = r.trigger_values.expect("calc path surfaces trigger values");
1181 assert!(tv.a.is_nan());
1182 assert!(tv.b.is_nan());
1183 assert!(tv.calc.is_nan());
1184 }
1185
1186 #[test]
1187 fn test_calc_trigger_skips_nan_and_inf_results() {
1188 let push_calc = |val: f64| {
1195 let expr = CalcExpression::parse("A").unwrap();
1196 let mut cb = CircularBuffer::new(
1197 2,
1198 2,
1199 TriggerCondition::Calc {
1200 attr_a: "attr_a".into(),
1201 attr_b: "attr_b".into(),
1202 expression: expr,
1203 },
1204 );
1205 cb.start(); cb.push(make_array_with_attrs(1, val, 0.0));
1207 cb.is_triggered()
1208 };
1209 assert!(!push_calc(f64::NAN));
1211 assert!(!push_calc(f64::INFINITY));
1212 assert!(!push_calc(f64::NEG_INFINITY));
1213 assert!(push_calc(1.0));
1216 assert!(!push_calc(0.0));
1217 }
1218
1219 #[test]
1220 fn test_calc_expression_parse() {
1221 let expr = CalcExpression::parse("A>5").unwrap();
1223 assert_eq!(expr.evaluate(6.0, 0.0), 1.0);
1224 assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
1225 assert_eq!(expr.evaluate(5.0, 0.0), 0.0); let expr = CalcExpression::parse("A>=5").unwrap();
1229 assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
1230 assert_eq!(expr.evaluate(4.9, 0.0), 0.0);
1231
1232 let expr = CalcExpression::parse("A>3&&B<10").unwrap();
1234 assert_eq!(expr.evaluate(4.0, 5.0), 1.0);
1235 assert_eq!(expr.evaluate(2.0, 5.0), 0.0);
1236 assert_eq!(expr.evaluate(4.0, 15.0), 0.0);
1237
1238 let expr = CalcExpression::parse("(A>10)||(B>10)").unwrap();
1240 assert_eq!(expr.evaluate(11.0, 0.0), 1.0);
1241 assert_eq!(expr.evaluate(0.0, 11.0), 1.0);
1242 assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
1243
1244 let expr = CalcExpression::parse("A!=0").unwrap();
1246 assert_eq!(expr.evaluate(1.0, 0.0), 1.0);
1247 assert_eq!(expr.evaluate(0.0, 0.0), 0.0);
1248
1249 let expr = CalcExpression::parse("A==B").unwrap();
1251 assert_eq!(expr.evaluate(5.0, 5.0), 1.0);
1252 assert_eq!(expr.evaluate(5.0, 6.0), 0.0);
1253
1254 let expr = CalcExpression::parse("!A").unwrap();
1256 assert_eq!(expr.evaluate(0.0, 0.0), 1.0);
1257 assert_eq!(expr.evaluate(1.0, 0.0), 0.0);
1258
1259 let expr = CalcExpression::parse("A=5").unwrap();
1262 assert_eq!(expr.evaluate(5.0, 0.0), 1.0);
1263 assert_eq!(expr.evaluate(4.0, 0.0), 0.0);
1264
1265 let expr = CalcExpression::parse("A&B").unwrap();
1266 assert_eq!(expr.evaluate(3.0, 1.0), 1.0);
1268
1269 let expr = CalcExpression::parse("ABS(A)").unwrap();
1271 assert_eq!(expr.evaluate(-5.0, 0.0), 5.0);
1272
1273 let expr = CalcExpression::parse("SQRT(A)").unwrap();
1274 assert!((expr.evaluate(9.0, 0.0) - 3.0).abs() < 1e-10);
1275
1276 let expr = CalcExpression::parse("A+B").unwrap();
1277 assert_eq!(expr.evaluate(3.0, 4.0), 7.0);
1278
1279 let expr = CalcExpression::parse("A-B").unwrap();
1280 assert_eq!(expr.evaluate(10.0, 3.0), 7.0);
1281
1282 let expr = CalcExpression::parse("A*B").unwrap();
1283 assert_eq!(expr.evaluate(3.0, 4.0), 12.0);
1284
1285 let expr = CalcExpression::parse("A/B").unwrap();
1286 assert_eq!(expr.evaluate(12.0, 4.0), 3.0);
1287
1288 let expr = CalcExpression::parse("A>5&&C>0").unwrap();
1290 let mut vars = [0.0f64; calc::CALC_NARGS];
1291 vars[0] = 6.0; vars[2] = 1.0; assert_eq!(expr.evaluate_vars(&vars), 1.0);
1294 vars[2] = 0.0; assert_eq!(expr.evaluate_vars(&vars), 0.0);
1296
1297 assert!(CalcExpression::parse("@@@").is_none());
1299 }
1300
1301 #[test]
1302 fn test_preset_trigger_count() {
1303 let mut cb = CircularBuffer::new(1, 1, TriggerCondition::External);
1304 cb.start(); cb.set_preset_trigger_count(2);
1306
1307 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1311
1312 cb.push(make_array(1));
1313 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1314
1315 cb.trigger();
1318 assert_eq!(cb.trigger_count(), 0);
1319 assert_eq!(cb.status(), BufferStatus::Flushing);
1320
1321 let done = cb.push(make_array(2));
1322 assert!(done.sequence_done);
1323 assert_eq!(cb.trigger_count(), 1); assert_eq!(cb.status(), BufferStatus::BufferFilling); cb.take_captured();
1327
1328 cb.push(make_array(3));
1330
1331 cb.trigger();
1333 assert_eq!(cb.trigger_count(), 1);
1334 assert_eq!(cb.status(), BufferStatus::Flushing);
1335
1336 let done = cb.push(make_array(4));
1337 assert!(done.sequence_done);
1338 assert_eq!(cb.trigger_count(), 2);
1339 assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
1340
1341 cb.take_captured();
1342
1343 let done = cb.push(make_array(5));
1345 assert!(!done.sequence_done);
1346 assert_eq!(cb.status(), BufferStatus::AcquisitionCompleted);
1347
1348 cb.trigger();
1350 assert_eq!(cb.trigger_count(), 2); }
1352
1353 #[test]
1354 fn test_stop_resets_current_image_and_status() {
1355 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1358
1359 let mut processor = CircularBuffProcessor::new(2, 1, TriggerCondition::External, 100);
1360 processor.params.control = Some(10);
1361 processor.params.current_image = Some(11);
1362 processor.params.status = Some(12);
1363
1364 let snapshot = PluginParamSnapshot {
1365 enable_callbacks: true,
1366 reason: 10,
1367 addr: 0,
1368 value: ParamChangeValue::Int32(0), };
1370 let result = processor.on_param_change(10, &snapshot);
1371
1372 assert!(
1373 result.param_updates.iter().any(|u| matches!(
1374 u,
1375 ParamUpdate::Int32 {
1376 reason: 11,
1377 value: 0,
1378 ..
1379 }
1380 )),
1381 "stop must post CURRENT_IMAGE=0"
1382 );
1383 assert!(
1384 result.param_updates.iter().any(|u| matches!(
1385 u,
1386 ParamUpdate::Octet { reason: 12, value, .. } if value == "Acquisition Stopped"
1387 )),
1388 "stop must post STATUS=Acquisition Stopped"
1389 );
1390 }
1391
1392 #[test]
1393 fn test_pre_count_validation() {
1394 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1399
1400 let make_proc = || {
1401 let mut p = CircularBuffProcessor::new(3, 1, TriggerCondition::External, 10);
1402 p.params.pre_trigger = Some(20);
1403 p.params.status = Some(12);
1404 p
1405 };
1406 let write = |p: &mut CircularBuffProcessor, v: i32| {
1407 let snap = PluginParamSnapshot {
1408 enable_callbacks: true,
1409 reason: 20,
1410 addr: 0,
1411 value: ParamChangeValue::Int32(v),
1412 };
1413 p.on_param_change(20, &snap)
1414 };
1415
1416 let mut p = make_proc();
1418 p.buffer.start();
1419 let r = write(&mut p, 7);
1420 assert_eq!(
1421 p.buffer.pre_count, 3,
1422 "reject while running, value unchanged"
1423 );
1424 assert!(r.param_updates.iter().any(|u| matches!(
1425 u,
1426 ParamUpdate::Octet { reason: 12, value, .. } if value == "Stop acquisition to set pre-count"
1427 )));
1428 assert!(r.param_updates.iter().any(|u| matches!(
1429 u,
1430 ParamUpdate::Int32 {
1431 reason: 20,
1432 value: 3,
1433 ..
1434 }
1435 )));
1436
1437 let mut p = make_proc();
1439 p.buffer.stop();
1440 let r = write(&mut p, -1);
1441 assert_eq!(p.buffer.pre_count, 3, "negative rejected, value unchanged");
1442 assert!(r.param_updates.iter().any(|u| matches!(
1443 u,
1444 ParamUpdate::Octet { reason: 12, value, .. } if value == "Invalid pre-count value"
1445 )));
1446
1447 let mut p = make_proc();
1449 p.buffer.stop();
1450 let r = write(&mut p, 10);
1451 assert_eq!(p.buffer.pre_count, 3, "too-high rejected, value unchanged");
1452 assert!(r.param_updates.iter().any(|u| matches!(
1453 u,
1454 ParamUpdate::Octet { reason: 12, value, .. } if value == "Pre-count too high"
1455 )));
1456 assert!(r.param_updates.iter().any(|u| matches!(
1457 u,
1458 ParamUpdate::Int32 {
1459 reason: 20,
1460 value: 3,
1461 ..
1462 }
1463 )));
1464
1465 let mut p = make_proc();
1467 p.buffer.stop();
1468 write(&mut p, 9);
1469 assert_eq!(p.buffer.pre_count, 9, "valid pre-count committed");
1470 }
1471
1472 #[test]
1473 fn test_frame_status_strings() {
1474 let mut cb = CircularBuffer::new(2, 2, TriggerCondition::External);
1478 cb.start(); assert_eq!(cb.push(make_array(1)).params.status, None);
1480 assert_eq!(
1483 cb.push(make_array(2)).params.status,
1484 Some("Buffer Wrapping")
1485 );
1486 assert_eq!(
1487 cb.push(make_array(3)).params.status,
1488 Some("Buffer Wrapping")
1489 );
1490 cb.trigger();
1492 assert_eq!(cb.push(make_array(4)).params.status, Some("Flushing"));
1493 assert_eq!(cb.push(make_array(5)).params.status, Some("Buffer filling"));
1495
1496 let mut cb = CircularBuffer::new(0, 1, TriggerCondition::External);
1499 cb.start(); assert_eq!(
1501 cb.push(make_array(1)).params.status,
1502 Some("Dropping frames")
1503 );
1504 cb.trigger();
1505 assert_eq!(
1506 cb.push(make_array(2)).params.status,
1507 Some("Dropping frames")
1508 );
1509
1510 let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
1512 cb.start(); cb.set_preset_trigger_count(1);
1514 cb.trigger();
1515 assert_eq!(
1516 cb.push(make_array(1)).params.status,
1517 Some("Acquisition Completed")
1518 );
1519 }
1520
1521 #[test]
1522 fn test_post_count_posted_per_flushed_frame() {
1523 let mut cb = CircularBuffer::new(2, 3, TriggerCondition::External);
1529 cb.start(); assert_eq!(cb.push(make_array(1)).params.post_count, None);
1532 assert_eq!(cb.push(make_array(2)).params.post_count, None);
1533
1534 cb.trigger();
1535 assert_eq!(cb.push(make_array(3)).params.post_count, Some(1));
1536 assert_eq!(cb.push(make_array(4)).params.post_count, Some(2));
1537 assert_eq!(cb.push(make_array(5)).params.post_count, Some(0));
1540
1541 cb.trigger();
1543 assert_eq!(cb.push(make_array(6)).params.post_count, Some(1));
1544 }
1545
1546 #[test]
1547 fn test_post_count_survives_acquisition_completed() {
1548 let mut cb = CircularBuffer::new(1, 2, TriggerCondition::External);
1552 cb.start(); cb.set_preset_trigger_count(1);
1554 cb.trigger();
1555 assert_eq!(cb.push(make_array(1)).params.post_count, Some(1));
1556 let done = cb.push(make_array(2));
1557 assert_eq!(done.params.post_count, Some(2), "final count, not reset");
1558 assert_eq!(done.params.status, Some("Acquisition Completed"));
1559 assert_eq!(done.params.control, Some(0), "C turns acquisition off");
1560 }
1561
1562 #[test]
1563 fn test_current_image_frozen_during_flush() {
1564 let mut cb = CircularBuffer::new(3, 2, TriggerCondition::External);
1570 cb.start(); assert_eq!(cb.push(make_array(1)).params.current_image, Some(1));
1572 assert_eq!(cb.push(make_array(2)).params.current_image, Some(2));
1573
1574 cb.trigger();
1575 let r1 = cb.push(make_array(3));
1578 assert_eq!(cb.pre_buffer_len(), 0, "the flush drained the ring");
1579 assert_eq!(r1.params.current_image, None);
1580 assert_eq!(cb.push(make_array(4)).params.current_image, None);
1581
1582 assert_eq!(cb.push(make_array(5)).params.current_image, Some(1));
1584 }
1585
1586 #[test]
1587 fn test_actual_trigger_count_increments_at_sequence_completion() {
1588 let mut cb = CircularBuffer::new(1, 2, TriggerCondition::External);
1593 cb.start(); cb.push(make_array(1));
1595 assert_eq!(cb.trigger_count(), 0);
1596
1597 cb.trigger();
1598 assert_eq!(cb.trigger_count(), 0, "the trigger alone completes nothing");
1599
1600 let r1 = cb.push(make_array(2));
1602 assert_eq!(r1.params.actual_trigger_count, None);
1603 assert_eq!(cb.trigger_count(), 0);
1604
1605 let r2 = cb.push(make_array(3));
1608 assert!(r2.sequence_done);
1609 assert_eq!(r2.params.actual_trigger_count, Some(1));
1610 assert_eq!(cb.trigger_count(), 1);
1611 assert_eq!(r2.params.soft_trigger, Some(0), "C clears the soft latch");
1612 assert_eq!(r2.params.triggered, Some(0));
1613 assert_eq!(r2.params.control, Some(1), "still acquiring");
1614 }
1615
1616 #[test]
1617 fn test_processor_emits_the_frame_params() {
1618 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1621 use ad_core_rs::plugin::runtime::ParamUpdate;
1622
1623 let mut p = CircularBuffProcessor::new(2, 2, TriggerCondition::External, 100);
1624 p.buffer.start(); p.params.current_image = Some(11);
1626 p.params.post_count = Some(13);
1627 p.params.actual_trigger_count = Some(16);
1628 let pool = NDArrayPool::new(0);
1629 let frame = || NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1630 let int32s = |r: &ProcessResult| -> Vec<(usize, i32)> {
1631 r.param_updates
1632 .iter()
1633 .filter_map(|u| match u {
1634 ParamUpdate::Int32 { reason, value, .. } => Some((*reason, *value)),
1635 _ => None,
1636 })
1637 .collect()
1638 };
1639
1640 let r = p.process_array(&frame(), &pool);
1642 assert!(int32s(&r).contains(&(11, 1)));
1643 assert!(!int32s(&r).iter().any(|(reason, _)| *reason == 13));
1644
1645 p.trigger();
1648 let r = p.process_array(&frame(), &pool);
1649 assert!(int32s(&r).contains(&(13, 1)), "POST_COUNT posted per frame");
1650 assert!(
1651 !int32s(&r).iter().any(|(reason, _)| *reason == 11),
1652 "CURRENT_IMAGE frozen during the flush"
1653 );
1654 assert!(
1655 !int32s(&r).iter().any(|(reason, _)| *reason == 16),
1656 "ActualTriggerCount only moves at completion"
1657 );
1658
1659 let r = p.process_array(&frame(), &pool);
1661 assert!(int32s(&r).contains(&(16, 1)));
1662 assert!(int32s(&r).contains(&(13, 0)));
1663 }
1664
1665 #[test]
1677 fn test_soft_trigger_write_latches_only_for_a_nonzero_value() {
1678 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1679 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1680
1681 let soft_trigger_write = |p: &mut CircularBuffProcessor, value: i32| {
1682 let reason = p.params.soft_trigger.unwrap();
1683 p.on_param_change(
1684 reason,
1685 &PluginParamSnapshot {
1686 enable_callbacks: true,
1687 reason,
1688 addr: 0,
1689 value: ParamChangeValue::Int32(value),
1690 },
1691 )
1692 };
1693 let processor = |flush_on_soft_trig: i32| {
1694 let mut p = CircularBuffProcessor::new(3, 2, TriggerCondition::External, 100);
1695 p.buffer.start(); p.params.soft_trigger = Some(20);
1697 p.params.triggered = Some(21);
1698 p.buffer.set_flush_on_soft_trigger(flush_on_soft_trig);
1699 let pool = NDArrayPool::new(0);
1700 for id in 1..=2 {
1702 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1703 a.unique_id = id;
1704 p.process_array(&a, &pool);
1705 }
1706 assert_eq!(p.buffer().pre_buffer_len(), 2);
1707 p
1708 };
1709
1710 let latched = |r: &ad_core_rs::plugin::runtime::ParamChangeResult| {
1711 r.param_updates.iter().any(|u| {
1712 matches!(
1713 u,
1714 ParamUpdate::Int32 {
1715 reason: 21,
1716 value: 1,
1717 ..
1718 }
1719 )
1720 })
1721 };
1722
1723 let mut p = processor(1);
1726 let r = soft_trigger_write(&mut p, 0);
1727 assert!(!p.buffer().is_triggered(), "SoftTrigger 0 must not arm");
1728 assert!(!latched(&r), "SoftTrigger 0 must not post Triggered=1");
1729 assert!(r.output_arrays.is_empty(), "SoftTrigger 0 must not flush");
1730 assert_eq!(p.buffer().pre_buffer_len(), 2);
1731
1732 let mut p = processor(0);
1735 let r = soft_trigger_write(&mut p, 1);
1736 assert!(p.buffer().is_triggered());
1737 assert!(latched(&r));
1738 assert!(
1739 r.output_arrays.is_empty(),
1740 "no flush when FlushOnSoftTrig = 0"
1741 );
1742 assert_eq!(p.buffer().pre_buffer_len(), 2);
1743
1744 let mut p = processor(1);
1746 let r = soft_trigger_write(&mut p, 1);
1747 assert!(p.buffer().is_triggered());
1748 let ids: Vec<_> = r.output_arrays.iter().map(|a| a.unique_id).collect();
1749 assert_eq!(ids, vec![1, 2], "pre-buffer flushed from the write");
1750 assert_eq!(p.buffer().pre_buffer_len(), 0);
1751 assert!(latched(&r));
1752
1753 let pool = NDArrayPool::new(0);
1757 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1758 a.unique_id = 3;
1759 let r = p.process_array(&a, &pool);
1760 let ids: Vec<_> = r.output_arrays.iter().map(|a| a.unique_id).collect();
1761 assert_eq!(ids, vec![3], "pre-buffer already flushed, not re-emitted");
1762 }
1763
1764 #[test]
1770 fn r11_63_flush_on_soft_trig_requires_a_positive_value() {
1771 use ad_core_rs::ndarray::{NDDataType, NDDimension};
1772 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
1773
1774 const FLUSH_ON: usize = 22;
1775 const SOFT_TRIG: usize = 20;
1776
1777 let write = |p: &mut CircularBuffProcessor, reason: usize, value: i32| {
1778 p.on_param_change(
1779 reason,
1780 &PluginParamSnapshot {
1781 enable_callbacks: true,
1782 reason,
1783 addr: 0,
1784 value: ParamChangeValue::Int32(value),
1785 },
1786 )
1787 };
1788
1789 for (flush_on, expect_flush) in [(-1, false), (0, false), (1, true)] {
1790 let mut p = CircularBuffProcessor::new(3, 2, TriggerCondition::External, 100);
1791 p.buffer.start();
1792 p.params.soft_trigger = Some(SOFT_TRIG);
1793 p.params.triggered = Some(21);
1794 p.params.flush_on_soft_trigger = Some(FLUSH_ON);
1795
1796 write(&mut p, FLUSH_ON, flush_on);
1797 assert_eq!(
1798 p.buffer().flushes_on_soft_trigger(),
1799 expect_flush,
1800 "FlushOnSoftTrig = {flush_on}: C flushes only when > 0"
1801 );
1802
1803 let pool = NDArrayPool::new(0);
1804 for id in 1..=2 {
1805 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1806 a.unique_id = id;
1807 p.process_array(&a, &pool);
1808 }
1809 assert_eq!(p.buffer().pre_buffer_len(), 2);
1810
1811 let r = write(&mut p, SOFT_TRIG, 1);
1812 assert!(p.buffer().is_triggered());
1813 if expect_flush {
1814 let ids: Vec<_> = r.output_arrays.iter().map(|a| a.unique_id).collect();
1815 assert_eq!(ids, vec![1, 2], "FlushOnSoftTrig = {flush_on}: flushed");
1816 assert_eq!(p.buffer().pre_buffer_len(), 0);
1817 } else {
1818 assert!(
1819 r.output_arrays.is_empty(),
1820 "FlushOnSoftTrig = {flush_on}: C does not flush from the write"
1821 );
1822 assert_eq!(p.buffer().pre_buffer_len(), 2);
1823 }
1824 }
1825 }
1826
1827 #[test]
1828 fn test_buffer_status_transitions() {
1829 let mut cb = CircularBuffer::new(2, 1, TriggerCondition::External);
1830
1831 assert_eq!(cb.status(), BufferStatus::Idle);
1833
1834 cb.start();
1837 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1838
1839 cb.push(make_array(1));
1840 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1841
1842 cb.push(make_array(2));
1843 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1844
1845 cb.trigger();
1847 assert_eq!(cb.status(), BufferStatus::Flushing);
1848
1849 let done = cb.push(make_array(3));
1851 assert!(done.sequence_done);
1852 assert_eq!(cb.status(), BufferStatus::BufferFilling);
1853
1854 cb.reset();
1856 assert_eq!(cb.status(), BufferStatus::Idle);
1857 assert_eq!(cb.trigger_count(), 0);
1858 }
1859}