1use std::collections::HashMap;
5
6use ciplink::Value;
7use ciplink::cip::TagResult;
8use ciplink::logix::driver::{LogixDriver, LogixOptions};
9use ciplink::logix::metadata::TextEncoding;
10use ciplink::logix::types::TypeRef;
11use indexmap::IndexMap;
12use serde_json::{Map, Value as Json, json};
13
14use crate::config::PlcConfig;
15
16pub trait Plc: Send {
19 fn open(&mut self) -> Result<(), String>;
21 fn close(&mut self);
23 fn read(&mut self, tags: &[&str]) -> Result<Vec<TagResult>, String>;
25 fn write(&mut self, tag: &str, value: &Value) -> Result<TagResult, String>;
27 fn catalog(&mut self) -> Catalog;
29 fn version(&mut self) -> Json;
31 fn connection_size(&self) -> usize;
33 fn describes(&self) -> bool {
35 false
36 }
37 fn descriptions(&mut self, tags: &[&str]) -> Result<Vec<Option<String>>, String> {
39 Ok(vec![None; tags.len()])
40 }
41}
42
43pub type Factory = std::sync::Arc<dyn Fn(&PlcConfig) -> Result<Box<dyn Plc>, String> + Send + Sync>;
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Kind {
49 Atomic,
51 Struct,
53 String,
55}
56
57#[derive(Clone, Debug, PartialEq)]
59pub struct Node {
60 pub type_name: String,
62 pub kind: Kind,
64 pub dims: Vec<u64>,
67 pub member: bool,
69 pub instance_id: Option<u32>,
71 pub structure_size: Option<u64>,
73}
74
75impl Node {
76 pub fn bool_array(&self) -> bool {
78 self.type_name == "DWORD" && !self.dims.is_empty()
79 }
80
81 pub fn client_type(&self) -> &str {
83 if self.kind == Kind::String {
84 "STRING"
85 } else if self.bool_array() {
86 "BOOL"
87 } else {
88 &self.type_name
89 }
90 }
91
92 pub fn client_dims(&self) -> Vec<u64> {
94 let mut dims = self.dims.clone();
95 if self.bool_array() {
96 if let Some(last) = dims.last_mut() {
97 *last *= 32;
98 }
99 }
100 dims
101 }
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
106pub struct Aoi {
107 pub whole_read: bool,
109 pub readable: Vec<String>,
111}
112
113#[derive(Clone, Debug, Default, PartialEq)]
115pub struct TypeDef {
116 pub members: IndexMap<String, Node>,
118 pub public: Vec<String>,
120 pub structure_size: u64,
122 pub aoi: Option<Aoi>,
124 pub whole_readable: bool,
126}
127
128#[derive(Clone, Debug, Default, PartialEq)]
130pub struct Catalog {
131 pub version: Json,
133 pub tags: IndexMap<String, Node>,
135 pub types: IndexMap<String, TypeDef>,
137 index: HashMap<String, String>,
138}
139
140pub fn atomic_size(type_name: &str) -> Option<u64> {
142 Some(match type_name {
143 "BOOL" | "SINT" | "USINT" | "BYTE" => 1,
144 "INT" | "UINT" | "WORD" => 2,
145 "DINT" | "UDINT" | "DWORD" | "REAL" => 4,
146 "LINT" | "ULINT" | "LWORD" | "LREAL" => 8,
147 _ => return None,
148 })
149}
150
151impl Catalog {
152 pub fn new(
154 version: Json,
155 tags: IndexMap<String, Node>,
156 mut types: IndexMap<String, TypeDef>,
157 ) -> Self {
158 let mut memo = HashMap::new();
159 let names: Vec<String> = types.keys().cloned().collect();
160 for name in &names {
161 whole_readable(name, &types, &mut memo);
162 }
163 for (name, t) in &mut types {
164 t.whole_readable = memo.get(name).copied().unwrap_or(true);
165 }
166 let index = tags.keys().map(|k| (k.to_lowercase(), k.clone())).collect();
167 Catalog {
168 version,
169 tags,
170 types,
171 index,
172 }
173 }
174
175 pub fn tag(&self, name: &str) -> Option<&Node> {
177 self.tags
178 .get(name)
179 .or_else(|| self.tags.get(self.index.get(&name.to_lowercase())?))
180 }
181
182 pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
184 self.types.get(name)
185 }
186
187 pub fn info(&self, name: &str) -> Option<&Node> {
190 if let Some(node) = self.tag(name) {
191 return Some(node); }
193 let (prefix, rest) = match name.find('.') {
194 Some(dot)
195 if name.len() >= 8
196 && name.is_char_boundary(8)
197 && name[..8].eq_ignore_ascii_case("program:") =>
198 {
199 (&name[..=dot], &name[dot + 1..])
200 }
201 _ => ("", name),
202 };
203 let stripped = strip(rest);
204 let mut attrs = stripped.split('.');
205 let base = attrs.next()?;
206 let mut node = self.tag(&format!("{prefix}{base}"))?;
207 for attr in attrs {
208 let t = self.types.get(&node.type_name)?;
209 node = t.members.get(attr).or_else(|| {
210 t.members
211 .iter()
212 .find(|(k, _)| k.eq_ignore_ascii_case(attr))
213 .map(|(_, v)| v)
214 })?;
215 }
216 Some(node)
217 }
218
219 pub fn struct_size(&self, node: &Node) -> Option<u64> {
221 if node.kind != Kind::Struct {
222 return None;
223 }
224 let t = self.types.get(&node.type_name)?;
225 t.whole_readable.then_some(t.structure_size)
226 }
227
228 pub fn tag_list(&self) -> Vec<Json> {
230 self.tags
231 .iter()
232 .map(|(name, node)| {
233 let mut entry = Map::new();
234 entry.insert("name".into(), json!(name));
235 entry.insert("type".into(), json!(node.client_type()));
236 if !node.dims.is_empty() {
237 entry.insert("dims".into(), json!(node.client_dims()));
238 }
239 if node.kind == Kind::Struct {
240 if let Some(aoi) = self.types.get(&node.type_name).and_then(|t| t.aoi.as_ref())
241 {
242 entry.insert("aoi_readable".into(), json!(aoi.readable));
243 }
244 }
245 Json::Object(entry)
246 })
247 .collect()
248 }
249
250 pub fn type_map(&self) -> Map<String, Json> {
253 self.types
254 .iter()
255 .map(|(name, t)| {
256 let names = t.aoi.as_ref().map_or(&t.public, |a| &a.readable);
257 let members: Map<String, Json> = names
258 .iter()
259 .map(|m| {
260 let mut member = Map::new();
261 match t.members.get(m) {
262 Some(node) => {
263 member.insert("type".into(), json!(node.client_type()));
264 if !node.dims.is_empty() {
265 member.insert("dims".into(), json!(node.client_dims()));
266 }
267 }
268 None => {
269 member.insert("type".into(), json!(""));
270 }
271 }
272 (m.clone(), Json::Object(member))
273 })
274 .collect();
275 let mut entry = Map::new();
276 entry.insert("members".into(), Json::Object(members));
277 if t.aoi.is_some() {
278 entry.insert("aoi".into(), json!(true));
279 }
280 (name.clone(), Json::Object(entry))
281 })
282 .collect()
283 }
284}
285
286fn whole_readable(
287 name: &str,
288 types: &IndexMap<String, TypeDef>,
289 memo: &mut HashMap<String, bool>,
290) -> bool {
291 if let Some(ok) = memo.get(name) {
292 return *ok;
293 }
294 memo.insert(name.to_owned(), true); let Some(t) = types.get(name) else {
296 return true;
297 };
298 let mut ok = !t.aoi.as_ref().is_some_and(|a| !a.whole_read);
299 for member in t.members.values() {
300 if ok && member.kind == Kind::Struct {
301 ok = whole_readable(&member.type_name, types, memo);
302 }
303 }
304 memo.insert(name.to_owned(), ok);
305 ok
306}
307
308fn strip(path: &str) -> String {
310 let mut out = String::with_capacity(path.len());
311 let mut skip = None;
312 for c in path.chars() {
313 match (skip, c) {
314 (None, '[') => skip = Some(']'),
315 (None, '{') => skip = Some('}'),
316 (Some(end), c) if c == end => skip = None,
317 (Some(_), _) => {}
318 (None, c) => out.push(c),
319 }
320 }
321 out
322}
323
324pub struct LogixPlc {
328 driver: LogixDriver,
329}
330
331impl LogixPlc {
332 pub fn new(cfg: &PlcConfig) -> Result<Self, String> {
334 let mut options = LogixOptions::default();
335 if let Some(cache) = &cfg.tag_cache_file {
336 if let Some(dir) = cache.parent() {
337 let _ = std::fs::create_dir_all(dir);
338 }
339 options.tag_cache_file = Some(cache.clone());
340 }
341 let driver = LogixDriver::new(&cfg.path, options).map_err(|e| e.to_string())?;
342 Ok(LogixPlc { driver })
343 }
344
345 pub fn factory() -> Factory {
347 std::sync::Arc::new(|cfg: &PlcConfig| Ok(Box::new(LogixPlc::new(cfg)?) as Box<dyn Plc>))
348 }
349}
350
351fn node_of(data_type: &TypeRef, dims: Vec<u64>, member: bool, instance_id: Option<u32>) -> Node {
352 let (kind, structure_size) = match data_type {
353 TypeRef::Atomic(_) => (Kind::Atomic, None),
354 TypeRef::Struct(dt) => (
355 if dt.string.is_some() {
356 Kind::String
357 } else {
358 Kind::Struct
359 },
360 Some(u64::from(dt.template.structure_size)),
361 ),
362 };
363 Node {
364 type_name: data_type.name().unwrap_or_default().to_owned(),
365 kind,
366 dims,
367 member,
368 instance_id,
369 structure_size,
370 }
371}
372
373impl Plc for LogixPlc {
374 fn open(&mut self) -> Result<(), String> {
375 self.driver.open().map(|_| ()).map_err(|e| e.to_string())
376 }
377
378 fn close(&mut self) {
379 let _ = self.driver.close();
380 }
381
382 fn read(&mut self, tags: &[&str]) -> Result<Vec<TagResult>, String> {
383 self.driver.read(tags).map_err(|e| e.to_string())
384 }
385
386 fn write(&mut self, tag: &str, value: &Value) -> Result<TagResult, String> {
387 let mut results = self
388 .driver
389 .write(&[(tag, value.clone())])
390 .map_err(|e| e.to_string())?;
391 results.pop().ok_or_else(|| "no write result".to_owned())
392 }
393
394 fn catalog(&mut self) -> Catalog {
395 let defs = self.driver.definitions();
396 let probes: HashMap<String, Aoi> = defs
397 .probes
398 .iter()
399 .filter_map(|(dt, probe)| {
400 Some((
401 dt.name.clone()?,
402 Aoi {
403 whole_read: probe.whole_read,
404 readable: probe.readable.clone(),
405 },
406 ))
407 })
408 .collect();
409 let tags = defs
410 .tags
411 .iter()
412 .map(|(name, t)| {
413 let dims = t.dimensions[..usize::from(t.dim.min(3))]
414 .iter()
415 .map(|&d| u64::from(d))
416 .collect();
417 (
418 name.clone(),
419 node_of(&t.data_type, dims, false, Some(t.instance_id)),
420 )
421 })
422 .collect();
423 let mut types = IndexMap::new();
424 for (name, dt) in &defs.data_types {
425 let Some(name) = name else { continue };
426 if dt.string.is_some() {
427 continue;
428 }
429 let members = dt
430 .internal_tags
431 .iter()
432 .map(|(m, info)| {
433 let dims = info
434 .array
435 .filter(|&a| a > 0)
436 .map(|a| vec![u64::from(a)])
437 .unwrap_or_default();
438 (m.clone(), node_of(&info.data_type, dims, true, None))
439 })
440 .collect();
441 types.insert(
442 name.clone(),
443 TypeDef {
444 members,
445 public: dt.attributes.clone(),
446 structure_size: u64::from(dt.template.structure_size),
447 aoi: probes.get(name).cloned(),
448 whole_readable: true,
449 },
450 );
451 }
452 Catalog::new(self.version(), tags, types)
453 }
454
455 fn version(&mut self) -> Json {
456 self.driver
457 .tag_table_version()
458 .map_or(Json::Null, |v| json!([v.0, v.1, v.2, v.3, v.4]))
459 }
460
461 fn connection_size(&self) -> usize {
462 usize::from(self.driver.connection_size())
463 }
464
465 fn describes(&self) -> bool {
466 true
467 }
468
469 fn descriptions(&mut self, tags: &[&str]) -> Result<Vec<Option<String>>, String> {
470 let results = self
471 .driver
472 .get_tag_descriptions(tags, 0x007F, TextEncoding::Utf8)
473 .map_err(|e| e.to_string())?;
474 Ok(results
475 .into_iter()
476 .map(|r| match (r.error, r.value) {
477 (None, Some(Value::String(s))) => Some(s),
478 _ => None,
479 })
480 .collect())
481 }
482}
483
484#[cfg(test)]
485#[allow(clippy::unwrap_used)]
486mod tests {
487 use super::*;
488
489 fn atomic(t: &str, dims: Vec<u64>, member: bool) -> Node {
490 Node {
491 type_name: t.into(),
492 kind: Kind::Atomic,
493 dims,
494 member,
495 instance_id: None,
496 structure_size: None,
497 }
498 }
499
500 fn catalog() -> Catalog {
501 let mut udt = TypeDef {
502 public: vec!["Flags".into(), "Speed".into()],
503 structure_size: 8,
504 ..TypeDef::default()
505 };
506 udt.members
507 .insert("Flags".into(), atomic("DWORD", vec![1], true));
508 udt.members
509 .insert("Speed".into(), atomic("REAL", vec![], true));
510 let mut tags = IndexMap::new();
511 tags.insert("Bits".into(), atomic("DWORD", vec![3], false)); tags.insert("Word".into(), atomic("DWORD", vec![], false));
513 tags.insert(
514 "Motor".into(),
515 Node {
516 type_name: "MotorData".into(),
517 kind: Kind::Struct,
518 dims: vec![],
519 member: false,
520 instance_id: Some(5),
521 structure_size: Some(8),
522 },
523 );
524 let mut types = IndexMap::new();
525 types.insert("MotorData".into(), udt);
526 Catalog::new(Json::Null, tags, types)
527 }
528
529 #[test]
530 fn bool_arrays_and_bit_strings() {
531 let c = catalog();
532 assert_eq!(
533 c.tag_list(),
534 vec![
535 json!({"name": "Bits", "type": "BOOL", "dims": [96]}),
536 json!({"name": "Word", "type": "DWORD"}),
537 json!({"name": "Motor", "type": "MotorData"}),
538 ]
539 );
540 assert_eq!(
541 Json::Object(c.type_map()),
542 json!({"MotorData": {"members": {"Flags": {"type": "BOOL", "dims": [32]}, "Speed": {"type": "REAL"}}}})
543 );
544 }
545
546 #[test]
547 fn info_ignores_case_and_indexes() {
548 let c = catalog();
549 assert_eq!(
550 c.info("motor.SPEED").map(|n| n.type_name.as_str()),
551 Some("REAL")
552 );
553 assert_eq!(
554 c.info("Motor.Flags[3]").map(|n| n.type_name.as_str()),
555 Some("DWORD")
556 );
557 assert_eq!(c.info("Bits{4}").map(|n| n.dims.clone()), Some(vec![3]));
558 assert!(c.info("Motor.Nope").is_none());
559 assert_eq!(c.struct_size(c.info("Motor").unwrap()), Some(8));
560 }
561 fn strct(t: &str, dims: Vec<u64>) -> Node {
564 Node {
565 kind: Kind::Struct,
566 structure_size: Some(8),
567 ..atomic(t, dims, false)
568 }
569 }
570
571 fn type_def(members: Vec<(&str, Node)>, public: &[&str], aoi: Option<Aoi>) -> TypeDef {
572 TypeDef {
573 members: members
574 .into_iter()
575 .map(|(m, n)| (m.to_owned(), Node { member: true, ..n }))
576 .collect(),
577 public: public.iter().map(|p| (*p).to_owned()).collect(),
578 structure_size: 8,
579 aoi,
580 whole_readable: true,
581 }
582 }
583
584 #[test]
585 fn tag_list_with_aoi() {
586 let mut tags = IndexMap::new();
587 tags.insert("Speed".to_owned(), atomic("REAL", vec![], false));
588 tags.insert("Valve1".to_owned(), strct("MyAOI", vec![]));
589 tags.insert("Recipe".to_owned(), strct("UDT_R", vec![]));
590 let mut types = IndexMap::new();
591 let aoi = Aoi {
592 whole_read: false,
593 readable: vec!["Open".into(), "Cmd".into()],
594 };
595 let bools = vec![
596 ("Open", atomic("BOOL", vec![], true)),
597 ("Cmd", atomic("BOOL", vec![], true)),
598 ("Local1", atomic("BOOL", vec![], true)),
599 ];
600 types.insert(
601 "MyAOI".to_owned(),
602 type_def(bools, &["Open", "Cmd", "Local1"], Some(aoi)),
603 );
604 types.insert("UDT_R".to_owned(), type_def(vec![], &[], None));
605 let c = Catalog::new(Json::Null, tags, types);
606 assert_eq!(
607 c.tag_list(),
608 [
609 json!({"name": "Speed", "type": "REAL"}),
610 json!({"name": "Valve1", "type": "MyAOI", "aoi_readable": ["Open", "Cmd"]}),
611 json!({"name": "Recipe", "type": "UDT_R"}),
612 ]
613 );
614 }
615
616 #[test]
617 fn tag_list_dims_and_strings_and_type_map() {
618 let string = Node {
619 kind: Kind::String,
620 ..strct("STR20", vec![])
621 };
622 let mut tags = IndexMap::new();
623 tags.insert("Tanks".to_owned(), strct("Tank", vec![4]));
624 tags.insert("Grid".to_owned(), atomic("DINT", vec![3, 5], false));
625 tags.insert("Msg".to_owned(), string.clone());
626 let mut types = IndexMap::new();
628 let tank = vec![
629 ("ZZZZZZZZZZTank0", atomic("SINT", vec![], true)),
630 ("Level", atomic("REAL", vec![], true)),
631 (
632 "Name",
633 Node {
634 type_name: "STRING".into(),
635 ..string
636 },
637 ),
638 ("Hist", atomic("REAL", vec![8], true)),
639 ];
640 types.insert(
641 "Tank".to_owned(),
642 type_def(tank, &["Level", "Name", "Hist"], None),
643 );
644 let old = vec![
645 ("__pad", atomic("SINT", vec![], true)),
646 ("A", atomic("DINT", vec![], true)),
647 ];
648 types.insert("Old".to_owned(), type_def(old, &["A"], None));
649 let valve = vec![
650 ("Open", atomic("BOOL", vec![], true)),
651 ("Cmd", atomic("BOOL", vec![], true)),
652 ];
653 let aoi = Aoi {
654 whole_read: true,
655 readable: vec!["Open".into()],
656 };
657 types.insert(
658 "Valve_AOI".to_owned(),
659 type_def(valve, &["Open", "Cmd"], Some(aoi)),
660 );
661 let c = Catalog::new(Json::Null, tags, types);
662 assert_eq!(
663 c.tag_list(),
664 [
665 json!({"name": "Tanks", "type": "Tank", "dims": [4]}),
666 json!({"name": "Grid", "type": "DINT", "dims": [3, 5]}),
667 json!({"name": "Msg", "type": "STRING"}),
668 ]
669 );
670 assert_eq!(
671 Json::Object(c.type_map()),
672 json!({
673 "Tank": {"members": {"Level": {"type": "REAL"}, "Name": {"type": "STRING"},
674 "Hist": {"type": "REAL", "dims": [8]}}},
675 "Old": {"members": {"A": {"type": "DINT"}}},
676 "Valve_AOI": {"members": {"Open": {"type": "BOOL"}}, "aoi": true},
677 })
678 );
679 }
680}