From d2c6b565101166d68d35d09055cf87fad89e6636 Mon Sep 17 00:00:00 2001 From: x01da Date: Mon, 3 Aug 2026 10:54:54 +0200 Subject: [PATCH] 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