1use std::sync::Arc;
2use web_time::SystemTime;
3
4#[derive(Clone, Debug, Default, PartialEq)]
6pub enum Value {
7 #[default]
9 None,
10 Double(f64),
11 Long(i64),
12 Str(String),
13 Enum(u16),
15 DoubleArray(Arc<[f64]>),
16 LongArray(Arc<[i64]>),
17 StrArray(Arc<[String]>),
18 Image(Arc<Image>),
20}
21
22#[derive(Clone, Debug, PartialEq)]
24pub struct Image {
25 pub width: usize,
26 pub height: usize,
27 pub rgb: bool,
29 pub data: ImageData,
30 pub unique_id: i64,
31}
32
33#[derive(Clone, Debug, PartialEq)]
35pub enum ImageData {
36 I8(Arc<[i8]>),
37 U8(Arc<[u8]>),
38 I16(Arc<[i16]>),
39 U16(Arc<[u16]>),
40 I32(Arc<[i32]>),
41 U32(Arc<[u32]>),
42 I64(Arc<[i64]>),
43 U64(Arc<[u64]>),
44 F32(Arc<[f32]>),
45 F64(Arc<[f64]>),
46}
47
48impl ImageData {
49 pub fn len(&self) -> usize {
50 match self {
51 ImageData::I8(a) => a.len(),
52 ImageData::U8(a) => a.len(),
53 ImageData::I16(a) => a.len(),
54 ImageData::U16(a) => a.len(),
55 ImageData::I32(a) => a.len(),
56 ImageData::U32(a) => a.len(),
57 ImageData::I64(a) => a.len(),
58 ImageData::U64(a) => a.len(),
59 ImageData::F32(a) => a.len(),
60 ImageData::F64(a) => a.len(),
61 }
62 }
63
64 pub fn is_empty(&self) -> bool {
65 self.len() == 0
66 }
67
68 pub fn get(&self, i: usize, unsigned: bool) -> f64 {
71 match self {
72 ImageData::I8(a) if unsigned => a[i] as u8 as f64,
73 ImageData::I8(a) => a[i] as f64,
74 ImageData::U8(a) => a[i] as f64,
75 ImageData::I16(a) if unsigned => a[i] as u16 as f64,
76 ImageData::I16(a) => a[i] as f64,
77 ImageData::U16(a) => a[i] as f64,
78 ImageData::I32(a) if unsigned => a[i] as u32 as f64,
79 ImageData::I32(a) => a[i] as f64,
80 ImageData::U32(a) => a[i] as f64,
81 ImageData::I64(a) if unsigned => a[i] as u64 as f64,
82 ImageData::I64(a) => a[i] as f64,
83 ImageData::U64(a) => a[i] as f64,
84 ImageData::F32(a) => a[i] as f64,
85 ImageData::F64(a) => a[i],
86 }
87 }
88}
89
90impl Value {
91 pub fn as_f64(&self) -> Option<f64> {
93 match self {
94 Value::Double(v) => Some(*v),
95 Value::Long(v) => Some(*v as f64),
96 Value::Enum(v) => Some(*v as f64),
97 Value::Str(s) => s.trim().parse().ok(),
98 Value::DoubleArray(a) => a.first().copied(),
99 Value::LongArray(a) => a.first().map(|v| *v as f64),
100 Value::StrArray(_) | Value::Image(_) | Value::None => None,
101 }
102 }
103
104 pub fn as_long_string(&self) -> Option<String> {
107 match self {
108 Value::LongArray(a) => Some(long_string(a.iter().map(|c| *c as u8))),
109 Value::DoubleArray(a) => Some(long_string(a.iter().map(|c| *c as i64 as u8))),
110 _ => None,
111 }
112 }
113
114 pub fn is_numeric_array(&self) -> bool {
116 matches!(self, Value::LongArray(_) | Value::DoubleArray(_))
117 }
118
119 pub fn as_i64(&self) -> Option<i64> {
120 match self {
121 Value::Long(v) => Some(*v),
122 Value::Enum(v) => Some(*v as i64),
123 other => other.as_f64().map(|v| v as i64),
124 }
125 }
126
127 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
130 pub(crate) fn from_u64(x: u64) -> Value {
131 i64::try_from(x).map_or(Value::Double(x as f64), Value::Long)
132 }
133
134 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
136 pub(crate) fn from_u64s(a: &[u64]) -> Value {
137 match a.iter().map(|x| i64::try_from(*x)).collect() {
138 Ok(longs) => Value::LongArray(longs),
139 Err(_) => Value::DoubleArray(a.iter().map(|x| *x as f64).collect()),
140 }
141 }
142}
143
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
146pub enum Severity {
147 #[default]
148 NoAlarm,
149 Minor,
150 Major,
151 Invalid,
152}
153
154impl Severity {
155 pub fn from_epics(sevr: u16) -> Self {
156 match sevr {
157 0 => Severity::NoAlarm,
158 1 => Severity::Minor,
159 2 => Severity::Major,
160 _ => Severity::Invalid,
161 }
162 }
163}
164
165#[derive(Clone, Debug, Default, PartialEq)]
167pub struct PvState {
168 pub connected: bool,
169 pub value: Value,
170 pub severity: Severity,
171 pub status: u16,
172 pub timestamp: Option<SystemTime>,
173 pub units: String,
174 pub precision: Option<i32>,
175 pub labels: Vec<String>,
177 pub display_range: Option<(f64, f64)>,
178 pub control_range: Option<(f64, f64)>,
179 pub alarm_range: Option<(f64, f64)>,
181 pub warning_range: Option<(f64, f64)>,
182 pub writable: bool,
185 pub writable_known: bool,
189 pub write_error: Option<String>,
191 pub error: Option<String>,
193}
194
195impl PvState {
196 pub fn format(&self, precision: Option<i32>, with_units: bool) -> String {
198 let prec = precision.or(self.precision);
199 let text = match &self.value {
200 Value::None => return String::new(),
201 Value::Double(v) => format_double(*v, prec),
202 Value::Long(v) => v.to_string(),
203 Value::Str(s) => s.clone(),
204 Value::Enum(i) => self
205 .labels
206 .get(*i as usize)
207 .cloned()
208 .unwrap_or_else(|| i.to_string()),
209 Value::DoubleArray(a) => join(a.iter().map(|v| format_double(*v, prec))),
210 Value::LongArray(a) => join(a.iter().map(|v| v.to_string())),
211 Value::StrArray(a) => join(a.iter().map(|s| format!("\"{}\"", s.replace('"', "\\\"")))),
212 Value::Image(img) => format!("{}x{} image #{}", img.width, img.height, img.unique_id),
213 };
214 let numeric = matches!(
215 self.value,
216 Value::Double(_) | Value::Long(_) | Value::DoubleArray(_) | Value::LongArray(_)
217 );
218 if with_units && numeric && !self.units.is_empty() {
219 format!("{text} {}", self.units)
220 } else {
221 text
222 }
223 }
224
225 pub fn range(&self) -> Option<(f64, f64)> {
227 self.display_range
228 .filter(|(lo, hi)| lo < hi)
229 .or(self.control_range.filter(|(lo, hi)| lo < hi))
230 }
231}
232
233fn format_double(v: f64, precision: Option<i32>) -> String {
234 match precision {
235 Some(p) if p >= 0 => format!("{v:.*}", p.min(17) as usize),
236 _ => {
237 let s = format!("{v}");
238 if s.len() > 12 { format!("{v:.6e}") } else { s }
239 }
240 }
241}
242
243const MAX_ARRAY_FORMATTING: usize = 256;
245
246fn join(mut items: impl ExactSizeIterator<Item = String>) -> String {
249 let more = items.len() > MAX_ARRAY_FORMATTING;
250 let mut text = String::from("[");
251 for (i, item) in items.by_ref().take(MAX_ARRAY_FORMATTING).enumerate() {
252 if i > 0 {
253 text.push_str(", ");
254 }
255 text.push_str(&item);
256 }
257 if more {
258 text.push_str(", ...");
259 }
260 text.push(']');
261 text
262}
263
264pub fn long_string(bytes: impl IntoIterator<Item = u8>) -> String {
267 let bytes: Vec<u8> = bytes.into_iter().take_while(|b| *b != 0).collect();
268 String::from_utf8_lossy(&bytes).into_owned()
269}
270
271pub fn long_string_bytes(text: &str, max: Option<usize>) -> Vec<u8> {
275 let mut end = text.len();
276 if let Some(max) = max {
277 end = end.min(max.saturating_sub(1));
278 while !text.is_char_boundary(end) {
279 end -= 1;
280 }
281 }
282 let mut bytes = Vec::with_capacity(end + 1);
283 bytes.extend_from_slice(&text.as_bytes()[..end]);
284 bytes.push(0);
285 bytes
286}
287
288pub fn split_array(text: &str) -> Vec<&str> {
291 let t = text.trim();
292 let t = t.strip_prefix('[').unwrap_or(t);
293 let t = t.strip_suffix(']').unwrap_or(t);
294 t.split([' ', ',']).filter(|x| !x.is_empty()).collect()
295}
296
297pub fn split_strings(text: &str) -> Vec<String> {
301 let t = text.trim();
302 let t = t.strip_prefix('[').unwrap_or(t);
303 let t = t.strip_suffix(']').unwrap_or(t);
304 let mut items = Vec::new();
305 let mut chars = t.chars().peekable();
306 while let Some(&c) = chars.peek() {
307 match c {
308 ' ' | '\t' => {
309 chars.next();
310 }
311 '"' => {
312 chars.next();
313 let mut item = String::new();
314 while let Some(c) = chars.next() {
315 match c {
316 '\\' if chars.peek() == Some(&'"') => {
317 item.push(chars.next().unwrap_or('"'))
318 }
319 '"' => break,
320 c => item.push(c),
321 }
322 }
323 items.push(item);
324 for c in chars.by_ref() {
326 if c == ',' {
327 break;
328 }
329 }
330 }
331 _ => {
332 let item: String = chars.by_ref().take_while(|c| *c != ',').collect();
333 items.push(item.trim().to_string());
334 }
335 }
336 }
337 items
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn unsigned_64_bit_values_never_wrap_negative() {
346 assert_eq!(Value::from_u64(5), Value::Long(5));
347 assert_eq!(Value::from_u64(u64::MAX), Value::Double(u64::MAX as f64));
348 assert_eq!(
349 Value::from_u64s(&[1, 2]),
350 Value::LongArray(Arc::from([1, 2]))
351 );
352 assert_eq!(
353 Value::from_u64s(&[1, u64::MAX]),
354 Value::DoubleArray(Arc::from([1.0, u64::MAX as f64]))
355 );
356 }
357
358 #[test]
359 fn formats_with_precision_and_units() {
360 let s = PvState {
361 value: Value::Double(2.54),
362 units: "Volts".into(),
363 precision: Some(3),
364 ..Default::default()
365 };
366 assert_eq!(s.format(None, true), "2.540 Volts");
367 assert_eq!(s.format(Some(1), false), "2.5");
368 }
369
370 #[test]
371 fn formats_enum_labels() {
372 let s = PvState {
373 value: Value::Enum(1),
374 labels: vec!["Done".into(), "Acquire".into()],
375 ..Default::default()
376 };
377 assert_eq!(s.format(None, true), "Acquire");
378 }
379
380 #[test]
381 fn formats_arrays_like_phoebus() {
382 let s = PvState {
383 value: Value::LongArray(Arc::from(vec![1024, 1024])),
384 ..Default::default()
385 };
386 assert_eq!(s.format(None, true), "[1024, 1024]");
387 let s = PvState {
388 value: Value::DoubleArray(Arc::from(vec![1.5, 2.0])),
389 precision: Some(2),
390 units: "mm".into(),
391 ..Default::default()
392 };
393 assert_eq!(s.format(None, true), "[1.50, 2.00] mm");
394 let s = PvState {
395 value: Value::LongArray((0..300).collect()),
396 ..Default::default()
397 };
398 let text = s.format(None, false);
399 assert!(text.starts_with("[0, 1, 2,"));
400 assert!(text.ends_with("254, 255, ...]"));
401 let s = PvState {
402 value: Value::StrArray(Arc::from(vec!["a b".to_string(), "say \"hi\"".to_string()])),
403 ..Default::default()
404 };
405 assert_eq!(s.format(None, true), r#"["a b", "say \"hi\""]"#);
407 }
408
409 #[test]
410 fn reads_long_strings_up_to_the_nul() {
411 assert_eq!(long_string(*b"/data/run1\0junk"), "/data/run1");
412 assert_eq!(long_string(*b"no nul"), "no nul");
413 assert_eq!(long_string(*b"\0"), "");
414 assert_eq!(long_string("caf\u{e9}".bytes()), "caf\u{e9}");
415 let v = Value::LongArray(Arc::from(vec![104, 105, -61, -87, 0, 120]));
417 assert_eq!(v.as_long_string().as_deref(), Some("hi\u{e9}"));
418 let v = Value::DoubleArray(Arc::from(vec![111.0, 107.0]));
419 assert_eq!(v.as_long_string().as_deref(), Some("ok"));
420 assert_eq!(Value::Str("x".into()).as_long_string(), None);
421 }
422
423 #[test]
424 fn writes_long_strings_nul_terminated_and_cut_to_fit() {
425 assert_eq!(long_string_bytes("abc", None), b"abc\0");
426 assert_eq!(long_string_bytes("abc", Some(256)), b"abc\0");
427 assert_eq!(long_string_bytes("abcdef", Some(4)), b"abc\0");
428 assert_eq!(long_string_bytes("abc", Some(4)), b"abc\0");
429 assert_eq!(long_string_bytes("", Some(4)), b"\0");
430 assert_eq!(long_string_bytes("abc", Some(1)), b"\0");
431 assert_eq!(long_string_bytes("abc", Some(0)), b"\0");
432 assert_eq!(long_string_bytes("ab\u{e9}", Some(4)), b"ab\0");
434 assert_eq!(
435 long_string(long_string_bytes("ab\u{e9}", Some(5))),
436 "ab\u{e9}"
437 );
438 }
439
440 #[test]
441 fn splits_typed_arrays() {
442 assert_eq!(split_array("[1024, 1024]"), ["1024", "1024"]);
443 assert_eq!(split_array(" 1 2,3 "), ["1", "2", "3"]);
444 assert!(split_array("[]").is_empty());
445 }
446
447 #[test]
448 fn splits_typed_string_arrays_like_phoebus() {
449 assert_eq!(
450 split_strings(r#"["a b", "say \"hi\"", c d , e]"#),
451 ["a b", r#"say "hi""#, "c d", "e"]
452 );
453 assert_eq!(split_strings("one"), ["one"]);
454 assert!(split_strings("[]").is_empty());
455 let s = PvState {
457 value: Value::StrArray(Arc::from(vec!["x, y".to_string(), "z".to_string()])),
458 ..Default::default()
459 };
460 assert_eq!(split_strings(&s.format(None, false)), ["x, y", "z"]);
461 }
462}