diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index 4c4fe53..616a732 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -48,9 +48,15 @@ NIDAQ_STREAM_CHANNELS = [ "xrd_ai0_std_dev", ] -#: Groups fed from the EPICS scalars during software-triggered scans. The -#: std_dev and stream groups have no synchronous source and stay hardware-only. -NIDAQ_SW_FORWARD_GROUPS = ("mean", "max") +#: Group channel -> device attribute of the live EPICS scalar backing it in +#: software-triggered scans. The backend's standby acquisition feeds ALL +#: channels (plus energy and encoder), so everything with an EPICS counterpart +#: is forwarded; std_dev and the derived stream channels are hardware-only. +NIDAQ_SW_FORWARD_SOURCES: dict[str, dict[str, str]] = { + "mean": {channel: channel for channel in NIDAQ_MEAN_CHANNELS}, + "max": {channel: channel for channel in NIDAQ_MAX_CHANNELS}, + "stream": {"energy": "energy_epics", "enc": "enc_epics"}, +} class NidaqError(Exception): @@ -215,31 +221,17 @@ class Nidaq(PSIDeviceBase, NidaqControl): self._forward_epics_readings() return data - def _enabled_sw_channels(self) -> dict[str, list[str]]: - """Channels currently enabled via the bitmasks, per software-forwarded group. - - Used as a data-quality filter for the software-mode forwarding only (do - not forward junk values of disabled inputs); it is NOT a validation - contract for the hardware stream, whose active channel set cannot be - predicted upfront. - """ - ai_mask = int(self.ai_chans.get() or 0) - ci_mask = int(self.ci_chans.get() or 0) - di_mask = int(self.di_chans.get() or 0) - return { - "mean": [f"ai{i}" for i in range(8) if ai_mask >> i & 1] - + [f"ci{i}" for i in range(18) if ci_mask >> i & 1], - "max": [f"di{i}" for i in range(5) if di_mask >> i & 1], - } - def _forward_epics_readings(self) -> None: """Forward the current EPICS scalar readings to the statistic groups. Software-triggered scans only: during the backend-driven scans (valid_scan_names) the per-oscillation statistics arrive through the - hardware path, which owns the groups. Only the groups whose members - have a synchronous EPICS counterpart are fed (NIDAQ_SW_FORWARD_GROUPS), - restricted to the bitmask-enabled channels. + hardware path, which owns the groups. ALL channels are forwarded: the + backend's standby acquisition runs every input regardless of the + measurement bitmasks (AIChans/CIChans/DIChans describe the last + hardware task, not what is live), so every EPICS scalar carries a + valid reading. std_dev and the derived stream channels have no EPICS + counterpart and stay hardware-only (NIDAQ_SW_FORWARD_SOURCES). """ if self._staged != Staged.yes: return @@ -250,10 +242,10 @@ class Nidaq(PSIDeviceBase, NidaqControl): ): return now = time.time() - for group, channels in self._enabled_sw_channels().items(): + for group, sources in NIDAQ_SW_FORWARD_SOURCES.items(): batch = { - channel: {"value": getattr(self, channel).get(), "timestamp": now} - for channel in channels + channel: {"value": getattr(self, attr).get(), "timestamp": now} + for channel, attr in sources.items() } if batch: # Annotate the alignment: these rows are in sync with the diff --git a/tests/tests_devices/test_nidaq.py b/tests/tests_devices/test_nidaq.py index 1b438e4..c606ba8 100644 --- a/tests/tests_devices/test_nidaq.py +++ b/tests/tests_devices/test_nidaq.py @@ -217,31 +217,20 @@ from types import SimpleNamespace from ophyd import Staged -def _enable_channels(dev, ai_mask=0b1, ci_mask=0b1000, di_mask=0b110): - """Enable ai0, ci3 and di1/di2 via the channel bitmasks.""" - dev.ai_chans._read_pv.mock_data = ai_mask - dev.ci_chans._read_pv.mock_data = ci_mask - dev.di_chans._read_pv.mock_data = di_mask - - -def test_enabled_sw_channels_follow_bitmasks(mock_nidaq): +def test_read_forwards_all_epics_scalars_when_gates_open(mock_nidaq, scan_info_mock): + """Software-triggered scans: the monitored readout forwards ALL EPICS + values into the SAME groups the hardware-triggered stream feeds. The + measurement bitmasks are irrelevant: the backend's standby acquisition + runs every channel.""" dev = mock_nidaq - _enable_channels(dev, ai_mask=0b101, ci_mask=0b10, di_mask=0b110) - enabled = dev._enabled_sw_channels() - assert enabled["mean"] == ["ai0", "ai2", "ci1"] - assert enabled["max"] == ["di1", "di2"] - - -def test_read_forwards_enabled_epics_scalars_when_gates_open(mock_nidaq, scan_info_mock): - """Software-triggered scans: the monitored readout forwards the ENABLED - EPICS values into the SAME groups the hardware-triggered stream feeds.""" - dev = mock_nidaq - _enable_channels(dev) # ai0, ci3, di1, di2 + dev.ai_chans._read_pv.mock_data = 0b1 # stale hardware-scan bitmask, must be ignored dev.ai0._read_pv.mock_data = 5.0 + dev.ai1._read_pv.mock_data = 99.0 # not in the bitmask, forwarded anyway dev.ci3._read_pv.mock_data = 42 dev.di1._read_pv.mock_data = 0 dev.di2._read_pv.mock_data = 1 - dev.ai1._read_pv.mock_data = 99.0 # disabled channel, must not be forwarded + dev.energy_epics._read_pv.mock_data = 8000.0 + dev.enc_epics._read_pv.mock_data = 12.5 scan_info_mock.scan_name = "xas_simple_scan" dev.scan_parameters = scan_info_mock @@ -255,19 +244,26 @@ def test_read_forwards_enabled_epics_scalars_when_gates_open(mock_nidaq, scan_in dev.read() assert dev.mean.get() is None - # software-triggered scan -> enabled channels forwarded under the unified names + # software-triggered scan -> ALL channels forwarded under the unified names scan_info_mock.scan_name = "line_scan" dev.read() out = dev.mean.get() + assert len(out.signals) == 26 # all 8 ai + 18 ci, bitmask ignored assert out.signals["nidaq_mean_ai0"]["value"] == 5.0 + assert out.signals["nidaq_mean_ai1"]["value"] == 99.0 assert out.signals["nidaq_mean_ci3"]["value"] == 42 - assert "nidaq_mean_ai1" not in out.signals # disabled -> filtered assert out.metadata["acquisition_group"] == "monitored" - assert dev.max.get().signals["nidaq_max_di1"]["value"] == 0 - assert dev.max.get().signals["nidaq_max_di2"]["value"] == 1 - # hardware-only groups untouched in software mode + out_max = dev.max.get() + assert len(out_max.signals) == 5 + assert out_max.signals["nidaq_max_di1"]["value"] == 0 + assert out_max.signals["nidaq_max_di2"]["value"] == 1 + # energy and encoder are live standby readings -> stream group + out_stream = dev.stream.get() + assert set(out_stream.signals) == {"nidaq_stream_energy", "nidaq_stream_enc"} + assert out_stream.signals["nidaq_stream_energy"]["value"] == 8000.0 + assert out_stream.signals["nidaq_stream_enc"]["value"] == 12.5 + # no software source for standard deviations assert dev.std_dev.get() is None - assert dev.stream.get() is None # scan not open -> no forwarding dev.ai0._read_pv.mock_data = 7.0