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 parking_lot::Mutex;
9use serde::Deserialize;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PosMode {
14 Discard,
15 Keep,
16}
17
18#[derive(Debug, Deserialize)]
20pub struct PositionList {
21 pub positions: Vec<HashMap<String, f64>>,
22}
23
24#[derive(Default)]
28struct PosParamIndices {
29 filename: Option<usize>,
30 file_valid: Option<usize>,
31 clear: Option<usize>,
32 running: Option<usize>,
33 restart: Option<usize>,
34 delete: Option<usize>,
35 mode: Option<usize>,
36 append: Option<usize>,
37 current_qty: Option<usize>,
38 current_index: Option<usize>,
39 current_pos: Option<usize>,
40 missing_frames: Option<usize>,
41 duplicate_frames: Option<usize>,
42 expected_id: Option<usize>,
43 id_name: Option<usize>,
44 id_difference: Option<usize>,
45 id_start: Option<usize>,
46}
47
48pub struct PosPluginProcessor {
50 state: Mutex<PosPluginState>,
51}
52
53struct PosPluginState {
56 positions: VecDeque<HashMap<String, f64>>,
57 all_positions: Vec<HashMap<String, f64>>,
58 mode: PosMode,
59 index: usize,
60 running: bool,
61 expected_id: i32,
62 id_start: i32,
65 id_difference: i32,
68 missing_frames: usize,
69 duplicate_frames: usize,
70 params: PosParamIndices,
71}
72
73impl PosPluginProcessor {
74 pub fn new(mode: PosMode) -> Self {
75 Self {
76 state: Mutex::new(PosPluginState::new(mode)),
77 }
78 }
79
80 pub fn load_positions_json(&self, json_str: &str) -> Result<usize, serde_json::Error> {
82 self.state.lock().load_positions_json(json_str)
83 }
84
85 pub fn load_positions_xml(&self, xml_str: &str) -> Result<usize, String> {
87 self.state.lock().load_positions_xml(xml_str)
88 }
89
90 pub fn load_positions_auto(&self, content: &str) -> Result<usize, String> {
92 self.state.lock().load_positions_auto(content)
93 }
94
95 pub fn load_positions(&self, positions: Vec<HashMap<String, f64>>) {
97 self.state.lock().load_positions(positions);
98 }
99
100 pub fn start(&self) {
102 self.state.lock().start();
103 }
104
105 pub fn stop(&self) {
107 self.state.lock().stop();
108 }
109
110 pub fn clear(&self) {
112 self.state.lock().clear();
113 }
114
115 pub fn missing_frames(&self) -> usize {
116 self.state.lock().missing_frames
117 }
118
119 pub fn duplicate_frames(&self) -> usize {
120 self.state.lock().duplicate_frames
121 }
122
123 pub fn remaining_positions(&self) -> usize {
124 self.state.lock().remaining_positions()
125 }
126}
127
128impl PosPluginState {
129 fn new(mode: PosMode) -> Self {
130 Self {
131 positions: VecDeque::new(),
132 all_positions: Vec::new(),
133 mode,
134 index: 0,
135 running: false,
136 expected_id: 0,
137 id_start: 1,
138 id_difference: 1,
139 missing_frames: 0,
140 duplicate_frames: 0,
141 params: PosParamIndices::default(),
142 }
143 }
144
145 fn load_positions_json(&mut self, json_str: &str) -> Result<usize, serde_json::Error> {
147 let list: PositionList = serde_json::from_str(json_str)?;
148 let count = list.positions.len();
149 self.all_positions = list.positions.clone();
150 self.positions = list.positions.into();
151 self.index = 0;
152 Ok(count)
153 }
154
155 fn load_positions_xml(&mut self, xml_str: &str) -> Result<usize, String> {
177 let positions = parse_positions_xml(xml_str)?;
178 let count = positions.len();
179 self.all_positions = positions.clone();
180 self.positions = positions.into();
181 self.index = 0;
182 Ok(count)
183 }
184
185 fn load_positions_auto(&mut self, content: &str) -> Result<usize, String> {
190 if content.trim_start().starts_with('<') {
191 self.load_positions_xml(content)
192 } else {
193 self.load_positions_json(content)
194 .map_err(|e| format!("JSON parse error: {}", e))
195 }
196 }
197
198 fn load_positions(&mut self, positions: Vec<HashMap<String, f64>>) {
200 self.all_positions = positions.clone();
201 self.positions = positions.into();
202 self.index = 0;
203 }
204
205 fn start(&mut self) {
212 self.running = true;
213 self.expected_id = self.id_start;
214 }
215
216 fn stop(&mut self) {
218 self.running = false;
219 }
220
221 fn clear(&mut self) {
223 self.positions.clear();
224 self.all_positions.clear();
225 self.index = 0;
226 }
227
228 fn remaining_positions(&self) -> usize {
229 match self.mode {
230 PosMode::Discard => self.positions.len(),
231 PosMode::Keep => self.all_positions.len(),
232 }
233 }
234
235 fn current_position(&self) -> Option<&HashMap<String, f64>> {
236 match self.mode {
237 PosMode::Discard => self.positions.front(),
238 PosMode::Keep => {
239 if self.index < self.all_positions.len() {
240 Some(&self.all_positions[self.index])
241 } else {
242 None
243 }
244 }
245 }
246 }
247
248 fn has_position(&self) -> bool {
252 match self.mode {
253 PosMode::Discard => !self.positions.is_empty(),
254 PosMode::Keep => self.index < self.all_positions.len(),
255 }
256 }
257
258 fn advance(&mut self) {
259 match self.mode {
260 PosMode::Discard => {
261 self.positions.pop_front();
262 }
263 PosMode::Keep => {
264 self.index += 1;
265 }
266 }
267 }
268
269 fn current_index_param(&self) -> i32 {
273 match self.mode {
274 PosMode::Discard => 0,
275 PosMode::Keep => self.index as i32,
276 }
277 }
278
279 fn exhausted_result(&mut self) -> ProcessResult {
283 self.running = false;
284 let mut updates = Vec::new();
285 push_int(&mut updates, self.params.running, 0);
286 ProcessResult {
287 output_arrays: vec![],
288 param_updates: updates,
289 scatter: false,
290 }
291 }
292}
293
294fn push_int(updates: &mut Vec<ParamUpdate>, idx: Option<usize>, value: i32) {
296 if let Some(i) = idx {
297 updates.push(ParamUpdate::int32(i, value));
298 }
299}
300
301fn push_str(updates: &mut Vec<ParamUpdate>, idx: Option<usize>, value: String) {
303 if let Some(i) = idx {
304 updates.push(ParamUpdate::octet(i, value));
305 }
306}
307
308fn format_cpp_g6(v: f64) -> String {
314 const PREC: i32 = 6;
315 if v == 0.0 {
316 return if v.is_sign_negative() { "-0" } else { "0" }.to_string();
317 }
318 if v.is_nan() {
319 return "nan".to_string();
320 }
321 if v.is_infinite() {
322 return if v < 0.0 { "-inf" } else { "inf" }.to_string();
323 }
324 let sci = format!("{:.*e}", (PREC - 1) as usize, v);
327 let epos = sci.find('e').unwrap();
328 let exp: i32 = sci[epos + 1..].parse().unwrap();
329 if exp >= -4 && exp < PREC {
330 let prec = (PREC - 1 - exp).max(0) as usize;
332 strip_g_trailing(&format!("{:.*}", prec, v))
333 } else {
334 let mantissa = strip_g_trailing(&sci[..epos]);
337 let sign = if exp < 0 { '-' } else { '+' };
338 format!("{}e{}{:02}", mantissa, sign, exp.abs())
339 }
340}
341
342fn strip_g_trailing(s: &str) -> String {
345 if s.contains('.') {
346 s.trim_end_matches('0').trim_end_matches('.').to_string()
347 } else {
348 s.to_string()
349 }
350}
351
352fn parse_positions_xml(xml: &str) -> Result<Vec<HashMap<String, f64>>, String> {
364 let dimensions: Vec<String> = element_tag_contents(xml, "dimension")
366 .into_iter()
367 .filter_map(|content| parse_tag_attributes(content).remove("name"))
368 .collect();
369
370 let mut positions: Vec<HashMap<String, f64>> = Vec::new();
371 for content in element_tag_contents(xml, "position") {
372 let attrs = parse_tag_attributes(content);
373 if attrs.is_empty() {
375 continue;
376 }
377 let mut pos = HashMap::new();
378 let mut ok = true;
379 for dim in &dimensions {
380 match attrs.get(dim).and_then(|v| v.parse::<f64>().ok()) {
381 Some(value) => {
382 pos.insert(dim.clone(), value);
383 }
384 None => {
385 ok = false;
387 break;
388 }
389 }
390 }
391 if ok {
392 positions.push(pos);
393 }
394 }
395
396 Ok(positions)
397}
398
399fn is_tag_boundary(c: char) -> bool {
404 c.is_ascii_whitespace() || c == '>' || c == '/'
405}
406
407fn element_tag_contents<'a>(xml: &'a str, name: &str) -> Vec<&'a str> {
411 let prefix = format!("<{}", name);
412 let mut out = Vec::new();
413 let mut from = 0;
414 while let Some(rel) = xml[from..].find(&prefix) {
415 let open = from + rel;
416 let after = open + prefix.len();
417 match xml[after..].chars().next() {
418 Some(c) if is_tag_boundary(c) => {}
419 _ => {
420 from = after;
422 continue;
423 }
424 }
425 let Some(rel_end) = xml[after..].find('>') else {
426 break;
427 };
428 let end = after + rel_end;
429 out.push(&xml[after..end]);
430 from = end + 1;
431 }
432 out
433}
434
435fn parse_tag_attributes(content: &str) -> HashMap<String, String> {
438 let mut attrs = HashMap::new();
439 let bytes = content.as_bytes();
440 let mut i = 0;
441 while let Some(eq_rel) = content[i..].find('=') {
442 let eq = i + eq_rel;
443 let mut k_end = eq;
445 while k_end > i && bytes[k_end - 1].is_ascii_whitespace() {
446 k_end -= 1;
447 }
448 let mut k_start = k_end;
449 while k_start > i
450 && !bytes[k_start - 1].is_ascii_whitespace()
451 && bytes[k_start - 1] != b'/'
452 && bytes[k_start - 1] != b'='
453 {
454 k_start -= 1;
455 }
456 let key = &content[k_start..k_end];
457 let mut j = eq + 1;
459 while j < bytes.len() && bytes[j].is_ascii_whitespace() {
460 j += 1;
461 }
462 if j >= bytes.len() {
463 break;
464 }
465 let quote = bytes[j];
466 if quote != b'"' && quote != b'\'' {
467 i = eq + 1;
468 continue;
469 }
470 j += 1;
471 let val_start = j;
472 while j < bytes.len() && bytes[j] != quote {
473 j += 1;
474 }
475 if j >= bytes.len() {
476 break; }
478 if !key.is_empty() {
479 attrs.insert(key.to_string(), content[val_start..j].to_string());
480 }
481 i = j + 1;
482 }
483 attrs
484}
485
486impl NDPluginProcess for PosPluginProcessor {
487 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
488 let mut state = self.state.lock();
489 if !state.running {
490 return ProcessResult::empty();
494 }
495
496 if !state.has_position() {
500 return state.exhausted_result();
501 }
502
503 let uid = array.unique_id;
508 if uid > state.expected_id {
509 while state.expected_id < uid && state.has_position() {
513 state.advance();
514 state.expected_id += state.id_difference;
515 state.missing_frames += 1;
516 }
517 if !state.has_position() {
518 return state.exhausted_result();
521 }
522 } else if uid < state.expected_id {
523 state.duplicate_frames += 1;
524 let mut updates = Vec::new();
527 push_int(
528 &mut updates,
529 state.params.duplicate_frames,
530 state.duplicate_frames as i32,
531 );
532 return ProcessResult {
533 output_arrays: vec![],
534 param_updates: updates,
535 scatter: false,
536 };
537 }
538
539 let position = state.current_position().unwrap().clone();
541
542 let mut out = array.clone();
543 let mut keys: Vec<&String> = position.keys().collect();
548 keys.sort();
549 let mut current_pos = String::from("[");
550 for (n, key) in keys.iter().enumerate() {
551 let value = position[*key];
552 if n > 0 {
553 current_pos.push(',');
554 }
555 current_pos.push_str(key);
556 current_pos.push('=');
557 current_pos.push_str(&format_cpp_g6(value));
558 out.attributes.add(NDAttribute::new_static(
559 (*key).clone(),
560 "Position of NDArray",
561 NDAttrSource::Driver,
562 NDAttrValue::Float64(value),
563 ));
564 }
565 current_pos.push(']');
566
567 state.advance();
568 state.expected_id += state.id_difference;
571
572 let mut updates = Vec::new();
576 push_int(
577 &mut updates,
578 state.params.missing_frames,
579 state.missing_frames as i32,
580 );
581 push_int(
582 &mut updates,
583 state.params.duplicate_frames,
584 state.duplicate_frames as i32,
585 );
586 push_int(
587 &mut updates,
588 state.params.current_qty,
589 state.remaining_positions() as i32,
590 );
591 push_int(
592 &mut updates,
593 state.params.current_index,
594 state.current_index_param(),
595 );
596 push_str(&mut updates, state.params.current_pos, current_pos);
598
599 ProcessResult {
600 output_arrays: vec![Arc::new(out)],
601 param_updates: updates,
602 scatter: false,
603 }
604 }
605
606 fn plugin_type(&self) -> &str {
607 "NDPositionPlugin"
610 }
611
612 fn register_params(
613 &mut self,
614 base: &mut asyn_rs::port::PortDriverBase,
615 ) -> asyn_rs::error::AsynResult<()> {
616 use asyn_rs::param::ParamType;
617 let state = self.state.get_mut();
618 base.create_param("NDPos_Filename", ParamType::Octet)?;
620 base.create_param("NDPos_FileValid", ParamType::Int32)?;
621 base.create_param("NDPos_Clear", ParamType::Int32)?;
622 base.create_param("NDPos_Running", ParamType::Int32)?;
623 base.create_param("NDPos_Restart", ParamType::Int32)?;
624 base.create_param("NDPos_Delete", ParamType::Int32)?;
625 base.create_param("NDPos_Mode", ParamType::Int32)?;
626 base.create_param("NDPos_Append", ParamType::Int32)?;
627 base.create_param("NDPos_CurrentQty", ParamType::Int32)?;
628 base.create_param("NDPos_CurrentIndex", ParamType::Int32)?;
629 base.create_param("NDPos_CurrentPos", ParamType::Octet)?;
630 base.create_param("NDPos_MissingFrames", ParamType::Int32)?;
631 base.create_param("NDPos_DuplicateFrames", ParamType::Int32)?;
632 base.create_param("NDPos_ExpectedID", ParamType::Int32)?;
633 base.create_param("NDPos_IDName", ParamType::Octet)?;
634 base.create_param("NDPos_IDDifference", ParamType::Int32)?;
635 base.create_param("NDPos_IDStart", ParamType::Int32)?;
636
637 state.params.filename = base.find_param("NDPos_Filename");
638 state.params.file_valid = base.find_param("NDPos_FileValid");
639 state.params.clear = base.find_param("NDPos_Clear");
640 state.params.running = base.find_param("NDPos_Running");
641 state.params.restart = base.find_param("NDPos_Restart");
642 state.params.delete = base.find_param("NDPos_Delete");
643 state.params.mode = base.find_param("NDPos_Mode");
644 state.params.append = base.find_param("NDPos_Append");
645 state.params.current_qty = base.find_param("NDPos_CurrentQty");
646 state.params.current_index = base.find_param("NDPos_CurrentIndex");
647 state.params.current_pos = base.find_param("NDPos_CurrentPos");
648 state.params.missing_frames = base.find_param("NDPos_MissingFrames");
649 state.params.duplicate_frames = base.find_param("NDPos_DuplicateFrames");
650 state.params.expected_id = base.find_param("NDPos_ExpectedID");
651 state.params.id_name = base.find_param("NDPos_IDName");
652 state.params.id_difference = base.find_param("NDPos_IDDifference");
653 state.params.id_start = base.find_param("NDPos_IDStart");
654
655 if let Some(i) = state.params.mode {
657 base.set_int32_param(i, 0, state.mode as i32)?;
658 }
659 if let Some(i) = state.params.file_valid {
660 base.set_int32_param(i, 0, 0)?;
661 }
662 if let Some(i) = state.params.current_index {
663 base.set_int32_param(i, 0, 0)?;
664 }
665 if let Some(i) = state.params.current_qty {
666 base.set_int32_param(i, 0, state.remaining_positions() as i32)?;
667 }
668 if let Some(i) = state.params.current_pos {
669 base.set_string_param(i, 0, String::new())?;
670 }
671 if let Some(i) = state.params.running {
672 base.set_int32_param(i, 0, 0)?;
673 }
674 if let Some(i) = state.params.id_name {
675 base.set_string_param(i, 0, String::new())?;
676 }
677 if let Some(i) = state.params.id_difference {
678 base.set_int32_param(i, 0, 1)?;
679 }
680 if let Some(i) = state.params.id_start {
681 base.set_int32_param(i, 0, 1)?;
682 }
683 if let Some(i) = state.params.expected_id {
684 base.set_int32_param(i, 0, 1)?;
685 }
686 if let Some(i) = state.params.missing_frames {
687 base.set_int32_param(i, 0, 0)?;
688 }
689 if let Some(i) = state.params.duplicate_frames {
690 base.set_int32_param(i, 0, 0)?;
691 }
692 Ok(())
693 }
694
695 fn on_param_change(
696 &self,
697 reason: usize,
698 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
699 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
700 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue};
701
702 let mut state = self.state.lock();
703 let mut updates = Vec::new();
704 if Some(reason) == state.params.running {
705 if params.value.as_i32() == 0 {
708 state.stop();
709 } else {
710 state.start();
711 push_int(&mut updates, state.params.expected_id, state.id_start);
712 }
713 } else if Some(reason) == state.params.id_start {
714 state.id_start = params.value.as_i32();
716 } else if Some(reason) == state.params.id_difference {
717 state.id_difference = params.value.as_i32();
719 } else if Some(reason) == state.params.mode {
720 state.mode = match params.value.as_i32() {
722 1 => PosMode::Keep,
723 _ => PosMode::Discard,
724 };
725 state.index = 0;
726 push_int(&mut updates, state.params.current_index, 0);
727 } else if Some(reason) == state.params.restart {
728 state.index = 0;
731 push_int(&mut updates, state.params.current_index, 0);
732 push_str(&mut updates, state.params.current_pos, String::new());
733 } else if Some(reason) == state.params.delete {
734 state.clear();
737 push_int(&mut updates, state.params.current_index, 0);
738 push_int(&mut updates, state.params.current_qty, 0);
739 push_str(&mut updates, state.params.current_pos, String::new());
740 } else if Some(reason) == state.params.filename {
741 if let ParamChangeValue::Octet(ref xml) = params.value {
744 match state.load_positions_auto(xml) {
745 Ok(_) => {
746 push_int(&mut updates, state.params.file_valid, 1);
747 push_int(
748 &mut updates,
749 state.params.current_qty,
750 state.remaining_positions() as i32,
751 );
752 }
753 Err(_) => {
754 push_int(&mut updates, state.params.file_valid, 0);
755 }
756 }
757 }
758 }
759 ParamChangeResult::updates(updates)
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use ad_core_rs::ndarray::{NDDataType, NDDimension};
767
768 fn make_array(id: i32) -> NDArray {
769 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
770 arr.unique_id = id;
771 arr
772 }
773
774 #[test]
775 fn test_discard_mode() {
776 let proc = PosPluginProcessor::new(PosMode::Discard);
777 let mut pos1 = HashMap::new();
778 pos1.insert("X".into(), 1.5);
779 pos1.insert("Y".into(), 2.3);
780 let mut pos2 = HashMap::new();
781 pos2.insert("X".into(), 3.1);
782 pos2.insert("Y".into(), 4.2);
783
784 proc.load_positions(vec![pos1, pos2]);
785 proc.start();
786
787 let pool = NDArrayPool::new(1_000_000);
788
789 let result = proc.process_array(&make_array(1), &pool);
790 assert_eq!(result.output_arrays.len(), 1);
791 let x = result.output_arrays[0]
792 .attributes
793 .get("X")
794 .unwrap()
795 .value
796 .as_f64()
797 .unwrap();
798 assert!((x - 1.5).abs() < 1e-10);
799
800 let result = proc.process_array(&make_array(2), &pool);
801 let x = result.output_arrays[0]
802 .attributes
803 .get("X")
804 .unwrap()
805 .value
806 .as_f64()
807 .unwrap();
808 assert!((x - 3.1).abs() < 1e-10);
809
810 assert_eq!(proc.remaining_positions(), 0);
811 }
812
813 #[test]
814 fn test_attribute_description() {
815 let proc = PosPluginProcessor::new(PosMode::Discard);
817 let mut pos = HashMap::new();
818 pos.insert("X".into(), 1.5);
819 proc.load_positions(vec![pos]);
820 proc.start();
821
822 let pool = NDArrayPool::new(1_000_000);
823 let result = proc.process_array(&make_array(1), &pool);
824 let attr = result.output_arrays[0].attributes.get("X").unwrap();
825 assert_eq!(attr.description, "Position of NDArray");
826 }
827
828 #[test]
829 fn test_keep_mode() {
830 let proc = PosPluginProcessor::new(PosMode::Keep);
831 let mut pos1 = HashMap::new();
832 pos1.insert("X".into(), 10.0);
833 let mut pos2 = HashMap::new();
834 pos2.insert("X".into(), 20.0);
835
836 proc.load_positions(vec![pos1, pos2]);
837 proc.start();
838
839 let pool = NDArrayPool::new(1_000_000);
840
841 let result = proc.process_array(&make_array(1), &pool);
842 let x = result.output_arrays[0]
843 .attributes
844 .get("X")
845 .unwrap()
846 .value
847 .as_f64()
848 .unwrap();
849 assert!((x - 10.0).abs() < 1e-10);
850
851 let result = proc.process_array(&make_array(2), &pool);
852 let x = result.output_arrays[0]
853 .attributes
854 .get("X")
855 .unwrap()
856 .value
857 .as_f64()
858 .unwrap();
859 assert!((x - 20.0).abs() < 1e-10);
860
861 let result = proc.process_array(&make_array(3), &pool);
864 assert!(result.output_arrays.is_empty());
865 assert!(!proc.state.lock().running);
866 }
867
868 #[test]
869 fn test_exhaustion_stops_and_drops() {
870 let proc = PosPluginProcessor::new(PosMode::Discard);
873 proc.state.lock().params.running = Some(5);
874 let mut p1 = HashMap::new();
875 p1.insert("x".into(), 1.0);
876 proc.load_positions(vec![p1]);
877 proc.start();
878
879 let pool = NDArrayPool::new(1_000_000);
880 let r1 = proc.process_array(&make_array(1), &pool);
882 assert_eq!(r1.output_arrays.len(), 1);
883 let r2 = proc.process_array(&make_array(2), &pool);
885 assert!(r2.output_arrays.is_empty());
886 assert!(!proc.state.lock().running);
887 use ad_core_rs::plugin::runtime::ParamUpdate;
888 assert!(r2.param_updates.iter().any(|u| matches!(
889 u,
890 ParamUpdate::Int32 {
891 reason: 5,
892 value: 0,
893 ..
894 }
895 )));
896 }
897
898 #[test]
899 fn test_missing_frames() {
900 let proc = PosPluginProcessor::new(PosMode::Discard);
901 let mut pos1 = HashMap::new();
902 pos1.insert("X".into(), 1.0);
903 let mut pos2 = HashMap::new();
904 pos2.insert("X".into(), 2.0);
905 let mut pos3 = HashMap::new();
906 pos3.insert("X".into(), 3.0);
907
908 proc.load_positions(vec![pos1, pos2, pos3]);
909 proc.start();
910
911 let pool = NDArrayPool::new(1_000_000);
912
913 proc.process_array(&make_array(1), &pool);
914
915 let result = proc.process_array(&make_array(3), &pool);
917 assert_eq!(proc.missing_frames(), 1);
918 let x = result.output_arrays[0]
919 .attributes
920 .get("X")
921 .unwrap()
922 .value
923 .as_f64()
924 .unwrap();
925 assert!((x - 3.0).abs() < 1e-10);
926 }
927
928 #[test]
929 fn test_duplicate_frames() {
930 let proc = PosPluginProcessor::new(PosMode::Discard);
931 let mut pos1 = HashMap::new();
932 pos1.insert("X".into(), 1.0);
933 let mut pos2 = HashMap::new();
934 pos2.insert("X".into(), 2.0);
935
936 proc.load_positions(vec![pos1, pos2]);
937 proc.start();
938
939 let pool = NDArrayPool::new(1_000_000);
940
941 proc.process_array(&make_array(1), &pool);
942
943 let result = proc.process_array(&make_array(1), &pool);
944 assert_eq!(proc.duplicate_frames(), 1);
945 assert!(result.output_arrays.is_empty());
946 }
947
948 #[test]
949 fn test_load_json() {
950 let proc = PosPluginProcessor::new(PosMode::Discard);
951 let json = r#"{"positions": [{"X": 1.5, "Y": 2.3}, {"X": 3.1, "Y": 4.2}]}"#;
952 let count = proc.load_positions_json(json).unwrap();
953 assert_eq!(count, 2);
954 assert_eq!(proc.remaining_positions(), 2);
955 }
956
957 #[test]
958 fn test_idle_drops_frame() {
959 let proc = PosPluginProcessor::new(PosMode::Discard);
962 let pool = NDArrayPool::new(1_000_000);
963 let result = proc.process_array(&make_array(1), &pool);
964 assert!(result.output_arrays.is_empty());
965 }
966
967 #[test]
968 fn test_load_xml() {
969 let proc = PosPluginProcessor::new(PosMode::Discard);
970 let xml = r#"<pos_layout>
971 <dimensions>
972 <dimension name="x"/>
973 </dimensions>
974 <positions>
975 <position x="1.5"/>
976 <position x="2.3"/>
977 <position x="3.7"/>
978 </positions>
979</pos_layout>"#;
980 let count = proc.load_positions_xml(xml).unwrap();
981 assert_eq!(count, 3);
982 assert_eq!(proc.remaining_positions(), 3);
983 }
984
985 #[test]
986 fn test_load_xml_dimension_keyed() {
987 let proc = PosPluginProcessor::new(PosMode::Discard);
990 let xml = r#"<pos_layout>
991 <dimensions>
992 <dimension name="x"/>
993 <dimension name="y"/>
994 </dimensions>
995 <positions>
996 <position x="10" y="100"/>
997 <position x="20" y="200"/>
998 </positions>
999</pos_layout>"#;
1000 let count = proc.load_positions_xml(xml).unwrap();
1001 assert_eq!(count, 2);
1002
1003 proc.start();
1004 let pool = NDArrayPool::new(1_000_000);
1005
1006 let result = proc.process_array(&make_array(1), &pool);
1007 let attrs = &result.output_arrays[0].attributes;
1008 assert!((attrs.get("x").unwrap().value.as_f64().unwrap() - 10.0).abs() < 1e-10);
1009 assert!((attrs.get("y").unwrap().value.as_f64().unwrap() - 100.0).abs() < 1e-10);
1010
1011 let result = proc.process_array(&make_array(2), &pool);
1012 let attrs = &result.output_arrays[0].attributes;
1013 assert!((attrs.get("x").unwrap().value.as_f64().unwrap() - 20.0).abs() < 1e-10);
1014 assert!((attrs.get("y").unwrap().value.as_f64().unwrap() - 200.0).abs() < 1e-10);
1015 }
1016
1017 #[test]
1018 fn test_load_xml_rejects_incomplete_position() {
1019 let proc = PosPluginProcessor::new(PosMode::Discard);
1022 let xml = r#"<pos_layout>
1023 <dimensions>
1024 <dimension name="x"/>
1025 <dimension name="y"/>
1026 </dimensions>
1027 <positions>
1028 <position x="1" y="2"/>
1029 <position x="3"/>
1030 <position x="5" y="6"/>
1031 </positions>
1032</pos_layout>"#;
1033 let count = proc.load_positions_xml(xml).unwrap();
1034 assert_eq!(count, 2);
1035 }
1036
1037 #[test]
1038 fn test_load_auto_json() {
1039 let proc = PosPluginProcessor::new(PosMode::Discard);
1040 let json = r#"{"positions": [{"X": 1.5}]}"#;
1041 let count = proc.load_positions_auto(json).unwrap();
1042 assert_eq!(count, 1);
1043 }
1044
1045 #[test]
1046 fn test_load_auto_xml() {
1047 let proc = PosPluginProcessor::new(PosMode::Discard);
1048 let xml = r#"<pos_layout><dimensions><dimension name="x"/></dimensions><positions><position x="99.9"/></positions></pos_layout>"#;
1049 let count = proc.load_positions_auto(xml).unwrap();
1050 assert_eq!(count, 1);
1051 }
1052
1053 #[test]
1054 fn test_load_xml_empty() {
1055 let proc = PosPluginProcessor::new(PosMode::Discard);
1056 let xml = r#"<pos_layout><dimensions><dimension name="x"/></dimensions><positions></positions></pos_layout>"#;
1057 let count = proc.load_positions_xml(xml).unwrap();
1058 assert_eq!(count, 0);
1059 }
1060
1061 #[test]
1062 fn test_param_posts_to_registered_indices() {
1063 use ad_core_rs::plugin::runtime::ParamUpdate;
1066 let proc = PosPluginProcessor::new(PosMode::Discard);
1067 proc.state.lock().params.missing_frames = Some(20);
1068 proc.state.lock().params.duplicate_frames = Some(21);
1069 proc.state.lock().params.current_qty = Some(22);
1070
1071 let mut p1 = HashMap::new();
1072 p1.insert("x".into(), 1.0);
1073 let mut p2 = HashMap::new();
1074 p2.insert("x".into(), 2.0);
1075 proc.load_positions(vec![p1, p2]);
1076 proc.start();
1077
1078 let pool = NDArrayPool::new(1_000_000);
1079 let result = proc.process_array(&make_array(1), &pool);
1080 assert!(
1082 result
1083 .param_updates
1084 .iter()
1085 .any(|u| matches!(u, ParamUpdate::Int32 { reason: 20, .. }))
1086 );
1087 assert!(result.param_updates.iter().any(|u| matches!(
1088 u,
1089 ParamUpdate::Int32 {
1090 reason: 22,
1091 value: 1,
1092 ..
1093 }
1094 )));
1095 assert!(
1096 !result
1097 .param_updates
1098 .iter()
1099 .any(|u| matches!(u, ParamUpdate::Int32 { reason: 0, .. }))
1100 );
1101 }
1102
1103 #[test]
1104 fn test_filename_param_loads_positions() {
1105 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1107 let proc = PosPluginProcessor::new(PosMode::Discard);
1108 proc.state.lock().params.filename = Some(0);
1109 proc.state.lock().params.file_valid = Some(1);
1110 proc.state.lock().params.current_qty = Some(8);
1111
1112 let xml = r#"<pos_layout><dimensions><dimension name="x"/></dimensions><positions><position x="1"/><position x="2"/></positions></pos_layout>"#;
1113 let snapshot = PluginParamSnapshot {
1114 enable_callbacks: true,
1115 reason: 0,
1116 addr: 0,
1117 value: ParamChangeValue::Octet(xml.to_string()),
1118 };
1119 let result = proc.on_param_change(0, &snapshot);
1120 assert_eq!(proc.remaining_positions(), 2);
1121 assert!(result.param_updates.iter().any(|u| matches!(
1122 u,
1123 ParamUpdate::Int32 {
1124 reason: 1,
1125 value: 1,
1126 ..
1127 }
1128 )));
1129 assert!(result.param_updates.iter().any(|u| matches!(
1130 u,
1131 ParamUpdate::Int32 {
1132 reason: 8,
1133 value: 2,
1134 ..
1135 }
1136 )));
1137 }
1138
1139 #[test]
1140 fn test_running_param_starts_and_stops() {
1141 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
1143 let proc = PosPluginProcessor::new(PosMode::Discard);
1144 proc.state.lock().params.running = Some(3);
1145
1146 let start = PluginParamSnapshot {
1147 enable_callbacks: true,
1148 reason: 3,
1149 addr: 0,
1150 value: ParamChangeValue::Int32(1),
1151 };
1152 proc.on_param_change(3, &start);
1153 assert!(proc.state.lock().running);
1154
1155 let stop = PluginParamSnapshot {
1156 enable_callbacks: true,
1157 reason: 3,
1158 addr: 0,
1159 value: ParamChangeValue::Int32(0),
1160 };
1161 proc.on_param_change(3, &stop);
1162 assert!(!proc.state.lock().running);
1163 }
1164
1165 #[test]
1166 fn test_current_pos_string_posted() {
1167 use ad_core_rs::plugin::runtime::ParamUpdate;
1170 let proc = PosPluginProcessor::new(PosMode::Discard);
1171 proc.state.lock().params.current_pos = Some(30);
1172
1173 let mut p = HashMap::new();
1174 p.insert("y".into(), 2.0);
1175 p.insert("x".into(), 1.5);
1176 proc.load_positions(vec![p]);
1177 proc.start();
1178
1179 let pool = NDArrayPool::new(1_000_000);
1180 let result = proc.process_array(&make_array(1), &pool);
1181 let s = result.param_updates.iter().find_map(|u| match u {
1182 ParamUpdate::Octet {
1183 reason: 30, value, ..
1184 } => Some(value.clone()),
1185 _ => None,
1186 });
1187 assert_eq!(s.as_deref(), Some("[x=1.5,y=2]"));
1188 }
1189
1190 #[test]
1191 fn test_first_frame_id_checked() {
1192 let proc = PosPluginProcessor::new(PosMode::Discard);
1196 let mut p1 = HashMap::new();
1197 p1.insert("x".into(), 1.0);
1198 let mut p2 = HashMap::new();
1199 p2.insert("x".into(), 2.0);
1200 let mut p3 = HashMap::new();
1201 p3.insert("x".into(), 3.0);
1202 proc.load_positions(vec![p1, p2, p3]);
1203 proc.start();
1204
1205 let pool = NDArrayPool::new(1_000_000);
1206 let result = proc.process_array(&make_array(3), &pool);
1208 assert_eq!(proc.missing_frames(), 2);
1209 let x = result.output_arrays[0]
1210 .attributes
1211 .get("x")
1212 .unwrap()
1213 .value
1214 .as_f64()
1215 .unwrap();
1216 assert!((x - 3.0).abs() < 1e-10);
1217 }
1218
1219 #[test]
1220 fn test_id_difference_stepping() {
1221 let proc = PosPluginProcessor::new(PosMode::Discard);
1224 proc.state.lock().id_difference = 2;
1225 let mut p1 = HashMap::new();
1226 p1.insert("x".into(), 1.0);
1227 let mut p2 = HashMap::new();
1228 p2.insert("x".into(), 2.0);
1229 let mut p3 = HashMap::new();
1230 p3.insert("x".into(), 3.0);
1231 proc.load_positions(vec![p1, p2, p3]);
1232 proc.start();
1233
1234 let pool = NDArrayPool::new(1_000_000);
1235 proc.process_array(&make_array(1), &pool);
1236 proc.process_array(&make_array(3), &pool);
1237 let r = proc.process_array(&make_array(5), &pool);
1238 assert_eq!(proc.missing_frames(), 0);
1239 let x = r.output_arrays[0]
1240 .attributes
1241 .get("x")
1242 .unwrap()
1243 .value
1244 .as_f64()
1245 .unwrap();
1246 assert!((x - 3.0).abs() < 1e-10);
1247 }
1248
1249 #[test]
1250 fn test_format_cpp_g6() {
1251 assert_eq!(format_cpp_g6(1.0), "1");
1253 assert_eq!(format_cpp_g6(1.5), "1.5");
1254 assert_eq!(format_cpp_g6(42.5), "42.5");
1255 assert_eq!(format_cpp_g6(100.0), "100");
1256 assert_eq!(format_cpp_g6(0.1), "0.1");
1257 assert_eq!(format_cpp_g6(0.0001), "0.0001");
1258 assert_eq!(format_cpp_g6(0.00001), "1e-05");
1259 assert_eq!(format_cpp_g6(1_000_000.0), "1e+06");
1260 assert_eq!(format_cpp_g6(1_234_567.0), "1.23457e+06");
1261 assert_eq!(format_cpp_g6(123456.0), "123456");
1262 assert_eq!(format_cpp_g6(-1.5), "-1.5");
1263 assert_eq!(format_cpp_g6(0.0), "0");
1264 }
1265}