From fbcb29fd78edd5fb73aa87130fdc66714a89520f Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 13 Oct 2023 14:41:38 -0700 Subject: [PATCH 1/4] Kind of working software triggering --- python/acquire/acquire.pyi | 4 ++++ src/runtime.rs | 9 +++++++++ src/storage.rs | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/python/acquire/acquire.pyi b/python/acquire/acquire.pyi index 2caf221..a673b1b 100644 --- a/python/acquire/acquire.pyi +++ b/python/acquire/acquire.pyi @@ -149,6 +149,7 @@ class Runtime: def get_state(self) -> DeviceState: ... def set_configuration(self, properties: Properties) -> Properties: ... def start(self) -> None: ... + def execute_trigger(self, stream_id: int) -> None: ... def stop(self) -> None: ... def abort(self) -> None: ... @@ -228,6 +229,9 @@ class StorageProperties: pixel_scale_um: Tuple[float, float] chunking: ChunkingProperties enable_multiscale: bool + max_frame_count: int + num_channels: int + num_slices: int def dict(self) -> Dict[str, Any]: ... @final diff --git a/src/runtime.rs b/src/runtime.rs index 8a0c588..e33a77c 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -63,6 +63,11 @@ impl RawRuntime { Ok(()) } + fn execute_trigger(&self, stream_id: u32) -> Result<()> { + unsafe { capi::acquire_execute_trigger(self.inner.as_ptr(), stream_id) }.ok()?; + Ok(()) + } + fn stop(&self) -> Result<()> { unsafe { capi::acquire_stop(self.inner.as_ptr()) }.ok()?; Ok(()) @@ -157,6 +162,10 @@ impl Runtime { .try_into()?) } + fn execute_trigger(&self, stream_id: u32, py: Python<'_>) -> PyResult<()> { + Python::allow_threads(py, || Ok(self.inner.execute_trigger(stream_id)?)) + } + fn get_available_data(&self, stream_id: u32) -> PyResult> { let mut beg = null_mut(); let mut end = null_mut(); diff --git a/src/storage.rs b/src/storage.rs index 7449107..5ad84a8 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -72,6 +72,15 @@ pub struct StorageProperties { #[pyo3(get, set)] pub(crate) enable_multiscale: bool, + + #[pyo3(get)] + pub(crate) max_frame_count: u64, + + #[pyo3(get, set)] + pub(crate) num_channels: u8, + + #[pyo3(get, set)] + pub(crate) num_slices: u8, } impl_plain_old_dict!(StorageProperties); @@ -86,6 +95,9 @@ impl Default for StorageProperties { pixel_scale_um: Default::default(), chunking, enable_multiscale: Default::default(), + max_frame_count: Default::default(), + num_channels: Default::default(), + num_slices: Default::default(), } } } @@ -140,6 +152,9 @@ impl TryFrom for StorageProperties { pixel_scale_um: (value.pixel_scale_um.x, value.pixel_scale_um.y), chunking, enable_multiscale: (value.enable_multiscale == 1), + max_frame_count: value.max_frame_count, + num_channels: value.num_channels, + num_slices: value.num_slices, }) } } @@ -215,6 +230,11 @@ impl TryFrom<&StorageProperties> for capi::StorageProperties { } { Err(anyhow::anyhow!("Failed acquire api status check")) } else { + // TODO: rethink this + out.max_frame_count = value.max_frame_count; + out.num_channels = value.num_channels; + out.num_slices = value.num_slices; + Ok(out) } } @@ -229,6 +249,9 @@ impl Default for capi::StorageProperties { pixel_scale_um: Default::default(), chunking: Default::default(), enable_multiscale: Default::default(), + max_frame_count: Default::default(), + num_channels: Default::default(), + num_slices: Default::default(), } } } From ce1cd5b2ecaf7838822f5fa573c5c7367c7e7318 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 13 Oct 2023 14:53:34 -0700 Subject: [PATCH 2/4] Remove unneeded changes --- python/acquire/acquire.pyi | 3 --- src/storage.rs | 23 ----------------------- 2 files changed, 26 deletions(-) diff --git a/python/acquire/acquire.pyi b/python/acquire/acquire.pyi index a673b1b..c21589d 100644 --- a/python/acquire/acquire.pyi +++ b/python/acquire/acquire.pyi @@ -229,9 +229,6 @@ class StorageProperties: pixel_scale_um: Tuple[float, float] chunking: ChunkingProperties enable_multiscale: bool - max_frame_count: int - num_channels: int - num_slices: int def dict(self) -> Dict[str, Any]: ... @final diff --git a/src/storage.rs b/src/storage.rs index 5ad84a8..7449107 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -72,15 +72,6 @@ pub struct StorageProperties { #[pyo3(get, set)] pub(crate) enable_multiscale: bool, - - #[pyo3(get)] - pub(crate) max_frame_count: u64, - - #[pyo3(get, set)] - pub(crate) num_channels: u8, - - #[pyo3(get, set)] - pub(crate) num_slices: u8, } impl_plain_old_dict!(StorageProperties); @@ -95,9 +86,6 @@ impl Default for StorageProperties { pixel_scale_um: Default::default(), chunking, enable_multiscale: Default::default(), - max_frame_count: Default::default(), - num_channels: Default::default(), - num_slices: Default::default(), } } } @@ -152,9 +140,6 @@ impl TryFrom for StorageProperties { pixel_scale_um: (value.pixel_scale_um.x, value.pixel_scale_um.y), chunking, enable_multiscale: (value.enable_multiscale == 1), - max_frame_count: value.max_frame_count, - num_channels: value.num_channels, - num_slices: value.num_slices, }) } } @@ -230,11 +215,6 @@ impl TryFrom<&StorageProperties> for capi::StorageProperties { } { Err(anyhow::anyhow!("Failed acquire api status check")) } else { - // TODO: rethink this - out.max_frame_count = value.max_frame_count; - out.num_channels = value.num_channels; - out.num_slices = value.num_slices; - Ok(out) } } @@ -249,9 +229,6 @@ impl Default for capi::StorageProperties { pixel_scale_um: Default::default(), chunking: Default::default(), enable_multiscale: Default::default(), - max_frame_count: Default::default(), - num_channels: Default::default(), - num_slices: Default::default(), } } } From 6d8d9c224d10fd836dad192e8bb9d6d9c85f5386 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 13 Oct 2023 16:04:11 -0700 Subject: [PATCH 3/4] Add test for triggers --- tests/test_basic.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_basic.py b/tests/test_basic.py index 91f8212..00404d3 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,6 +1,7 @@ import json import logging import time +from datetime import timedelta from time import sleep from typing import Any, Dict, List, Optional @@ -621,6 +622,61 @@ def test_abort(runtime: Runtime): assert nframes < p.video[0].max_frame_count +def wait_for_data(runtime: Runtime, stream_id: int = 0, timeout: Optional[timedelta] = None)-> acquire.AvailableData: + # None is used as a missing sentinel value, not to indicate no timeout. + if timeout is None: + timeout = timedelta(seconds=5) + sleep_duration = timedelta(microseconds=10000) + elapsed = timedelta() + while elapsed < timeout: + if packet := runtime.get_available_data(stream_id): + return packet + sleep(sleep_duration.total_seconds()) + elapsed += sleep_duration + raise RuntimeError(f"Timed out waiting for condition after {elapsed.total_seconds()} seconds.") + + +def test_execute_trigger(runtime: Runtime): + dm = runtime.device_manager() + p = runtime.get_configuration() + + p.video[0].camera.identifier = dm.select(DeviceKind.Camera, "simulated.*random.*") + p.video[0].storage.identifier = dm.select(DeviceKind.Storage, "Trash") + p.video[0].camera.settings.binning = 1 + p.video[0].camera.settings.shape = (64, 48) + p.video[0].camera.settings.exposure_time_us = 1e4 + p.video[0].camera.settings.pixel_type = acquire.SampleType.U8 + p.video[0].camera.settings.input_triggers.frame_start.line = 1 + p.video[0].camera.settings.input_triggers.frame_start.enable = True + p.video[0].max_frame_count = 100 + + p = runtime.set_configuration(p) + + runtime.start() + + # Snap one frame + runtime.execute_trigger(0) + packet = wait_for_data(runtime, 0) + # TODO: why do i get more than one frame? + assert packet.get_frame_count() >= 1 + frames = tuple(packet.frames()) + assert frames[0].metadata().frame_id >= 0 + del frames + del packet + + # Snap another frame + runtime.execute_trigger(0) + packet = wait_for_data(runtime, 0) + # TODO: why do i get more than one frame? + assert packet.get_frame_count() >= 1 + frames = tuple(packet.frames()) + assert frames[0].metadata().frame_id >= 1 + del frames + del packet + + runtime.stop() + + # FIXME: (nclack) awkwardness around references (available frames, f) # NOTES: From 432d90b05635a64d68ae6ba56d15ca05564247b0 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Mon, 16 Oct 2023 15:24:12 -0700 Subject: [PATCH 4/4] Ensure trigger is enabled --- tests/test_basic.py | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index 00404d3..f2fd623 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -640,39 +640,33 @@ def test_execute_trigger(runtime: Runtime): dm = runtime.device_manager() p = runtime.get_configuration() - p.video[0].camera.identifier = dm.select(DeviceKind.Camera, "simulated.*random.*") + p.video[0].camera.identifier = dm.select(DeviceKind.Camera, "simulated.*empty.*") p.video[0].storage.identifier = dm.select(DeviceKind.Storage, "Trash") p.video[0].camera.settings.binning = 1 p.video[0].camera.settings.shape = (64, 48) - p.video[0].camera.settings.exposure_time_us = 1e4 + p.video[0].camera.settings.exposure_time_us = 1e3 p.video[0].camera.settings.pixel_type = acquire.SampleType.U8 - p.video[0].camera.settings.input_triggers.frame_start.line = 1 - p.video[0].camera.settings.input_triggers.frame_start.enable = True - p.video[0].max_frame_count = 100 + p.video[0].camera.settings.input_triggers.frame_start = Trigger(enable=True, line=0, edge='Rising') + p.video[0].max_frame_count = 10 p = runtime.set_configuration(p) + assert p.video[0].camera.settings.input_triggers.frame_start.enable + runtime.start() - # Snap one frame - runtime.execute_trigger(0) - packet = wait_for_data(runtime, 0) - # TODO: why do i get more than one frame? - assert packet.get_frame_count() >= 1 - frames = tuple(packet.frames()) - assert frames[0].metadata().frame_id >= 0 - del frames - del packet + # No triggers yet, so no data. + assert runtime.get_available_data(0) is None - # Snap another frame - runtime.execute_trigger(0) - packet = wait_for_data(runtime, 0) - # TODO: why do i get more than one frame? - assert packet.get_frame_count() >= 1 - frames = tuple(packet.frames()) - assert frames[0].metadata().frame_id >= 1 - del frames - del packet + # Snap a few individual frames + for i in range(p.video[0].max_frame_count): + runtime.execute_trigger(0) + packet = wait_for_data(runtime, 0) + frames = tuple(packet.frames()) + assert packet.get_frame_count() == 1 + assert frames[0].metadata().frame_id == i + del frames + del packet runtime.stop()