pyTriggerSync 0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyTriggerSync/__init__.py +6 -0
- pyTriggerSync/config.json +9 -0
- pyTriggerSync/driver.py +535 -0
- pyTriggerSync/graphics/pause.png +0 -0
- pyTriggerSync/graphics/play.png +0 -0
- pyTriggerSync/graphics/refresh.png +0 -0
- pyTriggerSync/graphics/stop.png +0 -0
- pyTriggerSync/main.py +1257 -0
- pytriggersync-0.3.dist-info/METADATA +233 -0
- pytriggersync-0.3.dist-info/RECORD +13 -0
- pytriggersync-0.3.dist-info/WHEEL +5 -0
- pytriggersync-0.3.dist-info/entry_points.txt +2 -0
- pytriggersync-0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#If this package was installed only to use the low-level driver, the PyQt library is not necessarily installed. In this case, importing stuff from main.py would generate an error
|
|
2
|
+
import importlib.util
|
|
3
|
+
package_name = 'PyQt5'
|
|
4
|
+
spec = importlib.util.find_spec(package_name)
|
|
5
|
+
if spec:
|
|
6
|
+
from .main import interface, gui
|
pyTriggerSync/driver.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
''' Note: most of docstrings in this file have been generated automatically by Claude. AI can make mistakes'''
|
|
2
|
+
|
|
3
|
+
import serial
|
|
4
|
+
import serial.tools.list_ports
|
|
5
|
+
|
|
6
|
+
class pyTriggerSync():
|
|
7
|
+
"""
|
|
8
|
+
Low-level driver to communicate with a Teensy microcontroller running the
|
|
9
|
+
``Send_Trigger_Synced_With_External_Trigger_OnlyRemoteControl`` firmware over a
|
|
10
|
+
serial (USB) connection.
|
|
11
|
+
|
|
12
|
+
The Teensy generates an output trigger pulse synchronized to an external trigger
|
|
13
|
+
signal received on its input pin, either continuously (one output pulse per input
|
|
14
|
+
edge, optionally sub-sampled via :attr:`divider`) or in user-controlled bursts of a
|
|
15
|
+
fixed number of pulses (via :meth:`sendtrigger`). See the project's
|
|
16
|
+
``SERIAL_PROTOCOL.md`` for the full list of serial commands understood by the
|
|
17
|
+
firmware.
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
connected : bool
|
|
22
|
+
``True`` if a device is currently connected, ``False`` otherwise.
|
|
23
|
+
port : str
|
|
24
|
+
Serial port used for the connection (e.g. ``'COM4'``). Set at construction time
|
|
25
|
+
and updated by :meth:`connect_device` to reflect the port actually connected to.
|
|
26
|
+
baudrate : int
|
|
27
|
+
Baud rate used for the serial connection. Must match the firmware (115200).
|
|
28
|
+
timeout : float
|
|
29
|
+
Read timeout (in seconds) used for the serial connection. Note that this also
|
|
30
|
+
bounds how long :meth:`sendtrigger` will wait for the immediate acknowledgement
|
|
31
|
+
of a ``trg`` command - not for the burst to actually complete (see
|
|
32
|
+
:meth:`sendtrigger`).
|
|
33
|
+
identifier : str
|
|
34
|
+
Substring expected at the start of the device's identity string (the answer to
|
|
35
|
+
``idn?``). Used by :meth:`list_devices` to recognize a compatible device.
|
|
36
|
+
device : serial.Serial
|
|
37
|
+
The underlying PySerial connection. Only valid while :attr:`connected` is
|
|
38
|
+
``True``; created by :meth:`connect_device`.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self,port='COM4', baudrate=115200, timeout=0.1):
|
|
42
|
+
"""
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
port : str, optional
|
|
46
|
+
Serial port to connect to by default (used by :meth:`connect_device` when
|
|
47
|
+
no port is explicitly specified). Default is ``'COM4'``.
|
|
48
|
+
baudrate : int, optional
|
|
49
|
+
Baud rate to use for the serial connection. Must match the firmware
|
|
50
|
+
(115200). Default is ``115200``.
|
|
51
|
+
timeout : float, optional
|
|
52
|
+
Read timeout (in seconds) for the serial connection. Default is ``0.1``.
|
|
53
|
+
"""
|
|
54
|
+
self.connected = False
|
|
55
|
+
self.port = port
|
|
56
|
+
self.baudrate = baudrate
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
self.identifier = 'Send_Trigger_Synced_With_External_Trigger'
|
|
59
|
+
|
|
60
|
+
def list_devices(self):
|
|
61
|
+
'''
|
|
62
|
+
Scan all serial ports currently visible to the system and check whether any of
|
|
63
|
+
them is running compatible firmware, by briefly connecting to each one in turn
|
|
64
|
+
and comparing its identity string (the answer to ``idn?``) against
|
|
65
|
+
:attr:`identifier`.
|
|
66
|
+
|
|
67
|
+
Each candidate port is connected to and disconnected from in turn; only ports
|
|
68
|
+
that open successfully and respond with a matching identity are kept.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
list_valid_devices : list of str
|
|
73
|
+
A list of all found valid devices. Each element is a human-readable string
|
|
74
|
+
in the format ``"<port>: <description> [<hwid>]"``.
|
|
75
|
+
|
|
76
|
+
Raises
|
|
77
|
+
------
|
|
78
|
+
RuntimeError
|
|
79
|
+
If a device is already connected (scanning would interfere with the active
|
|
80
|
+
connection). Disconnect first.
|
|
81
|
+
'''
|
|
82
|
+
if self.connected:
|
|
83
|
+
raise RuntimeError("Cannot scan for devices while a device is connected. Disconnect first.")
|
|
84
|
+
ports = serial.tools.list_ports.comports()
|
|
85
|
+
list_valid_devices =[]
|
|
86
|
+
for port, desc, hwid in sorted(ports):
|
|
87
|
+
(Msg,ID) = self.connect_device(port)
|
|
88
|
+
if ID == 1:
|
|
89
|
+
try:
|
|
90
|
+
device_identity = self.identity
|
|
91
|
+
if device_identity and device_identity.startswith(self.identifier):
|
|
92
|
+
list_valid_devices.append(f"{port}: {desc} [{hwid}]")
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
finally:
|
|
96
|
+
#Always disconnect, even if reading the identity raised - otherwise the
|
|
97
|
+
#port is left open and the handle is discarded on the next loop iteration.
|
|
98
|
+
self.disconnect_device()
|
|
99
|
+
self.list_valid_devices = list_valid_devices
|
|
100
|
+
return self.list_valid_devices
|
|
101
|
+
|
|
102
|
+
def connect_device(self,port = None, baudrate = None, timeout = None):
|
|
103
|
+
'''
|
|
104
|
+
Attempt to connect to a device on the specified serial port.
|
|
105
|
+
|
|
106
|
+
Upon a successful connection, all relevant device parameters (mode, polarity,
|
|
107
|
+
divider, delay, trigger duration, number of triggers) are read once and cached
|
|
108
|
+
in the corresponding private attributes, so that code relying on the cached
|
|
109
|
+
values (e.g. :meth:`sendtrigger`, which reads the cached mode directly to avoid
|
|
110
|
+
an extra round-trip delay right before firing a trigger) is correct immediately
|
|
111
|
+
after connecting, without requiring an explicit read first.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
port : str, optional
|
|
116
|
+
Serial port to connect to (e.g. ``'COM4'``). If not specified, :attr:`port`
|
|
117
|
+
is used.
|
|
118
|
+
baudrate : int, optional
|
|
119
|
+
Baud rate for the connection. If not specified, :attr:`baudrate` is used.
|
|
120
|
+
timeout : float, optional
|
|
121
|
+
Read timeout (in seconds) for the connection. If not specified,
|
|
122
|
+
:attr:`timeout` is used.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
(Msg, ID) : (str, int)
|
|
127
|
+
``Msg`` is a confirmation message, or the exception raised while
|
|
128
|
+
connecting. ``ID`` is 1 if the connection (including reading back all
|
|
129
|
+
device parameters) was successful, 0 otherwise.
|
|
130
|
+
'''
|
|
131
|
+
if port == None:
|
|
132
|
+
port = self.port
|
|
133
|
+
if baudrate == None:
|
|
134
|
+
baudrate = self.baudrate
|
|
135
|
+
if timeout == None:
|
|
136
|
+
timeout = self.timeout
|
|
137
|
+
try:
|
|
138
|
+
self.device = serial.Serial(port = port, baudrate = baudrate, timeout = timeout)
|
|
139
|
+
self.connected = True
|
|
140
|
+
self.port = port #remember the port actually used, not just the configured default
|
|
141
|
+
#Populate all cached attributes by querying the device now, so that code relying
|
|
142
|
+
#on the cached values (e.g. sendtrigger()'s use of self._mode, kept deliberately
|
|
143
|
+
#cached to avoid an extra round-trip delay right before firing a trigger) is
|
|
144
|
+
#correct immediately after connecting, without requiring an explicit read first.
|
|
145
|
+
_ = self.mode
|
|
146
|
+
_ = self.polarity
|
|
147
|
+
_ = self.divider
|
|
148
|
+
_ = self.delay
|
|
149
|
+
_ = self.triggerduration
|
|
150
|
+
_ = self.number_of_triggers
|
|
151
|
+
ID = 1
|
|
152
|
+
Msg = 'Device connected on ' + self.port
|
|
153
|
+
except Exception as e:
|
|
154
|
+
ID = 0
|
|
155
|
+
Msg = e
|
|
156
|
+
#If the port opened but querying the device failed (e.g. it didn't respond as
|
|
157
|
+
#expected), don't leave a half-open connection behind.
|
|
158
|
+
try:
|
|
159
|
+
self.device.close()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
self.connected = False
|
|
163
|
+
return (Msg,ID)
|
|
164
|
+
|
|
165
|
+
def disconnect_device(self):
|
|
166
|
+
'''
|
|
167
|
+
Disconnect the currently connected device, closing the underlying serial
|
|
168
|
+
connection.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
(Msg, ID) : (str, int)
|
|
173
|
+
``Msg`` is a confirmation message, or the exception raised while
|
|
174
|
+
disconnecting. ``ID`` is 1 if disconnection was successful, 0 otherwise.
|
|
175
|
+
'''
|
|
176
|
+
try:
|
|
177
|
+
self.device.close()
|
|
178
|
+
ID = 1
|
|
179
|
+
Msg = 'Device ' + self.port + ' succesfully disconnected.'
|
|
180
|
+
except Exception as e:
|
|
181
|
+
ID = 0
|
|
182
|
+
Msg = e
|
|
183
|
+
if(ID==1):
|
|
184
|
+
self.connected = False
|
|
185
|
+
return (Msg,ID)
|
|
186
|
+
|
|
187
|
+
def check_valid_connection(self):
|
|
188
|
+
'''
|
|
189
|
+
Verify that a device is currently connected.
|
|
190
|
+
|
|
191
|
+
Raises
|
|
192
|
+
------
|
|
193
|
+
RuntimeError
|
|
194
|
+
If no device is currently connected.
|
|
195
|
+
'''
|
|
196
|
+
if not(self.connected):
|
|
197
|
+
raise RuntimeError("No device is currently connected.")
|
|
198
|
+
|
|
199
|
+
def query(self, q):
|
|
200
|
+
'''
|
|
201
|
+
Send a command to the device and return its reply.
|
|
202
|
+
|
|
203
|
+
Clears any stale unread data from the input buffer first, then writes ``q``
|
|
204
|
+
(terminated with a newline) and reads back exactly one reply line.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
q : str
|
|
209
|
+
Command to send (e.g. ``"mode?"``). A trailing newline is added
|
|
210
|
+
automatically if not already present.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
str
|
|
215
|
+
The device's reply, decoded and stripped of leading/trailing whitespace.
|
|
216
|
+
Empty if no reply was received within the configured :attr:`timeout`.
|
|
217
|
+
'''
|
|
218
|
+
self.device.reset_input_buffer() #This makes sure that there isn't any previous unread data in the serial buffer, which would spoil the data read with the readline() call.
|
|
219
|
+
self.device.write(bytes(q.strip() + '\n', 'utf-8'))
|
|
220
|
+
data = self.device.readline()
|
|
221
|
+
return data.decode().strip()
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def identity(self):
|
|
225
|
+
'''
|
|
226
|
+
str: The device's identity string (the answer to ``idn?``).
|
|
227
|
+
|
|
228
|
+
Raises
|
|
229
|
+
------
|
|
230
|
+
RuntimeError
|
|
231
|
+
If no device is currently connected.
|
|
232
|
+
'''
|
|
233
|
+
self.check_valid_connection()
|
|
234
|
+
identity_string = self.query("IDN?\n")
|
|
235
|
+
return identity_string
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def mode(self):
|
|
239
|
+
'''
|
|
240
|
+
int: The device's current operating mode: ``0`` for "Continuous
|
|
241
|
+
Trigger" (every qualifying input edge automatically fires an output pulse) or
|
|
242
|
+
``1`` for "Trigger Controlled by User" (output pulses only fire in response to
|
|
243
|
+
:meth:`sendtrigger`).
|
|
244
|
+
|
|
245
|
+
Reading this property queries the device (``mode?``) and caches the result.
|
|
246
|
+
|
|
247
|
+
Setting this property writes the new mode to the device (``mode <value>``)
|
|
248
|
+
and reads it back to confirm the change took effect.
|
|
249
|
+
|
|
250
|
+
Raises
|
|
251
|
+
------
|
|
252
|
+
RuntimeError
|
|
253
|
+
If no device is currently connected; or (setter only) if the device did
|
|
254
|
+
not acknowledge the command, or if the mode read back does not match the
|
|
255
|
+
requested value.
|
|
256
|
+
ValueError
|
|
257
|
+
(setter only) If the assigned value is not ``0`` or ``1``.
|
|
258
|
+
'''
|
|
259
|
+
self.check_valid_connection()
|
|
260
|
+
self._mode = int(self.query("mode?\n"))
|
|
261
|
+
return self._mode
|
|
262
|
+
|
|
263
|
+
@mode.setter
|
|
264
|
+
def mode(self, new_mode):
|
|
265
|
+
self.check_valid_connection()
|
|
266
|
+
if new_mode in [0,1]:
|
|
267
|
+
answer = self.query("mode " + str(new_mode) + "\n")
|
|
268
|
+
if answer != '0':
|
|
269
|
+
raise RuntimeError(f"Device was not able to set the mode correctly. Code returned: " + answer)
|
|
270
|
+
self._mode = int(self.query("mode?\n"))
|
|
271
|
+
if self._mode == new_mode:
|
|
272
|
+
return self._mode
|
|
273
|
+
else:
|
|
274
|
+
raise RuntimeError(f"Device acknowledged the command, but the mode read back ({self._mode}) does not match the requested value ({new_mode}).")
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError(f"Input parameter must be equal to either 0 or 1")
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def polarity(self):
|
|
281
|
+
'''
|
|
282
|
+
int: The device's current output trigger polarity: ``0`` for
|
|
283
|
+
"Negative" (output idles high, pulses low) or ``1`` for "Positive" (output
|
|
284
|
+
idles low, pulses high).
|
|
285
|
+
|
|
286
|
+
Reading this property queries the device (``polarity?``) and caches the
|
|
287
|
+
result.
|
|
288
|
+
|
|
289
|
+
Setting this property writes the new polarity to the device
|
|
290
|
+
(``polarity <value>``) and reads it back to confirm the change took effect.
|
|
291
|
+
|
|
292
|
+
Raises
|
|
293
|
+
------
|
|
294
|
+
RuntimeError
|
|
295
|
+
If no device is currently connected; or (setter only) if the device did
|
|
296
|
+
not acknowledge the command, or if the polarity read back does not match
|
|
297
|
+
the requested value.
|
|
298
|
+
ValueError
|
|
299
|
+
(setter only) If the assigned value is not ``0`` or ``1``.
|
|
300
|
+
'''
|
|
301
|
+
self.check_valid_connection()
|
|
302
|
+
self._polarity = int(self.query("polarity?\n"))
|
|
303
|
+
return self._polarity
|
|
304
|
+
|
|
305
|
+
@polarity.setter
|
|
306
|
+
def polarity(self, new_polarity):
|
|
307
|
+
self.check_valid_connection()
|
|
308
|
+
if new_polarity in [0,1]:
|
|
309
|
+
answer = self.query("polarity " + str(new_polarity) + "\n")
|
|
310
|
+
if answer != '0':
|
|
311
|
+
raise RuntimeError(f"Device was not able to set the polarity correctly. Code returned: " + answer)
|
|
312
|
+
self._polarity = int(self.query("polarity?\n"))
|
|
313
|
+
if self._polarity == new_polarity:
|
|
314
|
+
return self._polarity
|
|
315
|
+
else:
|
|
316
|
+
raise RuntimeError(f"Device acknowledged the command, but the polarity read back ({self._polarity}) does not match the requested value ({new_polarity}).")
|
|
317
|
+
else:
|
|
318
|
+
raise ValueError(f"Input parameter must be equal to either 0 or 1")
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def divider(self):
|
|
323
|
+
'''
|
|
324
|
+
int: The device's current sub-sampling divider. In Continuous Trigger
|
|
325
|
+
mode, one output pulse is emitted for every :attr:`divider` input edges (has
|
|
326
|
+
no effect during a :meth:`sendtrigger`-initiated burst).
|
|
327
|
+
|
|
328
|
+
Reading this property queries the device (``divider?``) and caches the
|
|
329
|
+
result.
|
|
330
|
+
|
|
331
|
+
Setting this property writes the new divider to the device
|
|
332
|
+
(``divider <value>``) and reads it back to confirm the change took effect.
|
|
333
|
+
|
|
334
|
+
Raises
|
|
335
|
+
------
|
|
336
|
+
RuntimeError
|
|
337
|
+
If no device is currently connected; or (setter only) if the device did
|
|
338
|
+
not acknowledge the command, or if the divider read back does not match
|
|
339
|
+
the requested value.
|
|
340
|
+
ValueError
|
|
341
|
+
(setter only) If the assigned value cannot be converted to a positive
|
|
342
|
+
``int``.
|
|
343
|
+
'''
|
|
344
|
+
self.check_valid_connection()
|
|
345
|
+
self._divider = int(self.query("divider?\n"))
|
|
346
|
+
return self._divider
|
|
347
|
+
|
|
348
|
+
@divider.setter
|
|
349
|
+
def divider(self, new_divider):
|
|
350
|
+
self.check_valid_connection()
|
|
351
|
+
try:
|
|
352
|
+
new_divider = int(new_divider)
|
|
353
|
+
except:
|
|
354
|
+
raise ValueError(f"Input parameter must be a valid integer")
|
|
355
|
+
if new_divider <= 0 :
|
|
356
|
+
raise ValueError(f"Input parameter must be positive")
|
|
357
|
+
answer = self.query("divider " + str(new_divider) + "\n")
|
|
358
|
+
if answer != '0':
|
|
359
|
+
raise RuntimeError(f"Device was not able to set the divider correctly. Code returned: " + answer)
|
|
360
|
+
self._divider = int(self.query("divider?\n"))
|
|
361
|
+
if self._divider == new_divider:
|
|
362
|
+
return self._divider
|
|
363
|
+
else:
|
|
364
|
+
raise RuntimeError(f"Device was not able to set the divider correctly.")
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def delay(self):
|
|
369
|
+
'''
|
|
370
|
+
int: The device's current delay (in nanoseconds) between an input
|
|
371
|
+
trigger edge and the generated output pulse.
|
|
372
|
+
|
|
373
|
+
Reading this property queries the device (``delay?``) and caches the result.
|
|
374
|
+
|
|
375
|
+
Setting this property writes the new delay to the device (``delay <value>``)
|
|
376
|
+
and reads it back to confirm the change took effect.
|
|
377
|
+
|
|
378
|
+
Raises
|
|
379
|
+
------
|
|
380
|
+
RuntimeError
|
|
381
|
+
If no device is currently connected; or (setter only) if the device did
|
|
382
|
+
not acknowledge the command, or if the delay read back does not match the
|
|
383
|
+
requested value.
|
|
384
|
+
ValueError
|
|
385
|
+
(setter only) If the assigned value cannot be converted to a non-negative
|
|
386
|
+
``int``.
|
|
387
|
+
'''
|
|
388
|
+
self.check_valid_connection()
|
|
389
|
+
self._delay = int(self.query("delay?\n"))
|
|
390
|
+
return self._delay
|
|
391
|
+
|
|
392
|
+
@delay.setter
|
|
393
|
+
def delay(self, new_delay):
|
|
394
|
+
self.check_valid_connection()
|
|
395
|
+
try:
|
|
396
|
+
new_delay = int(new_delay)
|
|
397
|
+
except:
|
|
398
|
+
raise ValueError(f"Input parameter must be a valid integer")
|
|
399
|
+
if new_delay < 0 :
|
|
400
|
+
raise ValueError(f"Input parameter must be positive or zero")
|
|
401
|
+
answer = self.query("delay " + str(new_delay) + "\n")
|
|
402
|
+
if answer != '0':
|
|
403
|
+
raise RuntimeError(f"Device was not able to set the delay correctly. Code returned: " + answer)
|
|
404
|
+
self._delay = int(self.query("delay?\n"))
|
|
405
|
+
if self._delay == new_delay:
|
|
406
|
+
return self._delay
|
|
407
|
+
else:
|
|
408
|
+
raise RuntimeError(f"Device was not able to set the delay correctly.")
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def triggerduration(self):
|
|
413
|
+
'''
|
|
414
|
+
int: The device's current output pulse duration, in nanoseconds.
|
|
415
|
+
|
|
416
|
+
Reading this property queries the device (``triggerduration?``) and caches
|
|
417
|
+
the result.
|
|
418
|
+
|
|
419
|
+
Setting this property writes the new duration to the device
|
|
420
|
+
(``triggerduration <value>``) and reads it back to confirm the change took
|
|
421
|
+
effect.
|
|
422
|
+
|
|
423
|
+
Raises
|
|
424
|
+
------
|
|
425
|
+
RuntimeError
|
|
426
|
+
If no device is currently connected; or (setter only) if the device did
|
|
427
|
+
not acknowledge the command, or if the duration read back does not match
|
|
428
|
+
the requested value.
|
|
429
|
+
ValueError
|
|
430
|
+
(setter only) If the assigned value cannot be converted to a positive
|
|
431
|
+
``int``.
|
|
432
|
+
'''
|
|
433
|
+
self.check_valid_connection()
|
|
434
|
+
self._triggerduration = int(self.query("triggerduration?\n"))
|
|
435
|
+
return self._triggerduration
|
|
436
|
+
|
|
437
|
+
@triggerduration.setter
|
|
438
|
+
def triggerduration(self, new_triggerduration):
|
|
439
|
+
self.check_valid_connection()
|
|
440
|
+
try:
|
|
441
|
+
new_triggerduration = int(new_triggerduration)
|
|
442
|
+
except:
|
|
443
|
+
raise ValueError(f"Input parameter must be a valid integer")
|
|
444
|
+
if new_triggerduration <= 0 :
|
|
445
|
+
raise ValueError(f"Input parameter must be positive")
|
|
446
|
+
answer = self.query("triggerduration " + str(new_triggerduration) + "\n")
|
|
447
|
+
if answer != '0':
|
|
448
|
+
raise RuntimeError(f"Device was not able to set the trigger duration correctly. Code returned: " + answer)
|
|
449
|
+
self._triggerduration = int(self.query("triggerduration?\n"))
|
|
450
|
+
if self._triggerduration == new_triggerduration:
|
|
451
|
+
return self._triggerduration
|
|
452
|
+
else:
|
|
453
|
+
raise RuntimeError(f"Device was not able to set the trigger duration correctly.")
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def number_of_triggers(self):
|
|
458
|
+
'''
|
|
459
|
+
int: The number of output pulses a single :meth:`sendtrigger` call
|
|
460
|
+
will produce (only meaningful in "Trigger Controlled by User" mode).
|
|
461
|
+
|
|
462
|
+
Reading this property queries the device (``numpulses?``) and caches the
|
|
463
|
+
result.
|
|
464
|
+
|
|
465
|
+
Setting this property writes the new pulse count to the device
|
|
466
|
+
(``numpulses <value>``) and reads it back to confirm the change took effect.
|
|
467
|
+
|
|
468
|
+
Raises
|
|
469
|
+
------
|
|
470
|
+
RuntimeError
|
|
471
|
+
If no device is currently connected; or (setter only) if the device did
|
|
472
|
+
not acknowledge the command, or if the value read back does not match the
|
|
473
|
+
requested value.
|
|
474
|
+
ValueError
|
|
475
|
+
(setter only) If the assigned value cannot be converted to a positive
|
|
476
|
+
``int``.
|
|
477
|
+
'''
|
|
478
|
+
self.check_valid_connection()
|
|
479
|
+
self._number_of_triggers = int(self.query("numpulses?\n"))
|
|
480
|
+
return self._number_of_triggers
|
|
481
|
+
|
|
482
|
+
@number_of_triggers.setter
|
|
483
|
+
def number_of_triggers(self, new_number_of_triggers):
|
|
484
|
+
self.check_valid_connection()
|
|
485
|
+
try:
|
|
486
|
+
new_number_of_triggers = int(new_number_of_triggers)
|
|
487
|
+
except:
|
|
488
|
+
raise ValueError(f"Input parameter must be a valid integer")
|
|
489
|
+
if new_number_of_triggers <= 0:
|
|
490
|
+
raise ValueError(f"Input parameter must be positive")
|
|
491
|
+
answer = self.query("numpulses " + str(new_number_of_triggers) + "\n")
|
|
492
|
+
if answer != '0':
|
|
493
|
+
raise RuntimeError(f"Device was not able to set the number of triggers correctly. Code returned: " + answer)
|
|
494
|
+
self._number_of_triggers = int(self.query("numpulses?\n"))
|
|
495
|
+
if self._number_of_triggers == new_number_of_triggers:
|
|
496
|
+
return self._number_of_triggers
|
|
497
|
+
else:
|
|
498
|
+
raise RuntimeError(f"Device was not able to set the number of triggers correctly.")
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
def sendtrigger(self):
|
|
502
|
+
'''
|
|
503
|
+
Start a burst of output trigger pulses. Only valid in "Trigger Controlled by
|
|
504
|
+
User" mode (:attr:`mode` == 1).
|
|
505
|
+
|
|
506
|
+
Sends the ``trg`` command and waits for the device's immediate
|
|
507
|
+
acknowledgement. The acknowledgement only confirms that the device accepted
|
|
508
|
+
the command and armed the burst - it does NOT mean the burst has completed.
|
|
509
|
+
Completion depends on real trigger edges arriving on the device's input pin
|
|
510
|
+
(one edge per pulse of :attr:`number_of_triggers`) and is reported later,
|
|
511
|
+
asynchronously, directly by the device - it is not read by this method.
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
bool
|
|
516
|
+
``True`` if the device acknowledged the command, ``False`` otherwise.
|
|
517
|
+
|
|
518
|
+
Raises
|
|
519
|
+
------
|
|
520
|
+
RuntimeError
|
|
521
|
+
If the device is currently in "Continuous Trigger" mode (:attr:`mode` ==
|
|
522
|
+
0), in which case sending a single trigger is not applicable.
|
|
523
|
+
'''
|
|
524
|
+
#NOTE: uses the cached self._mode (not the 'mode' property) deliberately, to avoid
|
|
525
|
+
#the extra round-trip delay of querying the device right before firing a trigger.
|
|
526
|
+
#self._mode is populated as soon as the device connects (see connect_device()),
|
|
527
|
+
#so it is guaranteed to exist and to be correct as long as nothing outside this
|
|
528
|
+
#driver changes the device's mode without going through this driver's mode setter.
|
|
529
|
+
if self._mode == 0:
|
|
530
|
+
raise RuntimeError(f"Device is in modality 'Continuous Trigger', it is not possible to send a single trigger. Change the mode to 'Trigger Controlled by User' first.")
|
|
531
|
+
data = self.query("trg 1\n")
|
|
532
|
+
if data == '0':
|
|
533
|
+
return True
|
|
534
|
+
else:
|
|
535
|
+
return False
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|