Skip to main content

bob/
group_link.rs

1//! The DLS vacuum `get_pv_name.py` widget script, which bobview does
2//! natively instead of running it: a widget that shows whichever group a
3//! vacuum space links to. The script reads one of the space's link fields,
4//! such as `SR03S-VA-SPACE-01:IONPSTA.INPA` reading
5//! `SR03S-VA-GIONP-01:STA NPP NMS`, and points the widget at that group: a
6//! label shows the group's device name, anything else reads the group's
7//! `:STA` (and is named after the group, so `$(name)` in its actions is the
8//! group), and an EDM symbol shows the frame of its strip for that status.
9//!
10//! An `.opi` widget with the script is translated with a `<group_link>`
11//! property, bobview's own, saying all that the script would decide:
12//!
13//! ```xml
14//! <group_link>
15//!   <pv_name>$(device):MAXSTA.INPA</pv_name>  <!-- the link field -->
16//!   <sets>pv</sets>                           <!-- or "text", for a label -->
17//!   <frames>9</frames>                        <!-- a symbol's top frame -->
18//! </group_link>
19//! ```
20
21use crate::{Element, Widget};
22
23/// The script, recognised by its file name.
24const SCRIPT: &str = "get_pv_name.py";
25
26/// The top frame of each symbol strip the script knows, by image file name:
27/// frame `status + 1`, frame 0 being the picture for a PV that is not
28/// connected. A symbol with any other image keeps its `image_index`.
29const SYMBOL_FRAMES: &[(&str, i64)] = &[
30    ("mks937aImg-symbol-34.png", 16),
31    ("mks937bImg-symbol-34.png", 16),
32    ("mks937aPirg-symbol-34.png", 16),
33    ("mks937bPirg-symbol-34.png", 16),
34    ("digitelMpcIonp-symbol-34.png", 9),
35    ("vacuumValve-symbol-34.png", 5),
36];
37
38/// The macro the first action, opening the group's display, passes the
39/// group's device name to.
40const DEVICE_MACRO: &str = "device";
41
42/// What a widget's [`GroupLink`] points at the group.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum LinkSets {
45    /// The `text`: the group's device name.
46    Text,
47    /// The `pv_name`: the group's `:STA`, with `name`, `tooltip` and the
48    /// first action's `device` macro following.
49    Pv,
50}
51
52/// A widget that shows the group a vacuum space's link field names.
53#[derive(Clone, Debug, PartialEq)]
54pub struct GroupLink {
55    /// The link field, macros unexpanded.
56    pub pv_name: String,
57    pub sets: LinkSets,
58    /// For an EDM symbol with a known strip, its top frame: its
59    /// `image_index` then follows the group's `:STA`.
60    pub frames: Option<i64>,
61}
62
63impl Widget {
64    /// The widget's `<group_link>`, if it has one.
65    pub fn group_link(&self) -> Option<GroupLink> {
66        let el = self.get("group_link")?;
67        let pv_name = el.child_text("pv_name")?.trim().to_string();
68        let sets = match el.child_text("sets").map(str::trim) {
69            Some("text") => LinkSets::Text,
70            _ => LinkSets::Pv,
71        };
72        let frames = el.child_text("frames").and_then(|f| f.trim().parse().ok());
73        (!pv_name.is_empty()).then_some(GroupLink {
74            pv_name,
75            sets,
76            frames,
77        })
78    }
79}
80
81impl GroupLink {
82    /// Point the widget properties `props` at the group `device`, as the
83    /// script does with `setPropertyValue`.
84    pub fn point_at(&self, props: &mut Element, device: &str) {
85        let mut set = |name: &str, value: String| props.set_child(Element::with_text(name, value));
86        match self.sets {
87            LinkSets::Text => set("text", device.to_string()),
88            LinkSets::Pv => {
89                set("pv_name", format!("{device}:STA"));
90                set("name", device.to_string());
91                // BOY expanded the tooltip once, at load, so the script
92                // names the PV in it; bobview expands it as it draws.
93                set("tooltip", "$(pv_name)\n$(pv_value)".to_string());
94                if let Some(macros) = props
95                    .child_mut("actions")
96                    .and_then(|a| a.children.first_mut())
97                    .filter(|a| a.attr("type") == Some("open_display"))
98                    .map(|a| {
99                        if a.child("macros").is_none() {
100                            a.children.push(Element::new("macros"));
101                        }
102                        a.child_mut("macros").unwrap()
103                    })
104                {
105                    macros.set_child(Element::with_text(DEVICE_MACRO, device));
106                }
107            }
108        }
109    }
110}
111
112/// The group's device name in the value of a link field: everything before
113/// the first `:` of its first word, so `SR03S-VA-GPIRG-01:CTL:SP NPP MS` is
114/// `SR03S-VA-GPIRG-01`. A `scheme://` before the name is kept with it, so a
115/// link to `loc://G:STA` names the group `loc://G`.
116pub fn group_device(link: &str) -> Option<String> {
117    let pv = link.split_whitespace().next()?;
118    let (scheme, name) = match pv.split_once("://") {
119        Some((scheme, name)) => (&pv[..scheme.len() + 3], name),
120        None => ("", pv),
121    };
122    let device = name.split(':').next().unwrap_or("");
123    (!device.is_empty()).then(|| format!("{scheme}{device}"))
124}
125
126/// The frame of a strip with top frame `top` for a group's status: `status
127/// + 1`, or 0 for a status past the strip or none (a PV not connected).
128pub fn group_frame(status: Option<i64>, top: i64) -> i64 {
129    match status.map(|s| s + 1) {
130        Some(frame) if (1..=top).contains(&frame) => frame,
131        _ => 0,
132    }
133}
134
135/// The `<group_link>` for a BOY widget `el` that runs the script, from its
136/// `<scripts>` (the link field is the script's second PV), `widget_type`
137/// and `image_file`.
138pub(crate) fn from_opi(el: &Element) -> Option<Element> {
139    let path = el
140        .child("scripts")?
141        .children
142        .iter()
143        .filter(|p| p.name == "path")
144        .find(|p| {
145            let path = p.attr("pathString").unwrap_or("");
146            path.rsplit(['/', '\\']).next() == Some(SCRIPT)
147        })?;
148    let link = path.children.iter().filter(|p| p.name == "pv").nth(1)?;
149    let widget_type = el.child_text("widget_type").unwrap_or("").trim();
150    let mut out =
151        Element::new("group_link").with_child(Element::with_text("pv_name", link.text.trim()));
152    if widget_type == "Label" {
153        return Some(out.with_child(Element::with_text("sets", "text")));
154    }
155    out.children.push(Element::with_text("sets", "pv"));
156    if widget_type == "Edm Symbol Widget" {
157        let image = el.child_text("image_file").unwrap_or("").trim();
158        let image = image.rsplit(['/', '\\']).next().unwrap_or("");
159        if let Some((_, top)) = SYMBOL_FRAMES.iter().find(|(f, _)| *f == image) {
160            out.children
161                .push(Element::with_text("frames", top.to_string()));
162        }
163    }
164    Some(out)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn link_fields_name_the_group() {
173        let device = |l: &str| group_device(l);
174        assert_eq!(
175            device("SR03S-VA-GIONP-01:STA NPP NMS").as_deref(),
176            Some("SR03S-VA-GIONP-01")
177        );
178        assert_eq!(
179            device("SR03S-VA-GVALV-01:STA NPP MS").as_deref(),
180            Some("SR03S-VA-GVALV-01")
181        );
182        assert_eq!(
183            device("SR03S-VA-GPIRG-01:CTL:SP NPP MS").as_deref(),
184            Some("SR03S-VA-GPIRG-01")
185        );
186        assert_eq!(
187            device("  BL01T-VA-IMG-01 CA").as_deref(),
188            Some("BL01T-VA-IMG-01")
189        );
190        assert_eq!(device("loc://G1:STA").as_deref(), Some("loc://G1"));
191        assert_eq!(device(""), None);
192        assert_eq!(device("   "), None);
193        assert_eq!(device(":STA NPP"), None);
194    }
195
196    #[test]
197    fn status_picks_a_frame_within_the_strip() {
198        assert_eq!(group_frame(Some(0), 9), 1);
199        assert_eq!(group_frame(Some(8), 9), 9);
200        // Past the strip, below it, or not connected: the blank frame.
201        assert_eq!(group_frame(Some(9), 9), 0);
202        assert_eq!(group_frame(Some(-1), 9), 0);
203        assert_eq!(group_frame(Some(5), 5), 0);
204        assert_eq!(group_frame(None, 16), 0);
205    }
206
207    #[test]
208    fn point_at_sets_what_the_script_does() {
209        let props = |xml: &str| {
210            let doc = roxmltree::Document::parse(xml).unwrap();
211            Element::from_node(doc.root_element())
212        };
213        let mut sym = props(
214            r#"<widget><name></name><pv_name></pv_name><actions>
215            <action type="open_display"><file>g.opi</file><macros><device>$(name)</device></macros></action>
216            </actions></widget>"#,
217        );
218        let link = GroupLink {
219            pv_name: "L".into(),
220            sets: LinkSets::Pv,
221            frames: Some(9),
222        };
223        link.point_at(&mut sym, "SR03S-VA-GIONP-01");
224        let w = Widget::from_element(sym);
225        assert_eq!(w.text("pv_name"), Some("SR03S-VA-GIONP-01:STA"));
226        assert_eq!(w.name(), "SR03S-VA-GIONP-01");
227        let crate::Action::OpenDisplay { macros, .. } = &w.actions()[0] else {
228            panic!("not an open display action")
229        };
230        assert_eq!(macros.get("device"), Some("SR03S-VA-GIONP-01"));
231
232        let mut label = props("<widget><text>SRXXC-VA-GXXX-YY</text></widget>");
233        let link = GroupLink {
234            sets: LinkSets::Text,
235            frames: None,
236            ..link
237        };
238        link.point_at(&mut label, "SR03S-VA-GIONP-01");
239        let w = Widget::from_element(label);
240        assert_eq!(w.text("text"), Some("SR03S-VA-GIONP-01"));
241        assert!(w.get("pv_name").is_none());
242    }
243}