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/main.py ADDED
@@ -0,0 +1,1257 @@
1
+ import os
2
+ import PyQt5
3
+ dirname = os.path.dirname(PyQt5.__file__)
4
+ plugin_path = os.path.join(dirname, 'plugins', 'platforms')
5
+ os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
6
+ import PyQt5.QtWidgets as Qt# QApplication, QWidget, QMainWindow, QPushButton, QHBoxLayout
7
+ import PyQt5.QtGui as QtGui
8
+ import PyQt5.QtCore as QtCore
9
+ import logging
10
+ import sys
11
+ import argparse
12
+
13
+ import abstract_instrument_interface
14
+ import pyTriggerSync.driver
15
+
16
+
17
+
18
+ graphics_dir = os.path.join(os.path.dirname(__file__), 'graphics')
19
+
20
+ ##This application follows the model-view-controller paradigm, but with the view and controller defined inside the same object (the GUI)
21
+ ##The model is defined by the class 'interface', and the view+controller is defined by the class 'gui'.
22
+
23
+ class interface(abstract_instrument_interface.abstract_interface):
24
+ """
25
+ High-level interface for the pyTriggerSync Teensy device.
26
+
27
+ Wraps :class:`pyTriggerSync.driver.pyTriggerSync` with input validation,
28
+ settings persistence, and Qt signal emission. Follows the
29
+ model-view-controller pattern: this class is the *model*, and the companion
30
+ :class:`gui` class is the *view + controller*.
31
+
32
+ Class-level attributes
33
+ ----------------------
34
+ output : dict
35
+ Dictionary of output data produced by this interface. Defined at class
36
+ level so callers can inspect the keys without instantiating the class.
37
+ Currently empty; reserved for future use.
38
+
39
+ Signals
40
+ -------
41
+ sig_list_devices_updated : list of str
42
+ Emitted when the list of available devices is refreshed. Carries the
43
+ new list of device-descriptor strings (each in the form
44
+ ``"<port>: <description> [<hwid>]"``).
45
+ sig_delay_changed : object
46
+ Emitted when the output trigger delay has been read from or written to
47
+ the device. Carries the new delay value in nanoseconds (``int``).
48
+ sig_triggerduration_changed : object
49
+ Emitted when the output trigger duration has been read from or written
50
+ to the device. Carries the new duration in nanoseconds (``int``).
51
+ sig_mode_changed : int
52
+ Emitted when the device mode changes. Carries ``0`` ("Continuous
53
+ Trigger") or ``1`` ("Trigger Controlled by User").
54
+ sig_polarity_changed : int
55
+ Emitted when the output polarity changes. Carries ``0`` ("Negative")
56
+ or ``1`` ("Positive").
57
+ sig_divider_changed : int
58
+ Emitted when the sub-sampling divider changes. Carries the new divider
59
+ value (positive integer).
60
+ sig_number_of_triggers_changed : int
61
+ Emitted when the number of output pulses per burst changes. Carries
62
+ the new value (positive integer).
63
+ sig_external_trigger_changed : bool
64
+ Emitted when the external-trigger enable flag changes. Carries the new
65
+ boolean value.
66
+
67
+ Instance attributes
68
+ -------------------
69
+ instrument : pyTriggerSync.driver.pyTriggerSync
70
+ Low-level driver instance used for all serial communication with the
71
+ device.
72
+ connected_device_name : str
73
+ Human-readable name of the currently connected device (empty string
74
+ when not connected).
75
+ connected_device_port : str
76
+ Serial port of the currently connected device (set after a successful
77
+ connection).
78
+ list_devices : list of str
79
+ Most recently scanned list of compatible device descriptors.
80
+ settings : dict
81
+ Persistent settings dict with the following keys:
82
+
83
+ ``'delay'`` : int
84
+ Output trigger delay in nanoseconds. Must be >= 0. Default: ``0``.
85
+ ``'triggerduration'`` : int
86
+ Output pulse duration in nanoseconds. Must be > 0. Default: ``100``.
87
+ ``'mode'`` : int
88
+ Device operating mode: ``0`` = Continuous Trigger,
89
+ ``1`` = Trigger Controlled by User. Default: ``0``.
90
+ ``'polarity'`` : int
91
+ Output polarity: ``0`` = Negative, ``1`` = Positive. Default: ``1``.
92
+ ``'divider'`` : int
93
+ Sub-sampling divider (pass 1-in-N input edges through). Must be
94
+ > 0. Default: ``1``.
95
+ ``'number_of_triggers'`` : int
96
+ Number of output pulses per :meth:`fire_trigger` call (only
97
+ meaningful in mode 1). Must be > 0. Default: ``1``.
98
+ ``'external_trigger'`` : bool
99
+ When ``True``, :meth:`receive_trigger` forwards incoming triggers
100
+ to :meth:`fire_trigger`. Default: ``True``.
101
+ """
102
+
103
+ output = {} #We define these also as class variables, to make it possible to see which data is produced by this interface without having to create an object
104
+
105
+ ## SIGNALS THAT WILL BE USED TO COMMUNICATE WITH THE GUI
106
+ # | Triggered when ... | Sends as parameter
107
+ # # -----------------------------------------------------------------------------------------------------------------------
108
+ sig_list_devices_updated = QtCore.pyqtSignal(list) # | List of devices is updated | List of devices
109
+ sig_delay_changed = QtCore.pyqtSignal(object) # | Delay has changed/been read from instrument | New Delay
110
+ sig_triggerduration_changed = QtCore.pyqtSignal(object) # | Trigger Duration has changed/been read from instrument | New Trigger Duration
111
+ sig_mode_changed = QtCore.pyqtSignal(int) # | The mode has changed | 0 ("Continuous Trigger") or 1 ("Trigger Controlled by User")
112
+ sig_polarity_changed = QtCore.pyqtSignal(int) # | The polarity has changed | 0 ("Negative") or 1 ("Positive")
113
+ sig_divider_changed = QtCore.pyqtSignal(int) # | The divider has changed | New Divider
114
+ sig_number_of_triggers_changed = QtCore.pyqtSignal(int) # | The number of triggers has changed | New Number of Triggers
115
+ sig_external_trigger_changed = QtCore.pyqtSignal(bool) # | The external trigger setting has changed | New External Trigger Setting
116
+
117
+ def __init__(self, **kwargs):
118
+ """
119
+ Initialize the interface, create the driver instance, and scan for
120
+ connected devices.
121
+
122
+ Parameters
123
+ ----------
124
+ **kwargs
125
+ Passed through to
126
+ :class:`abstract_instrument_interface.abstract_interface`.
127
+ """
128
+ self.output = {}
129
+ ### Default values of settings (might be overlapped by settings saved in .json files later)
130
+ self.settings = { 'delay': 0,
131
+ 'triggerduration': 100,
132
+ 'mode': 0,
133
+ 'polarity': 1,
134
+ 'divider': 1,
135
+ 'number_of_triggers': 1,
136
+ 'external_trigger' : True
137
+ }
138
+ self.list_devices = [] #list of devices found
139
+ self.connected_device_name = ''
140
+ self.continuous_read = True
141
+ self._units = 'ns'
142
+ self._mode_names = ["Continuous Trigger", "Trigger Controlled by User"]
143
+ self._polarity_names = ["Negative", "Positive"]
144
+
145
+ self.instrument = pyTriggerSync.driver.pyTriggerSync()
146
+ super().__init__(**kwargs)
147
+
148
+ self.refresh_list_devices()
149
+
150
+ def refresh_list_devices(self):
151
+ '''
152
+ Scan for available devices and update :attr:`list_devices`.
153
+
154
+ Calls :meth:`~pyTriggerSync.driver.pyTriggerSync.list_devices` on the
155
+ driver, logs the number of devices found, stores the result in
156
+ :attr:`list_devices`, and emits :attr:`sig_list_devices_updated` via
157
+ :meth:`send_list_devices`.
158
+ '''
159
+ self.logger.info(f"Looking for devices...")
160
+ list_valid_devices = self.instrument.list_devices()
161
+ self.logger.info(f"Found {len(list_valid_devices)} devices.")
162
+ self.list_devices = list_valid_devices
163
+ self.send_list_devices()
164
+
165
+ def send_list_devices(self):
166
+ '''
167
+ Emit :attr:`sig_list_devices_updated` with the current device list.
168
+
169
+ Passes :attr:`list_devices` directly as the signal payload. Typically
170
+ called after :meth:`refresh_list_devices`, and also called by the GUI
171
+ after it has connected its slots, to trigger an initial population of
172
+ the device combo box.
173
+ '''
174
+ self.sig_list_devices_updated.emit(self.list_devices)
175
+
176
+ def connect_device(self, device_full_name):
177
+ '''
178
+ Connect to the device identified by ``device_full_name``.
179
+
180
+ Extracts the serial port from the device descriptor string (everything
181
+ before the first ``':'``), calls the driver's
182
+ :meth:`~pyTriggerSync.driver.pyTriggerSync.connect_device`, and on
183
+ success calls :meth:`set_connected_state` to push the persisted
184
+ settings to the device. On failure, calls
185
+ :meth:`set_disconnected_state` and logs an error.
186
+
187
+ Parameters
188
+ ----------
189
+ device_full_name : str
190
+ A device descriptor string as returned by
191
+ :meth:`refresh_list_devices`, e.g.
192
+ ``"COM4: USB Serial [USB VID:PID=16C0:0483 ...]"``.
193
+ If empty, an error is logged and the method returns immediately.
194
+ '''
195
+ if(device_full_name==''): # Check that the name is not empty
196
+ self.logger.error("No valid device has been selected")
197
+ return
198
+ self.set_connecting_state()
199
+ device_port = device_full_name.split(':', 1)[0]
200
+ self.logger.info(f"Connecting to device on port {device_port}...")
201
+
202
+ try:
203
+ (Msg,ID) = self.instrument.connect_device(port = device_port)
204
+ if(ID==1): #If connection was successful
205
+ self.logger.info(f"Connected to device {device_full_name}.")
206
+ self.connected_device_name = device_full_name
207
+ self.connected_device_port = device_port
208
+ self.set_connected_state()
209
+ else: #If connection was not successful
210
+ self.logger.error(f"Error: {Msg}")
211
+ self.set_disconnected_state()
212
+ pass
213
+ except Exception as e:
214
+ self.logger.error(f"Error: {e}")
215
+ self.set_disconnected_state()
216
+
217
+ def disconnect_device(self):
218
+ '''
219
+ Disconnect the currently connected device.
220
+
221
+ Calls the driver's
222
+ :meth:`~pyTriggerSync.driver.pyTriggerSync.disconnect_device` and
223
+ transitions to the disconnected state regardless of whether the
224
+ driver-level call succeeds (a failed disconnection typically means the
225
+ device was already physically unplugged, so the GUI should still reset).
226
+ '''
227
+ self.logger.info(f"Disconnecting from device {self.connected_device_name}...")
228
+ self.set_disconnecting_state()
229
+ (Msg,ID) = self.instrument.disconnect_device()
230
+ if(ID==1): # If disconnection was successful
231
+ self.logger.info(f"Disconnected from device {self.connected_device_name}.")
232
+ self.set_disconnected_state()
233
+ else: #If disconnection was not successful
234
+ self.logger.error(f"Error: {Msg}")
235
+ self.set_disconnected_state() #When disconnection is not succeful, it is typically because the device alredy lost connection
236
+ #for some reason. In this case, it is still useful to have the widget reset to disconnected state
237
+
238
+ def close(self, **kwargs):
239
+ '''
240
+ Close this interface.
241
+
242
+ Delegates entirely to
243
+ :meth:`abstract_instrument_interface.abstract_interface.close`, which
244
+ emits :attr:`~abstract_instrument_interface.abstract_interface.sig_close`,
245
+ saves settings, and disconnects the device if connected.
246
+ '''
247
+ super().close(**kwargs)
248
+
249
+ # ──────────────────────────────────────────────────────────────────────────
250
+ # Properties — thin wrappers that route attribute access through the
251
+ # get_*/set_* methods, so that both the GUI and external callers can use
252
+ # natural Python attribute syntax (e.g. ``iface.delay = 500``).
253
+ # ──────────────────────────────────────────────────────────────────────────
254
+
255
+ @property
256
+ def delay(self):
257
+ '''int: Output trigger delay in nanoseconds. Reads from and writes to the device.'''
258
+ return self.get_delay()
259
+
260
+ @delay.setter
261
+ def delay(self, value):
262
+ self.set_delay(value)
263
+
264
+ @property
265
+ def triggerduration(self):
266
+ '''int: Output pulse duration in nanoseconds. Reads from and writes to the device.'''
267
+ return self.get_triggerduration()
268
+
269
+ @triggerduration.setter
270
+ def triggerduration(self, value):
271
+ self.set_triggerduration(value)
272
+
273
+ @property
274
+ def divider(self):
275
+ '''int: Sub-sampling divider. Reads from and writes to the device.'''
276
+ return self.get_divider()
277
+
278
+ @divider.setter
279
+ def divider(self, value):
280
+ self.set_divider(value)
281
+
282
+ @property
283
+ def mode(self):
284
+ '''int: Device operating mode (0 = Continuous, 1 = User-controlled). Reads from and writes to the device.'''
285
+ return self.get_mode()
286
+
287
+ @mode.setter
288
+ def mode(self, value):
289
+ self.set_mode(value)
290
+
291
+ @property
292
+ def polarity(self):
293
+ '''int: Output polarity (0 = Negative, 1 = Positive). Reads from and writes to the device.'''
294
+ return self.get_polarity()
295
+
296
+ @polarity.setter
297
+ def polarity(self, value):
298
+ self.set_polarity(value)
299
+
300
+ @property
301
+ def number_of_triggers(self):
302
+ '''int: Number of output pulses per burst (mode 1 only). Reads from and writes to the device.'''
303
+ return self.get_number_of_triggers()
304
+
305
+ @number_of_triggers.setter
306
+ def number_of_triggers(self, value):
307
+ self.set_number_of_triggers(value)
308
+
309
+ def set_connected_state(self):
310
+ '''
311
+ Extend the parent :meth:`~abstract_instrument_interface.abstract_interface.set_connected_state`
312
+ with device-specific initialization.
313
+
314
+ After calling the parent (which emits
315
+ :attr:`~abstract_instrument_interface.abstract_interface.sig_connected`
316
+ with ``SIG_CONNECTED``), pushes all persisted values from
317
+ :attr:`settings` to the device in order: mode, polarity, delay,
318
+ trigger duration, divider, and number of triggers.
319
+ '''
320
+ super().set_connected_state()
321
+ self.logger.info(f"Setting parameters (based on values in config.json file)... ")
322
+ self.set_mode(self.settings['mode'])
323
+ self.set_polarity(self.settings['polarity'])
324
+ self.set_delay(self.settings['delay'])
325
+ self.set_triggerduration(self.settings['triggerduration'])
326
+ self.set_divider(self.settings['divider'])
327
+ self.set_number_of_triggers(self.settings['number_of_triggers'])
328
+
329
+ def get_delay(self):
330
+ '''
331
+ Read the current output trigger delay from the device.
332
+
333
+ Updates :attr:`settings` ``['delay']`` and emits
334
+ :attr:`sig_delay_changed`.
335
+
336
+ Returns
337
+ -------
338
+ int
339
+ Current delay in nanoseconds.
340
+ '''
341
+ self.settings['delay'] = self.instrument.delay
342
+ self.sig_delay_changed.emit(self.settings['delay'])
343
+ return self.settings['delay']
344
+
345
+ def set_delay(self, delay):
346
+ '''
347
+ Validate and write a new output trigger delay to the device.
348
+
349
+ On success, updates :attr:`settings` ``['delay']`` and emits
350
+ :attr:`sig_delay_changed`. On any validation or communication error,
351
+ logs the error and re-emits :attr:`sig_delay_changed` with the
352
+ previous value so the GUI reverts.
353
+
354
+ Parameters
355
+ ----------
356
+ delay : int or str
357
+ Desired delay in nanoseconds. Must be convertible to ``int`` and
358
+ be >= 0.
359
+ '''
360
+ try:
361
+ delay = int(delay)
362
+ except:
363
+ self.logger.error(f"Delay value must be a valid integer number.")
364
+ self.sig_delay_changed.emit(self.settings['delay'])
365
+ return
366
+ if delay < 0:
367
+ self.logger.error(f"Delay value must be >= 0.")
368
+ self.sig_delay_changed.emit(self.settings['delay'])
369
+ return
370
+ self.logger.info(f"Changing delay to {delay}...")
371
+ try:
372
+ self.instrument.delay = delay
373
+ self.settings['delay'] = self.instrument.delay
374
+ self.sig_delay_changed.emit(self.settings['delay'])
375
+ except Exception as e:
376
+ self.logger.error(f"Error: {e}")
377
+ return
378
+
379
+ def get_divider(self):
380
+ '''
381
+ Read the current sub-sampling divider from the device.
382
+
383
+ Updates :attr:`settings` ``['divider']`` and emits
384
+ :attr:`sig_divider_changed`.
385
+
386
+ Returns
387
+ -------
388
+ int
389
+ Current divider value.
390
+ '''
391
+ self.settings['divider'] = self.instrument.divider
392
+ self.sig_divider_changed.emit(self.settings['divider'])
393
+ return self.settings['divider']
394
+
395
+ def set_divider(self, divider):
396
+ '''
397
+ Validate and write a new sub-sampling divider to the device.
398
+
399
+ On success, updates :attr:`settings` ``['divider']`` and emits
400
+ :attr:`sig_divider_changed`. On any validation or communication error,
401
+ logs the error and re-emits :attr:`sig_divider_changed` with the
402
+ previous value so the GUI reverts.
403
+
404
+ Parameters
405
+ ----------
406
+ divider : int or str
407
+ Desired divider. Must be convertible to ``int`` and be > 0.
408
+ '''
409
+ try:
410
+ divider = int(divider)
411
+ except:
412
+ self.logger.error(f"Divider value must be a valid integer number.")
413
+ self.sig_divider_changed.emit(self.settings['divider'])
414
+ return
415
+ if divider <= 0:
416
+ self.logger.error(f"Divider value must be > 0.")
417
+ self.sig_divider_changed.emit(self.settings['divider'])
418
+ return
419
+ self.logger.info(f"Changing divider to {divider}...")
420
+ try:
421
+ self.instrument.divider = divider
422
+ self.settings['divider'] = self.instrument.divider
423
+ self.sig_divider_changed.emit(self.settings['divider'])
424
+ except Exception as e:
425
+ self.logger.error(f"Error: {e}")
426
+ return
427
+
428
+ def get_polarity(self):
429
+ '''
430
+ Read the current output polarity from the device.
431
+
432
+ Updates :attr:`settings` ``['polarity']`` and emits
433
+ :attr:`sig_polarity_changed`.
434
+
435
+ Returns
436
+ -------
437
+ int
438
+ Current polarity: ``0`` (Negative) or ``1`` (Positive).
439
+ '''
440
+ self.settings['polarity'] = self.instrument.polarity
441
+ self.sig_polarity_changed.emit(self.settings['polarity'])
442
+ return self.settings['polarity']
443
+
444
+ def set_polarity(self, polarity):
445
+ '''
446
+ Validate and write a new output polarity to the device.
447
+
448
+ On success, updates :attr:`settings` ``['polarity']`` and emits
449
+ :attr:`sig_polarity_changed`. On any validation or communication error,
450
+ logs the error and re-emits :attr:`sig_polarity_changed` with the
451
+ previous value so the GUI reverts.
452
+
453
+ Parameters
454
+ ----------
455
+ polarity : int or str
456
+ Desired polarity. Must be convertible to ``int`` and be ``0``
457
+ (Negative) or ``1`` (Positive).
458
+ '''
459
+ try:
460
+ polarity = int(polarity)
461
+ except:
462
+ self.logger.error(f"Polarity value must be a valid integer number.")
463
+ self.sig_polarity_changed.emit(self.settings['polarity'])
464
+ return
465
+ if polarity not in [0, 1]:
466
+ self.logger.error(f"Polarity value must be 0 (Negative) or 1 (Positive).")
467
+ self.sig_polarity_changed.emit(self.settings['polarity'])
468
+ return
469
+ self.logger.info(f"Changing polarity to {polarity}...")
470
+ try:
471
+ self.instrument.polarity = polarity
472
+ self.settings['polarity'] = self.instrument.polarity
473
+ self.sig_polarity_changed.emit(self.settings['polarity'])
474
+ except Exception as e:
475
+ self.logger.error(f"Error: {e}")
476
+ return
477
+
478
+ def get_triggerduration(self):
479
+ '''
480
+ Read the current output pulse duration from the device.
481
+
482
+ Updates :attr:`settings` ``['triggerduration']`` and emits
483
+ :attr:`sig_triggerduration_changed`.
484
+
485
+ Returns
486
+ -------
487
+ int
488
+ Current trigger duration in nanoseconds.
489
+ '''
490
+ self.settings['triggerduration'] = self.instrument.triggerduration
491
+ self.sig_triggerduration_changed.emit(self.settings['triggerduration'])
492
+ return self.settings['triggerduration']
493
+
494
+ def set_triggerduration(self, triggerduration):
495
+ '''
496
+ Validate and write a new output pulse duration to the device.
497
+
498
+ On success, updates :attr:`settings` ``['triggerduration']`` and
499
+ emits :attr:`sig_triggerduration_changed`. On any validation or
500
+ communication error, logs the error and re-emits
501
+ :attr:`sig_triggerduration_changed` with the previous value so the
502
+ GUI reverts.
503
+
504
+ Parameters
505
+ ----------
506
+ triggerduration : int or str
507
+ Desired pulse duration in nanoseconds. Must be convertible to
508
+ ``int`` and be > 0.
509
+ '''
510
+ try:
511
+ triggerduration = int(triggerduration)
512
+ except:
513
+ self.logger.error(f"Trigger duration value must be a valid integer number.")
514
+ self.sig_triggerduration_changed.emit(self.settings['triggerduration'])
515
+ return
516
+ if triggerduration <= 0:
517
+ self.logger.error(f"Trigger duration must be > 0.")
518
+ self.sig_triggerduration_changed.emit(self.settings['triggerduration'])
519
+ return
520
+ self.logger.info(f"Changing Trigger duration to {triggerduration}...")
521
+ try:
522
+ self.instrument.triggerduration = triggerduration
523
+ self.settings['triggerduration'] = self.instrument.triggerduration
524
+ self.sig_triggerduration_changed.emit(self.settings['triggerduration'])
525
+ except Exception as e:
526
+ self.logger.error(f"Error: {e}")
527
+ return
528
+
529
+ def get_number_of_triggers(self):
530
+ '''
531
+ Read the current burst pulse count from the device.
532
+
533
+ Updates :attr:`settings` ``['number_of_triggers']`` and emits
534
+ :attr:`sig_number_of_triggers_changed`.
535
+
536
+ Returns
537
+ -------
538
+ int
539
+ Current number of output pulses per burst.
540
+ '''
541
+ self.settings['number_of_triggers'] = self.instrument.number_of_triggers
542
+ self.sig_number_of_triggers_changed.emit(self.settings['number_of_triggers'])
543
+ return self.settings['number_of_triggers']
544
+
545
+ def set_number_of_triggers(self, number_of_triggers):
546
+ '''
547
+ Validate and write a new burst pulse count to the device.
548
+
549
+ On success, updates :attr:`settings` ``['number_of_triggers']`` and
550
+ emits :attr:`sig_number_of_triggers_changed`. On any validation or
551
+ communication error, logs the error and re-emits
552
+ :attr:`sig_number_of_triggers_changed` with the previous value so the
553
+ GUI reverts.
554
+
555
+ Parameters
556
+ ----------
557
+ number_of_triggers : int or str
558
+ Desired number of output pulses per burst. Must be convertible to
559
+ ``int`` and be > 0.
560
+ '''
561
+ try:
562
+ number_of_triggers = int(number_of_triggers)
563
+ except:
564
+ self.logger.error(f"Number of triggers must be a valid integer number.")
565
+ self.sig_number_of_triggers_changed.emit(self.settings['number_of_triggers'])
566
+ return
567
+ if number_of_triggers <= 0:
568
+ self.logger.error(f"Number of triggers must be > 0.")
569
+ self.sig_number_of_triggers_changed.emit(self.settings['number_of_triggers'])
570
+ return
571
+ self.logger.info(f"Changing number of triggers to {number_of_triggers}...")
572
+ try:
573
+ self.instrument.number_of_triggers = number_of_triggers
574
+ self.settings['number_of_triggers'] = self.instrument.number_of_triggers
575
+ self.sig_number_of_triggers_changed.emit(self.settings['number_of_triggers'])
576
+ except Exception as e:
577
+ self.logger.error(f"Error: {e}")
578
+ return
579
+
580
+ def fire_trigger(self):
581
+ '''
582
+ Send a trigger burst command to the device.
583
+
584
+ Calls :meth:`~pyTriggerSync.driver.pyTriggerSync.sendtrigger` on the
585
+ driver. If the device does not acknowledge the command, logs a warning.
586
+ Only valid when the device is in "Trigger Controlled by User" mode
587
+ (mode 1); the driver will raise :exc:`RuntimeError` if called in mode 0.
588
+ '''
589
+ try:
590
+ self.logger.info(f"Fire!")
591
+ success = self.instrument.sendtrigger()
592
+ if not success:
593
+ self.logger.warning("Trigger command was not acknowledged by the device.")
594
+ except Exception as e:
595
+ self.logger.error(f"Error: {e}")
596
+ return
597
+
598
+ def get_mode(self):
599
+ '''
600
+ Read the current operating mode from the device.
601
+
602
+ Updates :attr:`settings` ``['mode']`` and emits
603
+ :attr:`sig_mode_changed`.
604
+
605
+ Returns
606
+ -------
607
+ int
608
+ Current mode: ``0`` (Continuous Trigger) or ``1`` (Trigger
609
+ Controlled by User).
610
+ '''
611
+ self.settings['mode'] = self.instrument.mode
612
+ self.sig_mode_changed.emit(self.settings['mode'])
613
+ return self.settings['mode']
614
+
615
+ def set_mode(self, mode: int):
616
+ '''
617
+ Validate and write a new operating mode to the device.
618
+
619
+ On success, updates :attr:`settings` ``['mode']`` and emits
620
+ :attr:`sig_mode_changed`. On validation failure, logs an error and
621
+ returns ``None`` without emitting (radio buttons in the GUI are
622
+ always in a valid state, so no revert signal is needed). On
623
+ communication error, logs the error and returns ``None``.
624
+
625
+ Parameters
626
+ ----------
627
+ mode : int
628
+ Desired mode. Must be ``0`` (Continuous Trigger) or ``1``
629
+ (Trigger Controlled by User).
630
+
631
+ Returns
632
+ -------
633
+ int or None
634
+ The new mode on success, ``None`` on failure.
635
+ '''
636
+ try:
637
+ if mode not in [0, 1]:
638
+ self.logger.error(f"Mode index must be 0 (Continuous) or 1 (Trigger Controlled by User).")
639
+ return None
640
+ self.logger.info(f"Setting mode to {self._mode_names[mode]}...")
641
+ self.instrument.mode = mode
642
+ self.settings['mode'] = self.instrument.mode
643
+ self.sig_mode_changed.emit(self.settings['mode'])
644
+ return mode
645
+ except Exception as e:
646
+ self.logger.error(f"Some error occurred when changing the device mode: {e}")
647
+ return None
648
+
649
+ def set_external_trigger(self, external_trigger: bool):
650
+ '''
651
+ Enable or disable forwarding of external triggers to :meth:`fire_trigger`.
652
+
653
+ Updates :attr:`settings` ``['external_trigger']`` and emits
654
+ :attr:`sig_external_trigger_changed`. This setting is only meaningful
655
+ when this interface is combined with other instrument interfaces that
656
+ can emit trigger signals.
657
+
658
+ Parameters
659
+ ----------
660
+ external_trigger : bool
661
+ If ``True``, :meth:`receive_trigger` will call :meth:`fire_trigger`
662
+ when a trigger is received from another interface. If ``False``,
663
+ external triggers are ignored.
664
+
665
+ Returns
666
+ -------
667
+ bool or None
668
+ The new setting on success, ``None`` on failure.
669
+ '''
670
+ self.logger.info(f"Setting external trigger to {external_trigger}...")
671
+ try:
672
+ external_trigger = bool(external_trigger)
673
+ self.settings['external_trigger'] = external_trigger
674
+ self.sig_external_trigger_changed.emit(self.settings['external_trigger'])
675
+ return external_trigger
676
+ except Exception as e:
677
+ self.logger.error(f"Some error occurred when changing the external trigger setting: {e}")
678
+ self.sig_external_trigger_changed.emit(self.settings['external_trigger'])
679
+ return None
680
+
681
+ def receive_trigger(self, **kwargs):
682
+ '''
683
+ Handle an incoming trigger from another instrument interface.
684
+
685
+ If :attr:`settings` ``['external_trigger']`` is ``True``, forwards the
686
+ trigger to :meth:`fire_trigger`. Otherwise, the trigger is silently
687
+ ignored. This method is called by the framework when a trigger signal
688
+ is received from a connected instrument.
689
+ '''
690
+ if self.settings['external_trigger'] == True:
691
+ self.fire_trigger()
692
+
693
+
694
+
695
+ class gui(abstract_instrument_interface.abstract_gui):
696
+ """
697
+ View and controller for the pyTriggerSync interface.
698
+
699
+ Builds the Qt widget layout, connects GUI events to
700
+ :class:`interface` methods (event functions), and updates widgets in
701
+ response to signals emitted by the :class:`interface` (event slots).
702
+
703
+ Attributes
704
+ ----------
705
+ interface : interface
706
+ Reference to the model object this GUI is bound to.
707
+ widgets_enabled_when_connected : list of QWidget
708
+ Widgets that are enabled only while a device is connected.
709
+ widgets_enabled_when_disconnected : list of QWidget
710
+ Widgets that are enabled only while no device is connected.
711
+ widgets_enabled_only_when_mode0 : list of QWidget
712
+ Widgets that are enabled only in Continuous Trigger mode (mode 0).
713
+ widgets_enabled_only_when_mode1 : list of QWidget
714
+ Widgets that are enabled only in User-Controlled mode (mode 1).
715
+ """
716
+
717
+ def __init__(self, interface, parent):
718
+ """
719
+ Parameters
720
+ ----------
721
+ interface : interface
722
+ The model object to bind this GUI to.
723
+ parent : QWidget
724
+ Parent Qt widget.
725
+ """
726
+ super().__init__(interface, parent)
727
+ self._decimal_digits_gui = 5 #How many decimal digits will be used to display position and voltage
728
+ self.initialize()
729
+
730
+ def initialize(self):
731
+ '''
732
+ Build the widget layout, wire up events and signals, and set the
733
+ initial widget state.
734
+
735
+ Called once from :meth:`__init__`. Calls :meth:`create_widgets`,
736
+ :meth:`connect_widgets_events_to_functions`, the parent
737
+ :meth:`~abstract_instrument_interface.abstract_gui.initialize`, and
738
+ then connects all :class:`interface` signals to their slots.
739
+ '''
740
+ self.create_widgets()
741
+ self.connect_widgets_events_to_functions()
742
+
743
+ ### Call the initialize method of the super class.
744
+ super().initialize()
745
+
746
+ ### Connect signals from model to event slots of this GUI
747
+ self.interface.sig_list_devices_updated.connect(self.on_list_devices_updated)
748
+ self.interface.sig_connected.connect(self.on_connection_status_change)
749
+ self.interface.sig_mode_changed.connect(self.on_mode_change)
750
+ self.interface.sig_polarity_changed.connect(self.on_polarity_change)
751
+ self.interface.sig_delay_changed.connect(self.on_delay_change)
752
+ self.interface.sig_triggerduration_changed.connect(self.on_triggerduration_change)
753
+ self.interface.sig_divider_changed.connect(self.on_divider_change)
754
+ self.interface.sig_number_of_triggers_changed.connect(self.on_number_of_triggers_change)
755
+ self.interface.sig_close.connect(self.on_close)
756
+ self.interface.sig_external_trigger_changed.connect(self.on_external_trigger_change)
757
+
758
+ ### SET INITIAL STATE OF WIDGETS
759
+ self.on_divider_change(self.interface.settings['divider'])
760
+ self.on_number_of_triggers_change(self.interface.settings['number_of_triggers'])
761
+ self.on_delay_change(self.interface.settings['delay'])
762
+ self.on_triggerduration_change(self.interface.settings['triggerduration'])
763
+ self.on_mode_change(mode = self.interface.settings['mode'])
764
+ self.on_polarity_change(polarity = self.interface.settings['polarity'])
765
+ self.on_external_trigger_change(external_trigger = self.interface.settings['external_trigger'])
766
+ self.interface.send_list_devices()
767
+ self.on_connection_status_change(self.interface.SIG_DISCONNECTED) #When GUI is created, all widgets are set to the "Disconnected" state
768
+
769
+ def create_widgets(self):
770
+ '''
771
+ Instantiate and lay out all Qt widgets.
772
+
773
+ Populates ``self.container`` (a :class:`QVBoxLayout`) with two rows:
774
+
775
+ - Row 0: connection panel (combo box, refresh button, connect button)
776
+ produced by
777
+ :meth:`~abstract_instrument_interface.abstract_gui.create_panel_connection_listdevices`.
778
+ - Row 1: mode radio buttons, divider field, num-pulses field,
779
+ external-trigger checkbox.
780
+ - Row 2: polarity radio buttons, delay field, trigger-duration field,
781
+ Fire button.
782
+
783
+ Also populates the widget-group lists used by
784
+ :meth:`on_connection_status_change` and :meth:`on_mode_change` to
785
+ enable/disable widgets in bulk.
786
+ '''
787
+
788
+ #Use the custom connection/listdevices panel, defined in abstract_instrument_interface.abstract_gui
789
+ hbox0, widgets_dict = self.create_panel_connection_listdevices()
790
+ for key, val in widgets_dict.items():
791
+ setattr(self,key,val)
792
+
793
+ hbox1 = Qt.QHBoxLayout()
794
+
795
+ self.buttongroup_mode = Qt.QButtonGroup(self.parent)
796
+ self.radio_ContinuousTrigger = Qt.QRadioButton()
797
+ self.radio_ContinuousTrigger.setText(self.interface._mode_names[0])
798
+ self.radio_ContinuousTrigger.value = "0"
799
+ self.label_Divider = Qt.QLabel("Divider: ")
800
+ self.label_Divider .setToolTip('Divider (non-negative integer number)')
801
+ self.label_Divider.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
802
+ self.edit_Divider = Qt.QLineEdit()
803
+ self.edit_Divider.setAlignment(QtCore.Qt.AlignRight)
804
+ self.edit_Divider.setMaximumWidth(50)
805
+ self.radio_UserControlled = Qt.QRadioButton()
806
+ self.radio_UserControlled.setText(self.interface._mode_names[1])
807
+ self.radio_UserControlled.value = "1"
808
+ self.buttongroup_mode.addButton(self.radio_ContinuousTrigger)
809
+ self.buttongroup_mode.addButton(self.radio_UserControlled)
810
+ self.label_NumPulses = Qt.QLabel("Num Pulses: ")
811
+ self.label_NumPulses.setToolTip('Number of output pulses per trigger burst (positive integer)')
812
+ self.label_NumPulses.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
813
+ self.edit_NumPulses = Qt.QLineEdit()
814
+ self.edit_NumPulses.setAlignment(QtCore.Qt.AlignRight)
815
+ self.edit_NumPulses.setMaximumWidth(50)
816
+ self.checkbox_ExternalTrigger = Qt.QCheckBox("External Trigger")
817
+ self.checkbox_ExternalTrigger.setToolTip('This setting is only important when this interface is combined with other instrument interfaces. If checked, the device will fire a trigger whenever it receives a trigger from some other instruments. If not checked, the device will ignore any trigger received at its input, and it will only fire a trigger when the "Fire" button is pressed.')
818
+
819
+ widgets_row1 = [self.radio_ContinuousTrigger,self.label_Divider, self.edit_Divider, self.radio_UserControlled, self.label_NumPulses, self.edit_NumPulses, self.checkbox_ExternalTrigger]
820
+ widgets_row1_stretches = [0]*len(widgets_row1)
821
+ for w,s in zip(widgets_row1,widgets_row1_stretches):
822
+ hbox1.addWidget(w,stretch=s)
823
+ hbox1.addStretch(1)
824
+
825
+ hbox2 = Qt.QHBoxLayout()
826
+
827
+ self.label_Polarity = Qt.QLabel("Trigger Polarity: ")
828
+ self.buttongroup_polarity = Qt.QButtonGroup(self.parent)
829
+ self.radio_PolarityPositive = Qt.QRadioButton()
830
+ self.radio_PolarityPositive.setText(self.interface._polarity_names[1])
831
+ self.radio_PolarityPositive.value = "1"
832
+ self.radio_PolarityNegative = Qt.QRadioButton()
833
+ self.radio_PolarityNegative.setText(self.interface._polarity_names[0])
834
+ self.radio_PolarityNegative.value = "0"
835
+ self.buttongroup_polarity.addButton(self.radio_PolarityPositive)
836
+ self.buttongroup_polarity.addButton(self.radio_PolarityNegative)
837
+
838
+ self.label_Delay = Qt.QLabel("Delay (ns): ")
839
+ self.label_Delay.setToolTip('Delay of generated trigger with respect to input trigger.')
840
+ self.label_Delay.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
841
+ self.edit_Delay = Qt.QLineEdit()
842
+ self.edit_Delay.setAlignment(QtCore.Qt.AlignRight)
843
+ self.edit_Delay.setMaximumWidth(120)
844
+
845
+ self.label_TriggerDuration = Qt.QLabel("Trigger Duration (ns): ")
846
+ self.label_TriggerDuration.setToolTip('Duration of generated trigger.')
847
+ self.label_TriggerDuration.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
848
+ self.edit_TriggerDuration = Qt.QLineEdit()
849
+ self.edit_TriggerDuration.setAlignment(QtCore.Qt.AlignRight)
850
+ self.edit_TriggerDuration.setMaximumWidth(120)
851
+
852
+
853
+
854
+ self.button_FireTrigger = Qt.QPushButton("Fire")
855
+
856
+ widgets_row2 = [self.label_Polarity,self.radio_PolarityPositive,self.radio_PolarityNegative,self.label_Delay,self.edit_Delay,self.label_TriggerDuration,self.edit_TriggerDuration,self.button_FireTrigger]
857
+ widgets_row2_stretches = [0]*len(widgets_row2)
858
+ for w,s in zip(widgets_row2,widgets_row2_stretches):
859
+ hbox2.addWidget(w,stretch=s)
860
+ hbox2.addStretch(1)
861
+
862
+
863
+ self.container = Qt.QVBoxLayout()
864
+ for box in [hbox0,hbox1,hbox2]:
865
+ self.container.addLayout(box)
866
+ self.container.addStretch(1)
867
+
868
+
869
+ # Widgets for which we want to constraint the width by using sizeHint()
870
+ widget_list = []
871
+ for w in widget_list:
872
+ w.setMaximumSize(w.sizeHint())
873
+
874
+ #These widgets are enabled ONLY when interface is connected to a device
875
+ self.widgets_enabled_when_connected = widgets_row1 + widgets_row2
876
+
877
+ #These widgets are enabled ONLY when interface is NOT connected to a device
878
+ self.widgets_enabled_when_disconnected = [self.combo_Devices, self.button_RefreshDeviceList]
879
+
880
+ self.widgets_enabled_only_when_mode0 = [self.edit_Divider, self.label_Divider]
881
+ self.widgets_enabled_only_when_mode1 = [self.button_FireTrigger, self.checkbox_ExternalTrigger, self.label_NumPulses, self.edit_NumPulses]
882
+
883
+
884
+ def connect_widgets_events_to_functions(self):
885
+ '''
886
+ Connect Qt widget signals to their event-function handlers.
887
+
888
+ Called once from :meth:`initialize`. Wires button clicks, line-edit
889
+ ``returnPressed``, radio-button clicks, and checkbox clicks to the
890
+ corresponding ``click_*`` / ``press_enter_*`` methods of this class.
891
+ '''
892
+ self.button_RefreshDeviceList.clicked.connect(self.click_button_refresh_list_devices)
893
+ self.button_ConnectDevice.clicked.connect(self.click_button_connect_disconnect)
894
+
895
+ self.edit_Divider.returnPressed.connect(self.press_enter_edit_divider)
896
+ self.edit_NumPulses.returnPressed.connect(self.press_enter_edit_numpulses)
897
+
898
+ self.button_FireTrigger.clicked.connect(self.click_button_Fire)
899
+ self.edit_Delay.returnPressed.connect(self.press_enter_edit_delay)
900
+ self.edit_TriggerDuration.returnPressed.connect(self.press_enter_edit_triggerduration)
901
+
902
+ self.radio_ContinuousTrigger.clicked.connect(self.click_radio_mode)
903
+ self.radio_UserControlled.clicked.connect(self.click_radio_mode)
904
+ self.radio_PolarityNegative.clicked.connect(self.click_radio_polarity)
905
+ self.radio_PolarityPositive.clicked.connect(self.click_radio_polarity)
906
+ self.checkbox_ExternalTrigger.clicked.connect(self.click_checkbox_external_trigger)
907
+
908
+ ###########################################################################################################
909
+ ### Event Slots. They are normally triggered by signals from the model, and change the GUI accordingly ###
910
+ ###########################################################################################################
911
+
912
+ def on_connection_status_change(self, status):
913
+ '''
914
+ Slot for :attr:`interface.sig_connected`. Enables/disables widget
915
+ groups and updates the Connect/Disconnect button label to reflect the
916
+ new connection state.
917
+
918
+ Parameters
919
+ ----------
920
+ status : int
921
+ One of ``SIG_DISCONNECTED``, ``SIG_DISCONNECTING``,
922
+ ``SIG_CONNECTING``, or ``SIG_CONNECTED``.
923
+ '''
924
+ if status == self.interface.SIG_DISCONNECTED:
925
+ self.disable_widget(self.widgets_enabled_when_connected)
926
+ self.enable_widget(self.widgets_enabled_when_disconnected)
927
+ self.button_ConnectDevice.setText("Connect")
928
+ if status == self.interface.SIG_DISCONNECTING:
929
+ self.disable_widget(self.widgets_enabled_when_connected)
930
+ self.enable_widget(self.widgets_enabled_when_disconnected)
931
+ self.button_ConnectDevice.setText("Disconnecting...")
932
+ if status == self.interface.SIG_CONNECTING:
933
+ self.disable_widget(self.widgets_enabled_when_connected)
934
+ self.enable_widget(self.widgets_enabled_when_disconnected)
935
+ self.button_ConnectDevice.setText("Connecting...")
936
+ if status == self.interface.SIG_CONNECTED:
937
+ self.enable_widget(self.widgets_enabled_when_connected)
938
+ self.disable_widget(self.widgets_enabled_when_disconnected)
939
+ self.button_ConnectDevice.setText("Disconnect")
940
+
941
+ def on_list_devices_updated(self, list_devices):
942
+ '''
943
+ Slot for :attr:`interface.sig_list_devices_updated`. Repopulates the
944
+ device combo box with the new list of device descriptors.
945
+
946
+ Parameters
947
+ ----------
948
+ list_devices : list of str
949
+ Device descriptor strings to display in the combo box.
950
+ '''
951
+ self.combo_Devices.clear() #First we empty the combobox
952
+ self.combo_Devices.addItems(list_devices)
953
+
954
+ def on_mode_change(self, mode):
955
+ '''
956
+ Slot for :attr:`interface.sig_mode_changed`. Checks the appropriate
957
+ mode radio button and enables/disables mode-specific widgets.
958
+
959
+ Parameters
960
+ ----------
961
+ mode : int
962
+ New mode: ``0`` (Continuous Trigger) or ``1`` (User Controlled).
963
+
964
+ Returns
965
+ -------
966
+ bool
967
+ ``True`` if ``mode`` was a recognised value, ``False`` otherwise.
968
+ '''
969
+ if mode == 0:
970
+ self.radio_ContinuousTrigger.setChecked(True)
971
+ self.enable_widget(self.widgets_enabled_only_when_mode0)
972
+ self.disable_widget(self.widgets_enabled_only_when_mode1)
973
+ return True
974
+ if mode == 1:
975
+ self.radio_UserControlled.setChecked(True)
976
+ self.enable_widget(self.widgets_enabled_only_when_mode1)
977
+ self.disable_widget(self.widgets_enabled_only_when_mode0)
978
+ return True
979
+ return False
980
+
981
+ def on_polarity_change(self, polarity):
982
+ '''
983
+ Slot for :attr:`interface.sig_polarity_changed`. Checks the
984
+ appropriate polarity radio button.
985
+
986
+ Parameters
987
+ ----------
988
+ polarity : int
989
+ New polarity: ``0`` (Negative) or ``1`` (Positive).
990
+
991
+ Returns
992
+ -------
993
+ bool
994
+ ``True`` if ``polarity`` was a recognised value, ``False`` otherwise.
995
+ '''
996
+ if polarity == 0:
997
+ self.radio_PolarityNegative.setChecked(True)
998
+ return True
999
+ if polarity == 1:
1000
+ self.radio_PolarityPositive.setChecked(True)
1001
+ return True
1002
+ return False
1003
+
1004
+ def on_external_trigger_change(self, external_trigger):
1005
+ '''
1006
+ Slot for :attr:`interface.sig_external_trigger_changed`. Updates the
1007
+ External Trigger checkbox state.
1008
+
1009
+ Parameters
1010
+ ----------
1011
+ external_trigger : bool
1012
+ New external-trigger enable state.
1013
+ '''
1014
+ self.checkbox_ExternalTrigger.setChecked(external_trigger)
1015
+
1016
+ def on_delay_change(self, value):
1017
+ '''
1018
+ Slot for :attr:`interface.sig_delay_changed`. Updates the Delay text
1019
+ field.
1020
+
1021
+ Parameters
1022
+ ----------
1023
+ value : int
1024
+ New delay in nanoseconds.
1025
+ '''
1026
+ self.edit_Delay.setText(f"{value}")
1027
+
1028
+ def on_divider_change(self, value):
1029
+ '''
1030
+ Slot for :attr:`interface.sig_divider_changed`. Updates the Divider
1031
+ text field.
1032
+
1033
+ Parameters
1034
+ ----------
1035
+ value : int
1036
+ New divider value.
1037
+ '''
1038
+ self.edit_Divider.setText(f"{value}")
1039
+
1040
+ def on_number_of_triggers_change(self, value):
1041
+ '''
1042
+ Slot for :attr:`interface.sig_number_of_triggers_changed`. Updates the
1043
+ Num Pulses text field.
1044
+
1045
+ Parameters
1046
+ ----------
1047
+ value : int
1048
+ New number of triggers per burst.
1049
+ '''
1050
+ self.edit_NumPulses.setText(f"{value}")
1051
+
1052
+ def on_triggerduration_change(self, value):
1053
+ '''
1054
+ Slot for :attr:`interface.sig_triggerduration_changed`. Updates the
1055
+ Trigger Duration text field.
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ value : int
1060
+ New trigger duration in nanoseconds.
1061
+ '''
1062
+ self.edit_TriggerDuration.setText(f"{value}")
1063
+
1064
+ def on_close(self):
1065
+ '''
1066
+ Slot for :attr:`interface.sig_close`. Called when the interface is
1067
+ closing. Reserved for any GUI teardown that may be needed in future.
1068
+ '''
1069
+ pass
1070
+
1071
+ #######################
1072
+ ### END Event Slots ###
1073
+ #######################
1074
+
1075
+ ###################################################################################################################################################
1076
+ ### GUI Events Functions. They are triggered by direct interaction with the GUI, and they call methods of the interface (i.e. the model) object.###
1077
+ ###################################################################################################################################################
1078
+
1079
+ def click_button_refresh_list_devices(self):
1080
+ '''
1081
+ Event function for the Refresh button. Calls
1082
+ :meth:`interface.refresh_list_devices`.
1083
+ '''
1084
+ self.interface.refresh_list_devices()
1085
+
1086
+ def click_button_connect_disconnect(self):
1087
+ '''
1088
+ Event function for the Connect/Disconnect button. Calls
1089
+ :meth:`interface.connect_device` or
1090
+ :meth:`interface.disconnect_device` depending on the current
1091
+ connection state.
1092
+ '''
1093
+ if(self.interface.instrument.connected == False): # We attempt connection
1094
+ device_full_name = self.combo_Devices.currentText() # Get the device name from the combobox
1095
+ self.interface.connect_device(device_full_name)
1096
+ elif(self.interface.instrument.connected == True): # We attempt disconnection
1097
+ self.interface.disconnect_device()
1098
+
1099
+ def click_button_Fire(self):
1100
+ '''
1101
+ Event function for the Fire button. Calls :meth:`interface.fire_trigger`.
1102
+ '''
1103
+ self.interface.fire_trigger()
1104
+
1105
+ def press_enter_edit_divider(self):
1106
+ '''
1107
+ Event function for the Divider field (Return key). Passes the current
1108
+ field text to :meth:`interface.set_divider`.
1109
+ '''
1110
+ return self.interface.set_divider(self.edit_Divider.text())
1111
+
1112
+ def press_enter_edit_numpulses(self):
1113
+ '''
1114
+ Event function for the Num Pulses field (Return key). Passes the
1115
+ current field text to :meth:`interface.set_number_of_triggers`.
1116
+ '''
1117
+ return self.interface.set_number_of_triggers(self.edit_NumPulses.text())
1118
+
1119
+ def press_enter_edit_delay(self):
1120
+ '''
1121
+ Event function for the Delay field (Return key). Passes the current
1122
+ field text to :meth:`interface.set_delay`.
1123
+ '''
1124
+ return self.interface.set_delay(self.edit_Delay.text())
1125
+
1126
+ def press_enter_edit_triggerduration(self):
1127
+ '''
1128
+ Event function for the Trigger Duration field (Return key). Passes
1129
+ the current field text to :meth:`interface.set_triggerduration`.
1130
+ '''
1131
+ return self.interface.set_triggerduration(self.edit_TriggerDuration.text())
1132
+
1133
+ def click_radio_mode(self, value):
1134
+ '''
1135
+ Event function for the mode radio buttons. Reads which button is
1136
+ currently checked and calls :meth:`interface.set_mode` with the
1137
+ corresponding mode index.
1138
+
1139
+ Parameters
1140
+ ----------
1141
+ value : bool
1142
+ Checked state passed by Qt (unused; the checked button is
1143
+ determined by polling the radio buttons directly).
1144
+
1145
+ Returns
1146
+ -------
1147
+ bool
1148
+ ``True`` if a valid mode was selected, ``False`` otherwise.
1149
+ '''
1150
+ if self.radio_ContinuousTrigger.isChecked():
1151
+ self.interface.set_mode(0)
1152
+ return True
1153
+ if self.radio_UserControlled.isChecked():
1154
+ self.interface.set_mode(1)
1155
+ return True
1156
+ return False
1157
+
1158
+ def click_radio_polarity(self, value):
1159
+ '''
1160
+ Event function for the polarity radio buttons. Reads which button is
1161
+ currently checked and calls :meth:`interface.set_polarity` with the
1162
+ corresponding polarity index.
1163
+
1164
+ Parameters
1165
+ ----------
1166
+ value : bool
1167
+ Checked state passed by Qt (unused; the checked button is
1168
+ determined by polling the radio buttons directly).
1169
+
1170
+ Returns
1171
+ -------
1172
+ bool
1173
+ ``True`` if a valid polarity was selected, ``False`` otherwise.
1174
+ '''
1175
+ if self.radio_PolarityNegative.isChecked():
1176
+ self.interface.set_polarity(0)
1177
+ return True
1178
+ if self.radio_PolarityPositive.isChecked():
1179
+ self.interface.set_polarity(1)
1180
+ return True
1181
+ return False
1182
+
1183
+ def click_checkbox_external_trigger(self, value):
1184
+ '''
1185
+ Event function for the External Trigger checkbox. Passes the new
1186
+ checked state to :meth:`interface.set_external_trigger`.
1187
+
1188
+ Parameters
1189
+ ----------
1190
+ value : bool
1191
+ New checked state of the checkbox (unused; the state is read
1192
+ directly from the widget to avoid Qt signal type ambiguity).
1193
+
1194
+ Returns
1195
+ -------
1196
+ bool
1197
+ Always ``True``.
1198
+ '''
1199
+ self.interface.set_external_trigger(self.checkbox_ExternalTrigger.isChecked())
1200
+ return True
1201
+
1202
+ #################################
1203
+ ### END GUI Events Functions ####
1204
+ #################################
1205
+
1206
+ class MainWindow(Qt.QWidget):
1207
+ '''
1208
+ Top-level application window.
1209
+
1210
+ A thin :class:`QWidget` subclass that hosts the :class:`gui` layout.
1211
+ Window title is set to the package name. Close events are currently
1212
+ passed through without action (teardown is handled by
1213
+ ``app.aboutToQuit`` → :meth:`interface.close`).
1214
+ '''
1215
+ def __init__(self):
1216
+ super().__init__()
1217
+ self.setWindowTitle(__package__)
1218
+
1219
+ def closeEvent(self, event):
1220
+ '''
1221
+ Handle the window close event. Currently a no-op; interface teardown
1222
+ is handled by ``app.aboutToQuit``.
1223
+ '''
1224
+ pass
1225
+
1226
+ def main():
1227
+ '''
1228
+ Entry point for the pyTriggerSync application.
1229
+
1230
+ Parses command-line arguments, creates the Qt application, instantiates
1231
+ the :class:`interface` and :class:`gui`, and starts the event loop.
1232
+
1233
+ Command-line arguments
1234
+ ----------------------
1235
+ -s, --decrease_verbose
1236
+ Reduce logging verbosity.
1237
+ -virtual
1238
+ Use the virtual (simulated) driver instead of a real serial device.
1239
+ '''
1240
+ parser = argparse.ArgumentParser(description = "",epilog = "")
1241
+ parser.add_argument("-s", "--decrease_verbose", help="Decrease verbosity.", action="store_true")
1242
+ parser.add_argument('-virtual', help=f"Initialize the virtual driver", action="store_true")
1243
+ args = parser.parse_args()
1244
+ virtual = args.virtual
1245
+
1246
+ app = Qt.QApplication(sys.argv)
1247
+ window = MainWindow()
1248
+ Interface = interface(app=app,virtual=virtual)
1249
+ Interface.verbose = not(args.decrease_verbose)
1250
+ app.aboutToQuit.connect(Interface.close)
1251
+ view = gui(interface = Interface, parent=window) #In this case window is the parent of the gui
1252
+
1253
+ window.show()
1254
+ app.exec()# Start the event loop.
1255
+
1256
+ if __name__ == '__main__':
1257
+ main()