fix(eiger): resolve live preview data channel per detector, not "default" only
CI for csaxs_bec / test (push) Successful in 2m55s
CI for csaxs_bec / test (pull_request) Successful in 2m59s

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 <noreply@anthropic.com>
This commit is contained in:
x12saandClaude Sonnet 5 committed 2026-09-29 12:35:56 +02:00
1 parent c88256b9f6
commit addffa1299
3 files changed
+58 -21

No files matched your search

+25 -18
View File
@@ -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
+10 -3
View File
@@ -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
+23
View File
@@ -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.