1use ciplink::cip::TagResult;
4use ciplink::{ErrorKind, Value};
5use serde_json::{Map, Number, Value as Json, json};
6
7pub 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#[derive(Clone, Debug, PartialEq)]
16pub struct Entry {
17 pub value: Json,
19 pub quality: &'static str,
21 pub t: i64,
23 pub error: Json,
25}
26
27impl Entry {
28 pub fn to_json(&self) -> Json {
30 json!([self.value, self.quality, self.t, self.error])
31 }
32
33 pub fn same(&self, other: &Entry) -> bool {
35 self.value == other.value && self.quality == other.quality && self.error == other.error
36 }
37}
38
39pub 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
48pub 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
92pub 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
118pub 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
138pub 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 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 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}