From 5dcb51f36cf606dd966bd099f7dab5061c8eec8d Mon Sep 17 00:00:00 2001 From: x01da Date: Thu, 30 Jul 2026 14:30:43 +0200 Subject: [PATCH 1/6] wip --- debye_bec/devices/mo1_bragg/acscontroller.py | 62 +++++++++++++++++++ .../devices/mo1_bragg/mo1_bragg_devices.py | 6 ++ 2 files changed, 68 insertions(+) create mode 100644 debye_bec/devices/mo1_bragg/acscontroller.py diff --git a/debye_bec/devices/mo1_bragg/acscontroller.py b/debye_bec/devices/mo1_bragg/acscontroller.py new file mode 100644 index 0000000..faf4900 --- /dev/null +++ b/debye_bec/devices/mo1_bragg/acscontroller.py @@ -0,0 +1,62 @@ +import socket +import threading + +import numpy as np +from ophyd.signal import Signal + + +class ACSSignal(Signal): + + def __init__(self, controller, tag, **kwargs): + self.controller = controller + self.tag = tag + super().__init__(**kwargs) + + def get(self): + value = self.controller.read_tag(self.tag) + self._readback = value + return value + + def put(self, value, **kwargs): + self.controller.write_tag(self.tag, value) + self._readback = value + return super().put(value, **kwargs) + + +class ACSController: + + def __init__(self, host: str, port: int, timeout: float = 2.0): + self.host = host + self.port = port + self.timeout = timeout + self._socket = None + self._lock = threading.Lock() + + self.precision = 12 + + def connect(self): + if self._socket is not None: + return + self._socket = socket.create_connection((self.host, self.port), timeout=self.timeout) + + def disconnect(self): + if self._socket is not None: + self._socket.close() + self._socket = None + + def _query(self, command: str) -> str: + with self._lock: + self.connect() + msg = (command + "\r\n").encode() + self._socket.sendall(msg) + reply = self._socket.recv(4096) + return reply.decode().strip() + + def read_tag(self, tag: int): + i = 0 # For arrays, to be implemented + response = self._query(f"?{{%0.{self.precision:0.0f}f}}GETVAR({tag},{i}") + return float(response) + + def write_tag(self, tag: int, value): + i = 0 # For arrays, to be implemented + self._query(f"SETVAR({np.round(value, self.precision)},{tag},{i}") diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 3b6aca7..ef34f0d 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -18,6 +18,7 @@ from ophyd import ( ) from ophyd.utils import LimitError +from debye_bec.devices.mo1_bragg.acs_controller import ACSSignal from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType # Initialise logger @@ -256,6 +257,8 @@ class Mo1BraggPositioner(Device, PositionerBase): angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True) + test = Cpt(ACSSignal, tag=53000) # s_scan_angle_low + ########## Move Command PVs ########## move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True) @@ -278,6 +281,9 @@ class Mo1BraggPositioner(Device, PositionerBase): self._stopped = False self.readback.name = self.name + self.controller = ACSController(host, port) + kwargs["controller"] = self.controller + def stop(self, *, success=False) -> None: """Stop any motion on the positioner -- 2.54.0 From d2c6b565101166d68d35d09055cf87fad89e6636 Mon Sep 17 00:00:00 2001 From: x01da Date: Mon, 3 Aug 2026 10:54:54 +0200 Subject: [PATCH 2/6] wip --- debye_bec/devices/mo1_bragg/acs.py | 119 ++++++++++++++++++ debye_bec/devices/mo1_bragg/mo1_bragg.py | 15 ++- .../devices/mo1_bragg/mo1_bragg_devices.py | 26 +++- 3 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 debye_bec/devices/mo1_bragg/acs.py diff --git a/debye_bec/devices/mo1_bragg/acs.py b/debye_bec/devices/mo1_bragg/acs.py new file mode 100644 index 0000000..f874d41 --- /dev/null +++ b/debye_bec/devices/mo1_bragg/acs.py @@ -0,0 +1,119 @@ +""" +ACS controller device exposing plain read/write variables (no motion). + +Uses the same BEC building blocks as before: + - ophyd_devices.utils.controller.Controller -> shared TCP/IP communicator + - ophyd_devices.utils.socket.SocketIO -> raw socket helper + - ophyd_devices.utils.socket.SocketSignal -> Signal base talking through it + +Protocol: + read: "?GETVAR(tag)" -> reply is the value + write: "SETVAR(value,tag)" +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from ophyd import Component as Cpt +from ophyd import Kind +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketIO, SocketSignal + +if TYPE_CHECKING: + from bec_lib.devicemanager import DeviceManagerBase + +from bec_lib.logger import bec_logger + +# Initialise logger +logger = bec_logger.logger + + +# --------------------------------------------------------------------------- +# Shared communicator +# --------------------------------------------------------------------------- + + +class ACSController(Controller): + """ + Shared TCP/IP communicator for one ACS controller. + + Instantiating this class twice with the same (socket_host, socket_port) + returns the same object (see `Controller.__new__`), so every variable + signal below -- across however many devices -- shares one connection. + """ + + _axes_per_controller = 0 # not used for plain variables, no motion axes + + @threadlocked + def get_var(self, tag: int, precision=3) -> float: + if self.sock is None: + self.on() + i = 0 + reply = self.socket_put_and_receive(f"?{{%0.{precision:0.0f}f}}GETVAR({tag},{i})") + return float(reply) + + @threadlocked + def set_var(self, tag: int, value, precision=3) -> None: + if self.sock is None: + self.on() + i = 0 + self.socket_put_and_receive(f"SETVAR({np.round(value, precision)},{tag},{i})") + + +# --------------------------------------------------------------------------- +# Signal talking through the shared controller +# --------------------------------------------------------------------------- + + +class ACSVariableSignal(SocketSignal): + """Read/write ACS controller variable, identified by its tag number.""" + + def __init__(self, *args, tag: int, **kwargs): + self.tag = tag + super().__init__(*args, **kwargs) + + @property + def controller(self) -> ACSController: + return self.root.controller + + def _socket_get(self): + logger.info(self.controller) + logger.info(self.controller.sock) + return self.controller.get_var(self.tag) + + def _socket_set(self, val): + self.controller.set_var(self.tag, val) + + +# --------------------------------------------------------------------------- +# The device +# --------------------------------------------------------------------------- + + +class ACSVariables(PSIDeviceBase): + """Three read/write variables on an ACS controller, sharing one connection.""" + + var1 = Cpt(ACSVariableSignal, tag=1, kind=Kind.normal) + var2 = Cpt(ACSVariableSignal, tag=2, kind=Kind.normal) + var3 = Cpt(ACSVariableSignal, tag=3, kind=Kind.normal) + + def __init__( + self, + name: str, + host: str, + port: int = 701, + device_manager: "DeviceManagerBase" | None = None, + **kwargs, + ): + # controller must exist before super().__init__() builds the Cpt signals + self.controller = ACSController( + socket_cls=SocketIO, socket_host=host, socket_port=port, device_manager=device_manager + ) + super().__init__(name=name, device_manager=device_manager, **kwargs) + + def on_connected(self): + # Idempotent: safe even if other devices already opened this controller. + self.controller.on() diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 54a813f..dea3421 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -13,6 +13,7 @@ from typing import Literal from bec_lib.devicemanager import ScanInfo from bec_lib.logger import bec_logger +from bec_server.device_server.devices.devicemanager import DeviceManagerDS from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd import Component as Cpt from ophyd import DeviceStatus, StatusBase @@ -59,7 +60,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): USER_ACCESS = ["set_advanced_xas_settings", "set_xtal", "convert_angle_energy"] - def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): # type: ignore + def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, device_manager: DeviceManagerDS | None = None, **kwargs): # type: ignore """ Initialize the PSI Device Base class. @@ -67,7 +68,9 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): name (str) : Name of the device scan_info (ScanInfo): The scan info to use. """ - super().__init__(name=name, scan_info=scan_info, prefix=prefix, **kwargs) + super().__init__( + name=name, scan_info=scan_info, prefix=prefix, device_manager=device_manager, **kwargs + ) self.scan_parameters: ScanServerScanInfo = None self.timeout_for_pvwait = 7.5 self.valid_scan_names = [ @@ -346,7 +349,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): self.cancel_on_stop(status) logger.info(f"Finished calling complete on {self.name} within {time.time()-time_started}s.") return status - + def _status_callback(self, status, **kwargs): logger.info(f"Complete finished on mo1bragg with {status.done} and {status.success}") @@ -380,7 +383,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): if scan_parameters.scan_name in self.valid_scan_names: return True return False - + def _progress_update(self, value, old_value, **kwargs) -> None: """Callback method to update the scan progress, runs a callback to SUB_PROGRESS subscribers, i.e. BEC. @@ -449,13 +452,13 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): in_signal = self.calculator.calc_energy out_signal = self.calculator.calc_angle else: - raise Mo1BraggError(f'Unknown mode {mode}') + raise Mo1BraggError(f"Unknown mode {mode}") in_signal.put(inp) status = CompareStatus(self.calculator.calc_done, 1) self.cancel_on_stop(status) status.wait(self.timeout_for_pvwait) - status = CompareStatus(out_signal, 0, operation_success='>') + status = CompareStatus(out_signal, 0, operation_success=">") self.cancel_on_stop(status) status.wait(self.timeout_for_pvwait) return out_signal.get() diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index ef34f0d..7b274b9 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -1,11 +1,14 @@ """Module for the Mo1 Bragg positioner""" +from __future__ import annotations + import threading import time import traceback -from typing import Literal +from typing import TYPE_CHECKING, Literal from bec_lib.logger import bec_logger +from bec_server.device_server.devices.devicemanager import DeviceManagerDS from ophyd import Component as Cpt from ophyd import ( Device, @@ -17,8 +20,10 @@ from ophyd import ( Signal, ) from ophyd.utils import LimitError +from ophyd_devices.utils.socket import SocketIO -from debye_bec.devices.mo1_bragg.acs_controller import ACSSignal +# from debye_bec.devices.mo1_bragg.acs_controller import ACSSignal +from debye_bec.devices.mo1_bragg.acs import ACSController, ACSVariableSignal from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType # Initialise logger @@ -257,7 +262,9 @@ class Mo1BraggPositioner(Device, PositionerBase): angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True) - test = Cpt(ACSSignal, tag=53000) # s_scan_angle_low + # test = Cpt(ACSSignal, tag=53000) # s_scan_angle_low + + test = Cpt(ACSVariableSignal, tag=53000, kind="normal") # s_scan_angle_low ########## Move Command PVs ########## @@ -268,7 +275,7 @@ class Mo1BraggPositioner(Device, PositionerBase): _default_sub = SUB_READBACK SUB_PROGRESS = "progress" - def __init__(self, prefix="", *, name: str, **kwargs): + def __init__(self, prefix="", *, name: str, device_manager: DeviceManagerDS, **kwargs): """Initialize the Mo1 Bragg positioner. Args: @@ -276,13 +283,20 @@ class Mo1BraggPositioner(Device, PositionerBase): name (str): Name of the device kwargs: Additional keyword arguments """ + + host = "129.129.123.32" + port = 701 + self.controller = ACSController( + socket_cls=SocketIO, socket_host=host, socket_port=port, device_manager=device_manager + ) + super().__init__(prefix, name=name, **kwargs) self._move_thread = None self._stopped = False self.readback.name = self.name - self.controller = ACSController(host, port) - kwargs["controller"] = self.controller + # self.controller = ACSController(host, port) + # kwargs["controller"] = self.controller def stop(self, *, success=False) -> None: """Stop any motion on the positioner -- 2.54.0 From f26d451e2fdaaefffc456fb4a38012fc849a9d99 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 4 Aug 2026 12:18:45 +0200 Subject: [PATCH 3/6] Added xtal enum --- debye_bec/devices/mo1_bragg/mo1_bragg_enums.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_enums.py b/debye_bec/devices/mo1_bragg/mo1_bragg_enums.py index 09602b7..44b6b47 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_enums.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_enums.py @@ -3,6 +3,13 @@ import enum +class Xtal(int, enum.Enum): + """Enum class for the xtal (crystal) of the Bragg positioner""" + + Si111 = 0 + Si311 = 1 + + class TriggerControlSource(int, enum.Enum): """Enum class for the trigger control source of the trigger generator""" -- 2.54.0 From 58fe6425239337176b75a7a521ce66ac03bb8c3f Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 4 Aug 2026 12:19:21 +0200 Subject: [PATCH 4/6] replace first EpicsSignals with AcsSignals --- .../devices/mo1_bragg/mo1_bragg_devices.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 7b274b9..13f515b 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -23,8 +23,8 @@ from ophyd.utils import LimitError from ophyd_devices.utils.socket import SocketIO # from debye_bec.devices.mo1_bragg.acs_controller import ACSSignal -from debye_bec.devices.mo1_bragg.acs import ACSController, ACSVariableSignal -from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType +from debye_bec.devices.mo1_bragg.acs import ACSController, AcsSignal, AcsSignalRO +from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType, Xtal # Initialise logger logger = bec_logger.logger @@ -112,8 +112,12 @@ class Mo1BraggCrystal(Device): current_xtal = Cpt( EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True ) + # current_xtal_string = Cpt( + # EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True, string=True + # ) + current_xtal_string = Cpt( - EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True, string=True + AcsSignalRO, tag=10501, prec=0, enum=Xtal, kind="normal", auto_monitor=True ) @@ -134,15 +138,19 @@ class Mo1BraggScanSettings(Device): # XAS simple scan settings s_scan_angle_hi = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_hi", kind="config") s_scan_angle_lo = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_lo", kind="config") - s_scan_energy_lo = Cpt( - EpicsSignalWithRBV, suffix="s_scan_energy_lo", kind="config", auto_monitor=True - ) - s_scan_energy_hi = Cpt( - EpicsSignalWithRBV, suffix="s_scan_energy_hi", kind="config", auto_monitor=True - ) - s_scan_scantime = Cpt( - EpicsSignalWithRBV, suffix="s_scan_scantime", kind="config", auto_monitor=True - ) + # s_scan_energy_lo = Cpt( + # EpicsSignalWithRBV, suffix="s_scan_energy_lo", kind="config", auto_monitor=True + # ) + # s_scan_energy_hi = Cpt( + # EpicsSignalWithRBV, suffix="s_scan_energy_hi", kind="config", auto_monitor=True + # ) + # s_scan_scantime = Cpt( + # EpicsSignalWithRBV, suffix="s_scan_scantime", kind="config", auto_monitor=True + # ) + + s_scan_energy_lo = Cpt(AcsSignal, tag=53003, prec=6, kind="config", auto_monitor=True) + s_scan_energy_hi = Cpt(AcsSignal, tag=53004, prec=6, kind="config", auto_monitor=True) + s_scan_scantime = Cpt(AcsSignal, tag=53002, prec=3, kind="config", auto_monitor=True) # XAS advanced scan settings a_scan_pos = Cpt(EpicsSignalWithRBV, suffix="a_scan_pos", kind="config", auto_monitor=True) @@ -262,9 +270,9 @@ class Mo1BraggPositioner(Device, PositionerBase): angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True) - # test = Cpt(ACSSignal, tag=53000) # s_scan_angle_low - - test = Cpt(ACSVariableSignal, tag=53000, kind="normal") # s_scan_angle_low + # test = Cpt(AcsSignal, tag=53000, prec=6, kind="normal") # s_scan_angle_low + # test2 = Cpt(AcsSignalRO, tag=12007, prec=6, kind="normal") # scan_msg + # test3 = Cpt(AcsSignal, tag=53500, prec=6, num_el=41, kind="normal") # a_scan_pos ########## Move Command PVs ########## -- 2.54.0 From e6625097d530a9ac53ce02323f79d6b530ba4e2b Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 4 Aug 2026 12:19:39 +0200 Subject: [PATCH 5/6] WIP controller and signals --- debye_bec/devices/mo1_bragg/acs.py | 131 ++++++++++--------- debye_bec/devices/mo1_bragg/acscontroller.py | 62 --------- 2 files changed, 70 insertions(+), 123 deletions(-) delete mode 100644 debye_bec/devices/mo1_bragg/acscontroller.py diff --git a/debye_bec/devices/mo1_bragg/acs.py b/debye_bec/devices/mo1_bragg/acs.py index f874d41..d423d03 100644 --- a/debye_bec/devices/mo1_bragg/acs.py +++ b/debye_bec/devices/mo1_bragg/acs.py @@ -13,29 +13,16 @@ Protocol: from __future__ import annotations -from typing import TYPE_CHECKING +from enum import Enum import numpy as np -from ophyd import Component as Cpt -from ophyd import Kind -from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase -from ophyd_devices.utils.controller import Controller, threadlocked -from ophyd_devices.utils.socket import SocketIO, SocketSignal - -if TYPE_CHECKING: - from bec_lib.devicemanager import DeviceManagerBase - from bec_lib.logger import bec_logger +from ophyd_devices.utils.controller import Controller, threadlocked +from ophyd_devices.utils.socket import SocketSignal -# Initialise logger logger = bec_logger.logger -# --------------------------------------------------------------------------- -# Shared communicator -# --------------------------------------------------------------------------- - - class ACSController(Controller): """ Shared TCP/IP communicator for one ACS controller. @@ -47,32 +34,57 @@ class ACSController(Controller): _axes_per_controller = 0 # not used for plain variables, no motion axes + def __init__(self, *, socket_cls, socket_host, socket_port, device_manager): + socket_cls.socket_timeout = 5 + super().__init__( + socket_cls=socket_cls, + socket_host=socket_host, + socket_port=socket_port, + device_manager=device_manager, + term="\r", + trail=["\r:\r", ":\r"], + ) + @threadlocked - def get_var(self, tag: int, precision=3) -> float: + def get_var(self, tag: int, prec: int, idx: int | None = None) -> float: if self.sock is None: self.on() - i = 0 - reply = self.socket_put_and_receive(f"?{{%0.{precision:0.0f}f}}GETVAR({tag},{i})") + + idx = f",{idx:0.0f}" if idx is not None else "" + reply = self.socket_put_and_receive(f"?{{%0.{prec:0.0f}f}}GETVAR({tag}{idx})") + + if reply.startswith("?"): + error = self._query_error(reply) + raise RuntimeError(f"ACS error {reply}: {error}") + return float(reply) @threadlocked - def set_var(self, tag: int, value, precision=3) -> None: + def _query_error(self, reply: str) -> str: + # reply is like "?2002" + return self.socket_put_and_receive(f"?{reply}") + + @threadlocked + def set_var(self, tag: int, value, prec: int, idx: int | None = None) -> None: if self.sock is None: self.on() - i = 0 - self.socket_put_and_receive(f"SETVAR({np.round(value, precision)},{tag},{i})") + idx = f",{idx:0.0f}" if idx is not None else "" + # logger.info(f"Send request: SETVAR({np.round(value, prec)},{tag}{idx})") + reply = self.socket_put_and_receive(f"SETVAR({np.round(value, prec)},{tag}{idx})") + + if reply.startswith("?"): + error = self._query_error(reply) + raise RuntimeError(f"ACS error {reply}: {error}") -# --------------------------------------------------------------------------- -# Signal talking through the shared controller -# --------------------------------------------------------------------------- - - -class ACSVariableSignal(SocketSignal): +class AcsSignal(SocketSignal): """Read/write ACS controller variable, identified by its tag number.""" - def __init__(self, *args, tag: int, **kwargs): + def __init__(self, *args, tag: int, prec: int, num_el: int = 1, enum: Enum = None, **kwargs): self.tag = tag + self.prec = prec + self.num_el = num_el + self.enum = enum super().__init__(*args, **kwargs) @property @@ -80,40 +92,37 @@ class ACSVariableSignal(SocketSignal): return self.root.controller def _socket_get(self): - logger.info(self.controller) - logger.info(self.controller.sock) - return self.controller.get_var(self.tag) + def convert(val): + return self.enum(val).name if self.enum is not None else val + + if self.num_el <= 1: + return convert(self.controller.get_var(self.tag, self.prec)) + return np.array( + [convert(self.controller.get_var(self.tag, self.prec, i)) for i in range(self.num_el)] + ) def _socket_set(self, val): - self.controller.set_var(self.tag, val) + def convert(v): + if self.enum is None: + return v + if isinstance(v, str): + return self.enum[v].value # e.g. "SI111" -> 0 + return self.enum(v).value # e.g. 0 or Xtal.SI111 -> 0 + + if self.num_el <= 1: + self.controller.set_var(self.tag, convert(val), self.prec) + else: + if len(val) != self.num_el: + raise ValueError( + f"Length of val ({len(val)}) must be equal to specified length of variable ({self.num_el})" + ) + + for i, v in enumerate(val): + self.controller.set_var(self.tag, convert(v), self.prec, i) -# --------------------------------------------------------------------------- -# The device -# --------------------------------------------------------------------------- +class AcsSignalRO(AcsSignal): + """Readonly ACS controller variable, identified by its tag number.""" - -class ACSVariables(PSIDeviceBase): - """Three read/write variables on an ACS controller, sharing one connection.""" - - var1 = Cpt(ACSVariableSignal, tag=1, kind=Kind.normal) - var2 = Cpt(ACSVariableSignal, tag=2, kind=Kind.normal) - var3 = Cpt(ACSVariableSignal, tag=3, kind=Kind.normal) - - def __init__( - self, - name: str, - host: str, - port: int = 701, - device_manager: "DeviceManagerBase" | None = None, - **kwargs, - ): - # controller must exist before super().__init__() builds the Cpt signals - self.controller = ACSController( - socket_cls=SocketIO, socket_host=host, socket_port=port, device_manager=device_manager - ) - super().__init__(name=name, device_manager=device_manager, **kwargs) - - def on_connected(self): - # Idempotent: safe even if other devices already opened this controller. - self.controller.on() + def _socket_set(self, val): + return diff --git a/debye_bec/devices/mo1_bragg/acscontroller.py b/debye_bec/devices/mo1_bragg/acscontroller.py deleted file mode 100644 index faf4900..0000000 --- a/debye_bec/devices/mo1_bragg/acscontroller.py +++ /dev/null @@ -1,62 +0,0 @@ -import socket -import threading - -import numpy as np -from ophyd.signal import Signal - - -class ACSSignal(Signal): - - def __init__(self, controller, tag, **kwargs): - self.controller = controller - self.tag = tag - super().__init__(**kwargs) - - def get(self): - value = self.controller.read_tag(self.tag) - self._readback = value - return value - - def put(self, value, **kwargs): - self.controller.write_tag(self.tag, value) - self._readback = value - return super().put(value, **kwargs) - - -class ACSController: - - def __init__(self, host: str, port: int, timeout: float = 2.0): - self.host = host - self.port = port - self.timeout = timeout - self._socket = None - self._lock = threading.Lock() - - self.precision = 12 - - def connect(self): - if self._socket is not None: - return - self._socket = socket.create_connection((self.host, self.port), timeout=self.timeout) - - def disconnect(self): - if self._socket is not None: - self._socket.close() - self._socket = None - - def _query(self, command: str) -> str: - with self._lock: - self.connect() - msg = (command + "\r\n").encode() - self._socket.sendall(msg) - reply = self._socket.recv(4096) - return reply.decode().strip() - - def read_tag(self, tag: int): - i = 0 # For arrays, to be implemented - response = self._query(f"?{{%0.{self.precision:0.0f}f}}GETVAR({tag},{i}") - return float(response) - - def write_tag(self, tag: int, value): - i = 0 # For arrays, to be implemented - self._query(f"SETVAR({np.round(value, self.precision)},{tag},{i}") -- 2.54.0 From ff85ff08e06117aa3d6f89ffa7c4d2527ef34776 Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 5 Aug 2026 10:09:25 +0200 Subject: [PATCH 6/6] wip socket signals --- debye_bec/devices/mo1_bragg/acs.py | 14 +++++++- .../devices/mo1_bragg/mo1_bragg_devices.py | 32 +++++++++++-------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/acs.py b/debye_bec/devices/mo1_bragg/acs.py index d423d03..6eafa61 100644 --- a/debye_bec/devices/mo1_bragg/acs.py +++ b/debye_bec/devices/mo1_bragg/acs.py @@ -13,6 +13,8 @@ Protocol: from __future__ import annotations +import time +import traceback from enum import Enum import numpy as np @@ -43,6 +45,7 @@ class ACSController(Controller): device_manager=device_manager, term="\r", trail=["\r:\r", ":\r"], + socket_timeout=0.1, ) @threadlocked @@ -85,6 +88,7 @@ class AcsSignal(SocketSignal): self.prec = prec self.num_el = num_el self.enum = enum + self.last_get = time.time() super().__init__(*args, **kwargs) @property @@ -92,11 +96,19 @@ class AcsSignal(SocketSignal): return self.root.controller def _socket_get(self): + now = time.time() + interval = now - self.last_get + self.last_get = now + logger.info(f"Get signal with tag {self.tag}, time to last get: {interval*1e3} ms") + # logger.info(f"socket_get called from: {traceback.format_stack()}") + def convert(val): return self.enum(val).name if self.enum is not None else val if self.num_el <= 1: - return convert(self.controller.get_var(self.tag, self.prec)) + val = convert(self.controller.get_var(self.tag, self.prec)) + logger.info(f"Get signal with tag {self.tag}, time to last get: {interval*1e3} ms") + return val return np.array( [convert(self.controller.get_var(self.tag, self.prec, i)) for i in range(self.num_el)] ) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index 13f515b..7ee3051 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -45,9 +45,9 @@ class MoveTypeSignal(Signal): # pylint: disable=arguments-differ def set(self, value: str | MoveType) -> None: """Returns currently active move method - - Args: - value (str | MoveType) : Can be either 'energy' or 'angle' + auto_monitor=True + Args: + value (str | MoveType) : Can be either 'energy' or 'angle' """ value = MoveType(value.lower()) @@ -112,14 +112,14 @@ class Mo1BraggCrystal(Device): current_xtal = Cpt( EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True ) - # current_xtal_string = Cpt( - # EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True, string=True - # ) - current_xtal_string = Cpt( - AcsSignalRO, tag=10501, prec=0, enum=Xtal, kind="normal", auto_monitor=True + EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True, string=True ) + # current_xtal_string = Cpt( + # AcsSignalRO, tag=10501, prec=0, enum=Xtal, kind="normal", auto_monitor=True + # ) + class Mo1BraggScanSettings(Device): """Mo1 Bragg PVs to set the scan setttings""" @@ -148,14 +148,18 @@ class Mo1BraggScanSettings(Device): # EpicsSignalWithRBV, suffix="s_scan_scantime", kind="config", auto_monitor=True # ) - s_scan_energy_lo = Cpt(AcsSignal, tag=53003, prec=6, kind="config", auto_monitor=True) - s_scan_energy_hi = Cpt(AcsSignal, tag=53004, prec=6, kind="config", auto_monitor=True) - s_scan_scantime = Cpt(AcsSignal, tag=53002, prec=3, kind="config", auto_monitor=True) + s_scan_energy_lo = Cpt(AcsSignal, tag=53003, prec=6, kind="config", auto_monitor=False) + s_scan_energy_hi = Cpt(AcsSignal, tag=53004, prec=6, kind="config", auto_monitor=False) + s_scan_scantime = Cpt(AcsSignal, tag=53002, prec=3, kind="config", auto_monitor=False) # XAS advanced scan settings - a_scan_pos = Cpt(EpicsSignalWithRBV, suffix="a_scan_pos", kind="config", auto_monitor=True) - a_scan_vel = Cpt(EpicsSignalWithRBV, suffix="a_scan_vel", kind="config", auto_monitor=True) - a_scan_time = Cpt(EpicsSignalWithRBV, suffix="a_scan_time", kind="config", auto_monitor=True) + a_scan_pos = Cpt(EpicsSignalWithRBV, suffix="a_scan_pos", kind="config", auto_monitor=False) + a_scan_vel = Cpt(EpicsSignalWithRBV, suffix="a_scan_vel", kind="config", auto_monitor=False) + a_scan_time = Cpt(EpicsSignalWithRBV, suffix="a_scan_time", kind="config", auto_monitor=False) + + # a_scan_pos = Cpt(AcsSignal, tag=53500, prec=6, num_el=41, kind="omitted") + # a_scan_vel = Cpt(AcsSignal, tag=53501, prec=6, num_el=41, kind="omitted") + # a_scan_time = Cpt(AcsSignal, tag=53502, prec=6, num_el=41, kind="omitted") class Mo1TriggerSettings(Device): -- 2.54.0