141 lines
4.7 KiB
Python
141 lines
4.7 KiB
Python
"""
|
|
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
|