Skip to main content

pv/
value.rs

1use std::sync::Arc;
2use web_time::SystemTime;
3
4/// A PV's value, reduced to the few shapes a display needs.
5#[derive(Clone, Debug, Default, PartialEq)]
6pub enum Value {
7    /// No value received yet.
8    #[default]
9    None,
10    Double(f64),
11    Long(i64),
12    Str(String),
13    /// Index into [`PvState::labels`].
14    Enum(u16),
15    DoubleArray(Arc<[f64]>),
16    LongArray(Arc<[i64]>),
17    StrArray(Arc<[String]>),
18    /// An areaDetector image (PVA NTNDArray).
19    Image(Arc<Image>),
20}
21
22/// A 2D image, row-major with x varying fastest.
23#[derive(Clone, Debug, PartialEq)]
24pub struct Image {
25    pub width: usize,
26    pub height: usize,
27    /// 3 interleaved samples per pixel (RGB1) instead of 1.
28    pub rgb: bool,
29    pub data: ImageData,
30    pub unique_id: i64,
31}
32
33/// Image samples in their native type.
34#[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    /// Sample `i` as a float; signed integer types are reinterpreted as
69    /// unsigned when `unsigned` is set, as the Phoebus image widget does.
70    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    /// The value as a number, using the first element of an array.
92    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    /// A numeric array as a long string, as Phoebus shows a char waveform
105    /// whose widget `format` is string: see [`long_string`].
106    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    /// Whether this is an array of numbers, which a long string is held in.
115    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    /// An unsigned 64-bit integer: a [`Value::Long`] when it fits, else a
128    /// [`Value::Double`] near it, rather than a negative number.
129    #[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    /// Unsigned 64-bit integers, as [`Value::from_u64`] takes one.
135    #[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/// EPICS alarm severity.
145#[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/// Everything known about a PV at one moment.
166#[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    /// Enum state labels.
176    pub labels: Vec<String>,
177    pub display_range: Option<(f64, f64)>,
178    pub control_range: Option<(f64, f64)>,
179    /// Alarm (LOLO, HIHI) and warning (LOW, HIGH) limits.
180    pub alarm_range: Option<(f64, f64)>,
181    pub warning_range: Option<(f64, f64)>,
182    /// Whether a write is expected to be accepted. Only meaningful as a
183    /// fact when `writable_known`; otherwise it is an optimistic guess.
184    pub writable: bool,
185    /// Whether the server told us `writable`. PV Access has no per-channel
186    /// access rights, so for it this stays false: writes are allowed but may
187    /// be refused by the server, and `write_error` then says so.
188    pub writable_known: bool,
189    /// Why the last write failed, until one succeeds.
190    pub write_error: Option<String>,
191    /// Why the PV cannot connect, if the backend knows.
192    pub error: Option<String>,
193}
194
195impl PvState {
196    /// Format the value as text. `precision` overrides the PV's own precision.
197    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    /// Display limits, falling back to the control limits, if set (low < high).
226    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
243/// Phoebus `max_array_formatting`: arrays show at most this many elements.
244const MAX_ARRAY_FORMATTING: usize = 256;
245
246/// An array as Phoebus formats it, `[1, 2, 3]`, ending `, ...]` when it has
247/// more than [`MAX_ARRAY_FORMATTING`] elements.
248fn 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
264/// The text held in a char array, as Phoebus's `getLongString` reads it:
265/// the bytes up to the first NUL (or all of them), as UTF-8.
266pub 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
271/// `text` as a char array holding a long string: its UTF-8 bytes and a
272/// terminating NUL, cut to fit `max` elements (the PV's element count, if
273/// known) with the NUL kept, and never inside a multi-byte character.
274pub 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
288/// The elements of an array typed as text, in the `[1, 2, 3]` form that
289/// arrays are shown in, or just separated by spaces or commas.
290pub 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
297/// The elements of a string array typed as text, as Phoebus's `StringList`
298/// reads it: `["a b", "c"]`, with `\"` for a quote inside a quoted item, or
299/// unquoted items separated by commas.
300pub 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                // skip to just past the next comma
325                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        // Strings are quoted, as Phoebus's StringList.join does.
406        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        // A char waveform's elements are signed over CA and PVA.
416        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        // Not inside the two bytes of the é.
433        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        // What format shows reads back as the same array.
456        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}