feat(nidaq)!: adopt channel-first signal names as the final convention

This commit is contained in:
wyzula_j committed 2026-08-11 13:14:17 +02:00
1 parent f7843d055b
commit 7cbdfb5b37
3 files changed
+181 -171

No files matched your search

+110 -79
View File
@@ -30,11 +30,12 @@ if TYPE_CHECKING: # pragma: no cover
logger = bec_logger.logger
#: Channels per statistic group. Group sub-signals are emitted as
#: 'nidaq_<group>_<channel>' (e.g. 'nidaq_mean_ai0') in BOTH trigger modes.
NIDAQ_MEAN_CHANNELS = [f"ai{i}" for i in range(8)] + [f"ci{i}" for i in range(18)]
NIDAQ_MAX_CHANNELS = [f"di{i}" for i in range(5)]
NIDAQ_STD_DEV_CHANNELS = [f"ai{i}" for i in range(8)] + [f"ci{i}" for i in range(18)]
#: FINAL naming convention (channel-first, identical to the backend stream):
#: nidaq_ai0_mean, nidaq_ai0_std_dev, nidaq_di0_max, nidaq_energy, ...
#: ai/ci channels are two-signal DynamicSignal groups (mean/std_dev); every
#: other channel is a single-signal group whose emitted name IS the group name.
NIDAQ_AI_CI_CHANNELS = [f"ai{i}" for i in range(8)] + [f"ci{i}" for i in range(18)]
NIDAQ_DI_CHANNELS = [f"di{i}_max" for i in range(5)]
NIDAQ_STREAM_CHANNELS = [
"energy",
"enc",
@@ -47,34 +48,28 @@ NIDAQ_STREAM_CHANNELS = [
"xrd_timestamp",
"xrd_angle",
"xrd_energy",
"xrd_ai0_mean",
"xrd_ai0_std_dev",
"xrd_ai0",
]
#: 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"},
}
#: Backend channel name -> (group, group channel) for the unified bridge.
#: Covers both backend naming variants: with RLE the per-oscillation statistics
#: carry _mean/_std_dev/_max suffixes, without RLE the names are bare.
#: Backend channel name -> (group component, put key) for the unified bridge.
#: Two-signal groups (ai/ci) take the statistic as key; single-signal groups
#: require the FULL emitted name as key (bec_signals single-channel rename).
#: Bare ai/ci names (backend no-RLE mode) map onto the mean channel.
NIDAQ_BRIDGE_ROUTING: dict[str, tuple[str, str]] = (
{f"ai{i}_mean": ("mean", f"ai{i}") for i in range(8)}
| {f"ai{i}": ("mean", f"ai{i}") for i in range(8)}
| {f"ai{i}_std_dev": ("std_dev", f"ai{i}") for i in range(8)}
| {f"ci{i}_mean": ("mean", f"ci{i}") for i in range(18)}
| {f"ci{i}": ("mean", f"ci{i}") for i in range(18)}
| {f"ci{i}_std_dev": ("std_dev", f"ci{i}") for i in range(18)}
| {f"di{i}_max": ("max", f"di{i}") for i in range(5)}
| {f"di{i}": ("max", f"di{i}") for i in range(5)}
| {channel: ("stream", channel) for channel in NIDAQ_STREAM_CHANNELS}
| {"xrd_ai0": ("stream", "xrd_ai0_mean")}
{f"{ch}_mean": (ch, "mean") for ch in NIDAQ_AI_CI_CHANNELS}
| {f"{ch}_std_dev": (ch, "std_dev") for ch in NIDAQ_AI_CI_CHANNELS}
| {ch: (ch, "mean") for ch in NIDAQ_AI_CI_CHANNELS}
| {f"di{i}_max": (f"di{i}_max", f"nidaq_di{i}_max") for i in range(5)}
| {f"di{i}": (f"di{i}_max", f"nidaq_di{i}_max") for i in range(5)}
| {ch: (ch, f"nidaq_{ch}") for ch in NIDAQ_STREAM_CHANNELS}
)
#: Software-mode sources: (group component, put key) -> EPICS scalar attribute.
#: std_dev and the derived stream channels have no EPICS counterpart.
NIDAQ_SW_FORWARD_SOURCES: dict[tuple[str, str], str] = (
{(ch, "mean"): f"{ch}_epics" for ch in NIDAQ_AI_CI_CHANNELS}
| {(f"di{i}_max", f"nidaq_di{i}_max"): f"di{i}_epics" for i in range(5)}
| {("energy", "nidaq_energy"): "energy_epics", ("enc", "nidaq_enc"): "enc_epics"}
)
@@ -86,39 +81,39 @@ class NidaqControl(Device):
"""Nidaq control class with all PVs"""
# fmt: off
ai0 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.omitted, doc="EPICS analog input 0", auto_monitor=True)
ai1 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.omitted, doc="EPICS analog input 1", auto_monitor=True)
ai2 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.omitted, doc="EPICS analog input 2", auto_monitor=True)
ai3 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.omitted, doc="EPICS analog input 3", auto_monitor=True)
ai4 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.omitted, doc="EPICS analog input 4", auto_monitor=True)
ai5 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.omitted, doc="EPICS analog input 5", auto_monitor=True)
ai6 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.omitted, doc="EPICS analog input 6", auto_monitor=True)
ai7 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.omitted, doc="EPICS analog input 7", auto_monitor=True)
ai0_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.omitted, doc="EPICS analog input 0", auto_monitor=True)
ai1_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.omitted, doc="EPICS analog input 1", auto_monitor=True)
ai2_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.omitted, doc="EPICS analog input 2", auto_monitor=True)
ai3_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.omitted, doc="EPICS analog input 3", auto_monitor=True)
ai4_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.omitted, doc="EPICS analog input 4", auto_monitor=True)
ai5_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.omitted, doc="EPICS analog input 5", auto_monitor=True)
ai6_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.omitted, doc="EPICS analog input 6", auto_monitor=True)
ai7_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.omitted, doc="EPICS analog input 7", auto_monitor=True)
ci0 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True)
ci1 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True)
ci2 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True)
ci3 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True)
ci4 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True)
ci5 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True)
ci6 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True)
ci7 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True)
ci8 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.omitted, doc="EPICS counter input 8", auto_monitor=True)
ci9 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.omitted, doc="EPICS counter input 9", auto_monitor=True)
ci10 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True)
ci11 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True)
ci12 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True)
ci13 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True)
ci14 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True)
ci15 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True)
ci16 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True)
ci17 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True)
ci0_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True)
ci1_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True)
ci2_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True)
ci3_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True)
ci4_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True)
ci5_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True)
ci6_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True)
ci7_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True)
ci8_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.omitted, doc="EPICS counter input 8", auto_monitor=True)
ci9_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.omitted, doc="EPICS counter input 9", auto_monitor=True)
ci10_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.omitted, doc="EPICS counter input 0", auto_monitor=True)
ci11_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.omitted, doc="EPICS counter input 1", auto_monitor=True)
ci12_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.omitted, doc="EPICS counter input 2", auto_monitor=True)
ci13_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.omitted, doc="EPICS counter input 3", auto_monitor=True)
ci14_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.omitted, doc="EPICS counter input 4", auto_monitor=True)
ci15_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.omitted, doc="EPICS counter input 5", auto_monitor=True)
ci16_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.omitted, doc="EPICS counter input 6", auto_monitor=True)
ci17_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.omitted, doc="EPICS counter input 7", auto_monitor=True)
di0 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.omitted, doc="EPICS digital input 0", auto_monitor=True)
di1 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.omitted, doc="EPICS digital input 1", auto_monitor=True)
di2 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.omitted, doc="EPICS digital input 2", auto_monitor=True)
di3 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.omitted, doc="EPICS digital input 3", auto_monitor=True)
di4 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.omitted, doc="EPICS digital input 4", auto_monitor=True)
di0_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.omitted, doc="EPICS digital input 0", auto_monitor=True)
di1_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.omitted, doc="EPICS digital input 1", auto_monitor=True)
di2_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.omitted, doc="EPICS digital input 2", auto_monitor=True)
di3_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.omitted, doc="EPICS digital input 3", auto_monitor=True)
di4_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.omitted, doc="EPICS digital input 4", auto_monitor=True)
enc_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENC", kind=Kind.omitted, doc="EPICS Encoder reading", auto_monitor=True)
@@ -188,12 +183,50 @@ class Nidaq(PSIDeviceBase, NidaqControl):
progress_signal = Cpt(ProgressSignal, name="progress_signal")
# fmt: off
### Statistic groups: emitted as nidaq_<group>_<channel> in both trigger modes ###
#TODO decide on how to group signals, statistic groupd, analog/counter/digital etc.
mean = Cpt(DynamicSignal, name="mean", ndim=1, max_size=1000, signals=NIDAQ_MEAN_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Per-oscillation means (HW) / synchronous readbacks (SW)")
max = Cpt(DynamicSignal, name="max", ndim=1, max_size=1000, signals=NIDAQ_MAX_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Per-oscillation maxima (HW) / synchronous readbacks (SW)")
std_dev = Cpt(DynamicSignal, name="std_dev", ndim=1, max_size=1000, signals=NIDAQ_STD_DEV_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Per-oscillation standard deviations (HW only)")
stream = Cpt(DynamicSignal, name="stream", ndim=1, max_size=1000, signals=NIDAQ_STREAM_CHANNELS, async_update={"type": "add", "max_shape": [None]}, doc="Energy, encoder, derived and XRD channels")
### Per-channel async groups: emitted names are channel-first (nidaq_ai0_mean) ###
ai0 = Cpt(DynamicSignal, name="ai0", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai1 = Cpt(DynamicSignal, name="ai1", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai2 = Cpt(DynamicSignal, name="ai2", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai3 = Cpt(DynamicSignal, name="ai3", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai4 = Cpt(DynamicSignal, name="ai4", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai5 = Cpt(DynamicSignal, name="ai5", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai6 = Cpt(DynamicSignal, name="ai6", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ai7 = Cpt(DynamicSignal, name="ai7", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci0 = Cpt(DynamicSignal, name="ci0", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci1 = Cpt(DynamicSignal, name="ci1", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci2 = Cpt(DynamicSignal, name="ci2", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci3 = Cpt(DynamicSignal, name="ci3", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci4 = Cpt(DynamicSignal, name="ci4", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci5 = Cpt(DynamicSignal, name="ci5", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci6 = Cpt(DynamicSignal, name="ci6", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci7 = Cpt(DynamicSignal, name="ci7", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci8 = Cpt(DynamicSignal, name="ci8", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci9 = Cpt(DynamicSignal, name="ci9", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci10 = Cpt(DynamicSignal, name="ci10", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci11 = Cpt(DynamicSignal, name="ci11", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci12 = Cpt(DynamicSignal, name="ci12", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci13 = Cpt(DynamicSignal, name="ci13", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci14 = Cpt(DynamicSignal, name="ci14", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci15 = Cpt(DynamicSignal, name="ci15", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci16 = Cpt(DynamicSignal, name="ci16", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
ci17 = Cpt(DynamicSignal, name="ci17", ndim=1, max_size=1000, signals=["mean", "std_dev"], async_update={"type": "add", "max_shape": [None]})
di0_max = Cpt(DynamicSignal, name="di0_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]})
di1_max = Cpt(DynamicSignal, name="di1_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]})
di2_max = Cpt(DynamicSignal, name="di2_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]})
di3_max = Cpt(DynamicSignal, name="di3_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]})
di4_max = Cpt(DynamicSignal, name="di4_max", ndim=1, max_size=1000, signals=["max"], async_update={"type": "add", "max_shape": [None]})
energy = Cpt(DynamicSignal, name="energy", ndim=1, max_size=1000, signals=["energy"], async_update={"type": "add", "max_shape": [None]})
enc = Cpt(DynamicSignal, name="enc", ndim=1, max_size=1000, signals=["enc"], async_update={"type": "add", "max_shape": [None]})
smpl_abs = Cpt(DynamicSignal, name="smpl_abs", ndim=1, max_size=1000, signals=["smpl_abs"], async_update={"type": "add", "max_shape": [None]})
smpl_fluo = Cpt(DynamicSignal, name="smpl_fluo", ndim=1, max_size=1000, signals=["smpl_fluo"], async_update={"type": "add", "max_shape": [None]})
ref_abs = Cpt(DynamicSignal, name="ref_abs", ndim=1, max_size=1000, signals=["ref_abs"], async_update={"type": "add", "max_shape": [None]})
cisum = Cpt(DynamicSignal, name="cisum", ndim=1, max_size=1000, signals=["cisum"], async_update={"type": "add", "max_shape": [None]})
rle = Cpt(DynamicSignal, name="rle", ndim=1, max_size=1000, signals=["rle"], async_update={"type": "add", "max_shape": [None]})
xas_timestamp = Cpt(DynamicSignal, name="xas_timestamp", ndim=1, max_size=1000, signals=["xas_timestamp"], async_update={"type": "add", "max_shape": [None]})
xrd_timestamp = Cpt(DynamicSignal, name="xrd_timestamp", ndim=1, max_size=1000, signals=["xrd_timestamp"], async_update={"type": "add", "max_shape": [None]})
xrd_angle = Cpt(DynamicSignal, name="xrd_angle", ndim=1, max_size=1000, signals=["xrd_angle"], async_update={"type": "add", "max_shape": [None]})
xrd_energy = Cpt(DynamicSignal, name="xrd_energy", ndim=1, max_size=1000, signals=["xrd_energy"], async_update={"type": "add", "max_shape": [None]})
xrd_ai0 = Cpt(DynamicSignal, name="xrd_ai0", ndim=1, max_size=1000, signals=["xrd_ai0"], async_update={"type": "add", "max_shape": [None]})
# fmt: on
USER_ACCESS = ["set_config"]
@@ -279,15 +312,13 @@ class Nidaq(PSIDeviceBase, NidaqControl):
):
return
now = time.time()
for group, sources in NIDAQ_SW_FORWARD_SOURCES.items():
batch = {
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
# monitored readouts (cf. MCSCardCSAXS acquisition_group logic).
self._guarded_group_put(group, batch, acquisition_group="monitored")
buckets: dict[str, dict] = defaultdict(dict)
for (group, key), attr in NIDAQ_SW_FORWARD_SOURCES.items():
buckets[group][key] = {"value": getattr(self, attr).get(), "timestamp": now}
for group, batch in buckets.items():
# Annotate the alignment: these rows are in sync with the
# monitored readouts (cf. MCSCardCSAXS acquisition_group logic).
self._guarded_group_put(group, batch, acquisition_group="monitored")
def _guarded_group_put(
self,
@@ -391,7 +422,7 @@ class Nidaq(PSIDeviceBase, NidaqControl):
logger.info(f"Device {self.name}: unified bridge stopped")
def _forward_backend_message(self, msg) -> None:
"""Route one backend DeviceMessage into the statistic groups."""
"""Route one backend DeviceMessage into the per-channel groups."""
device_prefix = f"{self.name}_"
buckets: dict[str, dict] = defaultdict(dict)
now = time.time()
@@ -405,8 +436,8 @@ class Nidaq(PSIDeviceBase, NidaqControl):
f"Device {self.name}: skipping unknown backend channel '{signal_name}'"
)
continue
group, group_channel = route
buckets[group][group_channel] = {
group, key = route
buckets[group][key] = {
"value": data["value"],
"timestamp": data.get("timestamp") or now,
}
+12 -14
View File
@@ -236,10 +236,10 @@ class DebyeNexusStructure(DefaultFormat):
self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value")
)
# The statistic groups are written by the async writer under the device itself,
# independent of the readout priority the device is configured with. Compression
# (RLE) no longer changes the name: per-oscillation means and raw readbacks both
# arrive in the 'mean' group.
# The per-channel async groups are written by the async writer under the device
# itself, using the FINAL channel-first names (identical to the backend stream).
# Compression (RLE) no longer changes the name: the bridge maps the bare no-RLE
# channel names onto the mean signals.
nidaq_data = "/entry/collection/devices/nidaq"
measurement_mode = entry.create_group(name="mode")
@@ -303,9 +303,7 @@ class DebyeNexusStructure(DefaultFormat):
energy.attrs["NX_class"] = "NXdata"
energy.attrs["units"] = "eV"
main_data.create_soft_link(
name="energy", target=f"{nidaq_data}/nidaq_stream_energy/value"
)
main_data.create_soft_link(name="energy", target=f"{nidaq_data}/nidaq_energy/value")
##################
## i0
@@ -316,7 +314,7 @@ class DebyeNexusStructure(DefaultFormat):
i0.attrs["NX_class"] = "NXdata"
i0.attrs["units"] = "V"
main_data.create_soft_link(name="i0", target=f"{nidaq_data}/nidaq_mean_ai0/value")
main_data.create_soft_link(name="i0", target=f"{nidaq_data}/nidaq_ai0_mean/value")
##################
## i1
@@ -327,7 +325,7 @@ class DebyeNexusStructure(DefaultFormat):
i1.attrs["NX_class"] = "NXdata"
i1.attrs["units"] = "V"
main_data.create_soft_link(name="i1", target=f"{nidaq_data}/nidaq_mean_ai2/value")
main_data.create_soft_link(name="i1", target=f"{nidaq_data}/nidaq_ai2_mean/value")
##################
## i2
@@ -338,7 +336,7 @@ class DebyeNexusStructure(DefaultFormat):
i2.attrs["NX_class"] = "NXdata"
i2.attrs["units"] = "V"
main_data.create_soft_link(name="i2", target=f"{nidaq_data}/nidaq_mean_ai4/value")
main_data.create_soft_link(name="i2", target=f"{nidaq_data}/nidaq_ai4_mean/value")
##################
## ci sum
@@ -350,7 +348,7 @@ class DebyeNexusStructure(DefaultFormat):
ci_sum.attrs["units"] = "counts"
main_data.create_soft_link(
name="Fluorescence_Sum", target=f"{nidaq_data}/nidaq_stream_cisum/value"
name="Fluorescence_Sum", target=f"{nidaq_data}/nidaq_cisum/value"
)
##################
@@ -362,7 +360,7 @@ class DebyeNexusStructure(DefaultFormat):
mu_sample.attrs["NX_class"] = "NXdata"
main_data.create_soft_link(
name="mu_sample", target=f"{nidaq_data}/nidaq_stream_smpl_abs/value"
name="mu_sample", target=f"{nidaq_data}/nidaq_smpl_abs/value"
)
##################
@@ -374,7 +372,7 @@ class DebyeNexusStructure(DefaultFormat):
mu_sample.attrs["NX_class"] = "NXdata"
main_data.create_soft_link(
name="fluo_sample", target=f"{nidaq_data}/nidaq_stream_smpl_fluo/value"
name="fluo_sample", target=f"{nidaq_data}/nidaq_smpl_fluo/value"
)
##################
@@ -386,5 +384,5 @@ class DebyeNexusStructure(DefaultFormat):
mu_reference.attrs["NX_class"] = "NXdata"
main_data.create_soft_link(
name="mu_reference", target=f"{nidaq_data}/nidaq_stream_ref_abs/value"
name="mu_reference", target=f"{nidaq_data}/nidaq_ref_abs/value"
)
+59 -78
View File
@@ -184,119 +184,100 @@ def test_on_complete(mock_nidaq, scan_info_mock):
assert status.done is True
########## DynamicSignal statistic groups ##########
########## per-channel DynamicSignal groups (channel-first naming) ##########
from ophyd import Kind
from types import SimpleNamespace
from ophyd import Kind, Staged
from ophyd_devices.utils.bec_signals import DynamicSignal
def test_statistic_groups_and_omitted_epics(mock_nidaq):
"""Groups own the unified namespace; EPICS scalars are silent data sources.
DynamicSignal is deliberate (per the NIDAQ maintainers, the active channel
set cannot be predicted upfront): channel names are validated, batch
completeness is not.
"""
def test_channel_groups_and_omitted_epics(mock_nidaq):
"""Per-channel groups own the FINAL channel-first namespace; the EPICS
scalars are silent (omitted) data sources for the software forwarding."""
dev = mock_nidaq
assert isinstance(dev.mean, DynamicSignal)
assert dev.mean.strict_signal_validation is False
assert dev.mean.name == "nidaq_mean"
assert dev.max.name == "nidaq_max"
assert dev.std_dev.name == "nidaq_std_dev"
assert dev.stream.name == "nidaq_stream"
# EPICS scalars demoted: no longer in the readout namespace
assert dev.ai0.kind == Kind.omitted
assert isinstance(dev.ai0, DynamicSignal)
assert dev.ai0.name == "nidaq_ai0"
assert [name for name, _ in dev.di0_max.signals] == ["nidaq_di0_max"]
assert [name for name, _ in dev.energy.signals] == ["nidaq_energy"]
assert dev.ai0_epics.kind == Kind.omitted
assert dev.enc_epics.kind == Kind.omitted
assert "nidaq_ai0" not in dev.read()
########## software-mode forwarding into the statistic groups ##########
from types import SimpleNamespace
from ophyd import Staged
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."""
"""Software mode: ALL EPICS scalars forwarded under the channel-first names."""
dev = mock_nidaq
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.ai0_epics._read_pv.mock_data = 5.0
dev.ai1_epics._read_pv.mock_data = 99.0
dev.ci3_epics._read_pv.mock_data = 42
dev.di2_epics._read_pv.mock_data = 1
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
with mock.patch.object(dev.scan_info, "msg", SimpleNamespace(status="open")):
# not staged -> no forwarding
dev.read()
assert dev.mean.get() is None
assert dev.ai0.get() is None # not staged
dev._staged = Staged.yes
# backend-driven scan -> no forwarding (the hardware stream owns the groups)
dev.read()
assert dev.mean.get() is None
assert dev.ai0.get() is None # backend-driven scan -> hardware path owns the groups
# 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 out.metadata["acquisition_group"] == "monitored"
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
assert dev.ai0.get().signals["nidaq_ai0_mean"]["value"] == 5.0
assert dev.ai1.get().signals["nidaq_ai1_mean"]["value"] == 99.0
assert dev.ci3.get().signals["nidaq_ci3_mean"]["value"] == 42
assert dev.ai0.get().metadata["acquisition_group"] == "monitored"
assert dev.di2_max.get().signals["nidaq_di2_max"]["value"] == 1
assert dev.energy.get().signals["nidaq_energy"]["value"] == 8000.0
assert dev.enc.get().signals["nidaq_enc"]["value"] == 12.5
# no software source for standard deviations
assert dev.std_dev.get() is None
# scan not open -> no forwarding
dev.ai0._read_pv.mock_data = 7.0
with mock.patch.object(dev.scan_info, "msg", SimpleNamespace(status="closed")):
dev.read()
assert dev.mean.get().signals["nidaq_mean_ai0"]["value"] == 5.0
assert "nidaq_ai0_std_dev" not in dev.ai0.get().signals
dev._staged = Staged.no
def test_guarded_put_reports_unknown_names_once(mock_nidaq, scan_info_mock):
"""An undeclared channel name is dropped and reported once per group per scan."""
def test_bridge_routes_backend_names_to_channel_groups(mock_nidaq):
"""Backend messages (both RLE and bare no-RLE names) land under the final names."""
from bec_lib import messages
dev = mock_nidaq
dev._guarded_group_put("mean", {"bogus": {"value": [1.0], "timestamp": 1.0}})
assert dev.mean.get() is None
assert "mean" in dev._group_put_errors
# staging for the next scan resets the reporting
msg = messages.DeviceMessage(
signals={
"nidaq_ai0_mean": {"value": [1.0, 2.0]},
"nidaq_ai0_std_dev": {"value": [0.1, 0.2]},
"ci3": {"value": [7.0]}, # bare no-RLE name -> mean
"di2_max": {"value": [1.0]},
"energy": {"value": [8000.0]},
"xrd_ai0": {"value": [3.0]},
"not_a_channel": {"value": [9.0]},
},
metadata={"async_update": {"type": "add_slice", "index": 4, "max_shape": [None, 57]}},
)
dev._forward_backend_message(msg)
assert dev.ai0.get().signals["nidaq_ai0_mean"]["value"] == [1.0, 2.0]
assert dev.ai0.get().signals["nidaq_ai0_std_dev"]["value"] == [0.1, 0.2]
assert dev.ai0.get().metadata["async_update"]["type"] == "add_slice"
assert dev.ci3.get().signals["nidaq_ci3_mean"]["value"] == [7.0]
assert dev.di2_max.get().signals["nidaq_di2_max"]["value"] == [1.0]
assert dev.energy.get().signals["nidaq_energy"]["value"] == [8000.0]
assert dev.xrd_ai0.get().signals["nidaq_xrd_ai0"]["value"] == [3.0]
assert "not_a_channel" in dev._unknown_bridge_channels
def test_guarded_put_reports_unknown_names_once(mock_nidaq, scan_info_mock):
dev = mock_nidaq
dev._guarded_group_put("ai0", {"bogus": {"value": [1.0], "timestamp": 1.0}})
assert dev.ai0.get() is None
assert "ai0" in dev._group_put_errors
scan_info_mock.scan_name = "line_scan"
dev.scan_info.msg.info.update(scan_info_mock.model_dump())
dev.on_stage() # returns early: not a backend-driven scan
assert dev._group_put_errors == set()
def test_group_put_notifies_subscribers(mock_nidaq):
"""The device server publishes to the per-signal device_async_signal endpoint
from BECMessageSignal subscriptions; a put must notify subscribers."""
dev = mock_nidaq
received = []
dev.mean.subscribe(lambda *a, **kw: received.append(kw.get("value")), run=False)
dev._guarded_group_put("mean", {"ai0": {"value": 5.0, "timestamp": 1.0}})
assert len(received) == 1
assert received[0].signals["nidaq_mean_ai0"]["value"] == 5.0
def test_device_manager_is_an_explicit_init_parameter():
"""The device server injects device_manager only if it is in the signature;
without it the unified bridge has no connector and silently never starts."""