diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index 96294d9a..7e7b9f5a 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -165,5 +165,6 @@ edges. PandA FPGA 4.1 PULSE block: an ENABLE edge, a parameter write or a dropped trigger often makes it replay the previous pulse train without a trigger. The device keeps the PULSE blocks enabled, writes their parameters only when they change (once per scan) and then waits -until such a train is over (pulses can appear on TTLOUT2/3 meanwhile), and the trigger path +until such a train is over (pulses can appear on TTLOUT2/3 meanwhile; the train starts only +~0.3-0.6 s after the write, so both outputs must stay low for 1 s), and the trigger path filters out edges during a line, so no trigger is ever dropped by a PULSE block. diff --git a/csaxs_bec/devices/panda_box/panda_line_trigger.py b/csaxs_bec/devices/panda_box/panda_line_trigger.py index 3b887789..e4336836 100644 --- a/csaxs_bec/devices/panda_box/panda_line_trigger.py +++ b/csaxs_bec/devices/panda_box/panda_line_trigger.py @@ -22,7 +22,9 @@ any trigger. With unchanged parameters and ENABLE kept high, hundreds of trigger a row were correct. So the PULSE blocks stay enabled, their parameters are only written when they change (once per scan), followed by a wait until a replayed train is over, and edges during a line are filtered out before the PULSE blocks. Replayed trains during a -parameter change do appear on TTLOUT2/3. +parameter change do appear on TTLOUT2/3. The replayed train starts only ~0.3-0.6 s after +the write (seen 2026-10-29), so configure_line waits until both PULSE outputs stayed low +for PARAM_QUIET_TIME. Only the control port (8888) is used: the fields are written by setup_layout() at connect, the PCAP data capture is not touched. Timing resolution 8 ns (125 MHz FPGA clock). @@ -129,6 +131,8 @@ class PandaLineTrigger(Device): TRIGGER_COUNTER = "COUNTER6" LINE_COUNTER = "COUNTER7" PULSE_COUNTER = "COUNTER8" + # a replayed train starts up to ~0.6 s after a parameter write + PARAM_QUIET_TIME = 1.0 SHUTTER_OUTPUT = "TTLOUT2" DETECTOR_OUTPUT = "TTLOUT3" @@ -282,25 +286,22 @@ class PandaLineTrigger(Device): self._pulse_params = params def _wait_pulses_idle(self, timeout: float) -> None: - """Wait until a replayed train is over: stable counter and both PULSE outputs low.""" + """Wait until a replayed train is over: both PULSE outputs low for PARAM_QUIET_TIME.""" sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE - deadline = time.time() + timeout - last = None + deadline = time.time() + timeout + self.PARAM_QUIET_TIME + last_active = time.time() while True: - time.sleep(0.02) - state = ( - self.panda.get(f"{self.PULSE_COUNTER}.OUT"), - self.panda.get(f"{sp}.OUT"), - self.panda.get(f"{dp}.OUT"), - ) - if state == last and state[1:] == ("0", "0"): + time.sleep(0.005) + now = time.time() + if self.panda.get(f"{sp}.OUT") != "0" or self.panda.get(f"{dp}.OUT") != "0": + last_active = now + elif now - last_active >= self.PARAM_QUIET_TIME: return - if time.time() > deadline: + if now > deadline: raise PandaLineTriggerError( f"{self.name}: PULSE blocks still active {timeout:.1f} s after a parameter" " change." ) - last = state def arm(self, one_shot: bool = False) -> None: """ diff --git a/tests/tests_devices/test_panda_line_trigger.py b/tests/tests_devices/test_panda_line_trigger.py index 287da0e7..4a9dfb53 100644 --- a/tests/tests_devices/test_panda_line_trigger.py +++ b/tests/tests_devices/test_panda_line_trigger.py @@ -52,6 +52,7 @@ class FakePanda(PandaControl): @pytest.fixture def trigger(): device = PandaLineTrigger(name="pandatrig", host="fake-panda") + device.PARAM_QUIET_TIME = 0.01 device.panda = FakePanda() device.wait_for_connection() return device @@ -219,3 +220,24 @@ def test_falling_edge_inverts_the_input(): def test_invalid_trigger_edge(): with pytest.raises(ValueError, match="trigger_edge"): PandaLineTrigger(name="pandatrig", host="fake-panda", trigger_edge="both") + + +def test_configure_line_waits_for_a_late_replayed_train(trigger): + """The replayed train starts only after the write; configure_line must outlast it.""" + import threading + import time + + trigger.PARAM_QUIET_TIME = 0.2 + fields = trigger.panda.fields + + def replay(): + time.sleep(0.05) + fields["PULSE4.OUT"] = "1" + time.sleep(0.05) + fields["PULSE4.OUT"] = "0" + + threading.Thread(target=replay, daemon=True).start() + t0 = time.time() + trigger.configure_line(5, 0.01) + # the train ended at ~0.1 s, then 0.2 s quiet + assert time.time() - t0 >= 0.28