From bda6e5f70901595439bd620fa9debf69c69fcad1 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 22 Oct 2026 15:56:56 +0200 Subject: [PATCH] refactor(timepix): leave detector setup to the IOC and only check it at stage --- superxas_bec/devices/timepix/timepix.py | 36 +++++++++++++++----- tests/tests_devices/test_timepix.py | 45 +++++++++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/superxas_bec/devices/timepix/timepix.py b/superxas_bec/devices/timepix/timepix.py index 2300747..0c41c97 100644 --- a/superxas_bec/devices/timepix/timepix.py +++ b/superxas_bec/devices/timepix/timepix.py @@ -213,6 +213,14 @@ TIMEPIX_FLY_BACKEND_DEFAULT_REDIS_TOPIC = "public/timepix_fly_backend/raw_data" TIMEPIX_SCAN_NAME = "timepix_scan" +# detector setup lives in the IOC (autosave); BEC only checks it at stage +EXPECTED_DETECTOR_SETUP = { + "trigger_mode": TRIGGERMODE.INTERNAL, + "trigger_source": TRIGGERSOURCE.HDMI1_1, + "exposure_mode": EXPOSUREMODE.TIMED, +} +EXPECTED_XES_SETUP = {"tdc1_edge": TDCEdge.RISING, "tdc1_output": TDCOuput.ALL_CHANNELS} + # max wait for the images of a lost acquisition before repeating it LOST_ACQUISITION_IMAGE_TIMEOUT = 2.0 @@ -721,13 +729,25 @@ class Timepix(PSIDeviceBase, TimePixControl): """Enable XES specific settings for the TimePix detector.""" enabled_value = 1 if enabled else 0 self.cam.tdc1_enable.set(enabled_value).wait(timeout=self._pv_timeout) - self.cam.tdc2_enable.set(enabled_value).wait(timeout=self._pv_timeout) + # TDC2 is not cabled; TimePixFly would count its edges as periods too + self.cam.tdc2_enable.set(0).wait(timeout=self._pv_timeout) self.cam.raw_enable.set(enabled_value).wait(timeout=self._pv_timeout) - if enabled: - self.cam.tdc1_edge.set(TDCEdge.RISING).wait(timeout=self._pv_timeout) - self.cam.tdc1_output.set(TDCOuput.ALL_CHANNELS).wait(timeout=self._pv_timeout) - self.cam.tdc2_edge.set(TDCEdge.RISING).wait(timeout=self._pv_timeout) - self.cam.tdc2_output.set(TDCOuput.ALL_CHANNELS).wait(timeout=self._pv_timeout) + + def _check_detector_setup(self) -> None: + """Warn about detector setup PVs, kept in the IOC, that differ from what BEC expects.""" + expected = dict(EXPECTED_DETECTOR_SETUP) + if self.enable_xes: + expected.update(EXPECTED_XES_SETUP) + wrong = [] + for attr, value in expected.items(): + current = getattr(self.cam, attr).get() + if current != value: + wrong.append(f"{attr}={current} (expected {value.name})") + if wrong: + logger.warning( + f"{self.name}: detector setup differs from what BEC expects: {', '.join(wrong)}. " + "These are set in the IOC, not by BEC." + ) def wait_for_connection( self, all_signals: bool = False, timeout: float | None = None, **kwargs @@ -745,9 +765,6 @@ class Timepix(PSIDeviceBase, TimePixControl): logger.info(f"On Connected of TimePix detector {self.name}...") # Prepare TimePix Detector self._enable_xes_settings(self.enable_xes) - self.cam.trigger_mode.set(TRIGGERMODE.INTERNAL).wait(timeout=self._pv_timeout) - self.cam.trigger_source.set(TRIGGERSOURCE.HDMI1_1).wait(timeout=self._pv_timeout) - self.cam.exposure_mode.set(EXPOSUREMODE.TIMED).wait(timeout=self._pv_timeout) # Reset array counter on connect self.cam.array_counter.set(0).wait(timeout=self._pv_timeout) # Set image mode to multiple @@ -830,6 +847,7 @@ class Timepix(PSIDeviceBase, TimePixControl): self.cam.acquire_period.set(exp_time).wait(timeout=self._pv_timeout) self.cam.num_images.set(burst_images).wait(timeout=self._pv_timeout) self.cam.data_source.set(DATASOURCE.IMAGE).wait(timeout=self._pv_timeout) + self._check_detector_setup() # Setup file writing self._full_path = get_full_path(scan_msg, name="timepix") diff --git a/tests/tests_devices/test_timepix.py b/tests/tests_devices/test_timepix.py index c2c4164..3a2c30b 100644 --- a/tests/tests_devices/test_timepix.py +++ b/tests/tests_devices/test_timepix.py @@ -264,16 +264,12 @@ def pixel_map(): def test_timepix_on_connected_configures_signals_and_registers_callback(timepix): """Connected hook should configure camera, file writer, and backend callback.""" + timepix.cam.tdc2_enable.put(1) timepix.on_connected() assert timepix.cam.tdc1_enable.get() == 1 - assert timepix.cam.tdc2_enable.get() == 1 + assert timepix.cam.tdc2_enable.get() == 0 assert timepix.cam.raw_enable.get() == 1 - assert timepix.cam.tdc1_edge.get() == TDCEdge.RISING - assert timepix.cam.tdc1_output.get() == TDCOuput.ALL_CHANNELS - assert timepix.cam.trigger_mode.get() == TRIGGERMODE.INTERNAL - assert timepix.cam.trigger_source.get() == TRIGGERSOURCE.HDMI1_1 - assert timepix.cam.exposure_mode.get() == EXPOSUREMODE.TIMED assert timepix.cam.array_counter.get() == 0 assert timepix.hdf.enable.get() == "1" assert timepix.hdf.file_write_mode.get() == FILEWRITEMODE.STREAM.value @@ -909,3 +905,40 @@ def test_timepix_stop_cancels_repetition_of_lost_acquisition(xes_timepix): assert timepix.backend.on_trigger_finished.call_count == 1 assert not timepix._xes_acquisitions assert timepix._images_to_skip == 0 + + +def test_timepix_on_connected_leaves_detector_setup_to_the_ioc(timepix): + """Trigger and TDC edge/output PVs belong to the IOC setup; connecting must not write them.""" + timepix.cam.trigger_mode.put(TRIGGERMODE.EXTERNAL) + timepix.cam.exposure_mode.put(EXPOSUREMODE.TRIGGER_WIDTH) + timepix.cam.tdc1_edge.put(TDCEdge.FALLING) + timepix.cam.tdc1_output.put(TDCOuput.CHANNEL_0) + + timepix.on_connected() + + assert timepix.cam.trigger_mode.get() == TRIGGERMODE.EXTERNAL + assert timepix.cam.exposure_mode.get() == EXPOSUREMODE.TRIGGER_WIDTH + assert timepix.cam.tdc1_edge.get() == TDCEdge.FALLING + assert timepix.cam.tdc1_output.get() == TDCOuput.CHANNEL_0 + + +def test_timepix_check_detector_setup_warns_about_unexpected_values(timepix): + """A setup PV that differs from what BEC expects is reported, not overwritten.""" + timepix._enable_xes = True + timepix.cam.trigger_mode.put(TRIGGERMODE.EXTERNAL) + timepix.cam.tdc1_edge.put(TDCEdge.FALLING) + + with mock.patch("superxas_bec.devices.timepix.timepix.logger") as logger: + timepix._check_detector_setup() + + message = logger.warning.call_args.args[0] + assert "trigger_mode" in message and "tdc1_edge" in message + assert timepix.cam.trigger_mode.get() == TRIGGERMODE.EXTERNAL + + +def test_timepix_check_detector_setup_is_quiet_when_setup_matches(timepix): + """No warning when the IOC setup matches.""" + timepix._enable_xes = True + with mock.patch("superxas_bec.devices.timepix.timepix.logger") as logger: + timepix._check_detector_setup() + logger.warning.assert_not_called()