Another SmarGon client approach

This commit is contained in:
gac-x06da
2025-03-11 13:24:06 +01:00
committed by mohacsi_i
parent c8a1add697
commit 6b4a175f78
3 changed files with 266 additions and 6 deletions
+20
View File
@@ -19,6 +19,26 @@ from bec_lib import bec_logger
logger = bec_logger.logger
class SilentNDDerivedSignal(NDDerivedSignal):
def inverse(self, value):
"""Shape the flat array to send as a result of ``.get``"""
array_shape = self.derived_shape[: self.derived_ndims]
if not any(array_shape):
raise RuntimeWarning(f"Invalid array size {self.derived_shape}")
return self._readback
array_len = np.prod(array_shape)
if len(value) < array_len:
raise RuntimeWarning(
f"cannot reshape array of size {len(value)} "
f"into shape {tuple(array_shape)}. Check IOC configuration."
)
return self._readback
return np.asarray(value[:array_len]).reshape(array_shape)
class NDArrayPreview(Device):
"""Wrapper class around AreaDetector's NDStdArray plugins
+13 -6
View File
@@ -9,6 +9,7 @@ The SmarGon axes are interfaced as positioners.
import time
from threading import Thread, Lock
import requests
from requests.adapters import HTTPAdapter, Retry
from ophyd import Component, Kind, Signal, PVPositioner
from ophyd.status import SubscriptionStatus
@@ -167,6 +168,12 @@ class SmarGonAxis(PVPositioner):
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
# Fine-tune HTTP connection behavior
# NOTE: SmarGon has a few failed requests every one in a while
self._s = requests.Session()
retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[ 500, 502, 503, 504 ])
self._s.mount('http://', HTTPAdapter(max_retries=retries))
super().__init__(
prefix=prefix,
name=name,
@@ -210,16 +217,16 @@ class SmarGonAxis(PVPositioner):
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = requests.get(cmd, timeout=1, **kwargs)
r = self._s.get(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = requests.get(cmd, timeout=0.5, **kwargs)
r = self._s.get(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = requests.get(cmd, timeout=0.5, **kwargs)
r = self._s.get(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}"
@@ -231,16 +238,16 @@ class SmarGonAxis(PVPositioner):
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = requests.put(cmd, timeout=1, **kwargs)
r = self._s.put(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = requests.put(cmd, timeout=0.5, **kwargs)
r = self._s.put(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = requests.put(cmd, timeout=0.5, **kwargs)
r = self._s.put(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}"
+233
View File
@@ -0,0 +1,233 @@
"""
``SmarGon`` --- SmarGon control software
******************************************
The module provides an object to control the SmarGon goniometer axes at PX III.
The SmarGon axes are interfaced as positioners.
"""
import time
import threading
from threading import Thread, Lock
import requests
from requests.adapters import HTTPAdapter, Retry
from ophyd import Component, Kind, Signal, PVPositioner
from ophyd.status import SubscriptionStatus
try:
from bec_lib import bec_logger
logger = bec_logger.logger
except ModuleNotFoundError:
import logging
logger = logging.getLogger("SmarGon")
# SmarGon contoller can't really handle multiple connections
# Use this mutex to ensure one access at a time
mutex = Lock()
class LimitedSmarGonSignal(Signal):
"""SmarGonSignal (R/W)
Small helper class to read/write parameters from SmarGon. As there is no
motion status readback from smargopolo, this should be substituted with
setting with 'settle_time'.
"""
def __init__(self, *args, write_addr="targetSCS", low_limit=None, high_limit=None, **kwargs):
self._limits = (low_limit, high_limit)
super().__init__(*args, **kwargs)
self.write_addr = write_addr
@property
def limits(self):
return self._limits
def check_value(self, value, **kwargs):
"""Check if value falls within limits"""
lol = self.limits[0]
if lol is not None:
if value < lol:
raise ValueError(f"Target {value} outside of limits {self.limits}")
hil = self.limits[1]
if hil is not None:
if value > hil:
raise ValueError(f"Target {value} outside of limits {self.limits}")
def put(self, value, *, timestamp=None, **kwargs):
"""Overriden put to add communication with smargopolo"""
# Validate new value and get timestamp
self.check_value(value)
if timestamp is None:
timestamp = time.time()
# Perform the actual write to SmargoPolo
# pylint: disable=protected-access
r = self.parent._go_n_put(f"{self.write_addr}?{self.parent.name.upper()}={value}")
# pylint: disable=attribute-defined-outside-init
old_value = self._readback
self._timestamp = timestamp
self._readback = r[self.parent.name.upper()]
self._value = r[self.parent.name.upper()]
# Notify subscribers
self._run_subs(
sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=self._timestamp
)
class SmarGonAxis(PVPositioner):
"""SmarGon client deice
This class controls the SmarGon goniometer via the REST interface. All
SmarGon axes share a common mutex to manage actual HW access.
"""
USER_ACCESS = ["omove"]
# Status attributes
sg_url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
# Axis parameters
readback = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
setpoint = Component(LimitedSmarGonSignal, kind=Kind.normal)
done = Component(Signal, value=1, kind=Kind.normal, metadata={"write_access": False})
_tol = 0.001
# pylint: disable=too-many-arguments
def __init__(
self,
prefix="SCS",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
low_limit=None,
high_limit=None,
**kwargs,
) -> None:
# self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}"
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
# Fine-tune HTTP connection behavior
# NOTE: SmarGon has a few failed requests every one in a while
self._s = requests.Session()
retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[ 500, 502, 503, 504 ])
self._s.mount('http://', HTTPAdapter(max_retries=retries))
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
def on_target():
"""NOTE: This assumes that both readback and setpoint is always up to date"""
time.sleep(2)
while True:
# Read back target and setpoint values
# pylint: disable=protected-access
r = self._go_n_get("readbackSCS")
rb = r[self.name.upper()]
self.readback.set(rb, force=True).wait()
r = self._go_n_get("targetSCS")
sp = r[self.name.upper()]
self.setpoint._value = sp
# print(f"Readback: {rb}\tSetpoint: {sp}")
# Check if they're within tolerance
distance = abs(rb - sp)
done = 1 if distance<self._tol else 0
self.done.put(done, force=True)
time.sleep(0.2)
self._mon = threading.Thread(target=on_target, daemon=True)
self._mon.start()
def move(self, position, wait=True, timeout=None, moved_cb=None):
"""Move command that's masked by BEC"""
return self.omove(position, wait, timeout, moved_cb)
def omove(self, position, wait=True, timeout=2.0, moved_cb=None):
"""Original move command without the BEC wrappers"""
status = self.setpoint.set(position, settle_time=0.1).wait()
if not wait:
return status
def on_target(*, value, **_):
distance = abs(value - self.setpoint._value)
print(f"[self.name] Distance: {distance}")
return bool(distance < self._tol)
status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1)
return status
def _pos_changed(self, timestamp=None, value=None, **kwargs):
pass
def _go_n_get(self, address, **kwargs):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = self._s.get(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = self._s.get(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = self._s.get(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}"
)
return r.json()
def _go_n_put(self, address, **kwargs):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = self._s.put(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = self._s.put(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = self._s.put(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}"
)
return r.json()
if __name__ == "__main__":
shx = SmarGonAxis(prefix="SCS", name="shx", sg_url="http://x06da-smargopolo.psi.ch:3000")
shy = SmarGonAxis(prefix="SCS", name="shy", sg_url="http://x06da-smargopolo.psi.ch:3000")
shz = SmarGonAxis(
prefix="SCS",
name="shz",
low_limit=10,
high_limit=22,
sg_url="http://x06da-smargopolo.psi.ch:3000",
)
shx.wait_for_connection()
shy.wait_for_connection()
shz.wait_for_connection()