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
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()
|