From addffa1299556c00a385e724ee8a3bd09c58cfd9 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 29 Sep 2026 12:35:56 +0200 Subject: [PATCH] fix(eiger): resolve live preview data channel per detector, not "default" only The Eiger2 Si 500K's multi-threshold JFJ preview messages key the frame under a threshold name (e.g. "threshold_1"), not "default" as eiger_1_5/ eiger_9 do -- confirmed by capturing the live JFJ ZMQ preview stream. _preview_callback hardcoded "default", so it silently dropped every frame for eiger2_si_500k (251+ "without data" errors in DeviceServer.log, matching the empty BEC image widget). Now prefers "default" when present and falls back to whichever single channel the broker actually sent. Also document the JFJoch Web UI (http://sls-jfjoch-001.psi.ch:8080) and the Eiger 2 Si 500K detector server address (129.129.122.159) in bl_detectors.yaml's Eiger section. Co-Authored-By: Claude Sonnet 5 --- csaxs_bec/device_configs/bl_detectors.yaml | 43 +++++++++++++--------- csaxs_bec/devices/jungfraujoch/eiger.py | 13 +++++-- tests/tests_devices/test_eiger.py | 23 ++++++++++++ 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/csaxs_bec/device_configs/bl_detectors.yaml b/csaxs_bec/device_configs/bl_detectors.yaml index 74cbc7b4..be88dd87 100644 --- a/csaxs_bec/device_configs/bl_detectors.yaml +++ b/csaxs_bec/device_configs/bl_detectors.yaml @@ -1,23 +1,16 @@ -eiger2_si_500k: - description: Eiger 2 Si 500K detector - deviceClass: csaxs_bec.devices.jungfraujoch.eiger2_si_500k.Eiger2Si500K - deviceConfig: - detector_distance: 1000 - beam_center: [200, 200] - onFailure: raise - enabled: True - readoutPriority: async - softwareTrigger: False - deviceTags: - - bl_detectors - - eiger +# Eiger detectors (JungfrauJoch). Only one may be enabled at a time -- the JFJ broker +# serves a single selected/initialised detector, so switching the active one here also +# requires selecting/initialising the matching detector on the JFJ broker itself. +# JungfrauJoch-side config (detector selection/init, pixel mask, azimuthal integration +# settings) is done in the Web UI at http://sls-jfjoch-001.psi.ch:8080. +# The Eiger 2 Si 500K detector server itself is reachable at 129.129.122.159. +# eiger2_si_500k: -# eiger_1_5: -# description: Eiger 1.5M in-vacuum detector -# deviceClass: csaxs_bec.devices.jungfraujoch.eiger_1_5m.Eiger1_5M +# description: Eiger 2 Si 500K detector +# deviceClass: csaxs_bec.devices.jungfraujoch.eiger2_si_500k.Eiger2Si500K # deviceConfig: -# detector_distance: 2200 -# beam_center: [870, 1203] +# detector_distance: 1000 +# beam_center: [200, 200] # onFailure: raise # enabled: True # readoutPriority: async @@ -26,6 +19,20 @@ eiger2_si_500k: # - bl_detectors # - eiger +eiger_1_5: + description: Eiger 1.5M in-vacuum detector + deviceClass: csaxs_bec.devices.jungfraujoch.eiger_1_5m.Eiger1_5M + deviceConfig: + detector_distance: 2200 + beam_center: [870, 1203] + onFailure: raise + enabled: True + readoutPriority: async + softwareTrigger: False + deviceTags: + - bl_detectors + - eiger + # eiger_9: # description: Eiger 9M detector # deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M diff --git a/csaxs_bec/devices/jungfraujoch/eiger.py b/csaxs_bec/devices/jungfraujoch/eiger.py index 6e41a007..c597ac10 100644 --- a/csaxs_bec/devices/jungfraujoch/eiger.py +++ b/csaxs_bec/devices/jungfraujoch/eiger.py @@ -226,11 +226,18 @@ class Eiger(PSIDeviceBase): message (dict): The message received from the preview stream. """ if message.get("type", "") == "image": - data = message.get("data", {}).get("default", None) - if data is None: + channels = message.get("data", {}) + # The data dict's key varies by detector: "default" for eiger_1_5/eiger_9, + # but a threshold name (e.g. "threshold_1") for the multi-threshold-capable + # Eiger2 Si 500K. Prefer "default" when present, else take whichever single + # channel the broker actually sent. + frame = channels.get("default") + if frame is None: + frame = next(iter(channels.values()), None) + if frame is None: logger.error(f"Received image message on device {self.name} without data.") return - self.preview_image.put(data) + self.preview_image.put(frame) # pylint: disable=missing-function-docstring @property diff --git a/tests/tests_devices/test_eiger.py b/tests/tests_devices/test_eiger.py index 5e036de5..979494c2 100644 --- a/tests/tests_devices/test_eiger.py +++ b/tests/tests_devices/test_eiger.py @@ -542,6 +542,29 @@ def test_eiger_preview_callback(eiger_1_5m): mock_preview_put.assert_not_called() +def test_eiger_preview_callback_falls_back_to_non_default_channel(eiger2_si_500k): + """The Eiger2 Si 500K's multi-threshold preview data has no "default" key (it's + named after the threshold, e.g. "threshold_1" -- confirmed against a live capture + of the JFJ preview stream); the callback must still forward that single channel. + """ + eiger = eiger2_si_500k + frame = np.array([[1, 2], [3, 4]]) + msg = {"type": "image", "data": {"threshold_1": frame}} + with mock.patch.object(eiger.preview_image, "put") as mock_preview_put: + eiger._preview_callback(msg) + mock_preview_put.assert_called_once_with(frame) + + +def test_eiger_preview_callback_logs_when_data_is_truly_empty(eiger_1_5m): + """No channel at all (empty or missing data dict) must still be treated as missing, + not silently pass through some other unrelated dict key.""" + eiger = eiger_1_5m + for msg in [{"type": "image", "data": {}}, {"type": "image"}]: + with mock.patch.object(eiger.preview_image, "put") as mock_preview_put: + eiger._preview_callback(msg) + mock_preview_put.assert_not_called() + + def test_eiger_acquisition_matches_the_gated_window(eiger_1_5m): """The detector must acquire for exp_time - readout, not for the whole period.