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() {
146 return Err("attribute selection out of range");
147 }
148 self.data_selections[block] = value;
149 Ok(())
150 }
151
152 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 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 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 fn rebuild_attributes(&mut self, array: &NDArray) {
187 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 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 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 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 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 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 fn build_updates(&self) -> Vec<ParamUpdate> {
283 let mut updates = Vec::new();
284 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 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 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 "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 return ParamChangeResult::updates(self.build_updates());
380 }
381 } else if Some(reason) == self.params.reset {
382 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 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 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(); 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 assert!(proc.set_data_select(0, 1).is_err());
470 assert!(proc.set_data_select(5, 0).is_err());
472 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 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 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 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 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 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 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 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 let proc = AttrPlotProcessor::new(8, 100, 1);
646 assert_eq!(proc.plugin_type(), "NDAttrPlot");
647 }
648}