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()