fix(panda): write layout and pulse fields only when they differ

A replayed PULSE train also came 1.3 s after an unchanged parameter write by a new
device instance (connect / configure_line), outside the 1 s quiet wait, and blocked
the first line trigger. Fields are now read first and written only if the PandA holds
a different value, so a reconnect on a configured PandA writes nothing; the quiet wait
after a real change is 2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM
This commit is contained in:
Mirko HollerandClaude Opus 5.5 committed 2026-10-29 12:08:34 +01:00
1 parent 972137b871
commit 28ba071d68
2 files changed
+60 -18

No files matched your search

@@ -22,9 +22,11 @@ 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. The replayed train starts only ~0.3-0.6 s after
parameter change do appear on TTLOUT2/3. The replayed train starts only ~0.3-1.3 s after
the write (seen 2026-10-29), so configure_line waits until both PULSE outputs stayed low
for PARAM_QUIET_TIME.
for PARAM_QUIET_TIME. All fields are read first and written only if the PandA holds a
different value: a new device instance (BEC restart, reconnect) on an already configured
PandA writes nothing, so it cannot cause a replay.
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).
@@ -37,6 +39,7 @@ import socket
import threading
import time
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import Device
@@ -131,8 +134,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
# a replayed train starts up to ~1.3 s after a parameter write
PARAM_QUIET_TIME = 2.0
SHUTTER_OUTPUT = "TTLOUT2"
DETECTOR_OUTPUT = "TTLOUT3"
@@ -231,9 +234,29 @@ class PandaLineTrigger(Device):
def setup_layout(self) -> None:
"""Write the wiring of the line trigger blocks (other blocks are not touched)."""
for field, value in self._layout():
self.panda.set(field, value)
written = [field for field, value in self._layout() if self._set_if_changed(field, value)]
self._pulse_params = None
pulses = (self.SHUTTER_PULSE, self.DETECTOR_PULSE)
if any(field.split(".")[0] in pulses for field in written):
self._wait_pulses_idle(2 * self._train_duration(None) + 1)
def _set_if_changed(self, field: str, value) -> bool:
"""
Write a field only if the PandA holds a different value (fewer writes, fewer replayed
PULSE trains). Actions (empty value, e.g. FORCE_RST) are always sent. Returns True
if the field was written.
"""
value = str(value)
if value != "":
current = self.panda.get(field)
try:
if np.isclose(float(current), float(value), rtol=1e-9, atol=1e-12):
return False
except ValueError:
if current == value:
return False
self.panda.set(field, value)
return True
@staticmethod
def _train_duration(params: dict | None) -> float:
@@ -270,19 +293,24 @@ class PandaLineTrigger(Device):
if params == self._pulse_params:
return
sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE
self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "")
self.panda.set(f"{dp}.DELAY", f"{params['delay']:.9f}")
self.panda.set(f"{dp}.WIDTH", f"{params['width']:.9f}")
self.panda.set(f"{dp}.STEP", f"{params['step']:.9f}")
self.panda.set(f"{dp}.PULSES", params["n_pulses"])
self.panda.set(f"{sp}.DELAY", "0")
self.panda.set(f"{sp}.WIDTH", f"{params['shutter_width']:.9f}")
self.panda.set(f"{sp}.STEP", "0")
self.panda.set(f"{sp}.PULSES", 1)
fields = [
(f"{dp}.DELAY", f"{params['delay']:.9f}"),
(f"{dp}.WIDTH", f"{params['width']:.9f}"),
(f"{dp}.STEP", f"{params['step']:.9f}"),
(f"{dp}.PULSES", params["n_pulses"]),
(f"{sp}.DELAY", "0"),
(f"{sp}.WIDTH", f"{params['shutter_width']:.9f}"),
(f"{sp}.STEP", "0"),
(f"{sp}.PULSES", 1),
]
previous = self._pulse_params
self._pulse_params = None
self._wait_pulses_idle(
2 * max(self._train_duration(self._pulse_params), self._train_duration(params)) + 1
)
self.panda.set(f"{self.ARM_GATE}.FORCE_RST", "")
written = [field for field, value in fields if self._set_if_changed(field, value)]
if written:
self._wait_pulses_idle(
2 * max(self._train_duration(previous), self._train_duration(params)) + 1
)
self._pulse_params = params
def _wait_pulses_idle(self, timeout: float) -> None:
@@ -241,3 +241,17 @@ def test_configure_line_waits_for_a_late_replayed_train(trigger):
trigger.configure_line(5, 0.01)
# the train ended at ~0.1 s, then 0.2 s quiet
assert time.time() - t0 >= 0.28
def test_unchanged_fields_are_not_written(trigger):
"""A new device instance on a configured PandA writes nothing (no replayed trains)."""
trigger.configure_line(5, 0.01, 0.002)
fields = dict(trigger.panda.fields)
again = PandaLineTrigger(name="pandatrig", host="fake-panda")
again.PARAM_QUIET_TIME = 0.01
again.panda = FakePanda()
again.panda.fields = fields
again.wait_for_connection()
again.configure_line(5, 0.01, 0.002)
values = [w for w in again.panda.writes if not w.split("=", 1)[1] == ""]
assert values == []