diff --git a/src/aare/common/models.py b/src/aare/common/models.py
index 39d8d979..bfad1976 100644
--- a/src/aare/common/models.py
+++ b/src/aare/common/models.py
@@ -11,6 +11,7 @@ from aare.common.sample_geometry import SampleGeometryModel
from jfjoch_client.models.scan_result import ScanResult
from aare.common.beamline import MXBeamline
+from aare.common.tell_models import TellStateModel
class StagePositionEnum(Enum):
@@ -773,6 +774,7 @@ class DAQStatusModel(BaseModel):
tell_connected: bool = True
tell_error: str | None = None
+ tell_state: TellStateModel | None = None
smargon_connected: bool = True
smargon_error: str | None = None
diff --git a/src/aare/common/tell_models.py b/src/aare/common/tell_models.py
new file mode 100644
index 00000000..e52b98e8
--- /dev/null
+++ b/src/aare/common/tell_models.py
@@ -0,0 +1,62 @@
+from enum import Enum
+
+from pydantic import BaseModel
+
+
+class TellActivityEnum(str, Enum):
+ IDLE = "idle"
+ MOUNTING = "mounting"
+ UNMOUNTING = "unmounting"
+ DRYING = "drying"
+ COOLING = "cooling"
+ ERROR = "error"
+
+ def display_name(self) -> str:
+ return {
+ TellActivityEnum.IDLE: "Idle",
+ TellActivityEnum.MOUNTING: "Mounting",
+ TellActivityEnum.UNMOUNTING: "Unmounting",
+ TellActivityEnum.DRYING: "Drying",
+ TellActivityEnum.COOLING: "Cooling",
+ TellActivityEnum.ERROR: "Error",
+ }.get(self, str(self.value).capitalize())
+
+
+class TellPhaseEnum(str, Enum):
+ IDLE = "idle"
+ PREPARING = "preparing"
+ AUTO_UNMOUNT = "auto_unmount"
+ RETURNING_OLD_SAMPLE = "returning_old_sample"
+ OLD_SAMPLE_RETURNED = "old_sample_returned"
+ PICKING_NEW_SAMPLE = "picking_new_sample"
+ PLACING_NEW_SAMPLE = "placing_new_sample"
+ FINALIZING = "finalizing"
+ COMPLETE = "complete"
+ FAILED = "failed"
+
+ def display_name(self) -> str:
+ return {
+ TellPhaseEnum.IDLE: "Idle",
+ TellPhaseEnum.PREPARING: "Preparing",
+ TellPhaseEnum.AUTO_UNMOUNT: "Auto-unmount",
+ TellPhaseEnum.RETURNING_OLD_SAMPLE: "Returning old sample",
+ TellPhaseEnum.OLD_SAMPLE_RETURNED: "Old sample returned",
+ TellPhaseEnum.PICKING_NEW_SAMPLE: "Picking new sample",
+ TellPhaseEnum.PLACING_NEW_SAMPLE: "Placing sample on gonio",
+ TellPhaseEnum.FINALIZING: "Finalizing",
+ TellPhaseEnum.COMPLETE: "Complete",
+ TellPhaseEnum.FAILED: "Failed",
+ }.get(self, str(self.value).replace("_", " ").capitalize())
+
+
+class TellStateModel(BaseModel):
+ activity: TellActivityEnum = TellActivityEnum.IDLE
+ message: str | None = None
+ last_event_class: str | None = None
+ last_event_value: str | None = None
+ last_update_ts: str | None = None
+ mount_success: bool | None = None
+ mount_error: str | None = None
+ sample_position: str | None = None
+ operation: str | None = None
+ phase: TellPhaseEnum | None = None
\ No newline at end of file
diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py
index 8d4d0885..3a32d70d 100644
--- a/src/aare/daq/daq.py
+++ b/src/aare/daq/daq.py
@@ -12,6 +12,7 @@ from aareDB import SampleEventType
from jfjoch_client.exceptions import NotFoundException
from jfjoch_client import ScanResult, ScanResultImagesInner
+from aare.common.tell_models import TellStateModel
from aare.daq import workflows
from aare.daq.aaredb import AareWrapper
@@ -3215,6 +3216,27 @@ class AareDAQ:
dtz_max=1000,
)
+ def _safe_tell_state(self) -> TellStateModel | None:
+ try:
+ redis_client = getattr(self._AareDAQ__cfg, "_BeamlineConfig__client", None)
+ beamline_key = getattr(self._AareDAQ__cfg, "_BeamlineConfig__bl", None)
+
+ if redis_client is None or beamline_key is None:
+ return None
+
+ redis_key = f"{beamline_key}:tell_state"
+ raw_value = redis_client.get(redis_key)
+ if raw_value in (None, "", b""):
+ return None
+
+ if isinstance(raw_value, bytes):
+ raw_value = raw_value.decode("utf-8")
+
+ return TellStateModel.model_validate_json(str(raw_value))
+ except Exception as e:
+ logger.debug(f"Failed to read tell_state from Redis: {e}")
+ return None
+
def _safe_diffraction_geometry(self) -> DiffractionGeometry:
try:
return self.diffraction_geometry
@@ -3236,7 +3258,7 @@ class AareDAQ:
def status(self) -> DAQStatusModel:
safe_sample, tell_ok, tell_err = self._safe_sample()
safe_geom, smargon_ok, smargon_err, aerotech_ok, aerotech_err = self._safe_geom()
-
+ safe_tell_state = self._safe_tell_state()
return DAQStatusModel(
state=self.state,
@@ -3256,6 +3278,7 @@ class AareDAQ:
crystal_size=self.__cfg.crystal_size,
tell_connected=tell_ok,
tell_error=tell_err,
+ tell_state=safe_tell_state,
smargon_connected=smargon_ok,
smargon_error=smargon_err,
aerotech_connected=aerotech_ok,
diff --git a/src/aare/daq/tell_state_machine.py b/src/aare/daq/tell_state_machine.py
new file mode 100644
index 00000000..8ed298c8
--- /dev/null
+++ b/src/aare/daq/tell_state_machine.py
@@ -0,0 +1,364 @@
+from __future__ import annotations
+
+import re
+from datetime import datetime, timezone
+
+from aare.common.tell_models import TellActivityEnum, TellPhaseEnum, TellStateModel
+
+MOUNT_CMD_RE = re.compile(r'^mount\("([^"]+)",\s*(\d+),\s*(\d+)')
+UNMOUNT_CMD_RE = re.compile(r"^unmount\(")
+UNMOUNT_STATUS_RE = re.compile(r"^unmount:\s*")
+
+
+def _utc_now_iso() -> str:
+ return datetime.now(timezone.utc).isoformat()
+
+
+def initial_tell_state() -> TellStateModel:
+ return TellStateModel(
+ activity=TellActivityEnum.IDLE,
+ message="Ready",
+ operation="idle",
+ phase=TellPhaseEnum.IDLE,
+ last_update_ts=_utc_now_iso(),
+ )
+
+
+def _parse_sample_position_from_mount_command(value: str) -> str | None:
+ match = MOUNT_CMD_RE.match(value)
+ if match is None:
+ return None
+
+ segment, puck, pin = match.groups()
+ return f"{segment}{int(puck)}-{int(pin)}"
+
+
+def _update_state(
+ state: TellStateModel,
+ *,
+ activity: TellActivityEnum | None = None,
+ message: str | None = None,
+ event_name: str | None = None,
+ event_value: str | None = None,
+ operation: str | None = None,
+ phase: TellPhaseEnum | None = None,
+ sample_position: str | None = None,
+ mount_success: bool | None = None,
+ mount_error: str | None = None,
+) -> TellStateModel:
+ updates = {
+ "last_update_ts": _utc_now_iso(),
+ }
+
+ if activity is not None:
+ updates["activity"] = activity
+ if message is not None:
+ updates["message"] = message
+ if event_name is not None:
+ updates["last_event_class"] = event_name
+ if event_value is not None:
+ updates["last_event_value"] = event_value
+ if operation is not None:
+ updates["operation"] = operation
+ if phase is not None:
+ updates["phase"] = phase
+ if sample_position is not None:
+ updates["sample_position"] = sample_position
+ if mount_success is not None:
+ updates["mount_success"] = mount_success
+ if mount_error is not None:
+ updates["mount_error"] = mount_error
+
+ return state.model_copy(update=updates)
+
+
+def _ready_state(state: TellStateModel, event_name: str, event_value: str | None) -> TellStateModel:
+ return _update_state(
+ state,
+ activity=TellActivityEnum.IDLE,
+ message="Ready",
+ event_name=event_name,
+ event_value=event_value,
+ operation="idle",
+ phase=TellPhaseEnum.IDLE,
+ mount_error="",
+ )
+
+
+def advance_tell_state(
+ state: TellStateModel,
+ event_name: str,
+ event_value: str | None,
+) -> TellStateModel:
+ value = (event_value or "").strip()
+
+ if event_name == "Motion Task" and value == "dry":
+ return _update_state(
+ state,
+ activity=TellActivityEnum.DRYING,
+ message="Drying",
+ event_name=event_name,
+ event_value=value,
+ operation="dry",
+ phase=TellPhaseEnum.PREPARING,
+ mount_success=None,
+ mount_error="",
+ )
+
+ if event_name == "Motion Sync" and value == "Gripper cooling down":
+ return _update_state(
+ state,
+ activity=TellActivityEnum.COOLING,
+ message="Cooling after drying",
+ event_name=event_name,
+ event_value=value,
+ operation="dry",
+ phase=TellPhaseEnum.FINALIZING,
+ )
+
+ if event_name == "shell":
+ if value == "true":
+ return _update_state(
+ state,
+ event_name=event_name,
+ event_value=value,
+ )
+
+ if value.startswith("mount("):
+ sample_position = _parse_sample_position_from_mount_command(value)
+ return _update_state(
+ state,
+ activity=TellActivityEnum.MOUNTING,
+ message=f"Mounting {sample_position}" if sample_position else "Mounting sample",
+ event_name=event_name,
+ event_value=value,
+ operation="mount",
+ phase=TellPhaseEnum.PREPARING,
+ sample_position=sample_position or "",
+ mount_success=None,
+ mount_error="",
+ )
+
+ if UNMOUNT_CMD_RE.match(value):
+ return _update_state(
+ state,
+ activity=TellActivityEnum.UNMOUNTING,
+ message="Unmounting sample",
+ event_name=event_name,
+ event_value=value,
+ operation="unmount",
+ phase=TellPhaseEnum.PREPARING,
+ mount_success=None,
+ mount_error="",
+ )
+
+ if UNMOUNT_STATUS_RE.match(value):
+ next_operation = "mount" if state.operation == "mount" else "unmount"
+ next_phase = TellPhaseEnum.AUTO_UNMOUNT if state.operation == "mount" else TellPhaseEnum.PREPARING
+
+ return _update_state(
+ state,
+ activity=TellActivityEnum.UNMOUNTING,
+ message="Unmounting current sample",
+ event_name=event_name,
+ event_value=value,
+ operation=next_operation,
+ phase=next_phase,
+ mount_success=None,
+ mount_error="",
+ )
+
+ if value == "got result True":
+ if state.operation == "mount" and state.phase in {
+ TellPhaseEnum.PREPARING,
+ TellPhaseEnum.AUTO_UNMOUNT,
+ TellPhaseEnum.RETURNING_OLD_SAMPLE,
+ TellPhaseEnum.OLD_SAMPLE_RETURNED,
+ TellPhaseEnum.PICKING_NEW_SAMPLE,
+ TellPhaseEnum.PLACING_NEW_SAMPLE,
+ TellPhaseEnum.FINALIZING,
+ }:
+ return _update_state(
+ state,
+ message=state.message or "Mount in progress",
+ event_name=event_name,
+ event_value=value,
+ )
+
+ if state.operation == "unmount":
+ return _update_state(
+ state,
+ message="Unmount step completed",
+ event_name=event_name,
+ event_value=value,
+ )
+
+ return _update_state(
+ state,
+ event_name=event_name,
+ event_value=value,
+ )
+
+ if value == "got result False":
+ if state.operation == "mount" and state.phase in {
+ TellPhaseEnum.AUTO_UNMOUNT,
+ TellPhaseEnum.RETURNING_OLD_SAMPLE,
+ TellPhaseEnum.OLD_SAMPLE_RETURNED,
+ }:
+ return _update_state(
+ state,
+ activity=TellActivityEnum.UNMOUNTING,
+ message="Unmounting current sample",
+ event_name=event_name,
+ event_value=value,
+ )
+
+ if state.operation == "unmount" and state.phase in {
+ TellPhaseEnum.RETURNING_OLD_SAMPLE,
+ TellPhaseEnum.COMPLETE,
+ }:
+ return _update_state(
+ state,
+ activity=TellActivityEnum.UNMOUNTING,
+ message=state.message or "Unmounting sample",
+ event_name=event_name,
+ event_value=value,
+ )
+
+ operation = state.operation or "robot operation"
+ return _update_state(
+ state,
+ activity=TellActivityEnum.ERROR,
+ message=f"{operation.capitalize()} failed",
+ event_name=event_name,
+ event_value=value,
+ mount_success=False,
+ mount_error=f"{operation.capitalize()} failed",
+ phase=TellPhaseEnum.FAILED,
+ )
+
+ if value.startswith("setting mounted sample to"):
+ sample_position = value.removeprefix("setting mounted sample to").strip()
+ return _update_state(
+ state,
+ activity=TellActivityEnum.MOUNTING,
+ message=f"Mounted {sample_position}",
+ event_name=event_name,
+ event_value=value,
+ operation="mount",
+ phase=TellPhaseEnum.COMPLETE,
+ sample_position=sample_position,
+ mount_success=True,
+ mount_error="",
+ )
+
+ return _update_state(
+ state,
+ event_name=event_name,
+ event_value=value,
+ )
+
+ if event_name == "Motion Sync":
+ if value == "Sample get from Gonio":
+ next_phase = (
+ TellPhaseEnum.RETURNING_OLD_SAMPLE
+ if state.operation == "mount"
+ else TellPhaseEnum.RETURNING_OLD_SAMPLE
+ )
+ next_operation = "mount" if state.operation == "mount" else "unmount"
+ return _update_state(
+ state,
+ activity=TellActivityEnum.UNMOUNTING,
+ message="Removing sample from gonio",
+ event_name=event_name,
+ event_value=value,
+ operation=next_operation,
+ phase=next_phase,
+ )
+
+ if value == "Sample put on Puck":
+ next_phase = (
+ TellPhaseEnum.OLD_SAMPLE_RETURNED
+ if state.operation == "mount"
+ else TellPhaseEnum.COMPLETE
+ )
+ next_operation = "mount" if state.operation == "mount" else "unmount"
+ next_message = "Returning sample to puck" if state.operation == "mount" else "Sample returned to puck"
+ return _update_state(
+ state,
+ activity=TellActivityEnum.UNMOUNTING,
+ message=next_message,
+ event_name=event_name,
+ event_value=value,
+ operation=next_operation,
+ phase=next_phase,
+ mount_success=True if state.operation == "unmount" else state.mount_success,
+ mount_error="" if state.operation == "unmount" else state.mount_error,
+ )
+
+ if value == "Sample get on Puck":
+ return _update_state(
+ state,
+ activity=TellActivityEnum.MOUNTING,
+ message="Picking sample from puck",
+ event_name=event_name,
+ event_value=value,
+ operation="mount",
+ phase=TellPhaseEnum.PICKING_NEW_SAMPLE,
+ mount_error="",
+ )
+
+ if value == "Sample put on Gonio":
+ return _update_state(
+ state,
+ activity=TellActivityEnum.MOUNTING,
+ message="Placing sample on gonio",
+ event_name=event_name,
+ event_value=value,
+ operation="mount",
+ phase=TellPhaseEnum.PLACING_NEW_SAMPLE,
+ mount_error="",
+ )
+
+ if value == "Robot Clear after mount":
+ return _update_state(
+ state,
+ activity=TellActivityEnum.MOUNTING,
+ message="Robot clear after mount",
+ event_name=event_name,
+ event_value=value,
+ operation="mount",
+ phase=TellPhaseEnum.FINALIZING,
+ mount_error="",
+ )
+
+ if event_name == "Gripper detection":
+ if value in {"No Pin in Gripper", "Pin still in Gripper", "Pin is lost"}:
+ return _update_state(
+ state,
+ activity=TellActivityEnum.ERROR,
+ message=value,
+ event_name=event_name,
+ event_value=value,
+ mount_success=False,
+ mount_error=value,
+ phase=TellPhaseEnum.FAILED,
+ )
+
+ if event_name == "state":
+ if value == "Ready":
+ return _ready_state(state, event_name, value)
+
+ if value == "Busy":
+ return _update_state(
+ state,
+ message=state.message if state.operation not in {None, "idle"} else "Busy",
+ event_name=event_name,
+ event_value=value,
+ )
+
+ return _update_state(
+ state,
+ event_name=event_name,
+ event_value=value,
+ )
\ No newline at end of file
diff --git a/src/aare/daq/tellupdater.py b/src/aare/daq/tellupdater.py
index e1ab97c7..aaa39897 100644
--- a/src/aare/daq/tellupdater.py
+++ b/src/aare/daq/tellupdater.py
@@ -19,8 +19,10 @@ from aare.devices.tell_client import TellClient
from aare.common.exception_handler import TellCommunicationError, TellConnectionException
from aare.common.logger_config import setup_logger
+from aare.common.tell_models import TellStateModel
from aare.daq.aaredb import AareWrapper
from aare.daq.config import BeamlineConfig
+from aare.daq.tell_state_machine import advance_tell_state, initial_tell_state
from aare.common.beamline import MXBeamline, mx_beamline
logger = setup_logger("aareDAQ")
@@ -62,8 +64,10 @@ TRACKED_MOTION_SYNC_EVENTS = {
latest_tell_events = {}
tell_event_history = deque(maxlen=25)
+current_tell_state = initial_tell_state()
TELL_JOURNAL_PREFIX = "[TELL][JOURNAL]"
TELL_EVENTS_REDIS_KEY_SUFFIX = "tell_events"
+TELL_STATE_REDIS_KEY_SUFFIX = "tell_state"
SSE_RECONNECT_DELAY_S = 5
SSE_RECOVERABLE_EXCEPTIONS = (
RequestException,
@@ -114,6 +118,30 @@ class TellEventRecord(BaseModel):
}
+def _get_redis_context() -> tuple[Any | None, str | None]:
+ if config is None:
+ logger.debug("[REDIS] BeamlineConfig unavailable; skipping TELL redis write")
+ return None, None
+
+ redis_client = getattr(config, "_BeamlineConfig__client", None)
+ beamline_key = getattr(config, "_BeamlineConfig__bl", None)
+ if redis_client is None or beamline_key is None:
+ logger.error("[REDIS] BeamlineConfig internals unavailable; skipping TELL redis write")
+ return None, None
+
+ return cast(Any, redis_client), str(beamline_key)
+
+
+def _set_json_in_redis(key_suffix: str, payload: Any, *, log_label: str) -> None:
+ redis_client, beamline_key = _get_redis_context()
+ if redis_client is None or beamline_key is None:
+ return
+
+ redis_key = f"{beamline_key}:{key_suffix}"
+ redis_client.set(redis_key, json.dumps(payload))
+ logger.info(f"[REDIS] Written {log_label} to: {redis_key}")
+
+
def _normalize_event_data(data):
if data is None:
return None
@@ -128,7 +156,11 @@ def _normalize_event_data(data):
try:
payload = TellSsePayload.model_validate_json(stripped)
except (ValidationError, json.JSONDecodeError, ValueError, TypeError):
- return stripped
+ try:
+ decoded = json.loads(stripped)
+ except (json.JSONDecodeError, ValueError, TypeError):
+ return stripped
+ return str(decoded).strip() if decoded is not None else None
return payload.normalized_value
@@ -158,19 +190,19 @@ def extract_tracked_tell_event(event_name, event_data):
def set_tell_events_in_redis(events: list[dict[str, str]]) -> None:
- if config is None:
- logger.debug("[REDIS] BeamlineConfig unavailable; skipping TELL event write")
- return
+ _set_json_in_redis(
+ TELL_EVENTS_REDIS_KEY_SUFFIX,
+ events,
+ log_label="TELL events",
+ )
- redis_client = getattr(config, "_BeamlineConfig__client", None)
- beamline_key = getattr(config, "_BeamlineConfig__bl", None)
- if redis_client is None or beamline_key is None:
- logger.error("[REDIS] BeamlineConfig internals unavailable; skipping TELL event write")
- return
- redis_key = f"{beamline_key}:{TELL_EVENTS_REDIS_KEY_SUFFIX}"
- cast(Any, redis_client).set(redis_key, json.dumps(events))
- logger.info(f"[REDIS] Written TELL events to: {redis_key}")
+def set_tell_state_in_redis(state: TellStateModel) -> None:
+ _set_json_in_redis(
+ TELL_STATE_REDIS_KEY_SUFFIX,
+ state.model_dump(),
+ log_label="TELL state",
+ )
def record_tell_event(event_name, event_value):
@@ -265,8 +297,19 @@ def handle_tell_change_event():
logger.exception("[SSE] Failed to update puck state after TELL change")
def on_sse_event(event):
+ global current_tell_state
+
logger.debug(f"[EVENT] SSE event={event.event} data={event.data!r}")
- tracked_event = extract_tracked_tell_event(event.event, event.data)
+ normalized_data = _normalize_event_data(event.data)
+
+ current_tell_state = advance_tell_state(
+ current_tell_state,
+ event.event,
+ normalized_data,
+ )
+ set_tell_state_in_redis(current_tell_state)
+
+ tracked_event = extract_tracked_tell_event(event.event, normalized_data)
if tracked_event is not None:
record_tell_event(*tracked_event)
if event.event == DEWAR_CONTENT_UPDATE_EVENT:
@@ -382,4 +425,6 @@ if __name__ == "__main__":
WS_HEADERS = [f"X-Shared-Password: {os.getenv('AAREDB_SHARED_PASSWORD')}"]
logger.debug(f"[WS] Headers configured: {WS_HEADERS}")
+ set_tell_state_in_redis(current_tell_state)
+
main()
diff --git a/src/aare/devices/tell_client.py b/src/aare/devices/tell_client.py
index 3ca25354..98fbecf0 100755
--- a/src/aare/devices/tell_client.py
+++ b/src/aare/devices/tell_client.py
@@ -263,6 +263,7 @@ class TellClient:
return self._last_cmd_id
def dry(self, heat_time=None, speed=None, wait_cold=None, wait=False):
+ #TODO add timeout variable????
self.backend.wait_state("Ready", timeout=30.0)
self._last_cmd_id = self.start_cmd("dry", heat_time, speed, wait_cold)
if wait:
diff --git a/src/aare/gui/panels/tell_sample_panel.py b/src/aare/gui/panels/tell_sample_panel.py
index fce011fd..253c8e3a 100644
--- a/src/aare/gui/panels/tell_sample_panel.py
+++ b/src/aare/gui/panels/tell_sample_panel.py
@@ -41,6 +41,8 @@ class TellSamplePanel(QFrame):
grid_layout.addWidget(self.table_view, 1, 0, 1, 4)
self.curr_sample_label = QLabel("No sample mounted", parent=self)
+ self.curr_sample_label.setTextFormat(Qt.TextFormat.RichText)
+ self.curr_sample_label.setWordWrap(True)
grid_layout.addWidget(self.curr_sample_label, 2, 0)
self.unmount_button = QPushButton("Unmount", parent=self)
@@ -182,24 +184,44 @@ class TellSamplePanel(QFrame):
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
sample = status.sample
+ tell_state = status.tell_state
+
+ tell_details = ""
+ if tell_state is not None:
+ activity = tell_state.activity.display_name()
+ phase = tell_state.phase.display_name() if tell_state.phase is not None else ""
+ message = (tell_state.message or "").strip()
+
+ tell_parts = [activity]
+ if phase:
+ tell_parts.append(phase)
+
+ tell_details = " / ".join(tell_parts)
+ if message:
+ tell_details = f"{tell_details} — {message}"
+
if sample is None:
- self.curr_sample_label.setText("No sample mounted")
+ base_text = "No sample mounted"
self.table_model.updateCurrentSample(current_puck=None, current_sample=None)
else:
try:
if sample.location is None:
- self.curr_sample_label.setText(
- f"Current sample: {sample.sample_name} (Manual mount)"
- )
+ base_text = f"Current sample: {sample.sample_name} (Manual mount)"
else:
- self.curr_sample_label.setText(
- f"Current sample: {sample.sample_name} ({sample.location.segment}{sample.location.pos}-{sample.pin})"
+ base_text = (
+ f"Current sample: {sample.sample_name} "
+ f"({sample.location.segment}{sample.location.pos}-{sample.pin})"
)
self.table_model.updateCurrentSample(
current_puck=sample.puck_name, current_sample=sample.db_id
)
except Exception as e:
- self.curr_sample_label.setText(f"Confusing information :/ {e}")
+ base_text = f"Confusing information :/ {e}"
+
+ if tell_details:
+ self.curr_sample_label.setText(f"{base_text}
TELL: {tell_details}")
+ else:
+ self.curr_sample_label.setText(base_text)
if status.session.current_pgroup is not None:
self.__current_pgroup = status.session.current_pgroup
diff --git a/tests/unit/daq/test_tell_state_updater.py b/tests/unit/daq/test_tell_state_updater.py
new file mode 100644
index 00000000..ed56402b
--- /dev/null
+++ b/tests/unit/daq/test_tell_state_updater.py
@@ -0,0 +1,135 @@
+from aare.common.tell_models import TellActivityEnum, TellPhaseEnum
+from aare.daq.tell_state_machine import advance_tell_state, initial_tell_state
+
+
+def test_mount_with_auto_unmount_uses_internal_phases():
+ state = initial_tell_state()
+
+ state = advance_tell_state(
+ state,
+ "shell",
+ 'mount("F", 2, 1, force=1, read_dm=0, auto_unmount=1)',
+ )
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.PREPARING
+ assert state.sample_position == "F2-1"
+ assert state.mount_success is None
+
+ state = advance_tell_state(state, "shell", "got result True")
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.PREPARING
+ assert state.mount_success is None
+
+ state = advance_tell_state(state, "shell", "unmount: None None None True")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.AUTO_UNMOUNT
+
+ state = advance_tell_state(state, "Motion Sync", "Sample get from Gonio")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.RETURNING_OLD_SAMPLE
+
+ state = advance_tell_state(state, "Motion Sync", "Sample put on Puck")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.OLD_SAMPLE_RETURNED
+
+ state = advance_tell_state(state, "Motion Sync", "Sample get on Puck")
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.PICKING_NEW_SAMPLE
+
+ state = advance_tell_state(state, "Motion Sync", "Sample put on Gonio")
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.PLACING_NEW_SAMPLE
+
+ state = advance_tell_state(state, "Motion Sync", "Robot Clear after mount")
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.FINALIZING
+
+ state = advance_tell_state(state, "shell", "setting mounted sample to F2-1")
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.COMPLETE
+ assert state.sample_position == "F2-1"
+ assert state.mount_success is True
+
+ state = advance_tell_state(state, "state", "Ready")
+ assert state.activity == TellActivityEnum.IDLE
+ assert state.operation == "idle"
+ assert state.phase == TellPhaseEnum.IDLE
+ assert state.message == "Ready"
+
+
+def test_plain_unmount_reaches_complete_before_ready():
+ state = initial_tell_state()
+
+ state = advance_tell_state(state, "shell", "unmount()")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.operation == "unmount"
+ assert state.phase == TellPhaseEnum.PREPARING
+
+ state = advance_tell_state(state, "Motion Sync", "Sample get from Gonio")
+ assert state.phase == TellPhaseEnum.RETURNING_OLD_SAMPLE
+
+ state = advance_tell_state(state, "Motion Sync", "Sample put on Puck")
+ assert state.phase == TellPhaseEnum.COMPLETE
+ assert state.mount_success is True
+
+ state = advance_tell_state(state, "state", "Ready")
+ assert state.activity == TellActivityEnum.IDLE
+ assert state.operation == "idle"
+ assert state.phase == TellPhaseEnum.IDLE
+
+
+def test_gripper_detection_moves_to_failed_phase():
+ state = initial_tell_state()
+ state = advance_tell_state(state, "shell", 'mount("F", 2, 1)')
+ state = advance_tell_state(state, "Gripper detection", "Pin is lost")
+
+ assert state.activity == TellActivityEnum.ERROR
+ assert state.phase == TellPhaseEnum.FAILED
+ assert state.mount_success is False
+ assert state.mount_error == "Pin is lost"
+
+
+def test_got_result_false_during_auto_unmount_is_not_error():
+ state = initial_tell_state()
+
+ state = advance_tell_state(
+ state,
+ "shell",
+ 'mount("D", 5, 14, force=1, read_dm=0, auto_unmount=1)',
+ )
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.PREPARING
+
+ state = advance_tell_state(state, "shell", "unmount: None None None True")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.AUTO_UNMOUNT
+
+ state = advance_tell_state(state, "Motion Sync", "Sample get from Gonio")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.phase == TellPhaseEnum.RETURNING_OLD_SAMPLE
+
+ state = advance_tell_state(state, "shell", "got result False")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.operation == "mount"
+ assert state.phase == TellPhaseEnum.RETURNING_OLD_SAMPLE
+ assert state.mount_success is None
+ assert state.mount_error in (None, "")
+
+ state = advance_tell_state(state, "Motion Sync", "Sample put on Puck")
+ assert state.activity == TellActivityEnum.UNMOUNTING
+ assert state.phase == TellPhaseEnum.OLD_SAMPLE_RETURNED
+
+ state = advance_tell_state(state, "Motion Sync", "Sample get on Puck")
+ assert state.activity == TellActivityEnum.MOUNTING
+ assert state.phase == TellPhaseEnum.PICKING_NEW_SAMPLE
\ No newline at end of file