From 4527fbfc20ca78cfaed8324ec8a15e018a08df6f Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 26 Jan 2026 12:43:07 +0100 Subject: [PATCH] added "kind" to channel list --- csaxs_bec/devices/epics/eps.py | 278 ++++++++++++++++++--------------- 1 file changed, 152 insertions(+), 126 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index 5777a97..1b5e5c3 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -4,100 +4,138 @@ from ophyd import Device, Component as Cpt, EpicsSignal # --------------------------- # Registry: sections/channels # --------------------------- + CHANNELS = { "Valves Frontend": [ - {"attr": "FEVVPG0000", "label": "FE-VVPG-0000", "pv": "X12SA-FE-VVPG-0000:PLC_OPEN"}, - {"attr": "FEVVPG1010", "label": "FE-VVPG-1010", "pv": "X12SA-FE-VVPG-1010:PLC_OPEN"}, - {"attr": "FEVVFV2010", "label": "FE-VVFV-2010", "pv": "X12SA-FE-VVFV-2010:PLC_OPEN"}, - {"attr": "FEVVPG2010", "label": "FE-VVPG-2010", "pv": "X12SA-FE-VVPG-2010:PLC_OPEN"}, + {"attr": "FEVVPG0000", "label": "FE-VVPG-0000", + "pv": "X12SA-FE-VVPG-0000:PLC_OPEN", "kind": "valve"}, + {"attr": "FEVVPG1010", "label": "FE-VVPG-1010", + "pv": "X12SA-FE-VVPG-1010:PLC_OPEN", "kind": "valve"}, + {"attr": "FEVVFV2010", "label": "FE-VVFV-2010", + "pv": "X12SA-FE-VVFV-2010:PLC_OPEN", "kind": "valve"}, + {"attr": "FEVVPG2010", "label": "FE-VVPG-2010", + "pv": "X12SA-FE-VVPG-2010:PLC_OPEN", "kind": "valve"}, ], "Valves Optics Hutch": [ - {"attr": "OPVVPG1010", "label": "OP-VVPG-1010", "pv": "X12SA-OP-VVPG-1010:PLC_OPEN"}, - {"attr": "OPVVPG2010", "label": "OP-VVPG-2010", "pv": "X12SA-OP-VVPG-2010:PLC_OPEN"}, - {"attr": "OPVVPG3010", "label": "OP-VVPG-3010", "pv": "X12SA-OP-VVPG-3010:PLC_OPEN"}, - {"attr": "OPVVPG3020", "label": "OP-VVPG-3020", "pv": "X12SA-OP-VVPG-3020:PLC_OPEN"}, - {"attr": "OPVVPG4010", "label": "OP-VVPG-4010", "pv": "X12SA-OP-VVPG-4010:PLC_OPEN"}, - {"attr": "OPVVPG5010", "label": "OP-VVPG-5010", "pv": "X12SA-OP-VVPG-5010:PLC_OPEN"}, - {"attr": "OPVVPG6010", "label": "OP-VVPG-6010", "pv": "X12SA-OP-VVPG-6010:PLC_OPEN"}, - {"attr": "OPVVPG7010", "label": "OP-VVPG-7010", "pv": "X12SA-OP-VVPG-7010:PLC_OPEN"}, + {"attr": "OPVVPG1010", "label": "OP-VVPG-1010", + "pv": "X12SA-OP-VVPG-1010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG2010", "label": "OP-VVPG-2010", + "pv": "X12SA-OP-VVPG-2010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG3010", "label": "OP-VVPG-3010", + "pv": "X12SA-OP-VVPG-3010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG3020", "label": "OP-VVPG-3020", + "pv": "X12SA-OP-VVPG-3020:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG4010", "label": "OP-VVPG-4010", + "pv": "X12SA-OP-VVPG-4010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG5010", "label": "OP-VVPG-5010", + "pv": "X12SA-OP-VVPG-5010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG6010", "label": "OP-VVPG-6010", + "pv": "X12SA-OP-VVPG-6010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPVVPG7010", "label": "OP-VVPG-7010", + "pv": "X12SA-OP-VVPG-7010:PLC_OPEN", "kind": "valve"}, ], "Valves ES Hutch": [ - {"attr": "ESVVPG1010", "label": "ES-VVPG-1010", "pv": "X12SA-ES-VVPG-1010:PLC_OPEN"}, + {"attr": "ESVVPG1010", "label": "ES-VVPG-1010", + "pv": "X12SA-ES-VVPG-1010:PLC_OPEN", "kind": "valve"}, ], "Shutters Frontend": [ - {"attr": "FEPSH1", "label": "FE-PSH1-EMLS-0010", "pv": "X12SA-FE-PSH1-EMLS-0010:OPEN"}, - {"attr": "FESTO1", "label": "FE-STO1-EMLS-0010", "pv": "X12SA-FE-STO1-EMLS-0010:OPEN"}, + {"attr": "FEPSH1", "label": "FE-PSH1-EMLS-0010", + "pv": "X12SA-FE-PSH1-EMLS-0010:OPEN", "kind": "shutter"}, + {"attr": "FESTO1", "label": "FE-STO1-EMLS-0010", + "pv": "X12SA-FE-STO1-EMLS-0010:OPEN", "kind": "shutter"}, ], "Shutters Endstation": [ - {"attr": "ESPSH17010", "label": "OP-PSH1-EMLS-7010", "pv": "X12SA-OP-PSH1-EMLS-7010:OPEN"}, + {"attr": "ESPSH17010", "label": "OP-PSH1-EMLS-7010", + "pv": "X12SA-OP-PSH1-EMLS-7010:OPEN", "kind": "shutter"}, ], - # ------------------------------- - # 🔵 DMM MONOCHROMATOR - # ------------------------------- + # ------------------------------------------------- + # DMM MONOCHROMATOR — with temperature, switches, + # heater faults, energy, string readbacks + # ------------------------------------------------- "DMM Monochromator": [ - # Temperature sensors (:TEMP) - {"attr": "DMM_ETTC_3010", "label": "DMM Temp Surface 1", "pv": "X12SA-OP-DMM-ETTC-3010:TEMP"}, - {"attr": "DMM_ETTC_3020", "label": "DMM Temp Surface 2", "pv": "X12SA-OP-DMM-ETTC-3020:TEMP"}, - {"attr": "DMM_ETTC_3030", "label": "DMM Temp Shield 1 (disaster)", "pv": "X12SA-OP-DMM-ETTC-3030:TEMP"}, - {"attr": "DMM_ETTC_3040", "label": "DMM Temp Shield 2 (disaster)", "pv": "X12SA-OP-DMM-ETTC-3040:TEMP"}, - # Translation and Bragg switches (:THRU / :IN) - {"attr": "DMM_EMLS_3010", "label": "DMM Translation ThruPos", "pv": "X12SA-OP-DMM-EMLS-3010:THRU"}, - {"attr": "DMM_EMLS_3020", "label": "DMM Translation InPos", "pv": "X12SA-OP-DMM-EMLS-3020:IN"}, - {"attr": "DMM_EMLS_3030", "label": "DMM Bragg ThruPos", "pv": "X12SA-OP-DMM-EMLS-3030:THRU"}, - {"attr": "DMM_EMLS_3040", "label": "DMM Bragg InPos", "pv": "X12SA-OP-DMM-EMLS-3040:IN"}, + # Temperature sensors (1 decimal) + {"attr": "DMM_ETTC_3010", "label": "DMM Temp Surface 1", + "pv": "X12SA-OP-DMM-ETTC-3010:TEMP", "kind": "temp"}, + {"attr": "DMM_ETTC_3020", "label": "DMM Temp Surface 2", + "pv": "X12SA-OP-DMM-ETTC-3020:TEMP", "kind": "temp"}, + {"attr": "DMM_ETTC_3030", "label": "DMM Temp Shield 1 (disaster)", + "pv": "X12SA-OP-DMM-ETTC-3030:TEMP", "kind": "temp"}, + {"attr": "DMM_ETTC_3040", "label": "DMM Temp Shield 2 (disaster)", + "pv": "X12SA-OP-DMM-ETTC-3040:TEMP", "kind": "temp"}, - # Heater faults (ALL use :SWITCH) - {"attr": "DMM_EMSW_3050_SWITCH", "label": "DMM Heater Fault XTAL 1 (switch)", "pv": "X12SA-OP-DMM-EMSW-3050:SWITCH"}, - {"attr": "DMM_EMSW_3060_SWITCH", "label": "DMM Heater Fault XTAL 2 (switch)", "pv": "X12SA-OP-DMM-EMSW-3060:SWITCH"}, - {"attr": "DMM_EMSW_3070_SWITCH", "label": "DMM Heater Fault Support 1 (switch)", "pv": "X12SA-OP-DMM-EMSW-3070:SWITCH"}, + # Switches (ACTIVE/INACTIVE) + {"attr": "DMM_EMLS_3010", "label": "DMM Translation ThruPos", + "pv": "X12SA-OP-DMM-EMLS-3010:THRU", "kind": "switch"}, + {"attr": "DMM_EMLS_3020", "label": "DMM Translation InPos", + "pv": "X12SA-OP-DMM-EMLS-3020:IN", "kind": "switch"}, + {"attr": "DMM_EMLS_3030", "label": "DMM Bragg ThruPos", + "pv": "X12SA-OP-DMM-EMLS-3030:THRU", "kind": "switch"}, + {"attr": "DMM_EMLS_3040", "label": "DMM Bragg InPos", + "pv": "X12SA-OP-DMM-EMLS-3040:IN", "kind": "switch"}, + + # Heater faults (OK / FAULT) + {"attr": "DMM_EMSW_3050_SWITCH", "label": "DMM Heater Fault XTAL 1", + "pv": "X12SA-OP-DMM-EMSW-3050:SWITCH", "kind": "fault"}, + {"attr": "DMM_EMSW_3060_SWITCH", "label": "DMM Heater Fault XTAL 2", + "pv": "X12SA-OP-DMM-EMSW-3060:SWITCH", "kind": "fault"}, + {"attr": "DMM_EMSW_3070_SWITCH", "label": "DMM Heater Fault Support 1", + "pv": "X12SA-OP-DMM-EMSW-3070:SWITCH", "kind": "fault"}, # Readbacks - {"attr": "DMM1_ENERGY_GET", "label": "DMM Energy", "pv": "X12SA-OP-DMM1:ENERGY-GET"}, - {"attr": "DMM1_POSITION", "label": "DMM Position", "pv": "X12SA-OP-DMM1:POSITION"}, - {"attr": "DMM1_STRIPE", "label": "DMM Stripe", "pv": "X12SA-OP-DMM1:STRIPE"}, + {"attr": "DMM1_ENERGY_GET", "label": "DMM Energy", + "pv": "X12SA-OP-DMM1:ENERGY-GET", "kind": "energy"}, + {"attr": "DMM1_POSITION", "label": "DMM Position", + "pv": "X12SA-OP-DMM1:POSITION", "kind": "string"}, + {"attr": "DMM1_STRIPE", "label": "DMM Stripe", + "pv": "X12SA-OP-DMM1:STRIPE", "kind": "string"}, ], - # ------------------------------- - # 🔵 CCM MONOCHROMATOR - # ------------------------------- + # ------------------------------------------------- + # CCM MONOCHROMATOR + # ------------------------------------------------- "CCM Monochromator": [ - # Temperature sensors (:TEMP) - {"attr": "CCM_ETTC_4010", "label": "CCM Temp Crystal", "pv": "X12SA-OP-CCM-ETTC-4010:TEMP"}, - {"attr": "CCM_ETTC_4020", "label": "CCM Temp Shield (disaster)", "pv": "X12SA-OP-CCM-ETTC-4020:TEMP"}, - # Heater faults (ALL use :SWITCH) - {"attr": "CCM_EMSW_4010_SWITCH", "label": "CCM Heater Fault 1 (switch)", "pv": "X12SA-OP-CCM-EMSW-4010:SWITCH"}, - {"attr": "CCM_EMSW_4020_SWITCH", "label": "CCM Heater Fault 2 (switch)", "pv": "X12SA-OP-CCM-EMSW-4020:SWITCH"}, - {"attr": "CCM_EMSW_4030_SWITCH", "label": "CCM Heater Fault 3 (switch)", "pv": "X12SA-OP-CCM-EMSW-4030:SWITCH"}, + # Temperatures + {"attr": "CCM_ETTC_4010", "label": "CCM Temp Crystal", + "pv": "X12SA-OP-CCM-ETTC-4010:TEMP", "kind": "temp"}, + {"attr": "CCM_ETTC_4020", "label": "CCM Temp Shield (disaster)", + "pv": "X12SA-OP-CCM-ETTC-4020:TEMP", "kind": "temp"}, + + # Heater faults + {"attr": "CCM_EMSW_4010_SWITCH", "label": "CCM Heater Fault 1", + "pv": "X12SA-OP-CCM-EMSW-4010:SWITCH", "kind": "fault"}, + {"attr": "CCM_EMSW_4020_SWITCH", "label": "CCM Heater Fault 2", + "pv": "X12SA-OP-CCM-EMSW-4020:SWITCH", "kind": "fault"}, + {"attr": "CCM_EMSW_4030_SWITCH", "label": "CCM Heater Fault 3", + "pv": "X12SA-OP-CCM-EMSW-4030:SWITCH", "kind": "fault"}, # Readbacks - {"attr": "CCM1_ENERGY_GET", "label": "CCM Energy", "pv": "X12SA-OP-CCM1:ENERGY-GET"}, - {"attr": "CCM1_POSITION", "label": "CCM Position", "pv": "X12SA-OP-CCM1:POSITION"}, + {"attr": "CCM1_ENERGY_GET", "label": "CCM Energy", + "pv": "X12SA-OP-CCM1:ENERGY-GET", "kind": "energy"}, + {"attr": "CCM1_POSITION", "label": "CCM Position", + "pv": "X12SA-OP-CCM1:POSITION", "kind": "string"}, ], } # ============================================================================== -# DYNAMIC CLASS CREATION (Ophyd-safe) — updated show_all formatting +# DYNAMIC CLASS CREATION — Ophyd-safe # ============================================================================== def create_dynamic_eps_class(): - """ - Create an Ophyd Device subclass with all Components generated - from CHANNELS at class-creation time (safe for Ophyd/BEC). - """ + class_attrs = dict( USER_ACCESS=["show_all"], SUB_VALUE="value", _default_sub="value", ) - # Define all Components before the class is created + # Dynamically define Components FIRST for section, items in CHANNELS.items(): for it in items: class_attrs[it["attr"]] = Cpt( @@ -106,15 +144,20 @@ def create_dynamic_eps_class(): read_pv=it["pv"] ) + # ---------------------------------------------------------- + # Methods + # ---------------------------------------------------------- class DynamicMethods: + def show_all(self): - red = "\x1b[91m" + red = "\x1b[91m" green = "\x1b[92m" white = "\x1b[0m" - bold = "\x1b[1m" + bold = "\x1b[1m" + cyan = "\x1b[96m" - # Render helpers def safe_get(attr): + """Get value or None.""" obj = getattr(self, attr, None) if obj is None: return None @@ -124,92 +167,76 @@ def create_dynamic_eps_class(): return None def is_bool_like(v): - # handles 0/1/True/False specifically return isinstance(v, (bool, int)) and v in (0, 1, True, False) - - def format_value(value, pv): - """ - - Temperature PVs (…:TEMP): one decimal (e.g., 23.7) - - Energy PVs (…:ENERGY-GET): four decimals (e.g., 12.3456) - - Bool-like: handled separately - - Others (strings / floats / ints): printed as-is - """ + def fmt_value(value, pv, kind): + """Format value based on semantic kind.""" if value is None: return f"{red}MISSING{white}" - # Temperature formatting → 1 decimal - if pv.endswith(":TEMP") and isinstance(value, (int, float)): + # ------------------- TEMPERATURE ------------------- + if kind == "temp" and isinstance(value, (int, float)): return f"{value:.1f}" - # Energy formatting → 4 decimals - if pv.endswith(":ENERGY-GET") and isinstance(value, (int, float)): + # ------------------- ENERGY ------------------------ + if kind == "energy" and isinstance(value, (int, float)): return f"{value:.4f}" - # Non-bool values (e.g., strings like POSITION/STRIPE) - if not is_bool_like(value): + # ------------------- STRINGS ----------------------- + if kind in ("string", "position"): return f"{value}" - # Return raw value for bool handling elsewhere - return value + # ------------------- SWITCH (ACTIVE/INACTIVE) ------ + if kind == "switch" and is_bool_like(value): + return f"{green+'ACTIVE'+white if value else red+'INACTIVE'+white}" + # ------------------- FAULT (OK/FAULT) -------------- + if kind == "fault" and is_bool_like(value): + return f"{green+'OK'+white if not value else red+'FAULT'+white}" - def render_line(label, value, pv, label_width): - # Booleans as OPEN/CLOSED with color - if is_bool_like(value): - is_open = bool(value) - color = green if is_open else red - status = "OPEN" if is_open else "CLOSED" - return f" – {label:<{label_width}} {color}{status}{white}" + # ------------------- VALVE/SHUTTER ----------------- + if kind in ("valve", "shutter") and is_bool_like(value): + return f"{green+'OPEN'+white if value else red+'CLOSED'+white}" - # Everything else formatted via format_value - fv = format_value(value, pv) - # If format_value returned raw bool-like, it means not temp but bool-like; still handle color - if fv in (0, 1, True, False): - is_open = bool(fv) - color = green if is_open else red - status = "OPEN" if is_open else "CLOSED" - return f" – {label:<{label_width}} {color}{status}{white}" + # ------------------- FALLBACK ----------------------- + # Non-boolean numeric: + return f"{value}" - return f" – {label:<{label_width}} {fv}" - - print("X12SA valve/shutter/mono status") + # ------------------- PRINT START --------------------- + print(f"{bold}X12SA valve/shutter/mono status{white}") for section, items in CHANNELS.items(): print(f"\n{bold}{section}{white}") - # Fetch values once + # Gather row values rows = [] for it in items: val = safe_get(it["attr"]) - rows.append((it["label"], val, it["pv"])) + rows.append((it["label"], val, it["pv"], it["kind"])) - # Compute a nice label width per section (min 32) - label_width = max(32, *(len(label) for (label, _, _) in rows) or [32]) + # Compute label width + label_width = max(32, *(len(label) for (label, _, _, _) in rows)) - # Section summary when all present values are pure booleans - present_vals = [v for (_, v, _) in rows if v is not None] - bool_like_vals = [v for v in present_vals if is_bool_like(v)] - if present_vals and len(bool_like_vals) == len(present_vals): - total = len(bool_like_vals) - open_cnt = sum(1 for v in bool_like_vals if bool(v)) - if open_cnt == total: - print(f"{green}All channels in this section are open.{white}") + # Detect if summary applies + present = [v for (_, v, _, k) in rows if v is not None and k in ("valve", "shutter", "switch", "fault")] + bools = [v for v in present if is_bool_like(v)] + if present and len(bools) == len(present): + total = len(bools) + active = sum(1 for v in bools if v) + if active == total: + print(f"{green}All channels in this section are active.{white}") else: - print(f"{red}Warning: {open_cnt}/{total} open.{white}") - elif not present_vals: - print(" (no channels found on this device)") + print(f"{red}Warning: {active}/{total} active.{white}") - # Lines - for label, value, pv in rows: - print(render_line(label, value, pv, label_width)) + # Print lines + for label, value, pv, kind in rows: + fv = fmt_value(value, pv, kind) + print(f" – {label:<{label_width}} {fv}") + # ---------------------------------------------------------- + # Duplicates / missing PV validation + # ---------------------------------------------------------- def consistency_report(self, *, verbose=True): - """ - Checks for: - - attributes listed in CHANNELS but missing on the device - - duplicate PVs in CHANNELS (should be none in a real system) - """ missing = [] dupes = [] seen = {} @@ -228,29 +255,28 @@ def create_dynamic_eps_class(): if verbose: print("=== Consistency Report ===") if missing: - print("\nMissing attributes on device:") - for section, attr, label, pv in missing: - print(f" - [{section}] {attr} ({label}) pv={pv}") + print("\nMissing attributes:") + for sec, a, lbl, pv in missing: + print(f" - [{sec}] {a} {lbl} pv={pv}") else: print("\nNo missing attributes.") if dupes: - print("\nDuplicate PVs detected:") - for pv, first, second in dupes: - print(f" {pv}") - print(f" First: {first}") - print(f" Second: {second}") + print("\nDuplicate PVs:") + for pv, f1, f2 in dupes: + print(f" {pv} → {f1} AND {f2}") else: print("\nNo duplicate PVs.") return {"missing_attrs": missing, "duplicate_pvs": dupes} - # Bind methods + # Bind methods into class class_attrs["show_all"] = DynamicMethods.show_all class_attrs["consistency_report"] = DynamicMethods.consistency_report - # Create the class + # Create the Device subclass return type("cSAXSEps", (Device,), class_attrs) -# Create the final class type for use in BEC + +# Create final class for BEC to import cSAXSEps = create_dynamic_eps_class()