Skip to main content

ad_plugins_rs/
attr_plot.rs

1//! NDPluginAttrPlot: caches numeric NDArray attribute values over an
2//! acquisition and exposes selected ones as waveform records.
3//!
4//! Port of ADCore `NDPluginAttrPlot`. The C++ model separates two counts:
5//!
6//! * `n_attributes` — the maximum number of *tracked* numeric attributes.
7//!   Attribute names are discovered from the first frame of an acquisition
8//!   (and re-discovered after a reset), sorted, and capped to `n_attributes`.
9//!   One circular buffer per tracked attribute.
10//! * `n_data_blocks` — the number of *waveform outputs* (asyn addresses).
11//!   Each data block has an independent `DataSelect` value that maps it to a
12//!   tracked attribute index, or to the special UID buffer (`-1`), or to
13//!   nothing (`-2`).
14//!
15//! `DataLabel` is the human-readable name of the attribute a block is bound
16//! to; `NPts` is the current number of cached points. The waveform emitted
17//! for a block is padded out to `cache_size` with the last valid point to
18//! avoid plot artifacts (C++ `callback_data`).
19
20use std::collections::VecDeque;
21
22use ad_core_rs::ndarray::NDArray;
23use ad_core_rs::ndarray_pool::NDArrayPool;
24use ad_core_rs::plugin::runtime::{
25    NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
26};
27
28/// `DataSelect` value meaning "this block plots the UID buffer".
29pub const ATTRPLOT_UID_INDEX: i32 = -1;
30/// `DataSelect` value meaning "this block plots nothing".
31pub const ATTRPLOT_NONE_INDEX: i32 = -2;
32/// `DataLabel` text for the UID buffer.
33pub const ATTRPLOT_UID_LABEL: &str = "UID";
34/// `DataLabel` text for an unbound block.
35pub const ATTRPLOT_NONE_LABEL: &str = "None";
36
37/// Processor that tracks attribute values over time in circular buffers.
38pub struct AttrPlotProcessor {
39    /// Maximum number of tracked attributes (C++ `n_attributes_`).
40    n_attributes: usize,
41    /// Number of waveform output blocks (C++ `n_data_blocks_`).
42    n_data_blocks: usize,
43    /// Cache size per buffer; `0` means unlimited.
44    cache_size: usize,
45    /// Tracked attribute names (sorted, length <= `n_attributes`).
46    attributes: Vec<String>,
47    /// One circular buffer per tracked attribute.
48    buffers: Vec<VecDeque<f64>>,
49    /// Circular buffer of unique_id values.
50    uid_buffer: VecDeque<f64>,
51    /// Per-block attribute selection: index into `attributes`, or one of the
52    /// `ATTRPLOT_UID_INDEX` / `ATTRPLOT_NONE_INDEX` sentinels.
53    data_selections: Vec<i32>,
54    /// Whether attributes have been discovered for the current acquisition.
55    initialized: bool,
56    /// The unique_id from the last processed frame.
57    last_uid: i32,
58    /// Param indices (set after registration).
59    params: AttrPlotParams,
60}
61
62/// Param reasons resolved after `register_params`.
63#[derive(Default)]
64struct AttrPlotParams {
65    /// `AP_Data` — Float64Array, addressed by data block.
66    data: Option<usize>,
67    /// `AP_DataLabel` — Octet, addressed by data block.
68    data_label: Option<usize>,
69    /// `AP_DataSelect` — Int32, addressed by data block.
70    data_select: Option<usize>,
71    /// `AP_Attribute` — Octet, addressed by attribute index.
72    attribute: Option<usize>,
73    /// `AP_Reset` — Int32.
74    reset: Option<usize>,
75    /// `AP_NPts` — Int32.
76    npts: Option<usize>,
77}
78
79impl AttrPlotProcessor {
80    /// Create a processor.
81    ///
82    /// * `n_attributes` — maximum tracked attributes.
83    /// * `cache_size` — per-buffer cache size (`0` = unlimited).
84    /// * `n_data_blocks` — number of waveform output blocks.
85    pub fn new(n_attributes: usize, cache_size: usize, n_data_blocks: usize) -> Self {
86        Self {
87            n_attributes,
88            n_data_blocks,
89            cache_size,
90            attributes: Vec::new(),
91            buffers: Vec::new(),
92            uid_buffer: VecDeque::new(),
93            data_selections: vec![ATTRPLOT_NONE_INDEX; n_data_blocks],
94            initialized: false,
95            last_uid: -1,
96            params: AttrPlotParams::default(),
97        }
98    }
99
100    /// Get the list of tracked attribute names.
101    pub fn attributes(&self) -> &[String] {
102        &self.attributes
103    }
104
105    /// Get the circular buffer for a specific attribute index.
106    pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
107        self.buffers.get(index)
108    }
109
110    /// Get the unique_id buffer.
111    pub fn uid_buffer(&self) -> &VecDeque<f64> {
112        &self.uid_buffer
113    }
114
115    /// Get the number of tracked attributes.
116    pub fn num_attributes(&self) -> usize {
117        self.attributes.len()
118    }
119
120    /// Get the number of waveform output blocks.
121    pub fn num_data_blocks(&self) -> usize {
122        self.n_data_blocks
123    }
124
125    /// Find the index of a named attribute. Returns `None` if not tracked.
126    pub fn find_attribute(&self, name: &str) -> Option<usize> {
127        self.attributes.iter().position(|n| n == name)
128    }
129
130    /// Current `DataSelect` value for a block.
131    pub fn data_select(&self, block: usize) -> Option<i32> {
132        self.data_selections.get(block).copied()
133    }
134
135    /// Bind a data block to an attribute index (or a UID/NONE sentinel).
136    ///
137    /// Mirrors C++ `writeInt32(NDAttrPlotDataSelect)`: rejects out-of-range
138    /// blocks and selections that point past the tracked attributes.
139    pub fn set_data_select(&mut self, block: usize, value: i32) -> Result<(), &'static str> {
140        if block >= self.n_data_blocks {
141            return Err("data block index out of range");
142        }
143        // C rejects only a strictly positive selection past the end; value 0 is
144        // always accepted, even with no tracked attributes (NDPluginAttrPlot.cpp:283).
145        if value > 0 && (value as usize) >= self.attributes.len() {
146            return Err("attribute selection out of range");
147        }
148        self.data_selections[block] = value;
149        Ok(())
150    }
151
152    /// The `DataLabel` text for a block, derived from its `DataSelect`.
153    pub fn data_label(&self, block: usize) -> String {
154        match self.data_selections.get(block).copied() {
155            Some(ATTRPLOT_UID_INDEX) => ATTRPLOT_UID_LABEL.to_string(),
156            Some(sel) if sel >= 0 && (sel as usize) < self.attributes.len() => {
157                self.attributes[sel as usize].clone()
158            }
159            _ => ATTRPLOT_NONE_LABEL.to_string(),
160        }
161    }
162
163    /// Reset all buffers; the next frame re-discovers attributes.
164    pub fn reset(&mut self) {
165        self.initialized = false;
166        self.uid_buffer.clear();
167        for buf in &mut self.buffers {
168            buf.clear();
169        }
170        self.last_uid = -1;
171    }
172
173    /// Push a value into a ring buffer, enforcing `cache_size`.
174    fn push_capped(buf: &mut VecDeque<f64>, value: f64, cache_size: usize) {
175        if cache_size > 0 && buf.len() >= cache_size {
176            buf.pop_front();
177        }
178        buf.push_back(value);
179    }
180
181    /// Discover the tracked attributes from a frame (C++ `rebuild_attributes`).
182    ///
183    /// Existing block selections are preserved by attribute *name*: a block
184    /// that pointed at "Temp" before the rebuild still points at "Temp"
185    /// afterwards (or `NONE` if "Temp" is no longer present).
186    fn rebuild_attributes(&mut self, array: &NDArray) {
187        // Remember each block's current selection by name.
188        let prior: Vec<Option<String>> = self
189            .data_selections
190            .iter()
191            .map(|&sel| match sel {
192                ATTRPLOT_UID_INDEX => Some(ATTRPLOT_UID_LABEL.to_string()),
193                s if s >= 0 && (s as usize) < self.attributes.len() => {
194                    Some(self.attributes[s as usize].clone())
195                }
196                _ => None,
197            })
198            .collect();
199
200        let mut names: Vec<String> = Vec::new();
201        for attr in array.attributes.iter() {
202            if attr.value.as_f64().is_some() {
203                names.push(attr.name.clone());
204            }
205        }
206        names.sort();
207        names.truncate(self.n_attributes);
208
209        self.buffers = vec![VecDeque::new(); names.len()];
210        self.attributes = names;
211
212        // Re-resolve each block selection against the new attribute list.
213        for (i, want) in prior.into_iter().enumerate() {
214            self.data_selections[i] = match want {
215                Some(ref n) if n == ATTRPLOT_UID_LABEL => ATTRPLOT_UID_INDEX,
216                Some(n) => self
217                    .attributes
218                    .iter()
219                    .position(|a| a == &n)
220                    .map(|p| p as i32)
221                    .unwrap_or(ATTRPLOT_NONE_INDEX),
222                None => ATTRPLOT_NONE_INDEX,
223            };
224        }
225        self.initialized = true;
226    }
227
228    /// Push the current frame's attribute values into the buffers.
229    fn push_data(&mut self, array: &NDArray) {
230        Self::push_capped(
231            &mut self.uid_buffer,
232            array.unique_id as f64,
233            self.cache_size,
234        );
235        for (i, name) in self.attributes.iter().enumerate() {
236            let value = array
237                .attributes
238                .get(name)
239                .and_then(|attr| attr.value.as_f64())
240                .unwrap_or(f64::NAN);
241            Self::push_capped(&mut self.buffers[i], value, self.cache_size);
242        }
243    }
244
245    /// Build the padded waveform for one data block (C++ `callback_data`).
246    ///
247    /// Returns the values padded to `cache_size` (or to the current point
248    /// count when `cache_size` is unlimited) with the last valid point.
249    fn block_waveform(&self, block: usize) -> Vec<f64> {
250        let selected = self
251            .data_selections
252            .get(block)
253            .copied()
254            .unwrap_or(ATTRPLOT_NONE_INDEX);
255        let src: Option<&VecDeque<f64>> = match selected {
256            ATTRPLOT_UID_INDEX => Some(&self.uid_buffer),
257            s if s >= 0 && (s as usize) < self.buffers.len() => Some(&self.buffers[s as usize]),
258            _ => None,
259        };
260        let size = self.uid_buffer.len();
261        // Target length: the fixed cache size, or the live count if unlimited.
262        let target = if self.cache_size > 0 {
263            self.cache_size
264        } else {
265            size
266        };
267        let mut out: Vec<f64> = match src {
268            Some(buf) => buf.iter().copied().collect(),
269            None => vec![f64::NAN; size],
270        };
271        // Pad the tail with the last valid point to suppress plot artifacts.
272        let pad = out.last().copied().unwrap_or(f64::NAN);
273        if out.len() < target {
274            out.resize(target, pad);
275        } else {
276            out.truncate(target);
277        }
278        out
279    }
280
281    /// Build all param updates emitted after a frame.
282    fn build_updates(&self) -> Vec<ParamUpdate> {
283        let mut updates = Vec::new();
284        // Per-block waveform + label.
285        if let Some(data) = self.params.data {
286            for block in 0..self.n_data_blocks {
287                updates.push(ParamUpdate::float64_array_addr(
288                    data,
289                    block as i32,
290                    self.block_waveform(block),
291                ));
292            }
293        }
294        if let Some(label) = self.params.data_label {
295            for block in 0..self.n_data_blocks {
296                updates.push(ParamUpdate::octet_addr(
297                    label,
298                    block as i32,
299                    self.data_label(block),
300                ));
301            }
302        }
303        if let Some(select) = self.params.data_select {
304            for block in 0..self.n_data_blocks {
305                updates.push(ParamUpdate::int32_addr(
306                    select,
307                    block as i32,
308                    self.data_selections[block],
309                ));
310            }
311        }
312        // Per-attribute name.
313        if let Some(attribute) = self.params.attribute {
314            for i in 0..self.n_attributes {
315                let name = self.attributes.get(i).cloned().unwrap_or_default();
316                updates.push(ParamUpdate::octet_addr(attribute, i as i32, name));
317            }
318        }
319        if let Some(npts) = self.params.npts {
320            updates.push(ParamUpdate::int32(npts, self.uid_buffer.len() as i32));
321        }
322        updates
323    }
324}
325
326impl NDPluginProcess for AttrPlotProcessor {
327    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
328        // Re-acquisition: a UID at or below the last cached one resets.
329        if !self.uid_buffer.is_empty() && array.unique_id <= self.last_uid {
330            self.reset();
331        }
332        self.last_uid = array.unique_id;
333
334        if !self.initialized {
335            self.rebuild_attributes(array);
336        }
337        self.push_data(array);
338
339        ProcessResult::sink(self.build_updates())
340    }
341
342    fn plugin_type(&self) -> &str {
343        // C sets PluginType to "NDAttrPlot" (NDPluginAttrPlot.cpp:87), not the
344        // class name.
345        "NDAttrPlot"
346    }
347
348    fn register_params(
349        &mut self,
350        base: &mut asyn_rs::port::PortDriverBase,
351    ) -> asyn_rs::error::AsynResult<()> {
352        use asyn_rs::param::ParamType;
353        base.create_param("AP_Data", ParamType::Float64Array)?;
354        base.create_param("AP_DataLabel", ParamType::Octet)?;
355        base.create_param("AP_DataSelect", ParamType::Int32)?;
356        base.create_param("AP_Attribute", ParamType::Octet)?;
357        base.create_param("AP_Reset", ParamType::Int32)?;
358        base.create_param("AP_NPts", ParamType::Int32)?;
359
360        self.params.data = base.find_param("AP_Data");
361        self.params.data_label = base.find_param("AP_DataLabel");
362        self.params.data_select = base.find_param("AP_DataSelect");
363        self.params.attribute = base.find_param("AP_Attribute");
364        self.params.reset = base.find_param("AP_Reset");
365        self.params.npts = base.find_param("AP_NPts");
366        Ok(())
367    }
368
369    fn on_param_change(
370        &mut self,
371        reason: usize,
372        params: &PluginParamSnapshot,
373    ) -> ParamChangeResult {
374        if Some(reason) == self.params.data_select {
375            let block = params.addr as usize;
376            let value = params.value.as_i32();
377            if self.set_data_select(block, value).is_ok() {
378                // Re-emit label + waveform for the rebound block.
379                return ParamChangeResult::updates(self.build_updates());
380            }
381        } else if Some(reason) == self.params.reset {
382            // C calls reset_data() on ANY write to the reset param — there is no
383            // value test (NDPluginAttrPlot.cpp:290-292).
384            self.reset();
385            return ParamChangeResult::updates(self.build_updates());
386        }
387        ParamChangeResult::updates(vec![])
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
395    use ad_core_rs::ndarray::{NDDataType, NDDimension};
396
397    fn make_array_with_attrs(uid: i32, attrs: &[(&str, f64)]) -> NDArray {
398        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
399        arr.unique_id = uid;
400        for (name, value) in attrs {
401            arr.attributes.add(NDAttribute::new_static(
402                *name,
403                String::new(),
404                NDAttrSource::Driver,
405                NDAttrValue::Float64(*value),
406            ));
407        }
408        arr
409    }
410
411    #[test]
412    fn test_attribute_auto_detection() {
413        let mut proc = AttrPlotProcessor::new(8, 100, 4);
414        let pool = NDArrayPool::new(1_000_000);
415
416        let mut arr = make_array_with_attrs(1, &[("Temp", 25.0), ("Gain", 1.5)]);
417        arr.attributes.add(NDAttribute::new_static(
418            "Label",
419            String::new(),
420            NDAttrSource::Driver,
421            NDAttrValue::String("test".to_string()),
422        ));
423        proc.process_array(&arr, &pool);
424
425        assert_eq!(proc.num_attributes(), 2);
426        assert_eq!(proc.attributes()[0], "Gain");
427        assert_eq!(proc.attributes()[1], "Temp");
428    }
429
430    #[test]
431    fn test_n_attributes_caps_tracked_count() {
432        // n_attributes = 2: only the first 2 (sorted) attributes are tracked.
433        let mut proc = AttrPlotProcessor::new(2, 100, 1);
434        let pool = NDArrayPool::new(1_000_000);
435        let arr = make_array_with_attrs(1, &[("D", 4.0), ("A", 1.0), ("C", 3.0), ("B", 2.0)]);
436        proc.process_array(&arr, &pool);
437        assert_eq!(proc.num_attributes(), 2);
438        assert_eq!(proc.attributes(), &["A", "B"]);
439    }
440
441    #[test]
442    fn test_data_select_maps_block_to_attribute() {
443        // 3 attributes, 2 data blocks. Block 0 -> "B" (idx 1), block 1 -> UID.
444        let mut proc = AttrPlotProcessor::new(8, 100, 2);
445        let pool = NDArrayPool::new(1_000_000);
446        let arr = make_array_with_attrs(1, &[("A", 10.0), ("B", 20.0), ("C", 30.0)]);
447        proc.process_array(&arr, &pool);
448
449        proc.set_data_select(0, 1).unwrap(); // "B"
450        proc.set_data_select(1, ATTRPLOT_UID_INDEX).unwrap();
451
452        assert_eq!(proc.data_label(0), "B");
453        assert_eq!(proc.data_label(1), ATTRPLOT_UID_LABEL);
454
455        let wf0 = proc.block_waveform(0);
456        assert!((wf0[0] - 20.0).abs() < 1e-10, "block 0 plots attribute B");
457        let wf1 = proc.block_waveform(1);
458        assert!((wf1[0] - 1.0).abs() < 1e-10, "block 1 plots UID");
459    }
460
461    #[test]
462    fn test_data_select_rejects_out_of_range() {
463        let mut proc = AttrPlotProcessor::new(8, 100, 2);
464        let pool = NDArrayPool::new(1_000_000);
465        let arr = make_array_with_attrs(1, &[("A", 1.0)]);
466        proc.process_array(&arr, &pool);
467
468        // Only 1 attribute -> selection 1 is out of range.
469        assert!(proc.set_data_select(0, 1).is_err());
470        // Block 5 does not exist.
471        assert!(proc.set_data_select(5, 0).is_err());
472        // Valid: attribute 0 and the UID sentinel.
473        assert!(proc.set_data_select(0, 0).is_ok());
474        assert!(proc.set_data_select(1, ATTRPLOT_UID_INDEX).is_ok());
475    }
476
477    #[test]
478    fn test_data_select_zero_accepted_with_no_attributes() {
479        // C accepts DataSelect 0 before any frame, even with no tracked
480        // attributes (the reject is `value > 0`, NDPluginAttrPlot.cpp:283).
481        let mut proc = AttrPlotProcessor::new(8, 100, 2);
482        assert!(proc.attributes.is_empty());
483        assert!(proc.set_data_select(0, 0).is_ok());
484        assert_eq!(proc.data_selections[0], 0);
485    }
486
487    #[test]
488    fn test_unbound_block_label_is_none() {
489        let mut proc = AttrPlotProcessor::new(8, 100, 3);
490        let pool = NDArrayPool::new(1_000_000);
491        let arr = make_array_with_attrs(1, &[("A", 1.0)]);
492        proc.process_array(&arr, &pool);
493        // Block 2 was never selected.
494        assert_eq!(proc.data_label(2), ATTRPLOT_NONE_LABEL);
495        assert_eq!(proc.data_select(2), Some(ATTRPLOT_NONE_INDEX));
496    }
497
498    #[test]
499    fn test_npts_tracks_point_count() {
500        let mut proc = AttrPlotProcessor::new(8, 100, 1);
501        let pool = NDArrayPool::new(1_000_000);
502        for i in 1..=4 {
503            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
504            proc.process_array(&arr, &pool);
505        }
506        assert_eq!(proc.uid_buffer().len(), 4);
507    }
508
509    #[test]
510    fn test_waveform_padded_to_cache_size() {
511        // cache_size = 6, only 3 points pushed -> waveform padded to 6 with
512        // the last point.
513        let mut proc = AttrPlotProcessor::new(8, 6, 1);
514        let pool = NDArrayPool::new(1_000_000);
515        for i in 1..=3 {
516            let arr = make_array_with_attrs(i, &[("X", i as f64 * 10.0)]);
517            proc.process_array(&arr, &pool);
518        }
519        proc.set_data_select(0, 0).unwrap();
520        let wf = proc.block_waveform(0);
521        assert_eq!(wf.len(), 6);
522        assert!((wf[0] - 10.0).abs() < 1e-10);
523        assert!((wf[2] - 30.0).abs() < 1e-10);
524        // Tail padded with the last point (30.0).
525        assert!((wf[3] - 30.0).abs() < 1e-10);
526        assert!((wf[5] - 30.0).abs() < 1e-10);
527    }
528
529    #[test]
530    fn test_data_select_preserved_across_rebuild() {
531        // Bind block 0 to "Temp", then re-acquire (UID resets). After the
532        // rebuild block 0 must still point at "Temp".
533        let mut proc = AttrPlotProcessor::new(8, 100, 1);
534        let pool = NDArrayPool::new(1_000_000);
535        let arr = make_array_with_attrs(5, &[("Gain", 1.0), ("Temp", 25.0)]);
536        proc.process_array(&arr, &pool);
537        let temp_idx = proc.find_attribute("Temp").unwrap() as i32;
538        proc.set_data_select(0, temp_idx).unwrap();
539
540        // Re-acquisition (UID drops); same attributes.
541        let arr2 = make_array_with_attrs(1, &[("Gain", 2.0), ("Temp", 99.0)]);
542        proc.process_array(&arr2, &pool);
543        assert_eq!(proc.data_label(0), "Temp");
544        let wf = proc.block_waveform(0);
545        assert!((wf[0] - 99.0).abs() < 1e-10);
546    }
547
548    #[test]
549    fn test_value_tracking() {
550        let mut proc = AttrPlotProcessor::new(8, 100, 1);
551        let pool = NDArrayPool::new(1_000_000);
552        for i in 1..=5 {
553            let arr = make_array_with_attrs(i, &[("Value", i as f64 * 10.0)]);
554            proc.process_array(&arr, &pool);
555        }
556        let idx = proc.find_attribute("Value").unwrap();
557        let buf = proc.buffer(idx).unwrap();
558        assert_eq!(buf.len(), 5);
559        assert!((buf[0] - 10.0).abs() < 1e-10);
560        assert!((buf[4] - 50.0).abs() < 1e-10);
561    }
562
563    #[test]
564    fn test_circular_buffer_cache_size() {
565        let mut proc = AttrPlotProcessor::new(8, 3, 1);
566        let pool = NDArrayPool::new(1_000_000);
567        for i in 1..=5 {
568            let arr = make_array_with_attrs(i, &[("Val", i as f64)]);
569            proc.process_array(&arr, &pool);
570        }
571        let idx = proc.find_attribute("Val").unwrap();
572        let buf = proc.buffer(idx).unwrap();
573        assert_eq!(buf.len(), 3);
574        assert!((buf[0] - 3.0).abs() < 1e-10);
575        assert!((buf[2] - 5.0).abs() < 1e-10);
576    }
577
578    #[test]
579    fn test_uid_decrease_resets_buffers() {
580        let mut proc = AttrPlotProcessor::new(8, 100, 1);
581        let pool = NDArrayPool::new(1_000_000);
582        for i in 1..=5 {
583            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
584            proc.process_array(&arr, &pool);
585        }
586        let idx = proc.find_attribute("X").unwrap();
587        assert_eq!(proc.buffer(idx).unwrap().len(), 5);
588
589        let arr = make_array_with_attrs(1, &[("X", 100.0)]);
590        proc.process_array(&arr, &pool);
591        let buf = proc.buffer(idx).unwrap();
592        assert_eq!(buf.len(), 1);
593        assert!((buf[0] - 100.0).abs() < 1e-10);
594    }
595
596    #[test]
597    fn test_missing_attribute_uses_nan() {
598        let mut proc = AttrPlotProcessor::new(8, 100, 1);
599        let pool = NDArrayPool::new(1_000_000);
600        let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
601        proc.process_array(&arr1, &pool);
602
603        let mut arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
604        arr2.unique_id = 2;
605        proc.process_array(&arr2, &pool);
606
607        let idx = proc.find_attribute("Temp").unwrap();
608        let buf = proc.buffer(idx).unwrap();
609        assert_eq!(buf.len(), 2);
610        assert!((buf[0] - 25.0).abs() < 1e-10);
611        assert!(buf[1].is_nan());
612    }
613
614    #[test]
615    fn test_manual_reset() {
616        let mut proc = AttrPlotProcessor::new(8, 100, 1);
617        let pool = NDArrayPool::new(1_000_000);
618        let arr = make_array_with_attrs(5, &[("A", 1.0), ("B", 2.0)]);
619        proc.process_array(&arr, &pool);
620        assert_eq!(proc.num_attributes(), 2);
621
622        proc.reset();
623        // Re-initializes from the next frame.
624        let arr2 = make_array_with_attrs(1, &[("C", 3.0)]);
625        proc.process_array(&arr2, &pool);
626        assert_eq!(proc.num_attributes(), 1);
627        assert_eq!(proc.attributes()[0], "C");
628    }
629
630    #[test]
631    fn test_unlimited_buffer() {
632        let mut proc = AttrPlotProcessor::new(8, 0, 1);
633        let pool = NDArrayPool::new(1_000_000);
634        for i in 1..=100 {
635            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
636            proc.process_array(&arr, &pool);
637        }
638        let idx = proc.find_attribute("X").unwrap();
639        assert_eq!(proc.buffer(idx).unwrap().len(), 100);
640    }
641
642    #[test]
643    fn test_plugin_type() {
644        // C PluginType is "NDAttrPlot" (NDPluginAttrPlot.cpp:87).
645        let proc = AttrPlotProcessor::new(8, 100, 1);
646        assert_eq!(proc.plugin_type(), "NDAttrPlot");
647    }
648}