1use std::cmp::Ordering;
11use std::collections::{BTreeSet, HashMap, HashSet};
12
13use ciplink::Value;
14use ciplink::cip::TagResult;
15use indexmap::IndexMap;
16
17use crate::plc::{Catalog, Kind, Node, Plc, atomic_size};
18
19pub const BYTES_PER_READ: u64 = 450;
21pub const PACKET_BYTES: usize = 4000;
23pub const VERSION_REQUEST: usize = 20;
25pub const VERSION_REPLY: usize = 44;
27
28#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
30pub enum Key {
31 Member(String),
33 Index(usize),
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
39pub struct Read {
40 pub name: String,
42 pub members: Vec<(String, Option<Vec<Key>>)>,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
50enum Tok {
51 Name(String),
52 Index(usize),
53 Multi(Vec<usize>),
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
58struct Token {
59 text: String,
60 key: Tok,
61}
62
63fn tokenize(name: &str) -> Option<Vec<Token>> {
65 let (prefix, rest) = match name.get(..8) {
66 Some(head) if head.eq_ignore_ascii_case("program:") => {
67 let dot = name.find('.')?;
68 (&name[..=dot], &name[dot + 1..])
69 }
70 _ => ("", name),
71 };
72 let bytes = rest.as_bytes();
73 let mut tokens: Vec<Token> = Vec::new();
74 let mut pos = 0;
75 while pos < bytes.len() {
76 let start = pos;
77 if bytes[pos] == b'[' {
78 let end = pos + rest[pos..].find(']')?;
79 let index = rest[pos + 1..end]
80 .split(',')
81 .map(|i| {
82 if i.is_empty() || !i.bytes().all(|b| b.is_ascii_digit()) {
83 None
84 } else {
85 i.parse::<usize>().ok()
86 }
87 })
88 .collect::<Option<Vec<_>>>()?;
89 pos = end + 1;
90 let key = match index.as_slice() {
91 [one] => Tok::Index(*one),
92 _ => Tok::Multi(index),
93 };
94 tokens.push(Token {
95 text: rest[start..pos].to_owned(),
96 key,
97 });
98 continue;
99 }
100 let dotted = bytes[pos] == b'.';
101 if dotted != (pos > 0) {
102 return None; }
104 if dotted {
105 pos += 1;
106 }
107 if !bytes
108 .get(pos)
109 .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
110 {
111 return None;
112 }
113 let name_start = pos;
114 while bytes
115 .get(pos)
116 .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
117 {
118 pos += 1;
119 }
120 tokens.push(Token {
121 text: rest[start..pos].to_owned(),
122 key: Tok::Name(rest[name_start..pos].to_owned()),
123 });
124 }
125 let first = tokens.first_mut()?;
126 let Tok::Name(base) = &first.key else {
127 return None;
128 };
129 first.key = Tok::Name(format!("{prefix}{base}"));
130 first.text = format!("{prefix}{}", first.text);
131 Some(tokens)
132}
133
134fn join(tokens: &[Token]) -> String {
135 tokens.iter().map(|t| t.text.as_str()).collect()
136}
137
138fn to_usize(v: u64) -> usize {
139 usize::try_from(v).unwrap_or(usize::MAX)
140}
141
142fn padded(text: &str) -> usize {
143 2 + text.len() + text.len() % 2
144}
145
146fn index_size(i: usize) -> usize {
147 if i < 256 { 2 } else { 4 }
148}
149
150fn bools(node: &Node) -> bool {
152 node.bool_array() || node.type_name == "BOOL"
153}
154
155fn request_size(name: &str, catalog: &Catalog) -> usize {
160 let Some(tokens) = tokenize(name) else {
161 return 10 + name.len() + name.len() % 2;
162 };
163 let base = &tokens[0].text;
164 let instance = catalog.info(base).and_then(|n| n.instance_id);
165 let mut size = 8 + match instance {
166 Some(i) if i < 256 => 2,
167 Some(i) if i >= 65536 => 6,
168 _ => 4,
169 };
170 if base
171 .get(..8)
172 .is_some_and(|h| h.eq_ignore_ascii_case("program:"))
173 {
174 size += padded(base.split('.').next().unwrap_or_default());
175 }
176 for token in &tokens[1..] {
177 size += match &token.key {
178 Tok::Name(n) => padded(n),
179 Tok::Index(i) => index_size(*i),
180 Tok::Multi(v) => v.iter().map(|i| index_size(*i)).sum(),
181 };
182 }
183 size
184}
185
186fn reply_size(node: Option<&Node>) -> usize {
188 match node {
189 None => 12,
190 Some(n) if n.kind != Kind::Atomic => {
191 to_usize(n.structure_size.unwrap_or(4)).saturating_add(10)
192 }
193 Some(n) => to_usize(atomic_size(&n.type_name).unwrap_or(4)) + 8,
194 }
195}
196
197fn range(name: &str) -> Option<(&str, usize)> {
199 let digits = name.strip_suffix('}')?;
200 let brace = digits.rfind('{')?;
201 let (base, count) = (&digits[..brace], &digits[brace + 1..]);
202 if base.is_empty() || count.is_empty() || !count.bytes().all(|b| b.is_ascii_digit()) {
203 return None;
204 }
205 Some((base, count.parse().ok()?))
206}
207
208fn read_size(name: &str, catalog: &Catalog) -> (usize, usize) {
210 let Some((base, count)) = range(name) else {
211 return (request_size(name, catalog), reply_size(catalog.info(name)));
212 };
213 let array = catalog.info(base);
214 let request = request_size(base, catalog);
215 if array.is_some_and(bools) {
216 return (request, count.div_ceil(32) * 4 + 8); }
218 let overhead = if array.is_some_and(|a| a.kind != Kind::Atomic) {
219 10
220 } else {
221 8
222 };
223 let element = reply_size(array).saturating_sub(overhead);
224 (
225 request,
226 count.saturating_mul(element).saturating_add(overhead),
227 )
228}
229
230fn element_size(node: Option<&Node>) -> Option<usize> {
233 let node = node?;
234 if node.kind != Kind::Atomic || node.dims.len() != 1 {
235 return None;
236 }
237 match node.type_name.as_str() {
238 "BOOL" | "BYTE" | "WORD" | "DWORD" | "LWORD" => None,
239 t => atomic_size(t).map(to_usize),
240 }
241}
242
243fn struct_size(catalog: &Catalog, node: Option<&Node>, last: &Tok) -> Option<u64> {
245 let node = node?;
246 if matches!(last, Tok::Name(_)) && !node.dims.is_empty() {
247 return None; }
249 catalog.struct_size(node)
250}
251
252fn pack(sizes: &[(usize, usize)], budget: usize) -> (Vec<Vec<usize>>, usize) {
258 let (empty_request, empty_reply) = (10 + VERSION_REQUEST, 8 + VERSION_REPLY);
259 let mut groups = Vec::new();
260 let mut fragmented = Vec::new();
261 let mut count = 0;
262 let mut current = Vec::new();
263 let (mut request, mut reply) = (empty_request, empty_reply);
264 for (i, &(req, rep)) in sizes.iter().enumerate() {
265 if empty_request.saturating_add(req) > budget || empty_reply.saturating_add(rep) > budget {
266 fragmented.push(vec![i]);
267 count += rep
268 .div_ceil(budget.saturating_sub(empty_reply).max(1))
269 .max(1)
270 + 2;
271 continue;
272 }
273 if !current.is_empty() && (request + req > budget || reply + rep > budget) {
274 groups.push(std::mem::take(&mut current));
275 count += 1;
276 (request, reply) = (empty_request, empty_reply);
277 }
278 current.push(i);
279 request += req;
280 reply += rep;
281 }
282 if !current.is_empty() {
283 groups.push(current);
284 count += 1;
285 }
286 groups.extend(fragmented);
287 (groups, count)
288}
289
290pub fn packets(sizes: &[(usize, usize)], budget: usize) -> usize {
292 pack(sizes, budget).1
293}
294
295#[derive(Clone)]
299struct Pending {
300 key: Option<String>,
301 members: Vec<String>,
302 size: (usize, usize),
303 name: String,
304}
305
306struct Run<'a> {
308 array: String,
309 size: usize,
310 index: HashMap<&'a str, usize>,
311}
312
313struct Merged {
315 reads: Vec<Pending>,
316 saved: usize,
317 extra: i128,
318 name: String,
319}
320
321pub fn plan(
325 tags: &[String],
326 catalog: &Catalog,
327 whole_max_bytes: u64,
328 no_group: &HashSet<String>,
329 budget: usize,
330 together: Option<&dyn Fn(&str) -> bool>,
331) -> Vec<Read> {
332 let mut parsed: HashMap<&str, Vec<Token>> = HashMap::new();
333 let mut ancestors: Vec<(&str, Vec<String>)> = Vec::new(); let mut depth: HashMap<&str, HashMap<String, usize>> = HashMap::new(); let mut names: HashMap<String, String> = HashMap::new(); let mut sizes: IndexMap<String, u64> = IndexMap::new(); let mut levels: HashMap<String, usize> = HashMap::new(); let mut whole: HashMap<String, &str> = HashMap::new(); let mut elements: IndexMap<String, Vec<(usize, &str)>> = IndexMap::new(); let mut arrays: HashMap<String, (String, usize)> = HashMap::new(); let mut seen = HashSet::new();
342 for tag in tags {
343 let Some(tokens) = tokenize(tag) else {
344 continue;
345 };
346 if !seen.insert(tag.to_lowercase()) {
347 continue;
348 }
349 let n = tokens.len();
350 let last = &tokens[n - 1].key;
351 let Some(info) = catalog.info(tag) else {
352 continue;
353 };
354 if !info.dims.is_empty() && matches!(last, Tok::Name(_)) {
355 continue; }
357 if let (true, Tok::Index(i)) = (n > 1, last) {
358 let array = join(&tokens[..n - 1]);
359 let akey = array.to_lowercase();
360 if let Some(size) = element_size(catalog.info(&array)) {
361 if !no_group.contains(&akey) {
362 elements
363 .entry(akey.clone())
364 .or_default()
365 .push((*i, tag.as_str()));
366 arrays.insert(akey, (array, size));
367 }
368 }
369 }
370 let mut found = Vec::new();
371 for k in (1..=n).rev() {
372 if k < n {
373 match &tokens[k].key {
375 Tok::Multi(_) => break,
376 Tok::Index(_) => {
377 if !catalog.info(&join(&tokens[..k])).is_some_and(|a| !bools(a)) {
378 break;
379 }
380 }
381 Tok::Name(_) => {}
382 }
383 }
384 let prefix = join(&tokens[..k]);
385 let key = prefix.to_lowercase();
386 if no_group.contains(&key) {
387 continue;
388 }
389 let Some(size) = struct_size(catalog, catalog.info(&prefix), &tokens[k - 1].key) else {
390 continue;
391 };
392 depth
393 .entry(tag.as_str())
394 .or_default()
395 .insert(key.clone(), k);
396 names.entry(key.clone()).or_insert(prefix);
397 sizes.insert(key.clone(), size);
398 levels.insert(key.clone(), k);
399 if k == n {
400 whole.insert(key, tag.as_str());
401 } else {
402 found.push(key);
403 }
404 }
405 ancestors.push((tag.as_str(), found));
406 parsed.insert(tag.as_str(), tokens);
407 }
408
409 let mut under: HashMap<String, HashSet<&str>> = HashMap::new(); for (tag, found) in &ancestors {
411 for key in found {
412 under.entry(key.clone()).or_default().insert(tag);
413 }
414 }
415 for (key, tag) in &whole {
416 under.entry(key.clone()).or_default().insert(tag);
417 }
418 let mut candidates: BTreeSet<String> = under
419 .iter()
420 .filter(|(key, served)| {
421 served.len() >= 2
422 && (whole.contains_key(*key)
423 || sizes.get(*key).is_some_and(|s| *s <= whole_max_bytes))
424 })
425 .map(|(key, _)| key.clone())
426 .collect();
427
428 let mut runs: HashMap<String, Run> = HashMap::new();
430 for (akey, items) in &mut elements {
431 items.sort_unstable();
432 let (array, size) = &arrays[akey];
433 let mut clusters: Vec<Vec<(usize, &str)>> = Vec::new();
434 for &(index, tag) in items.iter() {
435 let gap = clusters.last().and_then(|c| c.last()).map(|&(prev, _)| {
436 index
437 .saturating_sub(prev)
438 .saturating_sub(1)
439 .saturating_mul(*size)
440 });
441 if gap.is_none_or(|g| g as u64 > BYTES_PER_READ) {
442 clusters.push(Vec::new());
443 }
444 if let Some(c) = clusters.last_mut() {
445 c.push((index, tag));
446 }
447 }
448 for (c, cluster) in clusters.iter().enumerate() {
449 if cluster.len() >= 2 {
450 let key = format!("{akey}{{{c}"); under.insert(key.clone(), cluster.iter().map(|&(_, t)| t).collect());
452 runs.insert(
453 key.clone(),
454 Run {
455 array: array.clone(),
456 size: *size,
457 index: cluster.iter().map(|&(i, t)| (t, i)).collect(),
458 },
459 );
460 candidates.insert(key);
461 }
462 }
463 }
464
465 let start_of = |run: &Run, members: &[String]| -> (usize, usize) {
466 let indexes = members
467 .iter()
468 .filter_map(|t| run.index.get(t.as_str()).copied());
469 let start = indexes.clone().min().unwrap_or(0);
470 (start, indexes.max().unwrap_or(start) - start + 1)
471 };
472 let group = |key: &str, members: &[String]| -> (String, (usize, usize)) {
474 match runs.get(key) {
475 None => {
476 let name = names.get(key).cloned().unwrap_or_default();
477 let reply = to_usize(sizes.get(key).copied().unwrap_or(0)).saturating_add(10);
478 let request = request_size(&name, catalog);
479 (name, (request, reply))
480 }
481 Some(run) => {
482 let (start, count) = start_of(run, members);
483 let element = format!("{}[{start}]", run.array);
484 let reply = count.saturating_mul(run.size).saturating_add(8);
485 (
486 format!("{element}{{{count}}}"),
487 (request_size(&element, catalog), reply),
488 )
489 }
490 }
491 };
492 let merge = |key: &str, reads: &[Pending]| -> Option<Merged> {
494 let served = under.get(key)?;
495 let covered: Vec<usize> = (0..reads.len())
496 .filter(|&i| reads[i].members.iter().all(|t| served.contains(t.as_str())))
497 .collect();
498 if covered.len() < 2 {
499 return None;
500 }
501 let members: Vec<String> = covered
502 .iter()
503 .flat_map(|&i| reads[i].members.iter().cloned())
504 .collect();
505 let (name, size) = group(key, &members);
506 let before: usize = covered.iter().map(|&i| reads[i].size.1).sum();
507 let extra = size.1 as i128 - before as i128;
508 let mut new = Vec::with_capacity(reads.len() + 1 - covered.len());
509 for (i, r) in reads.iter().enumerate() {
510 if i == covered[0] {
511 new.push(Pending {
512 key: Some(key.to_owned()),
513 members: members.clone(),
514 size,
515 name: name.clone(),
516 });
517 } else if !covered.contains(&i) {
518 new.push(r.clone());
519 }
520 }
521 Some(Merged {
522 reads: new,
523 saved: covered.len() - 1,
524 extra,
525 name,
526 })
527 };
528 let sizes_of =
529 |reads: &[Pending]| -> Vec<(usize, usize)> { reads.iter().map(|r| r.size).collect() };
530
531 let mut reads: Vec<Pending> = tags
533 .iter()
534 .map(|tag| Pending {
535 key: None,
536 members: vec![tag.clone()],
537 size: read_size(tag, catalog),
538 name: tag.clone(),
539 })
540 .collect();
541 if let Some(together) = together {
542 let fits = budget.saturating_sub(8 + VERSION_REPLY);
544 let mut keys: Vec<&String> = sizes
545 .iter()
546 .filter(|(k, s)| {
547 under.get(*k).is_some_and(|u| u.len() >= 2)
548 && to_usize(**s).saturating_add(10) <= fits
549 })
550 .map(|(k, _)| k)
551 .collect();
552 keys.sort_by_key(|k| levels.get(*k).copied().unwrap_or(0)); for key in keys {
554 if together(names.get(key).map_or("", String::as_str)) {
555 if let Some(merged) = merge(key, &reads) {
556 reads = merged.reads;
557 }
558 candidates.remove(key);
559 }
560 }
561 }
562 let limit = packets(&sizes_of(&reads), budget);
563 loop {
564 let mut best: Option<(String, Merged)> = None;
566 for key in &candidates {
567 let Some(merged) = merge(key, &reads) else {
568 continue;
569 };
570 if merged.extra > i128::from(BYTES_PER_READ) * merged.saved as i128
571 || no_group.contains(&merged.name.to_lowercase())
572 || packets(&sizes_of(&merged.reads), budget) > limit
573 {
574 continue;
575 }
576 let better = best.as_ref().is_none_or(|(_, b)| {
577 (merged.extra * b.saved as i128)
578 .cmp(&(b.extra * merged.saved as i128))
579 .then(b.saved.cmp(&merged.saved))
580 == Ordering::Less
581 });
582 if better {
583 best = Some((key.clone(), merged));
584 }
585 }
586 let Some((key, merged)) = best else { break };
587 reads = merged.reads;
588 candidates.remove(&key);
589 }
590
591 reads
592 .into_iter()
593 .map(|r| {
594 let members = match &r.key {
595 None => vec![(r.name.clone(), None)],
596 Some(key) => match runs.get(key) {
597 Some(run) => {
598 let (start, _) = start_of(run, &r.members);
599 r.members
600 .iter()
601 .map(|t| {
602 let i = run.index.get(t.as_str()).copied().unwrap_or(start);
603 (t.clone(), Some(vec![Key::Index(i - start)]))
604 })
605 .collect()
606 }
607 None => r
608 .members
609 .iter()
610 .map(|t| {
611 let tokens = parsed.get(t.as_str()).map_or(&[][..], Vec::as_slice);
612 let k = depth
613 .get(t.as_str())
614 .and_then(|d| d.get(key))
615 .copied()
616 .unwrap_or(tokens.len());
617 let path = (k < tokens.len()).then(|| {
618 tokens[k..]
619 .iter()
620 .map(|tok| match &tok.key {
621 Tok::Name(n) => Key::Member(n.clone()),
622 Tok::Index(i) => Key::Index(*i),
623 Tok::Multi(v) => {
625 Key::Index(v.first().copied().unwrap_or(0))
626 }
627 })
628 .collect()
629 });
630 (t.clone(), path)
631 })
632 .collect(),
633 },
634 };
635 Read {
636 name: r.name,
637 members,
638 }
639 })
640 .collect()
641}
642
643pub fn chunks(reads: Vec<Read>, catalog: &Catalog, budget: usize) -> Vec<Vec<Read>> {
645 let sizes: Vec<_> = reads.iter().map(|r| read_size(&r.name, catalog)).collect();
646 let (groups, _) = pack(&sizes, budget);
647 let mut slots: Vec<Option<Read>> = reads.into_iter().map(Some).collect();
648 groups
649 .into_iter()
650 .map(|g| {
651 g.into_iter()
652 .filter_map(|i| slots.get_mut(i).and_then(Option::take))
653 .collect()
654 })
655 .collect()
656}
657
658pub fn dig(value: &Value, path: &[Key]) -> Option<Value> {
660 let mut value = value;
661 for key in path {
662 value = match (key, value) {
663 (Key::Index(i), Value::Array(items)) => items.get(*i)?,
664 (Key::Member(m), Value::Struct(members)) => members.get(m).or_else(|| {
665 members
666 .iter()
667 .find(|(k, _)| k.eq_ignore_ascii_case(m))
668 .map(|(_, v)| v)
669 })?,
670 _ => return None,
671 };
672 }
673 Some(value.clone())
674}
675
676#[allow(clippy::type_complexity)]
682pub fn execute(
683 plc: &mut dyn Plc,
684 chunks: &[Vec<Read>],
685 between: &mut dyn FnMut(&mut dyn Plc, HashMap<String, TagResult>) -> Result<(), String>,
686) -> Result<(HashMap<String, TagResult>, Vec<String>, usize), String> {
687 let mut out = HashMap::new();
688 let mut retry: Vec<String> = Vec::new();
689 let mut failed: Vec<String> = Vec::new();
690 let mut services = 0;
691 for (n, chunk) in chunks.iter().enumerate() {
692 let names: Vec<&str> = chunk.iter().map(|r| r.name.as_str()).collect();
693 let results = if names.is_empty() {
694 Vec::new()
695 } else {
696 plc.read(&names)?
697 };
698 services += names.len();
699 let mut part = HashMap::new();
700 for (read, result) in chunk.iter().zip(results) {
701 if let [(tag, None)] = read.members.as_slice() {
702 part.insert(tag.clone(), result);
703 continue;
704 }
705 if result.error.is_some() {
706 failed.push(read.name.clone());
707 retry.extend(read.members.iter().map(|(t, _)| t.clone()));
708 continue;
709 }
710 for (tag, path) in &read.members {
711 let Some(path) = path else {
712 part.insert(tag.clone(), result.clone());
713 continue;
714 };
715 match result.value.as_ref().and_then(|v| dig(v, path)) {
716 Some(value) => {
717 let sliced = TagResult {
718 tag: tag.clone(),
719 value: Some(value),
720 data_type: None,
721 error: None,
722 };
723 part.insert(tag.clone(), sliced);
724 }
725 None => {
726 if !failed.contains(&read.name) {
727 failed.push(read.name.clone());
728 }
729 retry.push(tag.clone());
730 }
731 }
732 }
733 }
734 if n + 1 < chunks.len() {
735 between(&mut *plc, part)?;
736 } else {
737 out.extend(part);
738 }
739 }
740 if !retry.is_empty() {
741 let names: Vec<&str> = retry.iter().map(String::as_str).collect();
742 let results = plc.read(&names)?;
743 out.extend(retry.iter().cloned().zip(results));
744 }
745 Ok((out, failed, services + retry.len()))
746}
747
748#[cfg(test)]
749mod tests {
750 use std::cell::RefCell;
753 use std::collections::BTreeMap;
754 use std::sync::{Arc, Mutex};
755
756 use ciplink::{ErrorKind, TagError};
757 use regex::Regex;
758 use serde_json::Value as Json;
759
760 use super::*;
761 use crate::plc::{Aoi, TypeDef};
762
763 fn atomic(t: &str, dims: &[u64]) -> Node {
766 Node {
767 type_name: t.into(),
768 kind: Kind::Atomic,
769 dims: dims.to_vec(),
770 member: false,
771 instance_id: None,
772 structure_size: None,
773 }
774 }
775
776 fn strct(t: &str, size: u64, dims: &[u64]) -> Node {
777 Node {
778 kind: Kind::Struct,
779 structure_size: Some(size),
780 ..atomic(t, dims)
781 }
782 }
783
784 fn udt(size: u64, members: Vec<(&str, Node)>, whole_read: Option<bool>) -> TypeDef {
785 TypeDef {
786 public: members.iter().map(|(m, _)| (*m).to_owned()).collect(),
787 members: members
788 .into_iter()
789 .map(|(m, n)| (m.to_owned(), Node { member: true, ..n }))
790 .collect(),
791 structure_size: size,
792 aoi: whole_read.map(|whole_read| Aoi {
793 whole_read,
794 readable: Vec::new(),
795 }),
796 whole_readable: true,
797 }
798 }
799
800 fn dints(size: u64) -> TypeDef {
801 udt(
802 size,
803 vec![("A", atomic("DINT", &[])), ("B", atomic("DINT", &[]))],
804 None,
805 )
806 }
807
808 fn base_types() -> Vec<(&'static str, TypeDef)> {
809 let bools = |names: &[&'static str]| {
810 names
811 .iter()
812 .map(|n| (*n, atomic("BOOL", &[])))
813 .collect::<Vec<_>>()
814 };
815 let valve = bools(&["EnableIn", "EnableOut", "Open", "Cmd"]);
816 let mut pump = bools(&["EnableIn", "EnableOut", "Run"]);
817 pump.push(("Speed", atomic("REAL", &[])));
818 let real = || atomic("REAL", &[]);
819 let dint = || atomic("DINT", &[]);
820 let motor = vec![
821 ("Speed", real()),
822 ("Current", real()),
823 ("Running", atomic("BOOL", &[])),
824 ("Count", dint()),
825 ];
826 let station = vec![
827 ("Motor", strct("Motor", 40, &[])),
828 ("Ready", atomic("BOOL", &[])),
829 ("Data", atomic("DINT", &[4])),
830 ("Bits", atomic("DWORD", &[1])), ];
832 let cell = vec![
834 ("Valve", strct("Valve_AOI", 60, &[])),
835 ("Mode", dint()),
836 ("Step", dint()),
837 ];
838 let tank = vec![
839 ("Level", real()),
840 ("Temp", real()),
841 ("Valve", atomic("BOOL", &[])),
842 ];
843 vec![
844 ("Motor", udt(40, motor, None)),
845 ("Station", udt(200, station, None)),
846 ("Big", dints(4000)),
847 ("Valve_AOI", udt(60, valve, Some(false))),
848 ("Pump_AOI", udt(40, pump, Some(true))),
849 ("Cell", udt(300, cell, None)),
850 ("Tank", udt(12, tank, None)),
851 ]
852 }
853
854 fn base_tags() -> Vec<(&'static str, Node)> {
855 let string = Node {
856 kind: Kind::String,
857 ..strct("STRING", 88, &[])
858 };
859 vec![
860 ("Line1", strct("Station", 200, &[])),
861 ("Line2", strct("Station", 200, &[])),
862 ("Arr", strct("Station", 200, &[5])),
863 ("Program:Station1.Motor", strct("Motor", 40, &[])),
864 ("Big1", strct("Big", 4000, &[])),
865 ("Valve1", strct("Valve_AOI", 60, &[])),
866 ("Pump1", strct("Pump_AOI", 40, &[])),
867 ("Cell1", strct("Cell", 300, &[])),
868 ("Name1", string),
869 ("Speed", atomic("REAL", &[])),
870 ("Count", atomic("DINT", &[])),
871 ("Flags", atomic("DINT", &[])),
872 ("Trend", atomic("REAL", &[16])),
873 ("Long", atomic("REAL", &[1000])),
874 ("BoolArr", atomic("DWORD", &[32])), ("Tank", strct("Tank", 12, &[4])),
876 ("Grid", atomic("DINT", &[4, 4])),
877 ]
878 }
879
880 fn catalog_with(tags: Vec<(&str, Node)>, types: Vec<(&str, TypeDef)>) -> Catalog {
881 let tags = base_tags()
882 .into_iter()
883 .chain(tags)
884 .map(|(k, v)| (k.to_owned(), v))
885 .collect();
886 let types = base_types()
887 .into_iter()
888 .chain(types)
889 .map(|(k, v)| (k.to_owned(), v))
890 .collect();
891 Catalog::new(Json::Null, tags, types)
892 }
893
894 fn catalog() -> Catalog {
895 catalog_with(vec![], vec![])
896 }
897
898 fn blob(size: u64) -> Catalog {
899 catalog_with(
900 vec![("Blob1", strct("Blob", size, &[]))],
901 vec![("Blob", dints(size))],
902 )
903 }
904
905 fn st(members: Vec<(&str, Value)>) -> Value {
908 Value::Struct(
909 members
910 .into_iter()
911 .map(|(k, v)| (k.to_owned(), v))
912 .collect(),
913 )
914 }
915
916 fn motor(speed: f32) -> Value {
917 st(vec![
918 ("Speed", Value::Real(speed)),
919 ("Current", Value::Real(speed / 10.0)),
920 ("Running", Value::Bool(true)),
921 ("Count", Value::DInt(0b101101)),
922 ])
923 }
924
925 fn station(n: i32) -> Value {
926 st(vec![
927 ("Motor", motor(n as f32 * 1.5)),
928 ("Ready", Value::Bool(n % 2 == 0)),
929 ("Data", Value::Array((n..n + 4).map(Value::DInt).collect())),
930 ("Bits", Value::Array(vec![Value::Bool(true); 32])),
931 ])
932 }
933
934 fn values() -> HashMap<String, Value> {
935 let b = Value::Bool;
936 let tank = |i: i32| {
937 st(vec![
938 ("Level", Value::Real(i as f32 * 10.0)),
939 ("Temp", Value::Real(20.0 + i as f32)),
940 ("Valve", b(i % 2 == 0)),
941 ])
942 };
943 let pump = vec![
944 ("EnableIn", b(true)),
945 ("EnableOut", b(true)),
946 ("Run", b(true)),
947 ("Speed", Value::Real(12.5)),
948 ];
949 let cell = vec![
950 (
951 "Valve",
952 st(vec![
953 ("EnableIn", b(true)),
954 ("EnableOut", b(false)),
955 ("Open", b(true)),
956 ]),
957 ),
958 ("Mode", Value::DInt(2)),
959 ("Step", Value::DInt(30)),
960 ];
961 let pairs = vec![
962 ("Line1", station(1)),
963 ("Line2", station(2)),
964 ("Arr", Value::Array((0..5).map(station).collect())),
965 ("Program:Station1.Motor", motor(7.5)),
966 (
967 "Big1",
968 st(vec![("A", Value::DInt(1)), ("B", Value::DInt(2))]),
969 ),
970 (
972 "Valve1",
973 st(vec![
974 ("EnableIn", b(true)),
975 ("EnableOut", b(true)),
976 ("Open", b(false)),
977 ]),
978 ),
979 ("Pump1", st(pump)),
980 ("Cell1", st(cell)),
981 ("Name1", Value::String("hello".into())),
982 ("Speed", Value::Real(3.5)),
983 ("Count", Value::DInt(9)),
984 ("Flags", Value::DInt(0b1010)),
985 (
986 "Trend",
987 Value::Array((0..16).map(|i| Value::Real(i as f32 / 2.0)).collect()),
988 ),
989 (
990 "Long",
991 Value::Array((0..1000).map(|i| Value::Real(i as f32)).collect()),
992 ),
993 (
994 "BoolArr",
995 Value::Array((0..32).map(|i| b(i % 3 == 0)).collect()),
996 ),
997 ("Tank", Value::Array((0..4).map(tank).collect())),
998 ];
999 pairs.into_iter().map(|(k, v)| (k.to_owned(), v)).collect()
1000 }
1001
1002 #[derive(Debug, PartialEq)]
1005 enum Event {
1006 Read(Vec<String>),
1007 Between(Vec<String>),
1008 }
1009
1010 struct Fake {
1012 catalog: Catalog,
1013 values: HashMap<String, Value>,
1014 errors: HashMap<String, String>,
1015 log: Arc<Mutex<Vec<Event>>>,
1016 }
1017
1018 impl Fake {
1019 fn new(catalog: Catalog) -> Self {
1020 Fake {
1021 catalog,
1022 values: values(),
1023 errors: HashMap::new(),
1024 log: Arc::default(),
1025 }
1026 }
1027
1028 fn reads(&self) -> Vec<Vec<String>> {
1029 let log = self.log.lock().expect("log");
1030 log.iter()
1031 .filter_map(|e| match e {
1032 Event::Read(names) => Some(names.clone()),
1033 Event::Between(_) => None,
1034 })
1035 .collect()
1036 }
1037
1038 fn value(&self, name: &str) -> Option<Value> {
1039 if let Some(v) = self.values.get(name) {
1040 return Some(v.clone());
1041 }
1042 let (prefix, rest) = if name.to_lowercase().starts_with("program:") {
1043 let (p, r) = name.split_once('.')?;
1044 (format!("{p}."), r)
1045 } else {
1046 (String::new(), name)
1047 };
1048 let re = Regex::new(r"[^.\[\]]+|\[\d+\]").expect("regex");
1049 let parts: Vec<&str> = re.find_iter(rest).map(|m| m.as_str()).collect();
1050 let base = format!("{prefix}{}", parts.first()?);
1051 let mut value = self
1052 .values
1053 .iter()
1054 .find(|(k, _)| k.eq_ignore_ascii_case(&base))
1055 .map(|(_, v)| v.clone())?;
1056 for part in &parts[1..] {
1057 value = if let Some(i) = part.strip_prefix('[') {
1058 dig(&value, &[Key::Index(i.trim_end_matches(']').parse().ok()?)])?
1059 } else if part.bytes().all(|b| b.is_ascii_digit()) {
1060 let bit: u32 = part.parse().ok()?;
1061 match value {
1062 Value::DInt(v) => Value::Bool(v >> bit & 1 == 1),
1063 _ => return None,
1064 }
1065 } else {
1066 dig(&value, &[Key::Member((*part).to_owned())])?
1067 };
1068 }
1069 Some(value)
1070 }
1071
1072 fn result(&self, tag: &str, range: &Regex) -> TagResult {
1073 let mut result = TagResult {
1074 tag: tag.to_owned(),
1075 value: None,
1076 data_type: None,
1077 error: None,
1078 };
1079 if let Some(e) = self.errors.get(tag) {
1080 result.error = Some(TagError::new(e.clone(), ErrorKind::Device));
1081 return result;
1082 }
1083 let value = match range.captures(tag) {
1084 Some(c) => {
1085 let start: usize = c.get(2).map_or(0, |m| m.as_str().parse().expect("start"));
1086 let count: usize = c[3].parse().expect("count");
1087 match self.value(&c[1]) {
1088 Some(Value::Array(items)) if items.len() >= start + count => {
1089 Some(Value::Array(items[start..start + count].to_vec()))
1090 }
1091 _ => None,
1092 }
1093 }
1094 None => self.value(tag),
1095 };
1096 match value {
1097 Some(v) => {
1098 result.value = Some(v);
1099 result.data_type = Some("DINT".into());
1100 }
1101 None => {
1102 let message = format!("Tag doesn't exist - {tag}");
1103 result.error = Some(TagError::new(message, ErrorKind::NotFound));
1104 }
1105 }
1106 result
1107 }
1108 }
1109
1110 impl Plc for Fake {
1111 fn open(&mut self) -> Result<(), String> {
1112 Ok(())
1113 }
1114
1115 fn close(&mut self) {}
1116
1117 fn read(&mut self, tags: &[&str]) -> Result<Vec<TagResult>, String> {
1118 let names = tags.iter().map(|t| (*t).to_owned()).collect();
1119 self.log.lock().expect("log").push(Event::Read(names));
1120 let range = Regex::new(r"^(.+?)(?:\[(\d+)\])?\{(\d+)\}$").expect("regex");
1121 Ok(tags.iter().map(|tag| self.result(tag, &range)).collect())
1122 }
1123
1124 fn write(&mut self, _tag: &str, _value: &Value) -> Result<TagResult, String> {
1125 Err("not used".into())
1126 }
1127
1128 fn catalog(&mut self) -> Catalog {
1129 self.catalog.clone()
1130 }
1131
1132 fn version(&mut self) -> Json {
1133 Json::Null
1134 }
1135
1136 fn connection_size(&self) -> usize {
1137 PACKET_BYTES
1138 }
1139 }
1140
1141 type Grouped = BTreeMap<String, Vec<String>>;
1144 type Got = HashMap<String, (Option<Value>, Option<TagError>)>;
1145 type Together<'a> = Option<&'a dyn Fn(&str) -> bool>;
1146
1147 fn owned(tags: &[&str]) -> Vec<String> {
1148 tags.iter().map(|t| (*t).to_owned()).collect()
1149 }
1150
1151 fn set(names: &[&str]) -> HashSet<String> {
1152 names.iter().map(|t| (*t).to_owned()).collect()
1153 }
1154
1155 fn grouped(reads: Vec<Read>) -> Grouped {
1156 reads
1157 .into_iter()
1158 .map(|r| {
1159 let mut tags: Vec<String> = r.members.into_iter().map(|(t, _)| t).collect();
1160 tags.sort();
1161 (r.name, tags)
1162 })
1163 .collect()
1164 }
1165
1166 fn reads_with(
1168 c: &Catalog,
1169 tags: &[&str],
1170 whole_max: u64,
1171 no_group: &[&str],
1172 budget: usize,
1173 together: Together,
1174 ) -> Grouped {
1175 grouped(plan(
1176 &owned(tags),
1177 c,
1178 whole_max,
1179 &set(no_group),
1180 budget,
1181 together,
1182 ))
1183 }
1184
1185 fn reads(c: &Catalog, tags: &[&str]) -> Grouped {
1186 reads_with(c, tags, 1500, &[], 4000, None)
1187 }
1188
1189 fn expect(pairs: &[(&str, &[&str])]) -> Grouped {
1190 pairs
1191 .iter()
1192 .map(|(name, tags)| ((*name).to_owned(), owned(tags)))
1193 .collect()
1194 }
1195
1196 fn plain(tags: &[&str]) -> Grouped {
1197 tags.iter()
1198 .map(|t| ((*t).to_owned(), vec![(*t).to_owned()]))
1199 .collect()
1200 }
1201
1202 fn got(results: HashMap<String, TagResult>) -> Got {
1203 results
1204 .into_iter()
1205 .map(|(t, r)| (t, (r.value, r.error)))
1206 .collect()
1207 }
1208
1209 fn direct(fake: &mut Fake, tags: &[&str]) -> Got {
1211 let results = fake.read(tags).expect("read");
1212 got(owned(tags).into_iter().zip(results).collect())
1213 }
1214
1215 fn run(fake: &mut Fake, tags: &[&str], together: Together) -> (Got, Vec<String>, usize) {
1217 let c = fake.catalog.clone();
1218 let parts = chunks(
1219 plan(&owned(tags), &c, 1500, &set(&[]), 4000, together),
1220 &c,
1221 4000,
1222 );
1223 let mut given = HashMap::new();
1224 let (out, failed, services) = execute(fake, &parts, &mut |_, part| {
1225 given.extend(part);
1226 Ok(())
1227 })
1228 .expect("execute");
1229 given.extend(out);
1230 (got(given), failed, services)
1231 }
1232
1233 fn sizes(names: &[&str], c: &Catalog) -> Vec<(usize, usize)> {
1234 names.iter().map(|n| read_size(n, c)).collect()
1235 }
1236
1237 #[test]
1240 fn tokenize_splits_names_and_refuses_what_is_never_grouped() {
1241 let toks = |name: &str| {
1242 tokenize(name).map(|t| t.into_iter().map(|t| (t.text, t.key)).collect::<Vec<_>>())
1243 };
1244 let n = |s: &str| Tok::Name(s.into());
1245 let expected = vec![
1246 ("Arr".into(), n("Arr")),
1247 ("[3]".into(), Tok::Index(3)),
1248 (".Status".into(), n("Status")),
1249 (".Ready".into(), n("Ready")),
1250 ];
1251 assert_eq!(toks("Arr[3].Status.Ready"), Some(expected));
1252 let expected = vec![
1253 ("Program:Station1.Motor".into(), n("Program:Station1.Motor")),
1254 (".Speed".into(), n("Speed")),
1255 ];
1256 assert_eq!(toks("Program:Station1.Motor.Speed"), Some(expected));
1257 assert_eq!(
1258 toks("M[1,2].X").expect("tokens")[1],
1259 ("[1,2]".into(), Tok::Multi(vec![1, 2]))
1260 );
1261 for never in ["X.Count.5", "X{3}", "X..Y", "[1]", "Program:Main", ""] {
1262 assert_eq!(toks(never), None, "{never}");
1263 }
1264 }
1265
1266 #[test]
1267 fn groups_members_and_merges_child_groups_into_the_parent() {
1268 let c = catalog();
1269 assert_eq!(
1270 reads(&c, &["Line1.Motor.Speed", "Line1.Motor.Current", "Speed"]),
1271 expect(&[
1272 ("Line1.Motor", &["Line1.Motor.Current", "Line1.Motor.Speed"]),
1273 ("Speed", &["Speed"])
1274 ])
1275 );
1276 let tags = [
1278 "Line1.Motor.Speed",
1279 "Line1.Motor.Current",
1280 "Line1.Ready",
1281 "Line1.Data[2]",
1282 "Speed",
1283 ];
1284 let line1: &[&str] = &[
1285 "Line1.Data[2]",
1286 "Line1.Motor.Current",
1287 "Line1.Motor.Speed",
1288 "Line1.Ready",
1289 ];
1290 assert_eq!(
1291 reads(&c, &tags),
1292 expect(&[("Line1", line1), ("Speed", &["Speed"])])
1293 );
1294 let tags = ["Line1.Motor.Speed", "Line2.Motor.Speed"]; assert_eq!(reads(&c, &tags), plain(&tags));
1296 }
1297
1298 #[test]
1299 fn array_elements_and_program_tags() {
1300 let tags = [
1301 "Arr[3].Motor.Speed",
1302 "Arr[3].Ready",
1303 "Arr[1].Ready",
1304 "Program:Station1.Motor.Speed",
1305 "Program:Station1.Motor.Running",
1306 ];
1307 let program: &[&str] = &[
1308 "Program:Station1.Motor.Running",
1309 "Program:Station1.Motor.Speed",
1310 ];
1311 assert_eq!(
1312 reads(&catalog(), &tags),
1313 expect(&[
1314 ("Arr[3]", &["Arr[3].Motor.Speed", "Arr[3].Ready"]),
1315 ("Program:Station1.Motor", program),
1316 ("Arr[1].Ready", &["Arr[1].Ready"]), ])
1318 );
1319 }
1320
1321 #[test]
1322 fn whole_max_bytes_caps_the_struct_size() {
1323 let c = blob(300);
1324 let tags = ["Blob1.A", "Blob1.B"];
1325 assert_eq!(reads(&c, &tags), expect(&[("Blob1", &tags)]));
1326 assert_eq!(reads_with(&c, &tags, 299, &[], 4000, None), plain(&tags));
1327 let tags = ["Line1.Motor.Speed", "Line1.Ready"];
1328 assert_eq!(reads_with(&c, &tags, 100, &[], 4000, None), plain(&tags));
1329 }
1330
1331 #[test]
1332 fn extra_reply_bytes_per_read_saved_threshold() {
1333 let tags = ["Blob1.A", "Blob1.B"];
1335 assert_eq!(reads(&blob(464), &tags), expect(&[("Blob1", &tags)]));
1336 assert_eq!(reads(&blob(465), &tags), plain(&tags));
1337 assert_eq!(
1339 reads_with(&catalog(), &["Big1.A", "Big1.B"], 5000, &[], 4000, None).len(),
1340 2
1341 );
1342 }
1343
1344 #[test]
1345 fn a_group_never_adds_a_packet() {
1346 let c = catalog();
1347 let tags = ["Line1.Motor.Speed", "Line1.Motor.Current"];
1348 let plain_sizes: Vec<_> = tags
1349 .iter()
1350 .map(|t| (request_size(t, &c), reply_size(c.info(t))))
1351 .collect();
1352 let group = [(
1353 request_size("Line1.Motor", &c),
1354 reply_size(c.info("Line1.Motor")),
1355 )];
1356 let plain_reply: usize = plain_sizes.iter().map(|s| s.1).sum();
1357 let plain_request: usize = plain_sizes.iter().map(|s| s.0).sum();
1358 assert!(group[0].1 > plain_reply); let budget = (52 + plain_reply).max(30 + plain_request) + 10; assert!(52 + group[0].1 > budget); assert_eq!(packets(&plain_sizes, budget), 1);
1362 assert_eq!(packets(&group, budget), 2 + 2); assert_eq!(reads_with(&c, &tags, 1500, &[], budget, None), plain(&tags));
1364 assert_eq!(
1365 reads(&c, &tags),
1366 expect(&[("Line1.Motor", &[tags[1], tags[0]])])
1367 );
1368 }
1369
1370 #[test]
1371 fn packet_estimate_packs_in_order() {
1372 assert_eq!(packets(&[(30, 1000); 3], 4000), 1);
1373 assert_eq!(packets(&[(30, 1000); 4], 4000), 2); assert_eq!(packets(&[(30, 5000)], 4000), 2 + 2); assert_eq!(packets(&[], 4000), 0);
1376 }
1377
1378 #[test]
1379 fn aois_are_read_whole_only_when_the_probe_allows() {
1380 let c = catalog();
1381 assert_eq!(reads(&c, &["Valve1.Open", "Valve1.EnableIn"]).len(), 2); let tags = ["Pump1.Run", "Pump1.Speed"];
1383 assert_eq!(reads(&c, &tags), expect(&[("Pump1", &tags)]));
1384 assert_eq!(reads(&c, &["Cell1.Mode", "Cell1.Step"]).len(), 2); let tags = ["Cell1.Valve.Open", "Cell1.Mode"];
1386 assert_eq!(reads(&c, &tags), plain(&tags));
1387 }
1388
1389 #[test]
1390 fn never_grouped() {
1391 let tags = [
1393 "Line1.Motor.Count.0",
1394 "Line1.Motor.Count.2",
1395 "Line1.Data{2}",
1396 "Line1.Motor.Speed",
1397 "Line1.Bits[3]",
1398 "Line1.Data",
1399 "Name1.LEN",
1400 "Name1.DATA",
1401 "Nope.A",
1402 "Nope.B",
1403 ];
1404 assert_eq!(reads(&catalog(), &tags), plain(&tags));
1405 }
1406
1407 #[test]
1408 fn requested_struct_is_the_group_read() {
1409 let c = catalog();
1410 let tags = ["Line1.Motor", "Line1.Motor.Speed", "Line1.Motor.Running"];
1411 let mut sorted = tags;
1412 sorted.sort_unstable();
1413 assert_eq!(reads(&c, &tags), expect(&[("Line1.Motor", &sorted)]));
1414 assert_eq!(
1416 reads(&c, &["Big1", "Big1.A"]),
1417 expect(&[("Big1", &["Big1", "Big1.A"])])
1418 );
1419 let tags = ["Line1.Motor", "Line1.Motor.Speed", "Line1.Ready"];
1421 assert_eq!(reads(&c, &tags), expect(&[("Line1", &tags)]));
1422 }
1423
1424 #[test]
1425 fn sliced_values_are_what_member_reads_give() {
1426 let mut fake = Fake::new(catalog());
1427 let tags = [
1428 "Line1.Motor.Speed",
1429 "line1.motor.current",
1430 "Line1.Ready",
1431 "Line1.Data[2]",
1432 "Arr[3].Motor.Speed",
1433 "Arr[3].Data[0]",
1434 "Program:Station1.Motor.Speed",
1435 "Program:Station1.Motor.Count",
1436 "Line2.Motor",
1437 "Line2.Motor.Running",
1438 "Pump1.Speed",
1439 "Pump1.Run",
1440 "Speed",
1441 "Count.1",
1442 "Nope",
1443 ];
1444 let (got, failed, services) = run(&mut fake, &tags, None);
1445 assert_eq!(got, direct(&mut fake, &tags));
1446 assert!(failed.is_empty());
1447 assert_eq!(services, 8);
1449 }
1450
1451 #[test]
1452 fn group_error_falls_back_to_member_reads() {
1453 let mut fake = Fake::new(catalog());
1454 fake.errors
1455 .insert("Line1.Motor".into(), "General error".into());
1456 let tags = ["Line1.Motor.Speed", "Line1.Motor.Current", "Speed"];
1457 let (got, failed, services) = run(&mut fake, &tags, None);
1458 assert_eq!(
1459 fake.reads()[1],
1460 owned(&["Line1.Motor.Speed", "Line1.Motor.Current"])
1461 );
1462 assert_eq!(got, direct(&mut fake, &tags));
1463 assert_eq!((failed, services), (owned(&["Line1.Motor"]), 4));
1464 let c = catalog();
1466 assert_eq!(
1467 reads_with(&c, &tags, 1500, &["line1.motor"], 4000, None),
1468 expect(&[
1469 ("Line1", &["Line1.Motor.Current", "Line1.Motor.Speed"]),
1470 ("Speed", &["Speed"])
1471 ])
1472 );
1473 assert_eq!(
1474 reads_with(&c, &tags, 1500, &["line1.motor", "line1"], 4000, None),
1475 plain(&tags)
1476 );
1477 }
1478
1479 #[test]
1480 fn slice_failure_falls_back() {
1481 let mut fake = Fake::new(catalog());
1482 if let Some(Value::Struct(line1)) = fake.values.get_mut("Line1") {
1483 line1.insert("Motor".into(), Value::DInt(5)); }
1485 fake.values
1486 .insert("Line1.Motor.Speed".into(), Value::Real(1.0));
1487 fake.values
1488 .insert("Line1.Motor.Current".into(), Value::Real(2.0));
1489 let (got, failed, _) = run(
1490 &mut fake,
1491 &["Line1.Motor.Speed", "Line1.Ready", "Line1.Motor.Current"],
1492 None,
1493 );
1494 assert_eq!(got["Line1.Motor.Speed"], (Some(Value::Real(1.0)), None));
1495 assert_eq!(got["Line1.Motor.Current"], (Some(Value::Real(2.0)), None));
1496 assert_eq!(failed, owned(&["Line1"]));
1497 }
1498
1499 #[test]
1500 fn array_runs() {
1501 let bit_strings = vec![
1502 ("Words", atomic("WORD", &[10])),
1503 ("Bytes", atomic("BYTE", &[10])),
1504 ];
1505 let c = catalog_with(bit_strings, vec![]);
1506 assert_eq!(
1507 reads(&c, &["Trend[3]", "Trend[4]", "Trend[7]", "Speed"]),
1508 expect(&[
1509 ("Trend[3]{5}", &["Trend[3]", "Trend[4]", "Trend[7]"]),
1510 ("Speed", &["Speed"])
1511 ])
1512 );
1513 assert_eq!(
1515 reads(
1516 &c,
1517 &[
1518 "Line1.Data[0]",
1519 "Line1.Data[3]",
1520 "Arr[2].Data[1]",
1521 "Arr[2].Data[2]"
1522 ]
1523 ),
1524 expect(&[
1525 ("Line1.Data[0]{4}", &["Line1.Data[0]", "Line1.Data[3]"]),
1526 ("Arr[2].Data[1]{2}", &["Arr[2].Data[1]", "Arr[2].Data[2]"]),
1527 ])
1528 );
1529 let never: [&[&str]; 9] = [
1532 &["BoolArr[1]", "BoolArr[2]"],
1533 &["Line1.Bits[0]", "Line1.Bits[1]"],
1534 &["Words[1]", "Words[2]"],
1535 &["Bytes[1]", "Bytes[2]"],
1536 &["Tank[1].Level", "Tank[2].Level"],
1537 &["Grid[1,1]", "Grid[1,2]"],
1538 &["Trend", "Trend[1]"],
1539 &["Trend[5]", "Speed"],
1540 &["Arr[1]", "Arr[2]"],
1541 ];
1542 for tags in never {
1543 assert_eq!(reads(&c, tags), plain(tags), "{tags:?}");
1544 }
1545 let tags = ["Trend[3]", "Trend[4]"];
1547 assert_eq!(
1548 reads_with(&c, &tags, 1500, &["trend[3]{2}"], 4000, None).len(),
1549 2
1550 );
1551 assert_eq!(reads_with(&c, &tags, 1500, &["trend"], 4000, None).len(), 2);
1552 }
1553
1554 #[test]
1555 fn array_run_cost_rule() {
1556 let c = catalog();
1557 for (last, grouped) in [(113, true), (114, false)] {
1559 let last_tag = format!("Long[{last}]");
1560 let tags = ["Long[0]", last_tag.as_str()];
1561 let run = BTreeMap::from([(format!("Long[0]{{{}}}", last + 1), owned(&tags))]);
1562 assert_eq!(reads(&c, &tags) == run, grouped, "{last}");
1563 }
1564 assert_eq!(
1566 reads(&c, &["Long[0]", "Long[1]", "Long[500]", "Long[502]"]),
1567 expect(&[
1568 ("Long[0]{2}", &["Long[0]", "Long[1]"]),
1569 ("Long[500]{3}", &["Long[500]", "Long[502]"])
1570 ])
1571 );
1572 }
1573
1574 #[test]
1575 fn array_run_never_adds_a_packet() {
1576 let c = catalog();
1577 let tags = ["Trend[0]", "Trend[15]"];
1578 let plain_sizes = sizes(&tags, &c);
1579 let whole = sizes(&["Trend[0]{16}"], &c);
1580 assert_eq!((whole[0].1, plain_sizes[0].1), (16 * 4 + 8, 12));
1581 assert_eq!(
1582 (packets(&plain_sizes, 100), packets(&whole, 100)),
1583 (1, 2 + 2)
1584 );
1585 assert_eq!(reads_with(&c, &tags, 1500, &[], 100, None), plain(&tags));
1586 assert_eq!(reads(&c, &tags), expect(&[("Trend[0]{16}", &tags)]));
1587 }
1588
1589 #[test]
1590 fn array_run_values_are_sliced() {
1591 let mut fake = Fake::new(catalog());
1592 let tags = [
1593 "Trend[3]",
1594 "Trend[4]",
1595 "Trend[7]",
1596 "Line1.Data[0]",
1597 "Line1.Data[3]",
1598 "Line1.Ready",
1599 "Speed",
1600 ];
1601 let (got, failed, services) = run(&mut fake, &tags, None);
1602 assert_eq!(got, direct(&mut fake, &tags));
1603 assert!(failed.is_empty());
1604 assert_eq!(got["Trend[7]"], (Some(Value::Real(3.5)), None));
1605 assert_eq!(services, 3); fake.values.insert(
1608 "Trend".into(),
1609 Value::Array(vec![Value::Real(1.0), Value::Real(2.0)]),
1610 );
1611 let (got, failed, _) = run(&mut fake, &["Trend[0]", "Trend[1]", "Trend[3]"], None);
1612 assert_eq!(failed, owned(&["Trend[0]{4}"]));
1613 assert_eq!(got["Trend[1]"], (Some(Value::Real(2.0)), None));
1614 assert_eq!(
1615 got["Trend[3]"].1.as_ref().map(|e| e.kind),
1616 Some(ErrorKind::NotFound)
1617 );
1618 }
1619
1620 #[test]
1621 fn together_reads_a_struct_whole() {
1622 let c = catalog();
1623 let all = |_: &str| true;
1624 let line1 = |p: &str| p == "Line1";
1625 let tags = ["Line1.Motor.Speed", "Line1.Ready"];
1626 assert_eq!(reads_with(&c, &tags, 100, &[], 4000, None), plain(&tags));
1627 assert_eq!(
1628 reads_with(&c, &tags, 100, &[], 4000, Some(&line1)),
1629 expect(&[("Line1", &tags)])
1630 );
1631 let tags = ["Blob1.A", "Blob1.B"];
1633 assert_eq!(
1634 reads_with(&blob(3000), &tags, 1500, &[], 4000, Some(&all)),
1635 expect(&[("Blob1", &tags)])
1636 );
1637 let tags = ["Line1.Motor.Speed", "Line1.Motor.Current", "Line1.Ready"];
1638 assert_eq!(
1639 reads_with(&c, &tags, 1500, &[], 4000, Some(&all)),
1640 expect(&[(
1641 "Line1",
1642 &["Line1.Motor.Current", "Line1.Motor.Speed", "Line1.Ready"]
1643 )])
1644 );
1645 let two = ["Line1.Motor.Speed", "Speed"];
1646 assert_eq!(
1647 reads_with(&c, &two, 1500, &[], 4000, Some(&all)),
1648 plain(&two)
1649 );
1650 let big = ["Big1.A", "Big1.B"];
1651 assert_eq!(
1652 reads_with(&c, &big, 1500, &[], 4000, Some(&all)),
1653 plain(&big)
1654 );
1655 let seen = RefCell::new(Vec::new());
1656 let record = |p: &str| {
1657 seen.borrow_mut().push(p.to_owned());
1658 false
1659 };
1660 reads_with(
1661 &c,
1662 &["Arr[1].Motor.Speed", "Arr[1].Motor.Count"],
1663 1500,
1664 &[],
1665 4000,
1666 Some(&record),
1667 );
1668 assert_eq!(seen.into_inner(), owned(&["Arr[1]", "Arr[1].Motor"]));
1669 let mut fake = Fake::new(catalog());
1671 fake.errors.insert("Line1".into(), "General error".into());
1672 let (got, failed, _) = run(&mut fake, &tags, Some(&line1));
1673 assert_eq!(failed, owned(&["Line1"]));
1674 assert_eq!(got, direct(&mut fake, &tags));
1675 }
1676
1677 #[test]
1678 fn chunks_are_one_packet_each() {
1679 let c = catalog();
1680 let mut tags = owned(&["Line1.Motor.Speed", "Line2.Motor.Speed"]);
1681 tags.extend((0..16).step_by(5).map(|i| format!("Trend[{i}]")));
1682 tags.extend((0..5).map(|i| format!("Arr[{i}].Ready")));
1683 tags.extend(owned(&["Speed", "Count", "Name1", "Big1", "Flags"]));
1684 let budget = 200;
1685 let planned = plan(&tags, &c, 1500, &set(&[]), budget, None);
1686 let parts = chunks(planned.clone(), &c, budget);
1687 let mut flat: Vec<Read> = parts.iter().flatten().cloned().collect();
1688 let mut sorted = planned;
1689 flat.sort();
1690 sorted.sort();
1691 assert_eq!(flat, sorted);
1692 assert_eq!(parts.len(), 3);
1693 let big1 = Read {
1695 name: "Big1".into(),
1696 members: vec![("Big1".into(), None)],
1697 };
1698 assert_eq!(parts[2], vec![big1]);
1699 let part_sizes: Vec<Vec<(usize, usize)>> = parts
1700 .iter()
1701 .map(|part| part.iter().map(|r| read_size(&r.name, &c)).collect())
1702 .collect();
1703 let big = packets(&sizes(&["Big1"], &c), budget); let all: Vec<(usize, usize)> = part_sizes.iter().flatten().copied().collect();
1705 assert!(big > 1);
1706 assert_eq!(parts.len(), packets(&all, budget) - big + 1);
1707 for (part, size) in parts.iter().zip(&part_sizes) {
1708 assert!(
1709 packets(size, budget) == 1 || part.iter().map(|r| r.name.as_str()).eq(["Big1"])
1710 );
1711 }
1712 assert!(chunks(Vec::new(), &c, PACKET_BYTES).is_empty());
1713 }
1714
1715 #[test]
1716 fn a_fragmented_read_does_not_split_the_reads_around_it() {
1717 let sizes = [(30, 100), (30, 5000), (30, 100)];
1718 assert_eq!(packets(&sizes, 4000), 5); assert_eq!(pack(&sizes, 4000).0, vec![vec![0, 2], vec![1]]);
1720 }
1721
1722 #[test]
1723 fn read_size_of_ranges() {
1724 let c = catalog_with(vec![("Bits", atomic("DWORD", &[128]))], vec![]); let station = reply_size(c.info("Arr")); assert_eq!(read_size("Arr{4}", &c).1, 4 * (station - 10) + 10);
1727 assert_eq!(read_size("Bits{64}", &c).1, 2 * 4 + 8);
1728 assert_eq!(read_size("Trend[2]{3}", &c).1, 3 * 4 + 8);
1729 }
1730
1731 #[test]
1732 fn between_runs_after_each_chunk_but_the_last() {
1733 let mut fake = Fake::new(catalog());
1734 let c = fake.catalog.clone();
1735 let tags = [
1736 "Line1.Motor.Speed",
1737 "Line1.Motor.Current",
1738 "Speed",
1739 "Count",
1740 "Trend[1]",
1741 "Trend[2]",
1742 "Flags",
1743 "Name1",
1744 ];
1745 let planned = plan(&owned(&tags), &c, 1500, &set(&[]), 100, None);
1746 let count = planned.len();
1747 let parts = chunks(planned, &c, 100);
1748 assert!(parts.len() >= 3);
1749 let log = Arc::clone(&fake.log);
1750 let mut given = HashMap::new();
1751 let (out, failed, services) = execute(&mut fake, &parts, &mut |_, part| {
1752 let mut names: Vec<String> = part.keys().cloned().collect();
1753 names.sort();
1754 log.lock().expect("log").push(Event::Between(names));
1755 given.extend(part);
1756 Ok(())
1757 })
1758 .expect("execute");
1759 let mut expected = Vec::new();
1760 for (i, part) in parts.iter().enumerate() {
1761 expected.push(Event::Read(part.iter().map(|r| r.name.clone()).collect()));
1762 if i + 1 < parts.len() {
1763 let mut names: Vec<String> = part
1764 .iter()
1765 .flat_map(|r| r.members.iter().map(|(t, _)| t.clone()))
1766 .collect();
1767 names.sort();
1768 expected.push(Event::Between(names));
1769 }
1770 }
1771 assert_eq!(*fake.log.lock().expect("log"), expected);
1772 assert!(out.keys().all(|t| !given.contains_key(t)));
1773 given.extend(out);
1774 assert_eq!(given.keys().cloned().collect::<HashSet<_>>(), set(&tags));
1775 assert_eq!(got(given), direct(&mut fake, &tags));
1776 assert_eq!((services, failed.len()), (count, 0));
1777 }
1778
1779 #[test]
1780 fn failed_group_is_read_after_the_last_chunk() {
1781 let mut fake = Fake::new(catalog());
1782 let c = fake.catalog.clone();
1783 fake.errors
1784 .insert("Line1.Motor".into(), "General error".into());
1785 let tags = [
1786 "Line1.Motor.Speed",
1787 "Line1.Motor.Current",
1788 "Speed",
1789 "Count",
1790 "Flags",
1791 "Name1",
1792 ];
1793 let parts = chunks(plan(&owned(&tags), &c, 1500, &set(&[]), 150, None), &c, 150);
1794 assert!(parts.len() >= 2 && parts[0][0].name == "Line1.Motor");
1795 let mut given = HashMap::new();
1796 let (out, failed, _) = execute(&mut fake, &parts, &mut |_, part| {
1797 given.extend(part);
1798 Ok(())
1799 })
1800 .expect("execute");
1801 let sent = fake.reads();
1802 assert_eq!(sent.len(), parts.len() + 1);
1803 assert_eq!(
1804 sent.last(),
1805 Some(&owned(&["Line1.Motor.Speed", "Line1.Motor.Current"]))
1806 );
1807 assert_eq!(failed, owned(&["Line1.Motor"]));
1808 assert!(out.contains_key("Line1.Motor.Speed") && !given.contains_key("Line1.Motor.Speed"));
1809 given.extend(out);
1810 assert_eq!(got(given), direct(&mut fake, &tags));
1811 }
1812
1813 #[test]
1814 fn execute_takes_a_plain_plan_or_nothing() {
1815 let mut fake = Fake::new(catalog());
1816 let c = fake.catalog.clone();
1817 let planned = plan(
1818 &owned(&["Speed", "Count"]),
1819 &c,
1820 1500,
1821 &set(&[]),
1822 PACKET_BYTES,
1823 None,
1824 );
1825 let mut never =
1826 |_: &mut dyn Plc, _| -> Result<(), String> { panic!("one chunk: no between") };
1827 let (out, failed, services) = execute(&mut fake, &[planned], &mut never).expect("execute");
1828 let values: HashMap<String, Option<Value>> =
1829 out.into_iter().map(|(t, r)| (t, r.value)).collect();
1830 let expected = HashMap::from([
1831 ("Speed".to_owned(), Some(Value::Real(3.5))),
1832 ("Count".to_owned(), Some(Value::DInt(9))),
1833 ]);
1834 assert_eq!(values, expected);
1835 assert_eq!((failed.len(), services), (0, 2));
1836 let (out, failed, services) = execute(&mut fake, &[], &mut never).expect("execute");
1837 assert!(out.is_empty() && failed.is_empty() && services == 0);
1838 }
1839
1840 #[test]
1841 fn execute_passes_on_a_lost_connection() {
1842 let mut fake = Fake::new(catalog());
1843 let parts = vec![vec![Read {
1844 name: "Speed".into(),
1845 members: vec![("Speed".into(), None)],
1846 }]];
1847 struct Lost<'a>(&'a mut Fake);
1848 impl Plc for Lost<'_> {
1849 fn open(&mut self) -> Result<(), String> {
1850 Ok(())
1851 }
1852 fn close(&mut self) {}
1853 fn read(&mut self, _: &[&str]) -> Result<Vec<TagResult>, String> {
1854 Err("connection lost".into())
1855 }
1856 fn write(&mut self, tag: &str, value: &Value) -> Result<TagResult, String> {
1857 self.0.write(tag, value)
1858 }
1859 fn catalog(&mut self) -> Catalog {
1860 self.0.catalog()
1861 }
1862 fn version(&mut self) -> Json {
1863 Json::Null
1864 }
1865 fn connection_size(&self) -> usize {
1866 PACKET_BYTES
1867 }
1868 }
1869 let result = execute(&mut Lost(&mut fake), &parts, &mut |_, _| Ok(()));
1870 assert_eq!(result, Err("connection lost".into()));
1871 }
1872
1873 #[test]
1874 fn request_size_follows_the_instance_id_and_assumes_16_bits() {
1875 let tag = |id| Node {
1876 instance_id: Some(id),
1877 ..atomic("DINT", &[])
1878 };
1879 let tags = [("small", tag(5)), ("mid", tag(300)), ("big", tag(70000))];
1880 let tags = tags.into_iter().map(|(k, v)| (k.to_owned(), v)).collect();
1881 let c = Catalog::new(Json::Null, tags, IndexMap::new());
1882 let got: Vec<usize> = ["small", "mid", "big"]
1883 .iter()
1884 .map(|n| request_size(&format!("{n}.Level"), &c))
1885 .collect();
1886 assert_eq!(got, vec![18, 20, 22]);
1887 assert_eq!(request_size("Motor7.Level", &c), 20); }
1889
1890 #[test]
1891 fn a_run_that_turns_fragmented_is_not_taken_for_free() {
1892 let tags = IndexMap::from([("Trend".to_owned(), atomic("REAL", &[2000]))]);
1893 let c = Catalog::new(Json::Null, tags, IndexMap::new());
1894 let tags: Vec<String> = (0..1000)
1895 .step_by(2)
1896 .map(|i| format!("Trend[{i}]"))
1897 .collect();
1898 assert_eq!(packets(&sizes(&["Trend[0]{999}"], &c), 4000), 2 + 2);
1900 assert_eq!(
1901 plan(&tags, &c, 1500, &set(&[]), 4000, None).len(),
1902 tags.len()
1903 );
1904 }
1905
1906 #[test]
1907 fn dig_slices_members_ignoring_case_and_elements() {
1908 let value = station(1);
1909 let path = [Key::Member("motor".into()), Key::Member("SPEED".into())];
1910 assert_eq!(dig(&value, &path), Some(Value::Real(1.5)));
1911 assert_eq!(
1912 dig(&value, &[Key::Member("Data".into()), Key::Index(3)]),
1913 Some(Value::DInt(4))
1914 );
1915 assert_eq!(
1916 dig(&value, &[Key::Member("Data".into()), Key::Index(4)]),
1917 None
1918 );
1919 assert_eq!(dig(&value, &[Key::Index(0)]), None); assert_eq!(dig(&Value::DInt(5), &[Key::Member("Speed".into())]), None); assert_eq!(dig(&value, &[]), Some(value.clone()));
1922 }
1923}