feat(mo1_bragg): add direct ACS controller communication for scan settings
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
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
|
||||
|
||||
import time
|
||||
import traceback
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketSignal
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
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
|
||||
|
||||
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"],
|
||||
socket_timeout=0.1,
|
||||
)
|
||||
|
||||
@threadlocked
|
||||
def get_var(self, tag: int, prec: int, idx: int | None = None) -> float:
|
||||
if self.sock is None:
|
||||
self.on()
|
||||
|
||||
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 _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()
|
||||
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}")
|
||||
|
||||
|
||||
class AcsSignal(SocketSignal):
|
||||
"""Read/write ACS controller variable, identified by its tag number."""
|
||||
|
||||
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
|
||||
self.last_get = time.time()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def controller(self) -> ACSController:
|
||||
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:
|
||||
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)]
|
||||
)
|
||||
|
||||
def _socket_set(self, 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)
|
||||
|
||||
|
||||
class AcsSignalRO(AcsSignal):
|
||||
"""Readonly ACS controller variable, identified by its tag number."""
|
||||
|
||||
def _socket_set(self, val):
|
||||
return
|
||||
@@ -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()
|
||||
|
||||
@@ -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,11 @@ from ophyd import (
|
||||
Signal,
|
||||
)
|
||||
from ophyd.utils import LimitError
|
||||
from ophyd_devices.utils.socket import SocketIO
|
||||
|
||||
from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType
|
||||
# from debye_bec.devices.mo1_bragg.acs_controller import ACSSignal
|
||||
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
|
||||
@@ -39,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())
|
||||
@@ -110,6 +116,10 @@ class Mo1BraggCrystal(Device):
|
||||
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"""
|
||||
@@ -128,20 +138,28 @@ 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=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):
|
||||
@@ -256,6 +274,10 @@ class Mo1BraggPositioner(Device, PositionerBase):
|
||||
|
||||
angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True)
|
||||
|
||||
# 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 ##########
|
||||
|
||||
move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True)
|
||||
@@ -265,7 +287,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:
|
||||
@@ -273,11 +295,21 @@ 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
|
||||
|
||||
def stop(self, *, success=False) -> None:
|
||||
"""Stop any motion on the positioner
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user