1use std::collections::{HashMap, VecDeque};
2use std::sync::Arc;
3
4use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
5use ad_core_rs::ndarray::NDArray;
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::runtime::{NDPluginProcess, ParamUpdate, ProcessResult};
8use serde::Deserialize;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum PosMode {
13 Discard,
14 Keep,
15}
16
17#[derive(Debug, Deserialize)]
19pub struct PositionList {
20 pub positions: Vec<HashMap<String, f64>>,
21}
22
23#[derive(Default)]
27struct PosParamIndices {
28 filename: Option<usize>,
29 file_valid: Option<usize>,
30 clear: Option<usize>,
31 running: Option<usize>,
32 restart: Option<usize>,
33 delete: Option<usize>,
34 mode: Option<usize>,
35 append: Option<usize>,
36 current_qty: Option<usize>,
37 current_index: Option<usize>,
38 current_pos: Option<usize>,
39 missing_frames: Option<usize>,
40 duplicate_frames: Option<usize>,
41 expected_id: Option<usize>,
42 id_name: Option<usize>,
43 id_difference: Option<usize>,
44 id_start: Option<usize>,
45}
46
47pub struct PosPluginProcessor {
49 positions: VecDeque<HashMap<String, f64>>,
50 all_positions: Vec<HashMap<String, f64>>,
51 mode: PosMode,
52 index: usize,
53 running: bool,
54 expected_id: i32,
55 id_start: i32,
58 id_difference: i32,
61 missing_frames: usize,
62 duplicate_frames: usize,
63 params: PosParamIndices,
64}
65
66impl PosPluginProcessor {
67 pub fn new(mode: PosMode) -> Self {
68 Self {
69 positions: VecDeque::new(),
70 all_positions: Vec::new(),
71 mode,
72 index: 0,
73 running: false,
74 expected_id: 0,
75 id_start: 1,
76 id_difference: 1,
77 missing_frames: 0,
78 duplicate_frames: 0,
79 params: PosParamIndices::default(),
80 }
81 }
82
83 pub fn load_positions_json(&mut self, json_str: &str) -> Result<usize, serde_json::Error> {
85 let list: PositionList = serde_json::from_str(json_str)?;
86 let count = list.positions.len();
87 self.all_positions = list.positions.clone();
88 self.positions = list.positions.into();
89 self.index = 0;
90 Ok(count)
91 }
92
93 pub fn load_positions_xml(&mut self, xml_str: &str) -> Result<usize, String> {
115 let positions = parse_positions_xml(xml_str)?;
116 let count = positions.len();
117 self.all_positions = positions.clone();
118 self.positions = positions.into();
119 self.index = 0;
120 Ok(count)
121 }
122
123 pub fn load_positions_auto(&mut self, content: &str) -> Result<usize, String> {
128 if content.trim_start().starts_with('<') {
129 self.load_positions_xml(content)
130 } else {
131 self.load_positions_json(content)
132 .map_err(|e| format!("JSON parse error: {}", e))
133 }
134 }
135
136 pub fn load_positions(&mut self, positions: Vec<HashMap<String, f64>>) {
138 self.all_positions = positions.clone();
139 self.positions = positions.into();
140 self.index = 0;
141 }
142
143 pub fn start(&mut self) {
150 self.running = true;
151 self.expected_id = self.id_start;
152 }
153
154 pub fn stop(&mut self) {
156 self.running = false;
157 }
158
159 pub fn clear(&mut self) {
161 self.positions.clear();
162 self.all_positions.clear();
163 self.index = 0;
164 }
165
166 pub fn missing_frames(&self) -> usize {
167 self.missing_frames
168 }
169
170 pub fn duplicate_frames(&self) -> usize {
171 self.duplicate_frames
172 }
173
174 pub fn remaining_positions(&self) -> usize {
175 match self.mode {
176 PosMode::Discard => self.positions.len(),
177 PosMode::Keep => self.all_positions.len(),
178 }
179 }
180
181 fn current_position(&self) -> Option<&HashMap<String, f64>> {
182 match self.mode {
183 PosMode::Discard => self.positions.front(),
184 PosMode::Keep => {
185 if self.index < self.all_positions.len() {
186 Some(&self.all_positions[self.index])
187 } else {
188 None
189 }
190 }
191 }
192 }
193
194 fn has_position(&self) -> bool {
198 match self.mode {
199 PosMode::Discard => !self.positions.is_empty(),
200 PosMode::Keep => self.index < self.all_positions.len(),
201 }
202 }
203
204 fn advance(&mut self) {
205 match self.mode {
206 PosMode::Discard => {
207 self.positions.pop_front();
208 }
209 PosMode::Keep => {
210 self.index += 1;
211 }
212 }
213 }
214
215 fn current_index_param(&self) -> i32 {
219 match self.mode {
220 PosMode::Discard => 0,
221 PosMode::Keep => self.index as i32,
222 }
223 }
224
225 fn exhausted_result(&mut self) -> ProcessResult {
229 self.running = false;
230 let mut updates = Vec::new();
231 push_int(&mut updates, self.params.running, 0);
232 ProcessResult {
233 output_arrays: vec![],
234 param_updates: updates,
235 scatter: false,
236 }
237 }
238}
239
240fn push_int(updates: &mut Vec<ParamUpdate>, idx: Option<usize>, value: i32) {
242 if let Some(i) = idx {
243 updates.push(ParamUpdate::int32(i, value));
244 }
245}
246
247fn push_str(updates: &mut Vec<ParamUpdate>, idx: Option<usize>, value: String) {
249 if let Some(i) = idx {
250 updates.push(ParamUpdate::octet(i, value));
251 }
252}
253
254fn format_cpp_g6(v: f64) -> String {
260 const PREC: i32 = 6;
261 if v == 0.0 {
262 return if v.is_sign_negative() { "-0" } else { "0" }.to_string();
263 }
264 if v.is_nan() {
265 return "nan".to_string();
266 }
267 if v.is_infinite() {
268 return if v < 0.0 { "-inf" } else { "inf" }.to_string();
269 }
270 let sci = format!("{:.*e}", (PREC - 1) as usize, v);
273 let epos = sci.find('e').unwrap();
274 let exp: i32 = sci[epos + 1..].parse().unwrap();
275 if exp >= -4 && exp < PREC {
276 let prec = (PREC - 1 - exp).max(0) as usize;
278 strip_g_trailing(&format!("{:.*}", prec, v))
279 } else {
280 let mantissa = strip_g_trailing(&sci[..epos]);
283 let sign = if exp < 0 { '-' } else { '+' };
284 format!("{}e{}{:02}", mantissa, sign, exp.abs())
285 }
286}
287
288fn strip_g_trailing(s: &str) -> String {
291 if s.contains('.') {
292 s.trim_end_matches('0').trim_end_matches('.').to_string()
293 } else {
294 s.to_string()
295 }
296}
297
298fn parse_positions_xml(xml: &str) -> Result<Vec<HashMap<String, f64>>, String> {
310 let dimensions: Vec<String> = element_tag_contents(xml, "dimension")
312 .into_iter()
313 .filter_map(|content| parse_tag_attributes(content).remove("name"))
314 .collect();
315
316 let mut positions: Vec<HashMap<String, f64>> = Vec::new();
317 for content in element_tag_contents(xml, "position") {
318 let attrs = parse_tag_attributes(content);
319 if attrs.is_empty() {
321 continue;
322 }
323 let mut pos = HashMap::new();
324 let mut ok = true;
325 for dim in &dimensions {
326 match attrs.get(dim).and_then(|v| v.parse::<f64>().ok()) {
327 Some(value) => {
328 pos.insert(dim.clone(), value);
329 }
330 None => {
331 ok = false;
333 break;
334 }
335 }
336 }
337 if ok {
338 positions.push(pos);
339 }
340 }
341
342 Ok(positions)
343}
344
345fn is_tag_boundary(c: char) -> bool {
350 c.is_ascii_whitespace() || c == '>' || c == '/'
351}
352
353fn element_tag_contents<'a>(xml: &'a str, name: &str) -> Vec<&'a str> {
357 let prefix = format!("<{}", name);
358 let mut out = Vec::new();
359 let mut from = 0;
360 while let Some(rel) = xml[from..].find(&prefix) {
361 let open = from + rel;
362 let after = open + prefix.len();
363 match xml[after..].chars().next() {
364 Some(c) if is_tag_boundary(c) => {}
365 _ => {
366 from = after;
368 continue;
369 }
370 }
371 let Some(rel_end) = xml[after..].find('>') else {
372 break;
373 };
374 let end = after + rel_end;
375 out.push(&xml[after..end]);
376 from = end + 1;
377 }
378 out
379}
380
381fn parse_tag_attributes(content: &str) -> HashMap<String, String> {
384 let mut attrs = HashMap::new();
385 let bytes = content.as_bytes();
386 let mut i = 0;
387 while let Some(eq_rel) = content[i..].find('=') {
388 let eq = i + eq_rel;
389 let mut k_end = eq;
391 while k_end > i && bytes[k_end - 1].is_ascii_whitespace() {
392 k_end -= 1;
393 }
394 let mut k_start = k_end;
395 while k_start > i
396 && !bytes[k_start - 1].is_ascii_whitespace()
397 && bytes[k_start - 1] != b'/'
398 && bytes[k_start - 1] != b'='
399 {
400 k_start -= 1;
401 }
402 let key = &content[k_start..k_end];
403 let mut j = eq + 1;
405 while j < bytes.len() && bytes[j].is_ascii_whitespace() {
406 j += 1;
407 }
408 if j >= bytes.len() {
409 break;
410 }
411 let quote = bytes[j];
412 if quote != b'"' && quote != b'\'' {
413 i = eq + 1;
414 continue;
415 }
416 j += 1;
417 let val_start = j;
418 while j < bytes.len() && bytes[j] != quote {
419 j += 1;
420 }
421 if j >= bytes.len() {
422 break; }
424 if !key.is_empty() {
425 attrs.insert(key.to_string(), content[val_start..j].to_string());
426 }
427 i = j + 1;
428 }
429 attrs
430}
431
432impl NDPluginProcess for PosPluginProcessor {
433 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
434 if !self.running {
435 return ProcessResult::empty();
439 }
440
441 if !self.has_position() {
445 return self.exhausted_result();
446 }
447
448 let uid = array.unique_id;
453 if uid > self.expected_id {
454 while self.expected_id < uid && self.has_position() {
458 self.advance();
459 self.expected_id += self.id_difference;
460 self.missing_frames += 1;
461 }
462 if !self.has_position() {
463 return self.exhausted_result();
466 }
467 } else if uid < self.expected_id {
468 self.duplicate_frames += 1;
469 let mut updates = Vec::new();
472 push_int(
473 &mut updates,
474 self.params.duplicate_frames,
475 self.duplicate_frames as i32,
476 );
477 return ProcessResult {
478 output_arrays: vec![],
479 param_updates: updates,
480 scatter: false,
481 };
482 }
483
484 let position = self.current_position().unwrap().clone();
486
487 let mut out = array.clone();
488 let mut keys: Vec<&String> = position.keys().collect();
493 keys.sort();
494 let mut current_pos = String::from("[");
495 for (n, key) in keys.iter().enumerate() {
496 let value = position[*key];
497 if n > 0 {
498 current_pos.push(',');
499 }
500 current_pos.push_str(key);
501 current_pos.push('=');
502 current_pos.push_str(&format_cpp_g6(value));
503 out.attributes.add(NDAttribute::new_static(
504 (*key).clone(),
505 "Position of NDArray",
506 NDAttrSource::Driver,
507 NDAttrValue::Float64(value),
508 ));
509 }
510 current_pos.push(']');
511
512 self.advance();
513 self.expected_id += self.id_difference;
516
517 let mut updates = Vec::new();
521 push_int(
522 &mut updates,
523 self.params.missing_frames,
524 self.missing_frames as i32,
525 );
526 push_int(
527 &mut updates,
528 self.params.duplicate_frames,
529 self.duplicate_frames as i32,
530 );
531 push_int(
532 &mut updates,
533 self.params.current_qty,
534 self.remaining_positions() as i32,
535 );
536 push_int(
537 &mut updates,
538 self.params.current_index,
539 self.current_index_param(),
540 );
541 push_str(&mut updates, self.params.current_pos, current_pos);
543
544 ProcessResult {
545 output_arrays: vec![Arc::new(out)],
546 param_updates: updates,
547 scatter: false,
548 }
549 }
550
551 fn plugin_type(&self) -> &str {
552 "NDPositionPlugin"
555 }
556
557 fn register_params(
558 &mut self,
559 base: &mut asyn_rs::port::PortDriverBase,
560 ) -> asyn_rs::error::AsynResult<()> {
561 use asyn_rs::param::ParamType;
562 base.create_param("NDPos_Filename", ParamType::Octet)?;
564 base.create_param("NDPos_FileValid", ParamType::Int32)?;
565 base.create_param("NDPos_Clear", ParamType::Int32)?;
566 base.create_param("NDPos_Running", ParamType::Int32)?;
567 base.create_param("NDPos_Restart", ParamType::Int32)?;
568 base.create_param("NDPos_Delete", ParamType::Int32)?;
569 base.create_param("NDPos_Mode", ParamType::Int32)?;
570 base.create_param("NDPos_Append", ParamType::Int32)?;
571 base.create_param("NDPos_CurrentQty", ParamType::Int32)?;
572 base.create_param("NDPos_CurrentIndex", ParamType::Int32)?;
573 base.create_param("NDPos_CurrentPos", ParamType::Octet)?;
574 base.create_param("NDPos_MissingFrames", ParamType::Int32)?;
575 base.create_param("NDPos_DuplicateFrames", ParamType::Int32)?;
576 base.create_param("NDPos_ExpectedID", ParamType::Int32)?;
577 base.create_param("NDPos_IDName", ParamType::Octet)?;
578 base.create_param("NDPos_IDDifference", ParamType::Int32)?;
579 base.create_param("NDPos_IDStart", ParamType::Int32)?;
580
581 self.params.filename = base.find_param("NDPos_Filename");
582 self.params.file_valid = base.find_param("NDPos_FileValid");
583 self.params.clear = base.find_param("NDPos_Clear");
584 self.params.running = base.find_param("NDPos_Running");
585 self.params.restart = base.find_param("NDPos_Restart");
586 self.params.delete = base.find_param("NDPos_Delete");
587 self.params.mode = base.find_param("NDPos_Mode");
588 self.params.append = base.find_param("NDPos_Append");
589 self.params.current_qty = base.find_param("NDPos_CurrentQty");
590 self.params.current_index = base.find_param("NDPos_CurrentIndex");
591 self.params.current_pos = base.find_param("NDPos_CurrentPos");
592 self.params.missing_frames = base.find_param("NDPos_MissingFrames");
593 self.params.duplicate_frames = base.find_param("NDPos_DuplicateFrames");
594 self.params.expected_id = base.find_param("NDPos_ExpectedID");
595 self.params.id_name = base.find_param("NDPos_IDName");
596 self.params.id_difference = base.find_param("NDPos_IDDifference");
597 self.params.id_start = base.find_param("NDPos_IDStart");
598
599 if let Some(i) = self.params.mode {
601 base.set_int32_param(i, 0, self.mode as i32)?;
602 }
603 if let Some(i) = self.params.file_valid {
604 base.set_int32_param(i, 0, 0)?;
605 }
606 if let Some(i) = self.params.current_index {
607 base.set_int32_param(i, 0, 0)?;
608 }
609 if let Some(i) = self.params.current_qty {
610 base.set_int32_param(i, 0, self.remaining_positions() as i32)?;
611 }
612 if let Some(i) = self.params.current_pos {
613 base.set_string_param(i, 0, String::new())?;
614 }
615 if let Some(i) = self.params.running {
616 base.set_int32_param(i, 0, 0)?;
617 }
618 if let Some(i) = self.params.id_name {
619 base.set_string_param(i, 0, String::new())?;
620 }
621 if let Some(i) = self.params.id_difference {
622 base.set_int32_param(i, 0, 1)?;
623 }
624 if let Some(i) = self.params.id_start {
625 base.set_int32_param(i, 0, 1)?;
626 }
627 if let Some(i) = self.params.expected_id {
628 base.set_int32_param(i, 0, 1)?;
629 }
630 if let Some(i) = self.params.missing_frames {
631 base.set_int32_param(i, 0, 0)?;
632 }
633 if let Some(i) = self.params.duplicate_frames {
634 base.set_int32_param(i, 0, 0)?;
635 }
636 Ok(())
637 }
638
639 fn on_param_change(
640 &mut self,
641 reason: usize,
642 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
643 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
644 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue};
645
646 let mut updates = Vec::new();
647 if Some(reason) == self.params.running {
648 if params.value.as_i32() == 0 {
651 self.stop();
652 } else {
653 self.start();
654 push_int(&mut updates, self.params.expected_id, self.id_start);
655 }
656 } else if Some(reason) == self.params.id_start {
657 self.id_start = params.value.as_i32();
659 } else if Some(reason) == self.params.id_difference {
660 self.id_difference = params.value.as_i32();
662 } else if Some(reason) == self.params.mode {
663 self.mode = match params.value.as_i32() {
665 1 => PosMode::Keep,
666 _ => PosMode::Discard,
667 };
668 self.index = 0;
669 push_int(&mut updates, self.params.current_index, 0);
670 } else if Some(reason) == self.params.restart {
671 self.index = 0;
674 push_int(&mut updates, self.params.current_index, 0);
675 push_str(&mut updates, self.params.current_pos, String::new());
676 } else if Some(reason) == self.params.delete {
677 self.clear();
680 push_int(&mut updates, self.params.current_index, 0);
681 push_int(&mut updates, self.params.current_qty, 0);
682 push_str(&mut updates, self.params.current_pos, String::new());
683 } else if Some(reason) == self.params.filename {
684 if let ParamChangeValue::Octet(ref xml) = params.value {
687 match self.load_positions_auto(xml) {
688 Ok(_) => {
689 push_int(&mut updates, self.params.file_valid, 1);
690 push_int(
691 &mut updates,
692 self.params.current_qty,
693 self.remaining_positions() as i32,
694 );
695 }
696 Err(_) => {
697 push_int(&mut updates, self.params.file_valid, 0);
698 }
699 }
700 }
701 }
702 ParamChangeResult::updates(updates)
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use ad_core_rs::ndarray::{NDDataType, NDDimension};
710
711 fn make_array(id: i32) -> NDArray {
712 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
713 arr.unique_id = id;
714 arr
715 }
716
717 #[test]
718 fn test_discard_mode() {
719 let mut proc = PosPluginProcessor::new(PosMode::Discard);
720 let mut pos1 = HashMap::new();
721 pos1.insert("X".into(), 1.5);
722 pos1.insert("Y".into(), 2.3);
723 let mut pos2 = HashMap::new();
724 pos2.insert("X".into(), 3.1);
725 pos2.insert("Y".into(), 4.2);
726
727 proc.load_positions(vec![pos1, pos2]);
728 proc.start();
729
730 let pool = NDArrayPool::new(1_000_000);
731
732 let result = proc.process_array(&make_array(1), &pool);
733 assert_eq!(result.output_arrays.len(), 1);
734 let x = result.output_arrays[0]
735 .attributes
736 .get("X")
737 .unwrap()
738 .value
739 .as_f64()
740 .unwrap();
741 assert!((x - 1.5).abs() < 1e-10);
742
743 let result = proc.process_array(&make_array(2), &pool);
744 let x = result.output_arrays[0]
745 .attributes
746 .get("X")
747 .unwrap()
748 .value
749 .as_f64()
750 .unwrap();
751 assert!((x - 3.1).abs() < 1e-10);
752
753 assert_eq!(proc.remaining_positions(), 0);
754 }
755
756 #[test]
757 fn test_attribute_description() {
758 let mut proc = PosPluginProcessor::new(PosMode::Discard);
760 let mut pos = HashMap::new();
761 pos.insert("X".into(), 1.5);
762 proc.load_positions(vec![pos]);
763 proc.start();
764
765 let pool = NDArrayPool::new(1_000_000);
766 let result = proc.process_array(&make_array(1), &pool);
767 let attr = result.output_arrays[0].attributes.get("X").unwrap();
768 assert_eq!(attr.description, "Position of NDArray");
769 }
770
771 #[test]
772 fn test_keep_mode() {
773 let mut proc = PosPluginProcessor::new(PosMode::Keep);
774 let mut pos1 = HashMap::new();
775 pos1.insert("X".into(), 10.0);
776 let mut pos2 = HashMap::new();
777 pos2.insert("X".into(), 20.0);
778
779 proc.load_positions(vec![pos1, pos2]);
780 proc.start();
781
782 let pool = NDArrayPool::new(1_000_000);
783
784 let result = proc.process_array(&make_array(1), &pool);
785 let x = result.output_arrays[0]
786 .attributes
787 .get("X")
788 .unwrap()
789 .value
790 .as_f64()
791 .unwrap();
792 assert!((x - 10.0).abs() < 1e-10);
793
794 let result = proc.process_array(&make_array(2), &pool);
795 let x = result.output_arrays[0]
796 .attributes
797 .get("X")
798 .unwrap()
799 .value
800 .as_f64()
801 .unwrap();
802 assert!((x - 20.0).abs() < 1e-10);
803
804 let result = proc.process_array(&make_array(3), &pool);
807 assert!(result.output_arrays.is_empty());
808 assert!(!proc.running);
809 }
810
811 #[test]
812 fn test_exhaustion_stops_and_drops() {
813 let mut proc = PosPluginProcessor::new(PosMode::Discard);
816 proc.params.running = Some(5);
817 let mut p1 = HashMap::new();
818 p1.insert("x".into(), 1.0);
819 proc.load_positions(vec![p1]);
820 proc.start();
821
822 let pool = NDArrayPool::new(1_000_000);
823 let r1 = proc.process_array(&make_array(1), &pool);
825 assert_eq!(r1.output_arrays.len(), 1);
826 let r2 = proc.process_array(&make_array(2), &pool);
828 assert!(r2.output_arrays.is_empty());
829 assert!(!proc.running);
830 use ad_core_rs::plugin::runtime::ParamUpdate;
831 assert!(r2.param_updates.iter().any(|u| matches!(
832 u,
833 ParamUpdate::Int32 {
834 reason: 5,
835 value: 0,
836 ..
837 }
838 )));
839 }
840
841 #[test]
842 fn test_missing_frames() {
843 let mut proc = PosPluginProcessor::new(PosMode::Discard);
844 let mut pos1 = HashMap::new();
845 pos1.insert("X".into(), 1.0);
846 let mut pos2 = HashMap::new();
847 pos2.insert("X".into(), 2.0);
848 let mut pos3 = HashMap::new();
849 pos3.insert("X".into(), 3.0);
850
851 proc.load_positions(vec![pos1, pos2, pos3]);
852 proc.start();
853
854 let pool = NDArrayPool::new(1_000_000);
855
856 proc.process_array(&make_array(1), &pool);
857
858 let result = proc.process_array(&make_array(3), &pool);
860 assert_eq!(proc.missing_frames(), 1);
861 let x = result.output_arrays[0]
862 .attributes
863 .get("X")
864 .unwrap()
865 .value
866 .as_f64()
867 .unwrap();
868 assert!((x - 3.0).abs() < 1e-10);
869 }
870
871 #[test]
872 fn test_duplicate_frames() {
873 let mut proc = PosPluginProcessor::new(PosMode::Discard);
874 let mut pos1 = HashMap::new();
875 pos1.insert("X".into(), 1.0);
876 let mut pos2 = HashMap::new();
877 pos2.insert("X".into(), 2.0);
878
879 proc.load_positions(vec![pos1, pos2]);
880 proc.start();
881
882 let pool = NDArrayPool::new(1_000_000);
883
884 proc.process_array(&make_array(1), &pool);
885
886 let result = proc.process_array(&make_array(1), &pool);
887 assert_eq!(proc.duplicate_frames(), 1);
888 assert!(result.output_arrays.is_empty());
889 }
890
891 #[test]
892 fn test_load_json() {
893 let mut proc = PosPluginProcessor::new(PosMode::Discard);
894 let json = r#"{"positions": [{"X": 1.5, "Y": 2.3}, {"X": 3.1, "Y": 4.2}]}"#;
895 let count = proc.load_positions_json(json).unwrap();
896 assert_eq!(count, 2);
897 assert_eq!(proc.remaining_positions(), 2);
898 }
899
900 #[test]
901 fn test_idle_drops_frame() {
902 let mut proc = PosPluginProcessor::new(PosMode::Discard);
905 let pool = NDArrayPool::new(1_000_000);
906 let result = proc.process_array(&make_array(1), &pool);
907 assert!(result.output_arrays.is_empty());
908 }
909
910 #[test]
911 fn test_load_xml() {
912 let mut proc = PosPluginProcessor::new(PosMode::Discard);
913 let xml = r#"<pos_layout>
914 <dimensions>
915 <dimension name="x"/>
916 </dimensions>
917 <positions>
918 <position x="1.5"/>
919 <position x="2.3"/>
920 <position x="3.7"/>
921 </positions>
922</pos_layout>"#;
923 let count = proc.load_positions_xml(xml).unwrap();
924 assert_eq!(count, 3);
925 assert_eq!(proc.remaining_positions(), 3);
926 }
927
928 #[test]
929 fn test_load_xml_dimension_keyed() {
930 let mut proc = PosPluginProcessor::new(PosMode::Discard);
933 let xml = r#"<pos_layout>
934 <dimensions>
935 <dimension name="x"/>
936 <dimension name="y"/>
937 </dimensions>
938 <positions>
939 <position x="10" y="100"/>
940 <position x="20" y="200"/>
941 </positions>
942</pos_layout>"#;
943 let count = proc.load_positions_xml(xml).unwrap();
944 assert_eq!(count, 2);
945
946 proc.start();
947 let pool = NDArrayPool::new(1_000_000);
948
949 let result = proc.process_array(&make_array(1), &pool);
950 let attrs = &result.output_arrays[0].attributes;
951 assert!((attrs.get("x").unwrap().value.as_f64().unwrap() - 10.0).abs() < 1e-10);
952 assert!((attrs.get("y").unwrap().value.as_f64().unwrap() - 100.0).abs() < 1e-10);
953
954 let result = proc.process_array(&make_array(2), &pool);
955 let attrs = &result.output_arrays[0].attributes;
956 assert!((attrs.get("x").unwrap().value.as_f64().unwrap() - 20.0).abs() < 1e-10);
957 assert!((attrs.get("y").unwrap().value.as_f64().unwrap() - 200.0).abs() < 1e-10);
958 }
959
960 #[test]
961 fn test_load_xml_rejects_incomplete_position() {
962 let mut proc = PosPluginProcessor::new(PosMode::Discard);
965 let xml = r#"<pos_layout>
966 <dimensions>
967 <dimension name="x"/>
968 <dimension name="y"/>
969 </dimensions>
970 <positions>
971 <position x="1" y="2"/>
972 <position x="3"/>
973 <position x="5" y="6"/>
974 </positions>
975</pos_layout>"#;
976 let count = proc.load_positions_xml(xml).unwrap();
977 assert_eq!(count, 2);
978 }
979
980 #[test]
981 fn test_load_auto_json() {
982 let mut proc = PosPluginProcessor::new(PosMode::Discard);
983 let json = r#"{"positions": [{"X": 1.5}]}"#;
984 let count = proc.load_positions_auto(json).unwrap();
985 assert_eq!(count, 1);
986 }
987
988 #[test]
989 fn test_load_auto_xml() {
990 let mut proc = PosPluginProcessor::new(PosMode::Discard);
991 let xml = r#"<pos_layout><dimensions><dimension name="x"/></dimensions><positions><position x="99.9"/></positions></pos_layout>"#;
992 let count = proc.load_positions_auto(xml).unwrap();
993 assert_eq!(count, 1);
994 }
995
996 #[test]
997 fn test_load_xml_empty() {
998 let mut proc = PosPluginProcessor::new(PosMode::Discard);
999 let xml = r#"<pos_layout><dimensions><dimension name="x"/></dimensions><positions></positions></pos_layout>"#;
1000 let count = proc.load_positions_xml(xml).unwrap();
1001 assert_eq!(count, 0);
1002 }
1003
1004 #[test]
1005 fn test_param_posts_to_registered_indices() {
1006 use ad_core_rs::plugin::runtime::ParamUpdate;
1009 let mut proc = PosPluginProcessor::new(PosMode::Discard);
1010 proc.params.missing_frames = Some(20);
1011 proc.params.duplicate_frames = Some(21);
1012 proc.params.current_qty = Some(22);
1013
1014 let mut p1 = HashMap::new();
1015 p1.insert("x".into(), 1.0);
1016 let mut p2 = HashMap::new();
1017 p2.insert("x".into(), 2.0);
1018 proc.load_positions(vec![p1, p2]);
1019 proc.start();
1020
1021 let pool = NDArrayPool::new(1_000_000);
1022 let result = proc.process_array(&make_array(1), &pool);
1023 assert!(
1025 result
1026 .param_updates
1027 .iter()
1028 .any(|u| matches!(u, ParamUpdate::Int32 { reason: 20, .. }))
1029 );
1030 assert!(result.param_updates.iter().any(|u| matches!(
1031 u,
1032 ParamUpdate::Int32 {
1033 reason: 22,
1034 value: 1,
1035 ..
1036 }
1037 )));
1038 assert!(
1039 !result
1040 .param_updates
1041 .iter()
1042 .any(|u| matches!(u, ParamUpdate::Int32 { reason: 0, .. }))
1043 );
1044 }
1045
1046 #[test]
1047 fn test_filename_param_loads_positions() {
1048 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1050 let mut proc = PosPluginProcessor::new(PosMode::Discard);
1051 proc.params.filename = Some(0);
1052 proc.params.file_valid = Some(1);
1053 proc.params.current_qty = Some(8);
1054
1055 let xml = r#"<pos_layout><dimensions><dimension name="x"/></dimensions><positions><position x="1"/><position x="2"/></positions></pos_layout>"#;
1056 let snapshot = PluginParamSnapshot {
1057 enable_callbacks: true,
1058 reason: 0,
1059 addr: 0,
1060 value: ParamChangeValue::Octet(xml.to_string()),
1061 };
1062 let result = proc.on_param_change(0, &snapshot);
1063 assert_eq!(proc.remaining_positions(), 2);
1064 assert!(result.param_updates.iter().any(|u| matches!(
1065 u,
1066 ParamUpdate::Int32 {
1067 reason: 1,
1068 value: 1,
1069 ..
1070 }
1071 )));
1072 assert!(result.param_updates.iter().any(|u| matches!(
1073 u,
1074 ParamUpdate::Int32 {
1075 reason: 8,
1076 value: 2,
1077 ..
1078 }
1079 )));
1080 }
1081
1082 #[test]
1083 fn test_running_param_starts_and_stops() {
1084 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
1086 let mut proc = PosPluginProcessor::new(PosMode::Discard);
1087 proc.params.running = Some(3);
1088
1089 let start = PluginParamSnapshot {
1090 enable_callbacks: true,
1091 reason: 3,
1092 addr: 0,
1093 value: ParamChangeValue::Int32(1),
1094 };
1095 proc.on_param_change(3, &start);
1096 assert!(proc.running);
1097
1098 let stop = PluginParamSnapshot {
1099 enable_callbacks: true,
1100 reason: 3,
1101 addr: 0,
1102 value: ParamChangeValue::Int32(0),
1103 };
1104 proc.on_param_change(3, &stop);
1105 assert!(!proc.running);
1106 }
1107
1108 #[test]
1109 fn test_current_pos_string_posted() {
1110 use ad_core_rs::plugin::runtime::ParamUpdate;
1113 let mut proc = PosPluginProcessor::new(PosMode::Discard);
1114 proc.params.current_pos = Some(30);
1115
1116 let mut p = HashMap::new();
1117 p.insert("y".into(), 2.0);
1118 p.insert("x".into(), 1.5);
1119 proc.load_positions(vec![p]);
1120 proc.start();
1121
1122 let pool = NDArrayPool::new(1_000_000);
1123 let result = proc.process_array(&make_array(1), &pool);
1124 let s = result.param_updates.iter().find_map(|u| match u {
1125 ParamUpdate::Octet {
1126 reason: 30, value, ..
1127 } => Some(value.clone()),
1128 _ => None,
1129 });
1130 assert_eq!(s.as_deref(), Some("[x=1.5,y=2]"));
1131 }
1132
1133 #[test]
1134 fn test_first_frame_id_checked() {
1135 let mut proc = PosPluginProcessor::new(PosMode::Discard);
1139 let mut p1 = HashMap::new();
1140 p1.insert("x".into(), 1.0);
1141 let mut p2 = HashMap::new();
1142 p2.insert("x".into(), 2.0);
1143 let mut p3 = HashMap::new();
1144 p3.insert("x".into(), 3.0);
1145 proc.load_positions(vec![p1, p2, p3]);
1146 proc.start();
1147
1148 let pool = NDArrayPool::new(1_000_000);
1149 let result = proc.process_array(&make_array(3), &pool);
1151 assert_eq!(proc.missing_frames(), 2);
1152 let x = result.output_arrays[0]
1153 .attributes
1154 .get("x")
1155 .unwrap()
1156 .value
1157 .as_f64()
1158 .unwrap();
1159 assert!((x - 3.0).abs() < 1e-10);
1160 }
1161
1162 #[test]
1163 fn test_id_difference_stepping() {
1164 let mut proc = PosPluginProcessor::new(PosMode::Discard);
1167 proc.id_difference = 2;
1168 let mut p1 = HashMap::new();
1169 p1.insert("x".into(), 1.0);
1170 let mut p2 = HashMap::new();
1171 p2.insert("x".into(), 2.0);
1172 let mut p3 = HashMap::new();
1173 p3.insert("x".into(), 3.0);
1174 proc.load_positions(vec![p1, p2, p3]);
1175 proc.start();
1176
1177 let pool = NDArrayPool::new(1_000_000);
1178 proc.process_array(&make_array(1), &pool);
1179 proc.process_array(&make_array(3), &pool);
1180 let r = proc.process_array(&make_array(5), &pool);
1181 assert_eq!(proc.missing_frames(), 0);
1182 let x = r.output_arrays[0]
1183 .attributes
1184 .get("x")
1185 .unwrap()
1186 .value
1187 .as_f64()
1188 .unwrap();
1189 assert!((x - 3.0).abs() < 1e-10);
1190 }
1191
1192 #[test]
1193 fn test_format_cpp_g6() {
1194 assert_eq!(format_cpp_g6(1.0), "1");
1196 assert_eq!(format_cpp_g6(1.5), "1.5");
1197 assert_eq!(format_cpp_g6(42.5), "42.5");
1198 assert_eq!(format_cpp_g6(100.0), "100");
1199 assert_eq!(format_cpp_g6(0.1), "0.1");
1200 assert_eq!(format_cpp_g6(0.0001), "0.0001");
1201 assert_eq!(format_cpp_g6(0.00001), "1e-05");
1202 assert_eq!(format_cpp_g6(1_000_000.0), "1e+06");
1203 assert_eq!(format_cpp_g6(1_234_567.0), "1.23457e+06");
1204 assert_eq!(format_cpp_g6(123456.0), "123456");
1205 assert_eq!(format_cpp_g6(-1.5), "-1.5");
1206 assert_eq!(format_cpp_g6(0.0), "0");
1207 }
1208}