Tell: start of rework - needs testing
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Callable, Protocol
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from aare.common.exception_handler import TellCommunicationError
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
from aare.common.beamline import MXBeamline # noqa: F401
|
||||
from pshell import PShellClient
|
||||
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
|
||||
VALID_DEWAR_POSITIONS = [f"{p}{n}" for n in "12345" for p in "ABCDEFX"]
|
||||
|
||||
|
||||
def is_valid_dewar_position(position):
|
||||
"""check if argument is a valid dewar position"""
|
||||
return position in VALID_DEWAR_POSITIONS
|
||||
|
||||
|
||||
POSITION_PARK = "pPark"
|
||||
POSITION_COLD = "pCold"
|
||||
POSITION_AUX = "pAux"
|
||||
POSITION_DEWAR = "pDewar"
|
||||
POSITION_HOME = "pHome"
|
||||
POSITION_HEATER = "pHeatB"
|
||||
|
||||
|
||||
class ManualMountException(Exception):
|
||||
"""Custom exception for manual mounting"""
|
||||
pass
|
||||
|
||||
|
||||
class SmartMagnetFaultException(Exception):
|
||||
"""Custom exception for smart magnet fault"""
|
||||
pass
|
||||
|
||||
|
||||
class TellMountFailedException(Exception):
|
||||
"""Custom exception for mount failure"""
|
||||
pass
|
||||
|
||||
|
||||
class TellCommandWhileBusyException(Exception):
|
||||
"""Custom exception for trying to move Tell when it is busy"""
|
||||
pass
|
||||
|
||||
|
||||
class TellConnectionException(Exception):
|
||||
"""Custom exception for connection problems"""
|
||||
pass
|
||||
|
||||
|
||||
class TellBackend(Protocol):
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
...
|
||||
|
||||
def get_state(self) -> str:
|
||||
...
|
||||
|
||||
def get_result(self, command_id: int = -1):
|
||||
...
|
||||
|
||||
def wait_state(self, state: str, timeout: float) -> None:
|
||||
...
|
||||
|
||||
def wait_state_not(self, state: str, timeout: float) -> None:
|
||||
...
|
||||
|
||||
def wait_events(self, events: dict[str, Any], timeout: float):
|
||||
...
|
||||
|
||||
def eval(self, expr: str):
|
||||
...
|
||||
|
||||
def start_eval(self, expr: str) -> int:
|
||||
...
|
||||
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
...
|
||||
|
||||
def abort(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class PShellTellBackend:
|
||||
def __init__(self, bl: MXBeamline):
|
||||
self._url = self._resolve_url(bl)
|
||||
|
||||
print(f"Connecting TELL p-shell service at {self._url} ...", end="")
|
||||
hostname = urlparse(self._url).hostname
|
||||
try:
|
||||
requests.get(f"{self._url}/history/0", timeout=1.0)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"...connection to {hostname} failed")
|
||||
raise TellCommunicationError(
|
||||
f"TELL connection failed ({hostname})",
|
||||
base_url=self._url,
|
||||
endpoint="history/0",
|
||||
operation="GET",
|
||||
) from e
|
||||
except requests.ReadTimeout as e:
|
||||
print(f"...PShell service {hostname} is down")
|
||||
raise TellCommunicationError(
|
||||
f"TELL connection timedout ({hostname})",
|
||||
base_url=self._url,
|
||||
endpoint="history/0",
|
||||
operation="GET",
|
||||
) from e
|
||||
|
||||
self._pshell = PShellClient(self._url)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_url(bl: MXBeamline) -> str:
|
||||
beamline = bl.value.lower()
|
||||
if bl == MXBeamline.X06DA:
|
||||
return f"http://{beamline}-tell.psi.ch:22222"
|
||||
if bl == MXBeamline.X10SA:
|
||||
return "http://PC17488:22222"
|
||||
if bl == MXBeamline.X06SA:
|
||||
raise NotImplementedError(f"TellClient not implemented for {beamline}")
|
||||
if bl == MXBeamline.SIMULATED:
|
||||
raise NotImplementedError("Use SimTellBackend for MXBeamline.SIMULATED")
|
||||
raise ValueError(f"Unknown beamline {beamline}")
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
return self._url
|
||||
|
||||
def get_state(self) -> str:
|
||||
return self._pshell.get_state()
|
||||
|
||||
def get_result(self, command_id: int = -1):
|
||||
return self._pshell.get_result(command_id)
|
||||
|
||||
def wait_state(self, state: str, timeout: float) -> None:
|
||||
self._pshell.wait_state(state, timeout=timeout)
|
||||
|
||||
def wait_state_not(self, state: str, timeout: float) -> None:
|
||||
self._pshell.wait_state_not(state, timeout=timeout)
|
||||
|
||||
def wait_events(self, events: dict[str, Any], timeout: float):
|
||||
return self._pshell.wait_events(events, timeout=timeout)
|
||||
|
||||
def eval(self, expr: str):
|
||||
return self._pshell.eval(expr)
|
||||
|
||||
def start_eval(self, expr: str) -> int:
|
||||
return self._pshell.start_eval(expr)
|
||||
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
self._pshell.run(path, pars=pars, background=background)
|
||||
|
||||
def abort(self) -> None:
|
||||
self._pshell.abort()
|
||||
|
||||
class SimTellBackend:
|
||||
def __init__(self):
|
||||
self._url: str | None = None
|
||||
self._state = "Ready"
|
||||
self._last_cmd_id = 1000
|
||||
self._mounted_sample = ""
|
||||
self._settings: dict[str, str] = {"mounted_sample_position": ""}
|
||||
self._results: dict[int, dict[str, Any]] = {}
|
||||
self._robot_status: dict[str, Any] = {
|
||||
"powered": True,
|
||||
"pos": POSITION_PARK,
|
||||
}
|
||||
self._current_mA = 30.0
|
||||
self._pin_offset = 0.0
|
||||
self._detected_pucks: list[dict[str, Any]] = []
|
||||
self._system_check_msg = "OK"
|
||||
self._smart_magnet_state = "Ready"
|
||||
self._in_mount_position = False
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
return self._url
|
||||
|
||||
def _next_cmd_id(self) -> int:
|
||||
self._last_cmd_id += 1
|
||||
return self._last_cmd_id
|
||||
|
||||
def _set_ready_soon(self) -> None:
|
||||
time.sleep(0.01)
|
||||
self._state = "Ready"
|
||||
|
||||
def get_state(self) -> str:
|
||||
return self._state
|
||||
|
||||
def get_result(self, command_id: int = -1):
|
||||
if command_id == -1:
|
||||
command_id = self._last_cmd_id
|
||||
return self._results.get(command_id, {"status": "completed"})
|
||||
|
||||
def wait_state(self, state: str, timeout: float) -> None:
|
||||
if self._state != state:
|
||||
time.sleep(min(timeout, 0.05))
|
||||
self._state = state
|
||||
|
||||
def wait_state_not(self, state: str, timeout: float) -> None:
|
||||
if self._state == state:
|
||||
time.sleep(min(timeout, 0.05))
|
||||
self._state = "Ready"
|
||||
|
||||
def wait_events(self, events: dict[str, Any], timeout: float):
|
||||
self.wait_state_not("Busy", timeout)
|
||||
if "Motion Sync" in events:
|
||||
return "Motion Sync", "Robot Clear after mount"
|
||||
if "Motion Task" in events:
|
||||
return "Motion Task", "idle"
|
||||
return None, self._state
|
||||
|
||||
def eval(self, expr: str):
|
||||
expr = expr.strip()
|
||||
|
||||
if expr == "in_mount_position&":
|
||||
return "true" if self._in_mount_position else "false"
|
||||
|
||||
if expr.startswith("in_mount_position = "):
|
||||
self._in_mount_position = "True" in expr or "true" in expr
|
||||
return None
|
||||
|
||||
if expr.startswith("set_setting("):
|
||||
match = re.match(r"set_setting\('([^']+)', '([^']*)'\)&", expr)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
self._settings[key] = value
|
||||
return None
|
||||
|
||||
if expr.startswith("get_setting("):
|
||||
match = re.match(r"get_setting\('([^']+)'\)&", expr)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
return self._settings.get(key, "")
|
||||
return ""
|
||||
|
||||
if expr == "system_check_msg()&":
|
||||
return self._system_check_msg
|
||||
|
||||
if expr == "robot.state&":
|
||||
return self._state
|
||||
|
||||
if expr == "robot.take()&":
|
||||
return str(self._robot_status)
|
||||
|
||||
if expr == "get_pucks_info()&":
|
||||
return json.dumps(self._detected_pucks)
|
||||
|
||||
if expr == "get_pin_offset()&":
|
||||
return str(self._pin_offset)
|
||||
|
||||
if expr == "smart_magnet.get_current_rb()&":
|
||||
return str(self._current_mA)
|
||||
|
||||
if expr.startswith("smart_magnet.set_current("):
|
||||
match = re.match(r"smart_magnet\.set_current\(([-+]?\d+(?:\.\d+)?)\)&", expr)
|
||||
if match:
|
||||
self._current_mA = float(match.group(1))
|
||||
return None
|
||||
|
||||
if expr == "enable_motion()&":
|
||||
self._robot_status["powered"] = True
|
||||
return None
|
||||
|
||||
if expr == "smart_magnet.state&":
|
||||
return self._smart_magnet_state
|
||||
|
||||
if expr == "smart_magnet.set_supress(True)&":
|
||||
return None
|
||||
|
||||
if expr == "smart_magnet.set_supress(False)&":
|
||||
return None
|
||||
|
||||
if expr == "smart_magnet.set_resting_current()&":
|
||||
return None
|
||||
|
||||
if expr == "robot.stop_task()&":
|
||||
self._state = "Ready"
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def start_eval(self, expr: str) -> int:
|
||||
cmd_id = self._next_cmd_id()
|
||||
self._state = "Busy"
|
||||
|
||||
if expr.startswith("mount("):
|
||||
parts = re.findall(r"'([^']*)'|([^,()]+)", expr)
|
||||
values = [a if a else b.strip() for a, b in parts]
|
||||
if len(values) >= 4:
|
||||
segment = values[0]
|
||||
puck = values[1]
|
||||
sample = values[2]
|
||||
mounted = f"{segment}{puck}{sample}"
|
||||
self._mounted_sample = mounted
|
||||
self._settings["mounted_sample_position"] = mounted
|
||||
self._robot_status["pos"] = POSITION_DEWAR
|
||||
|
||||
elif expr.startswith("unmount("):
|
||||
self._mounted_sample = ""
|
||||
self._settings["mounted_sample_position"] = ""
|
||||
self._robot_status["pos"] = POSITION_PARK
|
||||
|
||||
elif expr.startswith("move_park("):
|
||||
self._robot_status["pos"] = POSITION_PARK
|
||||
|
||||
elif expr.startswith("move_cold("):
|
||||
self._robot_status["pos"] = POSITION_COLD
|
||||
|
||||
elif expr.startswith("dry("):
|
||||
self._robot_status["pos"] = POSITION_HEATER
|
||||
|
||||
self._results[cmd_id] = {"status": "completed", "command": expr}
|
||||
self._set_ready_soon()
|
||||
return cmd_id
|
||||
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
_ = background
|
||||
|
||||
if path == "data/set_samples_info" and pars:
|
||||
try:
|
||||
data = json.loads(pars[0])
|
||||
self._detected_pucks = []
|
||||
for item in data:
|
||||
puck_address = item.get("puckAddress", "")
|
||||
if puck_address:
|
||||
self._detected_pucks.append(
|
||||
{
|
||||
"puckState": "Present",
|
||||
"puckAddress": puck_address,
|
||||
"puckBarcode": item.get("puckBarcode", ""),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to load simulated samples info")
|
||||
|
||||
def abort(self) -> None:
|
||||
self._state = "Ready"
|
||||
|
||||
|
||||
class LazyTellBackend:
|
||||
def __init__(self, factory: Callable[[], TellBackend], *, retry_interval_s: float = 2.0):
|
||||
self._factory = factory
|
||||
self._backend: TellBackend | None = None
|
||||
self._retry_interval_s = float(retry_interval_s)
|
||||
self._last_attempt_ts = 0.0
|
||||
self._last_error: Exception | None = None
|
||||
|
||||
def _get_backend(self) -> TellBackend:
|
||||
if self._backend is not None:
|
||||
return self._backend
|
||||
|
||||
now = time.monotonic()
|
||||
if now - self._last_attempt_ts < self._retry_interval_s and self._last_error is not None:
|
||||
raise self._last_error
|
||||
|
||||
self._last_attempt_ts = now
|
||||
try:
|
||||
self._backend = self._factory()
|
||||
self._last_error = None
|
||||
return self._backend
|
||||
except TellCommunicationError as e:
|
||||
self._last_error = e
|
||||
raise
|
||||
except Exception as e:
|
||||
wrapped = TellCommunicationError(
|
||||
"TELL connection failed",
|
||||
operation="CONNECT",
|
||||
)
|
||||
self._last_error = wrapped
|
||||
raise wrapped from e
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
return self._get_backend().url
|
||||
|
||||
def get_state(self) -> str:
|
||||
return self._get_backend().get_state()
|
||||
|
||||
def get_result(self, command_id: int = -1):
|
||||
return self._get_backend().get_result(command_id)
|
||||
|
||||
def wait_state(self, state: str, timeout: float) -> None:
|
||||
self._get_backend().wait_state(state, timeout)
|
||||
|
||||
def wait_state_not(self, state: str, timeout: float) -> None:
|
||||
self._get_backend().wait_state_not(state, timeout)
|
||||
|
||||
def wait_events(self, events: dict[str, Any], timeout: float):
|
||||
return self._get_backend().wait_events(events, timeout)
|
||||
|
||||
def eval(self, expr: str):
|
||||
return self._get_backend().eval(expr)
|
||||
|
||||
def start_eval(self, expr: str) -> int:
|
||||
return self._get_backend().start_eval(expr)
|
||||
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
self._get_backend().run(path, pars=pars, background=background)
|
||||
|
||||
def abort(self) -> None:
|
||||
self._get_backend().abort()
|
||||
+93
-361
@@ -1,14 +1,9 @@
|
||||
import ast
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from aare.common.exception_handler import TellCommunicationError
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.models import (
|
||||
PuckLoadedInfo,
|
||||
DewarAddress,
|
||||
@@ -16,93 +11,28 @@ from aare.common.models import (
|
||||
)
|
||||
from aareDB import PuckWithTellPosition
|
||||
|
||||
from aare.common.beamline import MXBeamline # noqa: F401
|
||||
from pshell import PShellClient
|
||||
from aare.devices.tell_backend import (
|
||||
TellBackend,
|
||||
SimTellBackend,
|
||||
LazyTellBackend,
|
||||
PShellTellBackend,
|
||||
ManualMountException,
|
||||
POSITION_COLD,
|
||||
SmartMagnetFaultException,
|
||||
TellConnectionException,
|
||||
TellMountFailedException,
|
||||
TellCommandWhileBusyException,
|
||||
)
|
||||
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
class ManualMountException(Exception):
|
||||
"""Custom exception for manual mounting"""
|
||||
pass
|
||||
|
||||
|
||||
class SmartMagnetFaultException(Exception):
|
||||
"""Custom exception for smart magnet fault"""
|
||||
pass
|
||||
|
||||
|
||||
class TellMountFailedException(Exception):
|
||||
"""Custom exception for mount failure"""
|
||||
pass
|
||||
|
||||
|
||||
class TellCommandWhileBusyException(Exception):
|
||||
"""Custom exception for trying to move Tell when it is busy"""
|
||||
pass
|
||||
|
||||
|
||||
class TellConnectionException(Exception):
|
||||
"""Custom exception for connection problems"""
|
||||
pass
|
||||
|
||||
VALID_DEWAR_POSITIONS = [f"{p}{n}" for n in "12345" for p in "ABCDEFX"]
|
||||
|
||||
def is_valid_dewar_position(position):
|
||||
"""check if argument is a valid dewar position"""
|
||||
return position in VALID_DEWAR_POSITIONS
|
||||
|
||||
POSITION_PARK = "pPark"
|
||||
POSITION_COLD = "pCold"
|
||||
POSITION_AUX = "pAux"
|
||||
POSITION_DEWAR = "pDewar"
|
||||
POSITION_HOME = "pHome"
|
||||
POSITION_HEATER = "pHeatB"
|
||||
|
||||
#Nov 26 13:36:00 mx-x06da-queue-01.psi.ch AareDAQ[2944444]: 2025-11-26 13:36:00,388 - aareDAQ - ERROR - Error getting status: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
|
||||
|
||||
class TellClient:
|
||||
"""High-level Tell robot API using PShellClient"""
|
||||
def __init__(self, bl: MXBeamline):
|
||||
self.__url = None
|
||||
beamline = bl.value.lower()
|
||||
"""High-level Tell robot API using a pluggable backend"""
|
||||
def __init__(self, bl: MXBeamline, backend: TellBackend | None = None):
|
||||
self.__beamline = bl
|
||||
if bl == MXBeamline.X06DA:
|
||||
self.__url = f"http://{beamline}-tell.psi.ch:22222"
|
||||
|
||||
elif bl == MXBeamline.X10SA:
|
||||
self.__url = f"http://PC17488:22222"
|
||||
|
||||
elif bl == MXBeamline.X06SA:
|
||||
self.__url = f""
|
||||
raise NotImplemented(f"TellClient not implemente for {beamline}")
|
||||
elif bl == MXBeamline.SIMULATED:
|
||||
raise NotImplemented(f"Use SimClient, generate tell client using"
|
||||
f"make_tell_client(beamline)")
|
||||
else:
|
||||
raise ValueError(f"Unknown beamline {beamline}")
|
||||
|
||||
print(f"Connecting TELL p-shell service at {self.__url} ...", end="")
|
||||
hostname = urlparse(self.__url).hostname
|
||||
try:
|
||||
requests.get(f"{self.__url}/history/0", timeout=1.0)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"...connection to {hostname} failed")
|
||||
raise TellCommunicationError(
|
||||
f"TELL connection failed ({hostname})",
|
||||
base_url=self.__url,
|
||||
endpoint="history/0",
|
||||
operation="GET",
|
||||
) from e
|
||||
except requests.ReadTimeout as e:
|
||||
print(f"...PShell service {hostname} is down")
|
||||
raise TellCommunicationError(
|
||||
f"TELL connection timedout ({hostname})",
|
||||
base_url=self.__url,
|
||||
endpoint="history/0",
|
||||
operation="GET",
|
||||
) from e
|
||||
|
||||
self.pshell = PShellClient(self.__url)
|
||||
self.backend = backend or PShellTellBackend(bl)
|
||||
|
||||
self._aborted = False
|
||||
self.state = self.get_state()
|
||||
@@ -112,26 +42,26 @@ class TellClient:
|
||||
@property
|
||||
def url(self):
|
||||
"""returns the configured base url for the Tell robot"""
|
||||
return self.__url
|
||||
return self.backend.url
|
||||
|
||||
def get_state(self):
|
||||
"""returns the current state of the robot"""
|
||||
self.state = self.pshell.get_state()
|
||||
self.state = self.backend.get_state()
|
||||
return self.state
|
||||
|
||||
def get_result(self, command_id=-1):
|
||||
"""returns the result of the last command issued to the robot"""
|
||||
return self.pshell.get_result(command_id)
|
||||
return self.backend.get_result(command_id)
|
||||
|
||||
def wait_ready(self, timeout: float = 360.0):
|
||||
"""waits until the robot is ready to accept commands returns None if simulation
|
||||
and raises an exception if the robot is not ready"""
|
||||
self.pshell.wait_state("Ready", timeout=timeout)
|
||||
self.backend.wait_state("Ready", timeout=timeout)
|
||||
|
||||
def wait_not_busy(self, timeout: float = 360.0):
|
||||
"""waits until the robot is not busy and returns None if simulation
|
||||
and raises an exception if the robot is busy"""
|
||||
self.pshell.wait_state_not("Busy", timeout=timeout)
|
||||
self.backend.wait_state_not("Busy", timeout=timeout)
|
||||
state = self.get_state()
|
||||
if state != "Ready":
|
||||
if state == "Initializing":
|
||||
@@ -143,11 +73,11 @@ class TellClient:
|
||||
def set_in_mount_position(self, value):
|
||||
"""tells the robot that the beamlien is safe and to set the in mount position flag allowing mounting
|
||||
:param value """
|
||||
self.pshell.eval("in_mount_position = " + str(value) + "&")
|
||||
self.backend.eval("in_mount_position = " + str(value) + "&")
|
||||
|
||||
def is_in_mount_position(self) -> bool:
|
||||
"""checks to see if the robot is in the mount position and returns a boolean"""
|
||||
return self.pshell.eval("in_mount_position&").lower() == "true"
|
||||
return self.backend.eval("in_mount_position&").lower() == "true"
|
||||
|
||||
def set_samples_info(self, info: List[PuckWithTellPosition]):
|
||||
"""sets the samples in the robot dewar based on the given list of PuckWithTellPosition objects
|
||||
@@ -160,7 +90,7 @@ class TellClient:
|
||||
"userName": x.pgroup,
|
||||
"dewarName": x.dewar_name or "",
|
||||
"puckName": x.puck_name,
|
||||
"puckType": "Unipuck", # could use x.puck_type
|
||||
"puckType": "Unipuck",
|
||||
"puckAddress": x.tell_position or "",
|
||||
"puckBarcode": x.puck_name,
|
||||
"sampleBarcode": "",
|
||||
@@ -171,8 +101,7 @@ class TellClient:
|
||||
}
|
||||
)
|
||||
|
||||
self.pshell.run("data/set_samples_info", pars=[json.dumps(j)], background=True)
|
||||
# self.pshell.eval("set_samples_info(" + json.dumps(info) + ")&")
|
||||
self.backend.run("data/set_samples_info", pars=[json.dumps(j)], background=True)
|
||||
|
||||
def start_cmd(self, cmd, *argv):
|
||||
"""starts a command on the robot and returns the command id"""
|
||||
@@ -180,7 +109,7 @@ class TellClient:
|
||||
for a in argv:
|
||||
cmd = cmd + (("'" + a + "'") if type(a) is str else str(a)) + ", "
|
||||
cmd = cmd + ")"
|
||||
ret = self.pshell.start_eval(cmd)
|
||||
ret = self.backend.start_eval(cmd)
|
||||
self.get_state()
|
||||
return ret
|
||||
|
||||
@@ -191,11 +120,10 @@ class TellClient:
|
||||
result = self.get_result(self._last_cmd_id)
|
||||
logger.debug(f"{msg} {result}")
|
||||
status = result["status"]
|
||||
if "completed" != status: #FIXME this is very limiting and depends on tell reporting statuses
|
||||
if "completed" != status:
|
||||
if "removed" != status:
|
||||
raise TellMountFailedException(f"{msg} {result}")
|
||||
else:
|
||||
return f"{msg} {result}"
|
||||
return f"{msg} {result}"
|
||||
|
||||
def estimate_mounting_time(self, segment) -> int:
|
||||
"""Adds additional time if cooling/drying is expected based on requested segment,
|
||||
@@ -206,7 +134,7 @@ class TellClient:
|
||||
gripper_in_cold = self.is_in_cold()
|
||||
|
||||
if current_mounted is None:
|
||||
unmount_needs_drying = 0 # might not have anything
|
||||
unmount_needs_drying = 0
|
||||
unmount_needs_cooling = 0
|
||||
else:
|
||||
segment_in_cold = current_mounted.puck.segment in "ABCDEF"
|
||||
@@ -219,27 +147,18 @@ class TellClient:
|
||||
needs_cooling = mount_needs_cooling + unmount_needs_cooling
|
||||
needs_drying = mount_needs_drying + unmount_needs_drying
|
||||
return needs_cooling * 30 + needs_drying * 120
|
||||
except:
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def mount(
|
||||
self,
|
||||
address: SampleDewarAddress,
|
||||
force: bool = False, # kept for future
|
||||
read_dm: bool = False, # read data matrix
|
||||
auto_unmount: bool = False, # single command, if False it will raise exception
|
||||
wait: bool = False, # blocking operation
|
||||
force: bool = False,
|
||||
read_dm: bool = False,
|
||||
auto_unmount: bool = False,
|
||||
wait: bool = False,
|
||||
timeout: float = 600.0,
|
||||
):
|
||||
"""send api request to mount sample from dewer after validating dewer address returns None or repsonse.
|
||||
If the robot is busy, mount will raise an exception.
|
||||
:param address: SampleDewarAddress
|
||||
:param force: bool
|
||||
:param read_dm: bool
|
||||
:param auto_unmount: bool
|
||||
:param wait: bool
|
||||
:param timeout: float
|
||||
"""
|
||||
SampleDewarAddress.model_validate(address)
|
||||
|
||||
segment = address.puck.segment
|
||||
@@ -258,9 +177,16 @@ class TellClient:
|
||||
wait_timeout = timeout + self.estimate_mounting_time(segment)
|
||||
logger.info("waiting for mount to complete")
|
||||
if wait and segment in "ABCDEF":
|
||||
event, value = self.pshell.wait_events({"state": None, "Motion Task": "dry",
|
||||
"Gripper detection" : None,
|
||||
"Motion Sync": "Robot Clear after mount"}, timeout=wait_timeout)
|
||||
event, value = self.backend.wait_events(
|
||||
{
|
||||
"state": None,
|
||||
"Motion Task": "dry",
|
||||
"Gripper detection": None,
|
||||
"Motion Sync": "Robot Clear after mount",
|
||||
},
|
||||
timeout=wait_timeout,
|
||||
)
|
||||
logger.info(f"event: {event} occurred with value: {value}")
|
||||
if event is None or event == "state":
|
||||
logger.info(f"event: {event} occurred with value: {value}, checking command completed okay")
|
||||
self.check_command_ok(
|
||||
@@ -299,11 +225,6 @@ class TellClient:
|
||||
return None
|
||||
|
||||
def unmount(self, force=False, wait=False, timeout=360.0):
|
||||
"""send api request to unmount sample from dewer returns None or repsonse.
|
||||
:param force: bool Force has a meaning, will unmount even if smart magnet is not detecting sample
|
||||
:param wait: bool If true will wait until unmount is completed
|
||||
:timeout: float"""
|
||||
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
@@ -315,82 +236,60 @@ class TellClient:
|
||||
return self._last_cmd_id
|
||||
|
||||
def dry(self, heat_time=None, speed=None, wait_cold=None, wait=False):
|
||||
"""send api request to dry tell gripper.
|
||||
:param: heat_time float if None Tell will use default for drying time
|
||||
:param: speed float if None Tell will use default for drying speed
|
||||
:param: wait_cold bool if -1 to go to park after dry. if None Tell will use default time to wait_cold.
|
||||
:param wait: bool If true will wait until drying is completed
|
||||
"""
|
||||
self.pshell.wait_state("Ready", timeout=30.0)
|
||||
self.backend.wait_state("Ready", timeout=30.0)
|
||||
self._last_cmd_id = self.start_cmd("dry", heat_time, speed, wait_cold)
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Dry failed")
|
||||
self.check_command_ok(timeout=360.0, msg="Dry failed")
|
||||
|
||||
def move_park(self, wait=False):
|
||||
"""send api request to move robot to park position"""
|
||||
self._last_cmd_id = self.start_cmd("move_park")
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Move to park failed")
|
||||
self.check_command_ok(timeout=360.0, msg="Move to park failed")
|
||||
|
||||
def move_cold(self, reset_timestamp=False, wait=False):
|
||||
"""send api request to move robot to cold position"""
|
||||
self._last_cmd_id = self.start_cmd("move_cold", reset_timestamp)
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Move to cold failed")
|
||||
self.check_command_ok(timeout=360.0, msg="Move to cold failed")
|
||||
|
||||
def abort_cmd(self):
|
||||
"""sends an abort pshell requesst and a robot stop task command"""
|
||||
self.pshell.abort()
|
||||
self.pshell.eval("robot.stop_task()&")
|
||||
self.backend.abort()
|
||||
self.backend.eval("robot.stop_task()&")
|
||||
|
||||
def set_setting(self, key: str, value: str):
|
||||
"""wrapper for pshell eval set_setting command
|
||||
:param key str, name of a setting in tell
|
||||
:param value str, the new value of the setting as a string"""
|
||||
self.pshell.eval(f"set_setting('{key}', '{value}')&")
|
||||
self.backend.eval(f"set_setting('{key}', '{value}')&")
|
||||
|
||||
def get_setting(self, key: str) -> str:
|
||||
"""wrapper for pshell eval get_setting command, returns the current value for key as a string
|
||||
:param key str, name of a setting in tell"""
|
||||
return self.pshell.eval(f"get_setting('{key}')&")
|
||||
return self.backend.eval(f"get_setting('{key}')&")
|
||||
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
"""get the current mounted sample and return a SampleDewarAddress object or None if no sample is mounted"""
|
||||
ret = self.get_setting('mounted_sample_position').strip()
|
||||
if not ret or len(ret) == 0:
|
||||
ret = self.get_setting("mounted_sample_position").strip()
|
||||
if not ret:
|
||||
return None
|
||||
|
||||
match = re.match(r"([A-Z])(\d)(\d{1,2})", ret)
|
||||
|
||||
if match:
|
||||
segment, puck, sample = match.groups()
|
||||
dewar_location = DewarAddress(segment=segment, pos=int(puck))
|
||||
return SampleDewarAddress(puck=dewar_location, pin=int(sample))
|
||||
else:
|
||||
logger.warning(f"Failed to decode mounted sample position: {ret}")
|
||||
return None
|
||||
|
||||
logger.warning(f"Failed to decode mounted sample position: {ret}")
|
||||
return None
|
||||
|
||||
def get_system_check(self):
|
||||
"""returns the current system check status"""
|
||||
return self.pshell.eval("system_check_msg()&")
|
||||
return self.backend.eval("system_check_msg()&")
|
||||
|
||||
def get_robot_state(self):
|
||||
"""returns the current robot state"""
|
||||
return self.pshell.eval("robot.state&")
|
||||
return self.backend.eval("robot.state&")
|
||||
|
||||
def get_robot_status(self):
|
||||
"""returns the current robot status"""
|
||||
status = self.pshell.eval("robot.take()&")
|
||||
return eval(status) # FIXME ALL functions must return a valid JSON object
|
||||
status = self.backend.eval("robot.take()&")
|
||||
#return eval(status)
|
||||
return ast.literal_eval(status)
|
||||
|
||||
def get_detected_pucks(self) -> List[PuckLoadedInfo]:
|
||||
"""returns a list of detected pucks as PuckLoadedInfo objects"""
|
||||
j = json.loads(self.pshell.eval("get_pucks_info()&"))
|
||||
j = json.loads(self.backend.eval("get_pucks_info()&"))
|
||||
|
||||
output = []
|
||||
|
||||
for i in j:
|
||||
if i["puckState"] == "Present":
|
||||
puck_address = i["puckAddress"]
|
||||
@@ -399,90 +298,77 @@ class TellClient:
|
||||
PuckLoadedInfo(
|
||||
puck_name=i["puckBarcode"],
|
||||
location=DewarAddress(
|
||||
segment=puck_address[0], pos=int(puck_address[1])
|
||||
segment=puck_address[0],
|
||||
pos=int(puck_address[1]),
|
||||
),
|
||||
),
|
||||
)
|
||||
return output
|
||||
|
||||
def get_pin_offset(self):
|
||||
"""get the pin offset for the smart magnet, returns offset as a float"""
|
||||
try:
|
||||
offset = float(self.pshell.eval("get_pin_offset()&"))
|
||||
offset = float(self.backend.eval("get_pin_offset()&"))
|
||||
except Exception:
|
||||
offset = 0.0
|
||||
return offset
|
||||
|
||||
def get_current(self):
|
||||
"""get the current drawn by the smart magnet, returns current as a float in mA"""
|
||||
current = self.pshell.eval("smart_magnet.get_current_rb()&")
|
||||
current = self.backend.eval("smart_magnet.get_current_rb()&")
|
||||
return float(current)
|
||||
|
||||
def set_current(self, current: float) -> float:
|
||||
"""set the current drawn by the smart magnet, returns current as a float in mA"""
|
||||
self.pshell.eval("smart_magnet.set_current({:.1f})&".format(current))
|
||||
current = self.pshell.eval("smart_magnet.get_current_rb()&")
|
||||
self.backend.eval("smart_magnet.set_current({:.1f})&".format(current))
|
||||
current = self.backend.eval("smart_magnet.get_current_rb()&")
|
||||
return float(current)
|
||||
|
||||
def is_powered(self):
|
||||
"""returns True if the robot is powered on"""
|
||||
return self.get_robot_status()["powered"]
|
||||
|
||||
def check_enable_motion(self):
|
||||
"""check if the robot is powered on and enable motion if not"""
|
||||
if not self.is_powered():
|
||||
self.pshell.eval("enable_motion()&")
|
||||
self.backend.eval("enable_motion()&")
|
||||
|
||||
def is_in_cold(self):
|
||||
"""Compare current robot position to the set cold position. Returns True if in cold position, False otherwise."""
|
||||
return self.is_position(POSITION_COLD)
|
||||
|
||||
def is_position(self, position: str) -> bool:
|
||||
"""Compare current robot position to a given position. Returns True if in position, False otherwise."""
|
||||
return position == self.get_robot_status()["pos"]
|
||||
|
||||
def is_ready(self):
|
||||
"""returns True if the robot is ready to receive commands"""
|
||||
return "ready" == self.get_state().lower()
|
||||
|
||||
def is_busy(self):
|
||||
"""returns True if the robot is busy"""
|
||||
return "busy" == self.get_state().lower()
|
||||
|
||||
def check_smart_magnet_mounted(self, timeout: float = 10.0, idle_time: float = 1.0, interval: float = 0.1):
|
||||
"""Reads smart_magent state and tries to infer if a sample is present
|
||||
Handles: PAUSED, Fault, Busy and Ready states.
|
||||
Raises a ManualMountException is the amgnet indicates a sample is present but get_mounted_sample is None.
|
||||
Raises a SmartMagnetFaultException if the magnet detects no sample but the robot thinks a sample is mounted"""
|
||||
#TODO tidy up
|
||||
initial_state = self.pshell.eval("smart_magnet.state&")
|
||||
#Not sure why unused, potentially can remove them
|
||||
_ = (timeout, idle_time, interval)
|
||||
|
||||
initial_state = self.backend.eval("smart_magnet.state&")
|
||||
|
||||
logger.debug(f"checking smart magnet_initial state: {initial_state}")
|
||||
|
||||
if initial_state == "Paused":
|
||||
self.pshell.eval("smart_magnet.set_supress(False)&")
|
||||
self.pshell.eval("smart_magnet.set_resting_current()&")
|
||||
|
||||
self.backend.eval("smart_magnet.set_supress(False)&")
|
||||
self.backend.eval("smart_magnet.set_resting_current()&")
|
||||
elif initial_state == "Fault":
|
||||
logger.error(f"tell smart magnet is in unknown state {initial_state}")
|
||||
raise SmartMagnetFaultException
|
||||
|
||||
state = self.pshell.eval("smart_magnet.state&")
|
||||
state = self.backend.eval("smart_magnet.state&")
|
||||
|
||||
try:
|
||||
if state == "Busy":
|
||||
logger.debug('state busy')
|
||||
self.pshell.eval("smart_magnet.set_supress(True)&")
|
||||
self.pshell.eval("smart_magnet.state&")
|
||||
sample_present = True
|
||||
logger.debug("state busy")
|
||||
self.backend.eval("smart_magnet.set_supress(True)&")
|
||||
self.backend.eval("smart_magnet.state&")
|
||||
if self.get_mounted_sample() is None:
|
||||
logger.warning("Check mount: A manually mounted sample is detected.")
|
||||
logger.warning("Remove before mounting with the robot.")
|
||||
raise ManualMountException
|
||||
return True
|
||||
elif state == "Ready":
|
||||
logger.debug('No sample detected, ready to mount')
|
||||
sample_present = False
|
||||
logger.debug("No sample detected, ready to mount")
|
||||
if self.get_mounted_sample():
|
||||
logger.error("Check mount: No sample detected, but robot thinks is mounted")
|
||||
raise SmartMagnetFaultException
|
||||
@@ -491,174 +377,20 @@ class TellClient:
|
||||
logger.debug("Smart magnet detection is paused")
|
||||
return None
|
||||
else:
|
||||
self.pshell.eval("smart_magnet.set_supress(True)&")
|
||||
self.backend.eval("smart_magnet.set_supress(True)&")
|
||||
logger.error(f"Tell smart magnet is in unknown state {state}")
|
||||
raise SmartMagnetFaultException
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"check_smart_magnet_mounted failed: {e}")
|
||||
raise e
|
||||
|
||||
class SimTellClient:
|
||||
"""
|
||||
Simulation-only Tell client.
|
||||
|
||||
Keeps behavior deterministic-ish and stateful without needing PShellClient.
|
||||
Implement more methods as your callers need them.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._state = "Ready"
|
||||
self._last_cmd_id = 1000
|
||||
self._mounted_sample: str = ""
|
||||
self._simulated_samples_info = {}
|
||||
self._simulated_detected_pucks = []
|
||||
self._simulated_current = 30.0
|
||||
self._simulated_suppress = True
|
||||
self._simulated_offset = 0.0
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return None
|
||||
|
||||
def _next_cmd_id(self) -> int:
|
||||
self._last_cmd_id += 1
|
||||
return self._last_cmd_id
|
||||
|
||||
def get_state(self) -> str:
|
||||
return self._state
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
return self._state.lower() == "ready"
|
||||
|
||||
def is_busy(self) -> bool:
|
||||
return self._state.lower() == "busy"
|
||||
|
||||
def wait_ready(self, timeout: float = 360.0):
|
||||
# Keep it simple: flip to Ready quickly.
|
||||
time.sleep(0.05)
|
||||
self._state = "Ready"
|
||||
|
||||
def mount(
|
||||
self,
|
||||
address: SampleDewarAddress,
|
||||
force: bool = False,
|
||||
read_dm: bool = False,
|
||||
auto_unmount: bool = False,
|
||||
wait: bool = False,
|
||||
timeout: float = 600.0,
|
||||
):
|
||||
SampleDewarAddress.model_validate(address)
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
cmd_id = self._next_cmd_id()
|
||||
self._state = "Busy"
|
||||
|
||||
segment = address.puck.segment
|
||||
puck = address.puck.pos
|
||||
sample = address.pin
|
||||
self._mounted_sample = f"{segment}{puck}{sample}"
|
||||
|
||||
if wait:
|
||||
self.wait_ready(timeout=timeout)
|
||||
else:
|
||||
# quickly become ready anyway, but asynchronously-ish
|
||||
time.sleep(0.01)
|
||||
self._state = "Ready"
|
||||
|
||||
return cmd_id
|
||||
|
||||
def unmount(self, force: bool = False, wait: bool = False, timeout: float = 360.0):
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("unmount received while robot is busy")
|
||||
|
||||
cmd_id = self._next_cmd_id()
|
||||
self._state = "Busy"
|
||||
self._mounted_sample = ""
|
||||
if wait:
|
||||
self.wait_ready(timeout=timeout)
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
self._state = "Ready"
|
||||
return cmd_id
|
||||
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
ret = self._mounted_sample
|
||||
if not ret:
|
||||
return None
|
||||
match = re.match(r"([A-Z])(\d)(\d{1,2})", ret)
|
||||
if not match:
|
||||
return None
|
||||
segment, puck, sample = match.groups()
|
||||
return SampleDewarAddress(puck=DewarAddress(segment=segment, pos=int(puck)), pin=int(sample))
|
||||
|
||||
class TellClientProxy:
|
||||
"""
|
||||
Lazy-connecting Tell client proxy that retries periodically.
|
||||
- Server can start even if TELL is down.
|
||||
- First use triggers connect; failures raise TellCommunicationError.
|
||||
"""
|
||||
def __init__(self, bl: MXBeamline, *, retry_interval_s: float = 2.0):
|
||||
self._bl = bl
|
||||
self._client: TellClient | None = None
|
||||
self._retry_interval_s = float(retry_interval_s)
|
||||
self._last_attempt_ts = 0.0
|
||||
self._last_error: Exception | None = None
|
||||
|
||||
def _get_client(self) -> TellClient:
|
||||
if self._client is not None:
|
||||
return self._client
|
||||
|
||||
now = time.monotonic()
|
||||
if now - self._last_attempt_ts < self._retry_interval_s and self._last_error is not None:
|
||||
raise self._last_error
|
||||
|
||||
self._last_attempt_ts = now
|
||||
try:
|
||||
self._client = TellClient(self._bl)
|
||||
self._last_error = None
|
||||
return self._client
|
||||
except TellCommunicationError as e:
|
||||
self._last_error = e
|
||||
raise
|
||||
except Exception as e:
|
||||
wrapped = TellCommunicationError(
|
||||
"TELL connection failed",
|
||||
operation="CONNECT",
|
||||
)
|
||||
self._last_error = wrapped
|
||||
raise wrapped from e
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._get_client().url
|
||||
|
||||
# Delegate methods used by DAQ; add more as needed
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
return self._get_client().get_mounted_sample()
|
||||
|
||||
def get_state(self):
|
||||
return self._get_client().get_state()
|
||||
|
||||
def wait_not_busy(self, timeout: float = 360.0):
|
||||
return self._get_client().wait_not_busy(timeout=timeout)
|
||||
|
||||
def check_enable_motion(self):
|
||||
return self._get_client().check_enable_motion()
|
||||
|
||||
def set_in_mount_position(self, value):
|
||||
return self._get_client().set_in_mount_position(value)
|
||||
|
||||
def mount(self, *args, **kwargs):
|
||||
return self._get_client().mount(*args, **kwargs)
|
||||
|
||||
def unmount(self, *args, **kwargs):
|
||||
return self._get_client().unmount(*args, **kwargs)
|
||||
|
||||
def abort_cmd(self):
|
||||
return self._get_client().abort_cmd()
|
||||
|
||||
def make_tell_client(bl: MXBeamline) -> TellClient | SimTellClient | TellClientProxy:
|
||||
def make_tell_client(bl: MXBeamline) -> TellClient:
|
||||
if bl == MXBeamline.SIMULATED:
|
||||
return SimTellClient()
|
||||
return TellClientProxy(bl, retry_interval_s=2.0)
|
||||
backend = SimTellBackend()
|
||||
else:
|
||||
backend = LazyTellBackend(
|
||||
factory=lambda: PShellTellBackend(bl),
|
||||
retry_interval_s=2.0,
|
||||
)
|
||||
return TellClient(bl, backend=backend)
|
||||
Reference in New Issue
Block a user