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