Skip to main content

ciplink_server/
values.rs

1//! Turns driver results into JSON-ready cache entries, and JSON write values into driver values.
2
3use ciplink::cip::TagResult;
4use ciplink::{ErrorKind, Value};
5use serde_json::{Map, Number, Value as Json, json};
6
7/// Milliseconds since the epoch.
8pub fn now_ms() -> i64 {
9    std::time::SystemTime::now()
10        .duration_since(std::time::UNIX_EPOCH)
11        .map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX))
12}
13
14/// A cache entry, sent as `[value, quality, t, error]`.
15#[derive(Clone, Debug, PartialEq)]
16pub struct Entry {
17    /// The value, null unless good (or bad/stale with a kept value).
18    pub value: Json,
19    /// `good`, `missing`, `bad` or `stale`.
20    pub quality: &'static str,
21    /// Milliseconds since the epoch of the read.
22    pub t: i64,
23    /// Null when good, else `{"kind", "message"}`.
24    pub error: Json,
25}
26
27impl Entry {
28    /// The entry as the protocol sends it.
29    pub fn to_json(&self) -> Json {
30        json!([self.value, self.quality, self.t, self.error])
31    }
32
33    /// What decides whether a client is told: value, quality and error, not the time.
34    pub fn same(&self, other: &Entry) -> bool {
35        self.value == other.value && self.quality == other.quality && self.error == other.error
36    }
37}
38
39/// good, missing (no such tag or not accessible) or bad, from an error kind.
40pub fn quality(kind: Option<ErrorKind>) -> &'static str {
41    match kind {
42        None => "good",
43        Some(ErrorKind::NotFound | ErrorKind::Access) => "missing",
44        Some(_) => "bad",
45    }
46}
47
48/// Objects for structs, lists for arrays, bytes as hex, non-finite floats as strings.
49pub fn jsonable(value: &Value) -> Json {
50    match value {
51        Value::Bool(b) => Json::Bool(*b),
52        Value::SInt(v) => json!(v),
53        Value::Int(v) => json!(v),
54        Value::DInt(v) => json!(v),
55        Value::LInt(v) => json!(v),
56        Value::USInt(v) => json!(v),
57        Value::UInt(v) => json!(v),
58        Value::UDInt(v) => json!(v),
59        Value::ULInt(v) => json!(v),
60        Value::Real(v) => float(f64::from(*v)),
61        Value::LReal(v) => float(*v),
62        Value::String(s) => Json::String(s.clone()),
63        Value::Bytes(b) => Json::String(b.iter().map(|x| format!("{x:02x}")).collect()),
64        Value::Array(items) => Json::Array(items.iter().map(jsonable).collect()),
65        Value::Struct(members) => Json::Object(
66            members
67                .iter()
68                .map(|(k, v)| (k.clone(), jsonable(v)))
69                .collect(),
70        ),
71    }
72}
73
74fn float(v: f64) -> Json {
75    Number::from_f64(v).map_or_else(
76        || {
77            Json::String(
78                if v.is_nan() {
79                    "NaN"
80                } else if v > 0.0 {
81                    "Infinity"
82                } else {
83                    "-Infinity"
84                }
85                .into(),
86            )
87        },
88        Json::Number,
89    )
90}
91
92/// A JSON write value as a driver value: integers stay integers, other numbers are LREAL; the driver checks
93/// it against the tag's type. None for null.
94pub fn from_json(value: &Json) -> Option<Value> {
95    Some(match value {
96        Json::Null => return None,
97        Json::Bool(b) => Value::Bool(*b),
98        Json::Number(n) => {
99            if let Some(i) = n.as_i64() {
100                Value::LInt(i)
101            } else if let Some(u) = n.as_u64() {
102                Value::ULInt(u)
103            } else {
104                Value::LReal(n.as_f64()?)
105            }
106        }
107        Json::String(s) => Value::String(s.clone()),
108        Json::Array(items) => Value::Array(items.iter().map(from_json).collect::<Option<_>>()?),
109        Json::Object(members) => Value::Struct(
110            members
111                .iter()
112                .map(|(k, v)| Some((k.clone(), from_json(v)?)))
113                .collect::<Option<_>>()?,
114        ),
115    })
116}
117
118/// (value, quality, error) of a driver result; the value is null unless the read succeeded.
119pub fn to_value(result: &TagResult) -> (Json, &'static str, Json) {
120    let kind = result.error.as_ref().map(|e| e.kind);
121    let q = quality(kind);
122    let value = match (&result.value, q) {
123        (Some(v), "good") => jsonable(v),
124        _ => Json::Null,
125    };
126    let error = match &result.error {
127        None => Json::Null,
128        Some(e) => {
129            let mut m = Map::new();
130            m.insert("kind".into(), json!(e.kind.as_str()));
131            m.insert("message".into(), json!(e.message));
132            Json::Object(m)
133        }
134    };
135    (value, q, error)
136}
137
138/// The cache entry of a read result; `bad` keeps the value of `old`, so a screen shows the last value marked bad.
139pub fn merge(old: Option<&Entry>, result: &TagResult, t: i64) -> Entry {
140    let (mut value, quality, error) = to_value(result);
141    if quality == "bad" {
142        if let Some(old) = old {
143            value = old.value.clone();
144        }
145    }
146    Entry {
147        value,
148        quality,
149        t,
150        error,
151    }
152}
153
154#[cfg(test)]
155#[allow(clippy::unwrap_used)]
156mod tests {
157    use super::*;
158    use ciplink::TagError;
159
160    fn result(value: Option<Value>, kind: Option<ErrorKind>) -> TagResult {
161        TagResult {
162            tag: "T".into(),
163            value,
164            data_type: None,
165            error: kind.map(|k| TagError::new("boom", k)),
166        }
167    }
168
169    #[test]
170    fn json_forms() {
171        assert_eq!(jsonable(&Value::Real(3.25)), json!(3.25));
172        assert_eq!(jsonable(&Value::LReal(f64::NAN)), json!("NaN"));
173        assert_eq!(
174            jsonable(&Value::Real(f32::NEG_INFINITY)),
175            json!("-Infinity")
176        );
177        assert_eq!(jsonable(&Value::Bytes(vec![0, 0xab])), json!("00ab"));
178        assert_eq!(jsonable(&Value::ULInt(u64::MAX)), json!(u64::MAX));
179    }
180
181    #[test]
182    fn write_values() {
183        assert_eq!(from_json(&json!(5)), Some(Value::LInt(5)));
184        assert_eq!(from_json(&json!(1.5)), Some(Value::LReal(1.5)));
185        assert_eq!(
186            from_json(&json!([true])),
187            Some(Value::Array(vec![Value::Bool(true)]))
188        );
189        assert_eq!(from_json(&Json::Null), None);
190    }
191
192    #[test]
193    fn quality_and_bad_keeps_the_last_value() {
194        let good = merge(None, &result(Some(Value::DInt(4)), None), 1);
195        assert_eq!(good.to_json(), json!([4, "good", 1, null]));
196        let bad = merge(Some(&good), &result(None, Some(ErrorKind::Comm)), 2);
197        assert_eq!(
198            bad.to_json(),
199            json!([4, "bad", 2, {"kind": "comm", "message": "boom"}])
200        );
201        let missing = merge(Some(&good), &result(None, Some(ErrorKind::NotFound)), 3);
202        assert_eq!(missing.value, Json::Null);
203        assert_eq!(missing.quality, "missing");
204    }
205    // ---- ports of ciplink-tagserver's tests/test_config_values.py
206
207    fn with_error(error: Option<TagError>, value: Option<Value>) -> TagResult {
208        TagResult {
209            tag: "x".into(),
210            value,
211            data_type: Some("DINT".into()),
212            error,
213        }
214    }
215
216    #[test]
217    fn quality_from_error_kinds() {
218        for (kind, expected) in [
219            (None, "good"),
220            (Some(ErrorKind::NotFound), "missing"),
221            (Some(ErrorKind::Access), "missing"),
222            (Some(ErrorKind::BadValue), "bad"),
223            (Some(ErrorKind::Refused), "bad"),
224            (Some(ErrorKind::TableChanged), "bad"),
225            (Some(ErrorKind::Comm), "bad"),
226            (Some(ErrorKind::Other), "bad"),
227        ] {
228            let r = with_error(kind.map(|k| TagError::new("boom", k)), Some(Value::DInt(5)));
229            assert_eq!(to_value(&r).1, expected);
230        }
231    }
232
233    #[test]
234    fn quality_never_reads_message_text() {
235        // a plain string error is kind other
236        let r = with_error(
237            Some(TagError::new("Tag doesn't exist - Foo", ErrorKind::Other)),
238            Some(Value::DInt(5)),
239        );
240        assert_eq!(to_value(&r).1, "bad");
241        let r = with_error(
242            Some(TagError::new("General failure", ErrorKind::Other)),
243            Some(Value::DInt(5)),
244        );
245        assert_eq!(
246            to_value(&r).2,
247            json!({"kind": "other", "message": "General failure"})
248        );
249    }
250
251    #[test]
252    fn to_value_and_merge() {
253        let missing = with_error(
254            Some(TagError::new("Tag doesn't exist - x", ErrorKind::NotFound)),
255            Some(Value::DInt(5)),
256        );
257        let bad = with_error(
258            Some(TagError::new("Service not supported", ErrorKind::Device)),
259            Some(Value::DInt(5)),
260        );
261        let device = json!({"kind": "device", "message": "Service not supported"});
262        assert_eq!(
263            to_value(&with_error(None, Some(Value::DInt(5)))),
264            (json!(5), "good", Json::Null)
265        );
266        assert_eq!(to_value(&bad), (Json::Null, "bad", device.clone()));
267        let seven = merge(None, &with_error(None, Some(Value::DInt(7))), 10);
268        assert_eq!(seven.to_json(), json!([7, "good", 10, null]));
269        assert_eq!(
270            merge(Some(&seven), &bad, 11).to_json(),
271            json!([7, "bad", 11, device])
272        );
273        let first_bad = merge(None, &bad, 11);
274        assert_eq!((first_bad.value, first_bad.quality), (Json::Null, "bad"));
275        assert_eq!(
276            merge(Some(&seven), &missing, 12).to_json(),
277            json!([null, "missing", 12, {"kind": "not_found", "message": "Tag doesn't exist - x"}])
278        );
279    }
280
281    #[test]
282    fn jsonable_values() {
283        let value = Value::Struct(
284            [
285                ("A", Value::Array(vec![Value::DInt(1), Value::DInt(2)])),
286                ("B", Value::Bytes(vec![0x01, 0xff])),
287                (
288                    "C",
289                    Value::Struct(
290                        [("D".to_owned(), Value::LReal(f64::NAN))]
291                            .into_iter()
292                            .collect(),
293                    ),
294                ),
295                ("E", Value::LReal(f64::NEG_INFINITY)),
296                ("F", Value::Bool(true)),
297            ]
298            .into_iter()
299            .map(|(k, v)| (k.to_owned(), v))
300            .collect(),
301        );
302        assert_eq!(
303            jsonable(&value),
304            json!({"A": [1, 2], "B": "01ff", "C": {"D": "NaN"}, "E": "-Infinity", "F": true})
305        );
306    }
307}