Skip to content

Commit 8cee25b

Browse files
Merge pull request #937 from TheDeanLab/conor-develop-20240711
New feature: WaitForExternalTrigger
2 parents 4b37039 + cc2e20c commit 8cee25b

File tree

3 files changed

+110
-2
lines changed

3 files changed

+110
-2
lines changed

src/navigate/model/devices/daq/ni.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import logging
3535
from threading import Lock
3636
import traceback
37+
import time
3738

3839
# Third Party Imports
3940
import nidaqmx
@@ -177,6 +178,53 @@ def set_external_trigger(self, external_trigger=None):
177178
task.register_done_event(None)
178179
task.register_done_event(self.restart_analog_task_callback_func(task))
179180

181+
def wait_for_external_trigger(
182+
self, trigger_channel, wait_internal=0.001, timeout=-1
183+
):
184+
"""Wait for a digital external trigger.
185+
186+
Parameters
187+
----------
188+
trigger_channel : str
189+
The name of the DAQ PFI digital input.
190+
wait_internal : float
191+
The internal waiting time to check the trigger
192+
timeout : float
193+
Continue on anyway if timeout is reached. timeout < 0 will
194+
run forever.
195+
196+
Returns
197+
-------
198+
result : bool
199+
True for the trigger, False for no trigger.
200+
"""
201+
if not trigger_channel:
202+
logger.info(
203+
"No external trigger channel is specified! Return from waiting!"
204+
)
205+
return False
206+
# Create a digital input task and wait until either a trigger is detected,
207+
# or the timeout is exceeded. If timeout < 0, wait forever...
208+
external_trigger_task = nidaqmx.Task("WaitDigitalEdge")
209+
external_trigger_task.di_channels.add_di_chan(trigger_channel)
210+
211+
total_wait_time = 0.0
212+
result = True
213+
214+
while not external_trigger_task.read():
215+
time.sleep(wait_internal)
216+
total_wait_time += wait_internal
217+
218+
if timeout > 0 and total_wait_time >= timeout:
219+
result = False
220+
break
221+
222+
# Close the task
223+
external_trigger_task.stop()
224+
external_trigger_task.close()
225+
226+
return result
227+
180228
@staticmethod
181229
def restart_analog_task_callback_func(task):
182230
"""Restart analog task callback function.

src/navigate/model/features/common_features.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
import time
3535
from functools import reduce
3636
from threading import Lock
37-
import copy
3837

39-
# Third party imports
4038

4139
# Local application imports
4240
from .image_writer import ImageWriter
@@ -199,6 +197,62 @@ def data_func(self, frame_ids):
199197
return True
200198

201199

200+
class WaitForExternalTrigger:
201+
"""WaitForExternalTrigger class to time parts of the feature list using external input.
202+
203+
This class waits for either an external trigger (or the timeout) before continuing
204+
on to the next feature block in the list. Useful when combined with LoopByCounts
205+
when each iteration may depend on some external event happening.
206+
207+
Notes:
208+
------
209+
- This class pauses the data thread while waiting for the trigger to avoid
210+
camera timeout issues.
211+
212+
- Only digital triggers are handeled at this time: use the PFI inputs on the DAQ.
213+
"""
214+
215+
def __init__(self, model, trigger_channel="/PCIe-6738/PFI4", timeout=-1):
216+
"""Initialize the WaitForExternalTrigger class.
217+
218+
Parameters:
219+
----------
220+
model : MicroscopeModel
221+
The microscope model object used for synchronization.
222+
trigger_channel : str
223+
The name of the DAQ PFI digital input.
224+
timeout : float
225+
Continue on anyway if timeout is reached. timeout < 0 will
226+
run forever.
227+
"""
228+
self.model = model
229+
230+
self.wait_interval = 0.001 # sec
231+
232+
self.task = None
233+
self.trigger_channel = trigger_channel
234+
self.timeout = timeout
235+
236+
self.config_table = {
237+
"signal": {
238+
"main": self.signal_func,
239+
}
240+
}
241+
242+
def signal_func(self):
243+
244+
# Pause the data thread to prevent camera timeout
245+
self.model.pause_data_thread()
246+
247+
result = self.model.active_microscope.daq.wait_for_external_trigger(
248+
self.trigger_channel, self.wait_interval, self.timeout
249+
)
250+
251+
# Resume the data thread
252+
self.model.resume_data_thread()
253+
254+
return result
255+
202256
class WaitToContinue:
203257
"""WaitToContinue class for synchronizing signal and data acquisition.
204258
@@ -619,11 +673,16 @@ def signal_func(self):
619673
self.model.pause_data_thread()
620674

621675
self.current_idx += 1
676+
# Make sure to go back to the beginning if using LoopByCount
677+
if self.current_idx == self.position_count:
678+
self.current_idx = 0
679+
622680
abs_pos_dict = dict(map(lambda k: (f"{k}_abs", pos_dict[k]), pos_dict.keys()))
623681
self.model.logger.debug(f"MoveToNextPositionInMultiPosition: " f"{pos_dict}")
624682
self.model.move_stage(abs_pos_dict, wait_until_done=True)
625683

626684
self.model.logger.debug("MoveToNextPositionInMultiPosition: move done")
685+
627686
# resume data thread
628687
if should_pause_data_thread:
629688
self.model.resume_data_thread()

src/navigate/model/features/feature_related_functions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
ChangeResolution, # noqa
4646
Snap, # noqa
4747
WaitToContinue, # noqa
48+
WaitForExternalTrigger, # noqa
4849
LoopByCount, # noqa
4950
PrepareNextChannel, # noqa
5051
MoveToNextPositionInMultiPositionTable, # noqa

0 commit comments

Comments
 (0)