1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::error::{ADError, ADResult};
5use crate::finalize::Finalize;
6use crate::ndarray::{NDArray, NDDataType, NDDimension};
7
8use super::file_base::{NDFileMode, NDFileWriter, NDPluginFileBase, with_open_file};
9use super::runtime::{
10 ParamChangeResult, ParamChangeValue, ParamUpdate, PluginParamSnapshot, ProcessResult,
11};
12
13#[derive(Default)]
15pub struct FileParamIndices {
16 pub file_path: Option<usize>,
17 pub file_name: Option<usize>,
18 pub file_number: Option<usize>,
19 pub file_template: Option<usize>,
20 pub auto_increment: Option<usize>,
21 pub write_file: Option<usize>,
22 pub read_file: Option<usize>,
23 pub write_mode: Option<usize>,
24 pub num_capture: Option<usize>,
25 pub capture: Option<usize>,
26 pub auto_save: Option<usize>,
27 pub create_dir: Option<usize>,
28 pub file_path_exists: Option<usize>,
29 pub write_status: Option<usize>,
30 pub write_message: Option<usize>,
31 pub full_file_name: Option<usize>,
32 pub file_temp_suffix: Option<usize>,
33 pub num_captured: Option<usize>,
34 pub lazy_open: Option<usize>,
35 pub delete_driver_file: Option<usize>,
36 pub free_capture: Option<usize>,
37 pub array_counter: Option<usize>,
40}
41
42pub struct FilePluginController<W: NDFileWriter> {
58 pub file_base: NDPluginFileBase,
59 pub writer: W,
60 pub params: FileParamIndices,
61 pub auto_save: bool,
62 pub capture_active: bool,
65 pub lazy_open: bool,
66 pub delete_driver_file: bool,
67 pub latest_array: Option<Arc<NDArray>>,
68 stream_dims: Option<Vec<usize>>,
71 stream_data_type: Option<NDDataType>,
73 port_name: String,
75 saved_frames: i32,
79}
80
81impl<W: NDFileWriter> FilePluginController<W> {
82 pub fn new(writer: W) -> Self {
83 Self {
84 file_base: NDPluginFileBase::new(),
85 writer,
86 params: FileParamIndices::default(),
87 auto_save: false,
88 capture_active: false,
89 lazy_open: false,
90 delete_driver_file: false,
91 latest_array: None,
92 stream_dims: None,
93 stream_data_type: None,
94 port_name: String::new(),
95 saved_frames: 0,
96 }
97 }
98
99 pub fn set_port_name(&mut self, name: impl Into<String>) {
102 self.port_name = name.into();
103 }
104
105 pub fn register_params(
107 &mut self,
108 base: &mut asyn_rs::port::PortDriverBase,
109 ) -> asyn_rs::error::AsynResult<()> {
110 self.params.file_path = base.find_param("FILE_PATH");
111 self.params.file_name = base.find_param("FILE_NAME");
112 self.params.file_number = base.find_param("FILE_NUMBER");
113 self.params.file_template = base.find_param("FILE_TEMPLATE");
114 self.params.auto_increment = base.find_param("AUTO_INCREMENT");
115 self.params.write_file = base.find_param("WRITE_FILE");
116 self.params.read_file = base.find_param("READ_FILE");
117 self.params.write_mode = base.find_param("WRITE_MODE");
118 self.params.num_capture = base.find_param("NUM_CAPTURE");
119 self.params.capture = base.find_param("CAPTURE");
120 self.params.auto_save = base.find_param("AUTO_SAVE");
121 self.params.create_dir = base.find_param("CREATE_DIR");
122 self.params.file_path_exists = base.find_param("FILE_PATH_EXISTS");
123 self.params.write_status = base.find_param("WRITE_STATUS");
124 self.params.write_message = base.find_param("WRITE_MESSAGE");
125 self.params.full_file_name = base.find_param("FULL_FILE_NAME");
126 self.params.file_temp_suffix = base.find_param("FILE_TEMP_SUFFIX");
127 self.params.num_captured = base.find_param("NUM_CAPTURED");
128 self.params.lazy_open = base.find_param("FILE_LAZY_OPEN");
129 self.params.delete_driver_file = base.find_param("DELETE_DRIVER_FILE");
130 self.params.free_capture = base.find_param("FREE_CAPTURE");
131 self.params.array_counter = base.find_param("ARRAY_COUNTER");
132 Ok(())
133 }
134
135 fn start_capture(&mut self, updates: &mut Vec<ParamUpdate>) -> ADResult<()> {
145 self.file_base.clear_capture();
146 self.stream_dims = None;
147 self.stream_data_type = None;
148 self.file_base.lazy_open = self.lazy_open;
149 self.file_base.delete_driver_file = self.delete_driver_file;
150
151 if self.file_base.mode() == NDFileMode::Stream
152 && !self.lazy_open
153 && self.writer.supports_multiple_arrays()
154 {
155 if let Some(array) = self.latest_array.clone() {
157 self.file_base.open_stream(&mut self.writer, &array)?;
158 }
159 }
160 self.capture_active = true;
161 self.push_capture_update(updates);
162 self.push_num_captured_update(updates);
163 Ok(())
164 }
165
166 fn stop_capture(&mut self, updates: &mut Vec<ParamUpdate>) -> ADResult<()> {
177 let mut stopping = Finalize::new(self, |ctrl: &mut Self| {
178 ctrl.capture_active = false;
179 ctrl.stream_dims = None;
180 ctrl.stream_data_type = None;
181 ctrl.push_capture_update(updates);
182 });
183 stopping.run(|ctrl| {
184 if ctrl.file_base.mode() == NDFileMode::Stream {
185 ctrl.file_base.close_stream(&mut ctrl.writer)?;
186 }
187 Ok(())
188 })
189 }
190
191 fn frame_valid(&mut self, array: &NDArray) -> bool {
195 let frame_dims: Vec<usize> = array.dims.iter().map(|d| d.size).collect();
196 let frame_dtype = array.data.data_type();
197 match (&self.stream_dims, self.stream_data_type) {
198 (Some(dims), Some(dtype)) => &frame_dims == dims && frame_dtype == dtype,
199 _ => {
200 self.stream_dims = Some(frame_dims);
201 self.stream_data_type = Some(frame_dtype);
202 true
203 }
204 }
205 }
206
207 fn destination_matches(&self, array: &NDArray) -> bool {
212 match array
213 .attributes
214 .get("FilePluginDestination")
215 .and_then(|attr| attr.value.as_string_typed())
216 {
217 Some(dest) => {
221 if dest.is_empty() {
224 return true;
225 }
226 let prefix = dest.len().min(3);
230 let matches_all = dest.as_bytes()[..prefix].eq_ignore_ascii_case(&b"all"[..prefix]);
231 matches_all || dest.eq_ignore_ascii_case(&self.port_name)
232 }
233 None => true,
234 }
235 }
236
237 fn refresh_file_path_exists(&mut self, updates: &mut Vec<ParamUpdate>) {
241 let idx = match self.params.file_path_exists {
242 Some(idx) => idx,
243 None => return,
244 };
245 let (normalized, exists) = check_file_path(&self.file_base.file_path);
246 self.file_base.file_path = normalized;
247 updates.push(ParamUpdate::Int32 {
248 reason: idx,
249 addr: 0,
250 value: if exists { 1 } else { 0 },
251 });
252 }
253
254 fn apply_filename_attributes(
259 &mut self,
260 array: &NDArray,
261 updates: &mut Vec<ParamUpdate>,
262 ) -> bool {
263 let mut reopen = false;
264 if let Some(name) = array
268 .attributes
269 .get("FilePluginFileName")
270 .and_then(|attr| attr.value.as_string_typed())
271 {
272 if !name.is_empty() && name != self.file_base.file_name {
273 self.file_base.file_name = name.to_string();
274 reopen = true;
275 if let Some(idx) = self.params.file_name {
276 updates.push(ParamUpdate::Octet {
277 reason: idx,
278 addr: 0,
279 value: name.to_string(),
280 });
281 }
282 }
283 }
284 if let Some(attr) = array.attributes.get("FilePluginFileNumber") {
285 if let Some(num) = attr.value.as_i64() {
286 let num = num as i32;
287 if num != self.file_base.file_number {
288 self.file_base.file_number = num;
289 self.file_base.auto_increment = false; reopen = true;
291 if let Some(idx) = self.params.file_number {
292 updates.push(ParamUpdate::Int32 {
293 reason: idx,
294 addr: 0,
295 value: num,
296 });
297 }
298 }
299 }
300 }
301 reopen
302 }
303
304 pub fn process_array(&mut self, array: &NDArray) -> ProcessResult {
306 let mut proc_result = ProcessResult::empty();
307 let array = Arc::new(array.clone());
308 self.latest_array = Some(array.clone());
309
310 if !self.destination_matches(&array) {
312 return proc_result;
313 }
314
315 self.refresh_file_path_exists(&mut proc_result.param_updates);
320
321 let force_close = array
323 .attributes
324 .get("FilePluginClose")
325 .and_then(|a| a.value.as_i64())
326 .map(|v| v != 0)
327 .unwrap_or(false);
328 if force_close {
329 if let Err(e) = self.file_base.force_close(&mut self.writer) {
330 self.fail_cycle(&mut proc_result, e.to_string());
331 return proc_result;
332 }
333 let _ = self.stop_capture(&mut proc_result.param_updates);
334 return proc_result;
335 }
336
337 let result = match self.file_base.mode() {
338 NDFileMode::Single => {
339 if self.auto_save {
340 let r = self.write_single(array);
341 if r.is_ok() {
342 self.saved_frames += 1; }
344 r
345 } else {
346 Ok(())
347 }
348 }
349 NDFileMode::Capture => {
350 if self.capture_active {
351 if !self.frame_valid(&array) {
353 return proc_result;
354 }
355 self.file_base.capture_array(array);
356 self.push_num_captured_update(&mut proc_result.param_updates);
357 let target = self.file_base.num_capture_target();
358 if target > 0 && self.file_base.num_captured() >= target {
359 if self.auto_save {
360 let to_save = self.file_base.num_captured() as i32;
361 if let Err(err) = self.file_base.flush_capture(&mut self.writer) {
362 Err(err)
363 } else {
364 self.saved_frames += to_save; self.push_full_file_name_update(&mut proc_result.param_updates);
366 self.push_num_captured_update(&mut proc_result.param_updates);
367 self.stop_capture(&mut proc_result.param_updates).ok();
368 Ok(())
369 }
370 } else {
371 self.stop_capture(&mut proc_result.param_updates).ok();
372 Ok(())
373 }
374 } else {
375 Ok(())
376 }
377 } else {
378 Ok(())
379 }
380 }
381 NDFileMode::Stream => {
382 if self.capture_active {
383 if !self.frame_valid(&array) {
385 return proc_result;
386 }
387 let reopen =
389 self.apply_filename_attributes(&array, &mut proc_result.param_updates);
390 if reopen && self.file_base.is_open() {
391 if let Err(e) = self.file_base.force_close(&mut self.writer) {
392 self.fail_cycle(&mut proc_result, e.to_string());
393 return proc_result;
394 }
395 }
396 let r = self.file_base.process_array(array, &mut self.writer);
397 if r.is_ok() {
398 self.saved_frames += 1; self.push_num_captured_update(&mut proc_result.param_updates);
404 }
405 let target = self.file_base.num_capture_target();
406 if r.is_ok() && target > 0 && self.file_base.num_captured() >= target {
407 if let Err(e) = self.file_base.close_stream(&mut self.writer) {
408 self.fail_cycle(&mut proc_result, e.to_string());
409 return proc_result;
410 }
411 self.stop_capture(&mut proc_result.param_updates).ok();
412 self.push_full_file_name_update(&mut proc_result.param_updates);
413 }
414 r
415 } else {
416 Ok(())
417 }
418 }
419 };
420
421 if result.is_ok() {
422 proc_result.param_updates.extend(self.success_updates());
423 if self.file_base.mode() == NDFileMode::Single && self.auto_save {
424 self.push_full_file_name_update(&mut proc_result.param_updates);
425 }
426 if self.file_base.mode() == NDFileMode::Stream && self.capture_active {
427 self.push_full_file_name_update(&mut proc_result.param_updates);
428 }
429 if let Some(idx) = self.params.array_counter {
433 proc_result.param_updates.push(ParamUpdate::Int32 {
434 reason: idx,
435 addr: 0,
436 value: self.saved_frames,
437 });
438 }
439 } else if let Err(err) = result {
440 self.fail_cycle(&mut proc_result, err.to_string());
441 }
442 proc_result
443 }
444
445 pub fn on_param_change(
447 &mut self,
448 reason: usize,
449 params: &PluginParamSnapshot,
450 ) -> ParamChangeResult {
451 let mut updates = Vec::new();
452
453 if Some(reason) == self.params.file_path {
454 if let ParamChangeValue::Octet(s) = ¶ms.value {
455 let (normalized, exists) = check_file_path(s);
456 self.file_base.file_path = normalized;
457 if let Some(idx) = self.params.file_path_exists {
458 updates.push(ParamUpdate::Int32 {
459 reason: idx,
460 addr: 0,
461 value: if exists { 1 } else { 0 },
462 });
463 }
464 }
465 } else if Some(reason) == self.params.file_name {
466 if let ParamChangeValue::Octet(s) = ¶ms.value {
467 self.file_base.file_name = s.clone();
468 }
469 } else if Some(reason) == self.params.file_number {
470 self.file_base.file_number = params.value.as_i32();
471 } else if Some(reason) == self.params.file_template {
472 if let ParamChangeValue::Octet(s) = ¶ms.value {
473 self.file_base.file_template = s.clone();
474 }
475 } else if Some(reason) == self.params.auto_increment {
476 self.file_base.auto_increment = params.value.as_i32() != 0;
477 } else if Some(reason) == self.params.auto_save {
478 self.auto_save = params.value.as_i32() != 0;
479 } else if Some(reason) == self.params.write_mode {
480 let new_mode = NDFileMode::from_i32(params.value.as_i32());
483 if self.capture_active && new_mode != self.file_base.mode() {
484 if let Err(e) = self.stop_capture(&mut updates) {
485 self.push_error_updates(&mut updates, false, false, e.to_string());
486 return ParamChangeResult::updates(updates);
487 }
488 }
489 self.file_base.set_mode(new_mode);
490 } else if Some(reason) == self.params.num_capture {
491 self.file_base
494 .set_num_capture(params.value.as_i32().max(0) as usize);
495 } else if Some(reason) == self.params.create_dir {
496 self.file_base.create_dir = params.value.as_i32();
497 } else if Some(reason) == self.params.file_temp_suffix {
498 if let ParamChangeValue::Octet(s) = ¶ms.value {
499 self.file_base.temp_suffix = s.clone();
500 }
501 } else if Some(reason) == self.params.write_file {
502 if params.value.as_i32() != 0 {
503 let result = match self.file_base.mode() {
504 NDFileMode::Single => {
505 if let Some(array) = self.latest_array.clone() {
506 self.write_single(array)
507 } else {
508 Err(ADError::UnsupportedConversion(
509 "no array available for write".into(),
510 ))
511 }
512 }
513 NDFileMode::Capture => self.file_base.flush_capture(&mut self.writer),
514 NDFileMode::Stream => {
515 if let Some(array) = self.latest_array.clone() {
516 self.file_base.process_array(array, &mut self.writer)
517 } else {
518 Err(ADError::UnsupportedConversion(
519 "no array available for write".into(),
520 ))
521 }
522 }
523 };
524 match result {
525 Ok(()) => {
526 updates.extend(self.success_updates());
527 self.push_num_captured_update(&mut updates);
528 self.push_full_file_name_update(&mut updates);
529 }
530 Err(err) => {
531 self.push_error_updates(&mut updates, false, true, err.to_string());
535 self.push_file_base_readbacks(&mut updates);
536 return ParamChangeResult::updates(updates);
537 }
538 }
539 }
540 } else if Some(reason) == self.params.read_file {
541 if params.value.as_i32() != 0 {
542 let result = (|| -> ADResult<Arc<NDArray>> {
543 let path = PathBuf::from(self.file_base.create_file_name());
544 let layout = NDArray::new(vec![NDDimension::new(1)], NDDataType::UInt8);
545 let array = with_open_file(
546 &mut self.writer,
547 &path,
548 NDFileMode::Single,
549 &layout,
550 |w| w.read_file().map(Arc::new),
551 )?;
552 self.latest_array = Some(array.clone());
553 Ok(array)
554 })();
555 match result {
556 Ok(array) => {
557 updates.extend(self.success_updates());
558 self.push_full_file_name_update(&mut updates);
559 return ParamChangeResult::combined(vec![array], updates);
560 }
561 Err(err) => {
562 self.push_error_updates(&mut updates, true, false, err.to_string());
563 return ParamChangeResult::updates(updates);
564 }
565 }
566 }
567 } else if Some(reason) == self.params.lazy_open {
568 self.lazy_open = params.value.as_i32() != 0;
569 } else if Some(reason) == self.params.delete_driver_file {
570 self.delete_driver_file = params.value.as_i32() != 0;
571 } else if Some(reason) == self.params.free_capture {
572 if params.value.as_i32() != 0 {
573 self.file_base.clear_capture();
574 self.push_num_captured_update(&mut updates);
575 }
576 } else if Some(reason) == self.params.capture {
577 if params.value.as_i32() != 0 {
579 if self.file_base.mode() == NDFileMode::Single {
580 let _ = self.stop_capture(&mut updates);
582 self.push_error_updates(
583 &mut updates,
584 false,
585 false,
586 "ERROR: capture not supported in Single mode".into(),
587 );
588 return ParamChangeResult::updates(updates);
589 }
590 if let Err(e) = self.start_capture(&mut updates) {
591 self.push_error_updates(&mut updates, false, false, e.to_string());
592 return ParamChangeResult::updates(updates);
593 }
594 } else if let Err(e) = self.stop_capture(&mut updates) {
595 self.push_error_updates(&mut updates, false, false, e.to_string());
596 return ParamChangeResult::updates(updates);
597 }
598 }
599
600 ParamChangeResult::updates(updates)
601 }
602
603 fn write_single(&mut self, array: Arc<NDArray>) -> ADResult<()> {
606 self.file_base.ensure_directory()?;
607 self.file_base.process_array(array, &mut self.writer)
608 }
609
610 fn success_updates(&self) -> Vec<ParamUpdate> {
611 let mut updates = Vec::new();
612 self.push_file_number_update(&mut updates);
613 if let Some(idx) = self.params.write_status {
614 updates.push(ParamUpdate::Int32 {
615 reason: idx,
616 addr: 0,
617 value: 0,
618 });
619 }
620 if let Some(idx) = self.params.write_message {
621 updates.push(ParamUpdate::Octet {
622 reason: idx,
623 addr: 0,
624 value: String::new(),
625 });
626 }
627 if let Some(idx) = self.params.write_file {
628 updates.push(ParamUpdate::Int32 {
629 reason: idx,
630 addr: 0,
631 value: 0,
632 });
633 }
634 self.push_capture_update(&mut updates);
635 if let Some(idx) = self.params.read_file {
636 updates.push(ParamUpdate::Int32 {
637 reason: idx,
638 addr: 0,
639 value: 0,
640 });
641 }
642 updates
643 }
644
645 fn push_capture_update(&self, updates: &mut Vec<ParamUpdate>) {
648 if let Some(idx) = self.params.capture {
649 updates.push(ParamUpdate::Int32 {
650 reason: idx,
651 addr: 0,
652 value: if self.capture_active { 1 } else { 0 },
653 });
654 }
655 }
656
657 fn push_num_captured_update(&self, updates: &mut Vec<ParamUpdate>) {
658 if let Some(idx) = self.params.num_captured {
659 updates.push(ParamUpdate::Int32 {
660 reason: idx,
661 addr: 0,
662 value: self.file_base.num_captured() as i32,
663 });
664 }
665 }
666
667 fn push_file_number_update(&self, updates: &mut Vec<ParamUpdate>) {
668 if let Some(idx) = self.params.file_number {
669 updates.push(ParamUpdate::Int32 {
670 reason: idx,
671 addr: 0,
672 value: self.file_base.file_number,
673 });
674 }
675 }
676
677 fn push_file_base_readbacks(&self, updates: &mut Vec<ParamUpdate>) {
690 self.push_num_captured_update(updates);
691 self.push_file_number_update(updates);
692 self.push_full_file_name_update(updates);
693 }
694
695 fn fail_cycle(&mut self, proc_result: &mut ProcessResult, message: String) {
699 self.push_error_updates(&mut proc_result.param_updates, false, false, message);
700 self.push_file_base_readbacks(&mut proc_result.param_updates);
701 }
702
703 fn push_full_file_name_update(&self, updates: &mut Vec<ParamUpdate>) {
704 if let Some(idx) = self.params.full_file_name {
705 updates.push(ParamUpdate::Octet {
706 reason: idx,
707 addr: 0,
708 value: self.file_base.last_written_name().to_string(),
709 });
710 }
711 }
712
713 fn push_error_updates(
731 &self,
732 updates: &mut Vec<ParamUpdate>,
733 read_reason: bool,
734 write_reason: bool,
735 message: String,
736 ) {
737 if write_reason {
738 if let Some(idx) = self.params.write_file {
739 updates.push(ParamUpdate::Int32 {
740 reason: idx,
741 addr: 0,
742 value: 0,
743 });
744 }
745 }
746 if read_reason {
747 if let Some(idx) = self.params.read_file {
748 updates.push(ParamUpdate::Int32 {
749 reason: idx,
750 addr: 0,
751 value: 0,
752 });
753 }
754 }
755 if let Some(idx) = self.params.write_status {
756 updates.push(ParamUpdate::Int32 {
757 reason: idx,
758 addr: 0,
759 value: 1,
760 });
761 }
762 if let Some(idx) = self.params.write_message {
763 updates.push(ParamUpdate::Octet {
764 reason: idx,
765 addr: 0,
766 value: message,
767 });
768 }
769 self.push_capture_update(updates);
773 }
774}
775
776fn check_file_path(path: &str) -> (String, bool) {
780 let mut normalized = path.to_string();
781 let exists = crate::driver::ndarray_driver::check_path_str(&mut normalized);
782 (normalized, exists)
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788 use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
789 use crate::ndarray::{NDArray, NDDataType, NDDimension};
790 use std::path::Path;
791
792 struct MockWriter {
794 opens: usize,
795 writes: usize,
796 closes: usize,
797 multi: bool,
798 fail_close: bool,
801 fail_write_after: Option<usize>,
804 }
805 impl MockWriter {
806 fn new(multi: bool) -> Self {
807 Self {
808 opens: 0,
809 writes: 0,
810 closes: 0,
811 multi,
812 fail_close: false,
813 fail_write_after: None,
814 }
815 }
816 }
817 impl NDFileWriter for MockWriter {
818 fn open_file(&mut self, _p: &Path, _m: NDFileMode, _a: &NDArray) -> ADResult<()> {
819 self.opens += 1;
820 Ok(())
821 }
822 fn write_file(&mut self, _a: &NDArray) -> ADResult<()> {
823 self.writes += 1;
824 if let Some(n) = self.fail_write_after {
825 if self.writes > n {
826 return Err(ADError::UnsupportedConversion("disk full".into()));
827 }
828 }
829 Ok(())
830 }
831 fn read_file(&mut self) -> ADResult<NDArray> {
832 Err(ADError::UnsupportedConversion("n/a".into()))
833 }
834 fn close_file(&mut self) -> ADResult<()> {
835 self.closes += 1;
836 if self.fail_close {
837 return Err(ADError::UnsupportedConversion("disk full".into()));
838 }
839 Ok(())
840 }
841 fn supports_multiple_arrays(&self) -> bool {
842 self.multi
843 }
844 }
845
846 fn array(id: i32) -> NDArray {
847 let mut a = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
848 a.unique_id = id;
849 a
850 }
851
852 fn with_str_attr(mut a: NDArray, name: &str, val: &str) -> NDArray {
853 a.attributes.add(NDAttribute::new_static(
854 name,
855 "",
856 NDAttrSource::Driver,
857 NDAttrValue::String(val.to_string()),
858 ));
859 a
860 }
861
862 fn with_i32_attr(mut a: NDArray, name: &str, val: i32) -> NDArray {
863 a.attributes.add(NDAttribute::new_static(
864 name,
865 "",
866 NDAttrSource::Driver,
867 NDAttrValue::Int32(val),
868 ));
869 a
870 }
871
872 #[test]
873 fn test_g9_destination_routing_skips_other_port() {
874 let mut c = FilePluginController::new(MockWriter::new(true));
877 c.set_port_name("MYFILE");
878 c.file_base.set_mode(NDFileMode::Single);
879 c.auto_save = true;
880
881 c.process_array(&with_str_attr(array(1), "FilePluginDestination", "OTHER"));
883 assert_eq!(c.writer.writes, 0, "frame for OTHER port must be skipped");
884
885 c.process_array(&with_str_attr(array(2), "FilePluginDestination", "MYFILE"));
887 assert_eq!(c.writer.writes, 1);
888
889 c.process_array(&with_str_attr(array(3), "FilePluginDestination", "all"));
891 assert_eq!(c.writer.writes, 2);
892 }
893
894 #[test]
895 fn test_g9_numeric_destination_attr_processed_not_stringified() {
896 let mut c = FilePluginController::new(MockWriter::new(true));
901 c.set_port_name("MYFILE");
902 c.file_base.set_mode(NDFileMode::Single);
903 c.auto_save = true;
904
905 c.process_array(&with_i32_attr(array(1), "FilePluginDestination", 42));
906 assert_eq!(
907 c.writer.writes, 1,
908 "numeric FilePluginDestination must be ignored (frame processed), \
909 not stringified to \"42\" and skipped"
910 );
911 }
912
913 #[test]
914 fn test_g9_numeric_filename_attr_ignored() {
915 let mut c = FilePluginController::new(MockWriter::new(true));
919 c.set_port_name("F");
920 let before = c.file_base.file_name.clone();
921 let mut updates = Vec::new();
922 let reopen = c.apply_filename_attributes(
923 &with_i32_attr(array(1), "FilePluginFileName", 7),
924 &mut updates,
925 );
926 assert!(
927 !reopen,
928 "numeric FilePluginFileName must not force a reopen"
929 );
930 assert_eq!(
931 c.file_base.file_name, before,
932 "numeric FilePluginFileName must not redefine the filename"
933 );
934 assert!(
935 updates.is_empty(),
936 "numeric FilePluginFileName must not post a FileName param update"
937 );
938 }
939
940 #[test]
941 fn test_g9_destination_compare_matches_c_attr_is_processing_required() {
942 let mut c = FilePluginController::new(MockWriter::new(true));
946 c.set_port_name("MYFILE");
947 c.file_base.set_mode(NDFileMode::Single);
948 c.auto_save = true;
949
950 c.process_array(&with_str_attr(array(1), "FilePluginDestination", "allfoo"));
952 assert_eq!(
953 c.writer.writes, 1,
954 "destination with \"all\" prefix is processed (C 3-char prefix match)"
955 );
956
957 c.process_array(&with_str_attr(array(2), "FilePluginDestination", "x"));
960 assert_eq!(
961 c.writer.writes, 1,
962 "1-char non-matching destination is skipped, not blanket-processed"
963 );
964 }
965
966 #[test]
967 fn test_g9_file_close_attribute_forces_close() {
968 let mut c = FilePluginController::new(MockWriter::new(true));
970 c.set_port_name("F");
971 c.file_base.set_mode(NDFileMode::Stream);
972 c.file_base.set_num_capture(10);
973 c.lazy_open = true;
974 let mut updates = Vec::new();
975 c.process_array(&array(1)); c.start_capture(&mut updates).unwrap();
977 let _ = &updates;
978 c.process_array(&array(2)); assert!(c.file_base.is_open());
980
981 c.process_array(&with_i32_attr(array(3), "FilePluginClose", 1));
982 assert!(
983 !c.file_base.is_open(),
984 "FilePluginClose must close the file"
985 );
986 assert!(!c.capture_active, "close attribute stops capture");
987 }
988
989 #[test]
990 fn test_b8_capture_owner_round_trip() {
991 let mut c = FilePluginController::new(MockWriter::new(true));
993 c.set_port_name("F");
994 c.file_base.set_mode(NDFileMode::Capture);
995 c.params.capture = Some(7);
996 let mut updates = Vec::new();
997 c.start_capture(&mut updates).unwrap();
998 assert!(c.capture_active);
999 c.stop_capture(&mut updates).unwrap();
1000 assert!(!c.capture_active);
1001 assert!(!updates.is_empty());
1003 }
1004
1005 #[test]
1006 fn test_b9_non_lazy_opens_eagerly_at_capture_start() {
1007 let mut c = FilePluginController::new(MockWriter::new(true));
1009 c.set_port_name("F");
1010 c.file_base.set_mode(NDFileMode::Stream);
1011 c.lazy_open = false;
1012 c.process_array(&array(1)); let mut updates = Vec::new();
1014 c.start_capture(&mut updates).unwrap();
1015 assert!(
1016 c.file_base.is_open(),
1017 "non-lazy stream opens at capture start"
1018 );
1019 assert_eq!(c.writer.opens, 1);
1020 }
1021
1022 #[test]
1023 fn test_b9_lazy_defers_open_to_first_frame() {
1024 let mut c = FilePluginController::new(MockWriter::new(true));
1025 c.set_port_name("F");
1026 c.file_base.set_mode(NDFileMode::Stream);
1027 c.file_base.set_num_capture(10);
1028 c.lazy_open = true;
1029 c.process_array(&array(1));
1030 let mut updates = Vec::new();
1031 c.start_capture(&mut updates).unwrap();
1032 assert!(
1033 !c.file_base.is_open(),
1034 "lazy stream does NOT open at capture start"
1035 );
1036 c.process_array(&array(2));
1037 assert!(c.file_base.is_open(), "lazy stream opens on first frame");
1038 }
1039
1040 #[test]
1041 fn test_g12_capture_mode_validates_frames() {
1042 let mut c = FilePluginController::new(MockWriter::new(true));
1044 c.set_port_name("F");
1045 c.file_base.set_mode(NDFileMode::Capture);
1046 c.file_base.set_num_capture(10);
1047 let mut updates = Vec::new();
1048 c.start_capture(&mut updates).unwrap();
1049
1050 c.process_array(&array(1)); assert_eq!(c.file_base.num_captured(), 1);
1052
1053 let mut big = NDArray::new(vec![NDDimension::new(8)], NDDataType::UInt8);
1055 big.unique_id = 2;
1056 c.process_array(&big);
1057 assert_eq!(c.file_base.num_captured(), 1, "mismatched frame rejected");
1058
1059 c.process_array(&array(3));
1061 assert_eq!(c.file_base.num_captured(), 2);
1062 }
1063
1064 #[test]
1068 fn read_file_closes_the_writer_when_the_read_fails() {
1069 let mut c = FilePluginController::new(MockWriter::new(false));
1070 c.set_port_name("F");
1071 c.params.read_file = Some(9);
1072
1073 let snap = PluginParamSnapshot {
1075 enable_callbacks: true,
1076 reason: 9,
1077 addr: 0,
1078 value: ParamChangeValue::Int32(1),
1079 };
1080 c.on_param_change(9, &snap);
1081
1082 assert_eq!(c.writer.opens, 1);
1083 assert_eq!(
1084 c.writer.closes, 1,
1085 "a failed read must still close the file it opened"
1086 );
1087 }
1088
1089 #[test]
1093 fn stop_capture_clears_capture_active_when_the_close_fails() {
1094 let mut c = FilePluginController::new(MockWriter::new(true));
1095 c.set_port_name("F");
1096 c.params.capture = Some(7);
1097 c.file_base.set_mode(NDFileMode::Stream);
1098 c.file_base.set_num_capture(0);
1099 c.process_array(&array(1));
1100
1101 let mut updates = Vec::new();
1102 c.start_capture(&mut updates).unwrap();
1103 assert!(c.capture_active);
1104 assert!(c.file_base.is_open());
1105
1106 c.writer.fail_close = true;
1107 let mut updates = Vec::new();
1108 assert!(
1109 c.stop_capture(&mut updates).is_err(),
1110 "the close failure is still reported"
1111 );
1112 assert!(
1113 !c.capture_active,
1114 "capture state must not latch on a failed close"
1115 );
1116 assert!(
1117 updates.iter().any(|u| matches!(
1118 u,
1119 ParamUpdate::Int32 {
1120 reason: 7,
1121 value: 0,
1122 ..
1123 }
1124 )),
1125 "CAPTURE=0 must still be posted"
1126 );
1127 }
1128
1129 #[test]
1132 fn capture_off_param_write_reports_error_with_capture_rbv_cleared() {
1133 let mut c = FilePluginController::new(MockWriter::new(true));
1134 c.set_port_name("F");
1135 c.params.capture = Some(7);
1136 c.file_base.set_mode(NDFileMode::Stream);
1137 c.file_base.set_num_capture(0);
1138 c.process_array(&array(1));
1139
1140 let on = PluginParamSnapshot {
1141 enable_callbacks: true,
1142 reason: 7,
1143 addr: 0,
1144 value: ParamChangeValue::Int32(1),
1145 };
1146 c.on_param_change(7, &on);
1147 assert!(c.capture_active);
1148
1149 c.writer.fail_close = true;
1150 let off = PluginParamSnapshot {
1151 enable_callbacks: true,
1152 reason: 7,
1153 addr: 0,
1154 value: ParamChangeValue::Int32(0),
1155 };
1156 let result = c.on_param_change(7, &off);
1157 assert!(!c.capture_active);
1158 assert!(
1159 result.param_updates.iter().any(|u| matches!(
1160 u,
1161 ParamUpdate::Int32 {
1162 reason: 7,
1163 value: 0,
1164 ..
1165 }
1166 )),
1167 "Capture_RBV must read 0 after the failed close"
1168 );
1169 }
1170
1171 #[test]
1172 fn test_b17_write_mode_switch_closes_open_stream() {
1173 let mut c = FilePluginController::new(MockWriter::new(true));
1175 c.set_port_name("F");
1176 c.file_base.set_mode(NDFileMode::Stream);
1177 c.file_base.set_num_capture(10);
1178 c.params.write_mode = Some(5);
1179 c.lazy_open = false;
1180 c.process_array(&array(1));
1181 let mut updates = Vec::new();
1182 c.start_capture(&mut updates).unwrap();
1183 assert!(c.file_base.is_open());
1184
1185 let snap = PluginParamSnapshot {
1187 enable_callbacks: true,
1188 reason: 5,
1189 addr: 0,
1190 value: ParamChangeValue::Int32(NDFileMode::Capture as i32),
1191 };
1192 c.on_param_change(5, &snap);
1193 assert!(
1194 !c.file_base.is_open(),
1195 "mode switch must close the open stream"
1196 );
1197 assert!(!c.capture_active);
1198 }
1199
1200 #[test]
1201 fn test_b7_capture_num_capture_zero_buffers_forever() {
1202 let mut c = FilePluginController::new(MockWriter::new(true));
1204 c.set_port_name("F");
1205 c.file_base.set_mode(NDFileMode::Capture);
1206 c.file_base.set_num_capture(0);
1207 c.auto_save = true;
1208 let mut updates = Vec::new();
1209 c.start_capture(&mut updates).unwrap();
1210 for id in 1..=5 {
1211 c.process_array(&array(id));
1212 }
1213 assert_eq!(
1214 c.file_base.num_captured(),
1215 5,
1216 "all frames buffered, no flush"
1217 );
1218 assert_eq!(c.writer.writes, 0, "num_capture==0 never auto-flushes");
1219 assert!(c.capture_active, "still capturing");
1220 }
1221
1222 #[test]
1223 fn stream_mode_publishes_num_captured_per_frame() {
1224 let mut c = FilePluginController::new(MockWriter::new(true));
1229 c.set_port_name("F");
1230 c.params.num_captured = Some(42);
1231 c.file_base.set_mode(NDFileMode::Stream);
1232 c.file_base.set_num_capture(0);
1233 let mut updates = Vec::new();
1234 c.process_array(&array(1)); c.start_capture(&mut updates).unwrap();
1236 for (id, expected) in [(2, 1), (3, 2)] {
1237 let r = c.process_array(&array(id));
1238 let published = r.param_updates.iter().find_map(|u| match u {
1239 ParamUpdate::Int32 {
1240 reason: 42, value, ..
1241 } => Some(*value),
1242 _ => None,
1243 });
1244 assert_eq!(
1245 published,
1246 Some(expected),
1247 "frame {id}: NUM_CAPTURED update missing or wrong"
1248 );
1249 }
1250 }
1251
1252 #[test]
1253 fn test_g10_array_counter_counts_saved_frames() {
1254 let mut c = FilePluginController::new(MockWriter::new(false));
1256 c.set_port_name("F");
1257 c.params.array_counter = Some(99);
1258 c.file_base.set_mode(NDFileMode::Single);
1259 c.auto_save = true;
1260 let r1 = c.process_array(&array(1));
1261 let counter1 = r1.param_updates.iter().find_map(|u| match u {
1262 ParamUpdate::Int32 {
1263 reason: 99, value, ..
1264 } => Some(*value),
1265 _ => None,
1266 });
1267 assert_eq!(counter1, Some(1), "first saved frame → ArrayCounter 1");
1268 let r2 = c.process_array(&array(2));
1269 let counter2 = r2.param_updates.iter().find_map(|u| match u {
1270 ParamUpdate::Int32 {
1271 reason: 99, value, ..
1272 } => Some(*value),
1273 _ => None,
1274 });
1275 assert_eq!(counter2, Some(2));
1276 }
1277
1278 fn int_update(updates: &[ParamUpdate], reason: usize) -> Option<i32> {
1282 updates.iter().rev().find_map(|u| match u {
1283 ParamUpdate::Int32 {
1284 reason: r, value, ..
1285 } if *r == reason => Some(*value),
1286 _ => None,
1287 })
1288 }
1289
1290 const NUM_CAPTURED: usize = 61;
1291 const WRITE_STATUS: usize = 62;
1292 const FILE_NUMBER: usize = 63;
1293 const PATH_EXISTS: usize = 64;
1294 const WRITE_FILE: usize = 65;
1295
1296 fn capture_controller(multi: bool) -> FilePluginController<MockWriter> {
1297 let mut c = FilePluginController::new(MockWriter::new(multi));
1298 c.set_port_name("F");
1299 c.params.num_captured = Some(NUM_CAPTURED);
1300 c.params.write_status = Some(WRITE_STATUS);
1301 c.params.file_number = Some(FILE_NUMBER);
1302 c.params.file_path_exists = Some(PATH_EXISTS);
1303 c.params.write_file = Some(WRITE_FILE);
1304 c.file_base.set_mode(NDFileMode::Capture);
1305 c.file_base.auto_increment = true;
1308 c.auto_save = true;
1309 c.capture_active = true;
1310 c
1311 }
1312
1313 #[test]
1319 fn a_partial_capture_flush_republishes_the_frames_still_queued() {
1320 let mut c = capture_controller(false);
1321 c.file_base.set_num_capture(4);
1322 c.writer.fail_write_after = Some(2);
1323 let first_number = c.file_base.file_number;
1324
1325 let mut last = ProcessResult::empty();
1326 for id in 1..=4 {
1327 last = c.process_array(&array(id));
1328 }
1329
1330 assert_eq!(
1331 c.file_base.num_captured(),
1332 2,
1333 "two frames never reached disk and stay queued"
1334 );
1335 assert_eq!(
1336 int_update(&last.param_updates, NUM_CAPTURED),
1337 Some(2),
1338 "the readback must name the frames still owed, not the 4 pushed \
1339 before the flush was attempted"
1340 );
1341 assert_eq!(
1342 int_update(&last.param_updates, FILE_NUMBER),
1343 Some(first_number + 2),
1344 "the two frames that DID land burned two file numbers"
1345 );
1346 assert_eq!(
1347 int_update(&last.param_updates, WRITE_STATUS),
1348 Some(1),
1349 "the failure is still reported"
1350 );
1351 }
1352
1353 #[test]
1358 fn a_partial_capture_flush_keeps_the_path_check_of_the_same_cycle() {
1359 let mut c = capture_controller(false);
1360 c.file_base.set_num_capture(4);
1361 c.writer.fail_write_after = Some(2);
1362
1363 let mut last = ProcessResult::empty();
1364 for id in 1..=4 {
1365 last = c.process_array(&array(id));
1366 }
1367
1368 assert!(
1369 int_update(&last.param_updates, PATH_EXISTS).is_some(),
1370 "FilePathExists was checked this cycle; the error exit dropped it"
1371 );
1372 }
1373
1374 #[test]
1379 fn a_failed_stream_close_keeps_the_count_the_same_cycle_published() {
1380 let mut c = FilePluginController::new(MockWriter::new(true));
1381 c.set_port_name("F");
1382 c.params.num_captured = Some(NUM_CAPTURED);
1383 c.params.write_status = Some(WRITE_STATUS);
1384 c.file_base.set_mode(NDFileMode::Stream);
1385 c.file_base.set_num_capture(1);
1386 c.capture_active = true;
1387 c.writer.fail_close = true;
1388
1389 let r = c.process_array(&array(1));
1390
1391 assert_eq!(
1392 int_update(&r.param_updates, NUM_CAPTURED),
1393 Some(1),
1394 "the frame reached disk before the close failed"
1395 );
1396 assert_eq!(
1397 int_update(&r.param_updates, WRITE_STATUS),
1398 Some(1),
1399 "the close failure is still reported"
1400 );
1401 }
1402
1403 #[test]
1407 fn a_failed_manual_write_republishes_the_capture_count() {
1408 let mut c = capture_controller(false);
1409 c.file_base.set_num_capture(0); for id in 1..=4 {
1411 c.process_array(&array(id));
1412 }
1413 assert_eq!(c.file_base.num_captured(), 4, "all four are buffered");
1414
1415 c.writer.fail_write_after = Some(2);
1416 let snap = PluginParamSnapshot {
1417 enable_callbacks: true,
1418 reason: WRITE_FILE,
1419 addr: 0,
1420 value: ParamChangeValue::Int32(1),
1421 };
1422 let r = c.on_param_change(WRITE_FILE, &snap);
1423
1424 assert_eq!(
1425 int_update(&r.param_updates, NUM_CAPTURED),
1426 Some(2),
1427 "a failed manual flush must still say how many frames are owed"
1428 );
1429 assert_eq!(int_update(&r.param_updates, WRITE_STATUS), Some(1));
1430 }
1431
1432 #[test]
1435 fn a_clean_capture_flush_reports_no_frames_left_and_no_error() {
1436 let mut c = capture_controller(false);
1437 c.file_base.set_num_capture(4);
1438
1439 let mut last = ProcessResult::empty();
1440 for id in 1..=4 {
1441 last = c.process_array(&array(id));
1442 }
1443
1444 assert_eq!(c.file_base.num_captured(), 0);
1445 assert_eq!(int_update(&last.param_updates, NUM_CAPTURED), Some(0));
1446 assert_eq!(int_update(&last.param_updates, WRITE_STATUS), Some(0));
1447 }
1448}