CHANGELOG.md updated and added autodocstrings to daq daq_worker and server.py

This commit is contained in:
2026-03-31 14:34:47 +02:00
parent 3fd80b94fc
commit 2c38b9f7ab
4 changed files with 964 additions and 12 deletions
+21 -5
View File
@@ -2,34 +2,50 @@
## 0.3.0
- Merge three repos into one, remove Ultralytics as dependency
- **DAQ/Server**:
- Added lifespan function for one-time initialization of core objects (devices, daq, config) on startup, reducing conflicting Redis and device connections.
- Big Aerotech upgrade: transitioned to Aerotech API with ported coordinate functions.
- Re-enabled and updated automation workflow 2.0 with new backend endpoints.
- Added `park_and_dry` functionality for repeated mount failures.
- Workflow assertions to ensure scintillator is down when possible.
- Improved aerotech movement and position calculations.
- Implementation of camera auto-exposure and zoom-dependent settings.
- Added SAS-TT school demo temporary raster function.
- Enhanced status polling with robust error handling for `Tell` and `Smargon` communication.
- `ml_box` updated with beamline-specific URLs and model class types.
- Enhanced status polling with robust error handling for `Tell`, `Smargon`, and `Aerotech` communication.
- `ml_box` updated with beamline-specific URLs, model class types, and new YOLO26 engine.
- Added separate authentication pathway for staff-only beamline recovery operations.
- Improved database message limits for omega (to 2 decimal places).
- Added screenshot to database from GUI; face detection sequence and beamline recovery updates.
- Throttled `UserRightsException` logging to reduce log noise.
- **Baton & Sessions**:
- Major rework of the baton exchange procedure and session management.
- Added `NOT_BATON_HOLDER` error code and specialized handling for baton requests.
- Integrated baton status messages into the camera image display and status bar.
- Improved session state transitions with new `Pending` states and P-group selection logic.
- **GUI**:
- Added `ui_state_manager` to handle `QSettings` for persistent UI state.
- Major rework of `predict_subscriber` and prediction stream updates; corrected default ZMQ stream for ML box.
- Added FPS counter and sharpness monitoring for sample camera.
- Improved camera thread performance and authentication failure handling.
- Added X10SA specific camera and prediction configurations.
- New `display_status` for user-facing connection/error messages.
- Integrated `dev_help` panel with logs and error code search.
- Added framework for building GUI tutorials.
- Added alert banner for critical disconnection issues (server/Tell).
- Added framework for building GUI tutorials with first draft of X10SA tutorial.
- Added alert banner for critical disconnection issues (server/Tell/Aerotech).
- New screenshot button and controls in sample camera GUI.
- Added SSE support for face detection panel updates during automation.
- Integrated sample position resync in recovery panel to reduce Tell polling.
- **TELL**:
- Fixed matplotlib warnings in face detection panel and improved layout persistence.
- **TELL & Infrastructure**:
- Added mTLS certificate authentication for `aaredb`, `tellupdater`, and `spreadsheetupdater`.
- Improved SSE client resilience in `tellupdater` with retry mechanisms and enhanced error logging.
- Major refactor: removed redundant code and added simulated client.
- Converted wait functions to use inbuilt pshell functionality.
- Added error handling and proxy support for connection failures.
- Fixed event names for mounting, and added missing events for gripper detection and motion sync.
- Refactored SSE connection handling for simplicity and robustness.
- **Common & Devices**:
- Added BEC worker and Aerotech controller initialization in `devices.py`.
- Added magnet position sensor functions and centralized transmission settings.
- Added centralized `error_codes` and refactored exception handling to use FastAPI handlers.
- Expanded device support with new PVs (beamstop, scintillator, collimator, cryojet, etc.).
- Cleaned up `mx_lib` wait functions and `my_motor` class; added `clean_filename` helper.
+69
View File
@@ -812,6 +812,16 @@ class AareDAQ:
raise Exception(f"Failed during raster: {e}") from e
def measure_raster(self, r: RasterGridRequest, auto: bool) -> CompletedRasterGrid:
"""
Execute a raster scan.
Args:
r: RasterGridRequest parameters for the scan.
auto: Boolean flag indicating if this is part of an automated sequence.
Returns:
CompletedRasterGrid result.
"""
self.__cfg.try_set_busy(timeout=ceil(360))
try:
if auto:
@@ -906,6 +916,15 @@ class AareDAQ:
return CompletedRotationScan(request=copy.deepcopy(request), result=result)
def measure_rotation(self, request: RotationScanRequest) -> CompletedRotationScan:
"""
Execute a rotation scan.
Args:
request: RotationScanRequest parameters.
Returns:
CompletedRotationScan result.
"""
total_time = request.exp_time_s * request.steps
logger.info(f"received rotation scan request: {request}, total time: {total_time}s, steps: {request.steps}")
self.__cfg.try_set_busy(timeout=ceil(total_time + 360))
@@ -1117,6 +1136,16 @@ class AareDAQ:
return SmargonCoordinate(sh_mm=coord), cls, classes
def ml_bounding_box(self, sample_id: int | None = None, filename: str | None = None) -> RasterGridRequest | None:
"""
Request an ML-based bounding box for the sample.
Args:
sample_id: Optional sample ID.
filename: Optional filename for saving/logging.
Returns:
RasterGridRequest object representing the found bounding box, or None if failed.
"""
try:
self.__cfg.try_set_busy(timeout=360)
r = self.__ml_bounding_box(sample_id, filename)
@@ -1127,6 +1156,16 @@ class AareDAQ:
raise
def face_detection(self, steps: int = 14, step_size: int = 15) -> dict:
"""
Perform a face detection sequence by rotating the sample and using ML to find the flat face.
Args:
steps: Number of rotation steps.
step_size: Size of each rotation step in degrees.
Returns:
Dictionary containing face detection results, including found samples and fits.
"""
self.__cfg.try_set_busy(timeout=360)
try:
logger.info("running face detection sequence")
@@ -1391,6 +1430,19 @@ class AareDAQ:
return False
def auto_loop_center(self, sample_id: int | None = None) -> float:
"""
Automatically center the loop using ML-based detection.
This performs a multi-step sequence including rotation and centering.
Args:
sample_id: Optional database ID of the sample. If None, uses current sample.
Returns:
Time taken for the centering process (seconds)
Raises:
LoopCenteringFailed: If the centering sequence fails
"""
start = time.perf_counter()
try:
self.__cfg.try_set_busy(timeout=360)
@@ -1430,6 +1482,13 @@ class AareDAQ:
cv2.imwrite(f"{filename}.jpg", bgr_image)
def save_screenshot_db(self, sample_id: int, filename: str):
"""
Capture a screenshot and upload it to the database for a specific sample.
Args:
sample_id: Database ID of the sample.
filename: Name to give to the uploaded image.
"""
#time.sleep(0.2) # Wait 200 ms to ensure camera image is stable
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
self.__aare.upload_image(sample_id, filename, bgr_image)
@@ -1549,6 +1608,16 @@ class AareDAQ:
return default_params, "defaults"
def measure(self, sample: SampleShortInfo) -> float:
"""
Main measurement sequence for a given sample.
Includes mounting, loop centering, face detection, auto-raster, and data collection.
Args:
sample: SampleShortInfo object containing sample details
Returns:
Total time taken for the measurement (seconds)
"""
start = time.perf_counter()
formatted_date = datetime.now().strftime('%Y%m%d')
sample_prefix = "{}/{}/{:02d}/{}".format(
+779 -5
View File
File diff suppressed because it is too large Load Diff
+95 -2
View File
@@ -68,6 +68,14 @@ class DAQWorker(QObject):
baton_timeout_checked = Signal(dict)
def __init__(self, base_url: str | None, token: str, parent=None):
"""
Initialize the DAQWorker.
Args:
base_url: The base URL of the DAQ server.
token: The authentication token.
parent: The parent QObject.
"""
super().__init__(parent)
self._active_status_error_key = None
self.__token = token
@@ -230,6 +238,9 @@ class DAQWorker(QObject):
@Slot()
def regular_update(self):
"""
Periodically triggered slot to update spreadsheet and request status from server.
"""
if self.__counter % SPREADHSEET_FREQUENCY == 0:
self.load_spreadsheet()
self.load_reference_tools()
@@ -237,6 +248,9 @@ class DAQWorker(QObject):
self.send_status_request()
def send_status_request(self):
"""
Send an asynchronous HTTP GET request to the /status endpoint.
"""
if self.__base_url is None:
return
@@ -252,6 +266,18 @@ class DAQWorker(QObject):
@staticmethod
def handle_response(reply: QNetworkReply):
"""
Common handler for QNetworkReply to read data and handle errors.
Args:
reply: The QNetworkReply object.
Returns:
The response data as a UTF-8 string.
Raises:
RuntimeError: If the reply contains a network error.
"""
if reply.error() == QNetworkReply.NetworkError.NoError:
response_data = reply.readAll().data().decode("utf-8")
reply.deleteLater()
@@ -320,6 +346,12 @@ class DAQWorker(QObject):
@Slot(QNetworkReply)
def handle_status_response(self, reply: QNetworkReply):
"""
Handle the server's response to a status request.
Args:
reply: The QNetworkReply object.
"""
try:
response_data = self.handle_response(reply)
parsed_response = DAQStatusModel.model_validate_json(response_data)
@@ -546,6 +578,13 @@ class DAQWorker(QObject):
self.http_error.emit(str(e))
def generic_post(self, url: str, body: str = ""):
"""
Send a generic HTTP POST request to the server.
Args:
url: The relative URL (endpoint).
body: The request body string.
"""
if self.__base_url is None:
logger.info(f"POST /{url}: {body}")
return
@@ -558,6 +597,13 @@ class DAQWorker(QObject):
reply.finished.connect(lambda: self.handle_req_response(reply))
def generic_put(self, url: str, body: str = ""):
"""
Send a generic HTTP PUT request to the server.
Args:
url: The relative URL (endpoint).
body: The request body string.
"""
if self.__base_url is None:
logger.info(f"PUT /{url}: {body}")
return
@@ -569,6 +615,12 @@ class DAQWorker(QObject):
reply.finished.connect(lambda: self.handle_req_response(reply))
def generic_delete(self, url: str):
"""
Send a generic HTTP DELETE request to the server.
Args:
url: The relative URL (endpoint).
"""
if self.__base_url is None:
logger.info(f"DELETE /{url}")
return
@@ -778,6 +830,12 @@ class DAQWorker(QObject):
@Slot(RotationScanRequest)
def standard_scan(self, r: RotationScanRequest):
"""
Trigger a standard rotation scan.
Args:
r: The RotationScanRequest object.
"""
self.run_number_incremented.emit()
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/rotation"))
@@ -826,6 +884,12 @@ class DAQWorker(QObject):
@Slot(RasterGridRequest)
def raster_scan(self, r: RasterGridRequest):
"""
Trigger a raster scan.
Args:
r: The RasterGridRequest object.
"""
self.run_number_incremented.emit()
if self.__base_url is None:
logger.info(f"POST /scan/raster: {r.model_dump_json()}")
@@ -861,6 +925,12 @@ class DAQWorker(QObject):
@Slot(RasterGridRequest)
def raster_scan_auto(self, r: RasterGridRequest):
"""
Trigger an automated raster scan.
Args:
r: The RasterGridRequest object.
"""
self.run_number_incremented.emit()
if self.__base_url is None:
logger.info(f"POST /scan/raster: {r.model_dump_json()}")
@@ -955,6 +1025,12 @@ class DAQWorker(QObject):
@Slot(SampleShortInfo)
def automated_scan(self, s: SampleShortInfo):
"""
Trigger a fully automated scan sequence for a sample.
Args:
s: The SampleShortInfo object.
"""
if self.__base_url is None:
logger.info(f"POST /scan/auto: {s.model_dump_json()}")
return
@@ -1045,6 +1121,9 @@ class DAQWorker(QObject):
@Slot()
def ml_bounding_box(self):
"""
Request an ML-based bounding box for the current sample.
"""
if self.__base_url is None:
logger.info(f"POST /alc/ml_bounding_box")
return
@@ -1102,6 +1181,13 @@ class DAQWorker(QObject):
@Slot()
def face_detection(self, steps: int, step_size: int):
"""
Trigger the face detection procedure.
Args:
steps: Number of rotation steps.
step_size: Degrees per step.
"""
if self.__base_url is None:
logger.info(f"POST /face_detection/run?steps={steps}&step_size={step_size}")
return
@@ -1355,7 +1441,9 @@ class DAQWorker(QObject):
@Slot()
def request_baton(self):
"""Request the baton."""
"""
Request the baton to gain write access to the beamline.
"""
if self.__base_url is None:
logger.info("POST /baton/request")
return
@@ -1388,7 +1476,12 @@ class DAQWorker(QObject):
@Slot(bool)
def respond_to_baton_request(self, accept: bool):
"""Respond to an incoming baton request."""
"""
Respond to an incoming baton request from another user.
Args:
accept: True to grant the baton, False to refuse.
"""
if self.__base_url is None:
logger.info(f"POST /baton/respond?accept={accept}")
return