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
202 .attributes
203 .get("FilePluginDestination")
204 .and_then(|attr| attr.value.as_string_typed())
205 {
206 Some(dest) => {
210 if dest.is_empty() {
213 return true;
214 }
215 let prefix = dest.len().min(3);
219 let matches_all = dest.as_bytes()[..prefix].eq_ignore_ascii_case(&b"all"[..prefix]);
220 matches_all || dest.eq_ignore_ascii_case(&self.port_name)
221 }
222 None => true,
223 }
224 }
225
226 fn refresh_file_path_exists(&mut self, updates: &mut Vec<ParamUpdate>) {
230 let idx = match self.params.file_path_exists {
231 Some(idx) => idx,
232 None => return,
233 };
234 let (normalized, exists) = check_file_path(&self.file_base.file_path);
235 self.file_base.file_path = normalized;
236 updates.push(ParamUpdate::Int32 {
237 reason: idx,
238 addr: 0,
239 value: if exists { 1 } else { 0 },
240 });
241 }
242
243 fn apply_filename_attributes(
248 &mut self,
249 array: &NDArray,
250 updates: &mut Vec<ParamUpdate>,
251 ) -> bool {
252 let mut reopen = false;
253 if let Some(name) = array
257 .attributes
258 .get("FilePluginFileName")
259 .and_then(|attr| attr.value.as_string_typed())
260 {
261 if !name.is_empty() && name != self.file_base.file_name {
262 self.file_base.file_name = name.to_string();
263 reopen = true;
264 if let Some(idx) = self.params.file_name {
265 updates.push(ParamUpdate::Octet {
266 reason: idx,
267 addr: 0,
268 value: name.to_string(),
269 });
270 }
271 }
272 }
273 if let Some(attr) = array.attributes.get("FilePluginFileNumber") {
274 if let Some(num) = attr.value.as_i64() {
275 let num = num as i32;
276 if num != self.file_base.file_number {
277 self.file_base.file_number = num;
278 self.file_base.auto_increment = false; reopen = true;
280 if let Some(idx) = self.params.file_number {
281 updates.push(ParamUpdate::Int32 {
282 reason: idx,
283 addr: 0,
284 value: num,
285 });
286 }
287 }
288 }
289 }
290 reopen
291 }
292
293 pub fn process_array(&mut self, array: &NDArray) -> ProcessResult {
295 let mut proc_result = ProcessResult::empty();
296 let array = Arc::new(array.clone());
297 self.latest_array = Some(array.clone());
298
299 if !self.destination_matches(&array) {
301 return proc_result;
302 }
303
304 self.refresh_file_path_exists(&mut proc_result.param_updates);
309
310 let force_close = array
312 .attributes
313 .get("FilePluginClose")
314 .and_then(|a| a.value.as_i64())
315 .map(|v| v != 0)
316 .unwrap_or(false);
317 if force_close {
318 if let Err(e) = self.file_base.force_close(&mut self.writer) {
319 return ProcessResult::sink(self.error_updates(false, false, e.to_string()));
320 }
321 let _ = self.stop_capture(&mut proc_result.param_updates);
322 return proc_result;
323 }
324
325 let result = match self.file_base.mode() {
326 NDFileMode::Single => {
327 if self.auto_save {
328 let r = self.write_single(array);
329 if r.is_ok() {
330 self.saved_frames += 1; }
332 r
333 } else {
334 Ok(())
335 }
336 }
337 NDFileMode::Capture => {
338 if self.capture_active {
339 if !self.frame_valid(&array) {
341 return proc_result;
342 }
343 self.file_base.capture_array(array);
344 self.push_num_captured_update(&mut proc_result.param_updates);
345 let target = self.file_base.num_capture_target();
346 if target > 0 && self.file_base.num_captured() >= target {
347 if self.auto_save {
348 let to_save = self.file_base.num_captured() as i32;
349 if let Err(err) = self.file_base.flush_capture(&mut self.writer) {
350 Err(err)
351 } else {
352 self.saved_frames += to_save; self.push_full_file_name_update(&mut proc_result.param_updates);
354 self.push_num_captured_update(&mut proc_result.param_updates);
355 self.stop_capture(&mut proc_result.param_updates).ok();
356 Ok(())
357 }
358 } else {
359 self.stop_capture(&mut proc_result.param_updates).ok();
360 Ok(())
361 }
362 } else {
363 Ok(())
364 }
365 } else {
366 Ok(())
367 }
368 }
369 NDFileMode::Stream => {
370 if self.capture_active {
371 if !self.frame_valid(&array) {
373 return proc_result;
374 }
375 let reopen =
377 self.apply_filename_attributes(&array, &mut proc_result.param_updates);
378 if reopen && self.file_base.is_open() {
379 if let Err(e) = self.file_base.force_close(&mut self.writer) {
380 return ProcessResult::sink(self.error_updates(
381 false,
382 false,
383 e.to_string(),
384 ));
385 }
386 }
387 let r = self.file_base.process_array(array, &mut self.writer);
388 if r.is_ok() {
389 self.saved_frames += 1; self.push_num_captured_update(&mut proc_result.param_updates);
395 }
396 let target = self.file_base.num_capture_target();
397 if r.is_ok() && target > 0 && self.file_base.num_captured() >= target {
398 if let Err(e) = self.file_base.close_stream(&mut self.writer) {
399 return ProcessResult::sink(self.error_updates(
400 false,
401 false,
402 e.to_string(),
403 ));
404 }
405 self.stop_capture(&mut proc_result.param_updates).ok();
406 self.push_full_file_name_update(&mut proc_result.param_updates);
407 }
408 r
409 } else {
410 Ok(())
411 }
412 }
413 };
414
415 if result.is_ok() {
416 proc_result.param_updates.extend(self.success_updates());
417 if self.file_base.mode() == NDFileMode::Single && self.auto_save {
418 self.push_full_file_name_update(&mut proc_result.param_updates);
419 }
420 if self.file_base.mode() == NDFileMode::Stream && self.capture_active {
421 self.push_full_file_name_update(&mut proc_result.param_updates);
422 }
423 if let Some(idx) = self.params.array_counter {
427 proc_result.param_updates.push(ParamUpdate::Int32 {
428 reason: idx,
429 addr: 0,
430 value: self.saved_frames,
431 });
432 }
433 } else if let Err(err) = result {
434 proc_result.param_updates = self.error_updates(false, false, err.to_string());
435 }
436 proc_result
437 }
438
439 pub fn on_param_change(
441 &mut self,
442 reason: usize,
443 params: &PluginParamSnapshot,
444 ) -> ParamChangeResult {
445 let mut updates = Vec::new();
446
447 if Some(reason) == self.params.file_path {
448 if let ParamChangeValue::Octet(s) = ¶ms.value {
449 let (normalized, exists) = check_file_path(s);
450 self.file_base.file_path = normalized;
451 if let Some(idx) = self.params.file_path_exists {
452 updates.push(ParamUpdate::Int32 {
453 reason: idx,
454 addr: 0,
455 value: if exists { 1 } else { 0 },
456 });
457 }
458 }
459 } else if Some(reason) == self.params.file_name {
460 if let ParamChangeValue::Octet(s) = ¶ms.value {
461 self.file_base.file_name = s.clone();
462 }
463 } else if Some(reason) == self.params.file_number {
464 self.file_base.file_number = params.value.as_i32();
465 } else if Some(reason) == self.params.file_template {
466 if let ParamChangeValue::Octet(s) = ¶ms.value {
467 self.file_base.file_template = s.clone();
468 }
469 } else if Some(reason) == self.params.auto_increment {
470 self.file_base.auto_increment = params.value.as_i32() != 0;
471 } else if Some(reason) == self.params.auto_save {
472 self.auto_save = params.value.as_i32() != 0;
473 } else if Some(reason) == self.params.write_mode {
474 let new_mode = NDFileMode::from_i32(params.value.as_i32());
477 if self.capture_active && new_mode != self.file_base.mode() {
478 if let Err(e) = self.stop_capture(&mut updates) {
479 return ParamChangeResult::updates(self.error_updates(
480 false,
481 false,
482 e.to_string(),
483 ));
484 }
485 }
486 self.file_base.set_mode(new_mode);
487 } else if Some(reason) == self.params.num_capture {
488 self.file_base
491 .set_num_capture(params.value.as_i32().max(0) as usize);
492 } else if Some(reason) == self.params.create_dir {
493 self.file_base.create_dir = params.value.as_i32();
494 } else if Some(reason) == self.params.file_temp_suffix {
495 if let ParamChangeValue::Octet(s) = ¶ms.value {
496 self.file_base.temp_suffix = s.clone();
497 }
498 } else if Some(reason) == self.params.write_file {
499 if params.value.as_i32() != 0 {
500 let result = match self.file_base.mode() {
501 NDFileMode::Single => {
502 if let Some(array) = self.latest_array.clone() {
503 self.write_single(array)
504 } else {
505 Err(ADError::UnsupportedConversion(
506 "no array available for write".into(),
507 ))
508 }
509 }
510 NDFileMode::Capture => self.file_base.flush_capture(&mut self.writer),
511 NDFileMode::Stream => {
512 if let Some(array) = self.latest_array.clone() {
513 self.file_base.process_array(array, &mut self.writer)
514 } else {
515 Err(ADError::UnsupportedConversion(
516 "no array available for write".into(),
517 ))
518 }
519 }
520 };
521 match result {
522 Ok(()) => {
523 updates.extend(self.success_updates());
524 self.push_num_captured_update(&mut updates);
525 self.push_full_file_name_update(&mut updates);
526 }
527 Err(err) => {
528 return ParamChangeResult::updates(self.error_updates(
529 false,
530 true,
531 err.to_string(),
532 ));
533 }
534 }
535 }
536 } else if Some(reason) == self.params.read_file {
537 if params.value.as_i32() != 0 {
538 let result = (|| -> ADResult<Arc<NDArray>> {
539 let path = PathBuf::from(self.file_base.create_file_name());
540 self.writer.open_file(
541 &path,
542 NDFileMode::Single,
543 &NDArray::new(vec![NDDimension::new(1)], NDDataType::UInt8),
544 )?;
545 let array = Arc::new(self.writer.read_file()?);
546 self.writer.close_file()?;
547 self.latest_array = Some(array.clone());
548 Ok(array)
549 })();
550 match result {
551 Ok(array) => {
552 updates.extend(self.success_updates());
553 self.push_full_file_name_update(&mut updates);
554 return ParamChangeResult::combined(vec![array], updates);
555 }
556 Err(err) => {
557 return ParamChangeResult::updates(self.error_updates(
558 true,
559 false,
560 err.to_string(),
561 ));
562 }
563 }
564 }
565 } else if Some(reason) == self.params.lazy_open {
566 self.lazy_open = params.value.as_i32() != 0;
567 } else if Some(reason) == self.params.delete_driver_file {
568 self.delete_driver_file = params.value.as_i32() != 0;
569 } else if Some(reason) == self.params.free_capture {
570 if params.value.as_i32() != 0 {
571 self.file_base.clear_capture();
572 self.push_num_captured_update(&mut updates);
573 }
574 } else if Some(reason) == self.params.capture {
575 if params.value.as_i32() != 0 {
577 if self.file_base.mode() == NDFileMode::Single {
578 let _ = self.stop_capture(&mut updates);
580 return ParamChangeResult::updates(self.error_updates(
581 false,
582 false,
583 "ERROR: capture not supported in Single mode".into(),
584 ));
585 }
586 if let Err(e) = self.start_capture(&mut updates) {
587 return ParamChangeResult::updates(self.error_updates(
588 false,
589 false,
590 e.to_string(),
591 ));
592 }
593 } else if let Err(e) = self.stop_capture(&mut updates) {
594 return ParamChangeResult::updates(self.error_updates(false, false, e.to_string()));
595 }
596 }
597
598 ParamChangeResult::updates(updates)
599 }
600
601 fn write_single(&mut self, array: Arc<NDArray>) -> ADResult<()> {
604 self.file_base.ensure_directory()?;
605 self.file_base.process_array(array, &mut self.writer)
606 }
607
608 fn success_updates(&self) -> Vec<ParamUpdate> {
609 let mut updates = Vec::new();
610 if let Some(idx) = self.params.file_number {
611 updates.push(ParamUpdate::Int32 {
612 reason: idx,
613 addr: 0,
614 value: self.file_base.file_number,
615 });
616 }
617 if let Some(idx) = self.params.write_status {
618 updates.push(ParamUpdate::Int32 {
619 reason: idx,
620 addr: 0,
621 value: 0,
622 });
623 }
624 if let Some(idx) = self.params.write_message {
625 updates.push(ParamUpdate::Octet {
626 reason: idx,
627 addr: 0,
628 value: String::new(),
629 });
630 }
631 if let Some(idx) = self.params.write_file {
632 updates.push(ParamUpdate::Int32 {
633 reason: idx,
634 addr: 0,
635 value: 0,
636 });
637 }
638 if let Some(idx) = self.params.capture {
639 updates.push(ParamUpdate::Int32 {
640 reason: idx,
641 addr: 0,
642 value: if self.capture_active { 1 } else { 0 },
643 });
644 }
645 if let Some(idx) = self.params.read_file {
646 updates.push(ParamUpdate::Int32 {
647 reason: idx,
648 addr: 0,
649 value: 0,
650 });
651 }
652 updates
653 }
654
655 fn push_capture_update(&self, updates: &mut Vec<ParamUpdate>) {
658 if let Some(idx) = self.params.capture {
659 updates.push(ParamUpdate::Int32 {
660 reason: idx,
661 addr: 0,
662 value: if self.capture_active { 1 } else { 0 },
663 });
664 }
665 }
666
667 fn push_num_captured_update(&self, updates: &mut Vec<ParamUpdate>) {
668 if let Some(idx) = self.params.num_captured {
669 updates.push(ParamUpdate::Int32 {
670 reason: idx,
671 addr: 0,
672 value: self.file_base.num_captured() as i32,
673 });
674 }
675 }
676
677 fn push_full_file_name_update(&self, updates: &mut Vec<ParamUpdate>) {
678 if let Some(idx) = self.params.full_file_name {
679 updates.push(ParamUpdate::Octet {
680 reason: idx,
681 addr: 0,
682 value: self.file_base.last_written_name().to_string(),
683 });
684 }
685 }
686
687 fn error_updates(
688 &self,
689 read_reason: bool,
690 write_reason: bool,
691 message: String,
692 ) -> Vec<ParamUpdate> {
693 let mut updates = Vec::new();
694 if write_reason {
695 if let Some(idx) = self.params.write_file {
696 updates.push(ParamUpdate::Int32 {
697 reason: idx,
698 addr: 0,
699 value: 0,
700 });
701 }
702 }
703 if read_reason {
704 if let Some(idx) = self.params.read_file {
705 updates.push(ParamUpdate::Int32 {
706 reason: idx,
707 addr: 0,
708 value: 0,
709 });
710 }
711 }
712 if let Some(idx) = self.params.write_status {
713 updates.push(ParamUpdate::Int32 {
714 reason: idx,
715 addr: 0,
716 value: 1,
717 });
718 }
719 if let Some(idx) = self.params.write_message {
720 updates.push(ParamUpdate::Octet {
721 reason: idx,
722 addr: 0,
723 value: message,
724 });
725 }
726 updates
727 }
728}
729
730fn check_file_path(path: &str) -> (String, bool) {
734 let mut normalized = path.to_string();
735 let exists = crate::driver::ndarray_driver::check_path_str(&mut normalized);
736 (normalized, exists)
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
743 use crate::ndarray::{NDArray, NDDataType, NDDimension};
744 use std::path::Path;
745
746 struct MockWriter {
748 opens: usize,
749 writes: usize,
750 closes: usize,
751 multi: bool,
752 }
753 impl MockWriter {
754 fn new(multi: bool) -> Self {
755 Self {
756 opens: 0,
757 writes: 0,
758 closes: 0,
759 multi,
760 }
761 }
762 }
763 impl NDFileWriter for MockWriter {
764 fn open_file(&mut self, _p: &Path, _m: NDFileMode, _a: &NDArray) -> ADResult<()> {
765 self.opens += 1;
766 Ok(())
767 }
768 fn write_file(&mut self, _a: &NDArray) -> ADResult<()> {
769 self.writes += 1;
770 Ok(())
771 }
772 fn read_file(&mut self) -> ADResult<NDArray> {
773 Err(ADError::UnsupportedConversion("n/a".into()))
774 }
775 fn close_file(&mut self) -> ADResult<()> {
776 self.closes += 1;
777 Ok(())
778 }
779 fn supports_multiple_arrays(&self) -> bool {
780 self.multi
781 }
782 }
783
784 fn array(id: i32) -> NDArray {
785 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
786 a.unique_id = id;
787 a
788 }
789
790 fn with_str_attr(mut a: NDArray, name: &str, val: &str) -> NDArray {
791 a.attributes.add(NDAttribute::new_static(
792 name,
793 "",
794 NDAttrSource::Driver,
795 NDAttrValue::String(val.to_string()),
796 ));
797 a
798 }
799
800 fn with_i32_attr(mut a: NDArray, name: &str, val: i32) -> NDArray {
801 a.attributes.add(NDAttribute::new_static(
802 name,
803 "",
804 NDAttrSource::Driver,
805 NDAttrValue::Int32(val),
806 ));
807 a
808 }
809
810 #[test]
811 fn test_g9_destination_routing_skips_other_port() {
812 let mut c = FilePluginController::new(MockWriter::new(true));
815 c.set_port_name("MYFILE");
816 c.file_base.set_mode(NDFileMode::Single);
817 c.auto_save = true;
818
819 c.process_array(&with_str_attr(array(1), "FilePluginDestination", "OTHER"));
821 assert_eq!(c.writer.writes, 0, "frame for OTHER port must be skipped");
822
823 c.process_array(&with_str_attr(array(2), "FilePluginDestination", "MYFILE"));
825 assert_eq!(c.writer.writes, 1);
826
827 c.process_array(&with_str_attr(array(3), "FilePluginDestination", "all"));
829 assert_eq!(c.writer.writes, 2);
830 }
831
832 #[test]
833 fn test_g9_numeric_destination_attr_processed_not_stringified() {
834 let mut c = FilePluginController::new(MockWriter::new(true));
839 c.set_port_name("MYFILE");
840 c.file_base.set_mode(NDFileMode::Single);
841 c.auto_save = true;
842
843 c.process_array(&with_i32_attr(array(1), "FilePluginDestination", 42));
844 assert_eq!(
845 c.writer.writes, 1,
846 "numeric FilePluginDestination must be ignored (frame processed), \
847 not stringified to \"42\" and skipped"
848 );
849 }
850
851 #[test]
852 fn test_g9_numeric_filename_attr_ignored() {
853 let mut c = FilePluginController::new(MockWriter::new(true));
857 c.set_port_name("F");
858 let before = c.file_base.file_name.clone();
859 let mut updates = Vec::new();
860 let reopen = c.apply_filename_attributes(
861 &with_i32_attr(array(1), "FilePluginFileName", 7),
862 &mut updates,
863 );
864 assert!(
865 !reopen,
866 "numeric FilePluginFileName must not force a reopen"
867 );
868 assert_eq!(
869 c.file_base.file_name, before,
870 "numeric FilePluginFileName must not redefine the filename"
871 );
872 assert!(
873 updates.is_empty(),
874 "numeric FilePluginFileName must not post a FileName param update"
875 );
876 }
877
878 #[test]
879 fn test_g9_destination_compare_matches_c_attr_is_processing_required() {
880 let mut c = FilePluginController::new(MockWriter::new(true));
884 c.set_port_name("MYFILE");
885 c.file_base.set_mode(NDFileMode::Single);
886 c.auto_save = true;
887
888 c.process_array(&with_str_attr(array(1), "FilePluginDestination", "allfoo"));
890 assert_eq!(
891 c.writer.writes, 1,
892 "destination with \"all\" prefix is processed (C 3-char prefix match)"
893 );
894
895 c.process_array(&with_str_attr(array(2), "FilePluginDestination", "x"));
898 assert_eq!(
899 c.writer.writes, 1,
900 "1-char non-matching destination is skipped, not blanket-processed"
901 );
902 }
903
904 #[test]
905 fn test_g9_file_close_attribute_forces_close() {
906 let mut c = FilePluginController::new(MockWriter::new(true));
908 c.set_port_name("F");
909 c.file_base.set_mode(NDFileMode::Stream);
910 c.file_base.set_num_capture(10);
911 c.lazy_open = true;
912 let mut updates = Vec::new();
913 c.process_array(&array(1)); c.start_capture(&mut updates).unwrap();
915 let _ = &updates;
916 c.process_array(&array(2)); assert!(c.file_base.is_open());
918
919 c.process_array(&with_i32_attr(array(3), "FilePluginClose", 1));
920 assert!(
921 !c.file_base.is_open(),
922 "FilePluginClose must close the file"
923 );
924 assert!(!c.capture_active, "close attribute stops capture");
925 }
926
927 #[test]
928 fn test_b8_capture_owner_round_trip() {
929 let mut c = FilePluginController::new(MockWriter::new(true));
931 c.set_port_name("F");
932 c.file_base.set_mode(NDFileMode::Capture);
933 c.params.capture = Some(7);
934 let mut updates = Vec::new();
935 c.start_capture(&mut updates).unwrap();
936 assert!(c.capture_active);
937 c.stop_capture(&mut updates).unwrap();
938 assert!(!c.capture_active);
939 assert!(!updates.is_empty());
941 }
942
943 #[test]
944 fn test_b9_non_lazy_opens_eagerly_at_capture_start() {
945 let mut c = FilePluginController::new(MockWriter::new(true));
947 c.set_port_name("F");
948 c.file_base.set_mode(NDFileMode::Stream);
949 c.lazy_open = false;
950 c.process_array(&array(1)); let mut updates = Vec::new();
952 c.start_capture(&mut updates).unwrap();
953 assert!(
954 c.file_base.is_open(),
955 "non-lazy stream opens at capture start"
956 );
957 assert_eq!(c.writer.opens, 1);
958 }
959
960 #[test]
961 fn test_b9_lazy_defers_open_to_first_frame() {
962 let mut c = FilePluginController::new(MockWriter::new(true));
963 c.set_port_name("F");
964 c.file_base.set_mode(NDFileMode::Stream);
965 c.file_base.set_num_capture(10);
966 c.lazy_open = true;
967 c.process_array(&array(1));
968 let mut updates = Vec::new();
969 c.start_capture(&mut updates).unwrap();
970 assert!(
971 !c.file_base.is_open(),
972 "lazy stream does NOT open at capture start"
973 );
974 c.process_array(&array(2));
975 assert!(c.file_base.is_open(), "lazy stream opens on first frame");
976 }
977
978 #[test]
979 fn test_g12_capture_mode_validates_frames() {
980 let mut c = FilePluginController::new(MockWriter::new(true));
982 c.set_port_name("F");
983 c.file_base.set_mode(NDFileMode::Capture);
984 c.file_base.set_num_capture(10);
985 let mut updates = Vec::new();
986 c.start_capture(&mut updates).unwrap();
987
988 c.process_array(&array(1)); assert_eq!(c.file_base.num_captured(), 1);
990
991 let mut big = NDArray::new(vec![NDDimension::new(8)], NDDataType::UInt8);
993 big.unique_id = 2;
994 c.process_array(&big);
995 assert_eq!(c.file_base.num_captured(), 1, "mismatched frame rejected");
996
997 c.process_array(&array(3));
999 assert_eq!(c.file_base.num_captured(), 2);
1000 }
1001
1002 #[test]
1003 fn test_b17_write_mode_switch_closes_open_stream() {
1004 let mut c = FilePluginController::new(MockWriter::new(true));
1006 c.set_port_name("F");
1007 c.file_base.set_mode(NDFileMode::Stream);
1008 c.file_base.set_num_capture(10);
1009 c.params.write_mode = Some(5);
1010 c.lazy_open = false;
1011 c.process_array(&array(1));
1012 let mut updates = Vec::new();
1013 c.start_capture(&mut updates).unwrap();
1014 assert!(c.file_base.is_open());
1015
1016 let snap = PluginParamSnapshot {
1018 enable_callbacks: true,
1019 reason: 5,
1020 addr: 0,
1021 value: ParamChangeValue::Int32(NDFileMode::Capture as i32),
1022 };
1023 c.on_param_change(5, &snap);
1024 assert!(
1025 !c.file_base.is_open(),
1026 "mode switch must close the open stream"
1027 );
1028 assert!(!c.capture_active);
1029 }
1030
1031 #[test]
1032 fn test_b7_capture_num_capture_zero_buffers_forever() {
1033 let mut c = FilePluginController::new(MockWriter::new(true));
1035 c.set_port_name("F");
1036 c.file_base.set_mode(NDFileMode::Capture);
1037 c.file_base.set_num_capture(0);
1038 c.auto_save = true;
1039 let mut updates = Vec::new();
1040 c.start_capture(&mut updates).unwrap();
1041 for id in 1..=5 {
1042 c.process_array(&array(id));
1043 }
1044 assert_eq!(
1045 c.file_base.num_captured(),
1046 5,
1047 "all frames buffered, no flush"
1048 );
1049 assert_eq!(c.writer.writes, 0, "num_capture==0 never auto-flushes");
1050 assert!(c.capture_active, "still capturing");
1051 }
1052
1053 #[test]
1054 fn stream_mode_publishes_num_captured_per_frame() {
1055 let mut c = FilePluginController::new(MockWriter::new(true));
1060 c.set_port_name("F");
1061 c.params.num_captured = Some(42);
1062 c.file_base.set_mode(NDFileMode::Stream);
1063 c.file_base.set_num_capture(0);
1064 let mut updates = Vec::new();
1065 c.process_array(&array(1)); c.start_capture(&mut updates).unwrap();
1067 for (id, expected) in [(2, 1), (3, 2)] {
1068 let r = c.process_array(&array(id));
1069 let published = r.param_updates.iter().find_map(|u| match u {
1070 ParamUpdate::Int32 {
1071 reason: 42, value, ..
1072 } => Some(*value),
1073 _ => None,
1074 });
1075 assert_eq!(
1076 published,
1077 Some(expected),
1078 "frame {id}: NUM_CAPTURED update missing or wrong"
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn test_g10_array_counter_counts_saved_frames() {
1085 let mut c = FilePluginController::new(MockWriter::new(false));
1087 c.set_port_name("F");
1088 c.params.array_counter = Some(99);
1089 c.file_base.set_mode(NDFileMode::Single);
1090 c.auto_save = true;
1091 let r1 = c.process_array(&array(1));
1092 let counter1 = r1.param_updates.iter().find_map(|u| match u {
1093 ParamUpdate::Int32 {
1094 reason: 99, value, ..
1095 } => Some(*value),
1096 _ => None,
1097 });
1098 assert_eq!(counter1, Some(1), "first saved frame → ArrayCounter 1");
1099 let r2 = c.process_array(&array(2));
1100 let counter2 = r2.param_updates.iter().find_map(|u| match u {
1101 ParamUpdate::Int32 {
1102 reason: 99, value, ..
1103 } => Some(*value),
1104 _ => None,
1105 });
1106 assert_eq!(counter2, Some(2));
1107 }
1108}