1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrSource, NDAttrValue};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
8use ad_core_rs::plugin::file_controller::FilePluginController;
9use ad_core_rs::plugin::runtime::{
10 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
11};
12
13use netcdf3::{DataSet, FileReader, FileWriter, Version};
14
15const VAR_NAME: &str = "array_data";
16const DIM_UNLIMITED: &str = "numArrays";
17
18struct DimMeta {
20 size: usize,
21 offset: usize,
22 binning: usize,
23 reverse: bool,
24}
25
26struct AttrData {
28 name: String,
29 description: String,
30 source: String,
32 source_type: String,
34 data_type_string: String,
36 value: NDAttrValue,
37}
38
39struct FrameData {
41 dims: Vec<usize>,
42 dim_meta: Vec<DimMeta>,
43 data: NDDataBuffer,
44 data_type: NDDataType,
45 attrs: Vec<AttrData>,
46 unique_id: i32,
47 time_stamp: f64,
48 epics_ts_sec: i32,
49 epics_ts_nsec: i32,
50}
51
52fn attr_source_type_string(src: &NDAttrSource) -> &'static str {
54 match src {
55 NDAttrSource::Driver => "NDAttrSourceDriver",
56 NDAttrSource::EpicsPV => "NDAttrSourceEPICSPV",
57 NDAttrSource::Param { .. } => "NDAttrSourceParam",
58 NDAttrSource::Function => "NDAttrSourceFunct",
59 NDAttrSource::Constant => "NDAttrSourceConst",
60 NDAttrSource::Undefined => "Undefined",
61 }
62}
63
64fn attr_source_string(src: &NDAttrSource) -> String {
66 match src {
67 NDAttrSource::Driver => "Driver".to_string(),
68 NDAttrSource::EpicsPV => "EPICS_PV".to_string(),
69 NDAttrSource::Param { param_name, .. } => param_name.clone(),
70 NDAttrSource::Function => "Function".to_string(),
71 NDAttrSource::Constant => "Const".to_string(),
72 NDAttrSource::Undefined => String::new(),
73 }
74}
75
76fn attr_data_type_string(value: &NDAttrValue) -> &'static str {
78 match value {
79 NDAttrValue::Int8(_) => "Int8",
80 NDAttrValue::UInt8(_) => "UInt8",
81 NDAttrValue::Int16(_) => "Int16",
82 NDAttrValue::UInt16(_) => "UInt16",
83 NDAttrValue::Int32(_) => "Int32",
84 NDAttrValue::UInt32(_) => "UInt32",
85 NDAttrValue::Int64(_) => "Int64",
86 NDAttrValue::UInt64(_) => "UInt64",
87 NDAttrValue::Float32(_) => "Float32",
88 NDAttrValue::Float64(_) => "Float64",
89 NDAttrValue::String(_) => "String",
90 NDAttrValue::Undefined => "Undefined",
91 }
92}
93
94pub struct NetcdfWriter {
102 current_path: Option<PathBuf>,
103 frames: Vec<FrameData>,
104}
105
106impl NetcdfWriter {
107 pub fn new() -> Self {
108 Self {
109 current_path: None,
110 frames: Vec::new(),
111 }
112 }
113}
114
115fn nc_data_type(dt: NDDataType) -> ADResult<netcdf3::DataType> {
118 match dt {
119 NDDataType::Int8 => Ok(netcdf3::DataType::I8),
120 NDDataType::UInt8 => Ok(netcdf3::DataType::U8),
121 NDDataType::Int16 | NDDataType::UInt16 => Ok(netcdf3::DataType::I16),
122 NDDataType::Int32 | NDDataType::UInt32 => Ok(netcdf3::DataType::I32),
123 NDDataType::Float32 => Ok(netcdf3::DataType::F32),
124 NDDataType::Float64 => Ok(netcdf3::DataType::F64),
125 NDDataType::Int64 | NDDataType::UInt64 => Ok(netcdf3::DataType::F64),
126 }
127}
128
129fn write_var_data(writer: &mut FileWriter, data: &NDDataBuffer) -> ADResult<()> {
131 let err = |e: netcdf3::error::WriteError| {
132 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
133 };
134 match data {
135 NDDataBuffer::I8(v) => writer.write_var_i8(VAR_NAME, v).map_err(err),
136 NDDataBuffer::U8(v) => writer.write_var_u8(VAR_NAME, v).map_err(err),
137 NDDataBuffer::I16(v) => writer.write_var_i16(VAR_NAME, v).map_err(err),
138 NDDataBuffer::U16(v) => {
139 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
140 writer.write_var_i16(VAR_NAME, &reinterp).map_err(err)
141 }
142 NDDataBuffer::I32(v) => writer.write_var_i32(VAR_NAME, v).map_err(err),
143 NDDataBuffer::U32(v) => {
144 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
145 writer.write_var_i32(VAR_NAME, &reinterp).map_err(err)
146 }
147 NDDataBuffer::F32(v) => writer.write_var_f32(VAR_NAME, v).map_err(err),
148 NDDataBuffer::F64(v) => writer.write_var_f64(VAR_NAME, v).map_err(err),
149 NDDataBuffer::I64(v) => {
150 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
151 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
152 }
153 NDDataBuffer::U64(v) => {
154 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
155 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
156 }
157 }
158}
159
160fn write_record_data(
162 writer: &mut FileWriter,
163 record_index: usize,
164 data: &NDDataBuffer,
165) -> ADResult<()> {
166 let err = |e: netcdf3::error::WriteError| {
167 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
168 };
169 match data {
170 NDDataBuffer::I8(v) => writer
171 .write_record_i8(VAR_NAME, record_index, v)
172 .map_err(err),
173 NDDataBuffer::U8(v) => writer
174 .write_record_u8(VAR_NAME, record_index, v)
175 .map_err(err),
176 NDDataBuffer::I16(v) => writer
177 .write_record_i16(VAR_NAME, record_index, v)
178 .map_err(err),
179 NDDataBuffer::U16(v) => {
180 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
181 writer
182 .write_record_i16(VAR_NAME, record_index, &reinterp)
183 .map_err(err)
184 }
185 NDDataBuffer::I32(v) => writer
186 .write_record_i32(VAR_NAME, record_index, v)
187 .map_err(err),
188 NDDataBuffer::U32(v) => {
189 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
190 writer
191 .write_record_i32(VAR_NAME, record_index, &reinterp)
192 .map_err(err)
193 }
194 NDDataBuffer::F32(v) => writer
195 .write_record_f32(VAR_NAME, record_index, v)
196 .map_err(err),
197 NDDataBuffer::F64(v) => writer
198 .write_record_f64(VAR_NAME, record_index, v)
199 .map_err(err),
200 NDDataBuffer::I64(v) => {
201 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
202 writer
203 .write_record_f64(VAR_NAME, record_index, &reinterp)
204 .map_err(err)
205 }
206 NDDataBuffer::U64(v) => {
207 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
208 writer
209 .write_record_f64(VAR_NAME, record_index, &reinterp)
210 .map_err(err)
211 }
212 }
213}
214
215const ATTR_STRING_DIM: &str = "attrStringSize";
216const ATTR_STRING_SIZE: usize = 256;
217
218fn attr_nc_type(value: &NDAttrValue) -> netcdf3::DataType {
220 match value {
221 NDAttrValue::Int8(_) | NDAttrValue::UInt8(_) | NDAttrValue::Undefined => {
222 netcdf3::DataType::I8
223 }
224 NDAttrValue::Int16(_) | NDAttrValue::UInt16(_) => netcdf3::DataType::I16,
225 NDAttrValue::Int32(_) | NDAttrValue::UInt32(_) => netcdf3::DataType::I32,
226 NDAttrValue::Float32(_) => netcdf3::DataType::F32,
227 NDAttrValue::Float64(_) | NDAttrValue::Int64(_) | NDAttrValue::UInt64(_) => {
228 netcdf3::DataType::F64
229 }
230 NDAttrValue::String(_) => netcdf3::DataType::I8,
231 }
232}
233
234fn write_attr_value(
237 writer: &mut FileWriter,
238 var_name: &str,
239 record_index: usize,
240 multi: bool,
241 value: &NDAttrValue,
242) -> ADResult<()> {
243 let werr = |e: netcdf3::error::WriteError| {
244 ADError::UnsupportedConversion(format!("NetCDF attr write error: {:?}", e))
245 };
246 if let NDAttrValue::String(s) = value {
248 let mut bytes: Vec<i8> = s.bytes().take(ATTR_STRING_SIZE).map(|b| b as i8).collect();
249 bytes.resize(ATTR_STRING_SIZE, 0);
250 return if multi {
251 writer
252 .write_record_i8(var_name, record_index, &bytes)
253 .map_err(werr)
254 } else {
255 writer.write_var_i8(var_name, &bytes).map_err(werr)
256 };
257 }
258 match attr_nc_type(value) {
259 netcdf3::DataType::I8 => {
260 let v = value.as_i64().unwrap_or(0) as i8;
261 if multi {
262 writer
263 .write_record_i8(var_name, record_index, &[v])
264 .map_err(werr)
265 } else {
266 writer.write_var_i8(var_name, &[v]).map_err(werr)
267 }
268 }
269 netcdf3::DataType::I16 => {
270 let v = value.as_i64().unwrap_or(0) as i16;
271 if multi {
272 writer
273 .write_record_i16(var_name, record_index, &[v])
274 .map_err(werr)
275 } else {
276 writer.write_var_i16(var_name, &[v]).map_err(werr)
277 }
278 }
279 netcdf3::DataType::I32 => {
280 let v = value.as_i64().unwrap_or(0) as i32;
281 if multi {
282 writer
283 .write_record_i32(var_name, record_index, &[v])
284 .map_err(werr)
285 } else {
286 writer.write_var_i32(var_name, &[v]).map_err(werr)
287 }
288 }
289 netcdf3::DataType::F32 => {
290 let v = value.as_f64().unwrap_or(0.0) as f32;
291 if multi {
292 writer
293 .write_record_f32(var_name, record_index, &[v])
294 .map_err(werr)
295 } else {
296 writer.write_var_f32(var_name, &[v]).map_err(werr)
297 }
298 }
299 netcdf3::DataType::F64 => {
300 let v = value.as_f64().unwrap_or(0.0);
301 if multi {
302 writer
303 .write_record_f64(var_name, record_index, &[v])
304 .map_err(werr)
305 } else {
306 writer.write_var_f64(var_name, &[v]).map_err(werr)
307 }
308 }
309 netcdf3::DataType::U8 => unreachable!("attr_nc_type never returns U8"),
310 }
311}
312
313impl NDFileWriter for NetcdfWriter {
314 fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
315 self.current_path = Some(path.to_path_buf());
316 self.frames.clear();
317 Ok(())
318 }
319
320 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
321 nc_data_type(array.data.data_type())?;
323
324 let dims: Vec<usize> = array.dims.iter().map(|d| d.size).collect();
325 let dim_meta: Vec<DimMeta> = array
326 .dims
327 .iter()
328 .map(|d| DimMeta {
329 size: d.size,
330 offset: d.offset,
331 binning: d.binning,
332 reverse: d.reverse,
333 })
334 .collect();
335 let attrs: Vec<AttrData> = array
336 .attributes
337 .iter()
338 .map(|a| AttrData {
339 name: a.name.clone(),
340 description: a.description.clone(),
341 source: attr_source_string(&a.source),
342 source_type: attr_source_type_string(&a.source).to_string(),
343 data_type_string: attr_data_type_string(&a.value).to_string(),
344 value: a.value.clone(),
345 })
346 .collect();
347
348 self.frames.push(FrameData {
349 dims,
350 dim_meta,
351 data: array.data.clone(),
352 data_type: array.data.data_type(),
353 attrs,
354 unique_id: array.unique_id,
355 time_stamp: array.time_stamp,
356 epics_ts_sec: array.timestamp.sec as i32,
357 epics_ts_nsec: array.timestamp.nsec as i32,
358 });
359 Ok(())
360 }
361
362 fn close_file(&mut self) -> ADResult<()> {
363 let path = match self.current_path.take() {
364 Some(p) => p,
365 None => return Ok(()),
366 };
367
368 if self.frames.is_empty() {
369 return Ok(());
370 }
371
372 let map_def = |e: netcdf3::error::InvalidDataSet| {
373 ADError::UnsupportedConversion(format!("NetCDF definition error: {:?}", e))
374 };
375 let map_write = |e: netcdf3::error::WriteError| {
376 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
377 };
378
379 let first = &self.frames[0];
380 let nc_dt = nc_data_type(first.data_type)?;
381 let multi = self.frames.len() > 1;
382
383 let mut ds = DataSet::new();
385
386 if multi {
392 ds.set_unlimited_dim(DIM_UNLIMITED, self.frames.len())
393 .map_err(map_def)?;
394 } else {
395 ds.add_fixed_dim(DIM_UNLIMITED, 1).map_err(map_def)?;
396 }
397
398 let ndims = first.dims.len();
400 let mut dim_names: Vec<String> = Vec::new();
401 for i in 0..ndims {
402 let dim_idx = ndims - 1 - i;
403 let name = format!("dim{}", i);
404 ds.add_fixed_dim(&name, first.dims[dim_idx])
405 .map_err(map_def)?;
406 dim_names.push(name);
407 }
408
409 let has_string_attr = self.frames.iter().any(|f| {
411 f.attrs
412 .iter()
413 .any(|a| matches!(a.value, NDAttrValue::String(_)))
414 });
415 if has_string_attr {
416 ds.add_fixed_dim(ATTR_STRING_DIM, ATTR_STRING_SIZE)
417 .map_err(map_def)?;
418 }
419
420 let var_dims: Vec<String> = {
422 let mut v = vec![DIM_UNLIMITED.to_string()];
423 v.extend(dim_names.iter().cloned());
424 v
425 };
426 let var_dim_refs: Vec<&str> = var_dims.iter().map(|s| s.as_str()).collect();
427 ds.add_var(VAR_NAME, &var_dim_refs, nc_dt)
428 .map_err(map_def)?;
429
430 ds.add_var("uniqueId", &[DIM_UNLIMITED], netcdf3::DataType::I32)
432 .map_err(map_def)?;
433 ds.add_var("timeStamp", &[DIM_UNLIMITED], netcdf3::DataType::F64)
434 .map_err(map_def)?;
435 ds.add_var("epicsTSSec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
436 .map_err(map_def)?;
437 ds.add_var("epicsTSNsec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
438 .map_err(map_def)?;
439
440 let mut attr_var_names: Vec<String> = Vec::new();
445 for attr in &first.attrs {
446 let var_name = format!("Attr_{}", attr.name);
447 let nc_type = attr_nc_type(&attr.value);
448 let is_string = matches!(attr.value, NDAttrValue::String(_));
449 if is_string {
450 ds.add_var(
451 &var_name,
452 &[DIM_UNLIMITED, ATTR_STRING_DIM],
453 netcdf3::DataType::I8,
454 )
455 .map_err(map_def)?;
456 } else {
457 ds.add_var(&var_name, &[DIM_UNLIMITED], nc_type)
458 .map_err(map_def)?;
459 }
460 attr_var_names.push(var_name);
461
462 ds.add_global_attr_string(
463 &format!("Attr_{}_DataType", attr.name),
464 &attr.data_type_string,
465 )
466 .map_err(map_def)?;
467 ds.add_global_attr_string(
468 &format!("Attr_{}_Description", attr.name),
469 &attr.description,
470 )
471 .map_err(map_def)?;
472 ds.add_global_attr_string(&format!("Attr_{}_Source", attr.name), &attr.source)
473 .map_err(map_def)?;
474 ds.add_global_attr_string(&format!("Attr_{}_SourceType", attr.name), &attr.source_type)
475 .map_err(map_def)?;
476 }
477
478 ds.add_global_attr_i32("uniqueId", vec![first.unique_id])
480 .map_err(map_def)?;
481 ds.add_global_attr_i32("dataType", vec![first.data_type as i32])
482 .map_err(map_def)?;
483 ds.add_global_attr_i32("numArrays", vec![self.frames.len() as i32])
484 .map_err(map_def)?;
485
486 ds.add_global_attr_i32("numArrayDims", vec![ndims as i32])
488 .map_err(map_def)?;
489 let dim_size: Vec<i32> = first.dim_meta.iter().map(|d| d.size as i32).collect();
490 ds.add_global_attr_i32("dimSize", dim_size)
491 .map_err(map_def)?;
492 let dim_offset: Vec<i32> = first.dim_meta.iter().map(|d| d.offset as i32).collect();
493 ds.add_global_attr_i32("dimOffset", dim_offset)
494 .map_err(map_def)?;
495 let dim_binning: Vec<i32> = first.dim_meta.iter().map(|d| d.binning as i32).collect();
496 ds.add_global_attr_i32("dimBinning", dim_binning)
497 .map_err(map_def)?;
498 let dim_reverse: Vec<i32> = first
499 .dim_meta
500 .iter()
501 .map(|d| if d.reverse { 1 } else { 0 })
502 .collect();
503 ds.add_global_attr_i32("dimReverse", dim_reverse)
504 .map_err(map_def)?;
505
506 let mut writer = FileWriter::open(&path).map_err(map_write)?;
508 writer
509 .set_def(&ds, Version::Classic, 0)
510 .map_err(map_write)?;
511
512 if multi {
513 for (i, frame) in self.frames.iter().enumerate() {
514 write_record_data(&mut writer, i, &frame.data)?;
515 writer
516 .write_record_i32("uniqueId", i, &[frame.unique_id])
517 .map_err(map_write)?;
518 writer
519 .write_record_f64("timeStamp", i, &[frame.time_stamp])
520 .map_err(map_write)?;
521 writer
522 .write_record_i32("epicsTSSec", i, &[frame.epics_ts_sec])
523 .map_err(map_write)?;
524 writer
525 .write_record_i32("epicsTSNsec", i, &[frame.epics_ts_nsec])
526 .map_err(map_write)?;
527 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
530 let value = frame
531 .attrs
532 .iter()
533 .find(|a| a.name == attr.name)
534 .map(|a| &a.value)
535 .unwrap_or(&attr.value);
536 write_attr_value(&mut writer, var_name, i, true, value)?;
537 }
538 }
539 } else {
540 write_var_data(&mut writer, &self.frames[0].data)?;
541 writer
542 .write_var_i32("uniqueId", &[first.unique_id])
543 .map_err(map_write)?;
544 writer
545 .write_var_f64("timeStamp", &[first.time_stamp])
546 .map_err(map_write)?;
547 writer
548 .write_var_i32("epicsTSSec", &[first.epics_ts_sec])
549 .map_err(map_write)?;
550 writer
551 .write_var_i32("epicsTSNsec", &[first.epics_ts_nsec])
552 .map_err(map_write)?;
553 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
554 write_attr_value(&mut writer, var_name, 0, false, &attr.value)?;
555 }
556 }
557
558 writer.close().map_err(map_write)?;
559 self.frames.clear();
560 Ok(())
561 }
562
563 fn read_file(&mut self) -> ADResult<NDArray> {
564 let path = self
565 .current_path
566 .as_ref()
567 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
568
569 let map_read = |e: netcdf3::error::ReadError| {
570 ADError::UnsupportedConversion(format!("NetCDF read error: {:?}", e))
571 };
572
573 let mut reader = FileReader::open(path).map_err(map_read)?;
574
575 let (is_record, dims, original_type_ordinal) = {
577 let ds = reader.data_set();
578 let var = ds.get_var(VAR_NAME).ok_or_else(|| {
579 ADError::UnsupportedConversion(format!(
580 "variable '{}' not found in NetCDF file",
581 VAR_NAME
582 ))
583 })?;
584
585 let is_record = ds.is_record_var(VAR_NAME).unwrap_or(false);
586
587 let var_dims_rc = var.get_dims();
588 let mut dims: Vec<NDDimension> = Vec::new();
589 for d in &var_dims_rc {
590 if d.is_unlimited() || d.name() == DIM_UNLIMITED {
594 continue;
595 }
596 dims.push(NDDimension::new(d.size()));
597 }
598
599 let original_type_ordinal = ds
600 .get_global_attr_i32("dataType")
601 .and_then(|slice| slice.first().copied());
602
603 (is_record, dims, original_type_ordinal)
604 };
605
606 let data_vec = if is_record {
608 reader.read_record(VAR_NAME, 0).map_err(map_read)?
609 } else {
610 reader.read_var(VAR_NAME).map_err(map_read)?
611 };
612
613 let (nd_type, buf) = match data_vec {
614 netcdf3::DataVector::I8(v) => (NDDataType::Int8, NDDataBuffer::I8(v)),
615 netcdf3::DataVector::U8(v) => (NDDataType::UInt8, NDDataBuffer::U8(v)),
616 netcdf3::DataVector::I16(v) => (NDDataType::Int16, NDDataBuffer::I16(v)),
617 netcdf3::DataVector::I32(v) => (NDDataType::Int32, NDDataBuffer::I32(v)),
618 netcdf3::DataVector::F32(v) => (NDDataType::Float32, NDDataBuffer::F32(v)),
619 netcdf3::DataVector::F64(v) => (NDDataType::Float64, NDDataBuffer::F64(v)),
620 };
621
622 let actual_type = original_type_ordinal
624 .and_then(|v| NDDataType::from_ordinal(v as u8))
625 .unwrap_or(nd_type);
626
627 let buf = match (actual_type, buf) {
629 (NDDataType::UInt16, NDDataBuffer::I16(v)) => {
630 NDDataBuffer::U16(v.into_iter().map(|x| x as u16).collect())
631 }
632 (NDDataType::UInt32, NDDataBuffer::I32(v)) => {
633 NDDataBuffer::U32(v.into_iter().map(|x| x as u32).collect())
634 }
635 (_, buf) => buf,
636 };
637
638 let mut arr = NDArray::new(dims, actual_type);
639 arr.data = buf;
640 Ok(arr)
641 }
642
643 fn supports_multiple_arrays(&self) -> bool {
644 true
645 }
646}
647
648pub struct NetcdfFileProcessor {
650 ctrl: FilePluginController<NetcdfWriter>,
651}
652
653impl NetcdfFileProcessor {
654 pub fn new() -> Self {
655 Self {
656 ctrl: FilePluginController::new(NetcdfWriter::new()),
657 }
658 }
659}
660
661impl Default for NetcdfFileProcessor {
662 fn default() -> Self {
663 Self::new()
664 }
665}
666
667impl NDPluginProcess for NetcdfFileProcessor {
668 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
669 self.ctrl.process_array(array)
670 }
671
672 fn plugin_type(&self) -> &str {
673 "NDFileNetCDF"
674 }
675
676 fn register_params(
677 &mut self,
678 base: &mut asyn_rs::port::PortDriverBase,
679 ) -> asyn_rs::error::AsynResult<()> {
680 self.ctrl.register_params(base)
681 }
682
683 fn on_param_change(
684 &mut self,
685 reason: usize,
686 params: &PluginParamSnapshot,
687 ) -> ParamChangeResult {
688 self.ctrl.on_param_change(reason, params)
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
696 use std::sync::atomic::{AtomicU32, Ordering};
697
698 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
699
700 fn temp_path(prefix: &str) -> PathBuf {
701 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
702 std::env::temp_dir().join(format!("adcore_test_{}_{}.nc", prefix, n))
703 }
704
705 #[test]
706 fn test_write_u8_mono() {
707 let path = temp_path("nc_u8");
708 let mut writer = NetcdfWriter::new();
709
710 let mut arr = NDArray::new(
711 vec![NDDimension::new(4), NDDimension::new(4)],
712 NDDataType::UInt8,
713 );
714 if let NDDataBuffer::U8(v) = &mut arr.data {
715 for i in 0..16 {
716 v[i] = i as u8;
717 }
718 }
719
720 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
721 writer.write_file(&arr).unwrap();
722 writer.close_file().unwrap();
723
724 let data = std::fs::read(&path).unwrap();
726 assert!(data.len() > 16);
727 assert_eq!(&data[0..3], b"CDF", "Expected NetCDF magic bytes");
728
729 std::fs::remove_file(&path).ok();
730 }
731
732 #[test]
733 fn test_write_u16() {
734 let path = temp_path("nc_u16");
735 let mut writer = NetcdfWriter::new();
736
737 let mut arr = NDArray::new(
738 vec![NDDimension::new(4), NDDimension::new(4)],
739 NDDataType::UInt16,
740 );
741 if let NDDataBuffer::U16(v) = &mut arr.data {
742 for i in 0..16 {
743 v[i] = (i * 1000) as u16;
744 }
745 }
746
747 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
748 writer.write_file(&arr).unwrap();
749 writer.close_file().unwrap();
750
751 let data = std::fs::read(&path).unwrap();
752 assert!(data.len() > 32);
753 assert_eq!(&data[0..3], b"CDF");
754
755 std::fs::remove_file(&path).ok();
756 }
757
758 #[test]
759 fn test_roundtrip_u8() {
760 let path = temp_path("nc_rt_u8");
761 let mut writer = NetcdfWriter::new();
762
763 let mut arr = NDArray::new(
764 vec![NDDimension::new(4), NDDimension::new(4)],
765 NDDataType::UInt8,
766 );
767 if let NDDataBuffer::U8(v) = &mut arr.data {
768 for i in 0..16 {
769 v[i] = (i * 10) as u8;
770 }
771 }
772
773 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
774 writer.write_file(&arr).unwrap();
775 writer.close_file().unwrap();
776
777 writer.current_path = Some(path.clone());
778 let read_back = writer.read_file().unwrap();
779 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
780 assert_eq!(orig, read);
781 } else {
782 panic!("data type mismatch on roundtrip");
783 }
784
785 std::fs::remove_file(&path).ok();
786 }
787
788 #[test]
789 fn test_roundtrip_i16() {
790 let path = temp_path("nc_rt_i16");
791 let mut writer = NetcdfWriter::new();
792
793 let mut arr = NDArray::new(
794 vec![NDDimension::new(4), NDDimension::new(4)],
795 NDDataType::Int16,
796 );
797 if let NDDataBuffer::I16(v) = &mut arr.data {
798 for i in 0..16 {
799 v[i] = (i as i16) * 100 - 500;
800 }
801 }
802
803 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
804 writer.write_file(&arr).unwrap();
805 writer.close_file().unwrap();
806
807 writer.current_path = Some(path.clone());
808 let read_back = writer.read_file().unwrap();
809 if let (NDDataBuffer::I16(orig), NDDataBuffer::I16(read)) = (&arr.data, &read_back.data) {
810 assert_eq!(orig, read);
811 } else {
812 panic!("data type mismatch on roundtrip");
813 }
814
815 std::fs::remove_file(&path).ok();
816 }
817
818 #[test]
819 fn test_roundtrip_f32() {
820 let path = temp_path("nc_rt_f32");
821 let mut writer = NetcdfWriter::new();
822
823 let mut arr = NDArray::new(
824 vec![NDDimension::new(4), NDDimension::new(4)],
825 NDDataType::Float32,
826 );
827 if let NDDataBuffer::F32(v) = &mut arr.data {
828 for i in 0..16 {
829 v[i] = i as f32 * 0.5;
830 }
831 }
832
833 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
834 writer.write_file(&arr).unwrap();
835 writer.close_file().unwrap();
836
837 writer.current_path = Some(path.clone());
838 let read_back = writer.read_file().unwrap();
839 if let (NDDataBuffer::F32(orig), NDDataBuffer::F32(read)) = (&arr.data, &read_back.data) {
840 assert_eq!(orig, read);
841 } else {
842 panic!("data type mismatch on roundtrip");
843 }
844
845 std::fs::remove_file(&path).ok();
846 }
847
848 #[test]
849 fn test_multiple_frames() {
850 let path = temp_path("nc_multi");
851 let mut writer = NetcdfWriter::new();
852
853 let mut arr1 = NDArray::new(
854 vec![NDDimension::new(4), NDDimension::new(4)],
855 NDDataType::UInt8,
856 );
857 if let NDDataBuffer::U8(v) = &mut arr1.data {
858 for i in 0..16 {
859 v[i] = i as u8;
860 }
861 }
862
863 let mut arr2 = NDArray::new(
864 vec![NDDimension::new(4), NDDimension::new(4)],
865 NDDataType::UInt8,
866 );
867 if let NDDataBuffer::U8(v) = &mut arr2.data {
868 for i in 0..16 {
869 v[i] = (i as u8).wrapping_add(100);
870 }
871 }
872
873 let mut arr3 = NDArray::new(
874 vec![NDDimension::new(4), NDDimension::new(4)],
875 NDDataType::UInt8,
876 );
877 if let NDDataBuffer::U8(v) = &mut arr3.data {
878 for i in 0..16 {
879 v[i] = (i as u8).wrapping_add(200);
880 }
881 }
882
883 writer.open_file(&path, NDFileMode::Stream, &arr1).unwrap();
884 writer.write_file(&arr1).unwrap();
885 writer.write_file(&arr2).unwrap();
886 writer.write_file(&arr3).unwrap();
887 writer.close_file().unwrap();
888
889 writer.current_path = Some(path.clone());
891 let read_back = writer.read_file().unwrap();
892 if let NDDataBuffer::U8(v) = &read_back.data {
893 assert_eq!(v.len(), 16);
894 for i in 0..16 {
895 assert_eq!(v[i], i as u8, "mismatch at index {}", i);
896 }
897 } else {
898 panic!("expected U8 data");
899 }
900
901 std::fs::remove_file(&path).ok();
902 }
903
904 #[test]
905 fn test_attributes_stored_as_per_frame_variables() {
906 let path = temp_path("nc_attrs");
907 let mut writer = NetcdfWriter::new();
908
909 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
910 arr.attributes.add(NDAttribute::new_static(
911 "exposure",
912 "Exposure time",
913 NDAttrSource::Driver,
914 NDAttrValue::Float64(0.5),
915 ));
916 arr.attributes.add(NDAttribute::new_static(
917 "gain",
918 "Detector gain",
919 NDAttrSource::Driver,
920 NDAttrValue::Int32(42),
921 ));
922
923 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
924 writer.write_file(&arr).unwrap();
925 writer.close_file().unwrap();
926
927 let mut reader = FileReader::open(&path).unwrap();
928 {
929 let ds = reader.data_set();
930 assert!(ds.get_var("Attr_exposure").is_some());
932 assert!(ds.get_var("Attr_gain").is_some());
933 assert_eq!(
935 ds.get_global_attr_as_string("Attr_exposure_DataType"),
936 Some("Float64".to_string())
937 );
938 assert_eq!(
939 ds.get_global_attr_as_string("Attr_gain_DataType"),
940 Some("Int32".to_string())
941 );
942 assert_eq!(
943 ds.get_global_attr_as_string("Attr_exposure_Description"),
944 Some("Exposure time".to_string())
945 );
946 assert_eq!(
947 ds.get_global_attr_as_string("Attr_gain_SourceType"),
948 Some("NDAttrSourceDriver".to_string())
949 );
950 }
951 if let netcdf3::DataVector::F64(v) = reader.read_var("Attr_exposure").unwrap() {
953 assert_eq!(v, vec![0.5]);
954 } else {
955 panic!("Attr_exposure should be F64");
956 }
957 if let netcdf3::DataVector::I32(v) = reader.read_var("Attr_gain").unwrap() {
958 assert_eq!(v, vec![42]);
959 } else {
960 panic!("Attr_gain should be I32");
961 }
962
963 drop(reader);
964 std::fs::remove_file(&path).ok();
965 }
966
967 #[test]
968 fn test_single_frame_array_data_has_leading_numarrays_dim() {
969 let path = temp_path("nc_rank");
970 let mut writer = NetcdfWriter::new();
971
972 let arr = NDArray::new(
973 vec![NDDimension::new(4), NDDimension::new(3)],
974 NDDataType::UInt8,
975 );
976 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
977 writer.write_file(&arr).unwrap();
978 writer.close_file().unwrap();
979
980 let reader = FileReader::open(&path).unwrap();
981 let ds = reader.data_set();
982 let var = ds.get_var("array_data").unwrap();
983 assert_eq!(var.get_dims().len(), 3);
986 assert_eq!(var.get_dims()[0].name(), "numArrays");
987 assert_eq!(var.get_dims()[0].size(), 1);
988
989 drop(reader);
990 std::fs::remove_file(&path).ok();
991 }
992
993 #[test]
994 fn test_all_four_metadata_variables_written_single_frame() {
995 let path = temp_path("nc_meta");
996 let mut writer = NetcdfWriter::new();
997
998 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
999 arr.unique_id = 99;
1000 arr.time_stamp = 12.5;
1001 arr.timestamp.sec = 555;
1002 arr.timestamp.nsec = 777;
1003
1004 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1005 writer.write_file(&arr).unwrap();
1006 writer.close_file().unwrap();
1007
1008 let mut reader = FileReader::open(&path).unwrap();
1009 for name in ["uniqueId", "timeStamp", "epicsTSSec", "epicsTSNsec"] {
1010 assert!(
1011 reader.data_set().get_var(name).is_some(),
1012 "{name} variable missing"
1013 );
1014 }
1015 match reader.read_var("uniqueId").unwrap() {
1016 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![99]),
1017 other => panic!("uniqueId wrong type: {other:?}"),
1018 }
1019 match reader.read_var("epicsTSSec").unwrap() {
1020 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![555]),
1021 other => panic!("epicsTSSec wrong type: {other:?}"),
1022 }
1023 match reader.read_var("epicsTSNsec").unwrap() {
1024 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![777]),
1025 other => panic!("epicsTSNsec wrong type: {other:?}"),
1026 }
1027
1028 drop(reader);
1029 std::fs::remove_file(&path).ok();
1030 }
1031
1032 #[test]
1033 fn test_nddatatype_ordinals_match_c() {
1034 assert_eq!(NDDataType::Int8 as i32, 0);
1038 assert_eq!(NDDataType::UInt8 as i32, 1);
1039 assert_eq!(NDDataType::Int16 as i32, 2);
1040 assert_eq!(NDDataType::UInt16 as i32, 3);
1041 assert_eq!(NDDataType::Int32 as i32, 4);
1042 assert_eq!(NDDataType::UInt32 as i32, 5);
1043 assert_eq!(NDDataType::Int64 as i32, 6);
1044 assert_eq!(NDDataType::UInt64 as i32, 7);
1045 assert_eq!(NDDataType::Float32 as i32, 8);
1046 assert_eq!(NDDataType::Float64 as i32, 9);
1047 }
1048}