diff --git a/pyproject.toml b/pyproject.toml
index bf5b60e..3936422 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,7 +19,7 @@ requires-python = ">=3.11"
[project.optional-dependencies]
dev = [
- "blueapi>=1.3.1",
+ "blueapi>=1.12.0",
"copier",
"pipdeptree",
"pre-commit",
diff --git a/src/test_rig_bluesky/pandabrick_baseline.yaml b/src/test_rig_bluesky/pandabrick_baseline.yaml
new file mode 100644
index 0000000..f820ddf
--- /dev/null
+++ b/src/test_rig_bluesky/pandabrick_baseline.yaml
@@ -0,0 +1,781 @@
+absenc.1.bits: 0
+absenc.1.clk: TTLIN1.VAL
+absenc.1.clk_delay: 0
+absenc.1.clk_period: 0.0
+absenc.1.clk_period_units: min
+absenc.1.clk_src: Internally Generated
+absenc.1.conn_capture: false
+absenc.1.data_capture: false
+absenc.1.encoding: Unsigned Binary
+absenc.1.frame_period: 0.0
+absenc.1.frame_period_units: min
+absenc.1.lsb_discard: 0
+absenc.1.msb_discard: 0
+absenc.1.protocol: SSI
+absenc.1.val_capture: "No"
+absenc.1.val_dataset: ""
+absenc.1.val_offset: 0.0
+absenc.1.val_scale: 0.0
+absenc.2.bits: 0
+absenc.2.clk: TTLIN1.VAL
+absenc.2.clk_delay: 0
+absenc.2.clk_period: 0.0
+absenc.2.clk_period_units: min
+absenc.2.clk_src: Internally Generated
+absenc.2.conn_capture: false
+absenc.2.data_capture: false
+absenc.2.encoding: Unsigned Binary
+absenc.2.frame_period: 0.0
+absenc.2.frame_period_units: min
+absenc.2.lsb_discard: 0
+absenc.2.msb_discard: 0
+absenc.2.protocol: SSI
+absenc.2.val_capture: "No"
+absenc.2.val_dataset: ""
+absenc.2.val_offset: 0.0
+absenc.2.val_scale: 0.0
+absenc.3.bits: 0
+absenc.3.clk: TTLIN1.VAL
+absenc.3.clk_delay: 0
+absenc.3.clk_period: 0.0
+absenc.3.clk_period_units: min
+absenc.3.clk_src: Internally Generated
+absenc.3.conn_capture: false
+absenc.3.data_capture: false
+absenc.3.encoding: Unsigned Binary
+absenc.3.frame_period: 0.0
+absenc.3.frame_period_units: min
+absenc.3.lsb_discard: 0
+absenc.3.msb_discard: 0
+absenc.3.protocol: SSI
+absenc.3.val_capture: "No"
+absenc.3.val_dataset: ""
+absenc.3.val_offset: 0.0
+absenc.3.val_scale: 0.0
+absenc.4.bits: 0
+absenc.4.clk: TTLIN1.VAL
+absenc.4.clk_delay: 0
+absenc.4.clk_period: 0.0
+absenc.4.clk_period_units: min
+absenc.4.clk_src: Internally Generated
+absenc.4.conn_capture: false
+absenc.4.data_capture: false
+absenc.4.encoding: Unsigned Binary
+absenc.4.frame_period: 0.0
+absenc.4.frame_period_units: min
+absenc.4.lsb_discard: 0
+absenc.4.msb_discard: 0
+absenc.4.protocol: SSI
+absenc.4.val_capture: "No"
+absenc.4.val_dataset: ""
+absenc.4.val_offset: 0.0
+absenc.4.val_scale: 0.0
+absenc.5.bits: 0
+absenc.5.clk: TTLIN1.VAL
+absenc.5.clk_delay: 0
+absenc.5.clk_period: 0.0
+absenc.5.clk_period_units: min
+absenc.5.clk_src: Internally Generated
+absenc.5.conn_capture: false
+absenc.5.data_capture: false
+absenc.5.encoding: Unsigned Binary
+absenc.5.frame_period: 0.0
+absenc.5.frame_period_units: min
+absenc.5.lsb_discard: 0
+absenc.5.msb_discard: 0
+absenc.5.protocol: SSI
+absenc.5.val_capture: "No"
+absenc.5.val_dataset: ""
+absenc.5.val_offset: 0.0
+absenc.5.val_scale: 0.0
+absenc.6.bits: 0
+absenc.6.clk: TTLIN1.VAL
+absenc.6.clk_delay: 0
+absenc.6.clk_period: 0.0
+absenc.6.clk_period_units: min
+absenc.6.clk_src: Internally Generated
+absenc.6.conn_capture: false
+absenc.6.data_capture: false
+absenc.6.encoding: Unsigned Binary
+absenc.6.frame_period: 0.0
+absenc.6.frame_period_units: min
+absenc.6.lsb_discard: 0
+absenc.6.msb_discard: 0
+absenc.6.protocol: SSI
+absenc.6.val_capture: "No"
+absenc.6.val_dataset: ""
+absenc.6.val_offset: 0.0
+absenc.6.val_scale: 0.0
+absenc.7.bits: 0
+absenc.7.clk: TTLIN1.VAL
+absenc.7.clk_delay: 0
+absenc.7.clk_period: 0.0
+absenc.7.clk_period_units: min
+absenc.7.clk_src: Internally Generated
+absenc.7.conn_capture: false
+absenc.7.data_capture: false
+absenc.7.encoding: Unsigned Binary
+absenc.7.frame_period: 0.0
+absenc.7.frame_period_units: min
+absenc.7.lsb_discard: 0
+absenc.7.msb_discard: 0
+absenc.7.protocol: SSI
+absenc.7.val_capture: "No"
+absenc.7.val_dataset: ""
+absenc.7.val_offset: 0.0
+absenc.7.val_scale: 0.0
+absenc.8.bits: 0
+absenc.8.clk: TTLIN1.VAL
+absenc.8.clk_delay: 0
+absenc.8.clk_period: 0.0
+absenc.8.clk_period_units: min
+absenc.8.clk_src: Internally Generated
+absenc.8.conn_capture: false
+absenc.8.data_capture: false
+absenc.8.encoding: Unsigned Binary
+absenc.8.frame_period: 0.0
+absenc.8.frame_period_units: min
+absenc.8.lsb_discard: 0
+absenc.8.msb_discard: 0
+absenc.8.protocol: SSI
+absenc.8.val_capture: "No"
+absenc.8.val_dataset: ""
+absenc.8.val_offset: 0.0
+absenc.8.val_scale: 0.0
+bits.a: false
+bits.b: false
+bits.c: false
+bits.d: false
+bits.outa_capture: false
+bits.outb_capture: false
+bits.outc_capture: false
+bits.outd_capture: false
+calc.1.inpa: ABSENC1.VAL
+calc.1.inpb: ABSENC1.VAL
+calc.1.inpc: ABSENC1.VAL
+calc.1.inpd: ABSENC1.VAL
+calc.1.out_capture: "No"
+calc.1.out_dataset: ""
+calc.1.out_offset: 0.0
+calc.1.out_scale: 0.0
+calc.1.shift: 0
+calc.1.typea: Value
+calc.1.typeb: Value
+calc.1.typec: Value
+calc.1.typed: Value
+calc.2.inpa: ABSENC1.VAL
+calc.2.inpb: ABSENC1.VAL
+calc.2.inpc: ABSENC1.VAL
+calc.2.inpd: ABSENC1.VAL
+calc.2.out_capture: "No"
+calc.2.out_dataset: ""
+calc.2.out_offset: 0.0
+calc.2.out_scale: 0.0
+calc.2.shift: 0
+calc.2.typea: Value
+calc.2.typeb: Value
+calc.2.typec: Value
+calc.2.typed: Value
+clock.1.enable: TTLIN1.VAL
+clock.1.enable_delay: 0
+clock.1.out_capture: false
+clock.1.period: 0.0
+clock.1.period_units: min
+clock.1.width: 0.0
+clock.1.width_units: min
+clock.2.enable: TTLIN1.VAL
+clock.2.enable_delay: 0
+clock.2.out_capture: false
+clock.2.period: 0.0
+clock.2.period_units: min
+clock.2.width: 0.0
+clock.2.width_units: min
+counter.1.carry_capture: false
+counter.1.dir: TTLIN1.VAL
+counter.1.dir_delay: 0
+counter.1.enable: TTLIN1.VAL
+counter.1.enable_delay: 0
+counter.1.max: 0
+counter.1.min: 0
+counter.1.out_capture: "No"
+counter.1.out_dataset: ""
+counter.1.out_mode: On-Change
+counter.1.out_offset: 0.0
+counter.1.out_scale: 0.0
+counter.1.set: 0
+counter.1.start: 0
+counter.1.step: 0
+counter.1.trig: TTLIN1.VAL
+counter.1.trig_delay: 0
+counter.1.trig_edge: Rising
+counter.2.carry_capture: false
+counter.2.dir: TTLIN1.VAL
+counter.2.dir_delay: 0
+counter.2.enable: TTLIN1.VAL
+counter.2.enable_delay: 0
+counter.2.max: 0
+counter.2.min: 0
+counter.2.out_capture: "No"
+counter.2.out_dataset: ""
+counter.2.out_mode: On-Change
+counter.2.out_offset: 0.0
+counter.2.out_scale: 0.0
+counter.2.set: 0
+counter.2.start: 0
+counter.2.step: 0
+counter.2.trig: TTLIN1.VAL
+counter.2.trig_delay: 0
+counter.2.trig_edge: Rising
+counter.3.carry_capture: false
+counter.3.dir: TTLIN1.VAL
+counter.3.dir_delay: 0
+counter.3.enable: TTLIN1.VAL
+counter.3.enable_delay: 0
+counter.3.max: 0
+counter.3.min: 0
+counter.3.out_capture: "No"
+counter.3.out_dataset: ""
+counter.3.out_mode: On-Change
+counter.3.out_offset: 0.0
+counter.3.out_scale: 0.0
+counter.3.set: 0
+counter.3.start: 0
+counter.3.step: 0
+counter.3.trig: TTLIN1.VAL
+counter.3.trig_delay: 0
+counter.3.trig_edge: Rising
+counter.4.carry_capture: false
+counter.4.dir: TTLIN1.VAL
+counter.4.dir_delay: 0
+counter.4.enable: TTLIN1.VAL
+counter.4.enable_delay: 0
+counter.4.max: 0
+counter.4.min: 0
+counter.4.out_capture: "No"
+counter.4.out_dataset: ""
+counter.4.out_mode: On-Change
+counter.4.out_offset: 0.0
+counter.4.out_scale: 0.0
+counter.4.set: 0
+counter.4.start: 0
+counter.4.step: 0
+counter.4.trig: TTLIN1.VAL
+counter.4.trig_delay: 0
+counter.4.trig_edge: Rising
+data.capture: false
+data.capture_mode: FOREVER
+data.create_directory: 0
+data.flush_period: 0.0
+data.hdf_directory: /dls/b01-1/data/2026/cm44191-1/spool
+data.hdf_file_name: b01-1-981-pandabrick.h5
+data.num_capture: 0
+div.1.divisor: 0
+div.1.enable: TTLIN1.VAL
+div.1.enable_delay: 0
+div.1.first_pulse: OutN
+div.1.inp: TTLIN1.VAL
+div.1.inp_delay: 0
+div.1.outd_capture: false
+div.1.outn_capture: false
+div.2.divisor: 0
+div.2.enable: TTLIN1.VAL
+div.2.enable_delay: 0
+div.2.first_pulse: OutN
+div.2.inp: TTLIN1.VAL
+div.2.inp_delay: 0
+div.2.outd_capture: false
+div.2.outn_capture: false
+equ.1.val_capture: false
+equ.2.val_capture: false
+equ.3.val_capture: false
+equ.4.val_capture: false
+equ.5.val_capture: false
+equ.6.val_capture: false
+equ.7.val_capture: false
+equ.8.val_capture: false
+filter.1.enable: TTLIN1.VAL
+filter.1.enable_delay: 0
+filter.1.inp: ABSENC1.VAL
+filter.1.mode: difference
+filter.1.out_capture: "No"
+filter.1.out_dataset: ""
+filter.1.out_offset: 0.0
+filter.1.out_scale: 0.0
+filter.1.ready_capture: false
+filter.1.trig: TTLIN1.VAL
+filter.1.trig_delay: 0
+filter.2.enable: TTLIN1.VAL
+filter.2.enable_delay: 0
+filter.2.inp: ABSENC1.VAL
+filter.2.mode: difference
+filter.2.out_capture: "No"
+filter.2.out_dataset: ""
+filter.2.out_offset: 0.0
+filter.2.out_scale: 0.0
+filter.2.ready_capture: false
+filter.2.trig: TTLIN1.VAL
+filter.2.trig_delay: 0
+incenc.1.conn_capture: false
+incenc.1.protocol: Quadrature
+incenc.1.rst_on_z: false
+incenc.1.val_capture: "No"
+incenc.1.val_dataset: ""
+incenc.1.val_offset: 0.0
+incenc.1.val_scale: 0.0
+incenc.2.conn_capture: false
+incenc.2.protocol: Quadrature
+incenc.2.rst_on_z: false
+incenc.2.val_capture: "No"
+incenc.2.val_dataset: X
+incenc.2.val_offset: 0.0
+incenc.2.val_scale: 0.0
+incenc.3.conn_capture: false
+incenc.3.protocol: Quadrature
+incenc.3.rst_on_z: false
+incenc.3.val_capture: "No"
+incenc.3.val_dataset: Y
+incenc.3.val_offset: 0.0
+incenc.3.val_scale: 0.0
+incenc.4.conn_capture: false
+incenc.4.protocol: Quadrature
+incenc.4.rst_on_z: false
+incenc.4.val_capture: "No"
+incenc.4.val_dataset: ""
+incenc.4.val_offset: 0.0
+incenc.4.val_scale: 0.0
+incenc.5.conn_capture: false
+incenc.5.protocol: Quadrature
+incenc.5.rst_on_z: false
+incenc.5.val_capture: "No"
+incenc.5.val_dataset: ""
+incenc.5.val_offset: 0.0
+incenc.5.val_scale: 0.0
+incenc.6.conn_capture: false
+incenc.6.protocol: Quadrature
+incenc.6.rst_on_z: false
+incenc.6.val_capture: "No"
+incenc.6.val_dataset: ""
+incenc.6.val_offset: 0.0
+incenc.6.val_scale: 0.0
+incenc.7.conn_capture: false
+incenc.7.protocol: Quadrature
+incenc.7.rst_on_z: false
+incenc.7.val_capture: "No"
+incenc.7.val_dataset: ""
+incenc.7.val_offset: 0.0
+incenc.7.val_scale: 0.0
+incenc.8.conn_capture: false
+incenc.8.protocol: Quadrature
+incenc.8.rst_on_z: false
+incenc.8.val_capture: "No"
+incenc.8.val_dataset: ""
+incenc.8.val_offset: 0.0
+incenc.8.val_scale: 0.0
+lut.1.func: ""
+lut.1.inpa: TTLIN1.VAL
+lut.1.inpa_delay: 0
+lut.1.inpb: TTLIN1.VAL
+lut.1.inpb_delay: 0
+lut.1.inpc: TTLIN1.VAL
+lut.1.inpc_delay: 0
+lut.1.inpd: TTLIN1.VAL
+lut.1.inpd_delay: 0
+lut.1.inpe: TTLIN1.VAL
+lut.1.inpe_delay: 0
+lut.1.out_capture: false
+lut.1.typea: Input-Level
+lut.1.typeb: Input-Level
+lut.1.typec: Input-Level
+lut.1.typed: Input-Level
+lut.1.typee: Input-Level
+lut.2.func: ""
+lut.2.inpa: TTLIN1.VAL
+lut.2.inpa_delay: 0
+lut.2.inpb: TTLIN1.VAL
+lut.2.inpb_delay: 0
+lut.2.inpc: TTLIN1.VAL
+lut.2.inpc_delay: 0
+lut.2.inpd: TTLIN1.VAL
+lut.2.inpd_delay: 0
+lut.2.inpe: TTLIN1.VAL
+lut.2.inpe_delay: 0
+lut.2.out_capture: false
+lut.2.typea: Input-Level
+lut.2.typeb: Input-Level
+lut.2.typec: Input-Level
+lut.2.typed: Input-Level
+lut.2.typee: Input-Level
+lut.3.func: ""
+lut.3.inpa: TTLIN1.VAL
+lut.3.inpa_delay: 0
+lut.3.inpb: TTLIN1.VAL
+lut.3.inpb_delay: 0
+lut.3.inpc: TTLIN1.VAL
+lut.3.inpc_delay: 0
+lut.3.inpd: TTLIN1.VAL
+lut.3.inpd_delay: 0
+lut.3.inpe: TTLIN1.VAL
+lut.3.inpe_delay: 0
+lut.3.out_capture: false
+lut.3.typea: Input-Level
+lut.3.typeb: Input-Level
+lut.3.typec: Input-Level
+lut.3.typed: Input-Level
+lut.3.typee: Input-Level
+lut.4.func: ""
+lut.4.inpa: TTLIN1.VAL
+lut.4.inpa_delay: 0
+lut.4.inpb: TTLIN1.VAL
+lut.4.inpb_delay: 0
+lut.4.inpc: TTLIN1.VAL
+lut.4.inpc_delay: 0
+lut.4.inpd: TTLIN1.VAL
+lut.4.inpd_delay: 0
+lut.4.inpe: TTLIN1.VAL
+lut.4.inpe_delay: 0
+lut.4.out_capture: false
+lut.4.typea: Input-Level
+lut.4.typeb: Input-Level
+lut.4.typec: Input-Level
+lut.4.typed: Input-Level
+lut.4.typee: Input-Level
+lut.5.func: ""
+lut.5.inpa: TTLIN1.VAL
+lut.5.inpa_delay: 0
+lut.5.inpb: TTLIN1.VAL
+lut.5.inpb_delay: 0
+lut.5.inpc: TTLIN1.VAL
+lut.5.inpc_delay: 0
+lut.5.inpd: TTLIN1.VAL
+lut.5.inpd_delay: 0
+lut.5.inpe: TTLIN1.VAL
+lut.5.inpe_delay: 0
+lut.5.out_capture: false
+lut.5.typea: Input-Level
+lut.5.typeb: Input-Level
+lut.5.typec: Input-Level
+lut.5.typed: Input-Level
+lut.5.typee: Input-Level
+lut.6.func: ""
+lut.6.inpa: TTLIN1.VAL
+lut.6.inpa_delay: 0
+lut.6.inpb: TTLIN1.VAL
+lut.6.inpb_delay: 0
+lut.6.inpc: TTLIN1.VAL
+lut.6.inpc_delay: 0
+lut.6.inpd: TTLIN1.VAL
+lut.6.inpd_delay: 0
+lut.6.inpe: TTLIN1.VAL
+lut.6.inpe_delay: 0
+lut.6.out_capture: false
+lut.6.typea: Input-Level
+lut.6.typeb: Input-Level
+lut.6.typec: Input-Level
+lut.6.typed: Input-Level
+lut.6.typee: Input-Level
+lut.7.func: ""
+lut.7.inpa: TTLIN1.VAL
+lut.7.inpa_delay: 0
+lut.7.inpb: TTLIN1.VAL
+lut.7.inpb_delay: 0
+lut.7.inpc: TTLIN1.VAL
+lut.7.inpc_delay: 0
+lut.7.inpd: TTLIN1.VAL
+lut.7.inpd_delay: 0
+lut.7.inpe: TTLIN1.VAL
+lut.7.inpe_delay: 0
+lut.7.out_capture: false
+lut.7.typea: Input-Level
+lut.7.typeb: Input-Level
+lut.7.typec: Input-Level
+lut.7.typed: Input-Level
+lut.7.typee: Input-Level
+lut.8.func: ""
+lut.8.inpa: TTLIN1.VAL
+lut.8.inpa_delay: 0
+lut.8.inpb: TTLIN1.VAL
+lut.8.inpb_delay: 0
+lut.8.inpc: TTLIN1.VAL
+lut.8.inpc_delay: 0
+lut.8.inpd: TTLIN1.VAL
+lut.8.inpd_delay: 0
+lut.8.inpe: TTLIN1.VAL
+lut.8.inpe_delay: 0
+lut.8.out_capture: false
+lut.8.typea: Input-Level
+lut.8.typeb: Input-Level
+lut.8.typec: Input-Level
+lut.8.typed: Input-Level
+lut.8.typee: Input-Level
+pcap.active_capture: false
+pcap.arm: false
+pcap.bits0_capture: "No"
+pcap.bits0_dataset: ""
+pcap.bits1_capture: "No"
+pcap.bits1_dataset: ""
+pcap.bits2_capture: "No"
+pcap.bits2_dataset: ""
+pcap.bits3_capture: "No"
+pcap.bits3_dataset: ""
+pcap.enable: TTLIN1.VAL
+pcap.enable_delay: 0
+pcap.gate: TTLIN1.VAL
+pcap.gate_delay: 0
+pcap.gate_duration_capture: "No"
+pcap.gate_duration_dataset: ""
+pcap.shift_sum: 0
+pcap.trig: TTLIN1.VAL
+pcap.trig_delay: 0
+pcap.trig_edge: Rising
+pcap.ts_end_capture: "No"
+pcap.ts_end_dataset: ""
+pcap.ts_start_capture: "No"
+pcap.ts_start_dataset: ""
+pcap.ts_trig_capture: "No"
+pcap.ts_trig_dataset: ""
+pcomp.1.active_capture: false
+pcomp.1.dir: Positive
+pcomp.1.enable: TTLIN1.VAL
+pcomp.1.enable_delay: 0
+pcomp.1.inp: ABSENC1.VAL
+pcomp.1.out_capture: false
+pcomp.1.pre_start: 0
+pcomp.1.pulses: 0
+pcomp.1.relative: Absolute
+pcomp.1.start: 0
+pcomp.1.step: 0
+pcomp.1.width: 0
+pcomp.2.active_capture: false
+pcomp.2.dir: Positive
+pcomp.2.enable: TTLIN1.VAL
+pcomp.2.enable_delay: 0
+pcomp.2.inp: ABSENC1.VAL
+pcomp.2.out_capture: false
+pcomp.2.pre_start: 0
+pcomp.2.pulses: 0
+pcomp.2.relative: Absolute
+pcomp.2.start: 0
+pcomp.2.step: 0
+pcomp.2.width: 0
+pmacenc.1.bits: 0
+pmacenc.1.clk_capture: false
+pmacenc.1.data: TTLIN1.VAL
+pmacenc.1.data_delay: 0
+pmacenc.1.enable: TTLIN1.VAL
+pmacenc.1.enable_delay: 0
+pmacenc.1.encoding: Unsigned Binary
+pmacenc.1.generator_error: "No"
+pmacenc.1.protocol: Passthrough - UVWT
+pmacenc.1.val: ABSENC1.VAL
+pmacenc.2.bits: 0
+pmacenc.2.clk_capture: false
+pmacenc.2.data: TTLIN1.VAL
+pmacenc.2.data_delay: 0
+pmacenc.2.enable: TTLIN1.VAL
+pmacenc.2.enable_delay: 0
+pmacenc.2.encoding: Unsigned Binary
+pmacenc.2.generator_error: "No"
+pmacenc.2.protocol: Passthrough - UVWT
+pmacenc.2.val: ABSENC1.VAL
+pmacenc.3.bits: 0
+pmacenc.3.clk_capture: false
+pmacenc.3.data: TTLIN1.VAL
+pmacenc.3.data_delay: 0
+pmacenc.3.enable: TTLIN1.VAL
+pmacenc.3.enable_delay: 0
+pmacenc.3.encoding: Unsigned Binary
+pmacenc.3.generator_error: "No"
+pmacenc.3.protocol: Passthrough - UVWT
+pmacenc.3.val: ABSENC1.VAL
+pmacenc.4.bits: 0
+pmacenc.4.clk_capture: false
+pmacenc.4.data: TTLIN1.VAL
+pmacenc.4.data_delay: 0
+pmacenc.4.enable: TTLIN1.VAL
+pmacenc.4.enable_delay: 0
+pmacenc.4.encoding: Unsigned Binary
+pmacenc.4.generator_error: "No"
+pmacenc.4.protocol: Passthrough - UVWT
+pmacenc.4.val: ABSENC1.VAL
+pmacenc.5.bits: 0
+pmacenc.5.clk_capture: false
+pmacenc.5.data: TTLIN1.VAL
+pmacenc.5.data_delay: 0
+pmacenc.5.enable: TTLIN1.VAL
+pmacenc.5.enable_delay: 0
+pmacenc.5.encoding: Unsigned Binary
+pmacenc.5.generator_error: "No"
+pmacenc.5.protocol: Passthrough - UVWT
+pmacenc.5.val: ABSENC1.VAL
+pmacenc.6.bits: 0
+pmacenc.6.clk_capture: false
+pmacenc.6.data: TTLIN1.VAL
+pmacenc.6.data_delay: 0
+pmacenc.6.enable: TTLIN1.VAL
+pmacenc.6.enable_delay: 0
+pmacenc.6.encoding: Unsigned Binary
+pmacenc.6.generator_error: "No"
+pmacenc.6.protocol: Passthrough - UVWT
+pmacenc.6.val: ABSENC1.VAL
+pmacenc.7.bits: 0
+pmacenc.7.clk_capture: false
+pmacenc.7.data: TTLIN1.VAL
+pmacenc.7.data_delay: 0
+pmacenc.7.enable: TTLIN1.VAL
+pmacenc.7.enable_delay: 0
+pmacenc.7.encoding: Unsigned Binary
+pmacenc.7.generator_error: "No"
+pmacenc.7.protocol: Passthrough - UVWT
+pmacenc.7.val: ABSENC1.VAL
+pmacenc.8.bits: 0
+pmacenc.8.clk_capture: false
+pmacenc.8.data: TTLIN1.VAL
+pmacenc.8.data_delay: 0
+pmacenc.8.enable: TTLIN1.VAL
+pmacenc.8.enable_delay: 0
+pmacenc.8.encoding: Unsigned Binary
+pmacenc.8.generator_error: "No"
+pmacenc.8.protocol: Passthrough - UVWT
+pmacenc.8.val: ABSENC1.VAL
+pulse.1.delay: 0.0
+pulse.1.delay_units: min
+pulse.1.enable: TTLIN1.VAL
+pulse.1.enable_delay: 0
+pulse.1.out_capture: false
+pulse.1.pulses: 0
+pulse.1.step: 0.0
+pulse.1.step_units: min
+pulse.1.trig: TTLIN1.VAL
+pulse.1.trig_delay: 0
+pulse.1.trig_edge: Rising
+pulse.1.width: 0.0
+pulse.1.width_units: min
+pulse.2.delay: 0.0
+pulse.2.delay_units: min
+pulse.2.enable: TTLIN1.VAL
+pulse.2.enable_delay: 0
+pulse.2.out_capture: false
+pulse.2.pulses: 0
+pulse.2.step: 0.0
+pulse.2.step_units: min
+pulse.2.trig: TTLIN1.VAL
+pulse.2.trig_delay: 0
+pulse.2.trig_edge: Rising
+pulse.2.width: 0.0
+pulse.2.width_units: min
+pulse.3.delay: 0.0
+pulse.3.delay_units: min
+pulse.3.enable: TTLIN1.VAL
+pulse.3.enable_delay: 0
+pulse.3.out_capture: false
+pulse.3.pulses: 0
+pulse.3.step: 0.0
+pulse.3.step_units: min
+pulse.3.trig: TTLIN1.VAL
+pulse.3.trig_delay: 0
+pulse.3.trig_edge: Rising
+pulse.3.width: 0.0
+pulse.3.width_units: min
+pulse.4.delay: 0.0
+pulse.4.delay_units: min
+pulse.4.enable: TTLIN1.VAL
+pulse.4.enable_delay: 0
+pulse.4.out_capture: false
+pulse.4.pulses: 0
+pulse.4.step: 0.0
+pulse.4.step_units: min
+pulse.4.trig: TTLIN1.VAL
+pulse.4.trig_delay: 0
+pulse.4.trig_edge: Rising
+pulse.4.width: 0.0
+pulse.4.width_units: min
+seq.1.active_capture: false
+seq.1.bita: TTLIN1.VAL
+seq.1.bita_delay: 0
+seq.1.bitb: TTLIN1.VAL
+seq.1.bitb_delay: 0
+seq.1.bitc: TTLIN1.VAL
+seq.1.bitc_delay: 0
+seq.1.enable: ZERO
+seq.1.enable_delay: 0
+seq.1.outa_capture: false
+seq.1.outb_capture: false
+seq.1.outc_capture: false
+seq.1.outd_capture: false
+seq.1.oute_capture: false
+seq.1.outf_capture: false
+seq.1.posa: ZERO
+seq.1.posb: ABSENC1.VAL
+seq.1.posc: ABSENC1.VAL
+seq.1.prescale: 1.0
+seq.1.prescale_units: us
+seq.1.repeats: 1
+seq.2.active_capture: false
+seq.2.bita: TTLIN1.VAL
+seq.2.bita_delay: 0
+seq.2.bitb: TTLIN1.VAL
+seq.2.bitb_delay: 0
+seq.2.bitc: TTLIN1.VAL
+seq.2.bitc_delay: 0
+seq.2.enable: ZERO
+seq.2.enable_delay: 0
+seq.2.outa_capture: false
+seq.2.outb_capture: false
+seq.2.outc_capture: false
+seq.2.outd_capture: false
+seq.2.oute_capture: false
+seq.2.outf_capture: false
+seq.2.posa: ZERO
+seq.2.posb: ABSENC1.VAL
+seq.2.posc: ABSENC1.VAL
+seq.2.prescale: 0.0
+seq.2.prescale_units: us
+seq.2.repeats: 1
+srgate.1.enable: TTLIN1.VAL
+srgate.1.enable_delay: 0
+srgate.1.out_capture: false
+srgate.1.rst: TTLIN1.VAL
+srgate.1.rst_delay: 0
+srgate.1.rst_edge: Rising
+srgate.1.set: TTLIN1.VAL
+srgate.1.set_delay: 0
+srgate.1.set_edge: Rising
+srgate.1.when_disabled: Set output low
+srgate.2.enable: TTLIN1.VAL
+srgate.2.enable_delay: 0
+srgate.2.out_capture: false
+srgate.2.rst: TTLIN1.VAL
+srgate.2.rst_delay: 0
+srgate.2.rst_edge: Rising
+srgate.2.set: TTLIN1.VAL
+srgate.2.set_delay: 0
+srgate.2.set_edge: Rising
+srgate.2.when_disabled: Set output low
+srgate.3.enable: TTLIN1.VAL
+srgate.3.enable_delay: 0
+srgate.3.out_capture: false
+srgate.3.rst: TTLIN1.VAL
+srgate.3.rst_delay: 0
+srgate.3.rst_edge: Rising
+srgate.3.set: TTLIN1.VAL
+srgate.3.set_delay: 0
+srgate.3.set_edge: Rising
+srgate.3.when_disabled: Set output low
+srgate.4.enable: TTLIN1.VAL
+srgate.4.enable_delay: 0
+srgate.4.out_capture: false
+srgate.4.rst: TTLIN1.VAL
+srgate.4.rst_delay: 0
+srgate.4.rst_edge: Rising
+srgate.4.set: TTLIN1.VAL
+srgate.4.set_delay: 0
+srgate.4.set_edge: Rising
+srgate.4.when_disabled: Set output low
+ttlin.1.term: High-Z
+ttlin.1.val_capture: false
+ttlin.2.term: High-Z
+ttlin.2.val_capture: false
+ttlout.1.val: TTLIN1.VAL
+ttlout.1.val_delay: 0
+ttlout.2.val: TTLIN1.VAL
+ttlout.2.val_delay: 0
diff --git a/src/test_rig_bluesky/plans.py b/src/test_rig_bluesky/plans.py
index 2008ed5..8ffc5ea 100644
--- a/src/test_rig_bluesky/plans.py
+++ b/src/test_rig_bluesky/plans.py
@@ -1,8 +1,10 @@
+import logging
import math
from pathlib import Path
from typing import Any
-from bluesky import plan_stubs as bps
+import bluesky.plan_stubs as bps
+import bluesky.preprocessors as bpp
from bluesky.plans import count
from bluesky.protocols import Movable
from bluesky.utils import MsgGenerator
@@ -10,25 +12,46 @@
from dodal.devices.motors import XYZStage
from dodal.plan_stubs.data_session import attach_data_session_metadata_decorator
from dodal.plans import spec_scan
-from ophyd_async.core import Device, Settings, SettingsProvider, YamlSettingsProvider
+from ophyd_async.core import (
+ DetectorTrigger,
+ Device,
+ Settings,
+ SettingsProvider,
+ StandardFlyer,
+ TriggerInfo,
+ YamlSettingsProvider,
+)
from ophyd_async.epics.adaravis import AravisDetector
from ophyd_async.epics.adcore import (
NDAttributeDataType,
NDAttributeParam,
+ NDROIStatNIO,
+ setup_ndattributes,
+)
+from ophyd_async.epics.pmac import PmacIO, PmacTrajectoryTriggerLogic
+from ophyd_async.fastcs.panda import (
+ HDFPanda,
+ ScanSpecInfo,
+ ScanSpecSeqTableTriggerLogic,
+ SeqBlock,
+ apply_panda_settings,
)
-from ophyd_async.epics.adcore._core_io import NDROIStatNIO
from ophyd_async.plan_stubs import (
apply_settings,
apply_settings_if_different,
retrieve_settings,
- setup_ndattributes,
store_settings,
)
-from scanspec.specs import Line, Spec
+from scanspec.specs import Fly, Line, Spec
+
+LOGGER = logging.getLogger(__name__)
-imaging_detector = inject("imaging_detector")
+# imaging_detector = inject("imaging_detector")
spectroscopy_detector = inject("spectroscopy_detector")
sample_stage = inject("sample_stage")
+pmac = inject("pmac")
+pandabrick = inject("pandabrick")
+# pmac_trigger_logic = inject("pmac_trigger_logic")
def save_settings(
@@ -58,6 +81,25 @@ def load_settings(
yield from apply_settings_if_different(settings_to_set, apply_settings)
+def load_panda_settings(
+ panda: HDFPanda,
+ design_name: str,
+ whitelist_pvs: list[str] | None = None,
+) -> MsgGenerator[None]:
+ provider = _settings_provider()
+ settings = yield from retrieve_settings(provider, design_name, panda)
+ if whitelist_pvs is None:
+ settings_to_set = settings
+ else:
+ signal_values = {
+ signal: value
+ for signal, value in settings.items()
+ if signal.name.replace(f"{panda.name}-", "") in whitelist_pvs
+ }
+ settings_to_set = Settings(settings.device, signal_values)
+ yield from apply_settings_if_different(settings_to_set, apply_panda_settings)
+
+
def _settings_provider() -> SettingsProvider:
this_directory = Path(__file__).parent
return YamlSettingsProvider(this_directory)
@@ -65,7 +107,7 @@ def _settings_provider() -> SettingsProvider:
@attach_data_session_metadata_decorator()
def snapshot(
- imaging_detector: AravisDetector = imaging_detector,
+ imaging_detector: AravisDetector,
spectroscopy_detector: AravisDetector = spectroscopy_detector,
sample_stage: XYZStage = sample_stage,
) -> MsgGenerator[None]:
@@ -76,8 +118,12 @@ def snapshot(
def spectroscopy(
spectroscopy_detector: AravisDetector = spectroscopy_detector,
sample_stage: XYZStage = sample_stage,
+ pmac: PmacIO = pmac,
+ pandabrick: HDFPanda = pandabrick,
+ num_points: int = 25,
spec: Spec[Movable] | None = None,
exposure_time: float = 0.1,
+ fly: bool = True,
metadata: dict[str, Any] | None = None,
) -> MsgGenerator[None]:
"""Do a spectroscopy scan."""
@@ -123,7 +169,7 @@ def spectroscopy(
yield from bps.mv(
*(spectroscopy_detector.driver.acquire_time, exposure_time),
*(spectroscopy_detector.driver.acquire_period, exposure_time + 1961e-6),
- wait=True,
+ group="spectroscopy_detector_aquire",
)
params: list[NDAttributeParam] = []
@@ -156,14 +202,181 @@ def spectroscopy(
],
)
- spec = spec or Line(sample_stage.x, 0, 5, 5)
+ # yield from load_panda_settings(
+ # panda=pandabrick,
+ # design_name="pandabrick_baseline",
+ # whitelist_pvs=["incenc-__3-val_dataset", "incenc-__2-val_dataset"],
+ # )
+
+ LOGGER.info("Panda Children: %s", list(pandabrick.absenc.children())) # type: ignore
+
+ spec = spec or Line(sample_stage.x, 0, 5, 5) # type: ignore
+
+ # NOTE: replace with spec_serialized = spec.serialize() when this merges: https://github.com/bluesky/scanspec/pull/208
+ from typing import Any
+
+ from pydantic import TypeAdapter
+ from scanspec.specs import Spec
+
+ spec_serialzied = TypeAdapter(Spec[Any]).dump_python(
+ spec, mode="json", fallback=repr
+ )
+
+ # write json-serializable form of spec into metadata
+ if metadata is None:
+ metadata = {}
+ # spec.serialize() when https://github.com/bluesky/scanspec/pull/208 merges
+ metadata["spec"] = spec_serialzied
+
+ if fly:
+ LOGGER.info("Performing a fly scan.")
+ yield from fly_scan(
+ spec,
+ spectroscopy_detector,
+ sample_stage,
+ pmac,
+ pandabrick,
+ num_points, # type: ignore
+ exposure_time,
+ metadata,
+ )
+ else:
+ LOGGER.info("Performing a step scan.")
+ yield from spec_scan(
+ {spectroscopy_detector, sample_stage},
+ spec, # type: ignore
+ metadata=metadata,
+ )
+
+
+def fly_scan(
+ spec: Spec[Movable],
+ spectroscopy_detector: AravisDetector = spectroscopy_detector,
+ sample_stage: XYZStage = sample_stage,
+ pmac: PmacIO = pmac,
+ pandabrick: HDFPanda = pandabrick,
+ num_points: int = 1_000,
+ exposure_time: float = 0.1,
+ metadata: dict[str, Any] | None = None,
+):
+ # yield from load_panda_settings(panda=pandabrick,design_name="pandabrick_baseline")
+
+ yield from load_panda_settings(
+ panda=pandabrick,
+ design_name="pandabrick_baseline",
+ whitelist_pvs=["incenc-3-val_dataset", "incenc-2-val_dataset"],
+ )
+
+ scan_frame_duration = exposure_time
+ fly_spec = Fly(scan_frame_duration @ spec) # type: ignore
+ detector_deadtime = 2e-3 * 1.01
+
+ trigger_logic = fly_spec
+ pmac_trigger_logic = PmacTrajectoryTriggerLogic(pmac)
+ table: SeqBlock = pandabrick.seq[1] # type: ignore # noqa: SLF001
+
+ scan_spec_info = ScanSpecInfo(spec=fly_spec, deadtime=detector_deadtime) # type: ignore
+
+ # motor_pos_out = {
+ # sample_stage.x: PosOutScaleOffset.from_inenc(pandabrick, 2),
+ # sample_stage.y: PosOutScaleOffset.from_inenc(pandabrick, 3)
+ # }
+
+ panda_trigger_logic = StandardFlyer(
+ ScanSpecSeqTableTriggerLogic(
+ table,
+ # motor_pos_out
+ )
+ )
+
+ scan_frame_livetime = scan_frame_duration - detector_deadtime
+
+ # Reproduce metadata present in spec_scan
+ _md = {
+ "plan_args": {
+ "detectors": {det.name for det in [pandabrick, spectroscopy_detector]},
+ "spec": repr(fly_spec),
+ },
+ "plan_name": "fly_scan",
+ "shape": fly_spec.shape(),
+ **(metadata or {}),
+ }
+
+ # Prepare Panda file writer trigger info
+ panda_hdf_info = TriggerInfo(
+ number_of_events=num_points,
+ trigger=DetectorTrigger.EXTERNAL_LEVEL,
+ livetime=scan_frame_livetime,
+ deadtime=detector_deadtime,
+ )
+
+ # Prepare Panda file writer trigger info
+ detector_info = TriggerInfo(
+ number_of_events=num_points,
+ trigger=DetectorTrigger.EXTERNAL_EDGE,
+ livetime=scan_frame_livetime,
+ deadtime=detector_deadtime,
+ )
+
+ @attach_data_session_metadata_decorator()
+ @bpp.run_decorator(md=_md)
+ @bpp.stage_decorator(
+ [pandabrick, panda_trigger_logic, spectroscopy_detector, pmac_trigger_logic]
+ )
+ def inner_plan():
+ # create a group that is waited for
+ # Hashable prepare_group = ["pmac_trigger_logic", "trigger_logic"]
+
+ # Prepare pmac with the trajectory
+ yield from bps.prepare(pmac_trigger_logic, trigger_logic)
+ # prepare sequencer table
+ yield from bps.prepare(panda_trigger_logic, scan_spec_info)
+ # prepare panda and hdf writer once, at start of scan
+ yield from bps.prepare(pandabrick, panda_hdf_info)
+ # prepare spectroscopy_detector and info
+ # waiting for this last prepare means all prepare functions will be complete
+ yield from bps.prepare(spectroscopy_detector, detector_info, wait=True)
+
+ # Need to run this after detectors are prepared
+ yield from bps.declare_stream(
+ pandabrick, spectroscopy_detector, name="primary", collect=True
+ )
+
+ # Start the detectors and hdf writers acquiring.
+ # Configure the panda (seq table triggering).
+ # create a group that is waited for before pmac_trigger_logic kicked off on
+ # its own with wait=true.
+ yield from bps.kickoff(pandabrick)
+ yield from bps.kickoff(panda_trigger_logic)
+ yield from bps.kickoff(spectroscopy_detector, wait=True)
+
+ # Start the trajectory.
+ yield from bps.kickoff(pmac_trigger_logic, wait=True)
+
+ # Wait for the scan to complete whilst continuously collecting the data.
+ yield from bps.collect_while_completing(
+ flyers=(
+ pmac_trigger_logic,
+ panda_trigger_logic,
+ pandabrick,
+ spectroscopy_detector,
+ ),
+ dets=(
+ pandabrick,
+ spectroscopy_detector,
+ ),
+ stream_name="primary",
+ flush_period=0.5,
+ )
- yield from spec_scan({spectroscopy_detector, sample_stage}, spec, metadata=metadata)
+ yield from inner_plan()
def demo_spectroscopy(
spectroscopy_detector: AravisDetector = spectroscopy_detector,
sample_stage: XYZStage = sample_stage,
+ pmac: PmacIO = pmac,
+ pandabrick: HDFPanda = pandabrick,
total_number_of_scan_points: int = 25,
grid_size: float = 5.0,
grid_origin_x: float = 0.0,
@@ -180,13 +393,41 @@ def demo_spectroscopy(
xmax = grid_origin_x + grid_size
ymin = grid_origin_y
ymax = grid_origin_y + grid_size
- grid = Line(sample_stage.y, ymin, ymax, ysteps) * Line(
- sample_stage.x, xmin, xmax, xsteps
+
+ velomax = grid_size / (xsteps * exposure_time)
+ if velomax <= 10 * 0.98:
+ fly = True
+ LOGGER.info("test-rig-bluesky")
+ LOGGER.info(
+ f"Estimated velocity is {velomax:.2f} mm/sec, performing a fly scan."
+ )
+ else:
+ fly = False
+ LOGGER.info(
+ f"Estimated velocity is {velomax:.2f} mm/sec, performing a step scan."
+ )
+
+ # Move to the start point
+ # pmac should handle premove
+ yield from bps.mv(
+ *(sample_stage.x, xmin), *(sample_stage.y, ymin), group="initial_move"
+ )
+
+ grid = Line(sample_stage.y, ymin, ymax, ysteps) * ~Line( # type: ignore
+ sample_stage.x, # type: ignore
+ xmin,
+ xmax,
+ xsteps,
)
+ # fly = False
yield from spectroscopy(
spectroscopy_detector=spectroscopy_detector,
sample_stage=sample_stage,
+ pmac=pmac,
+ pandabrick=pandabrick,
+ num_points=total_number_of_scan_points,
spec=grid,
exposure_time=exposure_time,
+ fly=fly,
metadata=metadata,
)
diff --git a/src/test_rig_bluesky/spectroscopy_detector_baseline.yaml b/src/test_rig_bluesky/spectroscopy_detector_baseline.yaml
index 79e8280..e701ef0 100644
--- a/src/test_rig_bluesky/spectroscopy_detector_baseline.yaml
+++ b/src/test_rig_bluesky/spectroscopy_detector_baseline.yaml
@@ -3,69 +3,68 @@ driver.acquire_period: 0.021815
driver.acquire_time: 0.10000
driver.array_counter: 3
driver.image_mode: Multiple
-driver.nd_attributes_file: ''
+driver.nd_attributes_file: ""
driver.num_images: 1
-driver.trigger_mode: 'Off'
+driver.trigger_mode: "Off"
driver.trigger_source: Freerun
driver.wait_for_plugins: false
-fileio.acquire: false
-fileio.array_counter: 3
-fileio.auto_increment: true
-fileio.capture: false
-fileio.chunk_size_auto: true
-fileio.compression: None
-fileio.create_directory: 0
-fileio.enable_callbacks: Enable
-fileio.file_name: 5073260b-9688-4283-84d1-204955e423aa
-fileio.file_number: 1
-fileio.file_path: /tmp/
-fileio.file_template: '%s%s.h5'
-fileio.file_write_mode: Stream
-fileio.flush_now: false
-fileio.lazy_open: true
-fileio.nd_array_address: 0
-fileio.nd_array_port: D2.roistat
-fileio.nd_attributes_file: ''
-fileio.num_capture: 0
-fileio.num_extra_dims: 0
-fileio.num_frames_chunks: 1
-fileio.position_mode: false
-fileio.queue_size: 2
-fileio.swmr_mode: true
-fileio.wait_for_plugins: false
-fileio.xml_file_name: ''
-roistat.acquire: false
+writer.array_counter: 3
+writer.auto_increment: true
+writer.capture: false
+writer.chunk_size_auto: true
+writer.compression: None
+writer.create_directory: 0
+writer.enable_callbacks: Enable
+writer.file_name: 5073260b-9688-4283-84d1-204955e423aa
+writer.file_number: 1
+writer.file_path: /tmp/
+writer.file_template: "%s%s.h5"
+writer.file_write_mode: Stream
+writer.flush_now: false
+writer.lazy_open: true
+writer.nd_array_address: 0
+writer.nd_array_port: D2.ROISTAT
+writer.nd_attributes_file: ""
+writer.num_capture: 0
+writer.num_extra_dims: 0
+writer.num_frames_chunks: 1
+writer.position_mode: false
+writer.queue_size: 2
+writer.swmr_mode: true
+writer.xml_file_name: ""
roistat.array_counter: 3
-roistat.channels.1.min_x: 95
-roistat.channels.1.min_y: 610
-roistat.channels.1.name_: Red
+roistat.channels.1.min_x: 295
+roistat.channels.1.min_y: 114
+roistat.channels.1.name_: Blue
roistat.channels.1.size_x: 150
roistat.channels.1.size_y: 150
roistat.channels.1.use: true
-roistat.channels.2.min_x: 880
-roistat.channels.2.min_y: 605
+roistat.channels.2.min_x: 907
+roistat.channels.2.min_y: 108
roistat.channels.2.name_: Green
roistat.channels.2.size_x: 150
roistat.channels.2.size_y: 150
roistat.channels.2.use: true
-roistat.channels.3.min_x: 1665
-roistat.channels.3.min_y: 600
-roistat.channels.3.name_: Blue
+roistat.channels.3.min_x: 1511
+roistat.channels.3.min_y: 104
+roistat.channels.3.name_: Red
roistat.channels.3.size_x: 150
roistat.channels.3.size_y: 150
roistat.channels.3.use: true
roistat.enable_callbacks: Enable
roistat.nd_array_address: 0
-roistat.nd_array_port: D2.CAM
-roistat.nd_attributes_file:
+roistat.nd_array_port: D2.DRV
+roistat.nd_attributes_file:
roistat.queue_size: 2
-roistat.wait_for_plugins: false