From bdf94533a5788424bc8d0decd43f0cd31b4eeed3 Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 22 Jan 2026 16:25:37 +0100 Subject: [PATCH 01/11] first version of csaxs eps --- csaxs_bec/device_configs/user_setup.yaml | 14 ++ csaxs_bec/devices/epics/eps.py | 204 +++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 csaxs_bec/devices/epics/eps.py diff --git a/csaxs_bec/device_configs/user_setup.yaml b/csaxs_bec/device_configs/user_setup.yaml index d8ccfe0..232849c 100644 --- a/csaxs_bec/device_configs/user_setup.yaml +++ b/csaxs_bec/device_configs/user_setup.yaml @@ -1 +1,15 @@ ############################################################ + + + +############################################################ +##################### EPS ################################## +############################################################ +x12saEPS: + description: X12SA EPS info and control + deviceClass: csaxs_bec.devices.epics.eps.cSAXSEps + deviceConfig: {} + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: baseline \ No newline at end of file diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py new file mode 100644 index 0000000..c7eb989 --- /dev/null +++ b/csaxs_bec/devices/epics/eps.py @@ -0,0 +1,204 @@ + +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"}, + ], + + "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"}, + ], + + "Valves ES Hutch": [ + {"attr": "ESVVPG1010", "label": "ES-VVPG-1010", "pv": "X12SA-ES-VVPG-1010:PLC_OPEN"}, + ], + + "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"}, + ], + + "Shutters Endstation": [ + {"attr": "ESPSH17010", "label": "OP-PSH1-EMLS-7010", "pv": "X12SA-OP-PSH1-EMLS-7010:OPEN"}, + ], +} + + +class cSAXSEps(Device): + # ------------------------------------------------------------------------- + # Cpt definitions (AUTO-GENERATED FROM CHANNELS: one per registry entry) + # ------------------------------------------------------------------------- + USER_ACCESS = [ + "show_all", + ] + SUB_VALUE = "value" + _default_sub = SUB_VALUE + # Valves Frontend + FEVVPG0000 = Cpt(EpicsSignal, name="FE-VVPG-0000", read_pv="X12SA-FE-VVPG-0000:PLC_OPEN") + FEVVPG1010 = Cpt(EpicsSignal, name="FE-VVPG-1010", read_pv="X12SA-FE-VVPG-1010:PLC_OPEN") + FEVVFV2010 = Cpt(EpicsSignal, name="FE-VVFV-2010", read_pv="X12SA-FE-VVFV-2010:PLC_OPEN") + FEVVPG2010 = Cpt(EpicsSignal, name="FE-VVPG-2010", read_pv="X12SA-FE-VVPG-2010:PLC_OPEN") + + # Valves Optics Hutch + OPVVPG1010 = Cpt(EpicsSignal, name="OP-VVPG-1010", read_pv="X12SA-OP-VVPG-1010:PLC_OPEN") + OPVVPG2010 = Cpt(EpicsSignal, name="OP-VVPG-2010", read_pv="X12SA-OP-VVPG-2010:PLC_OPEN") + OPVVPG3010 = Cpt(EpicsSignal, name="OP-VVPG-3010", read_pv="X12SA-OP-VVPG-3010:PLC_OPEN") + OPVVPG3020 = Cpt(EpicsSignal, name="OP-VVPG-3020", read_pv="X12SA-OP-VVPG-3020:PLC_OPEN") + OPVVPG4010 = Cpt(EpicsSignal, name="OP-VVPG-4010", read_pv="X12SA-OP-VVPG-4010:PLC_OPEN") + OPVVPG5010 = Cpt(EpicsSignal, name="OP-VVPG-5010", read_pv="X12SA-OP-VVPG-5010:PLC_OPEN") + OPVVPG6010 = Cpt(EpicsSignal, name="OP-VVPG-6010", read_pv="X12SA-OP-VVPG-6010:PLC_OPEN") + OPVVPG7010 = Cpt(EpicsSignal, name="OP-VVPG-7010", read_pv="X12SA-OP-VVPG-7010:PLC_OPEN") + + # Valves ES Hutch + ESVVPG1010 = Cpt(EpicsSignal, name="ES-VVPG-1010", read_pv="X12SA-ES-VVPG-1010:PLC_OPEN") + + # Shutters Frontend + FEPSH1 = Cpt(EpicsSignal, name="FE-PSH1-EMLS-0010", read_pv="X12SA-FE-PSH1-EMLS-0010:OPEN") + FESTO1 = Cpt(EpicsSignal, name="FE-STO1-EMLS-0010", read_pv="X12SA-FE-STO1-EMLS-0010:OPEN") + + # Shutters Endstation + ESPSH17010 = Cpt(EpicsSignal, name="OP-PSH1-EMLS-7010", read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN") + + # ------------------------ + # Status / consistency API + # ------------------------ + def show_all(self): + + """ + Print the status of all valves and shutters, grouped by section, driven by CHANNELS. + - Green = OPEN (True), Red = CLOSED (False) + - Missing attributes are shown as MISSING + """ + # ANSI colors + red = "\x1b[91m" + green = "\x1b[92m" + white = "\x1b[0m" + bold = "\x1b[1m" + + def safe_get_attr_value(attr_name): + obj = getattr(self, attr_name, None) + if obj is None: + return None + try: + return bool(obj.get()) + except Exception: + return None + + def render_line(label, is_open): + if is_open is None: + return f" - {label:<24} {red}MISSING{white}" + color = green if is_open else red + status = "OPEN" if is_open else "CLOSED" + return f" - {label:<24} {color}{status}{white}" + + def section_header(title): + print(f"\n{bold}{title}{white}") + + def show_section(title, items): + section_header(title) + + values = [(item["label"], safe_get_attr_value(item["attr"])) for item in items] + + # Summary + vals_present = [v for (_, v) in values if v is not None] + if not vals_present: + print(" (no channels found on this device)") + else: + total = len(vals_present) + open_cnt = sum(1 for v in vals_present if v) + all_open = (total > 0 and open_cnt == total) + if all_open: + print(f"{green}All channels in this section are open.{white}") + else: + print(f"{red}Warning: Not all channels in this section are open. ({open_cnt}/{total} open){white}") + + # Per-channel lines + for label, val in values: + print(render_line(label, val)) + + print("X12SA valve/shutter status") + for section, items in CHANNELS.items(): + show_section(section, items) + + 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 attributes + missing_attrs = [] + # Duplicate PVs + seen_pvs = {} + duplicates = [] + + for section, items in CHANNELS.items(): + for it in items: + # attr present? + if getattr(self, it["attr"], None) is None: + missing_attrs.append((section, it["attr"], it["label"], it["pv"])) + + # PV uniqueness + pv = it["pv"] + if pv in seen_pvs: + duplicates.append((pv, seen_pvs[pv], (section, it["attr"], it["label"]))) + else: + seen_pvs[pv] = (section, it["attr"], it["label"]) + + if verbose: + print("=== Consistency Report ===") + if missing_attrs: + print("\nMissing attributes on device (define matching Cpt):") + for section, attr, label, pv in missing_attrs: + print(f" - [{section}] {attr} ({label}) pv={pv}") + else: + print("\nNo missing attributes.") + + if duplicates: + print("\nDuplicate PVs in CHANNELS (fix registry; real system should have none):") + for pv, first, dup in duplicates: + fsec, fattr, flabel = first + dsec, dattr, dlabel = dup + print(f" - {pv}\n first: [{fsec}] {fattr} ({flabel})\n second: [{dsec}] {dattr} ({dlabel})") + else: + print("\nNo duplicate PVs.") + + return { + "missing_attrs": missing_attrs, + "duplicate_pvs": duplicates, + } + + +# ------------------------------- +# Developer helper (optional): re-generate Cpt definitions from CHANNELS +# ------------------------------- +def codegen_cpt_definitions(): + """Print Cpt(...) definitions for all channels based on CHANNELS.""" + lines = [] + lines.append("# === AUTO-GENERATED FROM CHANNELS ===") + for section, items in CHANNELS.items(): + lines.append(f"\n# {section}") + for it in items: + attr = it["attr"] + label = it["label"] + pv = it["pv"] + lines.append( + f'{attr} = Cpt(EpicsSignal, name="{label}", read_pv="{pv}")' + ) + print("\n".join(lines)) -- 2.49.1 From 7911717142c8da99a7d52739a16f341d6634e390 Mon Sep 17 00:00:00 2001 From: x12sa Date: Fri, 23 Jan 2026 13:46:52 +0100 Subject: [PATCH 02/11] create class dynamically, monos added --- csaxs_bec/devices/epics/eps.py | 372 +++++++++++++++++++-------------- 1 file changed, 212 insertions(+), 160 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index c7eb989..5777a97 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -1,8 +1,6 @@ from ophyd import Device, Component as Cpt, EpicsSignal - - # --------------------------- # Registry: sections/channels # --------------------------- @@ -37,168 +35,222 @@ CHANNELS = { "Shutters Endstation": [ {"attr": "ESPSH17010", "label": "OP-PSH1-EMLS-7010", "pv": "X12SA-OP-PSH1-EMLS-7010:OPEN"}, ], + + # ------------------------------- + # 🔵 DMM MONOCHROMATOR + # ------------------------------- + "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"}, + + # 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"}, + + # 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"}, + ], + + # ------------------------------- + # 🔵 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"}, + + # 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"}, + ], } -class cSAXSEps(Device): - # ------------------------------------------------------------------------- - # Cpt definitions (AUTO-GENERATED FROM CHANNELS: one per registry entry) - # ------------------------------------------------------------------------- - USER_ACCESS = [ - "show_all", - ] - SUB_VALUE = "value" - _default_sub = SUB_VALUE - # Valves Frontend - FEVVPG0000 = Cpt(EpicsSignal, name="FE-VVPG-0000", read_pv="X12SA-FE-VVPG-0000:PLC_OPEN") - FEVVPG1010 = Cpt(EpicsSignal, name="FE-VVPG-1010", read_pv="X12SA-FE-VVPG-1010:PLC_OPEN") - FEVVFV2010 = Cpt(EpicsSignal, name="FE-VVFV-2010", read_pv="X12SA-FE-VVFV-2010:PLC_OPEN") - FEVVPG2010 = Cpt(EpicsSignal, name="FE-VVPG-2010", read_pv="X12SA-FE-VVPG-2010:PLC_OPEN") +# ============================================================================== +# DYNAMIC CLASS CREATION (Ophyd-safe) — updated show_all formatting +# ============================================================================== +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", + ) - # Valves Optics Hutch - OPVVPG1010 = Cpt(EpicsSignal, name="OP-VVPG-1010", read_pv="X12SA-OP-VVPG-1010:PLC_OPEN") - OPVVPG2010 = Cpt(EpicsSignal, name="OP-VVPG-2010", read_pv="X12SA-OP-VVPG-2010:PLC_OPEN") - OPVVPG3010 = Cpt(EpicsSignal, name="OP-VVPG-3010", read_pv="X12SA-OP-VVPG-3010:PLC_OPEN") - OPVVPG3020 = Cpt(EpicsSignal, name="OP-VVPG-3020", read_pv="X12SA-OP-VVPG-3020:PLC_OPEN") - OPVVPG4010 = Cpt(EpicsSignal, name="OP-VVPG-4010", read_pv="X12SA-OP-VVPG-4010:PLC_OPEN") - OPVVPG5010 = Cpt(EpicsSignal, name="OP-VVPG-5010", read_pv="X12SA-OP-VVPG-5010:PLC_OPEN") - OPVVPG6010 = Cpt(EpicsSignal, name="OP-VVPG-6010", read_pv="X12SA-OP-VVPG-6010:PLC_OPEN") - OPVVPG7010 = Cpt(EpicsSignal, name="OP-VVPG-7010", read_pv="X12SA-OP-VVPG-7010:PLC_OPEN") - - # Valves ES Hutch - ESVVPG1010 = Cpt(EpicsSignal, name="ES-VVPG-1010", read_pv="X12SA-ES-VVPG-1010:PLC_OPEN") - - # Shutters Frontend - FEPSH1 = Cpt(EpicsSignal, name="FE-PSH1-EMLS-0010", read_pv="X12SA-FE-PSH1-EMLS-0010:OPEN") - FESTO1 = Cpt(EpicsSignal, name="FE-STO1-EMLS-0010", read_pv="X12SA-FE-STO1-EMLS-0010:OPEN") - - # Shutters Endstation - ESPSH17010 = Cpt(EpicsSignal, name="OP-PSH1-EMLS-7010", read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN") - - # ------------------------ - # Status / consistency API - # ------------------------ - def show_all(self): - - """ - Print the status of all valves and shutters, grouped by section, driven by CHANNELS. - - Green = OPEN (True), Red = CLOSED (False) - - Missing attributes are shown as MISSING - """ - # ANSI colors - red = "\x1b[91m" - green = "\x1b[92m" - white = "\x1b[0m" - bold = "\x1b[1m" - - def safe_get_attr_value(attr_name): - obj = getattr(self, attr_name, None) - if obj is None: - return None - try: - return bool(obj.get()) - except Exception: - return None - - def render_line(label, is_open): - if is_open is None: - return f" - {label:<24} {red}MISSING{white}" - color = green if is_open else red - status = "OPEN" if is_open else "CLOSED" - return f" - {label:<24} {color}{status}{white}" - - def section_header(title): - print(f"\n{bold}{title}{white}") - - def show_section(title, items): - section_header(title) - - values = [(item["label"], safe_get_attr_value(item["attr"])) for item in items] - - # Summary - vals_present = [v for (_, v) in values if v is not None] - if not vals_present: - print(" (no channels found on this device)") - else: - total = len(vals_present) - open_cnt = sum(1 for v in vals_present if v) - all_open = (total > 0 and open_cnt == total) - if all_open: - print(f"{green}All channels in this section are open.{white}") - else: - print(f"{red}Warning: Not all channels in this section are open. ({open_cnt}/{total} open){white}") - - # Per-channel lines - for label, val in values: - print(render_line(label, val)) - - print("X12SA valve/shutter status") - for section, items in CHANNELS.items(): - show_section(section, items) - - 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 attributes - missing_attrs = [] - # Duplicate PVs - seen_pvs = {} - duplicates = [] - - for section, items in CHANNELS.items(): - for it in items: - # attr present? - if getattr(self, it["attr"], None) is None: - missing_attrs.append((section, it["attr"], it["label"], it["pv"])) - - # PV uniqueness - pv = it["pv"] - if pv in seen_pvs: - duplicates.append((pv, seen_pvs[pv], (section, it["attr"], it["label"]))) - else: - seen_pvs[pv] = (section, it["attr"], it["label"]) - - if verbose: - print("=== Consistency Report ===") - if missing_attrs: - print("\nMissing attributes on device (define matching Cpt):") - for section, attr, label, pv in missing_attrs: - print(f" - [{section}] {attr} ({label}) pv={pv}") - else: - print("\nNo missing attributes.") - - if duplicates: - print("\nDuplicate PVs in CHANNELS (fix registry; real system should have none):") - for pv, first, dup in duplicates: - fsec, fattr, flabel = first - dsec, dattr, dlabel = dup - print(f" - {pv}\n first: [{fsec}] {fattr} ({flabel})\n second: [{dsec}] {dattr} ({dlabel})") - else: - print("\nNo duplicate PVs.") - - return { - "missing_attrs": missing_attrs, - "duplicate_pvs": duplicates, - } - - -# ------------------------------- -# Developer helper (optional): re-generate Cpt definitions from CHANNELS -# ------------------------------- -def codegen_cpt_definitions(): - """Print Cpt(...) definitions for all channels based on CHANNELS.""" - lines = [] - lines.append("# === AUTO-GENERATED FROM CHANNELS ===") + # Define all Components before the class is created for section, items in CHANNELS.items(): - lines.append(f"\n# {section}") for it in items: - attr = it["attr"] - label = it["label"] - pv = it["pv"] - lines.append( - f'{attr} = Cpt(EpicsSignal, name="{label}", read_pv="{pv}")' + class_attrs[it["attr"]] = Cpt( + EpicsSignal, + name=it["label"], + read_pv=it["pv"] ) - print("\n".join(lines)) + + class DynamicMethods: + def show_all(self): + red = "\x1b[91m" + green = "\x1b[92m" + white = "\x1b[0m" + bold = "\x1b[1m" + + # Render helpers + def safe_get(attr): + obj = getattr(self, attr, None) + if obj is None: + return None + try: + return obj.get() + except Exception: + 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 + """ + if value is None: + return f"{red}MISSING{white}" + + # Temperature formatting → 1 decimal + if pv.endswith(":TEMP") and isinstance(value, (int, float)): + return f"{value:.1f}" + + # Energy formatting → 4 decimals + if pv.endswith(":ENERGY-GET") and isinstance(value, (int, float)): + return f"{value:.4f}" + + # Non-bool values (e.g., strings like POSITION/STRIPE) + if not is_bool_like(value): + return f"{value}" + + # Return raw value for bool handling elsewhere + return value + + + 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}" + + # 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}" + + return f" – {label:<{label_width}} {fv}" + + print("X12SA valve/shutter/mono status") + + for section, items in CHANNELS.items(): + print(f"\n{bold}{section}{white}") + + # Fetch values once + rows = [] + for it in items: + val = safe_get(it["attr"]) + rows.append((it["label"], val, it["pv"])) + + # Compute a nice label width per section (min 32) + label_width = max(32, *(len(label) for (label, _, _) in rows) or [32]) + + # 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}") + else: + print(f"{red}Warning: {open_cnt}/{total} open.{white}") + elif not present_vals: + print(" (no channels found on this device)") + + # Lines + for label, value, pv in rows: + print(render_line(label, value, pv, label_width)) + + 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 = {} + + for section, items in CHANNELS.items(): + for it in items: + if not hasattr(self, it["attr"]): + missing.append((section, it["attr"], it["label"], it["pv"])) + + pv = it["pv"] + if pv in seen: + dupes.append((pv, seen[pv], (section, it["attr"], it["label"]))) + else: + seen[pv] = (section, it["attr"], it["label"]) + + 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}") + 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}") + else: + print("\nNo duplicate PVs.") + + return {"missing_attrs": missing, "duplicate_pvs": dupes} + + # Bind methods + class_attrs["show_all"] = DynamicMethods.show_all + class_attrs["consistency_report"] = DynamicMethods.consistency_report + + # Create the class + return type("cSAXSEps", (Device,), class_attrs) + +# Create the final class type for use in BEC +cSAXSEps = create_dynamic_eps_class() -- 2.49.1 From 541eb970960279f41af58149c95dd6db34f801af Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 26 Jan 2026 12:43:07 +0100 Subject: [PATCH 03/11] 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() -- 2.49.1 From 24bd5a71bccb704dc6b8df6d022417edd7fef981 Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 26 Jan 2026 13:03:41 +0100 Subject: [PATCH 04/11] added water cooling channels --- csaxs_bec/devices/epics/eps.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index 1b5e5c3..d0c8629 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -121,6 +121,37 @@ CHANNELS = { {"attr": "CCM1_POSITION", "label": "CCM Position", "pv": "X12SA-OP-CCM1:POSITION", "kind": "string"}, ], + + + # ------------------------------------------------- + # COOLING WATER (flows + supply/return valves) + # ------------------------------------------------- + "Cooling Water": [ + # Flows (1=OK, 0=FAIL). Labels mirror the tag style. + {"attr": "OPSL1EFSW2010", "label": "OP-SL1-EFSW-2010", + "pv": "X12SA-OP-SL1-EFSW-2010:FLOW", "kind": "flow"}, + {"attr": "OPSL2EFSW2010", "label": "OP-SL2-EFSW-2010", + "pv": "X12SA-OP-SL2-EFSW-2010:FLOW", "kind": "flow"}, + {"attr": "OPEB1EFSW5010", "label": "OP-EB1-EFSW-5010", + "pv": "X12SA-OP-EB1-EFSW-5010:FLOW", "kind": "flow"}, + {"attr": "OPEB1EFSW5020", "label": "OP-EB1-EFSW-5020", + "pv": "X12SA-OP-EB1-EFSW-5020:FLOW", "kind": "flow"}, + {"attr": "OPSL3EFSW5010", "label": "OP-SL3-EFSW-5010", + "pv": "X12SA-OP-SL3-EFSW-5010:FLOW", "kind": "flow"}, + {"attr": "OPKBEFSW6010", "label": "OP-KB-EFSW-6010", + "pv": "X12SA-OP-KB-EFSW-6010:FLOW", "kind": "flow"}, + {"attr": "OPPSH1EFSW7010", "label": "OP-PSH1-EFSW-7010", + "pv": "X12SA-OP-PSH1-EFSW-7010:FLOW", "kind": "flow"}, + {"attr": "ESEB2EFSW1010", "label": "ES-EB2-EFSW-1010", + "pv": "X12SA-ES-EB2-EFSW-1010:FLOW", "kind": "flow"}, + + # Cooling supply/return valves (OPEN/CLOSED) + {"attr": "OPCSECVW0010", "label": "OP-CS-ECVW-0010", + "pv": "X12SA-OP-CS-ECVW-0010:PLC_OPEN", "kind": "valve"}, + {"attr": "OPCSECVW0020", "label": "OP-CS-ECVW-0020", + "pv": "X12SA-OP-CS-ECVW-0020:PLC_OPEN", "kind": "valve"}, + ], + } @@ -198,6 +229,10 @@ def create_dynamic_eps_class(): if kind in ("valve", "shutter") and is_bool_like(value): return f"{green+'OPEN'+white if value else red+'CLOSED'+white}" + # Flow → OK / FAIL (1=OK, 0=FAIL) + if kind == "flow" and is_bool_like(value): + return f"{green}OK{white}" if bool(value) else f"{red}FAIL{white}" + # ------------------- FALLBACK ----------------------- # Non-boolean numeric: return f"{value}" -- 2.49.1 From e860571a646a0957356d85384d851f5fb5bc0791 Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 26 Jan 2026 13:53:51 +0100 Subject: [PATCH 05/11] water on method --- csaxs_bec/devices/epics/eps.py | 111 +++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 18 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index d0c8629..315959c 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -1,11 +1,26 @@ from ophyd import Device, Component as Cpt, EpicsSignal - +import time # --------------------------- # Registry: sections/channels # --------------------------- CHANNELS = { + "EPS Alarms": [ + { + "attr": "EPSAlarmCnt", + "label": "X12SA EPS Alarm count", + "pv": "X12SA-EPS-PLC:AlarmCnt_EPS", + "kind": "alarmcount", + }, + { + "attr": "MISAlarmCnt", + "label": "FrontEnd MIS Alarm count", + "pv": "ARS00-MIS-PLC-01:AlarmCnt_Frontends", + "kind": "alarmcount", + }, + ], + "Valves Frontend": [ {"attr": "FEVVPG0000", "label": "FE-VVPG-0000", "pv": "X12SA-FE-VVPG-0000:PLC_OPEN", "kind": "valve"}, @@ -152,6 +167,7 @@ CHANNELS = { "pv": "X12SA-OP-CS-ECVW-0020:PLC_OPEN", "kind": "valve"}, ], + } @@ -161,7 +177,7 @@ CHANNELS = { def create_dynamic_eps_class(): class_attrs = dict( - USER_ACCESS=["show_all"], + USER_ACCESS=["show_all", "water_cooling_op"], SUB_VALUE="value", _default_sub="value", ) @@ -180,6 +196,59 @@ def create_dynamic_eps_class(): # ---------------------------------------------------------- class DynamicMethods: + def water_cooling_op(self): + """ + Opens all water‑cooling valves (ECVW) with alarm reset if needed. + Polls valves for 20 seconds and reports success/failure. + """ + print("\n=== Water Cooling Operation ===") + + # --------------------------- + # Collect required signals + # --------------------------- + eps_alarm = getattr(self, "EPSAlarmCnt", None) + ackerr = EpicsSignal("X12SA-EPS-PLC:ACKERR-REQUEST") + request = EpicsSignal("X12SA-OP-CS-ECVW:PLC_REQUEST") + + # Cooling valves we must check + valve_attrs = ["OPCSECVW0010", "OPCSECVW0020"] + valves = [getattr(self, a) for a in valve_attrs] + + # --------------------------- + # 1. Reset alarms if needed + # --------------------------- + alarm_value = eps_alarm.get() if eps_alarm else 0 + if alarm_value and alarm_value > 0: + print(f"EPS alarms present ({alarm_value}), resetting…") + ackerr.put(1) + time.sleep(0.3) + + # --------------------------- + # 2. Send valve‑open request + # --------------------------- + print("Sending cooling‑valve OPEN request…") + request.put(1) + + # --------------------------- + # 3. Poll valves for 20 seconds + # --------------------------- + timeout = 20 + end = time.time() + timeout + + while time.time() < end: + states = [v.get() for v in valves] + status = ["OPEN" if s else "CLOSED" for s in states] + print(" Valve status:", status) + + if all(states): + print("\n→ All cooling valves are OPEN. Operation successful.") + return True + + time.sleep(2) + + print("\n→ TIMEOUT: Cooling valves failed to open.") + return False + def show_all(self): red = "\x1b[91m" green = "\x1b[92m" @@ -234,11 +303,10 @@ def create_dynamic_eps_class(): return f"{green}OK{white}" if bool(value) else f"{red}FAIL{white}" # ------------------- FALLBACK ----------------------- - # Non-boolean numeric: return f"{value}" # ------------------- PRINT START --------------------- - print(f"{bold}X12SA valve/shutter/mono status{white}") + print(f"{bold}X12SA EPS status{white}") for section, items in CHANNELS.items(): print(f"\n{bold}{section}{white}") @@ -247,27 +315,33 @@ def create_dynamic_eps_class(): rows = [] for it in items: val = safe_get(it["attr"]) - rows.append((it["label"], val, it["pv"], it["kind"])) + rows.append((it["label"], val, it["pv"], it["kind"], it["attr"])) # Compute label width - label_width = max(32, *(len(label) for (label, _, _, _) in rows)) - - # 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: {active}/{total} active.{white}") + label_width = max(32, *(len(label) for (label, _, _, _, _) in rows)) # Print lines - for label, value, pv, kind in rows: + for label, value, pv, kind, _attr in rows: fv = fmt_value(value, pv, kind) print(f" – {label:<{label_width}} {fv}") + # ------------------------------------------------- + # Contextual proposal: water_cooling() + # Triggered only in "Cooling Water" if both ECVW are CLOSED + # ------------------------------------------------- + if section == "Cooling Water": + v1 = safe_get("OPCSECVW0010") + v2 = safe_get("OPCSECVW0020") + + def is_closed(v): + return is_bool_like(v) and (v is False or v == 0) + + if is_closed(v1) and is_closed(v2): + print( + f"\n{cyan}Hint:{white} Water cooling valves OP are closed. " + f"You can request them to open them via {bold}dev.x12saEPS.water_cooling_op(){white}." + ) + # ---------------------------------------------------------- # Duplicates / missing PV validation # ---------------------------------------------------------- @@ -308,6 +382,7 @@ def create_dynamic_eps_class(): # Bind methods into class class_attrs["show_all"] = DynamicMethods.show_all class_attrs["consistency_report"] = DynamicMethods.consistency_report + class_attrs["water_cooling_op"] = DynamicMethods.water_cooling_op # Create the Device subclass return type("cSAXSEps", (Device,), class_attrs) -- 2.49.1 From ce88310125fe5e98cd68fc0fce9749ac19f6d111 Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 26 Jan 2026 14:20:46 +0100 Subject: [PATCH 06/11] fixed enable of water cooling --- csaxs_bec/devices/epics/eps.py | 302 +++++++++++++++++++++++---------- 1 file changed, 208 insertions(+), 94 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index 315959c..071b258 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -1,6 +1,7 @@ from ophyd import Device, Component as Cpt, EpicsSignal import time + # --------------------------- # Registry: sections/channels # --------------------------- @@ -68,13 +69,7 @@ CHANNELS = { "pv": "X12SA-OP-PSH1-EMLS-7010:OPEN", "kind": "shutter"}, ], - # ------------------------------------------------- - # DMM MONOCHROMATOR — with temperature, switches, - # heater faults, energy, string readbacks - # ------------------------------------------------- "DMM Monochromator": [ - - # 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", @@ -84,7 +79,6 @@ CHANNELS = { {"attr": "DMM_ETTC_3040", "label": "DMM Temp Shield 2 (disaster)", "pv": "X12SA-OP-DMM-ETTC-3040:TEMP", "kind": "temp"}, - # 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", @@ -94,7 +88,6 @@ CHANNELS = { {"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", @@ -102,7 +95,6 @@ CHANNELS = { {"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", "kind": "energy"}, {"attr": "DMM1_POSITION", "label": "DMM Position", @@ -111,18 +103,12 @@ CHANNELS = { "pv": "X12SA-OP-DMM1:STRIPE", "kind": "string"}, ], - # ------------------------------------------------- - # CCM MONOCHROMATOR - # ------------------------------------------------- "CCM Monochromator": [ - - # 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", @@ -130,44 +116,35 @@ CHANNELS = { {"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", "kind": "energy"}, {"attr": "CCM1_POSITION", "label": "CCM Position", "pv": "X12SA-OP-CCM1:POSITION", "kind": "string"}, ], - - # ------------------------------------------------- - # COOLING WATER (flows + supply/return valves) - # ------------------------------------------------- "Cooling Water": [ - # Flows (1=OK, 0=FAIL). Labels mirror the tag style. - {"attr": "OPSL1EFSW2010", "label": "OP-SL1-EFSW-2010", + {"attr": "OPSL1EFSW2010", "label": "OP-SL1-EFSW-2010", "pv": "X12SA-OP-SL1-EFSW-2010:FLOW", "kind": "flow"}, - {"attr": "OPSL2EFSW2010", "label": "OP-SL2-EFSW-2010", + {"attr": "OPSL2EFSW2010", "label": "OP-SL2-EFSW-2010", "pv": "X12SA-OP-SL2-EFSW-2010:FLOW", "kind": "flow"}, - {"attr": "OPEB1EFSW5010", "label": "OP-EB1-EFSW-5010", + {"attr": "OPEB1EFSW5010", "label": "OP-EB1-EFSW-5010", "pv": "X12SA-OP-EB1-EFSW-5010:FLOW", "kind": "flow"}, - {"attr": "OPEB1EFSW5020", "label": "OP-EB1-EFSW-5020", + {"attr": "OPEB1EFSW5020", "label": "OP-EB1-EFSW-5020", "pv": "X12SA-OP-EB1-EFSW-5020:FLOW", "kind": "flow"}, - {"attr": "OPSL3EFSW5010", "label": "OP-SL3-EFSW-5010", + {"attr": "OPSL3EFSW5010", "label": "OP-SL3-EFSW-5010", "pv": "X12SA-OP-SL3-EFSW-5010:FLOW", "kind": "flow"}, - {"attr": "OPKBEFSW6010", "label": "OP-KB-EFSW-6010", + {"attr": "OPKBEFSW6010", "label": "OP-KB-EFSW-6010", "pv": "X12SA-OP-KB-EFSW-6010:FLOW", "kind": "flow"}, {"attr": "OPPSH1EFSW7010", "label": "OP-PSH1-EFSW-7010", "pv": "X12SA-OP-PSH1-EFSW-7010:FLOW", "kind": "flow"}, - {"attr": "ESEB2EFSW1010", "label": "ES-EB2-EFSW-1010", + {"attr": "ESEB2EFSW1010", "label": "ES-EB2-EFSW-1010", "pv": "X12SA-ES-EB2-EFSW-1010:FLOW", "kind": "flow"}, - # Cooling supply/return valves (OPEN/CLOSED) {"attr": "OPCSECVW0010", "label": "OP-CS-ECVW-0010", "pv": "X12SA-OP-CS-ECVW-0010:PLC_OPEN", "kind": "valve"}, {"attr": "OPCSECVW0020", "label": "OP-CS-ECVW-0020", "pv": "X12SA-OP-CS-ECVW-0020:PLC_OPEN", "kind": "valve"}, ], - - } @@ -182,7 +159,7 @@ def create_dynamic_eps_class(): _default_sub="value", ) - # Dynamically define Components FIRST + # Dynamically define Components for section, items in CHANNELS.items(): for it in items: class_attrs[it["attr"]] = Cpt( @@ -196,58 +173,162 @@ def create_dynamic_eps_class(): # ---------------------------------------------------------- class DynamicMethods: + # ------------------------ + # Client messaging helper + # ------------------------ + def _notify(self, msg: str, show_asap=True): + """ + Send a message to the client UI through the device manager. + Falls back to print() if device_manager/connector is missing. + """ + try: + conn = getattr(getattr(self, "device_manager", None), "connector", None) + if conn and hasattr(conn, "send_client_info"): + conn.send_client_info(msg, scope="", show_asap=show_asap) + else: + print(msg) + except Exception: + print(msg) + + # ---------------------------------------------------------- + # Water cooling operation + # ---------------------------------------------------------- def water_cooling_op(self): """ - Opens all water‑cooling valves (ECVW) with alarm reset if needed. - Polls valves for 20 seconds and reports success/failure. + Open ECVW valves, reset EPS alarms, monitor for 20s, + then ensure stability (valves remain open) for 10s. + All messages sent to client. """ - print("\n=== Water Cooling Operation ===") + from ophyd import EpicsSignal - # --------------------------- - # Collect required signals - # --------------------------- - eps_alarm = getattr(self, "EPSAlarmCnt", None) - ackerr = EpicsSignal("X12SA-EPS-PLC:ACKERR-REQUEST") - request = EpicsSignal("X12SA-OP-CS-ECVW:PLC_REQUEST") + POLL_PERIOD = 2 + TIMEOUT = 20 + STABILITY = 15 + + self._notify("=== Water Cooling Operation ===") + + # --- Signals --- + eps_alarm_sig = getattr(self, "EPSAlarmCnt", None) + ackerr = EpicsSignal("X12SA-EPS-PLC:ACKERR-REQUEST") + request = EpicsSignal("X12SA-OP-CS-ECVW:PLC_REQUEST") - # Cooling valves we must check valve_attrs = ["OPCSECVW0010", "OPCSECVW0020"] - valves = [getattr(self, a) for a in valve_attrs] + valves = [getattr(self, a, None) for a in valve_attrs] - # --------------------------- - # 1. Reset alarms if needed - # --------------------------- - alarm_value = eps_alarm.get() if eps_alarm else 0 + # Flow channels list extracted from CHANNELS + flow_items = [ + (it["attr"], it["label"]) + for it in CHANNELS["Cooling Water"] + if it["kind"] == "flow" + ] + + def safe_get(sig, default=None): + try: + return sig.get() + except Exception: + return default + + # --- Step 1: EPS alarm reset --- + alarm_value = safe_get(eps_alarm_sig, 0) if alarm_value and alarm_value > 0: - print(f"EPS alarms present ({alarm_value}), resetting…") - ackerr.put(1) + self._notify(f"[WaterCooling] EPS alarms present ({alarm_value}) → resetting…") + try: + ackerr.put(1) + except Exception as ex: + self._notify(f"[WaterCooling] WARNING: ACKERR write failed: {ex}") time.sleep(0.3) + else: + self._notify("[WaterCooling] No EPS alarms detected.") - # --------------------------- - # 2. Send valve‑open request - # --------------------------- - print("Sending cooling‑valve OPEN request…") - request.put(1) + # --- Step 2: Issue open request --- + self._notify("[WaterCooling] Sending cooling‑valve OPEN request…") + try: + request.put(1) + except Exception as ex: + self._notify(f"[WaterCooling] ERROR: Failed to send OPEN request: {ex}") + return False - # --------------------------- - # 3. Poll valves for 20 seconds - # --------------------------- - timeout = 20 - end = time.time() + timeout + # --- Step 3: Monitoring loop (clean client table output) --- + start = time.time() + end = start + TIMEOUT + stable_until = None - while time.time() < end: - states = [v.get() for v in valves] - status = ["OPEN" if s else "CLOSED" for s in states] - print(" Valve status:", status) + # Print (server-side) header once + print("Monitoring valves and flow sensors...") + print(f" Valves: {valve_attrs[0][-4:]}, {valve_attrs[1][-4:]}") + print(f" Note: stability requires valves to remain OPEN for {STABILITY} seconds.") - if all(states): - print("\n→ All cooling valves are OPEN. Operation successful.") - return True + # One table header to the client (via device manager) + # Fixed-width columns for alignment in monospaced UI + table_header = ( + f"{'Time':>6} | {'Valves':<21} | {'Flows (OK/FAIL/N/A)':<20}" + ) + self._notify(table_header) - time.sleep(2) + def snapshot(): + # Valve snapshot + v_states = [safe_get(v, None) for v in valves] + v1 = ( + f"{valve_attrs[0][-4:]}=" + + ("OPEN " if v_states[0] is True or v_states[0] == 1 else + "CLOSED" if v_states[0] is False or v_states[0] == 0 else + "N/A ") + ) + v2 = ( + f"{valve_attrs[1][-4:]}=" + + ("OPEN " if v_states[1] is True or v_states[1] == 1 else + "CLOSED" if v_states[1] is False or v_states[1] == 0 else + "N/A ") + ) + # 2 valves with a single space between => width ~ 21 + valve_str = f"{v1} {v2}" - print("\n→ TIMEOUT: Cooling valves failed to open.") - return False + # Flow summary: OK/FAIL/N/A counts (compact) + flow_states = [] + for f_attr, _lbl in flow_items: + fsig = getattr(self, f_attr, None) + fval = safe_get(fsig, None) + flow_states.append(True if fval in (1, True) else False if fval in (0, False) else None) + + ok = sum(1 for f in flow_states if f is True) + fail = sum(1 for f in flow_states if f is False) + na = sum(1 for f in flow_states if f is None) + flow_summary = f"{ok:>2} / {fail:>2} / {na:>2}" + + return v_states, valve_str, flow_summary + + while True: + now = time.time() + elapsed = int(now - start) + + if now > end: + # One last line to client + v_states, valves_s, flows_s = snapshot() + self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}") + print("→ TIMEOUT: Cooling valves failed to remain OPEN.") + return False + + # Live snapshot + v_states, valves_s, flows_s = snapshot() + # Exactly one concise line to client per cycle + self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}") + + both_open = all(s is not None and bool(s) for s in v_states) + + if both_open: + if stable_until is None: + stable_until = now + STABILITY + print(f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…") + else: + if now >= stable_until: + print("→ SUCCESS: Valves remained OPEN during stability window.") + return True + else: + if stable_until is not None: + print("[WaterCooling] Valve closed again → restarting stability window.") + stable_until = None + + time.sleep(POLL_PERIOD) def show_all(self): red = "\x1b[91m" @@ -256,11 +337,20 @@ def create_dynamic_eps_class(): bold = "\x1b[1m" cyan = "\x1b[96m" + # ---- New: enum maps for numeric -> string rendering ---- + POSITION_ENUM = { + 0: "out of beam", + 1: "in beam", + } + STRIPE_ENUM = { + 0: "Stripe 1 W/B4C", + 1: "Stripe 2 NiV/B4C", + } + POSITION_ATTRS = {"DMM1_POSITION", "CCM1_POSITION"} + STRIPE_ATTRS = {"DMM1_STRIPE"} + def safe_get(attr): - """Get value or None.""" obj = getattr(self, attr, None) - if obj is None: - return None try: return obj.get() except Exception: @@ -269,11 +359,29 @@ def create_dynamic_eps_class(): def is_bool_like(v): return isinstance(v, (bool, int)) and v in (0, 1, True, False) - def fmt_value(value, pv, kind): - """Format value based on semantic kind.""" + # ---- Changed: accept attr in formatter so we can apply enum mapping ---- + def fmt_value(value, pv, kind, attr): if value is None: return f"{red}MISSING{white}" + # ---------- Explicit enum mappings by attribute ---------- + if attr in POSITION_ATTRS: + # Position comes as numeric 0/1 + try: + iv = int(value) + return POSITION_ENUM.get(iv, f"{iv}") + except Exception: + # Fallback if it’s already a string or unexpected + return f"{value}" + + if attr in STRIPE_ATTRS: + # Stripe comes as numeric 0/1 + try: + iv = int(value) + return STRIPE_ENUM.get(iv, f"{iv}") + except Exception: + return f"{value}" + # ------------------- TEMPERATURE ------------------- if kind == "temp" and isinstance(value, (int, float)): return f"{value:.1f}" @@ -284,6 +392,7 @@ def create_dynamic_eps_class(): # ------------------- STRINGS ----------------------- if kind in ("string", "position"): + # For other strings, just echo the value return f"{value}" # ------------------- SWITCH (ACTIVE/INACTIVE) ------ @@ -298,7 +407,7 @@ def create_dynamic_eps_class(): if kind in ("valve", "shutter") and is_bool_like(value): return f"{green+'OPEN'+white if value else red+'CLOSED'+white}" - # Flow → OK / FAIL (1=OK, 0=FAIL) + # ------------------- FLOW (OK/FAIL) ---------------- if kind == "flow" and is_bool_like(value): return f"{green}OK{white}" if bool(value) else f"{red}FAIL{white}" @@ -311,39 +420,31 @@ def create_dynamic_eps_class(): for section, items in CHANNELS.items(): print(f"\n{bold}{section}{white}") - # Gather row values rows = [] for it in items: val = safe_get(it["attr"]) rows.append((it["label"], val, it["pv"], it["kind"], it["attr"])) - # Compute label width label_width = max(32, *(len(label) for (label, _, _, _, _) in rows)) - # Print lines for label, value, pv, kind, _attr in rows: - fv = fmt_value(value, pv, kind) - print(f" – {label:<{label_width}} {fv}") + fv = fmt_value(value, pv, kind, _attr) # <-- pass attr to formatter + print(f" - {label:<{label_width}} {fv}") - # ------------------------------------------------- - # Contextual proposal: water_cooling() - # Triggered only in "Cooling Water" if both ECVW are CLOSED - # ------------------------------------------------- if section == "Cooling Water": v1 = safe_get("OPCSECVW0010") v2 = safe_get("OPCSECVW0020") - def is_closed(v): - return is_bool_like(v) and (v is False or v == 0) + def closed(v): return is_bool_like(v) and not bool(v) - if is_closed(v1) and is_closed(v2): + if closed(v1) and closed(v2): print( - f"\n{cyan}Hint:{white} Water cooling valves OP are closed. " - f"You can request them to open them via {bold}dev.x12saEPS.water_cooling_op(){white}." + f"\n{cyan}Hint:{white} Both water cooling valves are CLOSED.\n" + f"You can open them using: {bold}dev.x12saEPS.water_cooling_op(){white}" ) # ---------------------------------------------------------- - # Duplicates / missing PV validation + # Consistency report # ---------------------------------------------------------- def consistency_report(self, *, verbose=True): missing = [] @@ -363,6 +464,7 @@ def create_dynamic_eps_class(): if verbose: print("=== Consistency Report ===") + if missing: print("\nMissing attributes:") for sec, a, lbl, pv in missing: @@ -379,14 +481,26 @@ def create_dynamic_eps_class(): return {"missing_attrs": missing, "duplicate_pvs": dupes} - # Bind methods into class + + # Attach methods to class attributes class_attrs["show_all"] = DynamicMethods.show_all class_attrs["consistency_report"] = DynamicMethods.consistency_report class_attrs["water_cooling_op"] = DynamicMethods.water_cooling_op + class_attrs["_notify"] = DynamicMethods._notify - # Create the Device subclass - return type("cSAXSEps", (Device,), class_attrs) + # ---------------------------------------------------------- + # Custom __init__ to accept device_manager (BEC) + # ---------------------------------------------------------- + def __init__(self, *args, device_manager=None, **kwargs): + super(cls, self).__init__(*args, **kwargs) + self.device_manager = device_manager + + # Create class + cls = type("cSAXSEps", (Device,), class_attrs) + setattr(cls, "__init__", __init__) + + return cls -# Create final class for BEC to import +# Create final class for BEC import cSAXSEps = create_dynamic_eps_class() -- 2.49.1 From 0c81c718d89fb397265c4a3461bf8c05db998373 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 15:06:00 +0100 Subject: [PATCH 07/11] refactor(eps): Refactoring of EPS device from cSAXS --- csaxs_bec/devices/epics/eps.py | 1182 ++++++++++++++++++++++++-------- 1 file changed, 880 insertions(+), 302 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index 071b258..499c98d 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -1,151 +1,887 @@ +"""EPS module for cSAXS beamline: defines the EPS device with its components and methods.""" + +from __future__ import annotations -from ophyd import Device, Component as Cpt, EpicsSignal import time +from bec_lib.logger import bec_logger +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind +from ophyd_devices import PSIDeviceBase + +logger = bec_logger.logger + # --------------------------- # Registry: sections/channels # --------------------------- -CHANNELS = { - "EPS Alarms": [ - { - "attr": "EPSAlarmCnt", - "label": "X12SA EPS Alarm count", - "pv": "X12SA-EPS-PLC:AlarmCnt_EPS", - "kind": "alarmcount", - }, - { - "attr": "MISAlarmCnt", - "label": "FrontEnd MIS Alarm count", - "pv": "ARS00-MIS-PLC-01:AlarmCnt_Frontends", - "kind": "alarmcount", - }, - ], - "Valves Frontend": [ - {"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"}, - ], +class EPSSubDevices(Device): + """Base class for EPS sub-device components (e.g. alarms, valves, shutters). with common methods if needed.""" - "Valves Optics Hutch": [ - {"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"}, - ], + def describe(self) -> dict: + desc = super().describe() + for walk in self.walk_signals(): + if walk.item.attr_name not in desc: + desc[walk.item.attr_name] = walk.item.describe() + return desc - "Valves ES Hutch": [ - {"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", "kind": "shutter"}, - {"attr": "FESTO1", "label": "FE-STO1-EMLS-0010", - "pv": "X12SA-FE-STO1-EMLS-0010:OPEN", "kind": "shutter"}, - ], +class EPSAlarms(EPSSubDevices): + """EPS alarms at the cSAXS beamline.""" - "Shutters Endstation": [ - {"attr": "ESPSH17010", "label": "OP-PSH1-EMLS-7010", - "pv": "X12SA-OP-PSH1-EMLS-7010:OPEN", "kind": "shutter"}, - ], + eps_alarm_cnt = Cpt( + EpicsSignalRO, + read_pv="X12SA-EPS-PLC:AlarmCnt_EPS", + add_prefix=("",), + name="eps_alarm_cnt", + kind=Kind.omitted, + doc="X12SA EPS Alarm count", + auto_monitor=True, + labels={"alarm"}, + ) + mis_alarm_cnt = Cpt( + EpicsSignalRO, + read_pv="ARS00-MIS-PLC-01:AlarmCnt_Frontends", + add_prefix=("",), + name="mis_alarm_cnt", + kind=Kind.omitted, + doc="FrontEnd MIS Alarm count", + auto_monitor=True, + labels={"alarm"}, + ) - "DMM Monochromator": [ - {"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"}, - {"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"}, +class Valves(EPSSubDevices): + """Valves at the cSAXS beamline.""" - {"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"}, + ################ + ### Frontend ### + ################ + fe_vvpg_0000 = Cpt( + EpicsSignalRO, + read_pv="X12SA-FE-VVPG-0000:PLC_OPEN", + add_prefix=("",), + name="fevvpg0000", + kind=Kind.omitted, + doc="Frontend valve FE-VVPG-0000", + auto_monitor=True, + labels={"valve"}, + ) + fe_vvpg_1010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-FE-VVPG-1010:PLC_OPEN", + add_prefix=("",), + name="fevvpg1010", + kind=Kind.omitted, + doc="Frontend valve FE-VVPG-1010", + auto_monitor=True, + labels={"valve"}, + ) + fe_vvfv_2010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-FE-VVFV-2010:PLC_OPEN", + add_prefix=("",), + name="fevvfv2010", + kind=Kind.omitted, + doc="Frontend valve FE-VVFV-2010", + auto_monitor=True, + labels={"valve"}, + ) + fe_vvpg_2010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-FE-VVPG-2010:PLC_OPEN", + add_prefix=("",), + name="fevvpg2010", + kind=Kind.omitted, + doc="Frontend valve FE-VVPG-2010", + auto_monitor=True, + labels={"valve"}, + ) - {"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"}, - ], + ################ + ### Optics ### + ################ - "CCM Monochromator": [ - {"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"}, + op_vvpg_1010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-1010:PLC_OPEN", + add_prefix=("",), + name="opvvpg1010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-1010", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_2010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-2010:PLC_OPEN", + add_prefix=("",), + name="opvvpg2010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-2010", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_3010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-3010:PLC_OPEN", + add_prefix=("",), + name="opvvpg3010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-3010", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_3020 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-3020:PLC_OPEN", + add_prefix=("",), + name="opvvpg3020", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-3020", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_4010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-4010:PLC_OPEN", + add_prefix=("",), + name="opvvpg4010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-4010", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_5010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-5010:PLC_OPEN", + add_prefix=("",), + name="opvvpg5010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-5010", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_6010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-6010:PLC_OPEN", + add_prefix=("",), + name="opvvpg6010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-6010", + auto_monitor=True, + labels={"valve"}, + ) + op_vvpg_7010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-VVPG-7010:PLC_OPEN", + add_prefix=("",), + name="opvvpg7010", + kind=Kind.omitted, + doc="Optics valve OP-VVPG-7010", + auto_monitor=True, + labels={"valve"}, + ) - {"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"}, + ################## + ### Endstation ### + ################## - {"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"}, - ], + es_vvpg_1010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-ES-VVPG-1010:PLC_OPEN", + add_prefix=("",), + name="esvvpg1010", + kind=Kind.omitted, + doc="Endstation valve ES-VVPG-1010", + auto_monitor=True, + labels={"valve"}, + ) - "Cooling Water": [ - {"attr": "OPSL1EFSW2010", "label": "OP-SL1-EFSW-2010", - "pv": "X12SA-OP-SL1-EFSW-2010:FLOW", "kind": "flow"}, - {"attr": "OPSL2EFSW2010", "label": "OP-SL2-EFSW-2010", - "pv": "X12SA-OP-SL2-EFSW-2010:FLOW", "kind": "flow"}, - {"attr": "OPEB1EFSW5010", "label": "OP-EB1-EFSW-5010", - "pv": "X12SA-OP-EB1-EFSW-5010:FLOW", "kind": "flow"}, - {"attr": "OPEB1EFSW5020", "label": "OP-EB1-EFSW-5020", - "pv": "X12SA-OP-EB1-EFSW-5020:FLOW", "kind": "flow"}, - {"attr": "OPSL3EFSW5010", "label": "OP-SL3-EFSW-5010", - "pv": "X12SA-OP-SL3-EFSW-5010:FLOW", "kind": "flow"}, - {"attr": "OPKBEFSW6010", "label": "OP-KB-EFSW-6010", - "pv": "X12SA-OP-KB-EFSW-6010:FLOW", "kind": "flow"}, - {"attr": "OPPSH1EFSW7010", "label": "OP-PSH1-EFSW-7010", - "pv": "X12SA-OP-PSH1-EFSW-7010:FLOW", "kind": "flow"}, - {"attr": "ESEB2EFSW1010", "label": "ES-EB2-EFSW-1010", - "pv": "X12SA-ES-EB2-EFSW-1010:FLOW", "kind": "flow"}, - {"attr": "OPCSECVW0010", "label": "OP-CS-ECVW-0010", - "pv": "X12SA-OP-CS-ECVW-0010:PLC_OPEN", "kind": "valve"}, - {"attr": "OPCSECVW0020", "label": "OP-CS-ECVW-0020", - "pv": "X12SA-OP-CS-ECVW-0020:PLC_OPEN", "kind": "valve"}, - ], -} +class Shutters(EPSSubDevices): + """Shutters at the cSAXS beamline.""" + + fe_psh1 = Cpt( + EpicsSignalRO, + read_pv="X12SA-FE-PSH1-EMLS-0010:OPEN", + add_prefix=("",), + name="fepsh1", + kind=Kind.omitted, + doc="Frontend shutter FE-PSH1-EMLS-0010", + auto_monitor=True, + labels={"shutter"}, + ) + fe_sto1 = Cpt( + EpicsSignalRO, + read_pv="X12SA-FE-STO1-EMLS-0010:OPEN", + add_prefix=("",), + name="festo1", + kind=Kind.omitted, + doc="Frontend shutter FE-STO1-EMLS-0010", + auto_monitor=True, + labels={"shutter"}, + ) + + es_psh17010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN", + add_prefix=("",), + name="espsh17010", + kind=Kind.omitted, + doc="Endstation shutter OP-PSH1-EMLS-7010", + auto_monitor=True, + labels={"shutter"}, + ) + + +class DMMMonochromator(EPSSubDevices): + """DMM monochromator signals at the cSAXS beamline.""" + + dmm_temp_surface_1 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-ETTC-3010:TEMP", + add_prefix=("",), + name="dmm_temp_surface_1", + kind=Kind.omitted, + doc="DMM Temp Surface 1", + auto_monitor=True, + labels={"temp"}, + ) + dmm_temp_surface_2 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-ETTC-3020:TEMP", + add_prefix=("",), + name="dmm_temp_surface_2", + kind=Kind.omitted, + doc="DMM Temp Surface 2", + auto_monitor=True, + labels={"temp"}, + ) + dmm_temp_shield_1_disaster = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-ETTC-3030:TEMP", + add_prefix=("",), + name="dmm_temp_shield_1_disaster", + kind=Kind.omitted, + doc="DMM Temp Shield 1 (disaster)", + auto_monitor=True, + labels={"temp"}, + ) + dmm_temp_shield_2_disaster = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-ETTC-3040:TEMP", + add_prefix=("",), + name="dmm_temp_shield_2_disaster", + kind=Kind.omitted, + doc="DMM Temp Shield 2 (disaster)", + auto_monitor=True, + labels={"temp"}, + ) + + dmm_translation_thru = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMLS-3010:THRU", + add_prefix=("",), + name="dmm_translation_thru", + kind=Kind.omitted, + doc="DMM Translation ThruPos", + auto_monitor=True, + labels={"switch"}, + ) + dmm_translation_in = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMLS-3020:IN", + add_prefix=("",), + name="dmm_translation_in", + kind=Kind.omitted, + doc="DMM Translation InPos", + auto_monitor=True, + labels={"switch"}, + ) + dmm_bragg_thru = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMLS-3030:THRU", + add_prefix=("",), + name="dmm_bragg_thru", + kind=Kind.omitted, + doc="DMM Bragg ThruPos", + auto_monitor=True, + labels={"switch"}, + ) + dmm_bragg_in = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMLS-3040:IN", + add_prefix=("",), + name="dmm_bragg_in", + kind=Kind.omitted, + doc="DMM Bragg InPos", + auto_monitor=True, + labels={"switch"}, + ) + + dmm_heater_fault_xtal_1 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMSW-3050:SWITCH", + add_prefix=("",), + name="dmm_heater_fault_xtal_1", + kind=Kind.omitted, + doc="DMM Heater Fault XTAL 1", + auto_monitor=True, + labels={"fault"}, + ) + dmm_heater_fault_xtal_2 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMSW-3060:SWITCH", + add_prefix=("",), + name="dmm_heater_fault_xtal_2", + kind=Kind.omitted, + doc="DMM Heater Fault XTAL 2", + auto_monitor=True, + labels={"fault"}, + ) + dmm_heater_fault_support_1 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM-EMSW-3070:SWITCH", + add_prefix=("",), + name="dmm_heater_fault_support_1", + kind=Kind.omitted, + doc="DMM Heater Fault Support 1", + auto_monitor=True, + labels={"fault"}, + ) + + dmm_energy = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM1:ENERGY-GET", + add_prefix=("",), + name="dmm_energy", + kind=Kind.omitted, + doc="DMM monochromator energy", + auto_monitor=True, + labels={"energy"}, + ) + dmm_position = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM1:POSITION", + add_prefix=("",), + name="dmm_position", + kind=Kind.omitted, + doc="DMM Position", + auto_monitor=True, + labels={"string"}, + ) + dmm_stripe = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-DMM1:STRIPE", + add_prefix=("",), + name="dmm_stripe", + kind=Kind.omitted, + doc="DMM Stripe", + auto_monitor=True, + labels={"string"}, + ) + + +class CCMMonochromator(EPSSubDevices): + """CCM monochromator signals at the cSAXS beamline.""" + + ccm_temp_crystal = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM-ETTC-4010:TEMP", + add_prefix=("",), + name="ccm_temp_crystal", + kind=Kind.omitted, + doc="CCM Temp Crystal", + auto_monitor=True, + labels={"temp"}, + ) + ccm_temp_shield_disaster = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM-ETTC-4020:TEMP", + add_prefix=("",), + name="ccm_temp_shield_disaster", + kind=Kind.omitted, + doc="CCM Temp Shield (disaster)", + auto_monitor=True, + labels={"temp"}, + ) + + ccm_heater_fault_1 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM-EMSW-4010:SWITCH", + add_prefix=("",), + name="ccm_heater_fault_1", + kind=Kind.omitted, + doc="CCM Heater Fault 1", + auto_monitor=True, + labels={"fault"}, + ) + ccm_heater_fault_2 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM-EMSW-4020:SWITCH", + add_prefix=("",), + name="ccm_heater_fault_2", + kind=Kind.omitted, + doc="CCM Heater Fault 2", + auto_monitor=True, + labels={"fault"}, + ) + ccm_heater_fault_3 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM-EMSW-4030:SWITCH", + add_prefix=("",), + name="ccm_heater_fault_3", + kind=Kind.omitted, + doc="CCM Heater Fault 3", + auto_monitor=True, + labels={"fault"}, + ) + + ccm_energy = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM1:ENERGY-GET", + add_prefix=("",), + name="ccm_energy", + kind=Kind.omitted, + doc="CCM monochromator energy", + auto_monitor=True, + labels={"energy"}, + ) + ccm_position = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CCM1:POSITION", + add_prefix=("",), + name="ccm_position", + kind=Kind.omitted, + doc="CCM Position", + auto_monitor=True, + labels={"string"}, + ) + + +class CoolingWater(EPSSubDevices): + """Cooling water signals at the cSAXS beamline.""" + + op_sl1_efsw_2010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-SL1-EFSW-2010:FLOW", + add_prefix=("",), + name="op_sl1_efsw_2010_flow", + kind=Kind.omitted, + doc="OP-SL1-EFSW-2010", + auto_monitor=True, + labels={"flow"}, + ) + op_sl2_efsw_2010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-SL2-EFSW-2010:FLOW", + add_prefix=("",), + name="op_sl2_efsw_2010_flow", + kind=Kind.omitted, + doc="OP-SL2-EFSW-2010", + auto_monitor=True, + labels={"flow"}, + ) + op_eb1_efsw_5010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-EB1-EFSW-5010:FLOW", + add_prefix=("",), + name="op_eb1_efsw_5010_flow", + kind=Kind.omitted, + doc="OP-EB1-EFSW-5010", + auto_monitor=True, + labels={"flow"}, + ) + op_eb1_efsw_5020_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-EB1-EFSW-5020:FLOW", + add_prefix=("",), + name="op_eb1_efsw_5020_flow", + kind=Kind.omitted, + doc="OP-EB1-EFSW-5020", + auto_monitor=True, + labels={"flow"}, + ) + op_sl3_efsw_5010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-SL3-EFSW-5010:FLOW", + add_prefix=("",), + name="op_sl3_efsw_5010_flow", + kind=Kind.omitted, + doc="OP-SL3-EFSW-5010", + auto_monitor=True, + labels={"flow"}, + ) + op_kb_efsw_6010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-KB-EFSW-6010:FLOW", + add_prefix=("",), + name="op_kb_efsw_6010_flow", + kind=Kind.omitted, + doc="OP-KB-EFSW-6010", + auto_monitor=True, + labels={"flow"}, + ) + op_psh1_efsw_7010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-PSH1-EFSW-7010:FLOW", + add_prefix=("",), + name="op_psh1_efsw_7010_flow", + kind=Kind.omitted, + doc="OP-PSH1-EFSW-7010", + auto_monitor=True, + labels={"flow"}, + ) + es_eb2_efsw_1010_flow = Cpt( + EpicsSignalRO, + read_pv="X12SA-ES-EB2-EFSW-1010:FLOW", + add_prefix=("",), + name="es_eb2_efsw_1010_flow", + kind=Kind.omitted, + doc="ES-EB2-EFSW-1010", + auto_monitor=True, + labels={"flow"}, + ) + + op_cs_ecvw_0010 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CS-ECVW-0010:PLC_OPEN", + add_prefix=("",), + name="op_cs_ecvw_0010", + kind=Kind.omitted, + doc="OP-CS-ECVW-0010", + auto_monitor=True, + labels={"valve"}, + ) + op_cs_ecvw_0020 = Cpt( + EpicsSignalRO, + read_pv="X12SA-OP-CS-ECVW-0020:PLC_OPEN", + add_prefix=("",), + name="op_cs_ecvw_0020", + kind=Kind.omitted, + doc="OP-CS-ECVW-0020", + auto_monitor=True, + labels={"valve"}, + ) + + +class EPS(PSIDeviceBase): + + alarms = Cpt(EPSAlarms, name="alarms") + valves = Cpt(Valves, name="valves") + shutters = Cpt(Shutters, name="shutters") + dmm_monochromator = Cpt(DMMMonochromator, name="dmm_monochromator") + ccm_monochromator = Cpt(CCMMonochromator, name="ccm_monochromator") + cooling_water = Cpt(CoolingWater, name="cooling_water") + + # Acknowledgment signals for PLC communication (if needed for future use) + ackerr = EpicsSignal("X12SA-EPS-PLC:ACKERR-REQUEST") + request = EpicsSignal("X12SA-OP-CS-ECVW:PLC_REQUEST") + + def _notify(self, msg: str, show_as_client_msg: bool = True): + """Utility method to print a message, and optionally send it to the client UI if it should be shown also as a client message.""" + try: + if show_as_client_msg: + self.device_manager.connector.send_client_info(msg, scope="", show_asap=True) + else: + print(msg) + except Exception: + logger.error(f"Failed to send client message, falling back to print: {msg}") + print(str(msg)) + + # ---------------------------------------------------------- + # Water cooling operation + # ---------------------------------------------------------- + + def safe_get(self, sig, default=None): + """Helper method to safely get a signal value, returning a default if there's an error.""" + try: + return sig.get() + except Exception as ex: + logger.warning(f"Failed to get signal {sig.pvname}: {ex}") + return default + + def water_cooling_op(self): + """ + Open ECVW valves, reset EPS alarms, monitor for 20s, + then ensure stability (valves remain open) for 10s. + All messages sent to client. + """ + + POLL_PERIOD = 2 + TIMEOUT = 20 + STABILITY = 15 + + self._notify("=== Water Cooling Operation ===") + + # --- Signals --- + eps_alarm_sig = self.alarms.eps_alarm_cnt + ackerr = self.ackerr + request = self.request + + valves = [self.cooling_water.op_cs_ecvw_0010, self.cooling_water.op_cs_ecvw_0020] + + # Flow channels list extracted from CHANNELS + flow_items = [ + walk.item + for walk in self.cooling_water.walk_signals() + if "flow" in walk.item._ophyd_labels_ + ] + + # --- Step 1: EPS alarm reset --- + alarm_value = self.safe_get(eps_alarm_sig, 0) + if alarm_value and alarm_value > 0: + self._notify(f"[WaterCooling] EPS alarms present ({alarm_value}) → resetting…") + try: + ackerr.put(1) + except Exception as ex: + self._notify(f"[WaterCooling] WARNING: ACKERR write failed: {ex}") + time.sleep(0.3) + else: + self._notify("[WaterCooling] No EPS alarms detected.") + + # --- Step 2: Issue open request --- + self._notify("[WaterCooling] Sending cooling-valve OPEN request…") + try: + request.put(1) + except Exception as ex: + self._notify(f"[WaterCooling] ERROR: Failed to send OPEN request: {ex}") + return False + + # --- Step 3: Monitoring loop (clean client table output) --- + start = time.time() + end = start + TIMEOUT + stable_until = None + + # Print (server-side) header once + print("Monitoring valves and flow sensors...") + print(f" Valves: {valves[0].attr_name[-4:]}, {valves[1].attr_name[-4:]}") + print(f" Note: stability requires valves to remain OPEN for {STABILITY} seconds.") + + # One table header to the client (via device manager) + # Fixed-width columns for alignment in monospaced UI + table_header = f"{'Time':>6} | {'Valves':<21} | {'Flows (OK/FAIL/N/A)':<20}" + self._notify(table_header) + + def snapshot(): + # Valve snapshot + v_states = [self.safe_get(v, None) for v in valves] + v1 = f"{valves[0].attr_name[-4:]}=" + ( + "OPEN " + if v_states[0] is True or v_states[0] == 1 + else "CLOSED" if v_states[0] is False or v_states[0] == 0 else "N/A " + ) + v2 = f"{valves[1].attr_name[-4:]}=" + ( + "OPEN " + if v_states[1] is True or v_states[1] == 1 + else "CLOSED" if v_states[1] is False or v_states[1] == 0 else "N/A " + ) + # 2 valves with a single space between => width ~ 21 + valve_str = f"{v1} {v2}" + + # Flow summary: OK/FAIL/N/A counts (compact) + flow_states = [] + for fsig in flow_items: + fval = self.safe_get(fsig, None) + flow_states.append( + True if fval in (1, True) else False if fval in (0, False) else None + ) + + ok = sum(1 for f in flow_states if f is True) + fail = sum(1 for f in flow_states if f is False) + na = sum(1 for f in flow_states if f is None) + flow_summary = f"{ok:>2} / {fail:>2} / {na:>2}" + + return v_states, valve_str, flow_summary + + while True: + # TODO Consider adding a timeout to avoid infinite loop. + now = time.time() + elapsed = int(now - start) + + if now > end: + # One last line to client + v_states, valves_s, flows_s = snapshot() + self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}") + print("→ TIMEOUT: Cooling valves failed to remain OPEN.") + return False + + # Live snapshot + v_states, valves_s, flows_s = snapshot() + # Exactly one concise line to client per cycle + self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}") + + both_open = all(s is not None and bool(s) for s in v_states) + + if both_open: + if stable_until is None: + stable_until = now + STABILITY + print( + f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…" + ) + else: + if now >= stable_until: + print("→ SUCCESS: Valves remained OPEN during stability window.") + return True + else: + if stable_until is not None: + print("[WaterCooling] Valve closed again → restarting stability window.") + stable_until = None + + time.sleep(POLL_PERIOD) + + def show_all(self): + red = "\x1b[91m" + green = "\x1b[92m" + white = "\x1b[0m" + bold = "\x1b[1m" + cyan = "\x1b[96m" + + # ---- New: enum maps for numeric -> string rendering ---- + POSITION_ENUM = {0: "out of beam", 1: "in beam"} + STRIPE_ENUM = {0: "Stripe 1 W/B4C", 1: "Stripe 2 NiV/B4C"} + POSITION_ATTRS = { + self.dmm_monochromator.dmm_position.attr_name, + self.ccm_monochromator.ccm_position.attr_name, + } + STRIPE_ATTRS = {self.dmm_monochromator.dmm_stripe.attr_name} + + def is_bool_like(v): + return isinstance(v, (bool, int)) and v in (0, 1, True, False) + + # ---- Changed: accept attr in formatter so we can apply enum mapping ---- + def fmt_value(value: any, signal: EpicsSignalRO): + if value is None: + return f"{red}MISSING{white}" + + attr = signal.attr_name + + # ---------- Explicit enum mappings by attribute ---------- + if attr in POSITION_ATTRS: + # Position comes as numeric 0/1 + try: + iv = int(value) + return POSITION_ENUM.get(iv, f"{iv}") + except Exception: + # Fallback if it’s already a string or unexpected + return f"{value}" + + if attr in STRIPE_ATTRS: + # Stripe comes as numeric 0/1 + try: + iv = int(value) + return STRIPE_ENUM.get(iv, f"{iv}") + except Exception: + return f"{value}" + + # ------------------- TEMPERATURE ------------------- + if "temp" in signal._ophyd_labels_ and isinstance(value, (int, float)): + return f"{value:.1f}" + + # ------------------- ENERGY ------------------------ + if "energy" in signal._ophyd_labels_ and isinstance(value, (int, float)): + return f"{value:.4f}" + + # ------------------- STRINGS ----------------------- + if "string" in signal._ophyd_labels_ or "position" in signal._ophyd_labels_: + # For other strings, just echo the value + return f"{value}" + + # ------------------- SWITCH (ACTIVE/INACTIVE) ------ + if "switch" in signal._ophyd_labels_ and is_bool_like(value): + return f"{green+'ACTIVE'+white if value else red+'INACTIVE'+white}" + + # ------------------- FAULT (OK/FAULT) -------------- + if "fault" in signal._ophyd_labels_ and is_bool_like(value): + return f"{green+'OK'+white if not value else red+'FAULT'+white}" + + # ------------------- VALVE/SHUTTER ----------------- + if ( + "valve" in signal._ophyd_labels_ or "shutter" in signal._ophyd_labels_ + ) and is_bool_like(value): + return f"{green+'OPEN'+white if value else red+'CLOSED'+white}" + + # ------------------- FLOW (OK/FAIL) ---------------- + if "flow" in signal._ophyd_labels_ and is_bool_like(value): + return f"{green}OK{white}" if bool(value) else f"{red}FAIL{white}" + + # ------------------- FALLBACK ----------------------- + return f"{value}" + + # ------------------- PRINT START --------------------- + print(f"{bold}X12SA EPS status{white}") + + for sub_device in self.walk_components(): + print(f"\n{bold}{sub_device.name}{white}") + + rows = [] + for walk in sub_device.walk_components(): + cpt: Cpt = walk.ancestors[-1] + it: EpicsSignalRO = walk.item + val = self.safe_get(it) + rows.append((cpt.doc, val, it._read_pv.pvname, it._ophyd_labels_[0], it.attr_name)) + + label_width = max(32, *(len(label) for (label, _, _, _, _) in rows)) + + for label, value, pv, kind, _attr in rows: + fv = fmt_value(value, it) # <-- pass attr to formatter + print(f" - {label:<{label_width}} {fv}") + + if sub_device.name == "cooling_water": + v1 = self.safe_get(self.cooling_water.op_cs_ecvw_0010) + v2 = self.safe_get(self.cooling_water.op_cs_ecvw_0020) + + def closed(v): + return is_bool_like(v) and not bool(v) + + if closed(v1) and closed(v2): + print( + f"\n{cyan}Hint:{white} Both water cooling valves are CLOSED.\n" + f"You can open them using: {bold}dev.x12saEPS.water_cooling_op(){white}" + ) + + # ---------------------------------------------------------- + # Consistency report + # ---------------------------------------------------------- + # def consistency_report(self, *, verbose=True): + # missing = [] + # dupes = [] + # seen = {} + + # for sub_device in self.walk_components(): + # section = sub_device.name + # for walk in sub_device.walk_components(): + # cpt: Cpt = walk.ancestors[-1] + # it: EpicsSignalRO = walk.item + # if not hasattr(self, it["attr"]): + # missing.append((section, it["attr"], it["label"], it["pv"])) + + # pv = it["pv"] + # if pv in seen: + # dupes.append((pv, seen[pv], (section, it["attr"], it["label"]))) + # else: + # seen[pv] = (section, it["attr"], it["label"]) + + # if verbose: + # print("=== Consistency Report ===") + + # if missing: + # 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:") + # for pv, f1, f2 in dupes: + # print(f" {pv} → {f1} AND {f2}") + # else: + # print("\nNo duplicate PVs.") + + # return {"missing_attrs": missing, "duplicate_pvs": dupes} # ============================================================================== @@ -154,19 +890,13 @@ CHANNELS = { def create_dynamic_eps_class(): class_attrs = dict( - USER_ACCESS=["show_all", "water_cooling_op"], - SUB_VALUE="value", - _default_sub="value", + USER_ACCESS=["show_all", "water_cooling_op"], SUB_VALUE="value", _default_sub="value" ) # Dynamically define Components for section, items in CHANNELS.items(): for it in items: - class_attrs[it["attr"]] = Cpt( - EpicsSignal, - name=it["label"], - read_pv=it["pv"] - ) + class_attrs[it["attr"]] = Cpt(EpicsSignal, name=it["label"], read_pv=it["pv"]) # ---------------------------------------------------------- # Methods @@ -260,25 +990,21 @@ def create_dynamic_eps_class(): # One table header to the client (via device manager) # Fixed-width columns for alignment in monospaced UI - table_header = ( - f"{'Time':>6} | {'Valves':<21} | {'Flows (OK/FAIL/N/A)':<20}" - ) + table_header = f"{'Time':>6} | {'Valves':<21} | {'Flows (OK/FAIL/N/A)':<20}" self._notify(table_header) def snapshot(): # Valve snapshot v_states = [safe_get(v, None) for v in valves] - v1 = ( - f"{valve_attrs[0][-4:]}=" - + ("OPEN " if v_states[0] is True or v_states[0] == 1 else - "CLOSED" if v_states[0] is False or v_states[0] == 0 else - "N/A ") + v1 = f"{valve_attrs[0][-4:]}=" + ( + "OPEN " + if v_states[0] is True or v_states[0] == 1 + else "CLOSED" if v_states[0] is False or v_states[0] == 0 else "N/A " ) - v2 = ( - f"{valve_attrs[1][-4:]}=" - + ("OPEN " if v_states[1] is True or v_states[1] == 1 else - "CLOSED" if v_states[1] is False or v_states[1] == 0 else - "N/A ") + v2 = f"{valve_attrs[1][-4:]}=" + ( + "OPEN " + if v_states[1] is True or v_states[1] == 1 + else "CLOSED" if v_states[1] is False or v_states[1] == 0 else "N/A " ) # 2 valves with a single space between => width ~ 21 valve_str = f"{v1} {v2}" @@ -288,7 +1014,9 @@ def create_dynamic_eps_class(): for f_attr, _lbl in flow_items: fsig = getattr(self, f_attr, None) fval = safe_get(fsig, None) - flow_states.append(True if fval in (1, True) else False if fval in (0, False) else None) + flow_states.append( + True if fval in (1, True) else False if fval in (0, False) else None + ) ok = sum(1 for f in flow_states if f is True) fail = sum(1 for f in flow_states if f is False) @@ -318,7 +1046,9 @@ def create_dynamic_eps_class(): if both_open: if stable_until is None: stable_until = now + STABILITY - print(f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…") + print( + f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…" + ) else: if now >= stable_until: print("→ SUCCESS: Valves remained OPEN during stability window.") @@ -330,158 +1060,6 @@ def create_dynamic_eps_class(): time.sleep(POLL_PERIOD) - def show_all(self): - red = "\x1b[91m" - green = "\x1b[92m" - white = "\x1b[0m" - bold = "\x1b[1m" - cyan = "\x1b[96m" - - # ---- New: enum maps for numeric -> string rendering ---- - POSITION_ENUM = { - 0: "out of beam", - 1: "in beam", - } - STRIPE_ENUM = { - 0: "Stripe 1 W/B4C", - 1: "Stripe 2 NiV/B4C", - } - POSITION_ATTRS = {"DMM1_POSITION", "CCM1_POSITION"} - STRIPE_ATTRS = {"DMM1_STRIPE"} - - def safe_get(attr): - obj = getattr(self, attr, None) - try: - return obj.get() - except Exception: - return None - - def is_bool_like(v): - return isinstance(v, (bool, int)) and v in (0, 1, True, False) - - # ---- Changed: accept attr in formatter so we can apply enum mapping ---- - def fmt_value(value, pv, kind, attr): - if value is None: - return f"{red}MISSING{white}" - - # ---------- Explicit enum mappings by attribute ---------- - if attr in POSITION_ATTRS: - # Position comes as numeric 0/1 - try: - iv = int(value) - return POSITION_ENUM.get(iv, f"{iv}") - except Exception: - # Fallback if it’s already a string or unexpected - return f"{value}" - - if attr in STRIPE_ATTRS: - # Stripe comes as numeric 0/1 - try: - iv = int(value) - return STRIPE_ENUM.get(iv, f"{iv}") - except Exception: - return f"{value}" - - # ------------------- TEMPERATURE ------------------- - if kind == "temp" and isinstance(value, (int, float)): - return f"{value:.1f}" - - # ------------------- ENERGY ------------------------ - if kind == "energy" and isinstance(value, (int, float)): - return f"{value:.4f}" - - # ------------------- STRINGS ----------------------- - if kind in ("string", "position"): - # For other strings, just echo the value - return f"{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}" - - # ------------------- VALVE/SHUTTER ----------------- - if kind in ("valve", "shutter") and is_bool_like(value): - return f"{green+'OPEN'+white if value else red+'CLOSED'+white}" - - # ------------------- FLOW (OK/FAIL) ---------------- - if kind == "flow" and is_bool_like(value): - return f"{green}OK{white}" if bool(value) else f"{red}FAIL{white}" - - # ------------------- FALLBACK ----------------------- - return f"{value}" - - # ------------------- PRINT START --------------------- - print(f"{bold}X12SA EPS status{white}") - - for section, items in CHANNELS.items(): - print(f"\n{bold}{section}{white}") - - rows = [] - for it in items: - val = safe_get(it["attr"]) - rows.append((it["label"], val, it["pv"], it["kind"], it["attr"])) - - label_width = max(32, *(len(label) for (label, _, _, _, _) in rows)) - - for label, value, pv, kind, _attr in rows: - fv = fmt_value(value, pv, kind, _attr) # <-- pass attr to formatter - print(f" - {label:<{label_width}} {fv}") - - if section == "Cooling Water": - v1 = safe_get("OPCSECVW0010") - v2 = safe_get("OPCSECVW0020") - - def closed(v): return is_bool_like(v) and not bool(v) - - if closed(v1) and closed(v2): - print( - f"\n{cyan}Hint:{white} Both water cooling valves are CLOSED.\n" - f"You can open them using: {bold}dev.x12saEPS.water_cooling_op(){white}" - ) - - # ---------------------------------------------------------- - # Consistency report - # ---------------------------------------------------------- - def consistency_report(self, *, verbose=True): - missing = [] - dupes = [] - seen = {} - - for section, items in CHANNELS.items(): - for it in items: - if not hasattr(self, it["attr"]): - missing.append((section, it["attr"], it["label"], it["pv"])) - - pv = it["pv"] - if pv in seen: - dupes.append((pv, seen[pv], (section, it["attr"], it["label"]))) - else: - seen[pv] = (section, it["attr"], it["label"]) - - if verbose: - print("=== Consistency Report ===") - - if missing: - 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:") - for pv, f1, f2 in dupes: - print(f" {pv} → {f1} AND {f2}") - else: - print("\nNo duplicate PVs.") - - return {"missing_attrs": missing, "duplicate_pvs": dupes} - - # Attach methods to class attributes class_attrs["show_all"] = DynamicMethods.show_all class_attrs["consistency_report"] = DynamicMethods.consistency_report -- 2.49.1 From f35c51efa73dc1c040898f55bc72555bb964b025 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 19:03:16 +0100 Subject: [PATCH 08/11] test(eps): Add tests for EPS device --- csaxs_bec/devices/epics/eps.py | 222 +++----------------------------- tests/tests_devices/test_eps.py | 84 ++++++++++++ 2 files changed, 104 insertions(+), 202 deletions(-) create mode 100644 tests/tests_devices/test_eps.py diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index 499c98d..d7678f5 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -577,8 +577,26 @@ class EPS(PSIDeviceBase): cooling_water = Cpt(CoolingWater, name="cooling_water") # Acknowledgment signals for PLC communication (if needed for future use) - ackerr = EpicsSignal("X12SA-EPS-PLC:ACKERR-REQUEST") - request = EpicsSignal("X12SA-OP-CS-ECVW:PLC_REQUEST") + ackerr = Cpt( + EpicsSignal, + read_pv="X12SA-EPS-PLC:ACKERR-REQUEST", + add_prefix=("",), + name="ackerr", + kind=Kind.omitted, + doc="ACKERR request - OP-CS-ECVW-0020", + auto_monitor=True, + labels={"request"}, + ) + request = Cpt( + EpicsSignal, + read_pv="X12SA-OP-CS-ECVW:PLC_REQUEST", + add_prefix=("",), + name="op_cs_ecvw_request", + kind=Kind.omitted, + doc="PLC request - OP-CS-ECVW-PLC_REQUEST", + auto_monitor=True, + labels={"request"}, + ) def _notify(self, msg: str, show_as_client_msg: bool = True): """Utility method to print a message, and optionally send it to the client UI if it should be shown also as a client message.""" @@ -882,203 +900,3 @@ class EPS(PSIDeviceBase): # print("\nNo duplicate PVs.") # return {"missing_attrs": missing, "duplicate_pvs": dupes} - - -# ============================================================================== -# DYNAMIC CLASS CREATION — Ophyd-safe -# ============================================================================== -def create_dynamic_eps_class(): - - class_attrs = dict( - USER_ACCESS=["show_all", "water_cooling_op"], SUB_VALUE="value", _default_sub="value" - ) - - # Dynamically define Components - for section, items in CHANNELS.items(): - for it in items: - class_attrs[it["attr"]] = Cpt(EpicsSignal, name=it["label"], read_pv=it["pv"]) - - # ---------------------------------------------------------- - # Methods - # ---------------------------------------------------------- - class DynamicMethods: - - # ------------------------ - # Client messaging helper - # ------------------------ - def _notify(self, msg: str, show_asap=True): - """ - Send a message to the client UI through the device manager. - Falls back to print() if device_manager/connector is missing. - """ - try: - conn = getattr(getattr(self, "device_manager", None), "connector", None) - if conn and hasattr(conn, "send_client_info"): - conn.send_client_info(msg, scope="", show_asap=show_asap) - else: - print(msg) - except Exception: - print(msg) - - # ---------------------------------------------------------- - # Water cooling operation - # ---------------------------------------------------------- - def water_cooling_op(self): - """ - Open ECVW valves, reset EPS alarms, monitor for 20s, - then ensure stability (valves remain open) for 10s. - All messages sent to client. - """ - from ophyd import EpicsSignal - - POLL_PERIOD = 2 - TIMEOUT = 20 - STABILITY = 15 - - self._notify("=== Water Cooling Operation ===") - - # --- Signals --- - eps_alarm_sig = getattr(self, "EPSAlarmCnt", None) - ackerr = EpicsSignal("X12SA-EPS-PLC:ACKERR-REQUEST") - request = EpicsSignal("X12SA-OP-CS-ECVW:PLC_REQUEST") - - valve_attrs = ["OPCSECVW0010", "OPCSECVW0020"] - valves = [getattr(self, a, None) for a in valve_attrs] - - # Flow channels list extracted from CHANNELS - flow_items = [ - (it["attr"], it["label"]) - for it in CHANNELS["Cooling Water"] - if it["kind"] == "flow" - ] - - def safe_get(sig, default=None): - try: - return sig.get() - except Exception: - return default - - # --- Step 1: EPS alarm reset --- - alarm_value = safe_get(eps_alarm_sig, 0) - if alarm_value and alarm_value > 0: - self._notify(f"[WaterCooling] EPS alarms present ({alarm_value}) → resetting…") - try: - ackerr.put(1) - except Exception as ex: - self._notify(f"[WaterCooling] WARNING: ACKERR write failed: {ex}") - time.sleep(0.3) - else: - self._notify("[WaterCooling] No EPS alarms detected.") - - # --- Step 2: Issue open request --- - self._notify("[WaterCooling] Sending cooling‑valve OPEN request…") - try: - request.put(1) - except Exception as ex: - self._notify(f"[WaterCooling] ERROR: Failed to send OPEN request: {ex}") - return False - - # --- Step 3: Monitoring loop (clean client table output) --- - start = time.time() - end = start + TIMEOUT - stable_until = None - - # Print (server-side) header once - print("Monitoring valves and flow sensors...") - print(f" Valves: {valve_attrs[0][-4:]}, {valve_attrs[1][-4:]}") - print(f" Note: stability requires valves to remain OPEN for {STABILITY} seconds.") - - # One table header to the client (via device manager) - # Fixed-width columns for alignment in monospaced UI - table_header = f"{'Time':>6} | {'Valves':<21} | {'Flows (OK/FAIL/N/A)':<20}" - self._notify(table_header) - - def snapshot(): - # Valve snapshot - v_states = [safe_get(v, None) for v in valves] - v1 = f"{valve_attrs[0][-4:]}=" + ( - "OPEN " - if v_states[0] is True or v_states[0] == 1 - else "CLOSED" if v_states[0] is False or v_states[0] == 0 else "N/A " - ) - v2 = f"{valve_attrs[1][-4:]}=" + ( - "OPEN " - if v_states[1] is True or v_states[1] == 1 - else "CLOSED" if v_states[1] is False or v_states[1] == 0 else "N/A " - ) - # 2 valves with a single space between => width ~ 21 - valve_str = f"{v1} {v2}" - - # Flow summary: OK/FAIL/N/A counts (compact) - flow_states = [] - for f_attr, _lbl in flow_items: - fsig = getattr(self, f_attr, None) - fval = safe_get(fsig, None) - flow_states.append( - True if fval in (1, True) else False if fval in (0, False) else None - ) - - ok = sum(1 for f in flow_states if f is True) - fail = sum(1 for f in flow_states if f is False) - na = sum(1 for f in flow_states if f is None) - flow_summary = f"{ok:>2} / {fail:>2} / {na:>2}" - - return v_states, valve_str, flow_summary - - while True: - now = time.time() - elapsed = int(now - start) - - if now > end: - # One last line to client - v_states, valves_s, flows_s = snapshot() - self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}") - print("→ TIMEOUT: Cooling valves failed to remain OPEN.") - return False - - # Live snapshot - v_states, valves_s, flows_s = snapshot() - # Exactly one concise line to client per cycle - self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}") - - both_open = all(s is not None and bool(s) for s in v_states) - - if both_open: - if stable_until is None: - stable_until = now + STABILITY - print( - f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…" - ) - else: - if now >= stable_until: - print("→ SUCCESS: Valves remained OPEN during stability window.") - return True - else: - if stable_until is not None: - print("[WaterCooling] Valve closed again → restarting stability window.") - stable_until = None - - time.sleep(POLL_PERIOD) - - # Attach methods to class attributes - class_attrs["show_all"] = DynamicMethods.show_all - class_attrs["consistency_report"] = DynamicMethods.consistency_report - class_attrs["water_cooling_op"] = DynamicMethods.water_cooling_op - class_attrs["_notify"] = DynamicMethods._notify - - # ---------------------------------------------------------- - # Custom __init__ to accept device_manager (BEC) - # ---------------------------------------------------------- - def __init__(self, *args, device_manager=None, **kwargs): - super(cls, self).__init__(*args, **kwargs) - self.device_manager = device_manager - - # Create class - cls = type("cSAXSEps", (Device,), class_attrs) - setattr(cls, "__init__", __init__) - - return cls - - -# Create final class for BEC import -cSAXSEps = create_dynamic_eps_class() diff --git a/tests/tests_devices/test_eps.py b/tests/tests_devices/test_eps.py new file mode 100644 index 0000000..86a590e --- /dev/null +++ b/tests/tests_devices/test_eps.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import pytest +from ophyd_devices.tests.utils import patched_device + +from csaxs_bec.devices.epics.eps import EPS + +ALL_PVS = [ + # ALARMS + "X12SA-EPS-PLC:AlarmCnt_EPS", + "ARS00-MIS-PLC-01:AlarmCnt_Frontends", + # FRONTEND VALVES + "X12SA-FE-VVPG-0000:PLC_OPEN", + "X12SA-FE-VVPG-1010:PLC_OPEN", + "X12SA-FE-VVFV-2010:PLC_OPEN", + "X12SA-FE-VVPG-2010:PLC_OPEN", + # Optics VALVES + "X12SA-OP-VVPG-1010:PLC_OPEN", + "X12SA-OP-VVPG-2010:PLC_OPEN", + "X12SA-OP-VVPG-3010:PLC_OPEN", + "X12SA-OP-VVPG-3020:PLC_OPEN", + "X12SA-OP-VVPG-4010:PLC_OPEN", + "X12SA-OP-VVPG-5010:PLC_OPEN", + "X12SA-OP-VVPG-6010:PLC_OPEN", + "X12SA-OP-VVPG-7010:PLC_OPEN", + # Endstation VALVES + "X12SA-ES-VVPG-1010:PLC_OPEN", + # Frontend SHUTTERS + "X12SA-FE-PSH1-EMLS-0010:OPEN", + "X12SA-FE-STO1-EMLS-0010:OPEN", + # Optics SHUTTERS + "X12SA-OP-PSH1-EMLS-7010:OPEN", + # DMM Monochromator + "X12SA-OP-DMM-ETTC-3010:TEMP", + "X12SA-OP-DMM-ETTC-3020:TEMP", + "X12SA-OP-DMM-ETTC-3030:TEMP", + "X12SA-OP-DMM-ETTC-3040:TEMP", + "X12SA-OP-DMM-EMLS-3010:THRU", + "X12SA-OP-DMM-EMLS-3020:IN", + "X12SA-OP-DMM-EMLS-3030:THRU", + "X12SA-OP-DMM-EMLS-3040:IN", + "X12SA-OP-DMM-EMSW-3050:SWITCH", + "X12SA-OP-DMM-EMSW-3060:SWITCH", + "X12SA-OP-DMM-EMSW-3070:SWITCH", + "X12SA-OP-DMM1:ENERGY-GET", + "X12SA-OP-DMM1:POSITION", + "X12SA-OP-DMM1:STRIPE", + # CCM Monochromator + "X12SA-OP-CCM-ETTC-4010:TEMP", + "X12SA-OP-CCM-ETTC-4020:TEMP", + "X12SA-OP-CCM-EMSW-4010:SWITCH", + "X12SA-OP-CCM-EMSW-4020:SWITCH", + "X12SA-OP-CCM-EMSW-4030:SWITCH", + "X12SA-OP-CCM1:ENERGY-GET", + "X12SA-OP-CCM1:POSITION", + # Water Cooling + "X12SA-OP-SL1-EFSW-2010:FLOW", + "X12SA-OP-SL2-EFSW-2010:FLOW", + "X12SA-OP-EB1-EFSW-5010:FLOW", + "X12SA-OP-EB1-EFSW-5020:FLOW", + "X12SA-OP-SL3-EFSW-5010:FLOW", + "X12SA-OP-KB-EFSW-6010:FLOW", + "X12SA-OP-PSH1-EFSW-7010:FLOW", + "X12SA-ES-EB2-EFSW-1010:FLOW", + "X12SA-OP-CS-ECVW-0010:PLC_OPEN", + "X12SA-OP-CS-ECVW-0020:PLC_OPEN", + # Request PVs + "X12SA-EPS-PLC:ACKERR-REQUEST", + "X12SA-OP-CS-ECVW:PLC_REQUEST", +] + + +@pytest.fixture +def eps(): + dev_name = "EPS" + with patched_device(EPS, name=dev_name) as eps: + yield eps + + +def test_eps_has_signals(eps): + found_pvs = [walk.item._read_pv.pvname for walk in eps.walk_signals()] + assert set(found_pvs) == set( + ALL_PVS + ), f"Expected PVs {ALL_PVS} but found {set(ALL_PVS) - set(found_pvs)}" -- 2.49.1 From 501bc52867e3a4f71521f5c73fc36d4b5fd59e2b Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 20:24:06 +0100 Subject: [PATCH 09/11] fix(eps): Fix tests and eps integration --- csaxs_bec/devices/epics/eps.py | 94 ++++++++++++++++++++------------- tests/tests_devices/test_eps.py | 15 ++++++ 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index d7678f5..68ba3a3 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -52,8 +52,8 @@ class EPSAlarms(EPSSubDevices): ) -class Valves(EPSSubDevices): - """Valves at the cSAXS beamline.""" +class ValvesFrontend(EPSSubDevices): + """Valves frontend at the cSAXS beamline.""" ################ ### Frontend ### @@ -64,7 +64,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="fevvpg0000", kind=Kind.omitted, - doc="Frontend valve FE-VVPG-0000", + doc="FE-VVPG-0000", auto_monitor=True, labels={"valve"}, ) @@ -74,7 +74,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="fevvpg1010", kind=Kind.omitted, - doc="Frontend valve FE-VVPG-1010", + doc="FE-VVPG-1010", auto_monitor=True, labels={"valve"}, ) @@ -84,7 +84,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="fevvfv2010", kind=Kind.omitted, - doc="Frontend valve FE-VVFV-2010", + doc="FE-VVFV-2010", auto_monitor=True, labels={"valve"}, ) @@ -94,7 +94,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="fevvpg2010", kind=Kind.omitted, - doc="Frontend valve FE-VVPG-2010", + doc="FE-VVPG-2010", auto_monitor=True, labels={"valve"}, ) @@ -103,13 +103,17 @@ class Valves(EPSSubDevices): ### Optics ### ################ + +class ValvesOptics(EPSSubDevices): + """Valves at the optics hutch.""" + op_vvpg_1010 = Cpt( EpicsSignalRO, read_pv="X12SA-OP-VVPG-1010:PLC_OPEN", add_prefix=("",), name="opvvpg1010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-1010", + doc="OP-VVPG-1010", auto_monitor=True, labels={"valve"}, ) @@ -119,7 +123,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg2010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-2010", + doc="OP-VVPG-2010", auto_monitor=True, labels={"valve"}, ) @@ -129,7 +133,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg3010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-3010", + doc="OP-VVPG-3010", auto_monitor=True, labels={"valve"}, ) @@ -139,7 +143,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg3020", kind=Kind.omitted, - doc="Optics valve OP-VVPG-3020", + doc="OP-VVPG-3020", auto_monitor=True, labels={"valve"}, ) @@ -149,7 +153,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg4010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-4010", + doc="OP-VVPG-4010", auto_monitor=True, labels={"valve"}, ) @@ -159,7 +163,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg5010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-5010", + doc="OP-VVPG-5010", auto_monitor=True, labels={"valve"}, ) @@ -169,7 +173,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg6010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-6010", + doc="OP-VVPG-6010", auto_monitor=True, labels={"valve"}, ) @@ -179,7 +183,7 @@ class Valves(EPSSubDevices): add_prefix=("",), name="opvvpg7010", kind=Kind.omitted, - doc="Optics valve OP-VVPG-7010", + doc="OP-VVPG-7010", auto_monitor=True, labels={"valve"}, ) @@ -188,20 +192,24 @@ class Valves(EPSSubDevices): ### Endstation ### ################## + +class ValvesEndstation(EPSSubDevices): + """Endstation valves at the cSAXS beamline.""" + es_vvpg_1010 = Cpt( EpicsSignalRO, read_pv="X12SA-ES-VVPG-1010:PLC_OPEN", add_prefix=("",), name="esvvpg1010", kind=Kind.omitted, - doc="Endstation valve ES-VVPG-1010", + doc="ES-VVPG-1010", auto_monitor=True, labels={"valve"}, ) -class Shutters(EPSSubDevices): - """Shutters at the cSAXS beamline.""" +class ShuttersFrontend(EPSSubDevices): + """Shutters frontend.""" fe_psh1 = Cpt( EpicsSignalRO, @@ -209,7 +217,7 @@ class Shutters(EPSSubDevices): add_prefix=("",), name="fepsh1", kind=Kind.omitted, - doc="Frontend shutter FE-PSH1-EMLS-0010", + doc="FE-PSH1-EMLS-0010", auto_monitor=True, labels={"shutter"}, ) @@ -219,18 +227,22 @@ class Shutters(EPSSubDevices): add_prefix=("",), name="festo1", kind=Kind.omitted, - doc="Frontend shutter FE-STO1-EMLS-0010", + doc="FE-STO1-EMLS-0010", auto_monitor=True, labels={"shutter"}, ) + +class ShuttersEndstation(EPSSubDevices): + """Shutters at the endstation.""" + es_psh17010 = Cpt( EpicsSignalRO, read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN", add_prefix=("",), name="espsh17010", kind=Kind.omitted, - doc="Endstation shutter OP-PSH1-EMLS-7010", + doc="OP-PSH1-EMLS-7010", auto_monitor=True, labels={"shutter"}, ) @@ -358,7 +370,7 @@ class DMMMonochromator(EPSSubDevices): add_prefix=("",), name="dmm_energy", kind=Kind.omitted, - doc="DMM monochromator energy", + doc="DMM Energy", auto_monitor=True, labels={"energy"}, ) @@ -445,7 +457,7 @@ class CCMMonochromator(EPSSubDevices): add_prefix=("",), name="ccm_energy", kind=Kind.omitted, - doc="CCM monochromator energy", + doc="CCM Energy", auto_monitor=True, labels={"energy"}, ) @@ -569,12 +581,15 @@ class CoolingWater(EPSSubDevices): class EPS(PSIDeviceBase): - alarms = Cpt(EPSAlarms, name="alarms") - valves = Cpt(Valves, name="valves") - shutters = Cpt(Shutters, name="shutters") - dmm_monochromator = Cpt(DMMMonochromator, name="dmm_monochromator") - ccm_monochromator = Cpt(CCMMonochromator, name="ccm_monochromator") - cooling_water = Cpt(CoolingWater, name="cooling_water") + alarms = Cpt(EPSAlarms, name="alarms", doc="EPS Alarms") + valves_frontend = Cpt(ValvesFrontend, name="valves_frontend", doc="Valves Frontend") + valves_optics = Cpt(ValvesOptics, name="valves_optics", doc="Valves Optics Hutch") + valves_es = Cpt(ValvesEndstation, name="valves_es", doc="Valves ES Hutch") + shutters_frontend = Cpt(ShuttersFrontend, name="shutters_frontend", doc="Shutters Frontend") + shutters_es = Cpt(ShuttersEndstation, name="shutters_es", doc="Shutters Endstation") + dmm_monochromator = Cpt(DMMMonochromator, name="dmm_monochromator", doc="DMM Monochromator") + ccm_monochromator = Cpt(CCMMonochromator, name="ccm_monochromator", doc="CCM Monochromator") + cooling_water = Cpt(CoolingWater, name="cooling_water", doc="Cooling Water") # Acknowledgment signals for PLC communication (if needed for future use) ackerr = Cpt( @@ -831,23 +846,26 @@ class EPS(PSIDeviceBase): # ------------------- PRINT START --------------------- print(f"{bold}X12SA EPS status{white}") - for sub_device in self.walk_components(): - print(f"\n{bold}{sub_device.name}{white}") - + for name, component in self._sig_attrs.items(): + sub_device = getattr(self, name) rows = [] - for walk in sub_device.walk_components(): - cpt: Cpt = walk.ancestors[-1] - it: EpicsSignalRO = walk.item + # Only print sub-devices, not individual request signals + if not isinstance(sub_device, Device): + continue + print(f"\n{bold}{component.doc}{white}") + for sub_walk in sub_device.walk_components(): + cpt: Cpt = sub_walk.item + it: EpicsSignalRO = getattr(sub_device, cpt.attr) val = self.safe_get(it) - rows.append((cpt.doc, val, it._read_pv.pvname, it._ophyd_labels_[0], it.attr_name)) + rows.append((cpt.doc, val, it)) - label_width = max(32, *(len(label) for (label, _, _, _, _) in rows)) + label_width = max(32, *(len(label) for (label, _, _) in rows)) - for label, value, pv, kind, _attr in rows: + for label, value, it in rows: fv = fmt_value(value, it) # <-- pass attr to formatter print(f" - {label:<{label_width}} {fv}") - if sub_device.name == "cooling_water": + if sub_device.attr_name == "cooling_water": v1 = self.safe_get(self.cooling_water.op_cs_ecvw_0010) v2 = self.safe_get(self.cooling_water.op_cs_ecvw_0020) diff --git a/tests/tests_devices/test_eps.py b/tests/tests_devices/test_eps.py index 86a590e..61c914f 100644 --- a/tests/tests_devices/test_eps.py +++ b/tests/tests_devices/test_eps.py @@ -1,3 +1,4 @@ +# pylint: skip-file from __future__ import annotations import pytest @@ -78,7 +79,21 @@ def eps(): def test_eps_has_signals(eps): + """Test that all expected PVs are present in the eps device.""" found_pvs = [walk.item._read_pv.pvname for walk in eps.walk_signals()] assert set(found_pvs) == set( ALL_PVS ), f"Expected PVs {ALL_PVS} but found {set(ALL_PVS) - set(found_pvs)}" + + +# pylint: disable=line-too-long +expected_show_all_output = "\x1b[1mX12SA EPS status\x1b[0m\n\n\x1b[1mEPS Alarms\x1b[0m\n - X12SA EPS Alarm count 0\n - FrontEnd MIS Alarm count 0\n\n\x1b[1mValves Frontend\x1b[0m\n - FE-VVPG-0000 \x1b[91mCLOSED\x1b[0m\n - FE-VVPG-1010 \x1b[91mCLOSED\x1b[0m\n - FE-VVFV-2010 \x1b[91mCLOSED\x1b[0m\n - FE-VVPG-2010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mValves Optics Hutch\x1b[0m\n - OP-VVPG-1010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-2010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-3010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-3020 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-4010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-5010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-6010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-7010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mValves ES Hutch\x1b[0m\n - ES-VVPG-1010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mShutters Frontend\x1b[0m\n - FE-PSH1-EMLS-0010 \x1b[91mCLOSED\x1b[0m\n - FE-STO1-EMLS-0010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mShutters Endstation\x1b[0m\n - OP-PSH1-EMLS-7010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mDMM Monochromator\x1b[0m\n - DMM Temp Surface 1 0.0\n - DMM Temp Surface 2 0.0\n - DMM Temp Shield 1 (disaster) 0.0\n - DMM Temp Shield 2 (disaster) 0.0\n - DMM Translation ThruPos \x1b[91mINACTIVE\x1b[0m\n - DMM Translation InPos \x1b[91mINACTIVE\x1b[0m\n - DMM Bragg ThruPos \x1b[91mINACTIVE\x1b[0m\n - DMM Bragg InPos \x1b[91mINACTIVE\x1b[0m\n - DMM Heater Fault XTAL 1 \x1b[92mOK\x1b[0m\n - DMM Heater Fault XTAL 2 \x1b[92mOK\x1b[0m\n - DMM Heater Fault Support 1 \x1b[92mOK\x1b[0m\n - DMM Energy 0.0000\n - DMM Position out of beam\n - DMM Stripe Stripe 1 W/B4C\n\n\x1b[1mCCM Monochromator\x1b[0m\n - CCM Temp Crystal 0.0\n - CCM Temp Shield (disaster) 0.0\n - CCM Heater Fault 1 \x1b[92mOK\x1b[0m\n - CCM Heater Fault 2 \x1b[92mOK\x1b[0m\n - CCM Heater Fault 3 \x1b[92mOK\x1b[0m\n - CCM Energy 0.0000\n - CCM Position out of beam\n\n\x1b[1mCooling Water\x1b[0m\n - OP-SL1-EFSW-2010 \x1b[91mFAIL\x1b[0m\n - OP-SL2-EFSW-2010 \x1b[91mFAIL\x1b[0m\n - OP-EB1-EFSW-5010 \x1b[91mFAIL\x1b[0m\n - OP-EB1-EFSW-5020 \x1b[91mFAIL\x1b[0m\n - OP-SL3-EFSW-5010 \x1b[91mFAIL\x1b[0m\n - OP-KB-EFSW-6010 \x1b[91mFAIL\x1b[0m\n - OP-PSH1-EFSW-7010 \x1b[91mFAIL\x1b[0m\n - ES-EB2-EFSW-1010 \x1b[91mFAIL\x1b[0m\n - OP-CS-ECVW-0010 \x1b[91mCLOSED\x1b[0m\n - OP-CS-ECVW-0020 \x1b[91mCLOSED\x1b[0m\n\n\x1b[96mHint:\x1b[0m Both water cooling valves are CLOSED.\nYou can open them using: \x1b[1mdev.x12saEPS.water_cooling_op()\x1b[0m\n" + + +def test_eps_show_all(eps, capsys): + """Test that the show_all method outputs the expected status.""" + eps.show_all() + output = capsys.readouterr().out + assert ( + output == expected_show_all_output + ), f"Expected output does not match actual output.\nExpected:\n{expected_show_all_output}\nActual:\n{output}" -- 2.49.1 From fa35ddf1a905a1dc89b2372f3c9f75d325ac9bd6 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 21:17:30 +0100 Subject: [PATCH 10/11] refactor(eps): deactivate black for eps.py to keep signal component formatted in single line. --- csaxs_bec/devices/epics/eps.py | 690 +++++---------------------------- 1 file changed, 101 insertions(+), 589 deletions(-) diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index 68ba3a3..e3d9b61 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -1,5 +1,9 @@ """EPS module for cSAXS beamline: defines the EPS device with its components and methods.""" +# fmt: off +# Disable Black formatting for this file to preserve an easier readable structure for the component definitions. + +# pylint: disable=line-too-long from __future__ import annotations import time @@ -30,556 +34,105 @@ class EPSSubDevices(Device): class EPSAlarms(EPSSubDevices): """EPS alarms at the cSAXS beamline.""" - eps_alarm_cnt = Cpt( - EpicsSignalRO, - read_pv="X12SA-EPS-PLC:AlarmCnt_EPS", - add_prefix=("",), - name="eps_alarm_cnt", - kind=Kind.omitted, - doc="X12SA EPS Alarm count", - auto_monitor=True, - labels={"alarm"}, - ) - mis_alarm_cnt = Cpt( - EpicsSignalRO, - read_pv="ARS00-MIS-PLC-01:AlarmCnt_Frontends", - add_prefix=("",), - name="mis_alarm_cnt", - kind=Kind.omitted, - doc="FrontEnd MIS Alarm count", - auto_monitor=True, - labels={"alarm"}, - ) + eps_alarm_cnt = Cpt(EpicsSignalRO, read_pv="X12SA-EPS-PLC:AlarmCnt_EPS", add_prefix=("",), name="eps_alarm_cnt", kind=Kind.omitted, doc="X12SA EPS Alarm count", auto_monitor=True, labels={"alarm"}) + mis_alarm_cnt = Cpt(EpicsSignalRO, read_pv="ARS00-MIS-PLC-01:AlarmCnt_Frontends", add_prefix=("",), name="mis_alarm_cnt", kind=Kind.omitted, doc="FrontEnd MIS Alarm count", auto_monitor=True, labels={"alarm"}) class ValvesFrontend(EPSSubDevices): """Valves frontend at the cSAXS beamline.""" - ################ - ### Frontend ### - ################ - fe_vvpg_0000 = Cpt( - EpicsSignalRO, - read_pv="X12SA-FE-VVPG-0000:PLC_OPEN", - add_prefix=("",), - name="fevvpg0000", - kind=Kind.omitted, - doc="FE-VVPG-0000", - auto_monitor=True, - labels={"valve"}, - ) - fe_vvpg_1010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-FE-VVPG-1010:PLC_OPEN", - add_prefix=("",), - name="fevvpg1010", - kind=Kind.omitted, - doc="FE-VVPG-1010", - auto_monitor=True, - labels={"valve"}, - ) - fe_vvfv_2010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-FE-VVFV-2010:PLC_OPEN", - add_prefix=("",), - name="fevvfv2010", - kind=Kind.omitted, - doc="FE-VVFV-2010", - auto_monitor=True, - labels={"valve"}, - ) - fe_vvpg_2010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-FE-VVPG-2010:PLC_OPEN", - add_prefix=("",), - name="fevvpg2010", - kind=Kind.omitted, - doc="FE-VVPG-2010", - auto_monitor=True, - labels={"valve"}, - ) - - ################ - ### Optics ### - ################ + fe_vvpg_0000 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVPG-0000:PLC_OPEN", add_prefix=("",), name="fevvpg0000", kind=Kind.omitted, doc="FE-VVPG-0000", auto_monitor=True, labels={"valve"}) + fe_vvpg_1010 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVPG-1010:PLC_OPEN", add_prefix=("",), name="fevvpg1010", kind=Kind.omitted, doc="FE-VVPG-1010", auto_monitor=True, labels={"valve"}) + fe_vvfv_2010 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVFV-2010:PLC_OPEN", add_prefix=("",), name="fevvfv2010", kind=Kind.omitted, doc="FE-VVFV-2010", auto_monitor=True, labels={"valve"}) + fe_vvpg_2010 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVPG-2010:PLC_OPEN", add_prefix=("",), name="fevvpg2010", kind=Kind.omitted, doc="FE-VVPG-2010", auto_monitor=True, labels={"valve"}) class ValvesOptics(EPSSubDevices): """Valves at the optics hutch.""" - op_vvpg_1010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-1010:PLC_OPEN", - add_prefix=("",), - name="opvvpg1010", - kind=Kind.omitted, - doc="OP-VVPG-1010", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_2010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-2010:PLC_OPEN", - add_prefix=("",), - name="opvvpg2010", - kind=Kind.omitted, - doc="OP-VVPG-2010", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_3010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-3010:PLC_OPEN", - add_prefix=("",), - name="opvvpg3010", - kind=Kind.omitted, - doc="OP-VVPG-3010", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_3020 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-3020:PLC_OPEN", - add_prefix=("",), - name="opvvpg3020", - kind=Kind.omitted, - doc="OP-VVPG-3020", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_4010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-4010:PLC_OPEN", - add_prefix=("",), - name="opvvpg4010", - kind=Kind.omitted, - doc="OP-VVPG-4010", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_5010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-5010:PLC_OPEN", - add_prefix=("",), - name="opvvpg5010", - kind=Kind.omitted, - doc="OP-VVPG-5010", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_6010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-6010:PLC_OPEN", - add_prefix=("",), - name="opvvpg6010", - kind=Kind.omitted, - doc="OP-VVPG-6010", - auto_monitor=True, - labels={"valve"}, - ) - op_vvpg_7010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-VVPG-7010:PLC_OPEN", - add_prefix=("",), - name="opvvpg7010", - kind=Kind.omitted, - doc="OP-VVPG-7010", - auto_monitor=True, - labels={"valve"}, - ) - - ################## - ### Endstation ### - ################## - - + op_vvpg_1010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-1010:PLC_OPEN", add_prefix=("",), name="opvvpg1010", kind=Kind.omitted, doc="OP-VVPG-1010", auto_monitor=True, labels={"valve"}) + op_vvpg_2010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-2010:PLC_OPEN", add_prefix=("",), name="opvvpg2010", kind=Kind.omitted, doc="OP-VVPG-2010", auto_monitor=True, labels={"valve"}) + op_vvpg_3010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-3010:PLC_OPEN", add_prefix=("",), name="opvvpg3010", kind=Kind.omitted, doc="OP-VVPG-3010", auto_monitor=True, labels={"valve"}) + op_vvpg_3020 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-3020:PLC_OPEN", add_prefix=("",), name="opvvpg3020", kind=Kind.omitted, doc="OP-VVPG-3020", auto_monitor=True, labels={"valve"}) + op_vvpg_4010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-4010:PLC_OPEN", add_prefix=("",), name="opvvpg4010", kind=Kind.omitted, doc="OP-VVPG-4010", auto_monitor=True, labels={"valve"}) + op_vvpg_5010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-5010:PLC_OPEN", add_prefix=("",), name="opvvpg5010", kind=Kind.omitted, doc="OP-VVPG-5010", auto_monitor=True, labels={"valve"}) + op_vvpg_6010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-6010:PLC_OPEN", add_prefix=("",), name="opvvpg6010", kind=Kind.omitted, doc="OP-VVPG-6010", auto_monitor=True, labels={"valve"}) + op_vvpg_7010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-7010:PLC_OPEN", add_prefix=("",), name="opvvpg7010", kind=Kind.omitted, doc="OP-VVPG-7010", auto_monitor=True, labels={"valve"}) + + class ValvesEndstation(EPSSubDevices): """Endstation valves at the cSAXS beamline.""" - es_vvpg_1010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-ES-VVPG-1010:PLC_OPEN", - add_prefix=("",), - name="esvvpg1010", - kind=Kind.omitted, - doc="ES-VVPG-1010", - auto_monitor=True, - labels={"valve"}, - ) + es_vvpg_1010 = Cpt(EpicsSignalRO, read_pv="X12SA-ES-VVPG-1010:PLC_OPEN", add_prefix=("",), name="esvvpg1010", kind=Kind.omitted, doc="ES-VVPG-1010", auto_monitor=True, labels={"valve"}) class ShuttersFrontend(EPSSubDevices): """Shutters frontend.""" - fe_psh1 = Cpt( - EpicsSignalRO, - read_pv="X12SA-FE-PSH1-EMLS-0010:OPEN", - add_prefix=("",), - name="fepsh1", - kind=Kind.omitted, - doc="FE-PSH1-EMLS-0010", - auto_monitor=True, - labels={"shutter"}, - ) - fe_sto1 = Cpt( - EpicsSignalRO, - read_pv="X12SA-FE-STO1-EMLS-0010:OPEN", - add_prefix=("",), - name="festo1", - kind=Kind.omitted, - doc="FE-STO1-EMLS-0010", - auto_monitor=True, - labels={"shutter"}, - ) + fe_psh1 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-PSH1-EMLS-0010:OPEN", add_prefix=("",), name="fepsh1", kind=Kind.omitted, doc="FE-PSH1-EMLS-0010", auto_monitor=True, labels={"shutter"}) + fe_sto1 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-STO1-EMLS-0010:OPEN", add_prefix=("",), name="festo1", kind=Kind.omitted, doc="FE-STO1-EMLS-0010", auto_monitor=True, labels={"shutter"}) class ShuttersEndstation(EPSSubDevices): """Shutters at the endstation.""" - es_psh17010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN", - add_prefix=("",), - name="espsh17010", - kind=Kind.omitted, - doc="OP-PSH1-EMLS-7010", - auto_monitor=True, - labels={"shutter"}, - ) + es_psh17010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN", add_prefix=("",), name="espsh17010", kind=Kind.omitted, doc="OP-PSH1-EMLS-7010", auto_monitor=True, labels={"shutter"}) class DMMMonochromator(EPSSubDevices): """DMM monochromator signals at the cSAXS beamline.""" - dmm_temp_surface_1 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-ETTC-3010:TEMP", - add_prefix=("",), - name="dmm_temp_surface_1", - kind=Kind.omitted, - doc="DMM Temp Surface 1", - auto_monitor=True, - labels={"temp"}, - ) - dmm_temp_surface_2 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-ETTC-3020:TEMP", - add_prefix=("",), - name="dmm_temp_surface_2", - kind=Kind.omitted, - doc="DMM Temp Surface 2", - auto_monitor=True, - labels={"temp"}, - ) - dmm_temp_shield_1_disaster = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-ETTC-3030:TEMP", - add_prefix=("",), - name="dmm_temp_shield_1_disaster", - kind=Kind.omitted, - doc="DMM Temp Shield 1 (disaster)", - auto_monitor=True, - labels={"temp"}, - ) - dmm_temp_shield_2_disaster = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-ETTC-3040:TEMP", - add_prefix=("",), - name="dmm_temp_shield_2_disaster", - kind=Kind.omitted, - doc="DMM Temp Shield 2 (disaster)", - auto_monitor=True, - labels={"temp"}, - ) + dmm_temp_surface_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3010:TEMP", add_prefix=("",), name="dmm_temp_surface_1", kind=Kind.omitted, doc="DMM Temp Surface 1", auto_monitor=True, labels={"temp"}) + dmm_temp_surface_2 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3020:TEMP", add_prefix=("",), name="dmm_temp_surface_2", kind=Kind.omitted, doc="DMM Temp Surface 2", auto_monitor=True, labels={"temp"}) + dmm_temp_shield_1_disaster = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3030:TEMP", add_prefix=("",), name="dmm_temp_shield_1_disaster", kind=Kind.omitted, doc="DMM Temp Shield 1 (disaster)", auto_monitor=True, labels={"temp"}) + dmm_temp_shield_2_disaster = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3040:TEMP", add_prefix=("",), name="dmm_temp_shield_2_disaster", kind=Kind.omitted, doc="DMM Temp Shield 2 (disaster)", auto_monitor=True, labels={"temp"}) - dmm_translation_thru = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMLS-3010:THRU", - add_prefix=("",), - name="dmm_translation_thru", - kind=Kind.omitted, - doc="DMM Translation ThruPos", - auto_monitor=True, - labels={"switch"}, - ) - dmm_translation_in = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMLS-3020:IN", - add_prefix=("",), - name="dmm_translation_in", - kind=Kind.omitted, - doc="DMM Translation InPos", - auto_monitor=True, - labels={"switch"}, - ) - dmm_bragg_thru = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMLS-3030:THRU", - add_prefix=("",), - name="dmm_bragg_thru", - kind=Kind.omitted, - doc="DMM Bragg ThruPos", - auto_monitor=True, - labels={"switch"}, - ) - dmm_bragg_in = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMLS-3040:IN", - add_prefix=("",), - name="dmm_bragg_in", - kind=Kind.omitted, - doc="DMM Bragg InPos", - auto_monitor=True, - labels={"switch"}, - ) + dmm_translation_thru = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3010:THRU", add_prefix=("",), name="dmm_translation_thru", kind=Kind.omitted, doc="DMM Translation ThruPos", auto_monitor=True, labels={"switch"}) + dmm_translation_in = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3020:IN", add_prefix=("",), name="dmm_translation_in", kind=Kind.omitted, doc="DMM Translation InPos", auto_monitor=True, labels={"switch"}) + dmm_bragg_thru = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3030:THRU", add_prefix=("",), name="dmm_bragg_thru", kind=Kind.omitted, doc="DMM Bragg ThruPos", auto_monitor=True, labels={"switch"}) + dmm_bragg_in = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3040:IN", add_prefix=("",), name="dmm_bragg_in", kind=Kind.omitted, doc="DMM Bragg InPos", auto_monitor=True, labels={"switch"}) - dmm_heater_fault_xtal_1 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMSW-3050:SWITCH", - add_prefix=("",), - name="dmm_heater_fault_xtal_1", - kind=Kind.omitted, - doc="DMM Heater Fault XTAL 1", - auto_monitor=True, - labels={"fault"}, - ) - dmm_heater_fault_xtal_2 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMSW-3060:SWITCH", - add_prefix=("",), - name="dmm_heater_fault_xtal_2", - kind=Kind.omitted, - doc="DMM Heater Fault XTAL 2", - auto_monitor=True, - labels={"fault"}, - ) - dmm_heater_fault_support_1 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM-EMSW-3070:SWITCH", - add_prefix=("",), - name="dmm_heater_fault_support_1", - kind=Kind.omitted, - doc="DMM Heater Fault Support 1", - auto_monitor=True, - labels={"fault"}, - ) + dmm_heater_fault_xtal_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMSW-3050:SWITCH", add_prefix=("",), name="dmm_heater_fault_xtal_1", kind=Kind.omitted, doc="DMM Heater Fault XTAL 1", auto_monitor=True, labels={"fault"}) + dmm_heater_fault_xtal_2 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMSW-3060:SWITCH", add_prefix=("",), name="dmm_heater_fault_xtal_2", kind=Kind.omitted, doc="DMM Heater Fault XTAL 2", auto_monitor=True, labels={"fault"}) + dmm_heater_fault_support_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMSW-3070:SWITCH", add_prefix=("",), name="dmm_heater_fault_support_1", kind=Kind.omitted, doc="DMM Heater Fault Support 1", auto_monitor=True, labels={"fault"}) - dmm_energy = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM1:ENERGY-GET", - add_prefix=("",), - name="dmm_energy", - kind=Kind.omitted, - doc="DMM Energy", - auto_monitor=True, - labels={"energy"}, - ) - dmm_position = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM1:POSITION", - add_prefix=("",), - name="dmm_position", - kind=Kind.omitted, - doc="DMM Position", - auto_monitor=True, - labels={"string"}, - ) - dmm_stripe = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-DMM1:STRIPE", - add_prefix=("",), - name="dmm_stripe", - kind=Kind.omitted, - doc="DMM Stripe", - auto_monitor=True, - labels={"string"}, - ) + dmm_energy = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM1:ENERGY-GET", add_prefix=("",), name="dmm_energy", kind=Kind.omitted, doc="DMM Energy", auto_monitor=True, labels={"energy"}) + dmm_position = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM1:POSITION", add_prefix=("",), name="dmm_position", kind=Kind.omitted, doc="DMM Position", auto_monitor=True, labels={"string"}) + dmm_stripe = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM1:STRIPE", add_prefix=("",), name="dmm_stripe", kind=Kind.omitted, doc="DMM Stripe", auto_monitor=True, labels={"string"}) class CCMMonochromator(EPSSubDevices): """CCM monochromator signals at the cSAXS beamline.""" - ccm_temp_crystal = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM-ETTC-4010:TEMP", - add_prefix=("",), - name="ccm_temp_crystal", - kind=Kind.omitted, - doc="CCM Temp Crystal", - auto_monitor=True, - labels={"temp"}, - ) - ccm_temp_shield_disaster = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM-ETTC-4020:TEMP", - add_prefix=("",), - name="ccm_temp_shield_disaster", - kind=Kind.omitted, - doc="CCM Temp Shield (disaster)", - auto_monitor=True, - labels={"temp"}, - ) + ccm_temp_crystal = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-ETTC-4010:TEMP", add_prefix=("",), name="ccm_temp_crystal", kind=Kind.omitted, doc="CCM Temp Crystal", auto_monitor=True, labels={"temp"}) + ccm_temp_shield_disaster = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-ETTC-4020:TEMP", add_prefix=("",), name="ccm_temp_shield_disaster", kind=Kind.omitted, doc="CCM Temp Shield (disaster)", auto_monitor=True, labels={"temp"}) - ccm_heater_fault_1 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM-EMSW-4010:SWITCH", - add_prefix=("",), - name="ccm_heater_fault_1", - kind=Kind.omitted, - doc="CCM Heater Fault 1", - auto_monitor=True, - labels={"fault"}, - ) - ccm_heater_fault_2 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM-EMSW-4020:SWITCH", - add_prefix=("",), - name="ccm_heater_fault_2", - kind=Kind.omitted, - doc="CCM Heater Fault 2", - auto_monitor=True, - labels={"fault"}, - ) - ccm_heater_fault_3 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM-EMSW-4030:SWITCH", - add_prefix=("",), - name="ccm_heater_fault_3", - kind=Kind.omitted, - doc="CCM Heater Fault 3", - auto_monitor=True, - labels={"fault"}, - ) + ccm_heater_fault_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-EMSW-4010:SWITCH", add_prefix=("",), name="ccm_heater_fault_1", kind=Kind.omitted, doc="CCM Heater Fault 1", auto_monitor=True, labels={"fault"}) + ccm_heater_fault_2 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-EMSW-4020:SWITCH", add_prefix=("",), name="ccm_heater_fault_2", kind=Kind.omitted, doc="CCM Heater Fault 2", auto_monitor=True, labels={"fault"}) + ccm_heater_fault_3 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-EMSW-4030:SWITCH", add_prefix=("",), name="ccm_heater_fault_3", kind=Kind.omitted, doc="CCM Heater Fault 3", auto_monitor=True, labels={"fault"}) - ccm_energy = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM1:ENERGY-GET", - add_prefix=("",), - name="ccm_energy", - kind=Kind.omitted, - doc="CCM Energy", - auto_monitor=True, - labels={"energy"}, - ) - ccm_position = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CCM1:POSITION", - add_prefix=("",), - name="ccm_position", - kind=Kind.omitted, - doc="CCM Position", - auto_monitor=True, - labels={"string"}, - ) + ccm_energy = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM1:ENERGY-GET", add_prefix=("",), name="ccm_energy", kind=Kind.omitted, doc="CCM Energy", auto_monitor=True, labels={"energy"}) + ccm_position = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM1:POSITION", add_prefix=("",), name="ccm_position", kind=Kind.omitted, doc="CCM Position", auto_monitor=True, labels={"string"}) class CoolingWater(EPSSubDevices): """Cooling water signals at the cSAXS beamline.""" - op_sl1_efsw_2010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-SL1-EFSW-2010:FLOW", - add_prefix=("",), - name="op_sl1_efsw_2010_flow", - kind=Kind.omitted, - doc="OP-SL1-EFSW-2010", - auto_monitor=True, - labels={"flow"}, - ) - op_sl2_efsw_2010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-SL2-EFSW-2010:FLOW", - add_prefix=("",), - name="op_sl2_efsw_2010_flow", - kind=Kind.omitted, - doc="OP-SL2-EFSW-2010", - auto_monitor=True, - labels={"flow"}, - ) - op_eb1_efsw_5010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-EB1-EFSW-5010:FLOW", - add_prefix=("",), - name="op_eb1_efsw_5010_flow", - kind=Kind.omitted, - doc="OP-EB1-EFSW-5010", - auto_monitor=True, - labels={"flow"}, - ) - op_eb1_efsw_5020_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-EB1-EFSW-5020:FLOW", - add_prefix=("",), - name="op_eb1_efsw_5020_flow", - kind=Kind.omitted, - doc="OP-EB1-EFSW-5020", - auto_monitor=True, - labels={"flow"}, - ) - op_sl3_efsw_5010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-SL3-EFSW-5010:FLOW", - add_prefix=("",), - name="op_sl3_efsw_5010_flow", - kind=Kind.omitted, - doc="OP-SL3-EFSW-5010", - auto_monitor=True, - labels={"flow"}, - ) - op_kb_efsw_6010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-KB-EFSW-6010:FLOW", - add_prefix=("",), - name="op_kb_efsw_6010_flow", - kind=Kind.omitted, - doc="OP-KB-EFSW-6010", - auto_monitor=True, - labels={"flow"}, - ) - op_psh1_efsw_7010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-PSH1-EFSW-7010:FLOW", - add_prefix=("",), - name="op_psh1_efsw_7010_flow", - kind=Kind.omitted, - doc="OP-PSH1-EFSW-7010", - auto_monitor=True, - labels={"flow"}, - ) - es_eb2_efsw_1010_flow = Cpt( - EpicsSignalRO, - read_pv="X12SA-ES-EB2-EFSW-1010:FLOW", - add_prefix=("",), - name="es_eb2_efsw_1010_flow", - kind=Kind.omitted, - doc="ES-EB2-EFSW-1010", - auto_monitor=True, - labels={"flow"}, - ) + op_sl1_efsw_2010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-SL1-EFSW-2010:FLOW", add_prefix=("",), name="op_sl1_efsw_2010_flow", kind=Kind.omitted, doc="OP-SL1-EFSW-2010", auto_monitor=True, labels={"flow"}) + op_sl2_efsw_2010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-SL2-EFSW-2010:FLOW", add_prefix=("",), name="op_sl2_efsw_2010_flow", kind=Kind.omitted, doc="OP-SL2-EFSW-2010", auto_monitor=True, labels={"flow"}) + op_eb1_efsw_5010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-EB1-EFSW-5010:FLOW", add_prefix=("",), name="op_eb1_efsw_5010_flow", kind=Kind.omitted, doc="OP-EB1-EFSW-5010", auto_monitor=True, labels={"flow"}) + op_eb1_efsw_5020_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-EB1-EFSW-5020:FLOW", add_prefix=("",), name="op_eb1_efsw_5020_flow", kind=Kind.omitted, doc="OP-EB1-EFSW-5020", auto_monitor=True, labels={"flow"}) + op_sl3_efsw_5010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-SL3-EFSW-5010:FLOW", add_prefix=("",), name="op_sl3_efsw_5010_flow", kind=Kind.omitted, doc="OP-SL3-EFSW-5010", auto_monitor=True, labels={"flow"}) + op_kb_efsw_6010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-KB-EFSW-6010:FLOW", add_prefix=("",), name="op_kb_efsw_6010_flow", kind=Kind.omitted, doc="OP-KB-EFSW-6010", auto_monitor=True, labels={"flow"}) + op_psh1_efsw_7010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-PSH1-EFSW-7010:FLOW", add_prefix=("",), name="op_psh1_efsw_7010_flow", kind=Kind.omitted, doc="OP-PSH1-EFSW-7010", auto_monitor=True, labels={"flow"}) + es_eb2_efsw_1010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-ES-EB2-EFSW-1010:FLOW", add_prefix=("",), name="es_eb2_efsw_1010_flow", kind=Kind.omitted, doc="ES-EB2-EFSW-1010", auto_monitor=True, labels={"flow"}) - op_cs_ecvw_0010 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CS-ECVW-0010:PLC_OPEN", - add_prefix=("",), - name="op_cs_ecvw_0010", - kind=Kind.omitted, - doc="OP-CS-ECVW-0010", - auto_monitor=True, - labels={"valve"}, - ) - op_cs_ecvw_0020 = Cpt( - EpicsSignalRO, - read_pv="X12SA-OP-CS-ECVW-0020:PLC_OPEN", - add_prefix=("",), - name="op_cs_ecvw_0020", - kind=Kind.omitted, - doc="OP-CS-ECVW-0020", - auto_monitor=True, - labels={"valve"}, - ) + op_cs_ecvw_0010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CS-ECVW-0010:PLC_OPEN", add_prefix=("",), name="op_cs_ecvw_0010", kind=Kind.omitted, doc="OP-CS-ECVW-0010", auto_monitor=True, labels={"valve"}) + op_cs_ecvw_0020 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CS-ECVW-0020:PLC_OPEN", add_prefix=("",), name="op_cs_ecvw_0020", kind=Kind.omitted, doc="OP-CS-ECVW-0020", auto_monitor=True, labels={"valve"}) class EPS(PSIDeviceBase): + """EPS device for the cSAXS beamline.""" alarms = Cpt(EPSAlarms, name="alarms", doc="EPS Alarms") valves_frontend = Cpt(ValvesFrontend, name="valves_frontend", doc="Valves Frontend") @@ -592,26 +145,8 @@ class EPS(PSIDeviceBase): cooling_water = Cpt(CoolingWater, name="cooling_water", doc="Cooling Water") # Acknowledgment signals for PLC communication (if needed for future use) - ackerr = Cpt( - EpicsSignal, - read_pv="X12SA-EPS-PLC:ACKERR-REQUEST", - add_prefix=("",), - name="ackerr", - kind=Kind.omitted, - doc="ACKERR request - OP-CS-ECVW-0020", - auto_monitor=True, - labels={"request"}, - ) - request = Cpt( - EpicsSignal, - read_pv="X12SA-OP-CS-ECVW:PLC_REQUEST", - add_prefix=("",), - name="op_cs_ecvw_request", - kind=Kind.omitted, - doc="PLC request - OP-CS-ECVW-PLC_REQUEST", - auto_monitor=True, - labels={"request"}, - ) + ackerr = Cpt(EpicsSignal, read_pv="X12SA-EPS-PLC:ACKERR-REQUEST", add_prefix=("",), name="ackerr", kind=Kind.omitted, doc="ACKERR request - OP-CS-ECVW-0020", auto_monitor=True, labels={"request"}) + request = Cpt(EpicsSignal, read_pv="X12SA-OP-CS-ECVW:PLC_REQUEST", add_prefix=("",), name="op_cs_ecvw_request", kind=Kind.omitted, doc="PLC request - OP-CS-ECVW-PLC_REQUEST", auto_monitor=True, labels={"request"}) def _notify(self, msg: str, show_as_client_msg: bool = True): """Utility method to print a message, and optionally send it to the client UI if it should be shown also as a client message.""" @@ -657,11 +192,7 @@ class EPS(PSIDeviceBase): valves = [self.cooling_water.op_cs_ecvw_0010, self.cooling_water.op_cs_ecvw_0020] # Flow channels list extracted from CHANNELS - flow_items = [ - walk.item - for walk in self.cooling_water.walk_signals() - if "flow" in walk.item._ophyd_labels_ - ] + flow_items = [walk.item for walk in self.cooling_water.walk_signals() if "flow" in walk.item._ophyd_labels_] # --- Step 1: EPS alarm reset --- alarm_value = self.safe_get(eps_alarm_sig, 0) @@ -701,16 +232,8 @@ class EPS(PSIDeviceBase): def snapshot(): # Valve snapshot v_states = [self.safe_get(v, None) for v in valves] - v1 = f"{valves[0].attr_name[-4:]}=" + ( - "OPEN " - if v_states[0] is True or v_states[0] == 1 - else "CLOSED" if v_states[0] is False or v_states[0] == 0 else "N/A " - ) - v2 = f"{valves[1].attr_name[-4:]}=" + ( - "OPEN " - if v_states[1] is True or v_states[1] == 1 - else "CLOSED" if v_states[1] is False or v_states[1] == 0 else "N/A " - ) + v1 = f"{valves[0].attr_name[-4:]}=" + ("OPEN " if v_states[0] is True or v_states[0] == 1 else "CLOSED" if v_states[0] is False or v_states[0] == 0 else "N/A ") + v2 = f"{valves[1].attr_name[-4:]}=" + ("OPEN " if v_states[1] is True or v_states[1] == 1 else "CLOSED" if v_states[1] is False or v_states[1] == 0 else "N/A ") # 2 valves with a single space between => width ~ 21 valve_str = f"{v1} {v2}" @@ -718,9 +241,7 @@ class EPS(PSIDeviceBase): flow_states = [] for fsig in flow_items: fval = self.safe_get(fsig, None) - flow_states.append( - True if fval in (1, True) else False if fval in (0, False) else None - ) + flow_states.append(True if fval in (1, True) else False if fval in (0, False) else None) ok = sum(1 for f in flow_states if f is True) fail = sum(1 for f in flow_states if f is False) @@ -751,9 +272,7 @@ class EPS(PSIDeviceBase): if both_open: if stable_until is None: stable_until = now + STABILITY - print( - f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…" - ) + print(f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…") else: if now >= stable_until: print("→ SUCCESS: Valves remained OPEN during stability window.") @@ -775,10 +294,7 @@ class EPS(PSIDeviceBase): # ---- New: enum maps for numeric -> string rendering ---- POSITION_ENUM = {0: "out of beam", 1: "in beam"} STRIPE_ENUM = {0: "Stripe 1 W/B4C", 1: "Stripe 2 NiV/B4C"} - POSITION_ATTRS = { - self.dmm_monochromator.dmm_position.attr_name, - self.ccm_monochromator.ccm_position.attr_name, - } + POSITION_ATTRS = {self.dmm_monochromator.dmm_position.attr_name, self.ccm_monochromator.ccm_position.attr_name} STRIPE_ATTRS = {self.dmm_monochromator.dmm_stripe.attr_name} def is_bool_like(v): @@ -831,9 +347,7 @@ class EPS(PSIDeviceBase): return f"{green+'OK'+white if not value else red+'FAULT'+white}" # ------------------- VALVE/SHUTTER ----------------- - if ( - "valve" in signal._ophyd_labels_ or "shutter" in signal._ophyd_labels_ - ) and is_bool_like(value): + if ("valve" in signal._ophyd_labels_ or "shutter" in signal._ophyd_labels_) and is_bool_like(value): return f"{green+'OPEN'+white if value else red+'CLOSED'+white}" # ------------------- FLOW (OK/FAIL) ---------------- @@ -873,48 +387,46 @@ class EPS(PSIDeviceBase): return is_bool_like(v) and not bool(v) if closed(v1) and closed(v2): - print( - f"\n{cyan}Hint:{white} Both water cooling valves are CLOSED.\n" - f"You can open them using: {bold}dev.x12saEPS.water_cooling_op(){white}" - ) + print(f"\n{cyan}Hint:{white} Both water cooling valves are CLOSED.\n" f"You can open them using: {bold}dev.x12saEPS.water_cooling_op(){white}") - # ---------------------------------------------------------- - # Consistency report - # ---------------------------------------------------------- - # def consistency_report(self, *, verbose=True): - # missing = [] - # dupes = [] - # seen = {} +# fmt: on +# ---------------------------------------------------------- +# Consistency report +# ---------------------------------------------------------- +# def consistency_report(self, *, verbose=True): +# missing = [] +# dupes = [] +# seen = {} - # for sub_device in self.walk_components(): - # section = sub_device.name - # for walk in sub_device.walk_components(): - # cpt: Cpt = walk.ancestors[-1] - # it: EpicsSignalRO = walk.item - # if not hasattr(self, it["attr"]): - # missing.append((section, it["attr"], it["label"], it["pv"])) +# for sub_device in self.walk_components(): +# section = sub_device.name +# for walk in sub_device.walk_components(): +# cpt: Cpt = walk.ancestors[-1] +# it: EpicsSignalRO = walk.item +# if not hasattr(self, it["attr"]): +# missing.append((section, it["attr"], it["label"], it["pv"])) - # pv = it["pv"] - # if pv in seen: - # dupes.append((pv, seen[pv], (section, it["attr"], it["label"]))) - # else: - # seen[pv] = (section, it["attr"], it["label"]) +# pv = it["pv"] +# if pv in seen: +# dupes.append((pv, seen[pv], (section, it["attr"], it["label"]))) +# else: +# seen[pv] = (section, it["attr"], it["label"]) - # if verbose: - # print("=== Consistency Report ===") +# if verbose: +# print("=== Consistency Report ===") - # if missing: - # print("\nMissing attributes:") - # for sec, a, lbl, pv in missing: - # print(f" - [{sec}] {a} {lbl} pv={pv}") - # else: - # print("\nNo missing attributes.") +# if missing: +# 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:") - # for pv, f1, f2 in dupes: - # print(f" {pv} → {f1} AND {f2}") - # else: - # print("\nNo duplicate PVs.") +# if dupes: +# 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} +# return {"missing_attrs": missing, "duplicate_pvs": dupes} -- 2.49.1 From 5ab763ad38f943778f4f86f0c03cd607cd912749 Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 12 Feb 2026 09:52:47 +0100 Subject: [PATCH 11/11] test at beamline --- csaxs_bec/device_configs/user_setup.yaml | 2 +- csaxs_bec/devices/epics/eps.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/device_configs/user_setup.yaml b/csaxs_bec/device_configs/user_setup.yaml index 232849c..1e6681d 100644 --- a/csaxs_bec/device_configs/user_setup.yaml +++ b/csaxs_bec/device_configs/user_setup.yaml @@ -7,7 +7,7 @@ ############################################################ x12saEPS: description: X12SA EPS info and control - deviceClass: csaxs_bec.devices.epics.eps.cSAXSEps + deviceClass: csaxs_bec.devices.epics.eps.EPS deviceConfig: {} enabled: true onFailure: buffer diff --git a/csaxs_bec/devices/epics/eps.py b/csaxs_bec/devices/epics/eps.py index e3d9b61..e8eb8cc 100644 --- a/csaxs_bec/devices/epics/eps.py +++ b/csaxs_bec/devices/epics/eps.py @@ -133,7 +133,10 @@ class CoolingWater(EPSSubDevices): class EPS(PSIDeviceBase): """EPS device for the cSAXS beamline.""" - + USER_ACCESS = [ + "show_all", + "water_cooling_op", + ] alarms = Cpt(EPSAlarms, name="alarms", doc="EPS Alarms") valves_frontend = Cpt(ValvesFrontend, name="valves_frontend", doc="Valves Frontend") valves_optics = Cpt(ValvesOptics, name="valves_optics", doc="Valves Optics Hutch") -- 2.49.1