Files
csaxs_bec/csaxs_bec/devices/sim/sim_socket.py
T
x01dcandholler 704826c94a feat(sim): add simulated flOMNI endstation (galil, smaract, rt, cameras)
Protocol-level simulation of the flOMNI hardware injected via socket_cls:
real device/controller classes run unchanged against state machines
implementing fgalil.dmc, galil_micos_upr.dmc, the Smaract MCS protocol
and the Orchestra CommunicationServer. Cameras reuse the real device
classes with a synthetic frame source. Includes simulated_flomni device
config (boots referenced at 'in' positions) and an offline harness
covering moves, referencing, rt feedback/tracker, flyer scan readout
and the gripper transfer routine.
2026-07-12 07:52:37 +02:00

111 lines
4.0 KiB
Python

"""
Core infrastructure for simulated controller sockets.
The socket-based controllers in ophyd_devices (`Controller.on`) create their transport via
``self._socket_cls(host=..., port=...)``. Any class exposing the `SocketIO` interface
(`put`, `receive`, `open`, `close`, `host`, `port`, `is_open`) can be injected instead of a
real TCP socket. The simulated sockets in this package implement the wire protocols of the
flOMNI hardware (Galil DMC, Smaract MCS, flOMNI Orchestra communication server) as small
state machines, so that the *real* device and controller classes can be used unchanged.
Commands are dispatched synchronously within `put()`; replies are appended to an internal
queue that `receive()` drains. Commands that do not produce a reply on the real hardware
must not enqueue one here, otherwise the command/reply stream desynchronizes.
Simulation state is shared per (host, port) via a registry, mirroring the singleton
behavior of `ophyd_devices.utils.controller.Controller`. This allows the simulated device
classes to seed axis parameters (initial position, velocity, resolution) at construction
time, before the controller opens its socket.
"""
from __future__ import annotations
import threading
from bec_lib.logger import bec_logger
logger = bec_logger.logger
class SimStateRegistry:
"""Registry of simulation states, shared per (host, port) like the controllers."""
_states = {}
_lock = threading.RLock()
@classmethod
def get(cls, state_cls, host, port):
"""Return the simulation state for (host, port), creating it if needed."""
key = (state_cls, str(host), int(port))
with cls._lock:
if key not in cls._states:
cls._states[key] = state_cls(host=host, port=port)
return cls._states[key]
@classmethod
def reset(cls):
"""Clear all simulation states (mainly for tests)."""
with cls._lock:
cls._states.clear()
class SimSocketBase:
"""
Drop-in replacement for `ophyd_devices.utils.socket.SocketIO`.
Children must set `state_cls` and implement `handle_command(line) -> str | None`.
A returned string is encoded and appended to the reply queue; None means no reply.
"""
state_cls = None
def __init__(self, host, port, socket_timeout: int = 2):
self.host = host
self.port = port
self.socket_timeout = socket_timeout
self.is_open = False
self._recv_buffer = []
self._cmd_buffer = b""
self._lock = threading.RLock()
self.state = SimStateRegistry.get(self.state_cls, host, port)
# --- SocketIO interface -------------------------------------------------
def open(self, timeout: int = 10):
logger.info(f"[sim] Connecting to simulated controller {self.host}:{self.port}.")
self.is_open = True
def connect(self, timeout: int = 10):
self.is_open = True
def close(self):
self.is_open = False
def put(self, msg: bytes):
with self._lock:
self._cmd_buffer += msg
# both \r (Galil) and \n (RT, Smaract) terminate commands
normalized = self._cmd_buffer.replace(b"\r", b"\n")
*lines, rest = normalized.split(b"\n")
self._cmd_buffer = rest
for line in lines:
line = line.decode(errors="replace").strip()
if not line:
continue
try:
reply = self.handle_command(line)
except Exception: # pylint: disable=broad-except
logger.exception(f"[sim] {self.host}:{self.port} failed to handle '{line}'")
reply = None
if reply is not None:
self._recv_buffer.append(reply.encode())
def receive(self, buffer_length=1024):
with self._lock:
if self._recv_buffer:
return self._recv_buffer.pop(0)
return b""
# --- to be implemented by children ---------------------------------------
def handle_command(self, line: str):
raise NotImplementedError