diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index c0101817..ab3643df 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -646,6 +646,24 @@ calculated_signal: ############################################################ #################### OMNY Pandabox ######################### ############################################################ +# 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"). Only +# one of the two devices below should be active at a time, matching whichever layout is +# currently loaded on the PandA. +# +# 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 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 @@ -676,3 +694,31 @@ calculated_signal: # 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 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 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() @@ -25,9 +93,17 @@ class PandaBoxOMNY(PandaBox): 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 +113,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 +153,112 @@ 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 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_frame_count_complete(): + counted = 0 + start_time = time.monotonic() + try: + 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_frame_count_complete, run=True) + 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. + + 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..edf4d248 --- /dev/null +++ b/docs/developer/panda_box_free_running_data_schema.md @@ -0,0 +1,48 @@ +# 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, 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 + +`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 — `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 | +|---|---|---| +| `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`, 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. | + +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..74764e10 --- /dev/null +++ b/docs/developer/panda_box_free_running_setup.md @@ -0,0 +1,218 @@ +# PandABox (`omny_panda`) burst acquisition: two operation modes + +> **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, +> 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 + +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 `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` | 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 +exact behavior unchanged. + +## Physical wiring (Mode B only) + +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 + +### 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 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`, 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 + 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, 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) + ``` + +## 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. 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 + +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` 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 | +|---|---|---| +| `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, 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, 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) 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 + +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? + +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/docs/user/ptychography/flomni.md b/docs/user/ptychography/flomni.md index 6e3f389d..12369515 100644 --- a/docs/user/ptychography/flomni.md +++ b/docs/user/ptychography/flomni.md @@ -233,6 +233,15 @@ 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 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 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..5f3e316a 100644 --- a/docs/user/ptychography/omny.md +++ b/docs/user/ptychography/omny.md @@ -338,6 +338,15 @@ 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 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 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..48f6f2f2 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_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", "