1use 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
28pub const ATTRPLOT_UID_INDEX: i32 = -1;
30pub const ATTRPLOT_NONE_INDEX: i32 = -2;
32pub const ATTRPLOT_UID_LABEL: &str = "UID";
34pub const ATTRPLOT_NONE_LABEL: &str = "None";
36
37pub struct AttrPlotProcessor {
39 n_attributes: usize,
41 n_data_blocks: usize,
43 cache_size: usize,
45 attributes: Vec<String>,
47 buffers: Vec<VecDeque<f64>>,
49 uid_buffer: VecDeque<f64>,
51 data_selections: Vec<i32>,
54 initialized: bool,
56 last_uid: i32,
58 params: AttrPlotParams,
60}
61
62#[derive(Default)]
64struct AttrPlotParams {
65 data: Option<usize>,
67 data_label: Option<usize>,
69 data_select: Option<usize>,
71 attribute: Option<usize>,
73 reset: Option<usize>,
75 npts: Option<usize>,
77}
78
79impl AttrPlotProcessor {
80 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 pub fn attributes(&self) -> &[String] {
102 &self.attributes
103 }
104
105 pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
107 self.buffers.get(index)
108 }
109
110 pub fn uid_buffer(&self) -> &VecDeque<f64> {
112 &self.uid_buffer
113 }
114
115 pub fn num_attributes(&self) -> usize {
117 self.attributes.len()
118 }
119
120 pub fn num_data_blocks(&self) -> usize {
122 self.n_data_blocks
123 }
124
125 pub fn find_attribute(&self, name: &str) -> Option<usize> {
127 self.attributes.iter().position(|n| n == name)
128 }
129
130 pub fn data_select(&self, block: usize) -> Option<i32> {
132 self.data_selections.get(block).copied()
133 }
134
135 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 if value >= 0 && (value as usize) >= self.attributes.len() {
144 return Err("attribute selection out of range");
145 }
146 self.data_selections[block] = value;
147 Ok(())
148 }
149
150 pub fn data_label(&self, block: usize) -> String {
152 match self.data_selections.get(block).copied() {
153 Some(ATTRPLOT_UID_INDEX) => ATTRPLOT_UID_LABEL.to_string(),
154 Some(sel) if sel >= 0 && (sel as usize) < self.attributes.len() => {
155 self.attributes[sel as usize].clone()
156 }
157 _ => ATTRPLOT_NONE_LABEL.to_string(),
158 }
159 }
160
161 pub fn reset(&mut self) {
163 self.initialized = false;
164 self.uid_buffer.clear();
165 for buf in &mut self.buffers {
166 buf.clear();
167 }
168 self.last_uid = -1;
169 }
170
171 fn push_capped(buf: &mut VecDeque<f64>, value: f64, cache_size: usize) {
173 if cache_size > 0 && buf.len() >= cache_size {
174 buf.pop_front();
175 }
176 buf.push_back(value);
177 }
178
179 fn rebuild_attributes(&mut self, array: &NDArray) {
185 let prior: Vec<Option<String>> = self
187 .data_selections
188 .iter()
189 .map(|&sel| match sel {
190 ATTRPLOT_UID_INDEX => Some(ATTRPLOT_UID_LABEL.to_string()),
191 s if s >= 0 && (s as usize) < self.attributes.len() => {
192 Some(self.attributes[s as usize].clone())
193 }
194 _ => None,
195 })
196 .collect();
197
198 let mut names: Vec<String> = Vec::new();
199 for attr in array.attributes.iter() {
200 if attr.value.as_f64().is_some() {
201 names.push(attr.name.clone());
202 }
203 }
204 names.sort();
205 names.truncate(self.n_attributes);
206
207 self.buffers = vec![VecDeque::new(); names.len()];
208 self.attributes = names;
209
210 for (i, want) in prior.into_iter().enumerate() {
212 self.data_selections[i] = match want {
213 Some(ref n) if n == ATTRPLOT_UID_LABEL => ATTRPLOT_UID_INDEX,
214 Some(n) => self
215 .attributes
216 .iter()
217 .position(|a| a == &n)
218 .map(|p| p as i32)
219 .unwrap_or(ATTRPLOT_NONE_INDEX),
220 None => ATTRPLOT_NONE_INDEX,
221 };
222 }
223 self.initialized = true;
224 }
225
226 fn push_data(&mut self, array: &NDArray) {
228 Self::push_capped(
229 &mut self.uid_buffer,
230 array.unique_id as f64,
231 self.cache_size,
232 );
233 for (i, name) in self.attributes.iter().enumerate() {
234 let value = array
235 .attributes
236 .get(name)
237 .and_then(|attr| attr.value.as_f64())
238 .unwrap_or(f64::NAN);
239 Self::push_capped(&mut self.buffers[i], value, self.cache_size);
240 }
241 }
242
243 fn block_waveform(&self, block: usize) -> Vec<f64> {
248 let selected = self
249 .data_selections
250 .get(block)
251 .copied()
252 .unwrap_or(ATTRPLOT_NONE_INDEX);
253 let src: Option<&VecDeque<f64>> = match selected {
254 ATTRPLOT_UID_INDEX => Some(&self.uid_buffer),
255 s if s >= 0 && (s as usize) < self.buffers.len() => Some(&self.buffers[s as usize]),
256 _ => None,
257 };
258 let size = self.uid_buffer.len();
259 let target = if self.cache_size > 0 {
261 self.cache_size
262 } else {
263 size
264 };
265 let mut out: Vec<f64> = match src {
266 Some(buf) => buf.iter().copied().collect(),
267 None => vec![f64::NAN; size],
268 };
269 let pad = out.last().copied().unwrap_or(f64::NAN);
271 if out.len() < target {
272 out.resize(target, pad);
273 } else {
274 out.truncate(target);
275 }
276 out
277 }
278
279 fn build_updates(&self) -> Vec<ParamUpdate> {
281 let mut updates = Vec::new();
282 if let Some(data) = self.params.data {
284 for block in 0..self.n_data_blocks {
285 updates.push(ParamUpdate::float64_array_addr(
286 data,
287 block as i32,
288 self.block_waveform(block),
289 ));
290 }
291 }
292 if let Some(label) = self.params.data_label {
293 for block in 0..self.n_data_blocks {
294 updates.push(ParamUpdate::octet_addr(
295 label,
296 block as i32,
297 self.data_label(block),
298 ));
299 }
300 }
301 if let Some(select) = self.params.data_select {
302 for block in 0..self.n_data_blocks {
303 updates.push(ParamUpdate::int32_addr(
304 select,
305 block as i32,
306 self.data_selections[block],
307 ));
308 }
309 }
310 if let Some(attribute) = self.params.attribute {
312 for i in 0..self.n_attributes {
313 let name = self.attributes.get(i).cloned().unwrap_or_default();
314 updates.push(ParamUpdate::octet_addr(attribute, i as i32, name));
315 }
316 }
317 if let Some(npts) = self.params.npts {
318 updates.push(ParamUpdate::int32(npts, self.uid_buffer.len() as i32));
319 }
320 updates
321 }
322}
323
324impl NDPluginProcess for AttrPlotProcessor {
325 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
326 if !self.uid_buffer.is_empty() && array.unique_id <= self.last_uid {
328 self.reset();
329 }
330 self.last_uid = array.unique_id;
331
332 if !self.initialized {
333 self.rebuild_attributes(array);
334 }
335 self.push_data(array);
336
337 ProcessResult::sink(self.build_updates())
338 }
339
340 fn plugin_type(&self) -> &str {
341 "NDPluginAttrPlot"
342 }
343
344 fn register_params(
345 &mut self,
346 base: &mut asyn_rs::port::PortDriverBase,
347 ) -> asyn_rs::error::AsynResult<()> {
348 use asyn_rs::param::ParamType;
349 base.create_param("AP_Data", ParamType::Float64Array)?;
350 base.create_param("AP_DataLabel", ParamType::Octet)?;
351 base.create_param("AP_DataSelect", ParamType::Int32)?;
352 base.create_param("AP_Attribute", ParamType::Octet)?;
353 base.create_param("AP_Reset", ParamType::Int32)?;
354 base.create_param("AP_NPts", ParamType::Int32)?;
355
356 self.params.data = base.find_param("AP_Data");
357 self.params.data_label = base.find_param("AP_DataLabel");
358 self.params.data_select = base.find_param("AP_DataSelect");
359 self.params.attribute = base.find_param("AP_Attribute");
360 self.params.reset = base.find_param("AP_Reset");
361 self.params.npts = base.find_param("AP_NPts");
362 Ok(())
363 }
364
365 fn on_param_change(
366 &mut self,
367 reason: usize,
368 params: &PluginParamSnapshot,
369 ) -> ParamChangeResult {
370 if Some(reason) == self.params.data_select {
371 let block = params.addr as usize;
372 let value = params.value.as_i32();
373 if self.set_data_select(block, value).is_ok() {
374 return ParamChangeResult::updates(self.build_updates());
376 }
377 } else if Some(reason) == self.params.reset {
378 if params.value.as_i32() != 0 {
379 self.reset();
380 return ParamChangeResult::updates(self.build_updates());
381 }
382 }
383 ParamChangeResult::updates(vec![])
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
391 use ad_core_rs::ndarray::{NDDataType, NDDimension};
392
393 fn make_array_with_attrs(uid: i32, attrs: &[(&str, f64)]) -> NDArray {
394 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
395 arr.unique_id = uid;
396 for (name, value) in attrs {
397 arr.attributes.add(NDAttribute::new_static(
398 *name,
399 String::new(),
400 NDAttrSource::Driver,
401 NDAttrValue::Float64(*value),
402 ));
403 }
404 arr
405 }
406
407 #[test]
408 fn test_attribute_auto_detection() {
409 let mut proc = AttrPlotProcessor::new(8, 100, 4);
410 let pool = NDArrayPool::new(1_000_000);
411
412 let mut arr = make_array_with_attrs(1, &[("Temp", 25.0), ("Gain", 1.5)]);
413 arr.attributes.add(NDAttribute::new_static(
414 "Label",
415 String::new(),
416 NDAttrSource::Driver,
417 NDAttrValue::String("test".to_string()),
418 ));
419 proc.process_array(&arr, &pool);
420
421 assert_eq!(proc.num_attributes(), 2);
422 assert_eq!(proc.attributes()[0], "Gain");
423 assert_eq!(proc.attributes()[1], "Temp");
424 }
425
426 #[test]
427 fn test_n_attributes_caps_tracked_count() {
428 let mut proc = AttrPlotProcessor::new(2, 100, 1);
430 let pool = NDArrayPool::new(1_000_000);
431 let arr = make_array_with_attrs(1, &[("D", 4.0), ("A", 1.0), ("C", 3.0), ("B", 2.0)]);
432 proc.process_array(&arr, &pool);
433 assert_eq!(proc.num_attributes(), 2);
434 assert_eq!(proc.attributes(), &["A", "B"]);
435 }
436
437 #[test]
438 fn test_data_select_maps_block_to_attribute() {
439 let mut proc = AttrPlotProcessor::new(8, 100, 2);
441 let pool = NDArrayPool::new(1_000_000);
442 let arr = make_array_with_attrs(1, &[("A", 10.0), ("B", 20.0), ("C", 30.0)]);
443 proc.process_array(&arr, &pool);
444
445 proc.set_data_select(0, 1).unwrap(); proc.set_data_select(1, ATTRPLOT_UID_INDEX).unwrap();
447
448 assert_eq!(proc.data_label(0), "B");
449 assert_eq!(proc.data_label(1), ATTRPLOT_UID_LABEL);
450
451 let wf0 = proc.block_waveform(0);
452 assert!((wf0[0] - 20.0).abs() < 1e-10, "block 0 plots attribute B");
453 let wf1 = proc.block_waveform(1);
454 assert!((wf1[0] - 1.0).abs() < 1e-10, "block 1 plots UID");
455 }
456
457 #[test]
458 fn test_data_select_rejects_out_of_range() {
459 let mut proc = AttrPlotProcessor::new(8, 100, 2);
460 let pool = NDArrayPool::new(1_000_000);
461 let arr = make_array_with_attrs(1, &[("A", 1.0)]);
462 proc.process_array(&arr, &pool);
463
464 assert!(proc.set_data_select(0, 1).is_err());
466 assert!(proc.set_data_select(5, 0).is_err());
468 assert!(proc.set_data_select(0, 0).is_ok());
470 assert!(proc.set_data_select(1, ATTRPLOT_UID_INDEX).is_ok());
471 }
472
473 #[test]
474 fn test_unbound_block_label_is_none() {
475 let mut proc = AttrPlotProcessor::new(8, 100, 3);
476 let pool = NDArrayPool::new(1_000_000);
477 let arr = make_array_with_attrs(1, &[("A", 1.0)]);
478 proc.process_array(&arr, &pool);
479 assert_eq!(proc.data_label(2), ATTRPLOT_NONE_LABEL);
481 assert_eq!(proc.data_select(2), Some(ATTRPLOT_NONE_INDEX));
482 }
483
484 #[test]
485 fn test_npts_tracks_point_count() {
486 let mut proc = AttrPlotProcessor::new(8, 100, 1);
487 let pool = NDArrayPool::new(1_000_000);
488 for i in 1..=4 {
489 let arr = make_array_with_attrs(i, &[("X", i as f64)]);
490 proc.process_array(&arr, &pool);
491 }
492 assert_eq!(proc.uid_buffer().len(), 4);
493 }
494
495 #[test]
496 fn test_waveform_padded_to_cache_size() {
497 let mut proc = AttrPlotProcessor::new(8, 6, 1);
500 let pool = NDArrayPool::new(1_000_000);
501 for i in 1..=3 {
502 let arr = make_array_with_attrs(i, &[("X", i as f64 * 10.0)]);
503 proc.process_array(&arr, &pool);
504 }
505 proc.set_data_select(0, 0).unwrap();
506 let wf = proc.block_waveform(0);
507 assert_eq!(wf.len(), 6);
508 assert!((wf[0] - 10.0).abs() < 1e-10);
509 assert!((wf[2] - 30.0).abs() < 1e-10);
510 assert!((wf[3] - 30.0).abs() < 1e-10);
512 assert!((wf[5] - 30.0).abs() < 1e-10);
513 }
514
515 #[test]
516 fn test_data_select_preserved_across_rebuild() {
517 let mut proc = AttrPlotProcessor::new(8, 100, 1);
520 let pool = NDArrayPool::new(1_000_000);
521 let arr = make_array_with_attrs(5, &[("Gain", 1.0), ("Temp", 25.0)]);
522 proc.process_array(&arr, &pool);
523 let temp_idx = proc.find_attribute("Temp").unwrap() as i32;
524 proc.set_data_select(0, temp_idx).unwrap();
525
526 let arr2 = make_array_with_attrs(1, &[("Gain", 2.0), ("Temp", 99.0)]);
528 proc.process_array(&arr2, &pool);
529 assert_eq!(proc.data_label(0), "Temp");
530 let wf = proc.block_waveform(0);
531 assert!((wf[0] - 99.0).abs() < 1e-10);
532 }
533
534 #[test]
535 fn test_value_tracking() {
536 let mut proc = AttrPlotProcessor::new(8, 100, 1);
537 let pool = NDArrayPool::new(1_000_000);
538 for i in 1..=5 {
539 let arr = make_array_with_attrs(i, &[("Value", i as f64 * 10.0)]);
540 proc.process_array(&arr, &pool);
541 }
542 let idx = proc.find_attribute("Value").unwrap();
543 let buf = proc.buffer(idx).unwrap();
544 assert_eq!(buf.len(), 5);
545 assert!((buf[0] - 10.0).abs() < 1e-10);
546 assert!((buf[4] - 50.0).abs() < 1e-10);
547 }
548
549 #[test]
550 fn test_circular_buffer_cache_size() {
551 let mut proc = AttrPlotProcessor::new(8, 3, 1);
552 let pool = NDArrayPool::new(1_000_000);
553 for i in 1..=5 {
554 let arr = make_array_with_attrs(i, &[("Val", i as f64)]);
555 proc.process_array(&arr, &pool);
556 }
557 let idx = proc.find_attribute("Val").unwrap();
558 let buf = proc.buffer(idx).unwrap();
559 assert_eq!(buf.len(), 3);
560 assert!((buf[0] - 3.0).abs() < 1e-10);
561 assert!((buf[2] - 5.0).abs() < 1e-10);
562 }
563
564 #[test]
565 fn test_uid_decrease_resets_buffers() {
566 let mut proc = AttrPlotProcessor::new(8, 100, 1);
567 let pool = NDArrayPool::new(1_000_000);
568 for i in 1..=5 {
569 let arr = make_array_with_attrs(i, &[("X", i as f64)]);
570 proc.process_array(&arr, &pool);
571 }
572 let idx = proc.find_attribute("X").unwrap();
573 assert_eq!(proc.buffer(idx).unwrap().len(), 5);
574
575 let arr = make_array_with_attrs(1, &[("X", 100.0)]);
576 proc.process_array(&arr, &pool);
577 let buf = proc.buffer(idx).unwrap();
578 assert_eq!(buf.len(), 1);
579 assert!((buf[0] - 100.0).abs() < 1e-10);
580 }
581
582 #[test]
583 fn test_missing_attribute_uses_nan() {
584 let mut proc = AttrPlotProcessor::new(8, 100, 1);
585 let pool = NDArrayPool::new(1_000_000);
586 let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
587 proc.process_array(&arr1, &pool);
588
589 let mut arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
590 arr2.unique_id = 2;
591 proc.process_array(&arr2, &pool);
592
593 let idx = proc.find_attribute("Temp").unwrap();
594 let buf = proc.buffer(idx).unwrap();
595 assert_eq!(buf.len(), 2);
596 assert!((buf[0] - 25.0).abs() < 1e-10);
597 assert!(buf[1].is_nan());
598 }
599
600 #[test]
601 fn test_manual_reset() {
602 let mut proc = AttrPlotProcessor::new(8, 100, 1);
603 let pool = NDArrayPool::new(1_000_000);
604 let arr = make_array_with_attrs(5, &[("A", 1.0), ("B", 2.0)]);
605 proc.process_array(&arr, &pool);
606 assert_eq!(proc.num_attributes(), 2);
607
608 proc.reset();
609 let arr2 = make_array_with_attrs(1, &[("C", 3.0)]);
611 proc.process_array(&arr2, &pool);
612 assert_eq!(proc.num_attributes(), 1);
613 assert_eq!(proc.attributes()[0], "C");
614 }
615
616 #[test]
617 fn test_unlimited_buffer() {
618 let mut proc = AttrPlotProcessor::new(8, 0, 1);
619 let pool = NDArrayPool::new(1_000_000);
620 for i in 1..=100 {
621 let arr = make_array_with_attrs(i, &[("X", i as f64)]);
622 proc.process_array(&arr, &pool);
623 }
624 let idx = proc.find_attribute("X").unwrap();
625 assert_eq!(proc.buffer(idx).unwrap().len(), 100);
626 }
627
628 #[test]
629 fn test_plugin_type() {
630 let proc = AttrPlotProcessor::new(8, 100, 1);
631 assert_eq!(proc.plugin_type(), "NDPluginAttrPlot");
632 }
633}