quarchpy 2.2.15__py2.py3-none-any.whl → 2.2.16__py2.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.
- quarchpy/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/__pycache__/_version.cpython-310.pyc +0 -0
- quarchpy/__pycache__/connection.cpython-310.pyc +0 -0
- quarchpy/__pycache__/install_qps.cpython-310.pyc +0 -0
- quarchpy/_version.py +1 -1
- quarchpy/config_files/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/config_files/__pycache__/quarch_config_parser.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/StreamChannels.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/connection_QIS.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/connection_QPS.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/connection_ReST.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/connection_Serial.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/connection_TCP.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/connection_USB.cpython-310.pyc +0 -0
- quarchpy/connection_specific/__pycache__/mDNS.cpython-310.pyc +0 -0
- quarchpy/connection_specific/connection_QIS.py +1510 -1485
- quarchpy/connection_specific/jdk_jres/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/connection_specific/jdk_jres/__pycache__/fix_permissions.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/__pycache__/serialposix.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/__pycache__/serialutil.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/tools/__pycache__/list_ports.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/tools/__pycache__/list_ports_common.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/tools/__pycache__/list_ports_linux.cpython-310.pyc +0 -0
- quarchpy/connection_specific/serial/tools/__pycache__/list_ports_posix.cpython-310.pyc +0 -0
- quarchpy/debug/__pycache__/SystemTest.cpython-310.pyc +0 -0
- quarchpy/debug/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/debug/__pycache__/versionCompare.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/device.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/device_fixture_idn_info.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/device_idn_info.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/device_network_info.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/discovered_device.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/packet_processing.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/quarchArray.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/quarchPPM.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/quarchQPS.cpython-310.pyc +0 -0
- quarchpy/device/__pycache__/scanDevices.cpython-310.pyc +0 -0
- quarchpy/device/quarchPPM.py +7 -10
- quarchpy/disk_test/__pycache__/AbsDiskFinder.cpython-310.pyc +0 -0
- quarchpy/disk_test/__pycache__/DiskTargetSelection.cpython-310.pyc +0 -0
- quarchpy/disk_test/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/disk_test/__pycache__/iometerDiskFinder.cpython-310.pyc +0 -0
- quarchpy/docs/CHANGES.rst +4 -0
- quarchpy/docs/_build/doctrees/CHANGES.doctree +0 -0
- quarchpy/docs/_build/doctrees/environment.pickle +0 -0
- quarchpy/docs/_build/doctrees/source/changelog.doctree +0 -0
- quarchpy/docs/_build/doctrees/source/quarchpy.connection_specific.doctree +0 -0
- quarchpy/docs/_build/doctrees/source/quarchpy.device.doctree +0 -0
- quarchpy/docs/_build/doctrees/source/quarchpy.qis.doctree +0 -0
- quarchpy/docs/_build/html/CHANGES.html +163 -157
- quarchpy/docs/_build/html/_sources/CHANGES.rst.txt +4 -0
- quarchpy/docs/_build/html/genindex.html +7 -3
- quarchpy/docs/_build/html/index.html +80 -79
- quarchpy/docs/_build/html/objects.inv +0 -0
- quarchpy/docs/_build/html/searchindex.js +1 -1
- quarchpy/docs/_build/html/source/changelog.html +243 -236
- quarchpy/docs/_build/html/source/quarchpy.connection_specific.html +12 -3
- quarchpy/docs/_build/html/source/quarchpy.device.html +8 -16
- quarchpy/docs/_build/html/source/quarchpy.html +2 -0
- quarchpy/docs/_build/html/source/quarchpy.qis.html +12 -3
- quarchpy/fio/__pycache__/FIO_interface.cpython-310.pyc +0 -0
- quarchpy/fio/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/install_qps.py +99 -62
- quarchpy/iometer/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/iometer/__pycache__/gen_iometer_template.cpython-310.pyc +0 -0
- quarchpy/iometer/__pycache__/iometerFuncs.cpython-310.pyc +0 -0
- quarchpy/qis/__pycache__/StreamHeaderInfo.cpython-310.pyc +0 -0
- quarchpy/qis/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/qis/__pycache__/qisFuncs.cpython-310.pyc +0 -0
- quarchpy/qps/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/qps/__pycache__/qpsFuncs.cpython-310.pyc +0 -0
- quarchpy/qps/qpsFuncs.py +4 -0
- quarchpy/user_interface/__pycache__/__init__.cpython-310.pyc +0 -0
- quarchpy/user_interface/__pycache__/user_interface.cpython-310.pyc +0 -0
- quarchpy/utilities/__pycache__/TestCenter.cpython-310.pyc +0 -0
- quarchpy/utilities/__pycache__/TimeValue.cpython-310.pyc +0 -0
- quarchpy/utilities/__pycache__/__init__.cpython-310.pyc +0 -0
- {quarchpy-2.2.15.dist-info → quarchpy-2.2.16.dist-info}/METADATA +15 -2
- {quarchpy-2.2.15.dist-info → quarchpy-2.2.16.dist-info}/RECORD +85 -85
- {quarchpy-2.2.15.dist-info → quarchpy-2.2.16.dist-info}/WHEEL +1 -1
- {quarchpy-2.2.15.dist-info → quarchpy-2.2.16.dist-info}/top_level.txt +0 -0
|
@@ -1,1486 +1,1511 @@
|
|
|
1
|
-
import gzip
|
|
2
|
-
import re
|
|
3
|
-
import socket
|
|
4
|
-
import threading
|
|
5
|
-
import xml.etree.ElementTree as ET
|
|
6
|
-
from io import StringIO
|
|
7
|
-
|
|
8
|
-
import select
|
|
9
|
-
from connection_specific.StreamChannels import StreamGroups
|
|
10
|
-
|
|
11
|
-
from quarchpy.user_interface import *
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# QisInterface provides a way of connecting to a Quarch backend running at the specified ip address and port, defaults to localhost and 9722
|
|
15
|
-
class QisInterface:
|
|
16
|
-
def __init__(self, host='127.0.0.1', port=9722, connectionMessage=True):
|
|
17
|
-
self.host = host
|
|
18
|
-
self.port = port
|
|
19
|
-
self.maxRxBytes = 4096
|
|
20
|
-
self.sock = None
|
|
21
|
-
self.StreamRunSentSemaphore = threading.Semaphore()
|
|
22
|
-
self.sockSemaphore = threading.Semaphore()
|
|
23
|
-
self.stopFlagList = []
|
|
24
|
-
self.listSemaphore = threading.Semaphore()
|
|
25
|
-
self.deviceList = []
|
|
26
|
-
self.deviceDict = {}
|
|
27
|
-
self.dictSemaphore = threading.Semaphore()
|
|
28
|
-
self.connect(connection_message = connectionMessage)
|
|
29
|
-
self.stripesEvent = threading.Event()
|
|
30
|
-
|
|
31
|
-
self.qps_stream_header = None
|
|
32
|
-
self.qps_record_dir_path = None
|
|
33
|
-
self.qps_record_start_time = None
|
|
34
|
-
self.qps_stream_folder_name = None
|
|
35
|
-
|
|
36
|
-
self.module_xml_header = None
|
|
37
|
-
self.streamGroups = None
|
|
38
|
-
self.has_digitals = False
|
|
39
|
-
self.is_multirate = False
|
|
40
|
-
|
|
41
|
-
self.streamSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
42
|
-
self.streamSock.settimeout(5)
|
|
43
|
-
self.streamSock.connect((self.host, self.port))
|
|
44
|
-
self.pythonVersion = sys.version[0]
|
|
45
|
-
self.cursor = '>'
|
|
46
|
-
#clear packets
|
|
47
|
-
welcome_string = self.streamSock.recv(self.maxRxBytes).rstrip()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def connect(self, connection_message: bool = True) -> str:
|
|
51
|
-
"""
|
|
52
|
-
Opens the connection to the QIS backend
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
connection_message:
|
|
56
|
-
Defaults to True. If set to False, suppresses the warning message about an
|
|
57
|
-
instance already running on the specified port. This can be useful when
|
|
58
|
-
using `isQisRunning()` from `qisFuncs`.
|
|
59
|
-
|
|
60
|
-
Raises:
|
|
61
|
-
Exception:
|
|
62
|
-
If the connection fails or the welcome string is not received an exception is raised
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
The welcome string received from the backend server upon a successful
|
|
66
|
-
connection. This will confirm the QIS version but is generally not used other than
|
|
67
|
-
for debugging
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
self.device_dict_setup('QIS')
|
|
72
|
-
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
73
|
-
self.sock.settimeout(5)
|
|
74
|
-
self.sock.connect((self.host, self.port))
|
|
75
|
-
|
|
76
|
-
#clear packets
|
|
77
|
-
try:
|
|
78
|
-
welcome_string = self.sock.recv(self.maxRxBytes).rstrip()
|
|
79
|
-
welcome_string = 'Connected@' + str(self.host) + ':' + str(self.port) + ' ' + '\n ' + str(welcome_string)
|
|
80
|
-
self.deviceDict['QIS'][0:3] = [False, 'Connected', welcome_string]
|
|
81
|
-
return welcome_string
|
|
82
|
-
except Exception as e:
|
|
83
|
-
logging.error('No welcome received. Unable to connect to Quarch backend on specified host and port (' + self.host + ':' + str(self.port) + ')')
|
|
84
|
-
logging.error('Is backend running and host accessible?')
|
|
85
|
-
self.deviceDict['QIS'][0:3] = [True, 'Disconnected', 'Unable to connect to QIS']
|
|
86
|
-
raise e
|
|
87
|
-
except Exception as e:
|
|
88
|
-
self.device_dict_setup('QIS')
|
|
89
|
-
if connection_message:
|
|
90
|
-
logging.error('Unable to connect to Quarch backend on specified host and port (' + self.host + ':' + str(self.port) + ').')
|
|
91
|
-
logging.error('Is backend running and host accessible?')
|
|
92
|
-
self.deviceDict['QIS'][0:3] = [True, 'Disconnected', 'Unable to connect to QIS']
|
|
93
|
-
raise e
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def disconnect(self) -> str:
|
|
97
|
-
"""
|
|
98
|
-
Disconnects the current connection to the QIS backend.
|
|
99
|
-
|
|
100
|
-
This method attempts to gracefully disconnect from the backend server and updates
|
|
101
|
-
the connection state in the device dictionary. If an error occurs during the
|
|
102
|
-
disconnection process, the state is updated to indicate the failure, and the
|
|
103
|
-
exception is re-raised
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
str: A message indicating that the disconnection process has started.
|
|
107
|
-
|
|
108
|
-
Raises:
|
|
109
|
-
Exception: Propagates any exception that occurs during the disconnection process.
|
|
110
|
-
"""
|
|
111
|
-
res = 'Disconnecting from backend'
|
|
112
|
-
try:
|
|
113
|
-
self.sock.shutdown(socket.SHUT_RDWR)
|
|
114
|
-
self.sock.close()
|
|
115
|
-
self.deviceDict['QIS'][0:3] = [False, "Disconnected", 'Successfully disconnected from QIS']
|
|
116
|
-
except Exception as e:
|
|
117
|
-
exc_type, exc_obj, exc_tb = sys.exc_info()
|
|
118
|
-
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
|
|
119
|
-
message = 'Unable to end connection. ' + self.host + ':' + str(self.port) + ' \r\n' + str(exc_type) + ' ' + str(fname) + ' ' + str(exc_tb.tb_lineno)
|
|
120
|
-
self.deviceDict['QIS'][0:3] = [True, "Connected", message]
|
|
121
|
-
raise e
|
|
122
|
-
return res
|
|
123
|
-
|
|
124
|
-
def close_connection(self, sock=None, con_string: str=None) -> str:
|
|
125
|
-
"""
|
|
126
|
-
Orders QIS to release a given device (or all devices if no connection string is specified)
|
|
127
|
-
This is more important for TCP connected devices as the socket is held open until
|
|
128
|
-
specifically released.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
sock:
|
|
132
|
-
The socket object to close the connection to. Defaults to the existing socket.
|
|
133
|
-
con_string:
|
|
134
|
-
Specify the device ID to close, otherwise all devices will be closed
|
|
135
|
-
|
|
136
|
-
Raises:
|
|
137
|
-
ConnectionResetError: Raised if the socket connection has already been reset.
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
The response received after sending the close command. On success, this will be: 'OK'
|
|
141
|
-
|
|
142
|
-
"""
|
|
143
|
-
if sock is None:
|
|
144
|
-
sock = self.sock
|
|
145
|
-
if con_string is None:
|
|
146
|
-
cmd = "close"
|
|
147
|
-
else:
|
|
148
|
-
cmd = con_string + " close"
|
|
149
|
-
try:
|
|
150
|
-
response = self.sendAndReceiveText(sock, cmd)
|
|
151
|
-
return response
|
|
152
|
-
except ConnectionResetError:
|
|
153
|
-
logging.error('Unable to close connection to device(s), QIS may be already closed')
|
|
154
|
-
return "FAIL: Unable to close connection to device(s), QIS may be already closed"
|
|
155
|
-
|
|
156
|
-
def start_stream(self, module: str, file_name: str, max_file_size: int, release_on_data: bool, separator: str,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
The
|
|
165
|
-
|
|
166
|
-
The
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
The
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
A
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
self.
|
|
187
|
-
self.
|
|
188
|
-
self.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
file will
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
#
|
|
402
|
-
if
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if file_opened_by_function and f is not None:
|
|
507
|
-
try:
|
|
508
|
-
if not f.closed:
|
|
509
|
-
f.close()
|
|
510
|
-
except Exception as e_close:
|
|
511
|
-
logging.error(f"Error closing file {file_name}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
sock
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
favourite =
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
for
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
found_devices =
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
#
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
self.
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
"""
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
def
|
|
1269
|
-
"""
|
|
1270
|
-
|
|
1271
|
-
Args:
|
|
1272
|
-
sock:
|
|
1273
|
-
Socket to communicate over
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
"""
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
"""
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
"""
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
"""
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
"""
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
"""
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
"""
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1
|
+
import gzip
|
|
2
|
+
import re
|
|
3
|
+
import socket
|
|
4
|
+
import threading
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from io import StringIO
|
|
7
|
+
|
|
8
|
+
import select
|
|
9
|
+
from connection_specific.StreamChannels import StreamGroups
|
|
10
|
+
|
|
11
|
+
from quarchpy.user_interface import *
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# QisInterface provides a way of connecting to a Quarch backend running at the specified ip address and port, defaults to localhost and 9722
|
|
15
|
+
class QisInterface:
|
|
16
|
+
def __init__(self, host='127.0.0.1', port=9722, connectionMessage=True):
|
|
17
|
+
self.host = host
|
|
18
|
+
self.port = port
|
|
19
|
+
self.maxRxBytes = 4096
|
|
20
|
+
self.sock = None
|
|
21
|
+
self.StreamRunSentSemaphore = threading.Semaphore()
|
|
22
|
+
self.sockSemaphore = threading.Semaphore()
|
|
23
|
+
self.stopFlagList = []
|
|
24
|
+
self.listSemaphore = threading.Semaphore()
|
|
25
|
+
self.deviceList = []
|
|
26
|
+
self.deviceDict = {}
|
|
27
|
+
self.dictSemaphore = threading.Semaphore()
|
|
28
|
+
self.connect(connection_message = connectionMessage)
|
|
29
|
+
self.stripesEvent = threading.Event()
|
|
30
|
+
|
|
31
|
+
self.qps_stream_header = None
|
|
32
|
+
self.qps_record_dir_path = None
|
|
33
|
+
self.qps_record_start_time = None
|
|
34
|
+
self.qps_stream_folder_name = None
|
|
35
|
+
|
|
36
|
+
self.module_xml_header = None
|
|
37
|
+
self.streamGroups = None
|
|
38
|
+
self.has_digitals = False
|
|
39
|
+
self.is_multirate = False
|
|
40
|
+
|
|
41
|
+
self.streamSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
42
|
+
self.streamSock.settimeout(5)
|
|
43
|
+
self.streamSock.connect((self.host, self.port))
|
|
44
|
+
self.pythonVersion = sys.version[0]
|
|
45
|
+
self.cursor = '>'
|
|
46
|
+
#clear packets
|
|
47
|
+
welcome_string = self.streamSock.recv(self.maxRxBytes).rstrip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def connect(self, connection_message: bool = True) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Opens the connection to the QIS backend
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
connection_message:
|
|
56
|
+
Defaults to True. If set to False, suppresses the warning message about an
|
|
57
|
+
instance already running on the specified port. This can be useful when
|
|
58
|
+
using `isQisRunning()` from `qisFuncs`.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
Exception:
|
|
62
|
+
If the connection fails or the welcome string is not received an exception is raised
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The welcome string received from the backend server upon a successful
|
|
66
|
+
connection. This will confirm the QIS version but is generally not used other than
|
|
67
|
+
for debugging
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
self.device_dict_setup('QIS')
|
|
72
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
73
|
+
self.sock.settimeout(5)
|
|
74
|
+
self.sock.connect((self.host, self.port))
|
|
75
|
+
|
|
76
|
+
#clear packets
|
|
77
|
+
try:
|
|
78
|
+
welcome_string = self.sock.recv(self.maxRxBytes).rstrip()
|
|
79
|
+
welcome_string = 'Connected@' + str(self.host) + ':' + str(self.port) + ' ' + '\n ' + str(welcome_string)
|
|
80
|
+
self.deviceDict['QIS'][0:3] = [False, 'Connected', welcome_string]
|
|
81
|
+
return welcome_string
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logging.error('No welcome received. Unable to connect to Quarch backend on specified host and port (' + self.host + ':' + str(self.port) + ')')
|
|
84
|
+
logging.error('Is backend running and host accessible?')
|
|
85
|
+
self.deviceDict['QIS'][0:3] = [True, 'Disconnected', 'Unable to connect to QIS']
|
|
86
|
+
raise e
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self.device_dict_setup('QIS')
|
|
89
|
+
if connection_message:
|
|
90
|
+
logging.error('Unable to connect to Quarch backend on specified host and port (' + self.host + ':' + str(self.port) + ').')
|
|
91
|
+
logging.error('Is backend running and host accessible?')
|
|
92
|
+
self.deviceDict['QIS'][0:3] = [True, 'Disconnected', 'Unable to connect to QIS']
|
|
93
|
+
raise e
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def disconnect(self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Disconnects the current connection to the QIS backend.
|
|
99
|
+
|
|
100
|
+
This method attempts to gracefully disconnect from the backend server and updates
|
|
101
|
+
the connection state in the device dictionary. If an error occurs during the
|
|
102
|
+
disconnection process, the state is updated to indicate the failure, and the
|
|
103
|
+
exception is re-raised
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: A message indicating that the disconnection process has started.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
Exception: Propagates any exception that occurs during the disconnection process.
|
|
110
|
+
"""
|
|
111
|
+
res = 'Disconnecting from backend'
|
|
112
|
+
try:
|
|
113
|
+
self.sock.shutdown(socket.SHUT_RDWR)
|
|
114
|
+
self.sock.close()
|
|
115
|
+
self.deviceDict['QIS'][0:3] = [False, "Disconnected", 'Successfully disconnected from QIS']
|
|
116
|
+
except Exception as e:
|
|
117
|
+
exc_type, exc_obj, exc_tb = sys.exc_info()
|
|
118
|
+
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
|
|
119
|
+
message = 'Unable to end connection. ' + self.host + ':' + str(self.port) + ' \r\n' + str(exc_type) + ' ' + str(fname) + ' ' + str(exc_tb.tb_lineno)
|
|
120
|
+
self.deviceDict['QIS'][0:3] = [True, "Connected", message]
|
|
121
|
+
raise e
|
|
122
|
+
return res
|
|
123
|
+
|
|
124
|
+
def close_connection(self, sock=None, con_string: str=None) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Orders QIS to release a given device (or all devices if no connection string is specified)
|
|
127
|
+
This is more important for TCP connected devices as the socket is held open until
|
|
128
|
+
specifically released.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
sock:
|
|
132
|
+
The socket object to close the connection to. Defaults to the existing socket.
|
|
133
|
+
con_string:
|
|
134
|
+
Specify the device ID to close, otherwise all devices will be closed
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ConnectionResetError: Raised if the socket connection has already been reset.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The response received after sending the close command. On success, this will be: 'OK'
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
if sock is None:
|
|
144
|
+
sock = self.sock
|
|
145
|
+
if con_string is None:
|
|
146
|
+
cmd = "close"
|
|
147
|
+
else:
|
|
148
|
+
cmd = con_string + " close"
|
|
149
|
+
try:
|
|
150
|
+
response = self.sendAndReceiveText(sock, cmd)
|
|
151
|
+
return response
|
|
152
|
+
except ConnectionResetError:
|
|
153
|
+
logging.error('Unable to close connection to device(s), QIS may be already closed')
|
|
154
|
+
return "FAIL: Unable to close connection to device(s), QIS may be already closed"
|
|
155
|
+
|
|
156
|
+
def start_stream(self, module: str, file_name: str, max_file_size: int, release_on_data: bool, separator: str,
|
|
157
|
+
stream_duration: float=None, in_memory_data: StringIO=None, output_file_handle=None, use_gzip: bool=None,
|
|
158
|
+
gzip_compress_level: int=9):
|
|
159
|
+
"""
|
|
160
|
+
Begins a stream process which will record data from the module to a CSV file or in memory CSV equivalent
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
module:
|
|
164
|
+
The ID of the module for which the stream is being initiated.
|
|
165
|
+
file_name:
|
|
166
|
+
The target file path+name for storing the streamed data in CSV form.
|
|
167
|
+
max_file_size:
|
|
168
|
+
The maximum size in megabytes allowed for the output file.
|
|
169
|
+
release_on_data:
|
|
170
|
+
If set, blocks further streams until this one has started
|
|
171
|
+
separator:
|
|
172
|
+
The value separator used to format the streamed CSV data.
|
|
173
|
+
stream_duration:
|
|
174
|
+
The duration (in seconds) for which the streaming process should run. Unlimited if None.
|
|
175
|
+
in_memory_data:
|
|
176
|
+
An in memory CSV StringIO as an alternate to file output
|
|
177
|
+
output_file_handle:
|
|
178
|
+
A file handle to an output file where the stream data is written as an alternate to a file name
|
|
179
|
+
use_gzip:
|
|
180
|
+
A flag indicating whether the output file should be compressed using gzip to reduce disk use
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
None
|
|
184
|
+
"""
|
|
185
|
+
self.StreamRunSentSemaphore.acquire()
|
|
186
|
+
self.device_dict_setup('QIS')
|
|
187
|
+
i = self.device_control_index(module)
|
|
188
|
+
self.stopFlagList[i] = True
|
|
189
|
+
self.stripesEvent.set()
|
|
190
|
+
self.module_xml_header = None
|
|
191
|
+
|
|
192
|
+
# Create the worker thread to handle stream processing
|
|
193
|
+
t1 = threading.Thread(target=self.start_stream_thread, name=module,
|
|
194
|
+
args=(module, file_name, max_file_size, release_on_data, separator, stream_duration,
|
|
195
|
+
in_memory_data, output_file_handle, use_gzip, gzip_compress_level))
|
|
196
|
+
# Start the thread
|
|
197
|
+
t1.start()
|
|
198
|
+
|
|
199
|
+
while self.stripesEvent.is_set():
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
def stop_stream(self, module, blocking:bool = True):
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
module:
|
|
207
|
+
The quarchPPM module instance for which the streaming process is to be stopped.
|
|
208
|
+
blocking:
|
|
209
|
+
If set to True, the function will block and wait until the module has
|
|
210
|
+
completely stopped streaming. Defaults to True.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
None
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
module_name=module.ConString
|
|
218
|
+
i = self.device_control_index(module_name)
|
|
219
|
+
self.stopFlagList[i] = False
|
|
220
|
+
|
|
221
|
+
# Wait until the stream thread is finished before returning to the user.
|
|
222
|
+
# This means this function will block until the QIS buffer is emptied by the second while
|
|
223
|
+
# loop in startStreamThread. This may take some time, especially at low averaging,
|
|
224
|
+
# but should guarantee the data won't be lost and QIS buffer is emptied.
|
|
225
|
+
if blocking:
|
|
226
|
+
running = True
|
|
227
|
+
while running:
|
|
228
|
+
thread_name_list = []
|
|
229
|
+
for t1 in threading.enumerate():
|
|
230
|
+
thread_name_list.append(t1.name)
|
|
231
|
+
module_streaming= module.sendCommand("rec stream?").lower() #checking if module thinks its streaming.
|
|
232
|
+
module_streaming2= module.sendCommand("stream?").lower() #checking if the module has told qis it has stopped streaming.
|
|
233
|
+
|
|
234
|
+
if module_name in thread_name_list or "running" in module_streaming or "running" in module_streaming2:
|
|
235
|
+
time.sleep(0.1)
|
|
236
|
+
else:
|
|
237
|
+
running = False
|
|
238
|
+
|
|
239
|
+
def start_stream_thread(self, module: str, file_name: str, max_file_size: float, release_on_data: bool, separator: str,
|
|
240
|
+
stream_duration: int=None, in_memory_data=None, output_file_handle=None, use_gzip: bool=False,
|
|
241
|
+
gzip_compress_level: int=9):
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
module:
|
|
246
|
+
The name of the module from which data is to be streamed.
|
|
247
|
+
file_name:
|
|
248
|
+
The path to the file where streamed data will be written. Mandatory if neither an in-memory
|
|
249
|
+
buffer (in_memory_data) nor an external file handle (output_file_handle) is provided.
|
|
250
|
+
max_file_size:
|
|
251
|
+
The maximum permissible file size in MB. After reaching this limit, streaming to the current
|
|
252
|
+
file will stop
|
|
253
|
+
release_on_data:
|
|
254
|
+
True to prevent the stream lock from releasing until data has been received
|
|
255
|
+
separator:
|
|
256
|
+
Custom separator used to CSV data
|
|
257
|
+
stream_duration:
|
|
258
|
+
Duration of streaming in seconds, relative to the sampling period. Defaults to streaming
|
|
259
|
+
indefinitely.
|
|
260
|
+
in_memory_data:
|
|
261
|
+
An in-memory buffer of type StringIO to hold streamed data. If set, data is written here
|
|
262
|
+
instead of a file.
|
|
263
|
+
output_file_handle:
|
|
264
|
+
A pre-opened file handle where data will be written. If set, file_name is ignored.
|
|
265
|
+
use_gzip:
|
|
266
|
+
If True, writes streamed data to a gzip-compressed file.
|
|
267
|
+
gzip_compress_level:
|
|
268
|
+
(Default: 9) The compression level (0-9) to use for gzip.
|
|
269
|
+
1 is fastest with low compression. 9 is slowest with high compression.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
TypeError
|
|
273
|
+
If in_memory_data is passed but is not of type StringIO.
|
|
274
|
+
ValueError
|
|
275
|
+
If file_name is not provided and neither in_memory_data nor output_file_handle is given.
|
|
276
|
+
Also raised for invalid or undecodable sampling periods.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
None
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
f = None
|
|
283
|
+
max_mb_val = 0
|
|
284
|
+
file_opened_by_function = False # True if this function opens the file
|
|
285
|
+
is_in_memory_stream = False # True if using inMemoryData (StringIO)
|
|
286
|
+
|
|
287
|
+
# Output priority: 1. output_file_handle, 2. inMemoryData, 3. A new a file
|
|
288
|
+
if output_file_handle is not None:
|
|
289
|
+
f = output_file_handle
|
|
290
|
+
# Caller is responsible for the handle's mode (e.g., text/binary) and type.
|
|
291
|
+
elif in_memory_data is not None:
|
|
292
|
+
if not isinstance(in_memory_data, StringIO):
|
|
293
|
+
raise TypeError("Error! The parameter 'inMemoryData' must be of type StringIO.")
|
|
294
|
+
f = in_memory_data
|
|
295
|
+
is_in_memory_stream = True
|
|
296
|
+
else:
|
|
297
|
+
# No external handle or in-memory buffer, so open a file.
|
|
298
|
+
if not file_name: # fileName is mandatory if we are to open a file.
|
|
299
|
+
raise ValueError("fie_name must be provided if output_file_handle and in_memory_data are None.")
|
|
300
|
+
file_opened_by_function = True
|
|
301
|
+
if use_gzip:
|
|
302
|
+
# Open in text mode ('wt'). Encoding 'utf-8' is a good default.
|
|
303
|
+
# gzip.open in text mode handles newline conversions.
|
|
304
|
+
f = gzip.open(f'{file_name}.gz', 'wt', encoding='utf-8', compresslevel=gzip_compress_level)
|
|
305
|
+
else:
|
|
306
|
+
# Open in text mode ('w').
|
|
307
|
+
# newline='' ensures that '\n' is written as '\n' on all platforms.
|
|
308
|
+
f = open(file_name, 'w', encoding='utf-8', newline='')
|
|
309
|
+
|
|
310
|
+
# Check for a valid max file size limit
|
|
311
|
+
if max_file_size is not None:
|
|
312
|
+
try:
|
|
313
|
+
max_mb_val = int(max_file_size)
|
|
314
|
+
except (ValueError, TypeError):
|
|
315
|
+
logging.warning(f"Invalid max_file_size parameter: {max_file_size}. No limit will be applied")
|
|
316
|
+
max_file_size = None
|
|
317
|
+
|
|
318
|
+
# Send stream command so the module starts streaming data into the backends buffer
|
|
319
|
+
stream_res = self.send_command('rec stream', device=module, qis_socket=self.streamSock)
|
|
320
|
+
# Check the stream started
|
|
321
|
+
if 'OK' in stream_res:
|
|
322
|
+
if not release_on_data:
|
|
323
|
+
self.StreamRunSentSemaphore.release()
|
|
324
|
+
self.stripesEvent.clear()
|
|
325
|
+
self.deviceDict[module][0:3] = [False, 'Running', 'Stream Running']
|
|
326
|
+
else:
|
|
327
|
+
self.StreamRunSentSemaphore.release()
|
|
328
|
+
self.stripesEvent.clear()
|
|
329
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', module + " couldn't start because " + stream_res]
|
|
330
|
+
if file_opened_by_function and f:
|
|
331
|
+
try:
|
|
332
|
+
f.close()
|
|
333
|
+
except Exception as e_close:
|
|
334
|
+
logging.error(f"Error closing file {file_name} on stream start failure: {e_close}")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Poll for the stream header to become available. This is needed to configure the output file
|
|
338
|
+
base_sample_period = self.stream_header_average(device=module, sock=self.streamSock)
|
|
339
|
+
count = 0
|
|
340
|
+
max_tries = 10
|
|
341
|
+
while 'Header Not Available' in base_sample_period:
|
|
342
|
+
base_sample_period = self.stream_header_average(device=module, sock=self.streamSock)
|
|
343
|
+
time.sleep(0.1)
|
|
344
|
+
count += 1
|
|
345
|
+
if count > max_tries:
|
|
346
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', 'Header not available']
|
|
347
|
+
if file_opened_by_function and f:
|
|
348
|
+
try:
|
|
349
|
+
f.close()
|
|
350
|
+
except Exception as e_close:
|
|
351
|
+
logging.error(f"Error closing file {file_name} on header failure: {e_close}")
|
|
352
|
+
return # Changed from exit() for cleaner thread termination
|
|
353
|
+
|
|
354
|
+
# Format the header and write it to the output file
|
|
355
|
+
format_header = self.stream_header_format(device=module, sock=self.streamSock)
|
|
356
|
+
format_header = format_header.replace(", ", separator)
|
|
357
|
+
f.write(format_header + '\n')
|
|
358
|
+
|
|
359
|
+
# Initialize stream variables
|
|
360
|
+
max_file_exceeded = False
|
|
361
|
+
open_attempts = 0
|
|
362
|
+
leftover = 0
|
|
363
|
+
remaining_stripes = []
|
|
364
|
+
stream_overrun = False
|
|
365
|
+
stream_complete = False
|
|
366
|
+
stream_status_str = ""
|
|
367
|
+
|
|
368
|
+
# Calculate and verify stripe rate information
|
|
369
|
+
if 'ns' in base_sample_period.lower():
|
|
370
|
+
base_sample_unit_exponent = -9
|
|
371
|
+
elif 'us' in base_sample_period.lower():
|
|
372
|
+
base_sample_unit_exponent = -6
|
|
373
|
+
elif 'ms' in base_sample_period.lower():
|
|
374
|
+
base_sample_unit_exponent = -3
|
|
375
|
+
elif 'S' in base_sample_period.lower(): # Original was 'S', assuming it means 's'
|
|
376
|
+
base_sample_unit_exponent = 0
|
|
377
|
+
else:
|
|
378
|
+
# Clean up and raise error if baseSamplePeriod is undecodable
|
|
379
|
+
if file_opened_by_function and f:
|
|
380
|
+
try:
|
|
381
|
+
f.close()
|
|
382
|
+
except Exception as e_close:
|
|
383
|
+
logging.error(f"Error closing file {file_name} due to ValueError: {e_close}")
|
|
384
|
+
raise ValueError(f"couldn't decode samplePeriod: {base_sample_period}")
|
|
385
|
+
|
|
386
|
+
base_sample_period_period_s = int(re.search(r'^\d*\.?\d*', base_sample_period).group()) * (10 ** base_sample_unit_exponent)
|
|
387
|
+
|
|
388
|
+
# Now we loop to process the stripes of data as they are available
|
|
389
|
+
is_run = True
|
|
390
|
+
while is_run:
|
|
391
|
+
try:
|
|
392
|
+
# Check for exit flags. These can be from user request (stopFlagList) or from the stream
|
|
393
|
+
# process ending
|
|
394
|
+
i = self.device_control_index(module)
|
|
395
|
+
while self.stopFlagList[i] and (not stream_overrun) and (not stream_complete):
|
|
396
|
+
|
|
397
|
+
# Read a block of stripes from QIS
|
|
398
|
+
stream_status_str, new_stripes = self.stream_get_stripes_text(self.streamSock, module)
|
|
399
|
+
|
|
400
|
+
# Overrun is a termination event where the stream stopped earlier than desired and must
|
|
401
|
+
# be flagged to the user
|
|
402
|
+
if "overrun" in stream_status_str:
|
|
403
|
+
stream_overrun = True
|
|
404
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', 'Device buffer overrun']
|
|
405
|
+
if "eof" in stream_status_str:
|
|
406
|
+
stream_complete = True
|
|
407
|
+
|
|
408
|
+
# Continue here if there are stripes to process
|
|
409
|
+
if len(new_stripes) > 0:
|
|
410
|
+
# switch in the correct value seperator
|
|
411
|
+
new_stripes = new_stripes.replace(' ', separator)
|
|
412
|
+
|
|
413
|
+
# Track the total size of the file here if needed
|
|
414
|
+
if max_file_size is not None:
|
|
415
|
+
current_file_mb = 0.0
|
|
416
|
+
if is_in_memory_stream:
|
|
417
|
+
current_file_mb = f.tell() / 1048576.0
|
|
418
|
+
elif file_name:
|
|
419
|
+
try:
|
|
420
|
+
# os.stat reflects the size on disk. For buffered writes (incl. gzip),
|
|
421
|
+
# this might not be the exact current unwritten buffer size + disk size
|
|
422
|
+
# without a flush, but it's an decent estimate.
|
|
423
|
+
stat_info = os.stat(file_name)
|
|
424
|
+
current_file_mb = stat_info.st_size / 1048576.0
|
|
425
|
+
except FileNotFoundError:
|
|
426
|
+
current_file_mb = 0.0 # File might not exist yet or fileName is not locatable
|
|
427
|
+
except Exception as e_stat:
|
|
428
|
+
logging.warning(f"Could not get file size for {file_name}: {e_stat}")
|
|
429
|
+
current_file_mb = 0.0 # Default to small size on error
|
|
430
|
+
else:
|
|
431
|
+
# output_file_handle was given, but fileName was None. Cannot check disk size.
|
|
432
|
+
# Assume it's okay or managed by the caller. fileMaxMB check effectively bypassed.
|
|
433
|
+
current_file_mb = 0.0
|
|
434
|
+
|
|
435
|
+
# Flag the limit has been exceeded
|
|
436
|
+
if current_file_mb > max_mb_val:
|
|
437
|
+
max_file_exceeded = True
|
|
438
|
+
max_file_status = self.stream_buffer_status(device=module, sock=self.streamSock)
|
|
439
|
+
f.write('Warning: Max file size exceeded before end of stream.\n')
|
|
440
|
+
f.write('Unrecorded stripes in buffer when file full: ' + max_file_status + '.\n')
|
|
441
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', 'User defined max filesize reached']
|
|
442
|
+
break # Exit stream processing loop
|
|
443
|
+
|
|
444
|
+
# Release the stream semaphore now we have data
|
|
445
|
+
if release_on_data:
|
|
446
|
+
self.StreamRunSentSemaphore.release()
|
|
447
|
+
self.stripesEvent.clear()
|
|
448
|
+
release_on_data = False
|
|
449
|
+
|
|
450
|
+
# If a duration has been set, track it based on the time of the last stripe
|
|
451
|
+
if stream_duration is not None:
|
|
452
|
+
last_line = new_stripes.splitlines()[-1]
|
|
453
|
+
last_time = last_line.split(separator)[0]
|
|
454
|
+
|
|
455
|
+
# Write all the stripes if we can
|
|
456
|
+
if int(last_time) < int(stream_duration / (10 ** base_sample_unit_exponent)):
|
|
457
|
+
f.write(new_stripes)
|
|
458
|
+
# Otherwise only write stripes within the duration limit
|
|
459
|
+
else:
|
|
460
|
+
for this_line in new_stripes.splitlines():
|
|
461
|
+
this_time_str = this_line.split(separator)[0]
|
|
462
|
+
if int(this_time_str) < int(stream_duration / (10 ** base_sample_unit_exponent)):
|
|
463
|
+
f.write(this_line + '\r\n') # Put the CR back on the end
|
|
464
|
+
else:
|
|
465
|
+
stream_complete = True
|
|
466
|
+
break
|
|
467
|
+
# Default to writing all stripes
|
|
468
|
+
else:
|
|
469
|
+
f.write(new_stripes)
|
|
470
|
+
# If we have no data
|
|
471
|
+
else:
|
|
472
|
+
if stream_overrun:
|
|
473
|
+
break # Exit stream processing loop
|
|
474
|
+
elif "stopped" in stream_status_str:
|
|
475
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', 'User halted stream']
|
|
476
|
+
break # Exit stream processing loop
|
|
477
|
+
# End of stream data processing loop
|
|
478
|
+
|
|
479
|
+
# Ensure the stream is fully stopped, though standard exit cases should have ended it already
|
|
480
|
+
self.send_command('rec stop', device=module, qis_socket=self.streamSock)
|
|
481
|
+
stream_state = self.send_command('stream?', device=module, qis_socket=self.streamSock)
|
|
482
|
+
while "stopped" not in stream_state.lower():
|
|
483
|
+
logging.debug("waiting for stream? to return stopped")
|
|
484
|
+
time.sleep(0.1)
|
|
485
|
+
stream_state = self.send_command('stream?', device=module, qis_socket=self.streamSock)
|
|
486
|
+
|
|
487
|
+
if stream_overrun:
|
|
488
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', 'Device buffer overrun - QIS buffer empty']
|
|
489
|
+
elif not max_file_exceeded:
|
|
490
|
+
self.deviceDict[module][0:3] = [False, 'Stopped', 'Stream stopped']
|
|
491
|
+
|
|
492
|
+
is_run = False # Exit main while loop
|
|
493
|
+
except IOError as err:
|
|
494
|
+
logging.error(f"IOError in startStreamThread for module {module}: {err}")
|
|
495
|
+
|
|
496
|
+
# Check if this error might be related to GZIP performance
|
|
497
|
+
if use_gzip:
|
|
498
|
+
logging.warning(f"An IOError occurred while writing GZIP data for {module}.")
|
|
499
|
+
logging.warning("This can happen at high data rates if compression is too slow for the I/O buffer.")
|
|
500
|
+
logging.warning(f"Current compression level is {gzip_compress_level}. "
|
|
501
|
+
f"Consider re-running with a lower 'gzip_compress_level' (e.g., 6 or 1) "
|
|
502
|
+
"if this error persists.")
|
|
503
|
+
|
|
504
|
+
# File might have been closed by the system if it's a pipe and the other end closed or other severe errors.
|
|
505
|
+
# Attempt to close only if this function opened it, and it seems like it might be openable/closable.
|
|
506
|
+
if file_opened_by_function and f is not None:
|
|
507
|
+
try:
|
|
508
|
+
if not f.closed:
|
|
509
|
+
f.close()
|
|
510
|
+
except Exception as e_close:
|
|
511
|
+
logging.error(f"Error closing file {file_name} during IOError handling: {e_close}")
|
|
512
|
+
f = None # Avoid trying to close again in finally if error persists
|
|
513
|
+
|
|
514
|
+
time.sleep(0.5)
|
|
515
|
+
open_attempts += 1
|
|
516
|
+
if open_attempts > 4:
|
|
517
|
+
logging.error(f"Too many IOErrors in QisInterface for module {module}. Raising error.")
|
|
518
|
+
# Set device status before raising, if possible
|
|
519
|
+
self.deviceDict[module][0:3] = [True, 'Stopped', f'IOError limit exceeded: {err}']
|
|
520
|
+
raise # Re-raise the last IOError
|
|
521
|
+
finally:
|
|
522
|
+
if file_opened_by_function and f is not None:
|
|
523
|
+
try:
|
|
524
|
+
if not f.closed: # Check if not already closed (e.g. in IOError block)
|
|
525
|
+
f.close()
|
|
526
|
+
except Exception as e_close:
|
|
527
|
+
logging.error(f"Error closing file {file_name} in finally block: {e_close}")
|
|
528
|
+
# If output_file_handle was passed, the caller is responsible for closing.
|
|
529
|
+
# If inMemoryData was passed, it's managed by the caller.
|
|
530
|
+
|
|
531
|
+
def get_device_list(self, sock=None) -> filter:
|
|
532
|
+
"""
|
|
533
|
+
returns a list of device IDs that are available for connection
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
sock:
|
|
537
|
+
Optional connection socket
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
A filtered iterable list of devices
|
|
541
|
+
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
if sock is None:
|
|
545
|
+
sock = self.sock
|
|
546
|
+
|
|
547
|
+
dev_string = self.sendAndReceiveText(sock, '$list')
|
|
548
|
+
dev_string = dev_string.replace('>', '')
|
|
549
|
+
dev_string = dev_string.replace(r'\d+\) ', '')
|
|
550
|
+
dev_string = dev_string.split('\r\n')
|
|
551
|
+
dev_string = filter(None, dev_string) #remove empty elements
|
|
552
|
+
|
|
553
|
+
return dev_string
|
|
554
|
+
|
|
555
|
+
def get_list_details(self, sock=None) -> list:
|
|
556
|
+
"""
|
|
557
|
+
Extended version of get_device_list which also returns the additional details
|
|
558
|
+
fields for each module
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
sock:
|
|
562
|
+
Optional connection socket
|
|
563
|
+
Returns:
|
|
564
|
+
Iterable list of strings containing the details of each device available for connection.
|
|
565
|
+
|
|
566
|
+
"""
|
|
567
|
+
if sock is None:
|
|
568
|
+
sock = self.sock
|
|
569
|
+
|
|
570
|
+
dev_string = self.sendAndReceiveText(sock, '$list details')
|
|
571
|
+
dev_string = dev_string.replace('>', '')
|
|
572
|
+
dev_string = dev_string.replace(r'\d+\) ', '')
|
|
573
|
+
dev_string = dev_string.split('\r\n')
|
|
574
|
+
dev_string = [x for x in dev_string if x] # remove empty elements
|
|
575
|
+
return dev_string
|
|
576
|
+
|
|
577
|
+
def scan_ip(self, qis_connection, ip_address) -> bool:
|
|
578
|
+
"""
|
|
579
|
+
Triggers QIS to look at a specific IP address for a module
|
|
580
|
+
|
|
581
|
+
Arguments
|
|
582
|
+
|
|
583
|
+
QisConnection : QpsInterface
|
|
584
|
+
The interface to the instance of QIS you would like to use for the scan.
|
|
585
|
+
ipAddress : str
|
|
586
|
+
The IP address of the module you are looking for eg '192.168.123.123'
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
logging.debug("Starting QIS IP Address Lookup at " + ip_address)
|
|
590
|
+
if not ip_address.lower().__contains__("tcp::"):
|
|
591
|
+
ip_address = "TCP::" + ip_address
|
|
592
|
+
response = "No response from QIS Scan"
|
|
593
|
+
try:
|
|
594
|
+
response = qis_connection.send_command("$scan " + ip_address)
|
|
595
|
+
# The valid response is "Located device: 192.168.1.2"
|
|
596
|
+
if "located" in response.lower():
|
|
597
|
+
logging.debug(response)
|
|
598
|
+
# return the valid response
|
|
599
|
+
return True
|
|
600
|
+
else:
|
|
601
|
+
logging.warning("No module found at " + ip_address)
|
|
602
|
+
logging.warning(response)
|
|
603
|
+
return False
|
|
604
|
+
|
|
605
|
+
except Exception as e:
|
|
606
|
+
logging.warning("No module found at " + ip_address)
|
|
607
|
+
logging.warning(e)
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def get_qis_module_selection(self, preferred_connection_only=True, additional_options="DEF_ARGS", scan=True) -> str:
|
|
612
|
+
"""
|
|
613
|
+
Scans for available modules and allows the user to select one through an interactive selection process.
|
|
614
|
+
Can also handle additional custom options and some built-in ones such as rescanning
|
|
615
|
+
|
|
616
|
+
Arguments:
|
|
617
|
+
preferred_connection_only : bool
|
|
618
|
+
by default (True), returns only one preferred connection eg: USB for simplicity
|
|
619
|
+
additional_options: list
|
|
620
|
+
Additional operational options provided during module selection, such as rescan,
|
|
621
|
+
all connection types, and IP scan. Defaults to ['rescan', 'all con types', 'ip scan']. These allow the
|
|
622
|
+
additional options to be given to the user and handled in the top level script
|
|
623
|
+
scan : bool
|
|
624
|
+
Indicates whether to initiate a rescanning process for devices before listing. Defaults to True and
|
|
625
|
+
will take longer to return
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
str: The identifier of the selected module, or the action selected from the additional options.
|
|
629
|
+
|
|
630
|
+
Raises:
|
|
631
|
+
KeyError: Raised when unexpected keys are found in the scanned device data.
|
|
632
|
+
ValueError: Raised if no valid selection is made or the provided IP address is invalid.
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
# Avoid mutable warning by adding the argument list in the function rather than the header
|
|
636
|
+
if additional_options == "DEF_ARGS":
|
|
637
|
+
additional_options = ['rescan', 'all con types', 'ip scan']
|
|
638
|
+
|
|
639
|
+
table_headers = ["Modules"]
|
|
640
|
+
ip_address = None
|
|
641
|
+
favourite = preferred_connection_only
|
|
642
|
+
while True:
|
|
643
|
+
printText("Scanning for modules...")
|
|
644
|
+
found_devices = None
|
|
645
|
+
if scan and ip_address is None:
|
|
646
|
+
found_devices = self.qis_scan_devices(scan=scan, preferred_connection_only=favourite)
|
|
647
|
+
elif scan and ip_address is not None:
|
|
648
|
+
found_devices = self.qis_scan_devices(scan=scan, preferred_connection_only=favourite, ip_address=ip_address)
|
|
649
|
+
|
|
650
|
+
my_device_id = listSelection(title="Select a module",message="Select a module",
|
|
651
|
+
selectionList=found_devices, additionalOptions= additional_options,
|
|
652
|
+
nice=True, tableHeaders=table_headers, indexReq=True)
|
|
653
|
+
|
|
654
|
+
if my_device_id.lower() == 'rescan':
|
|
655
|
+
favourite = True
|
|
656
|
+
ip_address = None
|
|
657
|
+
continue
|
|
658
|
+
elif my_device_id.lower() == 'all con types':
|
|
659
|
+
favourite = False
|
|
660
|
+
printText("Displaying all connection types...")
|
|
661
|
+
continue
|
|
662
|
+
elif my_device_id.lower() == 'ip scan':
|
|
663
|
+
ip_address = requestDialog(title="Please input the IP Address you would like to scan")
|
|
664
|
+
favourite = False
|
|
665
|
+
continue
|
|
666
|
+
break
|
|
667
|
+
|
|
668
|
+
return my_device_id
|
|
669
|
+
|
|
670
|
+
def qis_scan_devices(self, scan=True, preferred_connection_only=True, ip_address=None) -> list:
|
|
671
|
+
"""
|
|
672
|
+
Begins a scan for devices and returns a simple list of devices
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
scan:
|
|
676
|
+
Should a scan be initiated? If False, the function will return immediately with the list
|
|
677
|
+
preferred_connection_only:
|
|
678
|
+
The default (True), returns only one preferred connection eg: USB for simplicity
|
|
679
|
+
ip_address:
|
|
680
|
+
IP address of the module you are looking for eg '192.168.123.123'
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
list: List of module strings found during scan
|
|
684
|
+
"""
|
|
685
|
+
|
|
686
|
+
device_list = []
|
|
687
|
+
found_devices = "1"
|
|
688
|
+
found_devices2 = "2" # this is used to check if new modules are being discovered or if all have been found.
|
|
689
|
+
scan_wait = 2 # The number of seconds waited between the scan and the initial list
|
|
690
|
+
list_wait = 1 # The time between checks for new devices in the list
|
|
691
|
+
|
|
692
|
+
if scan:
|
|
693
|
+
# Perform the initial scan attempt
|
|
694
|
+
if ip_address is None:
|
|
695
|
+
dev_string = self.sendAndReceiveText(self.sock, '$scan')
|
|
696
|
+
else:
|
|
697
|
+
dev_string = self.sendAndReceiveText(self.sock, '$scan TCP::' + ip_address)
|
|
698
|
+
# Wait for devices to enumerate
|
|
699
|
+
time.sleep(scan_wait)
|
|
700
|
+
# While new devices are being found, extend the wait time
|
|
701
|
+
while found_devices not in found_devices2:
|
|
702
|
+
found_devices = self.sendAndReceiveText(self.sock, '$list')
|
|
703
|
+
time.sleep(list_wait)
|
|
704
|
+
found_devices2 = self.sendAndReceiveText(self.sock, '$list')
|
|
705
|
+
else:
|
|
706
|
+
found_devices = self.sendAndReceiveText(self.sock, '$list')
|
|
707
|
+
|
|
708
|
+
# If we found devices, process them into a list to return
|
|
709
|
+
if not "no devices found" in found_devices.lower():
|
|
710
|
+
found_devices = found_devices.replace('>', '')
|
|
711
|
+
found_devices = found_devices.split('\r\n')
|
|
712
|
+
# Can't stream over REST. Removing all REST connections.
|
|
713
|
+
temp_list= list()
|
|
714
|
+
for item in found_devices:
|
|
715
|
+
if item is None or "rest" in item.lower() or item == "":
|
|
716
|
+
pass
|
|
717
|
+
else:
|
|
718
|
+
temp_list.append(item.split(")")[1].strip())
|
|
719
|
+
found_devices = temp_list
|
|
720
|
+
|
|
721
|
+
# If the preferred connection only flag is True, then only show one connection type for each module connected.
|
|
722
|
+
# First, order the devices by their preference type and then pick the first con type found for each module.
|
|
723
|
+
if preferred_connection_only:
|
|
724
|
+
found_devices = self.sort_favourite(found_devices)
|
|
725
|
+
else:
|
|
726
|
+
found_devices = ["***No Devices Found***"]
|
|
727
|
+
|
|
728
|
+
return found_devices
|
|
729
|
+
|
|
730
|
+
def sort_favourite(self, found_devices) -> list:
|
|
731
|
+
"""
|
|
732
|
+
Reduces the list of located devices by referencing to the preferred type of connection. Only
|
|
733
|
+
one connection type will be returned for each module for easier user selection. ie: A module connected
|
|
734
|
+
on both USB and TCP will now only return with USB
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
found_devices:
|
|
738
|
+
List of located devices from a scan operation
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
List of devices filtered/sorted devices
|
|
742
|
+
"""
|
|
743
|
+
|
|
744
|
+
index = 0
|
|
745
|
+
sorted_found_devices = []
|
|
746
|
+
con_pref = ["USB", "TCP", "SERIAL", "REST", "TELNET"]
|
|
747
|
+
while len(sorted_found_devices) != len(found_devices):
|
|
748
|
+
for device in found_devices:
|
|
749
|
+
if con_pref[index] in device.upper():
|
|
750
|
+
sorted_found_devices.append(device)
|
|
751
|
+
index += 1
|
|
752
|
+
found_devices = sorted_found_devices
|
|
753
|
+
|
|
754
|
+
# new dictionary only containing one favourite connection to each device.
|
|
755
|
+
fav_con_found_devices = []
|
|
756
|
+
index = 0
|
|
757
|
+
for device in sorted_found_devices:
|
|
758
|
+
if fav_con_found_devices == [] or not device.split("::")[1] in str(fav_con_found_devices):
|
|
759
|
+
fav_con_found_devices.append(device)
|
|
760
|
+
found_devices = fav_con_found_devices
|
|
761
|
+
return found_devices
|
|
762
|
+
|
|
763
|
+
def stream_running_status(self, device, sock=None) -> str:
|
|
764
|
+
"""
|
|
765
|
+
returns a single word status string for a given device. Generally this will be running, overrun, or stopped
|
|
766
|
+
|
|
767
|
+
Arguments
|
|
768
|
+
|
|
769
|
+
device : str
|
|
770
|
+
The device ID to target
|
|
771
|
+
sock:
|
|
772
|
+
The socket to communicate over, or None to use the default.
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
str: Single word status string to show the operation of streaming
|
|
776
|
+
"""
|
|
777
|
+
if sock is None:
|
|
778
|
+
sock = self.sock
|
|
779
|
+
|
|
780
|
+
index = 0
|
|
781
|
+
stream_status = self.sendAndReceiveText(sock, 'stream?', device)
|
|
782
|
+
|
|
783
|
+
# Split the response, select the first time and trim the colon
|
|
784
|
+
status_parts = stream_status.split('\r\n')
|
|
785
|
+
stream_status = re.sub(r':', '', status_parts[index])
|
|
786
|
+
return stream_status
|
|
787
|
+
|
|
788
|
+
def stream_buffer_status(self, device, sock=None) -> str:
|
|
789
|
+
"""
|
|
790
|
+
returns the info on the stripes buffered during the stream
|
|
791
|
+
|
|
792
|
+
Arguments
|
|
793
|
+
|
|
794
|
+
device : str
|
|
795
|
+
The device ID to target
|
|
796
|
+
sock:
|
|
797
|
+
The socket to communicate over, or None to use the default.
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
str: String with the numbers of stripes buffered
|
|
801
|
+
"""
|
|
802
|
+
if sock is None:
|
|
803
|
+
sock = self.sock
|
|
804
|
+
|
|
805
|
+
index = 1
|
|
806
|
+
stream_status = self.sendAndReceiveText(sock, 'stream?', device)
|
|
807
|
+
|
|
808
|
+
# Split the response, select the second the info on the stripes buffered
|
|
809
|
+
status_lines = stream_status.split('\r\n')
|
|
810
|
+
stream_status = re.sub(r'^Stripes Buffered: ', '', status_lines[index])
|
|
811
|
+
return stream_status
|
|
812
|
+
|
|
813
|
+
# TODO: MD - This function should be replaced with a more generic method of accessing the header
|
|
814
|
+
# The return of a string with concatenated value and units should be replaced with something easier to parse
|
|
815
|
+
def stream_header_average(self, device, sock=None) -> str:
|
|
816
|
+
"""
|
|
817
|
+
Gets the averaging used on the current stream, required for processing the stripe data returned from QIS
|
|
818
|
+
|
|
819
|
+
Arguments
|
|
820
|
+
|
|
821
|
+
device : str
|
|
822
|
+
The device ID to target
|
|
823
|
+
sock:
|
|
824
|
+
The socket to communicate over, or None to use the default.
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
str: String with the rate and unit
|
|
828
|
+
"""
|
|
829
|
+
try:
|
|
830
|
+
if sock is None:
|
|
831
|
+
sock = self.sock
|
|
832
|
+
|
|
833
|
+
index = 2 # index of relevant line in split string
|
|
834
|
+
stream_status = self.send_and_receive_text(sock, send_text='stream text header', device=device)
|
|
835
|
+
|
|
836
|
+
self.qps_stream_header = stream_status
|
|
837
|
+
|
|
838
|
+
# Check for the header format. If XML, process here
|
|
839
|
+
if self.is_xml_header(stream_status):
|
|
840
|
+
# Get the basic averaging rate (V3 header)
|
|
841
|
+
xml_root = self.get_stream_xml_header(device=device, sock=sock)
|
|
842
|
+
self.module_xml_header = xml_root
|
|
843
|
+
|
|
844
|
+
# Return the time-based averaging string
|
|
845
|
+
device_period = xml_root.find('.//devicePeriod')
|
|
846
|
+
if device_period is None:
|
|
847
|
+
device_period = xml_root.find('.//devicePerioduS')
|
|
848
|
+
if device_period is None:
|
|
849
|
+
device_period = xml_root.find('.//mainPeriod')
|
|
850
|
+
average_str = device_period.text
|
|
851
|
+
return average_str
|
|
852
|
+
# For legacy text headers, process here
|
|
853
|
+
else:
|
|
854
|
+
status_lines = stream_status.split('\r\n')
|
|
855
|
+
if 'Header Not Available' in status_lines[0]:
|
|
856
|
+
dummy = status_lines[0] + '. Check stream has been run on device.'
|
|
857
|
+
return dummy
|
|
858
|
+
average_str = re.sub(r'^Average: ', '', status_lines[index])
|
|
859
|
+
avg = average_str
|
|
860
|
+
avg = 2 ** int(avg)
|
|
861
|
+
return '{}'.format(avg)
|
|
862
|
+
except Exception as e:
|
|
863
|
+
logging.error(device + ' Unable to get stream average.' + self.host + ':' + str(self.port))
|
|
864
|
+
raise e
|
|
865
|
+
|
|
866
|
+
def stream_header_format(self, device, sock=None) -> str:
|
|
867
|
+
"""
|
|
868
|
+
Formats the stream header for use at the top of a CSV file. This adds the appropriate time column and
|
|
869
|
+
each of the channel data columns
|
|
870
|
+
|
|
871
|
+
Arguments
|
|
872
|
+
|
|
873
|
+
device:
|
|
874
|
+
The device ID to target
|
|
875
|
+
sock:
|
|
876
|
+
The socket to communicate over, or None to use the default.
|
|
877
|
+
|
|
878
|
+
Returns:
|
|
879
|
+
str: Get the CSV formatted header string for the current stream
|
|
880
|
+
"""
|
|
881
|
+
try:
|
|
882
|
+
if sock is None:
|
|
883
|
+
sock = self.sock
|
|
884
|
+
|
|
885
|
+
index = 1
|
|
886
|
+
stream_status = self.sendAndReceiveText(sock,'stream text header', device)
|
|
887
|
+
# Check if this is an XML form header
|
|
888
|
+
if self.is_xml_header (stream_status):
|
|
889
|
+
# Get the basic averaging rate (V3 header)
|
|
890
|
+
xml_root = self.get_stream_xml_header (device=device, sock=sock)
|
|
891
|
+
# Return the time-based averaging string
|
|
892
|
+
device_period = xml_root.find('.//devicePeriod')
|
|
893
|
+
time_unit = 'uS'
|
|
894
|
+
if device_period is None:
|
|
895
|
+
device_period = xml_root.find('.//devicePerioduS')
|
|
896
|
+
if device_period is None:
|
|
897
|
+
device_period = xml_root.find('.//mainPeriod')
|
|
898
|
+
if 'ns' in device_period.text:
|
|
899
|
+
time_unit = 'nS'
|
|
900
|
+
|
|
901
|
+
# The time column always first
|
|
902
|
+
format_header = 'Time ' + time_unit + ','
|
|
903
|
+
# Find the channels section of each group and iterate through it to add the channel columns
|
|
904
|
+
for group in xml_root.iter():
|
|
905
|
+
if group.tag == "channels":
|
|
906
|
+
for chan in group:
|
|
907
|
+
# Avoid children that are not named channels
|
|
908
|
+
if (chan.find('.//name') is not None):
|
|
909
|
+
name_str = chan.find('.//name').text
|
|
910
|
+
group_str = chan.find('.//group').text
|
|
911
|
+
unit_str = chan.find('.//units').text
|
|
912
|
+
format_header = format_header + name_str + " " + group_str + " " + unit_str + ","
|
|
913
|
+
format_header = format_header.rstrip(",")
|
|
914
|
+
return format_header
|
|
915
|
+
else:
|
|
916
|
+
stream_status = stream_status.split('\r\n')
|
|
917
|
+
if 'Header Not Available' in stream_status[0]:
|
|
918
|
+
err_str = stream_status[0] + '. Check stream has been ran on device.'
|
|
919
|
+
logging.error(err_str)
|
|
920
|
+
return err_str
|
|
921
|
+
output_mode = self.sendAndReceiveText(sock,'Config Output Mode?', device)
|
|
922
|
+
power_mode = self.sendAndReceiveText(sock,'stream mode power?', device)
|
|
923
|
+
data_format = int(re.sub(r'^Format: ', '', stream_status[index]))
|
|
924
|
+
b0 = 1 #12V_I
|
|
925
|
+
b1 = 1 << 1 #12V_V
|
|
926
|
+
b2 = 1 << 2 #5V_I
|
|
927
|
+
b3 = 1 << 3 #5V_V
|
|
928
|
+
format_header = 'StripeNum, Trig, '
|
|
929
|
+
if data_format & b3:
|
|
930
|
+
if '3V3' in output_mode:
|
|
931
|
+
format_header = format_header + '3V3_V,'
|
|
932
|
+
else:
|
|
933
|
+
format_header = format_header + '5V_V,'
|
|
934
|
+
if data_format & b2:
|
|
935
|
+
if '3V3' in output_mode:
|
|
936
|
+
format_header = format_header + ' 3V3_I,'
|
|
937
|
+
else:
|
|
938
|
+
format_header = format_header + ' 5V_I,'
|
|
939
|
+
|
|
940
|
+
if data_format & b1:
|
|
941
|
+
format_header = format_header + ' 12V_V,'
|
|
942
|
+
if data_format & b0:
|
|
943
|
+
format_header = format_header + ' 12V_I'
|
|
944
|
+
if 'Enabled' in power_mode:
|
|
945
|
+
if '3V3' in output_mode:
|
|
946
|
+
format_header = format_header + ' 3V3_P'
|
|
947
|
+
else:
|
|
948
|
+
format_header = format_header + ' 5V_P'
|
|
949
|
+
if (data_format & b1) or (data_format & b0):
|
|
950
|
+
format_header = format_header + ' 12V_P'
|
|
951
|
+
return format_header
|
|
952
|
+
except Exception as e:
|
|
953
|
+
logging.error(device + ' Unable to get stream format.' + self.host + ':' + '{}'.format(self.port))
|
|
954
|
+
raise e
|
|
955
|
+
|
|
956
|
+
def stream_get_stripes_text(self, sock, device: str) -> tuple[str, str]:
|
|
957
|
+
"""
|
|
958
|
+
Retrieve and process text data from a QIS stream.
|
|
959
|
+
We try to ready a block of data and also check for end of data and error cases
|
|
960
|
+
|
|
961
|
+
Args:
|
|
962
|
+
sock:
|
|
963
|
+
The socket instance used for communication with the device.
|
|
964
|
+
device:
|
|
965
|
+
The device ID string
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
A tuple containing:
|
|
969
|
+
- The status of the data stream as a comma seperated list of status items
|
|
970
|
+
- The retrieved text data from the stream.
|
|
971
|
+
"""
|
|
972
|
+
|
|
973
|
+
stream_status = "running"
|
|
974
|
+
is_end_of_block = False
|
|
975
|
+
|
|
976
|
+
# Try and read the next blocks of stripes from QIS
|
|
977
|
+
stripes = self.sendAndReceiveText(sock, 'stream text all', device)
|
|
978
|
+
|
|
979
|
+
# The 'eof' marker ONLY indicates that the full number of requested stripes was not available.
|
|
980
|
+
# More may be found later.
|
|
981
|
+
if stripes.endswith("eof\r\n>"):
|
|
982
|
+
is_end_of_block = True
|
|
983
|
+
stripes = stripes.rstrip("eof\r\n>")
|
|
984
|
+
if stripes.endswith("\r\n>"):
|
|
985
|
+
stripes= stripes[:-1] # remove the trailing ">"
|
|
986
|
+
# The current reader seems to lose the final line feeds, so check for this
|
|
987
|
+
if len(stripes) > 0:
|
|
988
|
+
if not stripes.endswith("\r\n"):
|
|
989
|
+
stripes += "\r\n"
|
|
990
|
+
|
|
991
|
+
# If there is an unusually small data set, check the stream status to make sure data is coming
|
|
992
|
+
# 7 is a little arbitrary, but smaller than any possible stripe size. Over calling will not matter anyway
|
|
993
|
+
if len(stripes) < 7 or is_end_of_block:
|
|
994
|
+
current_status = self.sendAndReceiveText(sock, 'stream?', device).lower()
|
|
995
|
+
if "running" in current_status:
|
|
996
|
+
stream_status = "running"
|
|
997
|
+
elif "overrun" in current_status or "out of buffer" in current_status:
|
|
998
|
+
stream_status = "overrun"
|
|
999
|
+
elif "stopped" in current_status:
|
|
1000
|
+
stream_status = "stopped"
|
|
1001
|
+
# If the stream is stopped and at end of block, we have read all the data
|
|
1002
|
+
if is_end_of_block:
|
|
1003
|
+
stream_status = stream_status + "eof"
|
|
1004
|
+
|
|
1005
|
+
return stream_status, stripes
|
|
1006
|
+
|
|
1007
|
+
def device_control_index(self, device) -> int:
|
|
1008
|
+
"""
|
|
1009
|
+
Returns the index of the device in the control lists. If the device is not
|
|
1010
|
+
registered, then it is added first. This is a key part of allowing us to
|
|
1011
|
+
track the status of multiple streaming devices and manage them from outside
|
|
1012
|
+
their streaming thread
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
device:
|
|
1016
|
+
Device ID string
|
|
1017
|
+
Returns:
|
|
1018
|
+
Index of the device in the various control lists
|
|
1019
|
+
|
|
1020
|
+
"""
|
|
1021
|
+
if device in self.deviceList:
|
|
1022
|
+
return self.deviceList.index(device)
|
|
1023
|
+
else:
|
|
1024
|
+
self.listSemaphore.acquire()
|
|
1025
|
+
self.deviceList.append(device)
|
|
1026
|
+
self.stopFlagList.append(True)
|
|
1027
|
+
self.listSemaphore.release()
|
|
1028
|
+
return self.deviceList.index(device)
|
|
1029
|
+
|
|
1030
|
+
def device_dict_setup(self, module) -> None:
|
|
1031
|
+
"""
|
|
1032
|
+
Adds a dictionary entry for a new module we are connecting to (including the base QIS connection)
|
|
1033
|
+
This is used for tracking the status of modules throughout the streaming process
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
module:
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
None
|
|
1040
|
+
"""
|
|
1041
|
+
if module in self.deviceDict.keys():
|
|
1042
|
+
return
|
|
1043
|
+
elif module == 'QIS':
|
|
1044
|
+
self.dictSemaphore.acquire()
|
|
1045
|
+
self.deviceDict[module] = [False, 'Disconnected', "No attempt to connect to QIS yet"]
|
|
1046
|
+
self.dictSemaphore.release()
|
|
1047
|
+
else:
|
|
1048
|
+
self.dictSemaphore.acquire()
|
|
1049
|
+
self.deviceDict[module] = [False, 'Stopped', "User hasn't started stream"]
|
|
1050
|
+
self.dictSemaphore.release()
|
|
1051
|
+
|
|
1052
|
+
# Pass in a stream header and we check if it is XML or legacy format
|
|
1053
|
+
def is_xml_header (self, header_text) -> bool:
|
|
1054
|
+
"""
|
|
1055
|
+
Checks if the given header string is in XML format (as apposed to legacy text format) or an invalid string
|
|
1056
|
+
|
|
1057
|
+
Args:
|
|
1058
|
+
header_text:
|
|
1059
|
+
The header string to evaluate
|
|
1060
|
+
Returns:
|
|
1061
|
+
True if the header is in XML form
|
|
1062
|
+
|
|
1063
|
+
"""
|
|
1064
|
+
if '?xml version=' not in header_text:
|
|
1065
|
+
return False
|
|
1066
|
+
else:
|
|
1067
|
+
return True
|
|
1068
|
+
|
|
1069
|
+
# Internal function. Gets the stream header and parses it into useful information
|
|
1070
|
+
def get_stream_xml_header (self, device, sock=None) -> ET.Element:
|
|
1071
|
+
"""
|
|
1072
|
+
Gets the XML format header from an attached device (which must have run or be running a stream)
|
|
1073
|
+
Parses the string into XML and returns the root element
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
device:
|
|
1077
|
+
Device ID to return from
|
|
1078
|
+
sock:
|
|
1079
|
+
Optional QIS socket to use for communication.
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
|
|
1083
|
+
"""
|
|
1084
|
+
header_data = None
|
|
1085
|
+
|
|
1086
|
+
try:
|
|
1087
|
+
if sock is None:
|
|
1088
|
+
sock = self.sock
|
|
1089
|
+
count = 0
|
|
1090
|
+
while True:
|
|
1091
|
+
if count > 5:
|
|
1092
|
+
break
|
|
1093
|
+
count += 1
|
|
1094
|
+
# Get the raw data
|
|
1095
|
+
header_data = self.send_and_receive_text(sock, send_text='stream text header', device=device)
|
|
1096
|
+
|
|
1097
|
+
# Check for no header (no stream started)
|
|
1098
|
+
if 'Header Not Available' in header_data:
|
|
1099
|
+
logging.error(device + ' Stream header not available.' + self.host + ':' + str(self.port))
|
|
1100
|
+
continue
|
|
1101
|
+
|
|
1102
|
+
# Check for XML format
|
|
1103
|
+
if '?xml version=' not in header_data:
|
|
1104
|
+
logging.error(device + ' Header not in XML form.' + self.host + ':' + str(self.port))
|
|
1105
|
+
continue
|
|
1106
|
+
|
|
1107
|
+
break
|
|
1108
|
+
# Parse XML into a structured format
|
|
1109
|
+
xml_root = ET.fromstring(header_data)
|
|
1110
|
+
|
|
1111
|
+
# Check header format is supported by quarchpy
|
|
1112
|
+
version_str = xml_root.find('.//version').text
|
|
1113
|
+
if 'V3' not in version_str:
|
|
1114
|
+
logging.error(device + ' Stream header version not compatible: ' + xml_root.find('version').text + '.' + self.host + ':' + str(self.port))
|
|
1115
|
+
raise Exception ("Stream header version not supported")
|
|
1116
|
+
|
|
1117
|
+
# Return the XML structure for the code to use
|
|
1118
|
+
return xml_root
|
|
1119
|
+
|
|
1120
|
+
except Exception as e:
|
|
1121
|
+
logging.error(device + ' Exception while parsing stream header XML.' + self.host + ':' + str(self.port))
|
|
1122
|
+
raise e
|
|
1123
|
+
|
|
1124
|
+
def send_command (self, command: str, device: str = '', qis_socket: socket.socket=None, no_cursor_expected: bool=False, no_response_expected: bool=False, command_delay: float=0.0) -> str:
|
|
1125
|
+
"""
|
|
1126
|
+
Sends a command and returns the response as a string. Multiple lines are escaped with CRLF.
|
|
1127
|
+
The command is sent to the QIS socket, and depending on the command will be replied by either QIS
|
|
1128
|
+
or the hardware module.
|
|
1129
|
+
|
|
1130
|
+
Args:
|
|
1131
|
+
command:
|
|
1132
|
+
Command string
|
|
1133
|
+
device:
|
|
1134
|
+
Optional Device ID string to send the command to. Use default/blank for QIS direct commands
|
|
1135
|
+
qis_socket:
|
|
1136
|
+
Optional Socket to use for the command, if the default is not wanted
|
|
1137
|
+
no_cursor_expected:
|
|
1138
|
+
Optional Flag true if the command does not return a cursor, so we should not wait for it
|
|
1139
|
+
no_response_expected:
|
|
1140
|
+
Optional Flag true if the command does not return a response, so we should not wait for it.
|
|
1141
|
+
command_delay:
|
|
1142
|
+
Optional delay to prevent commands running in close succession. Timed in seconds.
|
|
1143
|
+
Returns:
|
|
1144
|
+
Command response string or None if no response expected
|
|
1145
|
+
"""
|
|
1146
|
+
if qis_socket is None:
|
|
1147
|
+
qis_socket = self.sock
|
|
1148
|
+
|
|
1149
|
+
if no_response_expected:
|
|
1150
|
+
self.send_text(qis_socket, command, device)
|
|
1151
|
+
return ""
|
|
1152
|
+
else:
|
|
1153
|
+
if not (device == ''):
|
|
1154
|
+
self.device_dict_setup(device)
|
|
1155
|
+
res = self.send_and_receive_text(qis_socket, command, device, not no_cursor_expected)
|
|
1156
|
+
|
|
1157
|
+
# This is a poor sleep mechanism! Better would be to track time since the last command
|
|
1158
|
+
if command_delay > 0:
|
|
1159
|
+
time.sleep(command_delay)
|
|
1160
|
+
|
|
1161
|
+
# Trim the expected cursor at the end of the response
|
|
1162
|
+
if res[-3:] == '\r\n>':
|
|
1163
|
+
res = res[:-3] # remove last three chars - '\r\n>'
|
|
1164
|
+
elif res[-2:] == '\n>':
|
|
1165
|
+
res = res[:-2] # remove last 2 chars - '\n>'
|
|
1166
|
+
return res
|
|
1167
|
+
|
|
1168
|
+
def send_and_receive_text(self, sock, send_text='$help', device='', read_until_cursor=True) -> str:
|
|
1169
|
+
"""
|
|
1170
|
+
Internal function for command handling. This handles complex cases such as timeouts and XML
|
|
1171
|
+
response formatting, which conflicts with the default cursor
|
|
1172
|
+
|
|
1173
|
+
Args:
|
|
1174
|
+
sock:
|
|
1175
|
+
The socket to communicate over
|
|
1176
|
+
send_text:
|
|
1177
|
+
The command text to send
|
|
1178
|
+
device:
|
|
1179
|
+
Optional device ID to send the command to
|
|
1180
|
+
read_until_cursor:
|
|
1181
|
+
Flag to indicate if we should read until the cursor is returned
|
|
1182
|
+
|
|
1183
|
+
Returns:
|
|
1184
|
+
Response string from the module
|
|
1185
|
+
"""
|
|
1186
|
+
|
|
1187
|
+
# Avoid multiple threads trying to send at once. QIS only has a single socket for all devices
|
|
1188
|
+
self.sockSemaphore.acquire()
|
|
1189
|
+
try:
|
|
1190
|
+
# Send the command
|
|
1191
|
+
self.send_text(sock, send_text, device)
|
|
1192
|
+
# Receive Response
|
|
1193
|
+
res = self.receive_text(sock)
|
|
1194
|
+
# If we get no response, log an error and try to flush using a simple stream query command
|
|
1195
|
+
# If that works, we retry our command. In fail cases we raise an exception and abort as
|
|
1196
|
+
# the connection is bad
|
|
1197
|
+
if len(res) == 0:
|
|
1198
|
+
logging.error("Empty response from QIS for cmd: " + send_text + ". To device: " + device)
|
|
1199
|
+
self.send_text(sock, "stream?", device)
|
|
1200
|
+
res = self.receive_text(sock)
|
|
1201
|
+
if len(res) != 0:
|
|
1202
|
+
self.send_text(sock, send_text, device)
|
|
1203
|
+
res = self.receive_text(sock)
|
|
1204
|
+
if len(res) == 0:
|
|
1205
|
+
raise (Exception("Empty response from QIS. Sent: " + send_text))
|
|
1206
|
+
else:
|
|
1207
|
+
raise (Exception("Empty response from QIS. Sent: " + send_text))
|
|
1208
|
+
|
|
1209
|
+
if res[0] == self.cursor:
|
|
1210
|
+
logging.error('Only returned a cursor from QIS. Sent: ' + send_text)
|
|
1211
|
+
raise (Exception("Only returned a cursor from QIS. Sent: " + send_text))
|
|
1212
|
+
if 'Create Socket Fail' == res[0]: # If create socked fail (between QIS and tcp/ip module)
|
|
1213
|
+
logging.error(res[0])
|
|
1214
|
+
raise (Exception("Failed to open QIS to module socked. Sent: " + send_text))
|
|
1215
|
+
if 'Connection Timeout' == res[0]:
|
|
1216
|
+
logging.error(res[0])
|
|
1217
|
+
raise (Exception("Connection timeout from QIS. Sent: " + send_text))
|
|
1218
|
+
|
|
1219
|
+
# If reading until a cursor comes back, then keep reading until a cursor appears or max tries exceeded
|
|
1220
|
+
# Because large XML responses are possible, we need to validate them as complete before looking
|
|
1221
|
+
# for a final cursor
|
|
1222
|
+
if read_until_cursor:
|
|
1223
|
+
|
|
1224
|
+
max_reads = 1000
|
|
1225
|
+
count = 1
|
|
1226
|
+
is_xml = False
|
|
1227
|
+
|
|
1228
|
+
while True:
|
|
1229
|
+
|
|
1230
|
+
# Determine if the response is XML based on its start
|
|
1231
|
+
if count == 1: # Only check this on the first read
|
|
1232
|
+
if res.startswith("<?xml"): # Likely XML if it starts with '<'
|
|
1233
|
+
is_xml = True
|
|
1234
|
+
elif res.startswith("<XmlResponse"):
|
|
1235
|
+
is_xml = True
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
if is_xml:
|
|
1239
|
+
# Try to parse the XML to check if it's complete
|
|
1240
|
+
try:
|
|
1241
|
+
ET.fromstring(res[:-1]) # If it parses, the response is complete
|
|
1242
|
+
return res[:-1] # Exit the loop, valid XML received
|
|
1243
|
+
except ET.ParseError:
|
|
1244
|
+
pass # Keep reading until XML is complete
|
|
1245
|
+
else:
|
|
1246
|
+
# Handle normal strings
|
|
1247
|
+
if res[-1:] == self.cursor: # If the last character is '>', stop reading
|
|
1248
|
+
break
|
|
1249
|
+
|
|
1250
|
+
# Receive more data
|
|
1251
|
+
res += self.receive_text(sock)
|
|
1252
|
+
|
|
1253
|
+
# Increment count and check for max reads
|
|
1254
|
+
count += 1
|
|
1255
|
+
if count >= max_reads:
|
|
1256
|
+
raise Exception('Count = Error: max reads exceeded before response was complete')
|
|
1257
|
+
|
|
1258
|
+
return res
|
|
1259
|
+
|
|
1260
|
+
except Exception as e:
|
|
1261
|
+
# Something went wrong during send qis cmd
|
|
1262
|
+
logging.error("Error! Unable to retrieve response from QIS. Command: " + send_text)
|
|
1263
|
+
logging.error(e)
|
|
1264
|
+
raise e
|
|
1265
|
+
finally:
|
|
1266
|
+
self.sockSemaphore.release()
|
|
1267
|
+
|
|
1268
|
+
def receive_text(self, sock) -> str:
|
|
1269
|
+
"""
|
|
1270
|
+
Received bytes from the socket and converts to a test string
|
|
1271
|
+
Args:
|
|
1272
|
+
sock:
|
|
1273
|
+
Socket to communicate over
|
|
1274
|
+
|
|
1275
|
+
Returns:
|
|
1276
|
+
|
|
1277
|
+
"""
|
|
1278
|
+
res = bytearray()
|
|
1279
|
+
res.extend(self.rx_bytes(sock))
|
|
1280
|
+
res = res.decode()
|
|
1281
|
+
|
|
1282
|
+
return res
|
|
1283
|
+
|
|
1284
|
+
def send_text(self, sock, message='$help', device='') -> bool:
|
|
1285
|
+
"""
|
|
1286
|
+
|
|
1287
|
+
Args:
|
|
1288
|
+
sock:
|
|
1289
|
+
Socket to communicate over
|
|
1290
|
+
message:
|
|
1291
|
+
text command to send
|
|
1292
|
+
device:
|
|
1293
|
+
Optional device ID to target with the command
|
|
1294
|
+
|
|
1295
|
+
Returns:
|
|
1296
|
+
|
|
1297
|
+
"""
|
|
1298
|
+
# Send text to QIS, don't read its response
|
|
1299
|
+
if device != '':
|
|
1300
|
+
message = device + ' ' + message
|
|
1301
|
+
|
|
1302
|
+
conv_mess = message + '\r\n'
|
|
1303
|
+
sock.sendall(conv_mess.encode('utf-8'))
|
|
1304
|
+
return True
|
|
1305
|
+
|
|
1306
|
+
def rx_bytes(self,sock) -> bytes:
|
|
1307
|
+
"""
|
|
1308
|
+
Reads an array of bytes from the socket as part of handling a command response
|
|
1309
|
+
|
|
1310
|
+
Args:
|
|
1311
|
+
sock:
|
|
1312
|
+
Socket to communicate over
|
|
1313
|
+
Returns:
|
|
1314
|
+
Bytes read
|
|
1315
|
+
|
|
1316
|
+
"""
|
|
1317
|
+
|
|
1318
|
+
max_exceptions=10
|
|
1319
|
+
exceptions=0
|
|
1320
|
+
max_read_repeats=50
|
|
1321
|
+
read_repeats=0
|
|
1322
|
+
timeout_in_seconds = 10
|
|
1323
|
+
|
|
1324
|
+
#Keep trying to read bytes until we get some, unless the number of read repeats or exceptions is exceeded
|
|
1325
|
+
while True:
|
|
1326
|
+
try:
|
|
1327
|
+
# Select.select returns a list of waitable objects which are ready. On Windows, it has to be sockets.
|
|
1328
|
+
# The first argument is a list of objects to wait for reading, second writing, third 'exceptional condition'
|
|
1329
|
+
# We only use the read list and our socket to check if it is readable. if no timeout is specified,
|
|
1330
|
+
# then it blocks until it becomes readable.
|
|
1331
|
+
# TODO: AN: It is very unclear why we try to open a new socket if data is not ready.
|
|
1332
|
+
# This would seem like a hard failure case!
|
|
1333
|
+
ready = select.select([sock], [], [], timeout_in_seconds)
|
|
1334
|
+
if ready[0]:
|
|
1335
|
+
ret = sock.recv(self.maxRxBytes)
|
|
1336
|
+
return ret
|
|
1337
|
+
# If the socket is not ready for read, open a new one
|
|
1338
|
+
else:
|
|
1339
|
+
logging.error("Timeout: No bytes were available to read from QIS")
|
|
1340
|
+
logging.debug("Opening new QIS socket")
|
|
1341
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
1342
|
+
sock.connect((self.host, self.port))
|
|
1343
|
+
sock.settimeout(5)
|
|
1344
|
+
|
|
1345
|
+
try:
|
|
1346
|
+
welcome_string = self.sock.recv(self.maxRxBytes).rstrip()
|
|
1347
|
+
welcome_string = 'Connected@' + self.host + ':' + str(self.port) + ' ' + '\n ' + welcome_string
|
|
1348
|
+
logging.debug("Socket opened: " + welcome_string)
|
|
1349
|
+
except Exception as e:
|
|
1350
|
+
logging.error('Timeout: Failed to open new QIS socket and get the welcome message')
|
|
1351
|
+
raise e
|
|
1352
|
+
|
|
1353
|
+
read_repeats=read_repeats+1
|
|
1354
|
+
time.sleep(0.5)
|
|
1355
|
+
|
|
1356
|
+
except Exception as e:
|
|
1357
|
+
raise e
|
|
1358
|
+
|
|
1359
|
+
# If we read no data, its probably an error, but there may be cases where an empty response is valid
|
|
1360
|
+
if read_repeats >= max_read_repeats:
|
|
1361
|
+
logging.error('Max read repeats exceeded')
|
|
1362
|
+
return b''
|
|
1363
|
+
|
|
1364
|
+
def closeConnection(self, sock=None, conString: str=None) -> str:
|
|
1365
|
+
"""
|
|
1366
|
+
deprecated:: 2.2.13
|
|
1367
|
+
Use `close_connection` instead.
|
|
1368
|
+
"""
|
|
1369
|
+
return self.close_connection (sock=sock, con_string=conString)
|
|
1370
|
+
|
|
1371
|
+
def startStream(self, module: str, fileName: str, fileMaxMB: int, releaseOnData: bool, separator: str,
|
|
1372
|
+
streamDuration: int = None, inMemoryData=None, outputFileHandle=None, useGzip: bool = None,
|
|
1373
|
+
gzipCompressLevel: int = 9):
|
|
1374
|
+
"""
|
|
1375
|
+
deprecated:: 2.2.13
|
|
1376
|
+
Use `start_stream` instead.
|
|
1377
|
+
"""
|
|
1378
|
+
return self.start_stream(module, fileName, fileMaxMB, releaseOnData, separator, streamDuration, inMemoryData,
|
|
1379
|
+
outputFileHandle, useGzip, gzipCompressLevel)
|
|
1380
|
+
|
|
1381
|
+
def stopStream(self, module, blocking=True):
|
|
1382
|
+
"""
|
|
1383
|
+
deprecated:: 2.2.13
|
|
1384
|
+
Use `stop_stream` instead.
|
|
1385
|
+
"""
|
|
1386
|
+
return self.stop_stream(module, blocking)
|
|
1387
|
+
|
|
1388
|
+
def getDeviceList(self, sock=None):
|
|
1389
|
+
"""
|
|
1390
|
+
deprecated:: 2.2.13
|
|
1391
|
+
Use `start_stream_thread_qps` instead.
|
|
1392
|
+
"""
|
|
1393
|
+
return self.get_device_list(sock)
|
|
1394
|
+
|
|
1395
|
+
def scanIP(self, QisConnection, ipAddress):
|
|
1396
|
+
"""
|
|
1397
|
+
deprecated:: 2.2.13
|
|
1398
|
+
Use `scan_ip` instead.
|
|
1399
|
+
"""
|
|
1400
|
+
return self.scan_ip(QisConnection, ipAddress)
|
|
1401
|
+
|
|
1402
|
+
def GetQisModuleSelection(self, favouriteOnly=True, additionalOptions=['rescan', 'all con types', 'ip scan'],
|
|
1403
|
+
scan=True):
|
|
1404
|
+
"""
|
|
1405
|
+
deprecated:: 2.2.13
|
|
1406
|
+
Use `get_qis_module_selection` instead.
|
|
1407
|
+
"""
|
|
1408
|
+
return self.get_qis_module_selection(favouriteOnly, additionalOptions, scan)
|
|
1409
|
+
|
|
1410
|
+
def sendCommand(self, cmd, device="", timeout=20,sock=None,readUntilCursor=True, betweenCommandDelay=0.0, expectedResponse=True) -> str:
|
|
1411
|
+
"""
|
|
1412
|
+
deprecated:: 2.2.13
|
|
1413
|
+
Use `send_command` instead.
|
|
1414
|
+
"""
|
|
1415
|
+
return self.send_command(cmd, device, sock, False, not expectedResponse, betweenCommandDelay)
|
|
1416
|
+
|
|
1417
|
+
def sendCmd(self, device='', cmd='$help', sock=None, readUntilCursor=True, betweenCommandDelay=0.0, expectedResponse = True) -> str:
|
|
1418
|
+
"""
|
|
1419
|
+
deprecated:: 2.2.13
|
|
1420
|
+
Use `send_command` instead.
|
|
1421
|
+
"""
|
|
1422
|
+
return self.send_command(cmd, device, sock, not readUntilCursor, not expectedResponse, betweenCommandDelay)
|
|
1423
|
+
|
|
1424
|
+
def sendAndReceiveCmd(self, sock=None, cmd='$help', device='', readUntilCursor=True, betweenCommandDelay=0.0) -> str:
|
|
1425
|
+
"""
|
|
1426
|
+
deprecated:: 2.2.13
|
|
1427
|
+
Use `send_command` instead.
|
|
1428
|
+
"""
|
|
1429
|
+
return self.send_command(cmd, device, sock, not readUntilCursor, no_response_expected=False, command_delay=betweenCommandDelay)
|
|
1430
|
+
|
|
1431
|
+
def streamRunningStatus(self, device: str) -> str:
|
|
1432
|
+
"""
|
|
1433
|
+
deprecated:: 2.2.13
|
|
1434
|
+
Use `stream_running_status` instead.
|
|
1435
|
+
"""
|
|
1436
|
+
return self.stream_running_status(device)
|
|
1437
|
+
|
|
1438
|
+
def streamBufferStatus(self, device: str) -> str:
|
|
1439
|
+
"""
|
|
1440
|
+
deprecated:: 2.2.13
|
|
1441
|
+
Use `stream_buffer_status` instead.
|
|
1442
|
+
"""
|
|
1443
|
+
return self.stream_buffer_status(device)
|
|
1444
|
+
|
|
1445
|
+
def streamHeaderFormat(self, device, sock=None) -> str:
|
|
1446
|
+
"""
|
|
1447
|
+
deprecated:: 2.2.13
|
|
1448
|
+
Use `stream_header_format` instead.
|
|
1449
|
+
"""
|
|
1450
|
+
return self.stream_header_format(device, sock)
|
|
1451
|
+
|
|
1452
|
+
def streamInterrupt(self) -> bool:
|
|
1453
|
+
"""
|
|
1454
|
+
deprecated:: 2.2.13
|
|
1455
|
+
No indication this is used anywhere
|
|
1456
|
+
"""
|
|
1457
|
+
for key in self.deviceDict.keys():
|
|
1458
|
+
if self.deviceDict[key][0]:
|
|
1459
|
+
return True
|
|
1460
|
+
return False
|
|
1461
|
+
|
|
1462
|
+
def interruptList(self):
|
|
1463
|
+
"""
|
|
1464
|
+
deprecated:: 2.2.13
|
|
1465
|
+
No indication this is used anywhere
|
|
1466
|
+
"""
|
|
1467
|
+
streamIssueList = []
|
|
1468
|
+
for key in self.deviceDict.keys():
|
|
1469
|
+
if self.deviceDict[key][0]:
|
|
1470
|
+
streamIssue = [key]
|
|
1471
|
+
streamIssue.append(self.deviceDict[key][1])
|
|
1472
|
+
streamIssue.append(self.deviceDict[key][2])
|
|
1473
|
+
streamIssueList.append(streamIssue)
|
|
1474
|
+
return streamIssueList
|
|
1475
|
+
|
|
1476
|
+
def waitStop(self):
|
|
1477
|
+
"""
|
|
1478
|
+
deprecated:: 2.2.13
|
|
1479
|
+
No indication this is used anywhere
|
|
1480
|
+
"""
|
|
1481
|
+
running = 1
|
|
1482
|
+
while running != 0:
|
|
1483
|
+
threadNameList = []
|
|
1484
|
+
for t1 in threading.enumerate():
|
|
1485
|
+
threadNameList.append(t1.name)
|
|
1486
|
+
running = 0
|
|
1487
|
+
for module in self.deviceList:
|
|
1488
|
+
if (module in threadNameList):
|
|
1489
|
+
running += 1
|
|
1490
|
+
time.sleep(0.5)
|
|
1491
|
+
time.sleep(1)
|
|
1492
|
+
|
|
1493
|
+
def convertStreamAverage (self, streamAveraging):
|
|
1494
|
+
"""
|
|
1495
|
+
deprecated:: 2.2.13
|
|
1496
|
+
No indication this is used anywhere
|
|
1497
|
+
"""
|
|
1498
|
+
returnValue = 32000
|
|
1499
|
+
if ("k" in streamAveraging):
|
|
1500
|
+
returnValue = streamAveraging.replace("k", "000")
|
|
1501
|
+
else:
|
|
1502
|
+
returnValue = streamAveraging
|
|
1503
|
+
|
|
1504
|
+
return returnValue
|
|
1505
|
+
|
|
1506
|
+
def sendAndReceiveText(self, sock, sendText='$help', device='', readUntilCursor=True) -> str:
|
|
1507
|
+
"""
|
|
1508
|
+
deprecated:: 2.2.13
|
|
1509
|
+
Use `send_and_receive_text` instead.
|
|
1510
|
+
"""
|
|
1486
1511
|
return self.send_and_receive_text(sock, send_text=sendText, device=device, read_until_cursor=readUntilCursor)
|