1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::error::{ADError, ADResult};
5use crate::ndarray::{NDArray, NDDataType, NDDimension};
6
7use super::file_base::{NDFileMode, NDFileWriter, NDPluginFileBase};
8use super::runtime::{
9 ParamChangeResult, ParamChangeValue, ParamUpdate, PluginParamSnapshot, ProcessResult,
10};
11
12#[derive(Default)]
14pub struct FileParamIndices {
15 pub file_path: Option<usize>,
16 pub file_name: Option<usize>,
17 pub file_number: Option<usize>,
18 pub file_template: Option<usize>,
19 pub auto_increment: Option<usize>,
20 pub write_file: Option<usize>,
21 pub read_file: Option<usize>,
22 pub write_mode: Option<usize>,
23 pub num_capture: Option<usize>,
24 pub capture: Option<usize>,
25 pub auto_save: Option<usize>,
26 pub create_dir: Option<usize>,
27 pub file_path_exists: Option<usize>,
28 pub write_status: Option<usize>,
29 pub write_message: Option<usize>,
30 pub full_file_name: Option<usize>,
31 pub file_temp_suffix: Option<usize>,
32 pub num_captured: Option<usize>,
33 pub lazy_open: Option<usize>,
34 pub delete_driver_file: Option<usize>,
35 pub free_capture: Option<usize>,
36 pub array_counter: Option<usize>,
39}
40
41pub struct FilePluginController<W: NDFileWriter> {
57 pub file_base: NDPluginFileBase,
58 pub writer: W,
59 pub params: FileParamIndices,
60 pub auto_save: bool,
61 pub capture_active: bool,
64 pub lazy_open: bool,
65 pub delete_driver_file: bool,
66 pub latest_array: Option<Arc<NDArray>>,
67 stream_dims: Option<Vec<usize>>,
70 stream_data_type: Option<NDDataType>,
72 port_name: String,
74 saved_frames: i32,
78}
79
80impl<W: NDFileWriter> FilePluginController<W> {
81 pub fn new(writer: W) -> Self {
82 Self {
83 file_base: NDPluginFileBase::new(),
84 writer,
85 params: FileParamIndices::default(),
86 auto_save: false,
87 capture_active: false,
88 lazy_open: false,
89 delete_driver_file: false,
90 latest_array: None,
91 stream_dims: None,
92 stream_data_type: None,
93 port_name: String::new(),
94 saved_frames: 0,
95 }
96 }
97
98 pub fn set_port_name(&mut self, name: impl Into<String>) {
101 self.port_name = name.into();
102 }
103
104 pub fn register_params(
106 &mut self,
107 base: &mut asyn_rs::port::PortDriverBase,
108 ) -> asyn_rs::error::AsynResult<()> {
109 self.params.file_path = base.find_param("FILE_PATH");
110 self.params.file_name = base.find_param("FILE_NAME");
111 self.params.file_number = base.find_param("FILE_NUMBER");
112 self.params.file_template = base.find_param("FILE_TEMPLATE");
113 self.params.auto_increment = base.find_param("AUTO_INCREMENT");
114 self.params.write_file = base.find_param("WRITE_FILE");
115 self.params.read_file = base.find_param("READ_FILE");
116 self.params.write_mode = base.find_param("WRITE_MODE");
117 self.params.num_capture = base.find_param("NUM_CAPTURE");
118 self.params.capture = base.find_param("CAPTURE");
119 self.params.auto_save = base.find_param("AUTO_SAVE");
120 self.params.create_dir = base.find_param("CREATE_DIR");
121 self.params.file_path_exists = base.find_param("FILE_PATH_EXISTS");
122 self.params.write_status = base.find_param("WRITE_STATUS");
123 self.params.write_message = base.find_param("WRITE_MESSAGE");
124 self.params.full_file_name = base.find_param("FULL_FILE_NAME");
125 self.params.file_temp_suffix = base.find_param("FILE_TEMP_SUFFIX");
126 self.params.num_captured = base.find_param("NUM_CAPTURED");
127 self.params.lazy_open = base.find_param("FILE_LAZY_OPEN");
128 self.params.delete_driver_file = base.find_param("DELETE_DRIVER_FILE");
129 self.params.free_capture = base.find_param("FREE_CAPTURE");
130 self.params.array_counter = base.find_param("ARRAY_COUNTER");
131 Ok(())
132 }
133
134 fn start_capture(&mut self, updates: &mut Vec<ParamUpdate>) -> ADResult<()> {
144 self.file_base.clear_capture();
145 self.stream_dims = None;
146 self.stream_data_type = None;
147 self.file_base.lazy_open = self.lazy_open;
148 self.file_base.delete_driver_file = self.delete_driver_file;
149
150 if self.file_base.mode() == NDFileMode::Stream
151 && !self.lazy_open
152 && self.writer.supports_multiple_arrays()
153 {
154 if let Some(array) = self.latest_array.clone() {
156 self.file_base.open_stream_eager(&mut self.writer, &array)?;
157 }
158 }
159 self.capture_active = true;
160 self.push_capture_update(updates);
161 self.push_num_captured_update(updates);
162 Ok(())
163 }
164
165 fn stop_capture(&mut self, updates: &mut Vec<ParamUpdate>) -> ADResult<()> {
170 if self.file_base.mode() == NDFileMode::Stream {
171 self.file_base.close_stream(&mut self.writer)?;
172 }
173 self.capture_active = false;
174 self.stream_dims = None;
175 self.stream_data_type = None;
176 self.push_capture_update(updates);
177 Ok(())
178 }
179
180 fn frame_valid(&mut self, array: &NDArray) -> bool {
184 let frame_dims: Vec<usize> = array.dims.iter().map(|d| d.size).collect();
185 let frame_dtype = array.data.data_type();
186 match (&self.stream_dims, self.stream_data_type) {
187 (Some(dims), Some(dtype)) => &frame_dims == dims && frame_dtype == dtype,
188 _ => {
189 self.stream_dims = Some(frame_dims);
190 self.stream_data_type = Some(frame_dtype);
191 true
192 }
193 }
194 }
195
196 fn destination_matches(&self, array: &NDArray) -> bool {
201 match array.attributes.get("FilePluginDestination") {
202 Some(attr) => {
203 let dest = attr.value.as_string();
204 if dest.len() <= 1 {
205 return true;
206 }
207 dest.eq_ignore_ascii_case("all") || dest.eq_ignore_ascii_case(&self.port_name)
208 }
209 None => true,
210 }
211 }
212
213 fn refresh_file_path_exists(&self, updates: &mut Vec<ParamUpdate>) {
217 let idx = match self.params.file_path_exists {
218 Some(idx) => idx,
219 None => return,
220 };
221 let path = self
222 .file_base
223 .file_path
224 .trim_end_matches(std::path::MAIN_SEPARATOR);
225 let exists = !path.is_empty() && std::path::Path::new(path).is_dir();
226 updates.push(ParamUpdate::Int32 {
227 reason: idx,
228 addr: 0,
229 value: if exists { 1 } else { 0 },
230 });
231 }
232
233 fn apply_filename_attributes(
238 &mut self,
239 array: &NDArray,
240 updates: &mut Vec<ParamUpdate>,
241 ) -> bool {
242 let mut reopen = false;
243 if let Some(attr) = array.attributes.get("FilePluginFileName") {
244 let name = attr.value.as_string();
245 if !name.is_empty() {
246 if name != self.file_base.file_name {
247 self.file_base.file_name = name.clone();
248 reopen = true;
249 if let Some(idx) = self.params.file_name {
250 updates.push(ParamUpdate::Octet {
251 reason: idx,
252 addr: 0,
253 value: name,
254 });
255 }
256 }
257 }
258 }
259 if let Some(attr) = array.attributes.get("FilePluginFileNumber") {
260 if let Some(num) = attr.value.as_i64() {
261 let num = num as i32;
262 if num != self.file_base.file_number {
263 self.file_base.file_number = num;
264 self.file_base.auto_increment = false; reopen = true;
266 if let Some(idx) = self.params.file_number {
267 updates.push(ParamUpdate::Int32 {
268 reason: idx,
269 addr: 0,
270 value: num,
271 });
272 }
273 }
274 }
275 }
276 reopen
277 }
278
279 pub fn process_array(&mut self, array: &NDArray) -> ProcessResult {
281 let mut proc_result = ProcessResult::empty();
282 let array = Arc::new(array.clone());
283 self.latest_array = Some(array.clone());
284
285 if !self.destination_matches(&array) {
287 return proc_result;
288 }
289
290 self.refresh_file_path_exists(&mut proc_result.param_updates);
295
296 let force_close = array
298 .attributes
299 .get("FilePluginClose")
300 .and_then(|a| a.value.as_i64())
301 .map(|v| v != 0)
302 .unwrap_or(false);
303 if force_close {
304 if let Err(e) = self.file_base.force_close(&mut self.writer) {
305 return ProcessResult::sink(self.error_updates(false, false, e.to_string()));
306 }
307 let _ = self.stop_capture(&mut proc_result.param_updates);
308 return proc_result;
309 }
310
311 let result = match self.file_base.mode() {
312 NDFileMode::Single => {
313 if self.auto_save {
314 let r = self.write_single(array);
315 if r.is_ok() {
316 self.saved_frames += 1; }
318 r
319 } else {
320 Ok(())
321 }
322 }
323 NDFileMode::Capture => {
324 if self.capture_active {
325 if !self.frame_valid(&array) {
327 return proc_result;
328 }
329 self.file_base.capture_array(array);
330 self.push_num_captured_update(&mut proc_result.param_updates);
331 let target = self.file_base.num_capture_target();
332 if target > 0 && self.file_base.num_captured() >= target {
333 if self.auto_save {
334 let to_save = self.file_base.num_captured() as i32;
335 if let Err(err) = self.file_base.flush_capture(&mut self.writer) {
336 Err(err)
337 } else {
338 self.saved_frames += to_save; self.push_full_file_name_update(&mut proc_result.param_updates);
340 self.push_num_captured_update(&mut proc_result.param_updates);
341 self.stop_capture(&mut proc_result.param_updates).ok();
342 Ok(())
343 }
344 } else {
345 self.stop_capture(&mut proc_result.param_updates).ok();
346 Ok(())
347 }
348 } else {
349 Ok(())
350 }
351 } else {
352 Ok(())
353 }
354 }
355 NDFileMode::Stream => {
356 if self.capture_active {
357 if !self.frame_valid(&array) {
359 return proc_result;
360 }
361 let reopen =
363 self.apply_filename_attributes(&array, &mut proc_result.param_updates);
364 if reopen && self.file_base.is_open() {
365 if let Err(e) = self.file_base.force_close(&mut self.writer) {
366 return ProcessResult::sink(self.error_updates(
367 false,
368 false,
369 e.to_string(),
370 ));
371 }
372 }
373 let r = self.file_base.process_array(array, &mut self.writer);
374 if r.is_ok() {
375 self.saved_frames += 1; }
377 let target = self.file_base.num_capture_target();
378 if r.is_ok() && target > 0 && self.file_base.num_captured() >= target {
379 if let Err(e) = self.file_base.close_stream(&mut self.writer) {
380 return ProcessResult::sink(self.error_updates(
381 false,
382 false,
383 e.to_string(),
384 ));
385 }
386 self.stop_capture(&mut proc_result.param_updates).ok();
387 self.push_full_file_name_update(&mut proc_result.param_updates);
388 self.push_num_captured_update(&mut proc_result.param_updates);
389 }
390 r
391 } else {
392 Ok(())
393 }
394 }
395 };
396
397 if result.is_ok() {
398 proc_result.param_updates.extend(self.success_updates());
399 if self.file_base.mode() == NDFileMode::Single && self.auto_save {
400 self.push_full_file_name_update(&mut proc_result.param_updates);
401 }
402 if self.file_base.mode() == NDFileMode::Stream && self.capture_active {
403 self.push_full_file_name_update(&mut proc_result.param_updates);
404 }
405 if let Some(idx) = self.params.array_counter {
409 proc_result.param_updates.push(ParamUpdate::Int32 {
410 reason: idx,
411 addr: 0,
412 value: self.saved_frames,
413 });
414 }
415 } else if let Err(err) = result {
416 proc_result.param_updates = self.error_updates(false, false, err.to_string());
417 }
418 proc_result
419 }
420
421 pub fn on_param_change(
423 &mut self,
424 reason: usize,
425 params: &PluginParamSnapshot,
426 ) -> ParamChangeResult {
427 let mut updates = Vec::new();
428
429 if Some(reason) == self.params.file_path {
430 if let ParamChangeValue::Octet(s) = ¶ms.value {
431 let normalized = normalize_file_path(s);
432 self.file_base.file_path = normalized.clone();
433 let exists =
434 std::path::Path::new(normalized.trim_end_matches(std::path::MAIN_SEPARATOR))
435 .is_dir();
436 if let Some(idx) = self.params.file_path_exists {
437 updates.push(ParamUpdate::Int32 {
438 reason: idx,
439 addr: 0,
440 value: if exists { 1 } else { 0 },
441 });
442 }
443 }
444 } else if Some(reason) == self.params.file_name {
445 if let ParamChangeValue::Octet(s) = ¶ms.value {
446 self.file_base.file_name = s.clone();
447 }
448 } else if Some(reason) == self.params.file_number {
449 self.file_base.file_number = params.value.as_i32();
450 } else if Some(reason) == self.params.file_template {
451 if let ParamChangeValue::Octet(s) = ¶ms.value {
452 self.file_base.file_template = s.clone();
453 }
454 } else if Some(reason) == self.params.auto_increment {
455 self.file_base.auto_increment = params.value.as_i32() != 0;
456 } else if Some(reason) == self.params.auto_save {
457 self.auto_save = params.value.as_i32() != 0;
458 } else if Some(reason) == self.params.write_mode {
459 let new_mode = NDFileMode::from_i32(params.value.as_i32());
462 if self.capture_active && new_mode != self.file_base.mode() {
463 if let Err(e) = self.stop_capture(&mut updates) {
464 return ParamChangeResult::updates(self.error_updates(
465 false,
466 false,
467 e.to_string(),
468 ));
469 }
470 }
471 self.file_base.set_mode(new_mode);
472 } else if Some(reason) == self.params.num_capture {
473 self.file_base
476 .set_num_capture(params.value.as_i32().max(0) as usize);
477 } else if Some(reason) == self.params.create_dir {
478 self.file_base.create_dir = params.value.as_i32();
479 } else if Some(reason) == self.params.file_temp_suffix {
480 if let ParamChangeValue::Octet(s) = ¶ms.value {
481 self.file_base.temp_suffix = s.clone();
482 }
483 } else if Some(reason) == self.params.write_file {
484 if params.value.as_i32() != 0 {
485 let result = match self.file_base.mode() {
486 NDFileMode::Single => {
487 if let Some(array) = self.latest_array.clone() {
488 self.write_single(array)
489 } else {
490 Err(ADError::UnsupportedConversion(
491 "no array available for write".into(),
492 ))
493 }
494 }
495 NDFileMode::Capture => self.file_base.flush_capture(&mut self.writer),
496 NDFileMode::Stream => {
497 if let Some(array) = self.latest_array.clone() {
498 self.file_base.process_array(array, &mut self.writer)
499 } else {
500 Err(ADError::UnsupportedConversion(
501 "no array available for write".into(),
502 ))
503 }
504 }
505 };
506 match result {
507 Ok(()) => {
508 updates.extend(self.success_updates());
509 self.push_num_captured_update(&mut updates);
510 self.push_full_file_name_update(&mut updates);
511 }
512 Err(err) => {
513 return ParamChangeResult::updates(self.error_updates(
514 false,
515 true,
516 err.to_string(),
517 ));
518 }
519 }
520 }
521 } else if Some(reason) == self.params.read_file {
522 if params.value.as_i32() != 0 {
523 let result = (|| -> ADResult<Arc<NDArray>> {
524 let path = PathBuf::from(self.file_base.create_file_name());
525 self.writer.open_file(
526 &path,
527 NDFileMode::Single,
528 &NDArray::new(vec![NDDimension::new(1)], NDDataType::UInt8),
529 )?;
530 let array = Arc::new(self.writer.read_file()?);
531 self.writer.close_file()?;
532 self.latest_array = Some(array.clone());
533 Ok(array)
534 })();
535 match result {
536 Ok(array) => {
537 updates.extend(self.success_updates());
538 self.push_full_file_name_update(&mut updates);
539 return ParamChangeResult::combined(vec![array], updates);
540 }
541 Err(err) => {
542 return ParamChangeResult::updates(self.error_updates(
543 true,
544 false,
545 err.to_string(),
546 ));
547 }
548 }
549 }
550 } else if Some(reason) == self.params.lazy_open {
551 self.lazy_open = params.value.as_i32() != 0;
552 } else if Some(reason) == self.params.delete_driver_file {
553 self.delete_driver_file = params.value.as_i32() != 0;
554 } else if Some(reason) == self.params.free_capture {
555 if params.value.as_i32() != 0 {
556 self.file_base.clear_capture();
557 self.push_num_captured_update(&mut updates);
558 }
559 } else if Some(reason) == self.params.capture {
560 if params.value.as_i32() != 0 {
562 if self.file_base.mode() == NDFileMode::Single {
563 let _ = self.stop_capture(&mut updates);
565 return ParamChangeResult::updates(self.error_updates(
566 false,
567 false,
568 "ERROR: capture not supported in Single mode".into(),
569 ));
570 }
571 if let Err(e) = self.start_capture(&mut updates) {
572 return ParamChangeResult::updates(self.error_updates(
573 false,
574 false,
575 e.to_string(),
576 ));
577 }
578 } else if let Err(e) = self.stop_capture(&mut updates) {
579 return ParamChangeResult::updates(self.error_updates(false, false, e.to_string()));
580 }
581 }
582
583 ParamChangeResult::updates(updates)
584 }
585
586 fn write_single(&mut self, array: Arc<NDArray>) -> ADResult<()> {
589 self.file_base.ensure_directory()?;
590 self.file_base.process_array(array, &mut self.writer)
591 }
592
593 fn success_updates(&self) -> Vec<ParamUpdate> {
594 let mut updates = Vec::new();
595 if let Some(idx) = self.params.file_number {
596 updates.push(ParamUpdate::Int32 {
597 reason: idx,
598 addr: 0,
599 value: self.file_base.file_number,
600 });
601 }
602 if let Some(idx) = self.params.write_status {
603 updates.push(ParamUpdate::Int32 {
604 reason: idx,
605 addr: 0,
606 value: 0,
607 });
608 }
609 if let Some(idx) = self.params.write_message {
610 updates.push(ParamUpdate::Octet {
611 reason: idx,
612 addr: 0,
613 value: String::new(),
614 });
615 }
616 if let Some(idx) = self.params.write_file {
617 updates.push(ParamUpdate::Int32 {
618 reason: idx,
619 addr: 0,
620 value: 0,
621 });
622 }
623 if let Some(idx) = self.params.capture {
624 updates.push(ParamUpdate::Int32 {
625 reason: idx,
626 addr: 0,
627 value: if self.capture_active { 1 } else { 0 },
628 });
629 }
630 if let Some(idx) = self.params.read_file {
631 updates.push(ParamUpdate::Int32 {
632 reason: idx,
633 addr: 0,
634 value: 0,
635 });
636 }
637 updates
638 }
639
640 fn push_capture_update(&self, updates: &mut Vec<ParamUpdate>) {
643 if let Some(idx) = self.params.capture {
644 updates.push(ParamUpdate::Int32 {
645 reason: idx,
646 addr: 0,
647 value: if self.capture_active { 1 } else { 0 },
648 });
649 }
650 }
651
652 fn push_num_captured_update(&self, updates: &mut Vec<ParamUpdate>) {
653 if let Some(idx) = self.params.num_captured {
654 updates.push(ParamUpdate::Int32 {
655 reason: idx,
656 addr: 0,
657 value: self.file_base.num_captured() as i32,
658 });
659 }
660 }
661
662 fn push_full_file_name_update(&self, updates: &mut Vec<ParamUpdate>) {
663 if let Some(idx) = self.params.full_file_name {
664 updates.push(ParamUpdate::Octet {
665 reason: idx,
666 addr: 0,
667 value: self.file_base.last_written_name().to_string(),
668 });
669 }
670 }
671
672 fn error_updates(
673 &self,
674 read_reason: bool,
675 write_reason: bool,
676 message: String,
677 ) -> Vec<ParamUpdate> {
678 let mut updates = Vec::new();
679 if write_reason {
680 if let Some(idx) = self.params.write_file {
681 updates.push(ParamUpdate::Int32 {
682 reason: idx,
683 addr: 0,
684 value: 0,
685 });
686 }
687 }
688 if read_reason {
689 if let Some(idx) = self.params.read_file {
690 updates.push(ParamUpdate::Int32 {
691 reason: idx,
692 addr: 0,
693 value: 0,
694 });
695 }
696 }
697 if let Some(idx) = self.params.write_status {
698 updates.push(ParamUpdate::Int32 {
699 reason: idx,
700 addr: 0,
701 value: 1,
702 });
703 }
704 if let Some(idx) = self.params.write_message {
705 updates.push(ParamUpdate::Octet {
706 reason: idx,
707 addr: 0,
708 value: message,
709 });
710 }
711 updates
712 }
713}
714
715fn normalize_file_path(path: &str) -> String {
716 if path.is_empty() || path.ends_with(std::path::MAIN_SEPARATOR) {
717 path.to_string()
718 } else {
719 format!("{path}{}", std::path::MAIN_SEPARATOR)
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
727 use crate::ndarray::{NDArray, NDDataType, NDDimension};
728 use std::path::Path;
729
730 struct MockWriter {
732 opens: usize,
733 writes: usize,
734 closes: usize,
735 multi: bool,
736 }
737 impl MockWriter {
738 fn new(multi: bool) -> Self {
739 Self {
740 opens: 0,
741 writes: 0,
742 closes: 0,
743 multi,
744 }
745 }
746 }
747 impl NDFileWriter for MockWriter {
748 fn open_file(&mut self, _p: &Path, _m: NDFileMode, _a: &NDArray) -> ADResult<()> {
749 self.opens += 1;
750 Ok(())
751 }
752 fn write_file(&mut self, _a: &NDArray) -> ADResult<()> {
753 self.writes += 1;
754 Ok(())
755 }
756 fn read_file(&mut self) -> ADResult<NDArray> {
757 Err(ADError::UnsupportedConversion("n/a".into()))
758 }
759 fn close_file(&mut self) -> ADResult<()> {
760 self.closes += 1;
761 Ok(())
762 }
763 fn supports_multiple_arrays(&self) -> bool {
764 self.multi
765 }
766 }
767
768 fn array(id: i32) -> NDArray {
769 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
770 a.unique_id = id;
771 a
772 }
773
774 fn with_str_attr(mut a: NDArray, name: &str, val: &str) -> NDArray {
775 a.attributes.add(NDAttribute::new_static(
776 name,
777 "",
778 NDAttrSource::Driver,
779 NDAttrValue::String(val.to_string()),
780 ));
781 a
782 }
783
784 fn with_i32_attr(mut a: NDArray, name: &str, val: i32) -> NDArray {
785 a.attributes.add(NDAttribute::new_static(
786 name,
787 "",
788 NDAttrSource::Driver,
789 NDAttrValue::Int32(val),
790 ));
791 a
792 }
793
794 #[test]
795 fn test_g9_destination_routing_skips_other_port() {
796 let mut c = FilePluginController::new(MockWriter::new(true));
799 c.set_port_name("MYFILE");
800 c.file_base.set_mode(NDFileMode::Single);
801 c.auto_save = true;
802
803 c.process_array(&with_str_attr(array(1), "FilePluginDestination", "OTHER"));
805 assert_eq!(c.writer.writes, 0, "frame for OTHER port must be skipped");
806
807 c.process_array(&with_str_attr(array(2), "FilePluginDestination", "MYFILE"));
809 assert_eq!(c.writer.writes, 1);
810
811 c.process_array(&with_str_attr(array(3), "FilePluginDestination", "all"));
813 assert_eq!(c.writer.writes, 2);
814 }
815
816 #[test]
817 fn test_g9_file_close_attribute_forces_close() {
818 let mut c = FilePluginController::new(MockWriter::new(true));
820 c.set_port_name("F");
821 c.file_base.set_mode(NDFileMode::Stream);
822 c.file_base.set_num_capture(10);
823 c.lazy_open = true;
824 let mut updates = Vec::new();
825 c.process_array(&array(1)); c.start_capture(&mut updates).unwrap();
827 let _ = &updates;
828 c.process_array(&array(2)); assert!(c.file_base.is_open());
830
831 c.process_array(&with_i32_attr(array(3), "FilePluginClose", 1));
832 assert!(
833 !c.file_base.is_open(),
834 "FilePluginClose must close the file"
835 );
836 assert!(!c.capture_active, "close attribute stops capture");
837 }
838
839 #[test]
840 fn test_b8_capture_owner_round_trip() {
841 let mut c = FilePluginController::new(MockWriter::new(true));
843 c.set_port_name("F");
844 c.file_base.set_mode(NDFileMode::Capture);
845 c.params.capture = Some(7);
846 let mut updates = Vec::new();
847 c.start_capture(&mut updates).unwrap();
848 assert!(c.capture_active);
849 c.stop_capture(&mut updates).unwrap();
850 assert!(!c.capture_active);
851 assert!(!updates.is_empty());
853 }
854
855 #[test]
856 fn test_b9_non_lazy_opens_eagerly_at_capture_start() {
857 let mut c = FilePluginController::new(MockWriter::new(true));
859 c.set_port_name("F");
860 c.file_base.set_mode(NDFileMode::Stream);
861 c.lazy_open = false;
862 c.process_array(&array(1)); let mut updates = Vec::new();
864 c.start_capture(&mut updates).unwrap();
865 assert!(
866 c.file_base.is_open(),
867 "non-lazy stream opens at capture start"
868 );
869 assert_eq!(c.writer.opens, 1);
870 }
871
872 #[test]
873 fn test_b9_lazy_defers_open_to_first_frame() {
874 let mut c = FilePluginController::new(MockWriter::new(true));
875 c.set_port_name("F");
876 c.file_base.set_mode(NDFileMode::Stream);
877 c.file_base.set_num_capture(10);
878 c.lazy_open = true;
879 c.process_array(&array(1));
880 let mut updates = Vec::new();
881 c.start_capture(&mut updates).unwrap();
882 assert!(
883 !c.file_base.is_open(),
884 "lazy stream does NOT open at capture start"
885 );
886 c.process_array(&array(2));
887 assert!(c.file_base.is_open(), "lazy stream opens on first frame");
888 }
889
890 #[test]
891 fn test_g12_capture_mode_validates_frames() {
892 let mut c = FilePluginController::new(MockWriter::new(true));
894 c.set_port_name("F");
895 c.file_base.set_mode(NDFileMode::Capture);
896 c.file_base.set_num_capture(10);
897 let mut updates = Vec::new();
898 c.start_capture(&mut updates).unwrap();
899
900 c.process_array(&array(1)); assert_eq!(c.file_base.num_captured(), 1);
902
903 let mut big = NDArray::new(vec![NDDimension::new(8)], NDDataType::UInt8);
905 big.unique_id = 2;
906 c.process_array(&big);
907 assert_eq!(c.file_base.num_captured(), 1, "mismatched frame rejected");
908
909 c.process_array(&array(3));
911 assert_eq!(c.file_base.num_captured(), 2);
912 }
913
914 #[test]
915 fn test_b17_write_mode_switch_closes_open_stream() {
916 let mut c = FilePluginController::new(MockWriter::new(true));
918 c.set_port_name("F");
919 c.file_base.set_mode(NDFileMode::Stream);
920 c.file_base.set_num_capture(10);
921 c.params.write_mode = Some(5);
922 c.lazy_open = false;
923 c.process_array(&array(1));
924 let mut updates = Vec::new();
925 c.start_capture(&mut updates).unwrap();
926 assert!(c.file_base.is_open());
927
928 let snap = PluginParamSnapshot {
930 enable_callbacks: true,
931 reason: 5,
932 addr: 0,
933 value: ParamChangeValue::Int32(NDFileMode::Capture as i32),
934 };
935 c.on_param_change(5, &snap);
936 assert!(
937 !c.file_base.is_open(),
938 "mode switch must close the open stream"
939 );
940 assert!(!c.capture_active);
941 }
942
943 #[test]
944 fn test_b7_capture_num_capture_zero_buffers_forever() {
945 let mut c = FilePluginController::new(MockWriter::new(true));
947 c.set_port_name("F");
948 c.file_base.set_mode(NDFileMode::Capture);
949 c.file_base.set_num_capture(0);
950 c.auto_save = true;
951 let mut updates = Vec::new();
952 c.start_capture(&mut updates).unwrap();
953 for id in 1..=5 {
954 c.process_array(&array(id));
955 }
956 assert_eq!(
957 c.file_base.num_captured(),
958 5,
959 "all frames buffered, no flush"
960 );
961 assert_eq!(c.writer.writes, 0, "num_capture==0 never auto-flushes");
962 assert!(c.capture_active, "still capturing");
963 }
964
965 #[test]
966 fn test_g10_array_counter_counts_saved_frames() {
967 let mut c = FilePluginController::new(MockWriter::new(false));
969 c.set_port_name("F");
970 c.params.array_counter = Some(99);
971 c.file_base.set_mode(NDFileMode::Single);
972 c.auto_save = true;
973 let r1 = c.process_array(&array(1));
974 let counter1 = r1.param_updates.iter().find_map(|u| match u {
975 ParamUpdate::Int32 {
976 reason: 99, value, ..
977 } => Some(*value),
978 _ => None,
979 });
980 assert_eq!(counter1, Some(1), "first saved frame → ArrayCounter 1");
981 let r2 = c.process_array(&array(2));
982 let counter2 = r2.param_updates.iter().find_map(|u| match u {
983 ParamUpdate::Int32 {
984 reason: 99, value, ..
985 } => Some(*value),
986 _ => None,
987 });
988 assert_eq!(counter2, Some(2));
989 }
990}