From 82ecab4c211facbebb0c7c93ab5f8137f5f99c72 Mon Sep 17 00:00:00 2001 From: x01dc Date: Mon, 31 Aug 2026 20:21:46 +0200 Subject: [PATCH 1/7] feat(flomni): add continuous free-running raw-sample mode to PandaBoxOMNY Adds an opt-in raw_stream_mode to omny_panda alongside the existing per-gate averaged burst acquisition, so a burst window can instead be captured as continuous raw samples plus a detector-gate bit and a monotonic frame counter for completeness checking. Client-side coalescing bounds the message rate to the BEC message bus regardless of PandA's own network-frame batching, and completion uses a settle-based check since an exact expected sample count isn't predictable for continuous capture. Defaults to off, preserving today's behavior exactly; hardware wiring/layout and the raw data schema are documented under docs/developer/ pending PandA hardware access to configure and validate against. Co-Authored-By: Claude Sonnet 5 --- csaxs_bec/device_configs/ptycho_flomni.yaml | 70 ++++---- csaxs_bec/devices/panda_box/panda_box_omny.py | 165 +++++++++++++++++- .../panda_box_free_running_data_schema.md | 47 +++++ .../developer/panda_box_free_running_setup.md | 122 +++++++++++++ docs/user/ptychography/flomni.md | 8 + docs/user/ptychography/omny.md | 8 + tests/tests_devices/test_panda.py | 163 +++++++++++++++++ 7 files changed, 551 insertions(+), 32 deletions(-) create mode 100644 docs/developer/panda_box_free_running_data_schema.md create mode 100644 docs/developer/panda_box_free_running_setup.md diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index c0101817..f0fb0959 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -646,33 +646,43 @@ calculated_signal: ############################################################ #################### OMNY Pandabox ######################### ############################################################ -# omny_panda: -# readoutPriority: async -# deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY -# deviceConfig: -# host: omny-panda.psi.ch -# signal_alias: -# FMC_IN.VAL1.Min: cap_voltage_fzp_y_min -# FMC_IN.VAL1.Max: cap_voltage_fzp_y_max -# FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean -# FMC_IN.VAL2.Min: cap_voltage_fzp_x_min -# FMC_IN.VAL2.Max: cap_voltage_fzp_x_max -# FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean -# INENC1.VAL.Max: interf_st_fzp_y_max -# INENC1.VAL.Mean: interf_st_fzp_y_mean -# INENC1.VAL.Min: interf_st_fzp_y_min -# INENC2.VAL.Max: interf_st_fzp_x_max -# INENC2.VAL.Mean: interf_st_fzp_x_mean -# INENC2.VAL.Min: interf_st_fzp_x_min -# INENC3.VAL.Max: interf_st_rotz_max -# INENC3.VAL.Mean: interf_st_rotz_mean -# INENC3.VAL.Min: interf_st_rotz_min -# INENC4.VAL.Max: interf_st_rotx_max -# INENC4.VAL.Mean: interf_st_rotx_mean -# INENC4.VAL.Min: interf_st_rotx_min -# PCAP.GATE_DURATION.Value: pcap_gate_duration_value -# enabled: true -# readOnly: false -# softwareTrigger: false -# deviceTags: -# - ptycho_flomni +omny_panda: + readoutPriority: async + deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY + deviceConfig: + host: omny-panda.psi.ch + # raw_stream_mode selects the free-running raw-sample acquisition mode described in + # docs/developer/panda_box_free_running_setup.md. Defaults to false (legacy per-gate + # aggregate capture, as configured below) - do not enable until the PandaBox layout has + # been reconfigured for continuous capture per that document. + raw_stream_mode: false + signal_alias: + FMC_IN.VAL1.Min: cap_voltage_fzp_y_min + FMC_IN.VAL1.Max: cap_voltage_fzp_y_max + FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean + FMC_IN.VAL2.Min: cap_voltage_fzp_x_min + FMC_IN.VAL2.Max: cap_voltage_fzp_x_max + FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean + INENC1.VAL.Max: interf_st_fzp_y_max + INENC1.VAL.Mean: interf_st_fzp_y_mean + INENC1.VAL.Min: interf_st_fzp_y_min + INENC2.VAL.Max: interf_st_fzp_x_max + INENC2.VAL.Mean: interf_st_fzp_x_mean + INENC2.VAL.Min: interf_st_fzp_x_min + INENC3.VAL.Max: interf_st_rotz_max + INENC3.VAL.Mean: interf_st_rotz_mean + INENC3.VAL.Min: interf_st_rotz_min + INENC4.VAL.Max: interf_st_rotx_max + INENC4.VAL.Mean: interf_st_rotx_mean + INENC4.VAL.Min: interf_st_rotx_min + PCAP.GATE_DURATION.Value: pcap_gate_duration_value + # Free-running mode (raw_stream_mode: true) signals. Block numbers below are + # placeholders (TODO) pending the actual PandaBox wiring/layout - see + # docs/developer/panda_box_free_running_setup.md. + PCAP.BITS0.Value: gate_detector_active + COUNTER1.OUT.Value: frame_counter + enabled: true + readOnly: false + softwareTrigger: false + deviceTags: + - ptycho_flomni diff --git a/csaxs_bec/devices/panda_box/panda_box_omny.py b/csaxs_bec/devices/panda_box/panda_box_omny.py index b8448349..ccfa82f7 100644 --- a/csaxs_bec/devices/panda_box/panda_box_omny.py +++ b/csaxs_bec/devices/panda_box/panda_box_omny.py @@ -1,11 +1,14 @@ """Module to integrate the PandaBox for cSAXS measurements.""" +import threading import time +from typing import Any from bec_lib.logger import bec_logger from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd_devices import StatusBase from ophyd_devices.devices.panda_box.panda_box import PandaBox, PandaState +from pandablocks.responses import FrameData from csaxs_bec.devices.utils.utils import fetch_scan_info @@ -13,21 +16,81 @@ logger = bec_logger.logger class PandaBoxOMNY(PandaBox): - """PandaBox integration for OMNY. This class implements OMNY specific logic for the PandaBox integration.""" + """PandaBox integration for OMNY. This class implements OMNY specific logic for the PandaBox integration. + + Two operation modes are supported, selected via ``raw_stream_mode``: + + - Legacy mode (``raw_stream_mode=False``, default): unchanged behavior. Each PCAP gate + (one per scan point / burst sub-frame) produces one aggregate statistic + (Mean/Min/Max/Sum/Diff) that is published immediately, and completion is checked + against the exact expected number of gates (``num_points * frames_per_trigger``). + - Free-running mode (``raw_stream_mode=True``): the PCAP module is expected to be + configured on the PandaBox itself to capture raw ``Value`` samples continuously at a + much higher rate for the duration of a burst window, alongside a digital gate-input bit + and a monotonic frame counter (see docs/developer/panda_box_free_running_setup.md). + Incoming samples are coalesced client-side (by row count or time, whichever comes + first) before being published, to keep the message rate to the BEC message bus + bounded regardless of how the PandaBox itself batches network frames. Completion is + checked by waiting for the hardware-reported captured-frame count to stop increasing, + since an exact expected sample count can't be predicted client-side for continuous + capture. + """ + + def __init__( + self, + *, + name: str, + host: str, + signal_alias: dict[str, str] | None = None, + scan_info: ScanServerScanInfo | None = None, + device_manager: Any | None = None, + raw_stream_mode: bool = False, + raw_stream_flush_interval: float = 0.05, + raw_stream_flush_row_count: int = 500, + **kwargs, + ) -> None: + # Free-running raw-stream configuration. Defaults preserve today's behavior: + # raw_stream_mode=False means _receive_frame_data/on_stage/on_complete are + # byte-for-byte identical to before this mode was added. + self.raw_stream_mode = raw_stream_mode + self.raw_stream_flush_interval = raw_stream_flush_interval + self.raw_stream_flush_row_count = raw_stream_flush_row_count + self._raw_stream_buffer: dict[str, dict[str, Any]] = {} + self._raw_stream_buffer_row_count = 0 + self._raw_stream_buffer_lock = threading.Lock() + self._raw_stream_last_flush = time.monotonic() + + super().__init__( + name=name, + host=host, + signal_alias=signal_alias, + scan_info=scan_info, + device_manager=device_manager, + **kwargs, + ) def on_init(self): super().on_init() self._acquisition_group = "burst" self._timeout_on_completed = 10 + self._settle_timeout_in_s = 0.2 self.scan_parameters: ScanServerScanInfo | None = None def on_stage(self): start_time = time.time() super().on_stage() self.scan_parameters = fetch_scan_info(self.scan_info) + + with self._raw_stream_buffer_lock: + self._raw_stream_buffer = {} + self._raw_stream_buffer_row_count = 0 + self._raw_stream_last_flush = time.monotonic() + # TODO, adjust as seen fit. # Adjust the acquisition group based on scan parameters if needed - if self.scan_parameters.scan_type == "hardware_triggered": + if self.raw_stream_mode: + self._acquisition_group = "free_running" + elif self.scan_parameters.scan_type == "hardware_triggered": self._acquisition_group = "fly" elif self.scan_parameters.scan_type == "software_triggered": if self.scan_parameters.frames_per_trigger == 1: @@ -37,8 +100,19 @@ class PandaBoxOMNY(PandaBox): logger.info(f"PandaBox {self.name} on_stage completed in {time.time() - start_time:.3f}s.") + def on_unstage(self): + """Make sure no buffered raw-stream data is left behind before resetting the device.""" + self._flush_raw_stream_buffer() + return super().on_unstage() + def on_complete(self): """On complete is called after the scan is complete. We need to wait for the capture to complete before we can disarm the PandaBox.""" + if self.raw_stream_mode: + return self._on_complete_free_running() + return self._on_complete_burst() + + def _on_complete_burst(self): + """Legacy completion check: wait for the exact expected number of gated captures.""" def _check_capture_complete(): captured = 0 @@ -66,6 +140,93 @@ class PandaBoxOMNY(PandaBox): self.cancel_on_stop(status_captured) return status_captured + def _on_complete_free_running(self): + """Completion check for continuous free-running capture: wait until the + hardware-reported captured-frame count stops increasing (settles), since the exact + expected sample count can't be predicted client-side for continuous capture at an + arbitrary rate. The frame_counter signal remains the authoritative record of total + frames captured for later completeness verification. + """ + + def _check_capture_settled(): + captured = -1 + last_change = time.monotonic() + start_time = time.monotonic() + try: + while True: + ret = self.send_raw("*PCAP.CAPTURED?") + new_captured = int(ret[0].split("=")[-1]) + now = time.monotonic() + if new_captured != captured: + captured = new_captured + last_change = now + elif now - last_change > self._settle_timeout_in_s: + break + if now - start_time > self._timeout_on_completed: + raise TimeoutError( + f"Pandabox {self.name} capture did not settle after {self._timeout_on_completed}s, " + f"last captured count: {captured}." + ) + time.sleep(0.01) + finally: + self._flush_raw_stream_buffer() + self._disarm() + + status_captured = self.task_handler.submit_task(_check_capture_settled, run=True) + self.cancel_on_stop(status_captured) + return status_captured + + def _receive_frame_data(self, data: FrameData) -> None: + """Callback to receive frame data from the PandaBox. + + In legacy mode this is identical to the base class: publish immediately, one + FrameData in, one `.data.put()` out. In free-running mode, converted rows are + accumulated in a buffer and only published once a row-count or time threshold is + reached, to bound the message rate to the BEC message bus. + """ + if not self.raw_stream_mode: + super()._receive_frame_data(data) + return + + converted = self.convert_frame_data(frame_data=data) + with self._raw_stream_buffer_lock: + self._merge_into_raw_stream_buffer(converted) + elapsed = time.monotonic() - self._raw_stream_last_flush + if ( + self._raw_stream_buffer_row_count >= self.raw_stream_flush_row_count + or elapsed >= self.raw_stream_flush_interval + ): + self._flush_raw_stream_buffer_locked() + + def _merge_into_raw_stream_buffer(self, converted: dict[str, dict[str, Any]]) -> None: + """Append newly converted rows into the pending raw-stream buffer. Must be called + while holding `_raw_stream_buffer_lock`.""" + row_count = 0 + for key, entry in converted.items(): + buffered = self._raw_stream_buffer.setdefault( + key, {"value": [], "timestamp": entry["timestamp"]} + ) + buffered["value"].extend(entry["value"]) + buffered["timestamp"] = entry["timestamp"] + row_count = max(row_count, len(entry["value"])) + self._raw_stream_buffer_row_count += row_count + + def _flush_raw_stream_buffer(self) -> None: + """Flush any pending buffered raw-stream data, taking the lock. Safe to call from + any thread, e.g. on_complete/on_unstage, even if nothing is buffered.""" + with self._raw_stream_buffer_lock: + self._flush_raw_stream_buffer_locked() + + def _flush_raw_stream_buffer_locked(self) -> None: + """Flush any pending buffered raw-stream data. Must be called while holding + `_raw_stream_buffer_lock`.""" + self._raw_stream_last_flush = time.monotonic() + if not self._raw_stream_buffer: + return + out, self._raw_stream_buffer = self._raw_stream_buffer, {} + self._raw_stream_buffer_row_count = 0 + self.data.put(out, acquisition_group=self._acquisition_group) + if __name__ == "__main__": import time diff --git a/docs/developer/panda_box_free_running_data_schema.md b/docs/developer/panda_box_free_running_data_schema.md new file mode 100644 index 00000000..aabddc69 --- /dev/null +++ b/docs/developer/panda_box_free_running_data_schema.md @@ -0,0 +1,47 @@ +# PandABox free-running (`raw_stream_mode`) raw data schema + +> **Status: schema for the acquisition side only.** This document defines +> what raw data `omny_panda` writes to its HDF5 async dataset when +> `raw_stream_mode: true` (see `docs/developer/panda_box_free_running_setup.md` +> for the hardware/mode setup). Reconstructing the previous +> averaged/stdev-per-position view from this raw data (binning by the +> `gate_detector_active` transitions or timestamps) is a separate, +> not-yet-planned analysis task and is intentionally out of scope here. + +## Where this data lands + +`omny_panda` is configured with `readoutPriority: async` +(`csaxs_bec/device_configs/ptycho_flomni.yaml`). Its `data` signal is a +`DynamicSignal` (`ophyd_devices/devices/panda_box/panda_box.py`) that BEC's +async file writer appends to an HDF5 dataset per scan, under this device's +async data group. In `raw_stream_mode`, each write corresponds to one +coalesced flush (see the setup doc's mode-comparison table), not one raw +PandA network frame — the row-level data below is still preserved exactly, +just batched differently for publishing efficiency. + +## Per-row fields + +Each row corresponds to one PCAP capture tick (one raw sample at the +free-running capture rate, e.g. ~20 kHz — see the setup doc for the actual +configured rate): + +| Field (BEC signal name) | Source PandA block | Meaning | +|---|---|---| +| *(raw analog/position value(s), name TBD per setup doc)* | e.g. `FMC_IN.VAL1.Value` / `INENC1.VAL.Value` | The instantaneous raw sample at this capture tick, replacing the Mode A aggregate (Mean/Min/Max) for whichever channel(s) are switched to raw capture. | +| `gate_detector_active` | `PCAP.BITS0.Value` (block TBD) | The other detectors' gate signal, sampled at this same tick: `1` while the other detectors are actively gated/acquiring, `0` otherwise. This is what a future reconstruction step would use to delimit which raw samples belong to which detector-gate window. | +| `frame_counter` | `COUNTER1.OUT.Value` (block TBD) | Monotonically incrementing hardware counter, one increment per capture tick. Used to verify data completeness after acquisition: a gap-free, strictly increasing sequence means no samples were dropped between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). | +| `PCAP.TS_TRIG.Value` (existing) | `PCAP.TS_TRIG` | Absolute/relative timestamp already captured today; usable for time-alignment against other detectors' data during future reconstruction. | +| `PCAP.GATE_DURATION.Value` (existing, aliased as `pcap_gate_duration_value`) | `PCAP.GATE_DURATION` | Existing field, retained; less meaningful once the gate is held open continuously for the whole burst window rather than pulsed per point (see setup doc, Mode B gate/arm row) — re-evaluate whether this still needs to be captured once real hardware is available. | + +All rows across a burst window share the same set of fields; there is no +per-point boundary marker in this raw stream other than what can be derived +from `gate_detector_active` transitions and/or absolute timestamps — no +explicit "scan point index" field is captured by the PandA itself. + +## What this does *not* include + +- No on-the-fly or post-scan computation of Mean/Min/Max/stdev from the raw + samples — the raw stream is the complete record; deriving the previous + per-point statistics from it is a separate future task. +- No changes to how Mode A (legacy) data is written — this schema only + applies when `raw_stream_mode: true`. diff --git a/docs/developer/panda_box_free_running_setup.md b/docs/developer/panda_box_free_running_setup.md new file mode 100644 index 00000000..a225228a --- /dev/null +++ b/docs/developer/panda_box_free_running_setup.md @@ -0,0 +1,122 @@ +# PandABox (`omny_panda`) burst acquisition: two operation modes + +> **Status: design + software implemented, hardware not yet configured or +> tested.** The PandA layout changes and target sample rate below are +> placeholders (marked TODO) until real PandA hardware is available to +> configure and measure against. This document should be updated with actual +> values once that happens, per the flomni burst-acquisition redesign plan. + +## Context + +flomni burst scans acquire `frames_per_trigger` sub-frames per scan point. +Historically, the PandABox (`omny_panda`, device class `PandaBoxOMNY` in +`csaxs_bec/devices/panda_box/panda_box_omny.py`) reports one PCAP-aggregated +statistic (Mean/Min/Max/Sum/Diff) per gate, discarding the time structure +within a gate. This document describes the two supported operation modes — +the existing per-gate aggregate mode, and a new continuous free-running raw +mode — and how the PandA hardware and `ptycho_flomni.yaml` device config must +be set up for each. + +Mode selection is a single software flag (`raw_stream_mode` in the device +config); it does **not** by itself reconfigure the PandA hardware. The PandA +layout must independently match the intended mode (see "Open question" at +the end) before that mode can be used. + +## Mode comparison + +| | Mode A: legacy (default) | Mode B: free-running (new) | +|---|---|---| +| Selected by | `raw_stream_mode` unset / `false` | `raw_stream_mode: true` | +| PCAP capture fields | `Mean`/`Min`/`Max`/`Sum`/`Diff` per gate | `Value` (raw instantaneous sample) + gate bit + frame counter | +| Gate/arm timing | Pulsed per point / sub-frame (rate = scan rate × `frames_per_trigger`) | PCAP held open continuously for the burst window, free-running at a high internal sample rate (target: **TODO**, e.g. ~20 kHz) | +| `_acquisition_group` (BEC async metadata) | `fly` / `monitored` / `burst`, chosen from `scan_type`/`frames_per_trigger` | `free_running` | +| Message publishing | One `.data.put()` per PandA network frame, unchanged | Coalesced client-side: flushed every `raw_stream_flush_row_count` rows or `raw_stream_flush_interval` seconds, whichever comes first (mitigates Redis/HDF5 message-rate risk — see the acquisition plan's data-path notes) | +| Completion check (`on_complete`) | Exact count: waits for `*PCAP.CAPTURED?` to reach `num_points * frames_per_trigger` | Settle-based: waits for `*PCAP.CAPTURED?` to stop changing for `_settle_timeout_in_s`, since an exact expected sample count can't be predicted client-side for continuous capture | +| Data completeness verification | Implicit in the exact-count completion check | The `frame_counter` signal (Mode B only) — a monotonically incrementing hardware counter — is the record to check for gaps/drops after the fact | + +Both modes are implemented in the same `PandaBoxOMNY` class; `raw_stream_mode` +defaults to `false` so any device config that doesn't set it keeps today's +exact behavior unchanged. + +## Physical wiring (Mode B only) + +**TODO**: which PandA digital input connector/pin the other detectors' gate +TTL signal is patched into. Fill in once the cabling is done. + +## Block configuration + +### Mode A (legacy) — reference baseline + +The blocks already configured and aliased today, for reference / so this +layout can be restored unambiguously (see `ptycho_flomni.yaml`): + +- `FMC_IN.VAL1`/`FMC_IN.VAL2` (capacitive sensor analog inputs) — `Min`, + `Max`, `Mean` capture fields. +- `INENC1`-`INENC4.VAL` (position-capture encoder inputs) — `Min`, `Max`, + `Mean` capture fields. +- `PCAP.GATE_DURATION` — `Value`. +- PCAP gate is pulsed once per point / sub-frame by the existing external + trigger electronics (unchanged by this work). + +### Mode B (new) — free-running raw capture + +- **Gate-bit input**: the other detectors' gate signal (wired above) → + **TODO: which `PCAP.BITSx` block and bit position** — set + `CAPTURE=Value` so every captured row records the instantaneous gate + state (0/1). +- **Frame counter**: dedicate **TODO: which `COUNTERx.OUT` block** (any of + `COUNTER1`-`COUNTER8` not already in use) to free-run, incrementing once + per PCAP capture tick, with `CAPTURE=Value`. This is the + data-completeness signal (`frame_counter` in `signal_alias`, see below). +- **Raw analog/position capture**: switch **TODO: which of + `FMC_IN.VAL1/VAL2`/`INENC1`-`4.VAL`** from aggregate capture to + `CAPTURE=Value`, so the raw time series is recorded instead of (or in + addition to) the per-gate statistics. +- **Continuous gate/arm**: instead of pulsing PCAP per point, keep the gate + open for the whole burst window (armed once per point-group / scan) so + PCAP free-runs at the target sample rate — **TODO: confirm target rate** + (design target: ~20 kHz) and how the continuous gate is generated (e.g. a + PandA-internal clock/pulse-generator block feeding PCAP's gate input, + rather than the external per-point trigger used in Mode A). + +## Saving / restoring layouts + +Use `ophyd_devices/devices/panda_box/utility_scripts.py` to save the current +PandA configuration to a `.ini` layout file and reload it later, so a +configuration survives a PandA power cycle. **TODO**: once Mode B's block +configuration is finalized against real hardware, save it as a named layout +(e.g. `flomni_panda_free_running.ini`) alongside the existing Mode A layout, +and record the exact save/load commands used here. + +## Signal alias mapping + +The physical PandA block feeding each BEC signal name, kept here and in +`csaxs_bec/device_configs/ptycho_flomni.yaml` (`omny_panda.deviceConfig.signal_alias`) +in sync as a single source of truth: + +| PandA block | Mode | BEC signal name | +|---|---|---| +| `FMC_IN.VAL1.Min`/`.Max`/`.Mean` | A | `cap_voltage_fzp_y_min`/`_max`/`_mean` | +| `FMC_IN.VAL2.Min`/`.Max`/`.Mean` | A | `cap_voltage_fzp_x_min`/`_max`/`_mean` | +| `INENC1.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_fzp_y_min`/`_max`/`_mean` | +| `INENC2.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_fzp_x_min`/`_max`/`_mean` | +| `INENC3.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_rotz_min`/`_max`/`_mean` | +| `INENC4.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_rotx_min`/`_max`/`_mean` | +| `PCAP.GATE_DURATION.Value` | A | `pcap_gate_duration_value` | +| `PCAP.BITS0.Value` (**TODO: confirm bit block**) | B | `gate_detector_active` | +| `COUNTER1.OUT.Value` (**TODO: confirm counter block**) | B | `frame_counter` | + +The BEC-side data schema produced by Mode B (what ends up in the HDF5 async +dataset) is documented separately in +`docs/developer/panda_box_free_running_data_schema.md`. + +## Open question to resolve once hardware is available + +Can both modes coexist under a single static PandA layout — i.e. is mode +switching purely the `raw_stream_mode` software flag, with the PandA always +capturing both the aggregate and raw fields simultaneously — or do Mode A +and Mode B require two distinct saved layouts that must be loaded before a +scan depending on the desired mode? If two layouts are required, +`raw_stream_mode` alone is necessary but not sufficient to switch modes, and +the operator must also load the matching PandA layout beforehand — update +this document with the answer and, if applicable, the load procedure. diff --git a/docs/user/ptychography/flomni.md b/docs/user/ptychography/flomni.md index 6e3f389d..99b2e714 100644 --- a/docs/user/ptychography/flomni.md +++ b/docs/user/ptychography/flomni.md @@ -233,6 +233,14 @@ The basic scan function can be called by `scans.flomni_fermat_scan()` and offers Example: `scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01, frames_per_trigger=1)` +`frames_per_trigger` controls burst acquisition for detectors generally. +Separately, the `omny_panda` PandABox device has its own `raw_stream_mode` +device-config flag (`csaxs_bec/device_configs/ptycho_flomni.yaml`) that +switches it between the default per-gate averaged acquisition and a +continuous, high-rate raw-sample acquisition mode — see +`docs/developer/panda_box_free_running_setup.md` for what each mode captures +and how to enable it. + #### Overview of the alignment steps There are several corrections applied to maintain the sample in the FOV: diff --git a/docs/user/ptychography/omny.md b/docs/user/ptychography/omny.md index e1739929..052de3c5 100644 --- a/docs/user/ptychography/omny.md +++ b/docs/user/ptychography/omny.md @@ -338,6 +338,14 @@ The basic scan function can be called by `scans.omny_fermat_scan()` and offers a Example: `scans.omny_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01, frames_per_trigger=1, readout_time=0)` +`frames_per_trigger` controls burst acquisition for detectors generally. +Separately, the `omny_panda` PandABox device has its own `raw_stream_mode` +device-config flag (`csaxs_bec/device_configs/ptycho_flomni.yaml`) that +switches it between the default per-gate averaged acquisition and a +continuous, high-rate raw-sample acquisition mode — see +`docs/developer/panda_box_free_running_setup.md` for what each mode captures +and how to enable it. + #### Overview of the alignment steps There are several corrections applied to maintain the sample in the FOV: diff --git a/tests/tests_devices/test_panda.py b/tests/tests_devices/test_panda.py index 57afc8d1..90dd3d84 100644 --- a/tests/tests_devices/test_panda.py +++ b/tests/tests_devices/test_panda.py @@ -5,14 +5,26 @@ from __future__ import annotations from unittest import mock +import numpy as np import pytest from ophyd import Staged +from pandablocks.responses import FrameData from csaxs_bec.devices.panda_box.panda_box import PandaBoxCSAXS from csaxs_bec.devices.panda_box.panda_box_omny import PandaBoxOMNY from csaxs_bec.devices.utils.utils import fetch_scan_info +def _make_frame_data(n_rows: int, start: int = 0) -> FrameData: + """Build a synthetic FrameData batch with `n_rows` rows for the frame_counter / + gate_detector_active signals used by the raw-stream tests below.""" + data = np.array( + [(np.float64(i), np.float64(i % 2)) for i in range(start, start + n_rows)], + dtype=[("COUNTER1.OUT.Value", "= 5, flush triggered + + out = mock_put.call_args.args[0] + assert [v for v in out["frame_counter"]["value"]] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + assert [v for v in out["gate_detector_active"]["value"]] == [0.0, 1.0, 0.0, 1.0, 0.0, 1.0] + + +def test_panda_omny_raw_stream_flush_on_demand(panda_omny_raw_stream): + """Buffered data below the threshold must still be flushed on demand (as on_complete/ + on_unstage do), and flushing an empty buffer must not publish again.""" + dev = panda_omny_raw_stream + with mock.patch.object(dev.data, "put") as mock_put: + dev._receive_frame_data(_make_frame_data(2, start=0)) + mock_put.assert_not_called() + + dev._flush_raw_stream_buffer() + mock_put.assert_called_once() + out = mock_put.call_args.args[0] + assert len(out["frame_counter"]["value"]) == 2 + + dev._flush_raw_stream_buffer() + mock_put.assert_called_once() # still 1: nothing left to flush + + +def test_panda_omny_unstage_flushes_pending_raw_stream_data(panda_omny_raw_stream): + dev = panda_omny_raw_stream + dev.stage() + with ( + mock.patch.object(dev.data, "put") as mock_put, + mock.patch.object(dev, "_disarm", return_value=None), + ): + dev._receive_frame_data(_make_frame_data(2, start=0)) + mock_put.assert_not_called() + + dev.unstage() + mock_put.assert_called_once() + + +def test_panda_omny_complete_free_running_settles(panda_omny_raw_stream): + """Completion in free-running mode waits for the captured count to stop increasing, + rather than an exact expected count.""" + dev = panda_omny_raw_stream + dev.scan_info.msg.info["num_points"] = 1 + dev.scan_info.msg.info["frames_per_trigger"] = 1 + dev.scan_parameters = fetch_scan_info(dev.scan_info) + dev._timeout_on_completed = 5 + dev._settle_timeout_in_s = 0.1 + + call_count = {"n": 0} + + def _send_raw(*args, **kwargs): + # Ramp up for the first 3 calls, then plateau indefinitely at "=2" so the + # settle-timeout branch (rather than running out of a fixed side_effect list) is + # what ends the loop. + call_count["n"] += 1 + return [f"={min(call_count['n'] - 1, 2)}"] + + with ( + mock.patch.object(dev, "send_raw", side_effect=_send_raw), + mock.patch.object(dev, "_disarm", return_value=None) as mock_disarm, + mock.patch.object(dev, "_flush_raw_stream_buffer") as mock_flush, + ): + status = dev.on_complete() + status.wait(timeout=4) + assert status.done is True + assert status.success is True + mock_disarm.assert_called_once() + mock_flush.assert_called_once() + + +def test_panda_omny_complete_free_running_timeout(panda_omny_raw_stream): + """If the captured count never settles, the overall timeout must still fire.""" + dev = panda_omny_raw_stream + dev.scan_info.msg.info["num_points"] = 1 + dev.scan_info.msg.info["frames_per_trigger"] = 1 + dev.scan_parameters = fetch_scan_info(dev.scan_info) + dev._timeout_on_completed = 0.3 + dev._settle_timeout_in_s = 10 # never settles within the overall timeout + + with ( + mock.patch.object(dev, "send_raw", side_effect=lambda *a, **k: ["=0"]), + mock.patch.object(dev, "_disarm", return_value=None) as mock_disarm, + ): + status = dev.on_complete() + with pytest.raises(TimeoutError): + status.wait(timeout=4) + mock_disarm.assert_called_once() + + +def test_panda_omny_raw_stream_signal_alias(panda_omny_raw_stream): + all_signal_names = [name for name, _ in panda_omny_raw_stream.data.signals] + assert "gate_detector_active" in all_signal_names + assert "frame_counter" in all_signal_names + assert "PCAP.BITS0.Value" not in all_signal_names + assert "COUNTER1.OUT.Value" not in all_signal_names + + def test_panda_csaxs(panda_csaxs): assert panda_csaxs.name == "panda_csaxs" assert panda_csaxs.host == "csaxs-panda-box.psi.ch" -- 2.54.0 From 3012cd31548f8be5137cb2521c11bc6a13edde09 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 15 Sep 2026 14:13:22 +0200 Subject: [PATCH 2/7] fix(flomni): mask gate_detector_active to its own bit, record confirmed PandA wiring TTLIN1 (detector trigger) drives both COUNTER1 (frame_counter) and the PCAP.BITS0 gate-bit capture, confirmed 2026-09-15 against the omny-panda hardware via TTLIN1.VAL.CAPTURE_WORD?/OFFSET? (PCAP.BITS0, bit 0). PCAP.BITS0 is a shared 32-bit word -- other captured bit_out signals may occupy other bit positions of it, so the raw word is not itself a clean 0/1. PandaBoxOMNY now masks gate_detector_active down to bit 0 before publishing it, instead of trusting the whole word. Also records the confirmed free-running setup in the developer docs: all four encoders plus both FMC_IN channels switched to raw Value capture, CLOCK1 driving continuous PCAP gate/trig, and the initial 100 Hz test rate. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/device_configs/ptycho_flomni.yaml | 7 +- csaxs_bec/devices/panda_box/panda_box_omny.py | 23 ++++ .../panda_box_free_running_data_schema.md | 21 ++-- .../developer/panda_box_free_running_setup.md | 105 ++++++++++++------ tests/tests_devices/test_panda.py | 19 ++++ 5 files changed, 129 insertions(+), 46 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index f0fb0959..5a51d42a 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -676,9 +676,10 @@ omny_panda: INENC4.VAL.Mean: interf_st_rotx_mean INENC4.VAL.Min: interf_st_rotx_min PCAP.GATE_DURATION.Value: pcap_gate_duration_value - # Free-running mode (raw_stream_mode: true) signals. Block numbers below are - # placeholders (TODO) pending the actual PandaBox wiring/layout - see - # docs/developer/panda_box_free_running_setup.md. + # Free-running mode (raw_stream_mode: true) signals. TTLIN1 (detector trigger) feeds + # both blocks below - confirmed 2026-09-15 against the omny-panda hardware, see + # docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 is a shared 32-bit word; + # PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before publishing gate_detector_active. PCAP.BITS0.Value: gate_detector_active COUNTER1.OUT.Value: frame_counter enabled: true diff --git a/csaxs_bec/devices/panda_box/panda_box_omny.py b/csaxs_bec/devices/panda_box/panda_box_omny.py index ccfa82f7..e48d4770 100644 --- a/csaxs_bec/devices/panda_box/panda_box_omny.py +++ b/csaxs_bec/devices/panda_box/panda_box_omny.py @@ -36,6 +36,17 @@ class PandaBoxOMNY(PandaBox): capture. """ + # PCAP.BITS0 is a shared 32-bit capture word; other captured bit_out signals (e.g. other + # TTLIN/LVDSIN/encoder bits also flagged for capture) may share it at different bit + # offsets, so the raw word is not itself a clean 0/1. TTLIN1.VAL (the detector-trigger + # gate signal, aliased to _GATE_SIGNAL_NAME below) was confirmed at bit offset 0 of + # PCAP.BITS0 on the omny-panda hardware on 2026-09-15 via + # `TTLIN1.VAL.CAPTURE_WORD?`/`TTLIN1.VAL.OFFSET?` (see + # docs/developer/panda_box_free_running_setup.md). Re-verify with the same query if the + # PandA layout is ever rebuilt. + _GATE_SIGNAL_NAME = "gate_detector_active" + _GATE_BIT_OFFSET = 0 + def __init__( self, *, @@ -176,6 +187,18 @@ class PandaBoxOMNY(PandaBox): self.cancel_on_stop(status_captured) return status_captured + def convert_frame_data(self, frame_data: FrameData) -> dict[str, Any]: + """Same as the base class, but in raw_stream_mode additionally masks + ``gate_detector_active`` down to its own bit within the shared PCAP.BITS0 capture + word -- see the class-level note on ``_GATE_BIT_OFFSET`` for why the raw word can't + be trusted as a clean 0/1 on its own. + """ + out = super().convert_frame_data(frame_data) + if self.raw_stream_mode and self._GATE_SIGNAL_NAME in out: + entry = out[self._GATE_SIGNAL_NAME] + entry["value"] = [(int(v) >> self._GATE_BIT_OFFSET) & 1 for v in entry["value"]] + return out + def _receive_frame_data(self, data: FrameData) -> None: """Callback to receive frame data from the PandaBox. diff --git a/docs/developer/panda_box_free_running_data_schema.md b/docs/developer/panda_box_free_running_data_schema.md index aabddc69..1151918e 100644 --- a/docs/developer/panda_box_free_running_data_schema.md +++ b/docs/developer/panda_box_free_running_data_schema.md @@ -3,10 +3,11 @@ > **Status: schema for the acquisition side only.** This document defines > what raw data `omny_panda` writes to its HDF5 async dataset when > `raw_stream_mode: true` (see `docs/developer/panda_box_free_running_setup.md` -> for the hardware/mode setup). Reconstructing the previous -> averaged/stdev-per-position view from this raw data (binning by the -> `gate_detector_active` transitions or timestamps) is a separate, -> not-yet-planned analysis task and is intentionally out of scope here. +> for the hardware/mode setup, confirmed against real PandA hardware +> 2026-09-15). Reconstructing the previous averaged/stdev-per-position view +> from this raw data (binning by the `gate_detector_active` transitions or +> timestamps) is a separate, not-yet-planned analysis task and is +> intentionally out of scope here. ## Where this data lands @@ -22,16 +23,16 @@ just batched differently for publishing efficiency. ## Per-row fields Each row corresponds to one PCAP capture tick (one raw sample at the -free-running capture rate, e.g. ~20 kHz — see the setup doc for the actual -configured rate): +free-running capture rate — `CLOCK1`'s configured rate, started at 100 Hz for +initial testing on 2026-09-15, see the setup doc for the current rate): | Field (BEC signal name) | Source PandA block | Meaning | |---|---|---| -| *(raw analog/position value(s), name TBD per setup doc)* | e.g. `FMC_IN.VAL1.Value` / `INENC1.VAL.Value` | The instantaneous raw sample at this capture tick, replacing the Mode A aggregate (Mean/Min/Max) for whichever channel(s) are switched to raw capture. | -| `gate_detector_active` | `PCAP.BITS0.Value` (block TBD) | The other detectors' gate signal, sampled at this same tick: `1` while the other detectors are actively gated/acquiring, `0` otherwise. This is what a future reconstruction step would use to delimit which raw samples belong to which detector-gate window. | -| `frame_counter` | `COUNTER1.OUT.Value` (block TBD) | Monotonically incrementing hardware counter, one increment per capture tick. Used to verify data completeness after acquisition: a gap-free, strictly increasing sequence means no samples were dropped between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). | +| `cap_voltage_fzp_y`/`cap_voltage_fzp_x` etc. (raw analog/encoder value) | `FMC_IN.VAL1.Value` / `FMC_IN.VAL2.Value` / `INENC1`-`4.VAL.Value` | The instantaneous raw sample at this capture tick, replacing the Mode A aggregate (Mean/Min/Max). All six channels (both FMC analog inputs and all four encoders) are switched to raw capture together on this layout — not a subset. | +| `gate_detector_active` | `PCAP.BITS0.Value`, bit 0 (`TTLIN1.VAL`), masked in software — see setup doc | The detector-trigger signal, sampled at this same tick: `1` while a detector trigger is active, `0` otherwise. `PCAP.BITS0` is a shared 32-bit word; `PandaBoxOMNY` masks it down to bit 0 before this reaches the HDF5 file (see `_GATE_BIT_OFFSET` in `panda_box_omny.py`) — the raw word itself is not a clean 0/1. This is what a future reconstruction step would use to delimit which raw samples belong to which detector-gate window. | +| `frame_counter` | `COUNTER1.OUT.Value`, free-running off `TTLIN1.VAL` (i.e. increments once per detector trigger pulse, not once per PCAP capture tick as originally guessed here) | Monotonically incrementing hardware counter. Used to verify data completeness after acquisition: a gap-free, strictly increasing sequence means no detector triggers were missed between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). Because it counts detector triggers rather than capture ticks, it verifies detector-frame completeness directly rather than raw-sample completeness. | | `PCAP.TS_TRIG.Value` (existing) | `PCAP.TS_TRIG` | Absolute/relative timestamp already captured today; usable for time-alignment against other detectors' data during future reconstruction. | -| `PCAP.GATE_DURATION.Value` (existing, aliased as `pcap_gate_duration_value`) | `PCAP.GATE_DURATION` | Existing field, retained; less meaningful once the gate is held open continuously for the whole burst window rather than pulsed per point (see setup doc, Mode B gate/arm row) — re-evaluate whether this still needs to be captured once real hardware is available. | +| `PCAP.GATE_DURATION.Value` (existing, aliased as `pcap_gate_duration_value`) | `PCAP.GATE_DURATION` | Existing field, retained; less meaningful once the gate is held open continuously for the whole burst window rather than pulsed per point (see setup doc, Mode B gate/arm row) — re-evaluate whether this still needs to be captured now that real hardware testing has started. | All rows across a burst window share the same set of fields; there is no per-point boundary marker in this raw stream other than what can be derived diff --git a/docs/developer/panda_box_free_running_setup.md b/docs/developer/panda_box_free_running_setup.md index a225228a..6775d3c8 100644 --- a/docs/developer/panda_box_free_running_setup.md +++ b/docs/developer/panda_box_free_running_setup.md @@ -1,10 +1,11 @@ # PandABox (`omny_panda`) burst acquisition: two operation modes -> **Status: design + software implemented, hardware not yet configured or -> tested.** The PandA layout changes and target sample rate below are -> placeholders (marked TODO) until real PandA hardware is available to -> configure and measure against. This document should be updated with actual -> values once that happens, per the flomni burst-acquisition redesign plan. +> **Status: software implemented, Mode B layout wired and under initial +> testing on real PandA hardware (2026-09-15, omny-panda.psi.ch).** The block +> assignments below reflect what is actually loaded on the OMNY PandA design +> as of that date. The target free-running rate is still being ramped up +> (started at 100 Hz for initial testing); update the rate row once a final +> operating rate is settled. ## Context @@ -28,7 +29,7 @@ the end) before that mode can be used. |---|---|---| | Selected by | `raw_stream_mode` unset / `false` | `raw_stream_mode: true` | | PCAP capture fields | `Mean`/`Min`/`Max`/`Sum`/`Diff` per gate | `Value` (raw instantaneous sample) + gate bit + frame counter | -| Gate/arm timing | Pulsed per point / sub-frame (rate = scan rate × `frames_per_trigger`) | PCAP held open continuously for the burst window, free-running at a high internal sample rate (target: **TODO**, e.g. ~20 kHz) | +| Gate/arm timing | Pulsed per point / sub-frame (rate = scan rate × `frames_per_trigger`) | PCAP held open continuously for the burst window, free-running at `CLOCK1`'s configured rate (started at **100 Hz** for initial testing on 2026-09-15; will be ramped up as testing progresses) | | `_acquisition_group` (BEC async metadata) | `fly` / `monitored` / `burst`, chosen from `scan_type`/`frames_per_trigger` | `free_running` | | Message publishing | One `.data.put()` per PandA network frame, unchanged | Coalesced client-side: flushed every `raw_stream_flush_row_count` rows or `raw_stream_flush_interval` seconds, whichever comes first (mitigates Redis/HDF5 message-rate risk — see the acquisition plan's data-path notes) | | Completion check (`on_complete`) | Exact count: waits for `*PCAP.CAPTURED?` to reach `num_points * frames_per_trigger` | Settle-based: waits for `*PCAP.CAPTURED?` to stop changing for `_settle_timeout_in_s`, since an exact expected sample count can't be predicted client-side for continuous capture | @@ -40,8 +41,10 @@ exact behavior unchanged. ## Physical wiring (Mode B only) -**TODO**: which PandA digital input connector/pin the other detectors' gate -TTL signal is patched into. Fill in once the cabling is done. +The detector-trigger signal is patched into **TTL input 1** (`TTLIN1`) on the +PandA front panel. That same `TTLIN1.VAL` signal feeds both downstream +consumers (see "Block configuration" below): it is the source counted by +`COUNTER1` (the frame counter) and the source captured into the gate bit. ## Block configuration @@ -60,24 +63,50 @@ layout can be restored unambiguously (see `ptycho_flomni.yaml`): ### Mode B (new) — free-running raw capture -- **Gate-bit input**: the other detectors' gate signal (wired above) → - **TODO: which `PCAP.BITSx` block and bit position** — set - `CAPTURE=Value` so every captured row records the instantaneous gate - state (0/1). -- **Frame counter**: dedicate **TODO: which `COUNTERx.OUT` block** (any of - `COUNTER1`-`COUNTER8` not already in use) to free-run, incrementing once - per PCAP capture tick, with `CAPTURE=Value`. This is the - data-completeness signal (`frame_counter` in `signal_alias`, see below). -- **Raw analog/position capture**: switch **TODO: which of - `FMC_IN.VAL1/VAL2`/`INENC1`-`4.VAL`** from aggregate capture to - `CAPTURE=Value`, so the raw time series is recorded instead of (or in - addition to) the per-gate statistics. -- **Continuous gate/arm**: instead of pulsing PCAP per point, keep the gate - open for the whole burst window (armed once per point-group / scan) so - PCAP free-runs at the target sample rate — **TODO: confirm target rate** - (design target: ~20 kHz) and how the continuous gate is generated (e.g. a - PandA-internal clock/pulse-generator block feeding PCAP's gate input, - rather than the external per-point trigger used in Mode A). +- **Gate-bit input**: the detector-trigger signal, wired into `TTLIN1` (see + "Physical wiring" above). `TTLIN1.VAL` is captured into **`PCAP.BITS0`, + bit offset 0** — confirmed 2026-09-15 against the omny-panda hardware via: + + ``` + TTLIN1.VAL.CAPTURE_WORD? -> OK =PCAP.BITS0 + TTLIN1.VAL.OFFSET? -> OK =0 + ``` + + `PCAP.BITS0` is a shared 32-bit capture word — other captured `bit_out` + signals (several TTLIN/LVDSIN/encoder bits are also flagged for capture on + this layout) may occupy other bit positions of the same word, so the raw + `PCAP.BITS0.Value` is **not** itself a clean 0/1. `PandaBoxOMNY` masks it + down to bit 0 in `raw_stream_mode` (see `_GATE_BIT_OFFSET` in + `panda_box_omny.py`) before publishing it as `gate_detector_active` — treat + that masking as required, not optional, unless it's confirmed nothing else + shares `BITS0`. Re-run the query above (read-only, safe against live + hardware) if the PandA layout is ever rebuilt, since bit assignment is + fixed by the FPGA build and not guaranteed stable across rebuilds. +- **Frame counter**: `COUNTER1`, free-running off `TTLIN1.VAL` as its + `trig` input (see layout below) — i.e. it increments once per detector + trigger pulse, not once per PCAP capture tick as originally guessed here. + Captured with `CAPTURE=Value`; this is the data-completeness signal + (`frame_counter` in `signal_alias`, see below). +- **Raw analog/position capture**: `FMC_IN.VAL1`, `FMC_IN.VAL2` and all four + `INENC1`-`4.VAL` are switched from aggregate capture to `CAPTURE=Value` + on this layout (confirmed 2026-09-15) — i.e. every analog/encoder channel + goes raw together, not a subset. This means the current PandA layout + cannot simultaneously produce Mode A's aggregate (Min/Max/Mean) data — see + "Open question" below. +- **Continuous gate/arm**: `CLOCK1`, enabled by the constant `ONE`, drives + `PCAP`'s `gate` and `trig` inputs continuously (instead of the external + per-point trigger used in Mode A), so PCAP free-runs at `CLOCK1`'s + configured rate. Testing started at **100 Hz** on 2026-09-15 and will be + ramped up; there is no PandA-side layout change needed to change the + rate, only `CLOCK1`'s period. + + ``` + TTLIN1 (TTL input 1, detector trigger) + ├──▶ COUNTER1.trig (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value) + └──▶ captured into PCAP.BITS0 bit 0 ──▶ masked in software = gate_detector_active + + CLOCK1 (enabled by constant ONE) ──▶ PCAP.gate, PCAP.trig (continuous arm/capture) + ``` ## Saving / restoring layouts @@ -103,20 +132,30 @@ in sync as a single source of truth: | `INENC3.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_rotz_min`/`_max`/`_mean` | | `INENC4.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_rotx_min`/`_max`/`_mean` | | `PCAP.GATE_DURATION.Value` | A | `pcap_gate_duration_value` | -| `PCAP.BITS0.Value` (**TODO: confirm bit block**) | B | `gate_detector_active` | -| `COUNTER1.OUT.Value` (**TODO: confirm counter block**) | B | `frame_counter` | +| `PCAP.BITS0.Value` (bit 0 = `TTLIN1.VAL`, confirmed 2026-09-15; masked in software, see above) | B | `gate_detector_active` | +| `COUNTER1.OUT.Value` (confirmed 2026-09-15, free-running off `TTLIN1.VAL`) | B | `frame_counter` | The BEC-side data schema produced by Mode B (what ends up in the HDF5 async dataset) is documented separately in `docs/developer/panda_box_free_running_data_schema.md`. -## Open question to resolve once hardware is available +## Open question, now partially resolved Can both modes coexist under a single static PandA layout — i.e. is mode switching purely the `raw_stream_mode` software flag, with the PandA always capturing both the aggregate and raw fields simultaneously — or do Mode A and Mode B require two distinct saved layouts that must be loaded before a -scan depending on the desired mode? If two layouts are required, -`raw_stream_mode` alone is necessary but not sufficient to switch modes, and -the operator must also load the matching PandA layout beforehand — update -this document with the answer and, if applicable, the load procedure. +scan depending on the desired mode? + +As currently wired for testing (2026-09-15), the answer looks like **two +distinct layouts are required**: `FMC_IN.VAL1/VAL2` and `INENC1`-`4.VAL` are +each set to `CAPTURE=Value` (single selection), not simultaneously +`CAPTURE=Value,Min,Max,Mean` — so this layout cannot also produce Mode A's +aggregate statistics at the same time. This has not yet been deliberately +tested against the alternative (whether the PandA capture UI actually +supports selecting `Value` together with `Min`/`Max`/`Mean` on the same +field) — if that turns out to work, a single layout could serve both modes. +Once Mode B testing is far enough along to save a named layout (see +"Saving / restoring layouts" below), settle this explicitly and record the +answer plus, if two layouts are needed, the load procedure to switch between +them. diff --git a/tests/tests_devices/test_panda.py b/tests/tests_devices/test_panda.py index 90dd3d84..cc3d1a79 100644 --- a/tests/tests_devices/test_panda.py +++ b/tests/tests_devices/test_panda.py @@ -197,6 +197,25 @@ def test_panda_omny_raw_stream_coalesces_by_row_count(panda_omny_raw_stream): assert [v for v in out["gate_detector_active"]["value"]] == [0.0, 1.0, 0.0, 1.0, 0.0, 1.0] +def test_panda_omny_raw_stream_gate_bit_is_masked_from_shared_word(panda_omny_raw_stream): + """PCAP.BITS0 is a 32-bit word shared with other captured bit_out signals; only bit 0 + (TTLIN1.VAL, confirmed via CAPTURE_WORD?/OFFSET? against the omny-panda hardware) is the + detector-trigger gate. gate_detector_active must be masked down to that bit rather than + passed through as the raw word.""" + dev = panda_omny_raw_stream + data = np.array( + # bit 0 set (gate active) plus other bits from something else sharing BITS0 + [(0.0, 5.0), (1.0, 4.0), (2.0, 3.0)], + dtype=[("COUNTER1.OUT.Value", " Date: Tue, 15 Sep 2026 14:22:28 +0200 Subject: [PATCH 3/7] feat(flomni): split omny_panda into two devices, fix free-running completion check Mode A (per-gate aggregate) and Mode B (continuous raw-sample) need different PandA hardware layouts and can't run simultaneously, so omny_panda is now two device config entries in ptycho_flomni.yaml: omny_panda (legacy, kept commented as the restore reference) and omny_panda_continuous (raw_stream_mode: true, enabled). Also fixes on_complete for the continuous case. PCAP.CAPTURED can't be used to detect end-of-acquisition on this layout: CLOCK1 drives PCAP.gate/trig continuously, so the captured count never naturally stops increasing while armed -- the previous settle-based wait would have just run out its timeout. COUNTER1 (frame_counter) instead counts completed exposures directly (triggered on the detector-trigger's falling edge), so PandaBoxOMNY now waits for it to reach the exact expected num_points * frames_per_trigger, mirroring the legacy exact-count check instead of guessing when capture has "settled". Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/device_configs/ptycho_flomni.yaml | 84 ++++++++++++------- csaxs_bec/devices/panda_box/panda_box_omny.py | 63 ++++++++------ .../panda_box_free_running_data_schema.md | 2 +- .../developer/panda_box_free_running_setup.md | 20 +++-- docs/user/ptychography/flomni.md | 13 +-- docs/user/ptychography/omny.md | 13 +-- tests/tests_devices/test_panda.py | 28 +++---- 7 files changed, 131 insertions(+), 92 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index 5a51d42a..70eeca28 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -646,40 +646,68 @@ calculated_signal: ############################################################ #################### OMNY Pandabox ######################### ############################################################ -omny_panda: +# Mode A (legacy per-gate aggregate acquisition). The PandA layout has to match the +# selected mode (see docs/developer/panda_box_free_running_setup.md, "Open question") - +# it currently has the Mode B layout loaded (see omny_panda_continuous below), so this +# block is kept commented as the reference/restore point for Mode A rather than enabled +# alongside it. To go back to Mode A: disable/remove omny_panda_continuous below, restore +# the PandA layout for per-gate aggregate capture, and uncomment this block. +# omny_panda: +# readoutPriority: async +# deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY +# deviceConfig: +# host: omny-panda.psi.ch +# signal_alias: +# FMC_IN.VAL1.Min: cap_voltage_fzp_y_min +# FMC_IN.VAL1.Max: cap_voltage_fzp_y_max +# FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean +# FMC_IN.VAL2.Min: cap_voltage_fzp_x_min +# FMC_IN.VAL2.Max: cap_voltage_fzp_x_max +# FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean +# INENC1.VAL.Max: interf_st_fzp_y_max +# INENC1.VAL.Mean: interf_st_fzp_y_mean +# INENC1.VAL.Min: interf_st_fzp_y_min +# INENC2.VAL.Max: interf_st_fzp_x_max +# INENC2.VAL.Mean: interf_st_fzp_x_mean +# INENC2.VAL.Min: interf_st_fzp_x_min +# INENC3.VAL.Max: interf_st_rotz_max +# INENC3.VAL.Mean: interf_st_rotz_mean +# INENC3.VAL.Min: interf_st_rotz_min +# INENC4.VAL.Max: interf_st_rotx_max +# INENC4.VAL.Mean: interf_st_rotx_mean +# INENC4.VAL.Min: interf_st_rotx_min +# PCAP.GATE_DURATION.Value: pcap_gate_duration_value +# enabled: true +# readOnly: false +# softwareTrigger: false +# deviceTags: +# - ptycho_flomni + +# Mode B (continuous free-running raw-sample acquisition), see +# docs/developer/panda_box_free_running_setup.md. Requires the matching PandA layout to be +# loaded (TTLIN1 = detector trigger -> COUNTER1 falling-edge count = frame_counter and +# PCAP.BITS0 bit 0 = gate bit; CLOCK1 driving PCAP.gate/trig continuously; all six +# analog/encoder channels on raw Value capture) - confirmed against the omny-panda hardware +# 2026-09-15, currently running at 100 Hz for initial testing. +omny_panda_continuous: readoutPriority: async deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY deviceConfig: host: omny-panda.psi.ch - # raw_stream_mode selects the free-running raw-sample acquisition mode described in - # docs/developer/panda_box_free_running_setup.md. Defaults to false (legacy per-gate - # aggregate capture, as configured below) - do not enable until the PandaBox layout has - # been reconfigured for continuous capture per that document. - raw_stream_mode: false + raw_stream_mode: true signal_alias: - FMC_IN.VAL1.Min: cap_voltage_fzp_y_min - FMC_IN.VAL1.Max: cap_voltage_fzp_y_max - FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean - FMC_IN.VAL2.Min: cap_voltage_fzp_x_min - FMC_IN.VAL2.Max: cap_voltage_fzp_x_max - FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean - INENC1.VAL.Max: interf_st_fzp_y_max - INENC1.VAL.Mean: interf_st_fzp_y_mean - INENC1.VAL.Min: interf_st_fzp_y_min - INENC2.VAL.Max: interf_st_fzp_x_max - INENC2.VAL.Mean: interf_st_fzp_x_mean - INENC2.VAL.Min: interf_st_fzp_x_min - INENC3.VAL.Max: interf_st_rotz_max - INENC3.VAL.Mean: interf_st_rotz_mean - INENC3.VAL.Min: interf_st_rotz_min - INENC4.VAL.Max: interf_st_rotx_max - INENC4.VAL.Mean: interf_st_rotx_mean - INENC4.VAL.Min: interf_st_rotx_min + FMC_IN.VAL1.Value: cap_voltage_fzp_y + FMC_IN.VAL2.Value: cap_voltage_fzp_x + INENC1.VAL.Value: interf_st_fzp_y + INENC2.VAL.Value: interf_st_fzp_x + INENC3.VAL.Value: interf_st_rotz + INENC4.VAL.Value: interf_st_rotx PCAP.GATE_DURATION.Value: pcap_gate_duration_value - # Free-running mode (raw_stream_mode: true) signals. TTLIN1 (detector trigger) feeds - # both blocks below - confirmed 2026-09-15 against the omny-panda hardware, see - # docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 is a shared 32-bit word; - # PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before publishing gate_detector_active. + # TTLIN1 (detector trigger) feeds both blocks below - confirmed 2026-09-15 against the + # omny-panda hardware, see docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 + # is a shared 32-bit word; PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before + # publishing gate_detector_active. COUNTER1 counts TTLIN1's falling edge, i.e. one + # increment per completed exposure; it also doubles as the on_complete signal. PCAP.BITS0.Value: gate_detector_active COUNTER1.OUT.Value: frame_counter enabled: true diff --git a/csaxs_bec/devices/panda_box/panda_box_omny.py b/csaxs_bec/devices/panda_box/panda_box_omny.py index e48d4770..0033c8fa 100644 --- a/csaxs_bec/devices/panda_box/panda_box_omny.py +++ b/csaxs_bec/devices/panda_box/panda_box_omny.py @@ -31,9 +31,12 @@ class PandaBoxOMNY(PandaBox): Incoming samples are coalesced client-side (by row count or time, whichever comes first) before being published, to keep the message rate to the BEC message bus bounded regardless of how the PandaBox itself batches network frames. Completion is - checked by waiting for the hardware-reported captured-frame count to stop increasing, - since an exact expected sample count can't be predicted client-side for continuous - capture. + checked against the frame counter (COUNTER1, aliased to ``frame_counter``), which + increments on the detector-trigger's falling edge, reaching the expected + ``num_points * frames_per_trigger`` count once the last exposure finishes. + ``*PCAP.CAPTURED?`` can't be used for this: on this layout PCAP's gate/trig are driven + continuously by an internal clock, so it never naturally stops incrementing while + armed. """ # PCAP.BITS0 is a shared 32-bit capture word; other captured bit_out signals (e.g. other @@ -84,7 +87,6 @@ class PandaBoxOMNY(PandaBox): super().on_init() self._acquisition_group = "burst" self._timeout_on_completed = 10 - self._settle_timeout_in_s = 0.2 self.scan_parameters: ScanServerScanInfo | None = None def on_stage(self): @@ -152,38 +154,45 @@ class PandaBoxOMNY(PandaBox): return status_captured def _on_complete_free_running(self): - """Completion check for continuous free-running capture: wait until the - hardware-reported captured-frame count stops increasing (settles), since the exact - expected sample count can't be predicted client-side for continuous capture at an - arbitrary rate. The frame_counter signal remains the authoritative record of total - frames captured for later completeness verification. + """Completion check for continuous free-running capture: wait for the frame counter + (COUNTER1, aliased to frame_counter) to reach the exact expected number of frames. + + *PCAP.CAPTURED? can't be used here, unlike _on_complete_burst: on this layout PCAP's + gate/trig are driven continuously by an internal clock (CLOCK1), so the captured + count never naturally stops increasing while PCAP stays armed -- there's nothing to + settle. COUNTER1 instead increments on the detector-trigger's falling edge, i.e. once + per completed exposure, so it reaches num_points * frames_per_trigger -- the same + target _on_complete_burst waits for -- exactly when the last exposure finishes. See + docs/developer/panda_box_free_running_setup.md. """ - def _check_capture_settled(): - captured = -1 - last_change = time.monotonic() + def _check_frame_count_complete(): + counted = 0 start_time = time.monotonic() try: - while True: - ret = self.send_raw("*PCAP.CAPTURED?") - new_captured = int(ret[0].split("=")[-1]) - now = time.monotonic() - if new_captured != captured: - captured = new_captured - last_change = now - elif now - last_change > self._settle_timeout_in_s: - break - if now - start_time > self._timeout_on_completed: - raise TimeoutError( - f"Pandabox {self.name} capture did not settle after {self._timeout_on_completed}s, " - f"last captured count: {captured}." - ) + expected_frames = int( + self.scan_parameters.num_points * self.scan_parameters.frames_per_trigger + ) + while counted < expected_frames: + ret = self.send_raw("COUNTER1.OUT?") + counted = int(float(ret[0].split("=")[-1])) time.sleep(0.01) + if (time.monotonic() - start_time) > self._timeout_on_completed / 2: + logger.info( + f"Waiting for frame_counter on device {self.name} to complete: " + f"counted {counted}/{expected_frames} frames." + ) + if (time.monotonic() - start_time) > self._timeout_on_completed: + raise TimeoutError( + f"Pandabox {self.name} did not complete after " + f"{self._timeout_on_completed}s with frames counted " + f"{counted}/{expected_frames}" + ) finally: self._flush_raw_stream_buffer() self._disarm() - status_captured = self.task_handler.submit_task(_check_capture_settled, run=True) + status_captured = self.task_handler.submit_task(_check_frame_count_complete, run=True) self.cancel_on_stop(status_captured) return status_captured diff --git a/docs/developer/panda_box_free_running_data_schema.md b/docs/developer/panda_box_free_running_data_schema.md index 1151918e..edf4d248 100644 --- a/docs/developer/panda_box_free_running_data_schema.md +++ b/docs/developer/panda_box_free_running_data_schema.md @@ -30,7 +30,7 @@ initial testing on 2026-09-15, see the setup doc for the current rate): |---|---|---| | `cap_voltage_fzp_y`/`cap_voltage_fzp_x` etc. (raw analog/encoder value) | `FMC_IN.VAL1.Value` / `FMC_IN.VAL2.Value` / `INENC1`-`4.VAL.Value` | The instantaneous raw sample at this capture tick, replacing the Mode A aggregate (Mean/Min/Max). All six channels (both FMC analog inputs and all four encoders) are switched to raw capture together on this layout — not a subset. | | `gate_detector_active` | `PCAP.BITS0.Value`, bit 0 (`TTLIN1.VAL`), masked in software — see setup doc | The detector-trigger signal, sampled at this same tick: `1` while a detector trigger is active, `0` otherwise. `PCAP.BITS0` is a shared 32-bit word; `PandaBoxOMNY` masks it down to bit 0 before this reaches the HDF5 file (see `_GATE_BIT_OFFSET` in `panda_box_omny.py`) — the raw word itself is not a clean 0/1. This is what a future reconstruction step would use to delimit which raw samples belong to which detector-gate window. | -| `frame_counter` | `COUNTER1.OUT.Value`, free-running off `TTLIN1.VAL` (i.e. increments once per detector trigger pulse, not once per PCAP capture tick as originally guessed here) | Monotonically incrementing hardware counter. Used to verify data completeness after acquisition: a gap-free, strictly increasing sequence means no detector triggers were missed between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). Because it counts detector triggers rather than capture ticks, it verifies detector-frame completeness directly rather than raw-sample completeness. | +| `frame_counter` | `COUNTER1.OUT.Value`, triggered off `TTLIN1.VAL`'s falling edge (i.e. increments once per *completed* detector exposure, not once per PCAP capture tick as originally guessed here) | Monotonically incrementing hardware counter. Doubles as the completion signal for `on_complete` (`PandaBoxOMNY._on_complete_free_running`, `panda_box_omny.py`) — waits for it to reach `num_points * frames_per_trigger`, since `*PCAP.CAPTURED?` free-runs continuously and never naturally stops while armed. Also the post-hoc data-completeness record: a gap-free, strictly increasing sequence means no detector exposures were missed between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). Because it counts completed exposures rather than capture ticks, it verifies detector-frame completeness directly rather than raw-sample completeness. | | `PCAP.TS_TRIG.Value` (existing) | `PCAP.TS_TRIG` | Absolute/relative timestamp already captured today; usable for time-alignment against other detectors' data during future reconstruction. | | `PCAP.GATE_DURATION.Value` (existing, aliased as `pcap_gate_duration_value`) | `PCAP.GATE_DURATION` | Existing field, retained; less meaningful once the gate is held open continuously for the whole burst window rather than pulsed per point (see setup doc, Mode B gate/arm row) — re-evaluate whether this still needs to be captured now that real hardware testing has started. | diff --git a/docs/developer/panda_box_free_running_setup.md b/docs/developer/panda_box_free_running_setup.md index 6775d3c8..533b0853 100644 --- a/docs/developer/panda_box_free_running_setup.md +++ b/docs/developer/panda_box_free_running_setup.md @@ -32,8 +32,8 @@ the end) before that mode can be used. | Gate/arm timing | Pulsed per point / sub-frame (rate = scan rate × `frames_per_trigger`) | PCAP held open continuously for the burst window, free-running at `CLOCK1`'s configured rate (started at **100 Hz** for initial testing on 2026-09-15; will be ramped up as testing progresses) | | `_acquisition_group` (BEC async metadata) | `fly` / `monitored` / `burst`, chosen from `scan_type`/`frames_per_trigger` | `free_running` | | Message publishing | One `.data.put()` per PandA network frame, unchanged | Coalesced client-side: flushed every `raw_stream_flush_row_count` rows or `raw_stream_flush_interval` seconds, whichever comes first (mitigates Redis/HDF5 message-rate risk — see the acquisition plan's data-path notes) | -| Completion check (`on_complete`) | Exact count: waits for `*PCAP.CAPTURED?` to reach `num_points * frames_per_trigger` | Settle-based: waits for `*PCAP.CAPTURED?` to stop changing for `_settle_timeout_in_s`, since an exact expected sample count can't be predicted client-side for continuous capture | -| Data completeness verification | Implicit in the exact-count completion check | The `frame_counter` signal (Mode B only) — a monotonically incrementing hardware counter — is the record to check for gaps/drops after the fact | +| Completion check (`on_complete`) | Exact count: waits for `*PCAP.CAPTURED?` to reach `num_points * frames_per_trigger` | Exact count: waits for `COUNTER1.OUT?` (`frame_counter`) to reach `num_points * frames_per_trigger`. `*PCAP.CAPTURED?` can't be used here — `CLOCK1` drives `PCAP.gate`/`PCAP.trig` continuously (see "Continuous gate/arm" below), so the captured count never naturally stops increasing while armed; `COUNTER1` instead counts completed detector exposures directly (it's clocked off the detector-trigger's falling edge), so it reaches the expected total exactly when the last exposure finishes | +| Data completeness verification | Implicit in the exact-count completion check | Same mechanism as Mode A: `frame_counter` (`COUNTER1`) is both the completion signal and the post-hoc completeness record — a gap in an otherwise-monotonic sequence in the HDF5 data means dropped rows even though the expected total was reached | Both modes are implemented in the same `PandaBoxOMNY` class; `raw_stream_mode` defaults to `false` so any device config that doesn't set it keeps today's @@ -82,11 +82,14 @@ layout can be restored unambiguously (see `ptycho_flomni.yaml`): shares `BITS0`. Re-run the query above (read-only, safe against live hardware) if the PandA layout is ever rebuilt, since bit assignment is fixed by the FPGA build and not guaranteed stable across rebuilds. -- **Frame counter**: `COUNTER1`, free-running off `TTLIN1.VAL` as its - `trig` input (see layout below) — i.e. it increments once per detector - trigger pulse, not once per PCAP capture tick as originally guessed here. - Captured with `CAPTURE=Value`; this is the data-completeness signal - (`frame_counter` in `signal_alias`, see below). +- **Frame counter**: `COUNTER1`, triggered off `TTLIN1.VAL`'s **falling + edge** (see layout below) — i.e. it increments once per *completed* + detector exposure, not once per PCAP capture tick as originally guessed + here. Captured with `CAPTURE=Value`; this is both the data-completeness + signal and the completion-detection signal (`frame_counter` in + `signal_alias`, see below, and "Completion check" in the mode-comparison + table above) — once the last expected exposure's trigger pulse has fallen, + `COUNTER1.OUT` reaches `num_points * frames_per_trigger` exactly. - **Raw analog/position capture**: `FMC_IN.VAL1`, `FMC_IN.VAL2` and all four `INENC1`-`4.VAL` are switched from aggregate capture to `CAPTURE=Value` on this layout (confirmed 2026-09-15) — i.e. every analog/encoder channel @@ -102,7 +105,8 @@ layout can be restored unambiguously (see `ptycho_flomni.yaml`): ``` TTLIN1 (TTL input 1, detector trigger) - ├──▶ COUNTER1.trig (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value) + ├──▶ COUNTER1.trig, falling edge (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value) + │ = completion signal too: reaches num_points * frames_per_trigger when the last exposure finishes └──▶ captured into PCAP.BITS0 bit 0 ──▶ masked in software = gate_detector_active CLOCK1 (enabled by constant ONE) ──▶ PCAP.gate, PCAP.trig (continuous arm/capture) diff --git a/docs/user/ptychography/flomni.md b/docs/user/ptychography/flomni.md index 99b2e714..12369515 100644 --- a/docs/user/ptychography/flomni.md +++ b/docs/user/ptychography/flomni.md @@ -234,12 +234,13 @@ Example: `scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01, frames_per_trigger=1)` `frames_per_trigger` controls burst acquisition for detectors generally. -Separately, the `omny_panda` PandABox device has its own `raw_stream_mode` -device-config flag (`csaxs_bec/device_configs/ptycho_flomni.yaml`) that -switches it between the default per-gate averaged acquisition and a -continuous, high-rate raw-sample acquisition mode — see -`docs/developer/panda_box_free_running_setup.md` for what each mode captures -and how to enable it. +Separately, the OMNY PandABox is configured as one of two distinct devices in +`csaxs_bec/device_configs/ptycho_flomni.yaml` — `omny_panda` (per-gate +averaged acquisition, currently kept as a commented reference) or +`omny_panda_continuous` (continuous, high-rate raw-sample acquisition) — +because the two modes require different PandA hardware layouts and can't run +simultaneously. See `docs/developer/panda_box_free_running_setup.md` for what +each mode captures and how to switch between them. #### Overview of the alignment steps diff --git a/docs/user/ptychography/omny.md b/docs/user/ptychography/omny.md index 052de3c5..5f3e316a 100644 --- a/docs/user/ptychography/omny.md +++ b/docs/user/ptychography/omny.md @@ -339,12 +339,13 @@ Example: `scans.omny_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01, frames_per_trigger=1, readout_time=0)` `frames_per_trigger` controls burst acquisition for detectors generally. -Separately, the `omny_panda` PandABox device has its own `raw_stream_mode` -device-config flag (`csaxs_bec/device_configs/ptycho_flomni.yaml`) that -switches it between the default per-gate averaged acquisition and a -continuous, high-rate raw-sample acquisition mode — see -`docs/developer/panda_box_free_running_setup.md` for what each mode captures -and how to enable it. +Separately, the OMNY PandABox is configured as one of two distinct devices in +`csaxs_bec/device_configs/ptycho_flomni.yaml` — `omny_panda` (per-gate +averaged acquisition, currently kept as a commented reference) or +`omny_panda_continuous` (continuous, high-rate raw-sample acquisition) — +because the two modes require different PandA hardware layouts and can't run +simultaneously. See `docs/developer/panda_box_free_running_setup.md` for what +each mode captures and how to switch between them. #### Overview of the alignment steps diff --git a/tests/tests_devices/test_panda.py b/tests/tests_devices/test_panda.py index cc3d1a79..48f6f2f2 100644 --- a/tests/tests_devices/test_panda.py +++ b/tests/tests_devices/test_panda.py @@ -247,27 +247,20 @@ def test_panda_omny_unstage_flushes_pending_raw_stream_data(panda_omny_raw_strea mock_put.assert_called_once() -def test_panda_omny_complete_free_running_settles(panda_omny_raw_stream): - """Completion in free-running mode waits for the captured count to stop increasing, - rather than an exact expected count.""" +def test_panda_omny_complete_free_running_waits_for_frame_counter(panda_omny_raw_stream): + """Completion in free-running mode waits for COUNTER1 (frame_counter) to reach the exact + expected frame count -- PCAP.CAPTURED can't be used since it free-runs continuously off + CLOCK1 and never naturally stops increasing while armed.""" dev = panda_omny_raw_stream dev.scan_info.msg.info["num_points"] = 1 dev.scan_info.msg.info["frames_per_trigger"] = 1 dev.scan_parameters = fetch_scan_info(dev.scan_info) dev._timeout_on_completed = 5 - dev._settle_timeout_in_s = 0.1 - - call_count = {"n": 0} - - def _send_raw(*args, **kwargs): - # Ramp up for the first 3 calls, then plateau indefinitely at "=2" so the - # settle-timeout branch (rather than running out of a fixed side_effect list) is - # what ends the loop. - call_count["n"] += 1 - return [f"={min(call_count['n'] - 1, 2)}"] with ( - mock.patch.object(dev, "send_raw", side_effect=_send_raw), + mock.patch.object( + dev, "send_raw", side_effect=[["=0"], ["=0"], ["=1"]] + ) as mock_send_raw, mock.patch.object(dev, "_disarm", return_value=None) as mock_disarm, mock.patch.object(dev, "_flush_raw_stream_buffer") as mock_flush, ): @@ -277,25 +270,28 @@ def test_panda_omny_complete_free_running_settles(panda_omny_raw_stream): assert status.success is True mock_disarm.assert_called_once() mock_flush.assert_called_once() + assert mock_send_raw.call_args.args[0] == "COUNTER1.OUT?" def test_panda_omny_complete_free_running_timeout(panda_omny_raw_stream): - """If the captured count never settles, the overall timeout must still fire.""" + """If the frame count never reaches the expected total, the overall timeout must still + fire.""" dev = panda_omny_raw_stream dev.scan_info.msg.info["num_points"] = 1 dev.scan_info.msg.info["frames_per_trigger"] = 1 dev.scan_parameters = fetch_scan_info(dev.scan_info) dev._timeout_on_completed = 0.3 - dev._settle_timeout_in_s = 10 # never settles within the overall timeout with ( mock.patch.object(dev, "send_raw", side_effect=lambda *a, **k: ["=0"]), mock.patch.object(dev, "_disarm", return_value=None) as mock_disarm, + mock.patch.object(dev, "_flush_raw_stream_buffer") as mock_flush, ): status = dev.on_complete() with pytest.raises(TimeoutError): status.wait(timeout=4) mock_disarm.assert_called_once() + mock_flush.assert_called_once() def test_panda_omny_raw_stream_signal_alias(panda_omny_raw_stream): -- 2.54.0 From 6415db0e2db6b216897867c94ab78cc0cb74a3ca Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 15 Sep 2026 15:02:57 +0200 Subject: [PATCH 4/7] config(flomni): switch panda config back to Mode A, record Mode B success Mode B (omny_panda_continuous) confirmed working end-to-end against real PandA hardware on 2026-09-15: correct per-channel raw values, masked gate_detector_active, and frame_counter-based completion all verified. Switching back to Mode A (omny_panda) for A/B comparison testing. Both device entries now stay uncommented in ptycho_flomni.yaml, toggled via enabled: true/false, rather than commenting/uncommenting blocks -- confirmed a disabled device is an inert placeholder that BEC's device server never connects to. Speed testing Mode B beyond the initial 100 Hz is still TODO. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/device_configs/ptycho_flomni.yaml | 88 +++++++++---------- .../developer/panda_box_free_running_setup.md | 35 +++++--- 2 files changed, 69 insertions(+), 54 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index 70eeca28..f840e47e 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -646,49 +646,48 @@ calculated_signal: ############################################################ #################### OMNY Pandabox ######################### ############################################################ -# Mode A (legacy per-gate aggregate acquisition). The PandA layout has to match the -# selected mode (see docs/developer/panda_box_free_running_setup.md, "Open question") - -# it currently has the Mode B layout loaded (see omny_panda_continuous below), so this -# block is kept commented as the reference/restore point for Mode A rather than enabled -# alongside it. To go back to Mode A: disable/remove omny_panda_continuous below, restore -# the PandA layout for per-gate aggregate capture, and uncomment this block. -# omny_panda: -# readoutPriority: async -# deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY -# deviceConfig: -# host: omny-panda.psi.ch -# signal_alias: -# FMC_IN.VAL1.Min: cap_voltage_fzp_y_min -# FMC_IN.VAL1.Max: cap_voltage_fzp_y_max -# FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean -# FMC_IN.VAL2.Min: cap_voltage_fzp_x_min -# FMC_IN.VAL2.Max: cap_voltage_fzp_x_max -# FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean -# INENC1.VAL.Max: interf_st_fzp_y_max -# INENC1.VAL.Mean: interf_st_fzp_y_mean -# INENC1.VAL.Min: interf_st_fzp_y_min -# INENC2.VAL.Max: interf_st_fzp_x_max -# INENC2.VAL.Mean: interf_st_fzp_x_mean -# INENC2.VAL.Min: interf_st_fzp_x_min -# INENC3.VAL.Max: interf_st_rotz_max -# INENC3.VAL.Mean: interf_st_rotz_mean -# INENC3.VAL.Min: interf_st_rotz_min -# INENC4.VAL.Max: interf_st_rotx_max -# INENC4.VAL.Mean: interf_st_rotx_mean -# INENC4.VAL.Min: interf_st_rotx_min -# PCAP.GATE_DURATION.Value: pcap_gate_duration_value -# enabled: true -# readOnly: false -# softwareTrigger: false -# deviceTags: -# - ptycho_flomni +# Mode A (legacy per-gate aggregate acquisition) and Mode B (continuous free-running +# raw-sample acquisition, see docs/developer/panda_box_free_running_setup.md) need different +# PandA hardware layouts and can't run simultaneously (see that doc's "Open question"), so +# only one of the two devices below should have enabled: true at a time, matching whichever +# layout is currently loaded on the PandA. A disabled device is an inert placeholder - BEC's +# device server never opens a connection to it (device_server/devices/devicemanager.py) - so +# both stay uncommented for easy switching instead of commenting/uncommenting blocks. +# +# Currently: Mode A enabled, Mode B disabled (switched back 2026-09-15 for A/B comparison +# testing after confirming Mode B works end-to-end; Mode B speed testing beyond 100 Hz is +# still TODO). +omny_panda: + readoutPriority: async + deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY + deviceConfig: + host: omny-panda.psi.ch + signal_alias: + FMC_IN.VAL1.Min: cap_voltage_fzp_y_min + FMC_IN.VAL1.Max: cap_voltage_fzp_y_max + FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean + FMC_IN.VAL2.Min: cap_voltage_fzp_x_min + FMC_IN.VAL2.Max: cap_voltage_fzp_x_max + FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean + INENC1.VAL.Max: interf_st_fzp_y_max + INENC1.VAL.Mean: interf_st_fzp_y_mean + INENC1.VAL.Min: interf_st_fzp_y_min + INENC2.VAL.Max: interf_st_fzp_x_max + INENC2.VAL.Mean: interf_st_fzp_x_mean + INENC2.VAL.Min: interf_st_fzp_x_min + INENC3.VAL.Max: interf_st_rotz_max + INENC3.VAL.Mean: interf_st_rotz_mean + INENC3.VAL.Min: interf_st_rotz_min + INENC4.VAL.Max: interf_st_rotx_max + INENC4.VAL.Mean: interf_st_rotx_mean + INENC4.VAL.Min: interf_st_rotx_min + PCAP.GATE_DURATION.Value: pcap_gate_duration_value + enabled: true + readOnly: false + softwareTrigger: false + deviceTags: + - ptycho_flomni -# Mode B (continuous free-running raw-sample acquisition), see -# docs/developer/panda_box_free_running_setup.md. Requires the matching PandA layout to be -# loaded (TTLIN1 = detector trigger -> COUNTER1 falling-edge count = frame_counter and -# PCAP.BITS0 bit 0 = gate bit; CLOCK1 driving PCAP.gate/trig continuously; all six -# analog/encoder channels on raw Value capture) - confirmed against the omny-panda hardware -# 2026-09-15, currently running at 100 Hz for initial testing. omny_panda_continuous: readoutPriority: async deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY @@ -707,10 +706,11 @@ omny_panda_continuous: # omny-panda hardware, see docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 # is a shared 32-bit word; PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before # publishing gate_detector_active. COUNTER1 counts TTLIN1's falling edge, i.e. one - # increment per completed exposure; it also doubles as the on_complete signal. + # increment per completed exposure; it also doubles as the on_complete signal. Confirmed + # working end-to-end 2026-09-15; speed testing beyond the initial 100 Hz is still TODO. PCAP.BITS0.Value: gate_detector_active COUNTER1.OUT.Value: frame_counter - enabled: true + enabled: false readOnly: false softwareTrigger: false deviceTags: diff --git a/docs/developer/panda_box_free_running_setup.md b/docs/developer/panda_box_free_running_setup.md index 533b0853..defb17ad 100644 --- a/docs/developer/panda_box_free_running_setup.md +++ b/docs/developer/panda_box_free_running_setup.md @@ -1,11 +1,11 @@ # PandABox (`omny_panda`) burst acquisition: two operation modes -> **Status: software implemented, Mode B layout wired and under initial -> testing on real PandA hardware (2026-09-15, omny-panda.psi.ch).** The block -> assignments below reflect what is actually loaded on the OMNY PandA design -> as of that date. The target free-running rate is still being ramped up -> (started at 100 Hz for initial testing); update the rate row once a final -> operating rate is settled. +> **Status: Mode B confirmed working end-to-end against real PandA hardware +> (2026-09-15, omny-panda.psi.ch)** — correct per-channel raw values, masked +> `gate_detector_active`, and `frame_counter`-based completion all verified. +> Config switched back to Mode A afterward for A/B comparison testing (see +> "Switching between modes" below); re-enable `omny_panda_continuous` to test +> Mode B again. Remaining TODO: speed testing beyond the initial 100 Hz. ## Context @@ -124,8 +124,9 @@ and record the exact save/load commands used here. ## Signal alias mapping The physical PandA block feeding each BEC signal name, kept here and in -`csaxs_bec/device_configs/ptycho_flomni.yaml` (`omny_panda.deviceConfig.signal_alias`) -in sync as a single source of truth: +`csaxs_bec/device_configs/ptycho_flomni.yaml` (`omny_panda.deviceConfig.signal_alias` for +Mode A, `omny_panda_continuous.deviceConfig.signal_alias` for Mode B) in sync as a single +source of truth: | PandA block | Mode | BEC signal name | |---|---|---| @@ -135,14 +136,28 @@ in sync as a single source of truth: | `INENC2.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_fzp_x_min`/`_max`/`_mean` | | `INENC3.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_rotz_min`/`_max`/`_mean` | | `INENC4.VAL.Min`/`.Max`/`.Mean` | A | `interf_st_rotx_min`/`_max`/`_mean` | -| `PCAP.GATE_DURATION.Value` | A | `pcap_gate_duration_value` | +| `PCAP.GATE_DURATION.Value` | A, B | `pcap_gate_duration_value` | +| `FMC_IN.VAL1/VAL2.Value`, `INENC1`-`4.VAL.Value` | B | `cap_voltage_fzp_y`/`_x`, `interf_st_fzp_y`/`_x`/`_rotz`/`_rotx` | | `PCAP.BITS0.Value` (bit 0 = `TTLIN1.VAL`, confirmed 2026-09-15; masked in software, see above) | B | `gate_detector_active` | -| `COUNTER1.OUT.Value` (confirmed 2026-09-15, free-running off `TTLIN1.VAL`) | B | `frame_counter` | +| `COUNTER1.OUT.Value` (confirmed 2026-09-15, triggered off `TTLIN1.VAL`'s falling edge) | B | `frame_counter` | The BEC-side data schema produced by Mode B (what ends up in the HDF5 async dataset) is documented separately in `docs/developer/panda_box_free_running_data_schema.md`. +## Switching between modes + +`omny_panda` (Mode A) and `omny_panda_continuous` (Mode B) are both defined in +`ptycho_flomni.yaml` at all times; exactly one should have `enabled: true`, matching +whichever layout is currently loaded on the PandA (see "Open question" below for why they +can't both be enabled at once). A disabled device is an inert placeholder — BEC's device +server never opens a connection to it (`bec_server/device_server/devices/devicemanager.py`, +`connect_device` is only called when `enabled`) — so switching is just flipping the +`enabled` flag on both entries and reloading the device config; no need to comment/uncomment +blocks. Confirmed end-to-end 2026-09-15: Mode B tested working (correct per-channel values, +masked `gate_detector_active`, `frame_counter`-based completion), then switched back to +Mode A for A/B comparison testing. + ## Open question, now partially resolved Can both modes coexist under a single static PandA layout — i.e. is mode -- 2.54.0 From f82fd51a72898131fafc3868da3e55d1136e20db Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 15 Sep 2026 15:04:59 +0200 Subject: [PATCH 5/7] fix(flomni): comment out omny_panda_continuous, enabled:false wasn't enough Both omny_panda and omny_panda_continuous ended up connected and active simultaneously against the same physical PandA, even though only omny_panda was enabled: true -- a device already connected apparently doesn't disconnect just because a later config reload disables it. Comment the inactive mode's block out entirely instead of relying on enabled: false, so BEC never constructs the device object at all. Docs updated to correct the earlier (wrong) guidance that toggling enabled alone was sufficient to switch between modes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/device_configs/ptycho_flomni.yaml | 77 ++++++++++--------- .../developer/panda_box_free_running_setup.md | 31 +++++--- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index f840e47e..00344e9e 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -648,15 +648,22 @@ calculated_signal: ############################################################ # Mode A (legacy per-gate aggregate acquisition) and Mode B (continuous free-running # raw-sample acquisition, see docs/developer/panda_box_free_running_setup.md) need different -# PandA hardware layouts and can't run simultaneously (see that doc's "Open question"), so -# only one of the two devices below should have enabled: true at a time, matching whichever -# layout is currently loaded on the PandA. A disabled device is an inert placeholder - BEC's -# device server never opens a connection to it (device_server/devices/devicemanager.py) - so -# both stay uncommented for easy switching instead of commenting/uncommenting blocks. +# PandA hardware layouts and can't run simultaneously (see that doc's "Open question"). Only +# one of the two devices below should be active at a time, matching whichever layout is +# currently loaded on the PandA. # -# Currently: Mode A enabled, Mode B disabled (switched back 2026-09-15 for A/B comparison -# testing after confirming Mode B works end-to-end; Mode B speed testing beyond 100 Hz is -# still TODO). +# NOTE 2026-09-15: setting enabled: false on omny_panda_continuous was not sufficient by +# itself to stop it being active in the running BEC session (both devices ended up +# connected/active simultaneously against the same physical PandA at once) - a device that +# was already connected apparently doesn't get disconnected just by disabling it in a config +# reload. Commenting the block out is the reliable way to guarantee it's not loaded; do the +# same for omny_panda when switching to Mode B, and don't rely on enabled: false alone +# without also confirming (e.g. restarting the device server) that the other device has +# actually dropped its connection. +# +# Currently: Mode A active, Mode B commented out (switched back 2026-09-15 for A/B +# comparison testing after confirming Mode B works end-to-end; Mode B speed testing beyond +# 100 Hz is still TODO). omny_panda: readoutPriority: async deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY @@ -688,30 +695,30 @@ omny_panda: deviceTags: - ptycho_flomni -omny_panda_continuous: - readoutPriority: async - deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY - deviceConfig: - host: omny-panda.psi.ch - raw_stream_mode: true - signal_alias: - FMC_IN.VAL1.Value: cap_voltage_fzp_y - FMC_IN.VAL2.Value: cap_voltage_fzp_x - INENC1.VAL.Value: interf_st_fzp_y - INENC2.VAL.Value: interf_st_fzp_x - INENC3.VAL.Value: interf_st_rotz - INENC4.VAL.Value: interf_st_rotx - PCAP.GATE_DURATION.Value: pcap_gate_duration_value - # TTLIN1 (detector trigger) feeds both blocks below - confirmed 2026-09-15 against the - # omny-panda hardware, see docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 - # is a shared 32-bit word; PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before - # publishing gate_detector_active. COUNTER1 counts TTLIN1's falling edge, i.e. one - # increment per completed exposure; it also doubles as the on_complete signal. Confirmed - # working end-to-end 2026-09-15; speed testing beyond the initial 100 Hz is still TODO. - PCAP.BITS0.Value: gate_detector_active - COUNTER1.OUT.Value: frame_counter - enabled: false - readOnly: false - softwareTrigger: false - deviceTags: - - ptycho_flomni +# omny_panda_continuous: +# readoutPriority: async +# deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY +# deviceConfig: +# host: omny-panda.psi.ch +# raw_stream_mode: true +# signal_alias: +# FMC_IN.VAL1.Value: cap_voltage_fzp_y +# FMC_IN.VAL2.Value: cap_voltage_fzp_x +# INENC1.VAL.Value: interf_st_fzp_y +# INENC2.VAL.Value: interf_st_fzp_x +# INENC3.VAL.Value: interf_st_rotz +# INENC4.VAL.Value: interf_st_rotx +# PCAP.GATE_DURATION.Value: pcap_gate_duration_value +# # TTLIN1 (detector trigger) feeds both blocks below - confirmed 2026-09-15 against the +# # omny-panda hardware, see docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 +# # is a shared 32-bit word; PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before +# # publishing gate_detector_active. COUNTER1 counts TTLIN1's falling edge, i.e. one +# # increment per completed exposure; it also doubles as the on_complete signal. Confirmed +# # working end-to-end 2026-09-15; speed testing beyond the initial 100 Hz is still TODO. +# PCAP.BITS0.Value: gate_detector_active +# COUNTER1.OUT.Value: frame_counter +# enabled: true +# readOnly: false +# softwareTrigger: false +# deviceTags: +# - ptycho_flomni diff --git a/docs/developer/panda_box_free_running_setup.md b/docs/developer/panda_box_free_running_setup.md index defb17ad..34154c10 100644 --- a/docs/developer/panda_box_free_running_setup.md +++ b/docs/developer/panda_box_free_running_setup.md @@ -147,16 +147,27 @@ dataset) is documented separately in ## Switching between modes -`omny_panda` (Mode A) and `omny_panda_continuous` (Mode B) are both defined in -`ptycho_flomni.yaml` at all times; exactly one should have `enabled: true`, matching -whichever layout is currently loaded on the PandA (see "Open question" below for why they -can't both be enabled at once). A disabled device is an inert placeholder — BEC's device -server never opens a connection to it (`bec_server/device_server/devices/devicemanager.py`, -`connect_device` is only called when `enabled`) — so switching is just flipping the -`enabled` flag on both entries and reloading the device config; no need to comment/uncomment -blocks. Confirmed end-to-end 2026-09-15: Mode B tested working (correct per-channel values, -masked `gate_detector_active`, `frame_counter`-based completion), then switched back to -Mode A for A/B comparison testing. +`omny_panda` (Mode A) and `omny_panda_continuous` (Mode B) need different PandA hardware +layouts and can't run simultaneously (see "Open question" below), so exactly one should be +active in `ptycho_flomni.yaml` at a time, matching whichever layout is currently loaded on +the PandA — **comment out the other one's block entirely**, don't rely on `enabled: false` +alone. + +That's a correction from earlier in this project: `enabled: false` on a device BEC has never +connected to is an inert placeholder — `connect_device` in +`bec_server/device_server/devices/devicemanager.py` is only called when `enabled` — so the +theory was that both entries could stay uncommented and switching would just be flipping the +flag. In practice (2026-09-15), disabling `omny_panda_continuous` this way was **not** +sufficient once it had already been connected in the running session: both devices ended up +active simultaneously against the same physical PandA. A device that's already connected +doesn't appear to get disconnected just because a later config reload marks it disabled. +Commenting the block out (so BEC never even constructs the device object) is the reliable +way to guarantee it's gone; if in doubt, confirm the other device has actually dropped its +connection (e.g. via a device server restart) rather than trusting `enabled: false` alone. + +Confirmed end-to-end 2026-09-15: Mode B tested working (correct per-channel values, masked +`gate_detector_active`, `frame_counter`-based completion), then switched back to Mode A +(commented out) for A/B comparison testing. ## Open question, now partially resolved -- 2.54.0 From 842efeb553ebc88d76231748ce7cd6772c7b79c4 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 15 Sep 2026 15:08:12 +0200 Subject: [PATCH 6/7] config(flomni): swap panda config back to Mode B for speed testing Mode A commented out, omny_panda_continuous active -- next step is testing above the initial 100 Hz CLOCK1 rate now that Mode B is confirmed working end-to-end. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/device_configs/ptycho_flomni.yaml | 100 ++++++++++---------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index 00344e9e..ab3643df 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -661,64 +661,64 @@ calculated_signal: # without also confirming (e.g. restarting the device server) that the other device has # actually dropped its connection. # -# Currently: Mode A active, Mode B commented out (switched back 2026-09-15 for A/B -# comparison testing after confirming Mode B works end-to-end; Mode B speed testing beyond -# 100 Hz is still TODO). -omny_panda: - readoutPriority: async - deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY - deviceConfig: - host: omny-panda.psi.ch - signal_alias: - FMC_IN.VAL1.Min: cap_voltage_fzp_y_min - FMC_IN.VAL1.Max: cap_voltage_fzp_y_max - FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean - FMC_IN.VAL2.Min: cap_voltage_fzp_x_min - FMC_IN.VAL2.Max: cap_voltage_fzp_x_max - FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean - INENC1.VAL.Max: interf_st_fzp_y_max - INENC1.VAL.Mean: interf_st_fzp_y_mean - INENC1.VAL.Min: interf_st_fzp_y_min - INENC2.VAL.Max: interf_st_fzp_x_max - INENC2.VAL.Mean: interf_st_fzp_x_mean - INENC2.VAL.Min: interf_st_fzp_x_min - INENC3.VAL.Max: interf_st_rotz_max - INENC3.VAL.Mean: interf_st_rotz_mean - INENC3.VAL.Min: interf_st_rotz_min - INENC4.VAL.Max: interf_st_rotx_max - INENC4.VAL.Mean: interf_st_rotx_mean - INENC4.VAL.Min: interf_st_rotx_min - PCAP.GATE_DURATION.Value: pcap_gate_duration_value - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - ptycho_flomni - -# omny_panda_continuous: +# Currently: Mode B active for speed testing beyond the initial 100 Hz, Mode A commented out +# (both modes confirmed working end-to-end 2026-09-15; this swap 2026-09-15 is for Mode B +# speed testing specifically). +# omny_panda: # readoutPriority: async # deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY # deviceConfig: # host: omny-panda.psi.ch -# raw_stream_mode: true # signal_alias: -# FMC_IN.VAL1.Value: cap_voltage_fzp_y -# FMC_IN.VAL2.Value: cap_voltage_fzp_x -# INENC1.VAL.Value: interf_st_fzp_y -# INENC2.VAL.Value: interf_st_fzp_x -# INENC3.VAL.Value: interf_st_rotz -# INENC4.VAL.Value: interf_st_rotx +# FMC_IN.VAL1.Min: cap_voltage_fzp_y_min +# FMC_IN.VAL1.Max: cap_voltage_fzp_y_max +# FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean +# FMC_IN.VAL2.Min: cap_voltage_fzp_x_min +# FMC_IN.VAL2.Max: cap_voltage_fzp_x_max +# FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean +# INENC1.VAL.Max: interf_st_fzp_y_max +# INENC1.VAL.Mean: interf_st_fzp_y_mean +# INENC1.VAL.Min: interf_st_fzp_y_min +# INENC2.VAL.Max: interf_st_fzp_x_max +# INENC2.VAL.Mean: interf_st_fzp_x_mean +# INENC2.VAL.Min: interf_st_fzp_x_min +# INENC3.VAL.Max: interf_st_rotz_max +# INENC3.VAL.Mean: interf_st_rotz_mean +# INENC3.VAL.Min: interf_st_rotz_min +# INENC4.VAL.Max: interf_st_rotx_max +# INENC4.VAL.Mean: interf_st_rotx_mean +# INENC4.VAL.Min: interf_st_rotx_min # PCAP.GATE_DURATION.Value: pcap_gate_duration_value -# # TTLIN1 (detector trigger) feeds both blocks below - confirmed 2026-09-15 against the -# # omny-panda hardware, see docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 -# # is a shared 32-bit word; PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before -# # publishing gate_detector_active. COUNTER1 counts TTLIN1's falling edge, i.e. one -# # increment per completed exposure; it also doubles as the on_complete signal. Confirmed -# # working end-to-end 2026-09-15; speed testing beyond the initial 100 Hz is still TODO. -# PCAP.BITS0.Value: gate_detector_active -# COUNTER1.OUT.Value: frame_counter # enabled: true # readOnly: false # softwareTrigger: false # deviceTags: # - ptycho_flomni + +omny_panda_continuous: + readoutPriority: async + deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY + deviceConfig: + host: omny-panda.psi.ch + raw_stream_mode: true + signal_alias: + FMC_IN.VAL1.Value: cap_voltage_fzp_y + FMC_IN.VAL2.Value: cap_voltage_fzp_x + INENC1.VAL.Value: interf_st_fzp_y + INENC2.VAL.Value: interf_st_fzp_x + INENC3.VAL.Value: interf_st_rotz + INENC4.VAL.Value: interf_st_rotx + PCAP.GATE_DURATION.Value: pcap_gate_duration_value + # TTLIN1 (detector trigger) feeds both blocks below - confirmed 2026-09-15 against the + # omny-panda hardware, see docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 + # is a shared 32-bit word; PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before + # publishing gate_detector_active. COUNTER1 counts TTLIN1's falling edge, i.e. one + # increment per completed exposure; it also doubles as the on_complete signal. Confirmed + # working end-to-end 2026-09-15; now under speed testing beyond the initial 100 Hz. + PCAP.BITS0.Value: gate_detector_active + COUNTER1.OUT.Value: frame_counter + enabled: true + readOnly: false + softwareTrigger: false + deviceTags: + - ptycho_flomni -- 2.54.0 From acdca8213777b0d640c4ccdb4a756b2be77dc65b Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 15 Sep 2026 15:30:10 +0200 Subject: [PATCH 7/7] docs(flomni): save the confirmed Mode A/Mode B PandA layouts Captured 2026-09-15 directly from the omny-panda hardware via GetState() (ophyd_devices/devices/panda_box/utility_scripts.py), one per Design: omny.ini (Mode A) and omny_freerun.ini (Mode B). Mode B was speed-tested successfully up to 20 kHz same day, though the saved omny_freerun.ini itself reflects CLOCK1.PERIOD=0.0001 (10 kHz) -- flagged in the setup doc as a discrepancy to double-check rather than assumed away. Setup doc updated with the exact save/load commands and a diff summary of the two layouts' mode-specific differences (PCAP.GATE/TRIG source, CLOCK1.ENABLE, capture types). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/devices/panda_box/layouts/omny.ini | 876 +++++++++++++++++ .../panda_box/layouts/omny_freerun.ini | 877 ++++++++++++++++++ .../developer/panda_box_free_running_setup.md | 43 +- 3 files changed, 1788 insertions(+), 8 deletions(-) create mode 100644 csaxs_bec/devices/panda_box/layouts/omny.ini create mode 100644 csaxs_bec/devices/panda_box/layouts/omny_freerun.ini diff --git a/csaxs_bec/devices/panda_box/layouts/omny.ini b/csaxs_bec/devices/panda_box/layouts/omny.ini new file mode 100644 index 00000000..e2c505bf --- /dev/null +++ b/csaxs_bec/devices/panda_box/layouts/omny.ini @@ -0,0 +1,876 @@ +OUTENC1.DATA.DELAY=0 +OUTENC2.DATA.DELAY=0 +OUTENC3.DATA.DELAY=0 +OUTENC4.DATA.DELAY=0 +OUTENC1.A.DELAY=0 +OUTENC2.A.DELAY=0 +OUTENC3.A.DELAY=0 +OUTENC4.A.DELAY=0 +OUTENC1.QPERIOD.UNITS=s +OUTENC2.QPERIOD.UNITS=s +OUTENC3.QPERIOD.UNITS=s +OUTENC4.QPERIOD.UNITS=s +OUTENC1.Z.DELAY=0 +OUTENC2.Z.DELAY=0 +OUTENC3.Z.DELAY=0 +OUTENC4.Z.DELAY=0 +OUTENC1.B.DELAY=0 +OUTENC2.B.DELAY=0 +OUTENC3.B.DELAY=0 +OUTENC4.B.DELAY=0 +OUTENC1.ENABLE.DELAY=0 +OUTENC2.ENABLE.DELAY=0 +OUTENC3.ENABLE.DELAY=0 +OUTENC4.ENABLE.DELAY=0 +PCOMP1.ENABLE.DELAY=0 +PCOMP2.ENABLE.DELAY=0 +TTLOUT1.VAL.DELAY=0 +TTLOUT2.VAL.DELAY=0 +TTLOUT3.VAL.DELAY=0 +TTLOUT4.VAL.DELAY=0 +TTLOUT5.VAL.DELAY=0 +TTLOUT6.VAL.DELAY=0 +TTLOUT7.VAL.DELAY=0 +TTLOUT8.VAL.DELAY=0 +TTLOUT9.VAL.DELAY=0 +TTLOUT10.VAL.DELAY=0 +PCAP.TS_END.CAPTURE=No +PCAP.GATE.DELAY=1 +PCAP.TS_START.CAPTURE=No +PCAP.ENABLE.DELAY=0 +PCAP.BITS2.CAPTURE=No +PCAP.BITS0.CAPTURE=No +PCAP.BITS3.CAPTURE=No +PCAP.BITS1.CAPTURE=No +PCAP.GATE_DURATION.CAPTURE=Value +PCAP.TS_TRIG.CAPTURE=No +PCAP.TRIG.DELAY=1 +SFP3_SYNC_IN.POS3.UNITS= +SFP3_SYNC_IN.POS3.OFFSET=0 +SFP3_SYNC_IN.POS3.SCALE=1 +SFP3_SYNC_IN.POS3.CAPTURE=No +SFP3_SYNC_IN.POS4.UNITS= +SFP3_SYNC_IN.POS4.OFFSET=0 +SFP3_SYNC_IN.POS4.SCALE=1 +SFP3_SYNC_IN.POS4.CAPTURE=No +SFP3_SYNC_IN.POS2.UNITS= +SFP3_SYNC_IN.POS2.OFFSET=0 +SFP3_SYNC_IN.POS2.SCALE=1 +SFP3_SYNC_IN.POS2.CAPTURE=No +SFP3_SYNC_IN.POS1.UNITS= +SFP3_SYNC_IN.POS1.OFFSET=0 +SFP3_SYNC_IN.POS1.SCALE=1 +SFP3_SYNC_IN.POS1.CAPTURE=No +DIV1.INP.DELAY=0 +DIV2.INP.DELAY=0 +DIV1.ENABLE.DELAY=0 +DIV2.ENABLE.DELAY=0 +INENC1.CLK.DELAY=0 +INENC2.CLK.DELAY=0 +INENC3.CLK.DELAY=0 +INENC4.CLK.DELAY=0 +INENC1.VAL.UNITS= +INENC2.VAL.UNITS= +INENC3.VAL.UNITS= +INENC4.VAL.UNITS= +INENC1.VAL.OFFSET=0 +INENC2.VAL.OFFSET=0 +INENC3.VAL.OFFSET=0 +INENC4.VAL.OFFSET=0 +INENC1.VAL.SCALE=1 +INENC2.VAL.SCALE=1 +INENC3.VAL.SCALE=1 +INENC4.VAL.SCALE=1 +INENC1.VAL.CAPTURE=Min Max Mean +INENC2.VAL.CAPTURE=Min Max Mean +INENC3.VAL.CAPTURE=Min Max Mean +INENC4.VAL.CAPTURE=Min Max Mean +INENC1.CLK_PERIOD.UNITS=s +INENC2.CLK_PERIOD.UNITS=s +INENC3.CLK_PERIOD.UNITS=s +INENC4.CLK_PERIOD.UNITS=s +INENC1.FRAME_PERIOD.UNITS=s +INENC2.FRAME_PERIOD.UNITS=s +INENC3.FRAME_PERIOD.UNITS=s +INENC4.FRAME_PERIOD.UNITS=s +SFP3_SYNC_OUT.BIT8.DELAY=0 +SFP3_SYNC_OUT.BIT1.DELAY=0 +SFP3_SYNC_OUT.BIT3.DELAY=0 +SFP3_SYNC_OUT.BIT2.DELAY=0 +SFP3_SYNC_OUT.BIT5.DELAY=0 +SFP3_SYNC_OUT.BIT4.DELAY=0 +SFP3_SYNC_OUT.BIT7.DELAY=0 +SFP3_SYNC_OUT.BIT6.DELAY=0 +SEQ1.ENABLE.DELAY=0 +SEQ2.ENABLE.DELAY=0 +SEQ1.PRESCALE.UNITS=s +SEQ2.PRESCALE.UNITS=s +SEQ1.BITA.DELAY=0 +SEQ2.BITA.DELAY=0 +SEQ1.BITC.DELAY=0 +SEQ2.BITC.DELAY=0 +SEQ1.BITB.DELAY=0 +SEQ2.BITB.DELAY=0 +SEQ1.TABLE.QUEUED_LINES=0 +SEQ2.TABLE.QUEUED_LINES=0 +SRGATE1.SET.DELAY=0 +SRGATE2.SET.DELAY=0 +SRGATE3.SET.DELAY=0 +SRGATE4.SET.DELAY=0 +SRGATE1.ENABLE.DELAY=0 +SRGATE2.ENABLE.DELAY=0 +SRGATE3.ENABLE.DELAY=0 +SRGATE4.ENABLE.DELAY=0 +SRGATE1.RST.DELAY=0 +SRGATE2.RST.DELAY=0 +SRGATE3.RST.DELAY=0 +SRGATE4.RST.DELAY=0 +LUT1.INPB.DELAY=0 +LUT2.INPB.DELAY=0 +LUT3.INPB.DELAY=0 +LUT4.INPB.DELAY=0 +LUT5.INPB.DELAY=0 +LUT6.INPB.DELAY=0 +LUT7.INPB.DELAY=0 +LUT8.INPB.DELAY=0 +LUT1.INPC.DELAY=0 +LUT2.INPC.DELAY=0 +LUT3.INPC.DELAY=0 +LUT4.INPC.DELAY=0 +LUT5.INPC.DELAY=0 +LUT6.INPC.DELAY=0 +LUT7.INPC.DELAY=0 +LUT8.INPC.DELAY=0 +LUT1.INPA.DELAY=0 +LUT2.INPA.DELAY=0 +LUT3.INPA.DELAY=0 +LUT4.INPA.DELAY=0 +LUT5.INPA.DELAY=0 +LUT6.INPA.DELAY=0 +LUT7.INPA.DELAY=0 +LUT8.INPA.DELAY=0 +LUT1.INPD.DELAY=0 +LUT2.INPD.DELAY=0 +LUT3.INPD.DELAY=0 +LUT4.INPD.DELAY=0 +LUT5.INPD.DELAY=0 +LUT6.INPD.DELAY=0 +LUT7.INPD.DELAY=0 +LUT8.INPD.DELAY=0 +LUT1.INPE.DELAY=0 +LUT2.INPE.DELAY=0 +LUT3.INPE.DELAY=0 +LUT4.INPE.DELAY=0 +LUT5.INPE.DELAY=0 +LUT6.INPE.DELAY=0 +LUT7.INPE.DELAY=0 +LUT8.INPE.DELAY=0 +CALC1.OUT.UNITS= +CALC2.OUT.UNITS= +CALC1.OUT.OFFSET=0 +CALC2.OUT.OFFSET=0 +CALC1.OUT.SCALE=1 +CALC2.OUT.SCALE=1 +CALC1.OUT.CAPTURE=No +CALC2.OUT.CAPTURE=No +LVDSOUT1.VAL.DELAY=0 +LVDSOUT2.VAL.DELAY=0 +CLOCK1.ENABLE.DELAY=0 +CLOCK2.ENABLE.DELAY=0 +CLOCK1.PERIOD.UNITS=s +CLOCK2.PERIOD.UNITS=s +CLOCK1.WIDTH.UNITS=s +CLOCK2.WIDTH.UNITS=us +COUNTER1.OUT.UNITS= +COUNTER2.OUT.UNITS= +COUNTER3.OUT.UNITS= +COUNTER4.OUT.UNITS= +COUNTER5.OUT.UNITS= +COUNTER6.OUT.UNITS= +COUNTER7.OUT.UNITS= +COUNTER8.OUT.UNITS= +COUNTER1.OUT.OFFSET=0 +COUNTER2.OUT.OFFSET=0 +COUNTER3.OUT.OFFSET=0 +COUNTER4.OUT.OFFSET=0 +COUNTER5.OUT.OFFSET=0 +COUNTER6.OUT.OFFSET=0 +COUNTER7.OUT.OFFSET=0 +COUNTER8.OUT.OFFSET=0 +COUNTER1.OUT.SCALE=1 +COUNTER2.OUT.SCALE=1 +COUNTER3.OUT.SCALE=1 +COUNTER4.OUT.SCALE=1 +COUNTER5.OUT.SCALE=1 +COUNTER6.OUT.SCALE=1 +COUNTER7.OUT.SCALE=1 +COUNTER8.OUT.SCALE=1 +COUNTER1.OUT.CAPTURE=No +COUNTER2.OUT.CAPTURE=No +COUNTER3.OUT.CAPTURE=No +COUNTER4.OUT.CAPTURE=No +COUNTER5.OUT.CAPTURE=No +COUNTER6.OUT.CAPTURE=No +COUNTER7.OUT.CAPTURE=No +COUNTER8.OUT.CAPTURE=No +COUNTER1.DIR.DELAY=0 +COUNTER2.DIR.DELAY=0 +COUNTER3.DIR.DELAY=0 +COUNTER4.DIR.DELAY=0 +COUNTER5.DIR.DELAY=0 +COUNTER6.DIR.DELAY=0 +COUNTER7.DIR.DELAY=0 +COUNTER8.DIR.DELAY=0 +COUNTER1.ENABLE.DELAY=0 +COUNTER2.ENABLE.DELAY=0 +COUNTER3.ENABLE.DELAY=0 +COUNTER4.ENABLE.DELAY=0 +COUNTER5.ENABLE.DELAY=0 +COUNTER6.ENABLE.DELAY=0 +COUNTER7.ENABLE.DELAY=0 +COUNTER8.ENABLE.DELAY=0 +COUNTER1.TRIG.DELAY=0 +COUNTER2.TRIG.DELAY=0 +COUNTER3.TRIG.DELAY=0 +COUNTER4.TRIG.DELAY=0 +COUNTER5.TRIG.DELAY=0 +COUNTER6.TRIG.DELAY=0 +COUNTER7.TRIG.DELAY=0 +COUNTER8.TRIG.DELAY=0 +PULSE1.ENABLE.DELAY=0 +PULSE2.ENABLE.DELAY=0 +PULSE3.ENABLE.DELAY=0 +PULSE4.ENABLE.DELAY=0 +PULSE1.DELAY.UNITS=s +PULSE2.DELAY.UNITS=s +PULSE3.DELAY.UNITS=s +PULSE4.DELAY.UNITS=s +PULSE1.TRIG.DELAY=0 +PULSE2.TRIG.DELAY=0 +PULSE3.TRIG.DELAY=0 +PULSE4.TRIG.DELAY=0 +PULSE1.WIDTH.UNITS=s +PULSE2.WIDTH.UNITS=s +PULSE3.WIDTH.UNITS=s +PULSE4.WIDTH.UNITS=s +PULSE1.STEP.UNITS=s +PULSE2.STEP.UNITS=s +PULSE3.STEP.UNITS=s +PULSE4.STEP.UNITS=s +FILTER1.OUT.UNITS= +FILTER2.OUT.UNITS= +FILTER1.OUT.OFFSET=0 +FILTER2.OUT.OFFSET=0 +FILTER1.OUT.SCALE=1 +FILTER2.OUT.SCALE=1 +FILTER1.OUT.CAPTURE=No +FILTER2.OUT.CAPTURE=No +FILTER1.ENABLE.DELAY=0 +FILTER2.ENABLE.DELAY=0 +FILTER1.TRIG.DELAY=0 +FILTER2.TRIG.DELAY=0 +FMC_IN.VAL8.UNITS=V +FMC_IN.VAL8.OFFSET=0 +FMC_IN.VAL8.SCALE=4.65661287e-09 +FMC_IN.VAL8.CAPTURE=No +FMC_IN.VAL1.UNITS=V +FMC_IN.VAL1.OFFSET=0 +FMC_IN.VAL1.SCALE=4.65661287e-09 +FMC_IN.VAL1.CAPTURE=Min Max Mean +FMC_IN.VAL3.UNITS=V +FMC_IN.VAL3.OFFSET=0 +FMC_IN.VAL3.SCALE=4.65661287e-09 +FMC_IN.VAL3.CAPTURE=No +FMC_IN.VAL2.UNITS=V +FMC_IN.VAL2.OFFSET=0 +FMC_IN.VAL2.SCALE=4.65661287e-09 +FMC_IN.VAL2.CAPTURE=Min Max Mean +FMC_IN.VAL5.UNITS=V +FMC_IN.VAL5.OFFSET=0 +FMC_IN.VAL5.SCALE=4.65661287e-09 +FMC_IN.VAL5.CAPTURE=No +FMC_IN.VAL4.UNITS=V +FMC_IN.VAL4.OFFSET=0 +FMC_IN.VAL4.SCALE=4.65661287e-09 +FMC_IN.VAL4.CAPTURE=No +FMC_IN.VAL7.UNITS=V +FMC_IN.VAL7.OFFSET=0 +FMC_IN.VAL7.SCALE=4.65661287e-09 +FMC_IN.VAL7.CAPTURE=No +FMC_IN.VAL6.UNITS=V +FMC_IN.VAL6.OFFSET=0 +FMC_IN.VAL6.SCALE=4.65661287e-09 +FMC_IN.VAL6.CAPTURE=No +PGEN1.OUT.UNITS= +PGEN2.OUT.UNITS= +PGEN1.OUT.OFFSET=0 +PGEN2.OUT.OFFSET=0 +PGEN1.OUT.SCALE=1 +PGEN2.OUT.SCALE=1 +PGEN1.OUT.CAPTURE=No +PGEN2.OUT.CAPTURE=No +PGEN1.ENABLE.DELAY=0 +PGEN2.ENABLE.DELAY=0 +PGEN1.TABLE.QUEUED_LINES=0 +PGEN2.TABLE.QUEUED_LINES=0 +PGEN1.TRIG.DELAY=0 +PGEN2.TRIG.DELAY=0 +TTLIN1.TERM=High-Z +TTLIN2.TERM=High-Z +TTLIN3.TERM=High-Z +TTLIN4.TERM=High-Z +TTLIN5.TERM=High-Z +TTLIN6.TERM=High-Z +OUTENC1.DATA=ZERO +OUTENC2.DATA=ZERO +OUTENC3.DATA=ZERO +OUTENC4.DATA=ZERO +OUTENC1.A=ZERO +OUTENC2.A=ZERO +OUTENC3.A=ZERO +OUTENC4.A=ZERO +OUTENC1.ENCODING=Unsigned Binary +OUTENC2.ENCODING=Unsigned Binary +OUTENC3.ENCODING=Unsigned Binary +OUTENC4.ENCODING=Unsigned Binary +OUTENC1.QPERIOD=0 +OUTENC2.QPERIOD=0 +OUTENC3.QPERIOD=0 +OUTENC4.QPERIOD=0 +OUTENC1.Z=ZERO +OUTENC2.Z=ZERO +OUTENC3.Z=ZERO +OUTENC4.Z=ZERO +OUTENC1.B=ZERO +OUTENC2.B=ZERO +OUTENC3.B=ZERO +OUTENC4.B=ZERO +OUTENC1.VAL=ZERO +OUTENC2.VAL=ZERO +OUTENC3.VAL=ZERO +OUTENC4.VAL=ZERO +OUTENC1.PROTOCOL=Quadrature +OUTENC2.PROTOCOL=Quadrature +OUTENC3.PROTOCOL=Quadrature +OUTENC4.PROTOCOL=Quadrature +OUTENC1.ENABLE=ZERO +OUTENC2.ENABLE=ZERO +OUTENC3.ENABLE=ZERO +OUTENC4.ENABLE=ZERO +OUTENC1.GENERATOR_ERROR=No +OUTENC2.GENERATOR_ERROR=No +OUTENC3.GENERATOR_ERROR=No +OUTENC4.GENERATOR_ERROR=No +OUTENC1.BITS=0 +OUTENC2.BITS=0 +OUTENC3.BITS=0 +OUTENC4.BITS=0 +PCOMP1.PULSES=0 +PCOMP2.PULSES=0 +PCOMP1.PRE_START=0 +PCOMP2.PRE_START=0 +PCOMP1.ENABLE=ZERO +PCOMP2.ENABLE=ZERO +PCOMP1.DIR=Positive +PCOMP2.DIR=Positive +PCOMP1.START=0 +PCOMP2.START=0 +PCOMP1.WIDTH=0 +PCOMP2.WIDTH=0 +PCOMP1.INP=ZERO +PCOMP2.INP=ZERO +PCOMP1.RELATIVE=Absolute +PCOMP2.RELATIVE=Absolute +PCOMP1.STEP=0 +PCOMP2.STEP=0 +TTLOUT1.FINE_DELAY=0 +TTLOUT2.FINE_DELAY=0 +TTLOUT3.FINE_DELAY=0 +TTLOUT4.FINE_DELAY=0 +TTLOUT5.FINE_DELAY=0 +TTLOUT6.FINE_DELAY=0 +TTLOUT7.FINE_DELAY=0 +TTLOUT8.FINE_DELAY=0 +TTLOUT9.FINE_DELAY=0 +TTLOUT10.FINE_DELAY=0 +TTLOUT1.QUARTER_DELAY=0 +TTLOUT2.QUARTER_DELAY=0 +TTLOUT3.QUARTER_DELAY=0 +TTLOUT4.QUARTER_DELAY=0 +TTLOUT5.QUARTER_DELAY=0 +TTLOUT6.QUARTER_DELAY=0 +TTLOUT7.QUARTER_DELAY=0 +TTLOUT8.QUARTER_DELAY=0 +TTLOUT9.QUARTER_DELAY=0 +TTLOUT10.QUARTER_DELAY=0 +TTLOUT1.VAL=ZERO +TTLOUT2.VAL=ZERO +TTLOUT3.VAL=ZERO +TTLOUT4.VAL=ZERO +TTLOUT5.VAL=ZERO +TTLOUT6.VAL=ZERO +TTLOUT7.VAL=ZERO +TTLOUT8.VAL=ZERO +TTLOUT9.VAL=ZERO +TTLOUT10.VAL=ZERO +PCAP.GATE=TTLIN1.VAL +PCAP.ENABLE=ONE +PCAP.SHIFT_SUM=0 +PCAP.TRIG_EDGE=Falling +PCAP.TRIG=TTLIN1.VAL +DIV1.DIVISOR=0 +DIV2.DIVISOR=0 +DIV1.FIRST_PULSE=OutN +DIV2.FIRST_PULSE=OutN +DIV1.INP=ZERO +DIV2.INP=ZERO +DIV1.ENABLE=ZERO +DIV2.ENABLE=ZERO +INENC1.ENCODING=Unsigned Binary +INENC2.ENCODING=Unsigned Binary +INENC3.ENCODING=Unsigned Binary +INENC4.ENCODING=Unsigned Binary +INENC1.CLK=ZERO +INENC2.CLK=ZERO +INENC3.CLK=ZERO +INENC4.CLK=ZERO +INENC1.MSB_DISCARD=0 +INENC2.MSB_DISCARD=0 +INENC3.MSB_DISCARD=0 +INENC4.MSB_DISCARD=0 +INENC1.LSB_DISCARD=0 +INENC2.LSB_DISCARD=0 +INENC3.LSB_DISCARD=0 +INENC4.LSB_DISCARD=0 +INENC1.CLK_SRC=Internally Generated +INENC2.CLK_SRC=Internally Generated +INENC3.CLK_SRC=Internally Generated +INENC4.CLK_SRC=Internally Generated +INENC1.PROTOCOL=ZMI +INENC2.PROTOCOL=ZMI +INENC3.PROTOCOL=ZMI +INENC4.PROTOCOL=ZMI +INENC1.RST_ON_Z=0 +INENC2.RST_ON_Z=0 +INENC3.RST_ON_Z=0 +INENC4.RST_ON_Z=0 +INENC1.CLK_PERIOD=0.2 +INENC2.CLK_PERIOD=0 +INENC3.CLK_PERIOD=0 +INENC4.CLK_PERIOD=0 +INENC1.BITS=0 +INENC2.BITS=0 +INENC3.BITS=0 +INENC4.BITS=0 +INENC1.FRAME_PERIOD=0.2 +INENC2.FRAME_PERIOD=0 +INENC3.FRAME_PERIOD=0 +INENC4.FRAME_PERIOD=0 +BITS.A=0 +BITS.C=0 +BITS.D=0 +BITS.B=1 +SFP3_SYNC_OUT.POS4=ZERO +SFP3_SYNC_OUT.POS2=ZERO +SFP3_SYNC_OUT.POS3=ZERO +SFP3_SYNC_OUT.POS1=ZERO +SFP3_SYNC_OUT.BIT8=ZERO +SFP3_SYNC_OUT.BIT1=ZERO +SFP3_SYNC_OUT.BIT3=ZERO +SFP3_SYNC_OUT.BIT2=ZERO +SFP3_SYNC_OUT.BIT5=ZERO +SFP3_SYNC_OUT.BIT4=ZERO +SFP3_SYNC_OUT.BIT7=ZERO +SFP3_SYNC_OUT.BIT6=ZERO +SEQ1.ENABLE=ZERO +SEQ2.ENABLE=ZERO +SEQ1.PRESCALE=0 +SEQ2.PRESCALE=0 +SEQ1.BITA=ZERO +SEQ2.BITA=ZERO +SEQ1.BITC=ZERO +SEQ2.BITC=ZERO +SEQ1.BITB=ZERO +SEQ2.BITB=ZERO +SEQ1.REPEATS=0 +SEQ2.REPEATS=0 +SEQ1.POSB=ZERO +SEQ2.POSB=ZERO +SEQ1.POSC=ZERO +SEQ2.POSC=ZERO +SEQ1.POSA=ZERO +SEQ2.POSA=ZERO +SYSTEM.CLOCK_SOURCE=int clock +SYSTEM.TIMESTAMP_SOURCE=None +SRGATE1.WHEN_DISABLED=Set output low +SRGATE2.WHEN_DISABLED=Set output low +SRGATE3.WHEN_DISABLED=Set output low +SRGATE4.WHEN_DISABLED=Set output low +SRGATE1.RST_EDGE=Rising +SRGATE2.RST_EDGE=Rising +SRGATE3.RST_EDGE=Rising +SRGATE4.RST_EDGE=Rising +SRGATE1.SET=ZERO +SRGATE2.SET=ZERO +SRGATE3.SET=ZERO +SRGATE4.SET=ZERO +SRGATE1.SET_EDGE=Rising +SRGATE2.SET_EDGE=Rising +SRGATE3.SET_EDGE=Rising +SRGATE4.SET_EDGE=Rising +SRGATE1.ENABLE=ZERO +SRGATE2.ENABLE=ZERO +SRGATE3.ENABLE=ZERO +SRGATE4.ENABLE=ZERO +SRGATE1.RST=ZERO +SRGATE2.RST=ZERO +SRGATE3.RST=ZERO +SRGATE4.RST=ZERO +FMC_OUT.GAIN4=5V +FMC_OUT.GAIN1=5V +FMC_OUT.GAIN3=5V +FMC_OUT.GAIN2=5V +FMC_OUT.VAL1=ZERO +FMC_OUT.VAL3=ZERO +FMC_OUT.VAL2=ZERO +FMC_OUT.VAL4=ZERO +LUT1.INPB=ZERO +LUT2.INPB=ZERO +LUT3.INPB=ZERO +LUT4.INPB=ZERO +LUT5.INPB=ZERO +LUT6.INPB=ZERO +LUT7.INPB=ZERO +LUT8.INPB=ZERO +LUT1.INPC=ZERO +LUT2.INPC=ZERO +LUT3.INPC=ZERO +LUT4.INPC=ZERO +LUT5.INPC=ZERO +LUT6.INPC=ZERO +LUT7.INPC=ZERO +LUT8.INPC=ZERO +LUT1.INPA=ZERO +LUT2.INPA=ZERO +LUT3.INPA=ZERO +LUT4.INPA=ZERO +LUT5.INPA=ZERO +LUT6.INPA=ZERO +LUT7.INPA=ZERO +LUT8.INPA=ZERO +LUT1.INPD=ZERO +LUT2.INPD=ZERO +LUT3.INPD=ZERO +LUT4.INPD=ZERO +LUT5.INPD=ZERO +LUT6.INPD=ZERO +LUT7.INPD=ZERO +LUT8.INPD=ZERO +LUT1.INPE=ZERO +LUT2.INPE=ZERO +LUT3.INPE=ZERO +LUT4.INPE=ZERO +LUT5.INPE=ZERO +LUT6.INPE=ZERO +LUT7.INPE=ZERO +LUT8.INPE=ZERO +LUT1.FUNC=0x00000000 +LUT2.FUNC=0x00000000 +LUT3.FUNC=0x00000000 +LUT4.FUNC=0x00000000 +LUT5.FUNC=0x00000000 +LUT6.FUNC=0x00000000 +LUT7.FUNC=0x00000000 +LUT8.FUNC=0x00000000 +LUT1.TYPEA=Input-Level +LUT2.TYPEA=Input-Level +LUT3.TYPEA=Input-Level +LUT4.TYPEA=Input-Level +LUT5.TYPEA=Input-Level +LUT6.TYPEA=Input-Level +LUT7.TYPEA=Input-Level +LUT8.TYPEA=Input-Level +LUT1.TYPEC=Input-Level +LUT2.TYPEC=Input-Level +LUT3.TYPEC=Input-Level +LUT4.TYPEC=Input-Level +LUT5.TYPEC=Input-Level +LUT6.TYPEC=Input-Level +LUT7.TYPEC=Input-Level +LUT8.TYPEC=Input-Level +LUT1.TYPEB=Input-Level +LUT2.TYPEB=Input-Level +LUT3.TYPEB=Input-Level +LUT4.TYPEB=Input-Level +LUT5.TYPEB=Input-Level +LUT6.TYPEB=Input-Level +LUT7.TYPEB=Input-Level +LUT8.TYPEB=Input-Level +LUT1.TYPEE=Input-Level +LUT2.TYPEE=Input-Level +LUT3.TYPEE=Input-Level +LUT4.TYPEE=Input-Level +LUT5.TYPEE=Input-Level +LUT6.TYPEE=Input-Level +LUT7.TYPEE=Input-Level +LUT8.TYPEE=Input-Level +LUT1.TYPED=Input-Level +LUT2.TYPED=Input-Level +LUT3.TYPED=Input-Level +LUT4.TYPED=Input-Level +LUT5.TYPED=Input-Level +LUT6.TYPED=Input-Level +LUT7.TYPED=Input-Level +LUT8.TYPED=Input-Level +CALC1.INPC=ZERO +CALC2.INPC=ZERO +CALC1.INPA=ZERO +CALC2.INPA=ZERO +CALC1.SHIFT=0 +CALC2.SHIFT=0 +CALC1.INPB=ZERO +CALC2.INPB=ZERO +CALC1.INPD=ZERO +CALC2.INPD=ZERO +CALC1.TYPEA=Value +CALC2.TYPEA=Value +CALC1.TYPEC=Value +CALC2.TYPEC=Value +CALC1.TYPEB=Value +CALC2.TYPEB=Value +CALC1.TYPED=Value +CALC2.TYPED=Value +LVDSOUT1.FINE_DELAY=0 +LVDSOUT2.FINE_DELAY=0 +LVDSOUT1.QUARTER_DELAY=0 +LVDSOUT2.QUARTER_DELAY=0 +LVDSOUT1.VAL=ZERO +LVDSOUT2.VAL=ZERO +CLOCK1.ENABLE=ZERO +CLOCK2.ENABLE=ZERO +CLOCK1.PERIOD=1 +CLOCK2.PERIOD=0.2 +CLOCK1.WIDTH=3e-05 +CLOCK2.WIDTH=5 +COUNTER1.OUT_MODE=On-Change +COUNTER2.OUT_MODE=On-Change +COUNTER3.OUT_MODE=On-Change +COUNTER4.OUT_MODE=On-Change +COUNTER5.OUT_MODE=On-Change +COUNTER6.OUT_MODE=On-Change +COUNTER7.OUT_MODE=On-Change +COUNTER8.OUT_MODE=On-Change +COUNTER1.SET=0 +COUNTER2.SET=0 +COUNTER3.SET=0 +COUNTER4.SET=0 +COUNTER5.SET=0 +COUNTER6.SET=0 +COUNTER7.SET=0 +COUNTER8.SET=0 +COUNTER1.DIR=ZERO +COUNTER2.DIR=ZERO +COUNTER3.DIR=ZERO +COUNTER4.DIR=ZERO +COUNTER5.DIR=ZERO +COUNTER6.DIR=ZERO +COUNTER7.DIR=ZERO +COUNTER8.DIR=ZERO +COUNTER1.ENABLE=PCAP.ACTIVE +COUNTER2.ENABLE=ZERO +COUNTER3.ENABLE=ZERO +COUNTER4.ENABLE=ZERO +COUNTER5.ENABLE=ZERO +COUNTER6.ENABLE=ZERO +COUNTER7.ENABLE=ZERO +COUNTER8.ENABLE=ZERO +COUNTER1.TRIG=TTLIN1.VAL +COUNTER2.TRIG=ZERO +COUNTER3.TRIG=ZERO +COUNTER4.TRIG=ZERO +COUNTER5.TRIG=ZERO +COUNTER6.TRIG=ZERO +COUNTER7.TRIG=ZERO +COUNTER8.TRIG=ZERO +COUNTER1.TRIG_EDGE=Falling +COUNTER2.TRIG_EDGE=Rising +COUNTER3.TRIG_EDGE=Rising +COUNTER4.TRIG_EDGE=Rising +COUNTER5.TRIG_EDGE=Rising +COUNTER6.TRIG_EDGE=Rising +COUNTER7.TRIG_EDGE=Rising +COUNTER8.TRIG_EDGE=Rising +COUNTER1.MIN=0 +COUNTER2.MIN=0 +COUNTER3.MIN=0 +COUNTER4.MIN=0 +COUNTER5.MIN=0 +COUNTER6.MIN=0 +COUNTER7.MIN=0 +COUNTER8.MIN=0 +COUNTER1.MAX=0 +COUNTER2.MAX=0 +COUNTER3.MAX=0 +COUNTER4.MAX=0 +COUNTER5.MAX=0 +COUNTER6.MAX=0 +COUNTER7.MAX=0 +COUNTER8.MAX=0 +COUNTER1.START=0 +COUNTER2.START=0 +COUNTER3.START=0 +COUNTER4.START=0 +COUNTER5.START=0 +COUNTER6.START=0 +COUNTER7.START=0 +COUNTER8.START=0 +COUNTER1.STEP=1 +COUNTER2.STEP=1 +COUNTER3.STEP=0 +COUNTER4.STEP=0 +COUNTER5.STEP=0 +COUNTER6.STEP=0 +COUNTER7.STEP=0 +COUNTER8.STEP=0 +PULSE1.PULSES=0 +PULSE2.PULSES=0 +PULSE3.PULSES=0 +PULSE4.PULSES=0 +PULSE1.ENABLE=ONE +PULSE2.ENABLE=ZERO +PULSE3.ENABLE=ZERO +PULSE4.ENABLE=ZERO +PULSE1.DELAY=0 +PULSE2.DELAY=0.2 +PULSE3.DELAY=0.3 +PULSE4.DELAY=0.4 +PULSE1.TRIG=ZERO +PULSE2.TRIG=ZERO +PULSE3.TRIG=ZERO +PULSE4.TRIG=ZERO +PULSE1.TRIG_EDGE=Rising +PULSE2.TRIG_EDGE=Rising +PULSE3.TRIG_EDGE=Rising +PULSE4.TRIG_EDGE=Rising +PULSE1.WIDTH=0.001 +PULSE2.WIDTH=0.1 +PULSE3.WIDTH=0.1 +PULSE4.WIDTH=0.1 +PULSE1.STEP=0 +PULSE2.STEP=0 +PULSE3.STEP=0 +PULSE4.STEP=0 +FILTER1.INP=ZERO +FILTER2.INP=ZERO +FILTER1.MODE=average +FILTER2.MODE=difference +FILTER1.ENABLE=ZERO +FILTER2.ENABLE=ZERO +FILTER1.TRIG=ZERO +FILTER2.TRIG=ZERO +FMC_IN.GAIN5=10V +FMC_IN.GAIN4=10V +FMC_IN.GAIN7=10V +FMC_IN.GAIN6=10V +FMC_IN.GAIN1=10V +FMC_IN.GAIN3=10V +FMC_IN.GAIN2=10V +FMC_IN.GAIN8=10V +PGEN1.REPEATS=0 +PGEN2.REPEATS=0 +PGEN1.ENABLE=ZERO +PGEN2.ENABLE=ZERO +PGEN1.TRIG=ZERO +PGEN2.TRIG=ZERO +*METADATA.LABEL_CALC1= +*METADATA.LABEL_FILTER1= +*METADATA.LABEL_CALC2= +*METADATA.LABEL_SRGATE4= +*METADATA.LABEL_FMC_IN1= +*METADATA.LABEL_TTLIN6= +*METADATA.LABEL_SRGATE1= +*METADATA.LABEL_TTLIN4= +*METADATA.LABEL_TTLIN5= +*METADATA.LABEL_TTLIN2=TTL input 2 +*METADATA.LABEL_TTLIN3= +*METADATA.LABEL_CLOCK1= +*METADATA.LABEL_TTLIN1=TTL input 1 +*METADATA.LABEL_PCOMP1= +*METADATA.LABEL_LUT8= +*METADATA.LABEL_BITS1=Soft inputs and constant bits +*METADATA.LABEL_COUNTER8= +*METADATA.LABEL_COUNTER7= +*METADATA.LABEL_COUNTER6= +*METADATA.LABEL_COUNTER5= +*METADATA.LABEL_COUNTER4= +*METADATA.LABEL_COUNTER3= +*METADATA.LABEL_COUNTER2= +*METADATA.LABEL_COUNTER1= +*METADATA.LABEL_TTLOUT3=TTL output 3 +*METADATA.LABEL_OUTENC2= +*METADATA.LABEL_OUTENC3= +*METADATA.LABEL_SYSTEM1=System control FPGA +*METADATA.LABEL_OUTENC1= +*METADATA.LABEL_PGEN2= +*METADATA.LABEL_OUTENC4= +*METADATA.LABEL_PGEN1= +*METADATA.LABEL_LUT2= +*METADATA.LABEL_LUT3= +*METADATA.LABEL_LUT1=Lookup table 1 +*METADATA.LABEL_LUT6= +*METADATA.LABEL_LUT7= +*METADATA.LABEL_LUT4= +*METADATA.LABEL_LUT5= +*METADATA.LABEL_LVDSOUT2= +*METADATA.LABEL_LVDSOUT1= +*METADATA.LABEL_SFP3_SYNC_OUT1= +*METADATA.LABEL_PULSE3= +*METADATA.LABEL_TTLOUT7= +*METADATA.LABEL_SRGATE2= +*METADATA.LABEL_LVDSIN2= +*METADATA.LABEL_LVDSIN1= +*METADATA.LABEL_SEQ1= +*METADATA.LABEL_CLOCK2= +*METADATA.LABEL_PULSE4= +*METADATA.LABEL_INENC1= +*METADATA.LABEL_INENC2= +*METADATA.LABEL_INENC3= +*METADATA.LABEL_INENC4= +*METADATA.LABEL_PULSE1= +*METADATA.LABEL_PULSE2= +*METADATA.LABEL_FILTER2= +*METADATA.LABEL_TTLOUT10= +*METADATA.LABEL_SEQ2= +*METADATA.LABEL_PCOMP2= +*METADATA.LABEL_DIV1= +*METADATA.LABEL_SRGATE3= +*METADATA.LABEL_FMC_OUT1= +*METADATA.LABEL_DIV2= +*METADATA.LABEL_SFP3_SYNC_IN1= +*METADATA.LABEL_TTLOUT8= +*METADATA.LABEL_TTLOUT9= +*METADATA.LABEL_TTLOUT6= +*METADATA.LABEL_PCAP1=Position capture control +*METADATA.LABEL_TTLOUT4=TTL output 4 +*METADATA.LABEL_TTLOUT5= +*METADATA.LABEL_TTLOUT2=TTL output 2 +*METADATA.DESIGN= +*METADATA.LABEL_TTLOUT1=TTL output 1 +SEQ1.TABLE **Status: Mode B confirmed working end-to-end against real PandA hardware > (2026-09-15, omny-panda.psi.ch)** — correct per-channel raw values, masked -> `gate_detector_active`, and `frame_counter`-based completion all verified. -> Config switched back to Mode A afterward for A/B comparison testing (see -> "Switching between modes" below); re-enable `omny_panda_continuous` to test -> Mode B again. Remaining TODO: speed testing beyond the initial 100 Hz. +> `gate_detector_active`, and `frame_counter`-based completion all verified, +> then speed-tested up to 20 kHz with no reported issues (though see the +> caveat under "Saving / restoring layouts" — the saved `omny_freerun.ini` +> layout itself was captured at 10 kHz, not 20 kHz). `ptycho_flomni.yaml` +> currently has `omny_panda_continuous` (Mode B) active — note the PandA's +> Design was switched back to `OMNY` on 2026-09-15 solely to capture the +> `omny.ini` layout file below, so double-check which Design is actually +> loaded on the physical PandA before running a scan against either config. ## Context @@ -116,10 +120,33 @@ layout can be restored unambiguously (see `ptycho_flomni.yaml`): Use `ophyd_devices/devices/panda_box/utility_scripts.py` to save the current PandA configuration to a `.ini` layout file and reload it later, so a -configuration survives a PandA power cycle. **TODO**: once Mode B's block -configuration is finalized against real hardware, save it as a named layout -(e.g. `flomni_panda_free_running.ini`) alongside the existing Mode A layout, -and record the exact save/load commands used here. +configuration survives a PandA power cycle. Both layouts are saved in this +repo under `csaxs_bec/devices/panda_box/layouts/` — `omny.ini` (Mode A) and +`omny_freerun.ini` (Mode B), captured 2026-09-15 directly from the PandA +design named `OMNY`/`omny_freerun` respectively (switch the PandA's active +Design first, then save): + +``` +python ophyd_devices/devices/panda_box/utility_scripts.py \ + --host omny-panda.psi.ch --save-layout csaxs_bec/devices/panda_box/layouts/omny.ini + +python ophyd_devices/devices/panda_box/utility_scripts.py \ + --host omny-panda.psi.ch --save-layout csaxs_bec/devices/panda_box/layouts/omny_freerun.ini +``` + +To restore either, swap `--save-layout` for `--load-layout` with the same +file. Diffing the two confirms the mode-specific differences documented +above: `PCAP.GATE`/`PCAP.TRIG` are `TTLIN1.VAL` in Mode A vs `CLOCK1.OUT` in +Mode B; `CLOCK1.ENABLE` is `ZERO` (off) in Mode A vs `ONE` in Mode B; +`INENC1`-`4.VAL.CAPTURE`/`FMC_IN.VAL1/VAL2.CAPTURE` are `Min Max Mean` in +Mode A vs `Value` in Mode B; `PCAP.BITS0.CAPTURE`/`COUNTER1.OUT.CAPTURE` are +`No` in Mode A vs `Value` in Mode B. + +`omny_freerun.ini` was captured with `CLOCK1.PERIOD=0.0001` (100 µs → 10 kHz) +— note this doesn't match the 20 kHz reported as tested successfully the +same day, so it may not be the exact layout state the 20 kHz test ran at; +worth double-checking before relying on this file as "the" validated +high-rate layout. ## Signal alias mapping -- 2.54.0