Another SmarGon client approach
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user