DAQ: MAJOR: Refactored mounting service

This commit is contained in:
appleb_m committed 2026-05-20 16:11:44 +02:00
1 parent ff609bc68d
commit 0b03c984bd
4 files changed
+282 -221

No files matched your search

+65 -221
View File
@@ -56,6 +56,7 @@ from aare.common.sample_geometry import SampleGeometryModel
from aare.daq.operations.face_detection import FaceDetectionContext, FaceDetectionService, FaceDetectionResult
from aare.daq.operations.loop_centering import LoopCenteringService, LoopCenteringContext
from aare.daq.operations.loop_centering.models import LoopCenteringSettings
from aare.daq.operations.mounting import MountingContext, MountingService
from aare.devices.area_detector import AutoEnum
from aare.devices.jfjoch import JFJochWrapper
@@ -123,7 +124,7 @@ class AareDAQ:
self._automation_last_sample_name = ""
self._automation_samples_in_queue = 0
self._last_mount_error_message = ""
self.__reset_mount_failure_counter("DAQ startup")
self._create_mounting_service().reset_mount_failure_counter("DAQ startup")
def _is_hardware_failure(self, error: Exception) -> bool:
return isinstance(
@@ -567,52 +568,52 @@ class AareDAQ:
),
logger=logger,
)
#
# def _create_mounting_service(self):
# return MountingService(
# context=MountingContext(
# cfg=self.__cfg,
# devs=self.__devs,
# mount_position=ABR_POS_MOUNT,
# ),
# logger=logger,
# )
#
# def _create_raster_service(self):
# return RasterService(
# context=RasterContext(
# cfg=self.__cfg,
# devs=self.__devs,
# mlbox=self.__mlbox,
# jfjoch=self.__jfjoch,
# aare=self.__aare,
# sample_provider=lambda: self.sample,
# sample_geometry_provider=lambda: self.sample_geometry,
# status_provider=lambda: self.status,
# set_state=self.__set_state,
# save_screenshot_db=self.save_screenshot_db,
# upload_raster_diffraction_preview=self._upload_raster_diffraction_preview,
# auto_center_line_scan_top_left=self._auto_center_line_scan_top_left,
# ml_bounding_box=self.__ml_bounding_box,
# ),
# logger=logger,
# )
#
# def _create_rotation_service(self):
# return RotationService(
# context=RotationContext(
# cfg=self.__cfg,
# devs=self.__devs,
# jfjoch=self.__jfjoch,
# aare=self.__aare,
# sample_provider=lambda: self.sample,
# sample_geometry_provider=lambda: self.sample_geometry,
# status_provider=lambda: self.status,
# set_state=self.__set_state,
# save_screenshot_db=self.save_screenshot_db,
# ),
# logger=logger,
# )
def _create_mounting_service(self):
return MountingService(
context=MountingContext(
cfg=self.__cfg,
devs=self.__devs,
mount_position=ABR_POS_MOUNT,
),
logger=logger,
)
def _create_raster_service(self):
return RasterService(
context=RasterContext(
cfg=self.__cfg,
devs=self.__devs,
mlbox=self.__mlbox,
jfjoch=self.__jfjoch,
aare=self.__aare,
sample_provider=lambda: self.sample,
sample_geometry_provider=lambda: self.sample_geometry,
status_provider=lambda: self.status,
set_state=self.__set_state,
save_screenshot_db=self.save_screenshot_db,
upload_raster_diffraction_preview=self._upload_raster_diffraction_preview,
auto_center_line_scan_top_left=self._auto_center_line_scan_top_left,
ml_bounding_box=self.__ml_bounding_box,
),
logger=logger,
)
def _create_rotation_service(self):
return RotationService(
context=RotationContext(
cfg=self.__cfg,
devs=self.__devs,
jfjoch=self.__jfjoch,
aare=self.__aare,
sample_provider=lambda: self.sample,
sample_geometry_provider=lambda: self.sample_geometry,
status_provider=lambda: self.status,
set_state=self.__set_state,
save_screenshot_db=self.save_screenshot_db,
),
logger=logger,
)
#--------------------------------------------
@@ -703,18 +704,29 @@ class AareDAQ:
if previous_sample is not None and previous_sample.db_id is not None:
self.__aare.send_sample_event(previous_sample, SampleEventType.UNMOUNTING)
self.__set_state(BeamlineStateEnum.RobotSampleExchange)
if sample is not None and sample.db_id is not None:
self.__aare.send_sample_event(sample, SampleEventType.MOUNTING)
self.__mount(sample)
mounting_result = self._create_mounting_service().execute(target=sample)
if not mounting_result.success:
raise mounting_result.error or MountingFailed(
mounting_result.comment or "Mount failed"
)
if previous_sample is not None and previous_sample.db_id is not None:
self.__aare.send_sample_event(previous_sample, SampleEventType.UNMOUNTED)
self.__set_state(BeamlineStateEnum.SampleAlignment)
if sample is not None and sample.db_id is not None:
self.__aare.send_sample_event(sample, SampleEventType.MOUNTED)
self.save_screenshot_db(sample.db_id, f"{sample.db_id}_mounted")
self.__set_state(BeamlineStateEnum.SampleAlignment)
return True
except TransformationInvalidException as e:
self._last_mount_error_message = str(e) or "Mount failed"
logger.error(f"Mount failed due to invalid transformation: {e}")
@@ -725,14 +737,15 @@ class AareDAQ:
event_type=SampleEventType.MOUNTFAILED,
)
raise
except Exception as e:
self._last_mount_error_message = str(e) or "Mount failed"
logger.error(f"Mount failed: {e}")
previous_sample_unmounted = (
previous_sample is not None
and previous_sample.db_id is not None
and self._was_previous_sample_unmounted_since(mount_started_at)
previous_sample is not None
and previous_sample.db_id is not None
and self._was_previous_sample_unmounted_since(mount_started_at)
)
if previous_sample_unmounted:
@@ -1428,20 +1441,7 @@ class AareDAQ:
raise
def _execute_dry(self, park=True):
self.__devs.tell.check_enable_motion()
self.__devs.tell.wait_not_busy()
self.__devs.tell.set_in_mount_position(True)
if self.sample:
self.__devs.tell.unmount(wait=True, timeout=60.0)
self.__cfg.current_sample = None
if park:
"""set wait_cold to -1 to go to park after drying"""
self.__devs.tell.dry(wait_cold=-1, wait=True)
else:
"""use default tell wait cold after drying"""
self.__devs.tell.dry(wait=True)
self._create_mounting_service().dry(park=park)
def park_and_dry(self, park = True):
self.__cfg.try_set_busy(timeout=360)
@@ -1452,28 +1452,6 @@ class AareDAQ:
self.__cfg.state_busy = False
logger.error(f"Failed to park and dry: {e}")
raise
def __reset_mount_failure_counter(self, reason: str) -> None:
previous_count = self.__cfg.get_mount_fail_count()
if previous_count > 0:
logger.warning(
f"Resetting mount failure counter from {previous_count} due to: {reason}"
)
else:
logger.debug(f"Mount failure counter already clear: {reason}")
self.__cfg.record_mount_success()
def tell_dry(self):
self.__cfg.try_set_busy(timeout=360)
try:
self.__devs.tell.check_enable_motion()
self.__devs.tell.wait_not_busy()
self.__devs.tell.dry(wait=True)
self.__cfg.state_busy = False
except Exception as e:
self.__cfg.state_busy = False
logger.error(f"Failed to dry TELL: {e}")
raise
def tell_toggle_blower(self):
try:
@@ -1501,140 +1479,6 @@ class AareDAQ:
logger.error(f"Failed to initialise detector: {e}")
raise
def __magnet_position_sensor_check(self, timeout: float = 1.0, repeat: bool = True):
#TODO check this works, add beamstop z controls and test.
# logger.info("checking beamstop")
# if self.__devs.beamstop_z.value < 24.0:
# raise Exception("Beamstop Z below 24.0 mm - potentially unsafe with mounting")
if self.__devs.magnet_position_sensor.value != 0:
logger.warning("Goniometer is not in position based on magnet position sensor readout")
for i in range(round(timeout * 10.0)):
if self.__devs.magnet_position_sensor.value == 0:
return
time.sleep(0.1)
logger.error("Goniometer didn't reach position based on magnet position sensor readout")
raise Exception("Goniometer is not in position based on magnet position sensor readout")
def __mount_fail_handler(
self,
mount_error: bool = False,
dry_after_fail_count: int = 3,
end_automation_after_fail_count: int = 5
):
# if mount_error:
# logger.error("Mount failed")
# logger.debug("Mount error: ", extra={"mount_error": mount_error})
# logger.debug("Currently fail handler disabled due to bugs..?")
if mount_error:
count = self.__cfg.record_mount_failure()
if count > end_automation_after_fail_count:
logger.error(f"Mounting continued to fail after stopping automation, contact your local contact")
logger.debug(f"Clearing mount fail count")
self.__cfg.record_mount_success()
raise CriticalTellException(f"Mounting continued to fail after stopping automation, contact your local contact.")
elif count == end_automation_after_fail_count:
logger.error(f"Mounting failed {count} times, stopping automation")
try:
self._execute_dry(park=True)
except Exception as e:
logger.exception(f"Failed to dry after mount failure: {e}")
raise CriticalTellException(f"Mount failed {count} times in a row, stopping automation.")
elif count == dry_after_fail_count:
#Dry after several fails
logger.warning(f"Mount failed {count} times in a row; drying")
try:
self._execute_dry(park=False)
except Exception as e:
logger.exception(f"Failed to dry after mount failure: {e}")
else:
return
else:
logger.debug("Mounting succeeded, resetting mount failure counter")
self.__cfg.record_mount_success()
def __mount_handler(self, target : SampleShortInfo):
value = self.__devs.tell.mount(address=target.tell_address(), force=True, auto_unmount=True, read_dm=False,
wait=True, timeout=360.0)
logger.debug(f"Mount response: {value.value}")
if isinstance(value, str):
logger.error(f"Unexpected string response from Tell mount: {value}")
raise CriticalTellException(f"Critical error in TELL mount: unexpected response '{value}'")
if value is None:
logger.error("Tell mount returned no response (None)")
raise CriticalTellException("Critical error in TELL mount: no response")
safe_val = getattr(value, "value", str(value))
logger.debug(f"Mount response: {safe_val}")
if value == TellEventValueEnum.NO_PIN_IN_GRIPPER:
"""No Pin in gripper is classed as a non critical mount fail
Unless it happens multiple times in a row, in which case the mount fail handler should dry the gripper
and if it continues stop automation as either there is several missing samples, they are stuck in the dewar
or a problem with the gripper
"""
raise MountingFailed("No Pin in Gripper")
elif value == TellEventValueEnum.PIN_IS_LOST_GRIPPER:
"""Pin is lost gripper is classed as a non critical mount fail
Unless it happens multiple times in a row, in which case the mount fail handler should dry the gripper
asit may be a problem with the gripper or the type of pins. If it continues after this automation should stop.
"""
logger.error("Pin is lost")
raise MountingFailed("Pin is lost")
elif value == TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT or value == TellEventValueEnum.DRY or value == TellEventValueEnum.COLD:
"""Robot clear after mount, tell dry or tell cold are not fails, they are events telling the daq to allow
automation to continue as the robot is busy but will not crash if the end station state is changed
this allows us to save a lot of time in autoamtion"""
logger.info(f"Robot is {getattr(value, 'value', str(value))} - freeing beamline for user")
else:
"""Currently there are no other fail messages from Tell"""
# Treat any unknown/unsupported event/value as critical for now
msg = f"Mount response not handled: {getattr(value, 'value', str(value))}"
logger.error(msg)
raise CriticalTellException(f"Critical error in TELL mount: {msg}")
return
def __mount(self, target: SampleShortInfo | None):
self.__devs.smargon_move_home()
self.__devs.aerotech_pos = ABR_POS_MOUNT
#collimator should be down!!!
self.__magnet_position_sensor_check(timeout=360.0)
self.__devs.tell.check_enable_motion()
self.__devs.tell.wait_not_busy()
self.__devs.tell.set_in_mount_position(True)
previous_sample = self.__cfg.current_sample
if target is None:
if previous_sample is not None:
logger.debug(f"Unmounting sample: {previous_sample}")
try:
self.__devs.tell.unmount(wait=True, timeout=60.0)
self.__cfg.current_sample = target
except Exception as e:
raise UnmountingFailed(f"Failed to unmount: {e}")
else:
try:
#TODO: how to differentiate between mounting and unmounting fails
# during a typucal mount call as this is handled by Tell?
log_msg = f"Mounting sample: {target}"
if previous_sample is not None:
log_msg = (f"Unmounting sample: {previous_sample} \n "
f"and {log_msg}")
logger.debug(log_msg)
self.__mount_handler(target)
self.__mount_fail_handler(mount_error=False)
self.__cfg.current_sample = target
except CriticalTellException as e:
logger.error(f"Critical Tell error: {e}")
raise
except Exception as e:
logger.error(f"Mount failed: {e}")
self.__mount_fail_handler(mount_error=True)
raise
def recovery_unmount_sample(self) -> None:
self.__cfg.try_set_busy(timeout=360)
try:
@@ -0,0 +1,8 @@
from aare.daq.operations.mounting.models import MountingContext, MountingResult
from aare.daq.operations.mounting.service import MountingService
__all__ = [
"MountingContext",
"MountingResult",
"MountingService",
]
@@ -0,0 +1,26 @@
from dataclasses import dataclass
from aare.common.coordinate import AerotechCoordinate
from aare.daq.config import BeamlineConfig
from aare.daq.devices import BeamlineDevices
@dataclass
class MountingContext:
cfg: BeamlineConfig
devs: BeamlineDevices
mount_position: AerotechCoordinate
@dataclass
class MountingResult:
success: bool
mounted_sample: object | None = None
previous_sample: object | None = None
did_unmount_previous: bool = False
error: Exception | None = None
comment: str | None = None
@property
def is_error(self) -> bool:
return self.error is not None
+183
View File
@@ -0,0 +1,183 @@
import time
from aare.common.exception_handler import (
CriticalTellException,
MountingFailed,
UnmountingFailed,
)
from aare.devices.tell_client import TellEventValueEnum
from aare.daq.operations.mounting.models import MountingContext, MountingResult
class MountingService:
def __init__(self, *, context: MountingContext, logger):
self.ctx = context
self.logger = logger
def reset_mount_failure_counter(self, reason: str) -> None:
previous_count = self.ctx.cfg.get_mount_fail_count()
if previous_count > 0:
self.logger.warning(
f"Resetting mount failure counter from {previous_count} due to: {reason}"
)
else:
self.logger.debug(f"Mount failure counter already clear: {reason}")
self.ctx.cfg.record_mount_success()
def _magnet_position_sensor_check(self, timeout: float = 1.0) -> None:
if self.ctx.devs.magnet_position_sensor.value != 0:
self.logger.warning(
"Goniometer is not in position based on magnet position sensor readout"
)
for _ in range(round(timeout * 10.0)):
if self.ctx.devs.magnet_position_sensor.value == 0:
return
time.sleep(0.1)
self.logger.error(
"Goniometer didn't reach position based on magnet position sensor readout"
)
raise Exception("Goniometer is not in position based on magnet position sensor readout")
def _mount_fail_handler(
self,
*,
mount_error: bool = False,
dry_after_fail_count: int = 3,
end_automation_after_fail_count: int = 5,
) -> None:
if mount_error:
count = self.ctx.cfg.record_mount_failure()
if count > end_automation_after_fail_count:
self.logger.error(
"Mounting continued to fail after stopping automation, contact your local contact"
)
self.logger.debug("Clearing mount fail count")
self.ctx.cfg.record_mount_success()
raise CriticalTellException(
"Mounting continued to fail after stopping automation, contact your local contact."
)
if count == end_automation_after_fail_count:
self.logger.error(f"Mounting failed {count} times, stopping automation")
try:
self.execute(target=None)
self.dry(park=True)
except Exception as e:
self.logger.exception(f"Failed to dry after mount failure: {e}")
raise CriticalTellException(f"Mount failed {count} times in a row, stopping automation.")
if count == dry_after_fail_count:
self.logger.warning(f"Mount failed {count} times in a row; drying")
try:
self.dry(park=False)
except Exception as e:
self.logger.exception(f"Failed to dry after mount failure: {e}")
else:
self.logger.debug("Mounting succeeded, resetting mount failure counter")
self.ctx.cfg.record_mount_success()
def _mount_handler(self, target) -> None:
value = self.ctx.devs.tell.mount(
address=target.tell_address(),
force=True,
auto_unmount=True,
read_dm=False,
wait=True,
timeout=360.0,
)
if isinstance(value, str):
self.logger.error(f"Unexpected string response from Tell mount: {value}")
raise CriticalTellException(f"Critical error in TELL mount: unexpected response '{value}'")
if value is None:
self.logger.error("Tell mount returned no response (None)")
raise CriticalTellException("Critical error in TELL mount: no response")
safe_val = getattr(value, "value", str(value))
self.logger.debug(f"Mount response: {safe_val}")
if value == TellEventValueEnum.NO_PIN_IN_GRIPPER:
raise MountingFailed("No Pin in Gripper")
if value == TellEventValueEnum.PIN_IS_LOST_GRIPPER:
self.logger.error("Pin is lost")
raise MountingFailed("Pin is lost")
if value in (
TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT,
TellEventValueEnum.DRY,
TellEventValueEnum.COLD,
):
self.logger.info(f"Robot is {safe_val} - freeing beamline for user")
return
message = f"Mount response not handled: {safe_val}"
self.logger.error(message)
raise CriticalTellException(f"Critical error in TELL mount: {message}")
def _prepare_mount_hardware(self) -> None:
self.ctx.devs.smargon_move_home()
self.ctx.devs.aerotech_pos = self.ctx.mount_position
self._magnet_position_sensor_check(timeout=360.0)
self.ctx.devs.tell.check_enable_motion()
self.ctx.devs.tell.wait_not_busy()
self.ctx.devs.tell.set_in_mount_position(True)
def dry(self, *, park: bool) -> None:
self.ctx.devs.tell.check_enable_motion()
self.ctx.devs.tell.wait_not_busy()
self.ctx.devs.tell.set_in_mount_position(True)
if self.ctx.cfg.current_sample:
self.ctx.devs.tell.unmount(wait=True, timeout=60.0)
self.ctx.cfg.current_sample = None
if park:
self.ctx.devs.tell.dry(wait_cold=-1, wait=True)
else:
self.ctx.devs.tell.dry(wait=True)
def execute(self, *, target) -> MountingResult:
try:
self._prepare_mount_hardware()
previous_sample = self.ctx.cfg.current_sample
if target is None:
if previous_sample is not None:
self.logger.debug(f"Unmounting sample: {previous_sample}")
try:
self.ctx.devs.tell.unmount(wait=True, timeout=60.0)
self.ctx.cfg.current_sample = None
return MountingResult(success=True)
except Exception as e:
raise UnmountingFailed(f"Failed to unmount: {e}") from e
log_msg = f"Mounting sample: {target}"
if previous_sample is not None:
log_msg = f"Unmounting sample: {previous_sample} \n and {log_msg}"
self.logger.debug(log_msg)
self._mount_handler(target)
self._mount_fail_handler(mount_error=False)
self.ctx.cfg.current_sample = target
return MountingResult(success=True)
except CriticalTellException as e:
self.logger.error(f"Critical Tell error: {e}")
return MountingResult(success=False, error=e, comment=str(e))
except Exception as e:
self.logger.error(f"Mount failed: {e}")
try:
self._mount_fail_handler(mount_error=True)
except Exception as fail_handler_error:
return MountingResult(
success=False,
error=fail_handler_error,
comment=str(fail_handler_error),
)
return MountingResult(success=False, error=e, comment=str(e))