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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user