DAQ: added tell_state handler that creates states based on sse stream from tell in redis. Currently not used in logc, tracking for debugging before implementing.

This commit is contained in:
2026-05-04 17:01:07 +02:00
parent e99681ddd8
commit a82a083c67
8 changed files with 675 additions and 21 deletions
+2
View File
@@ -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
+62
View File
@@ -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
+24 -1
View File
@@ -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,
+364
View File
@@ -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,
)
+58 -13
View File
@@ -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()
+1
View File
@@ -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:
+29 -7
View File
@@ -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: <b>{sample.sample_name} (Manual mount)</b>"
)
base_text = f"Current sample: <b>{sample.sample_name} (Manual mount)</b>"
else:
self.curr_sample_label.setText(
f"Current sample: <b>{sample.sample_name} ({sample.location.segment}{sample.location.pos}-{sample.pin})</b>"
base_text = (
f"Current sample: <b>{sample.sample_name} "
f"({sample.location.segment}{sample.location.pos}-{sample.pin})</b>"
)
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}<br/><span style='color:#555;'>TELL: {tell_details}</span>")
else:
self.curr_sample_label.setText(base_text)
if status.session.current_pgroup is not None:
self.__current_pgroup = status.session.current_pgroup
+135
View File
@@ -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