quarchpy 2.2.17.dev1__py3-none-any.whl → 2.2.17.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
quarchpy/qps/qpsFuncs.py CHANGED
@@ -1,14 +1,21 @@
1
+ import socket
2
+ from subprocess import CompletedProcess, Popen
1
3
  from threading import Thread, Lock, Event
2
4
  from queue import Queue, Empty
3
5
  import platform
6
+ from typing import Optional, List, Any, Tuple, Union
7
+
8
+ import quarchpy_binaries
4
9
 
5
10
  from quarchpy.install_qps import find_qps
6
11
  from quarchpy.qis import isQisRunning, startLocalQis
7
12
  from quarchpy.connection_specific.connection_QPS import QpsInterface
8
13
  from quarchpy.connection_specific.jdk_jres.fix_permissions import main as fix_permissions, find_java_permissions
14
+ from quarchpy.qis.qisFuncs import isQisRunningAndResponding
9
15
  from quarchpy.user_interface import *
10
16
  import subprocess
11
17
  import logging
18
+
12
19
  logger = logging.getLogger(__name__)
13
20
 
14
21
 
@@ -16,7 +23,7 @@ def isQpsRunning(host='127.0.0.1', port=9822, timeout=0):
16
23
  '''
17
24
  This func will return true if QPS is running with a working QIS connection.
18
25
  '''
19
- myQps=None
26
+ myQps = None
20
27
  logger.debug("Checking if QPS is running")
21
28
  start = time.time()
22
29
  while True:
@@ -32,12 +39,13 @@ def isQpsRunning(host='127.0.0.1', port=9822, timeout=0):
32
39
  logger.debug("QPS is not running")
33
40
  return False
34
41
 
35
- logger.debug("Checking if QPS reports a QIS connection") # "$qis status" returns connected if it has ever had a QIS connection.
36
- answer=0
37
- counter=0
42
+ logger.debug(
43
+ "Checking if QPS reports a QIS connection") # "$qis status" returns connected if it has ever had a QIS connection.
44
+ answer = 0
45
+ counter = 0
38
46
  while True:
39
47
  answer = myQps.sendCmdVerbose(cmd="$qis status")
40
- if answer.lower()=="connected":
48
+ if answer.lower() == "connected":
41
49
  logger.debug("QPS Running With QIS Connected")
42
50
  break
43
51
  else:
@@ -45,7 +53,7 @@ def isQpsRunning(host='127.0.0.1', port=9822, timeout=0):
45
53
  time.sleep(0.5)
46
54
  counter += 1
47
55
  if counter > 5:
48
- logger.debug("QPS Running QIS NOT found after "+str(counter)+" attempts.")
56
+ logger.debug("QPS Running QIS NOT found after " + str(counter) + " attempts.")
49
57
  return False
50
58
 
51
59
  logger.debug("Checking if QPS/QIS comms are running")
@@ -69,124 +77,88 @@ def isQpsRunning(host='127.0.0.1', port=9822, timeout=0):
69
77
  return False
70
78
 
71
79
 
72
- def startLocalQps(keepQisRunning=False, args=[], timeout=30, startQPSMinimised=True):
73
-
80
+ def startLocalQps(
81
+ keepQisRunning: bool = False,
82
+ args: Optional[List[str]] = [],
83
+ timeout: int = 30,
84
+ startQPSMinimised: bool = True,
85
+ host: str = '127.0.0.1',
86
+ port: int = 9822,
87
+ qis_port: int = 9722,
88
+ qis_rest_port: int = 9780
89
+ ) -> Optional['QpsInterface']:
90
+ """
91
+ Main entry point to start a local QPS instance.
92
+ """
93
+ # 1. Prepare Arguments
94
+ # We copy the list to avoid modifying the input in place
95
+ launch_args = args.copy()
96
+ if not launch_args:
97
+ launch_args = ""
98
+
99
+ # Sync 'port' arg to CLI flags if non-default
100
+ if port != 9822:
101
+ # Only add if not already manually present in args
102
+ if not any("-port=" in arg for arg in launch_args):
103
+ launch_args.append(f"-port={port}")
104
+
105
+ # Sync 'qisport' arg to CLI flags if non-default
106
+ if qis_port != 9722:
107
+ if not any("-qisport=" in arg for arg in launch_args):
108
+ launch_args.append(f"-qisport={qis_port}")
109
+
110
+ # Sync 'qisrestport' arg to CLI flags if non-default
111
+ if qis_rest_port != 9780:
112
+ if not any("-qisrestport=" in arg for arg in launch_args):
113
+ launch_args.append(f"-qisrestport={qis_port}")
114
+
115
+ # 2. Check if already running on the specific target port
116
+ if _check_port_open(host, port):
117
+ logger.debug(f"QPS instance on port {port} is already running. Connecting...")
118
+ return QpsInterface(host=host, port=port)
119
+
120
+ # 3. Check for QPS installation
74
121
  if not find_qps():
75
122
  logger.error("Unable to find or install QPS... Aborting...")
76
- return
123
+ return None
77
124
 
125
+ # 4. Handle QIS Backend (if required)
78
126
  if keepQisRunning:
79
- if not isQisRunning():
80
- startLocalQis()
127
+ if not _ensure_qis_running(host, qis_port, qis_rest_port, timeout):
128
+ return None
81
129
 
82
- if args.__len__() !=0:
83
- args = " ".join(args)
84
- else:
85
- args=" "
86
- if startQPSMinimised == True:
87
- if "-ccs" not in args.lower():
88
- args +=" -ccs=MIN"
130
+ # 5. Prepare Command and Environment
131
+ command, qps_dir = _prepare_qps_launch_env(launch_args, startQPSMinimised)
132
+ if not command:
133
+ return None
89
134
 
90
- # Record current working directory
135
+ # 6. Launch QPS Process
91
136
  current_dir = os.getcwd()
92
137
 
93
- # JRE path
94
- java_path = os.path.dirname(os.path.abspath(__file__))
95
- java_path, junk = os.path.split(java_path)
96
- java_path = os.path.join(java_path, "connection_specific", "jdk_jres")
97
- java_path = "\"" + java_path
98
- # Start to build the path towards qps.jar
99
- qps_path = os.path.dirname(os.path.abspath(__file__))
100
- qps_path, junk = os.path.split(qps_path)
101
-
102
- # Check the current OS
103
- current_os = platform.system()
104
- current_arch = platform.machine()
105
- current_arch = current_arch.lower() # ensure comparing same case
106
-
107
- # Currently officially unsupported
108
- if (current_os in "Linux" and current_arch == "aarch64") or (current_os in "Darwin" and current_arch == "arm64"):
109
- logger.warning("The system [" + current_os + ", " + current_arch + "] is not officially supported.")
110
- logger.warning("Please contact Quarch support for running QuarchPy on this system.")
111
- return
112
-
113
- # ensure the jres folder has the required permissions
114
- permissions, message = find_java_permissions()
115
- if permissions is False:
116
- logger.warning(message)
117
- logger.warning("Not having correct permissions will prevent Quarch Java Programs from launching.")
118
- logger.warning("Run \"python -m quarchpy.run permission_fix\" to fix this.")
119
- user_input = input("Would you like to use auto run this now? (Y/N)")
120
- if user_input.lower() == "y":
121
- fix_permissions()
122
- permissions, message = find_java_permissions()
123
- time.sleep(0.5)
124
- if permissions is False:
125
- logger.warning("Attempt to fix permissions was unsuccessful. Please fix manually.")
126
- else:
127
- logger.warning("Attempt to fix permissions was successful. Now continuing.")
128
-
129
-
130
- qps_path = os.path.join(qps_path, "connection_specific", "QPS", "qps.jar")
131
-
132
-
133
- # Change the working directory to the directory containing qps.jar
134
- os.chdir(os.path.dirname(qps_path))
135
-
136
-
137
- # OS dependency
138
- if current_os in "Windows":
139
- command = java_path + "\\win_amd64_jdk_jre\\bin\\java\" -jar qps.jar " + str(args)
140
- elif current_os in "Linux" and current_arch == "x86_64":
141
- command = java_path + "/lin_amd64_jdk_jre/bin/java\" -jar qps.jar " + str(args)
142
- elif current_os in "Linux" and current_arch == "aarch64":
143
- command = java_path + "/lin_arm64_jdk_jre/bin/java\" -jar qps.jar " + str(args)
144
- elif current_os in "Darwin" and current_arch == "x86_64":
145
- command = java_path + "/mac_amd64_jdk_jre/bin/java\" -jar qps.jar " + str(args)
146
- elif current_os in "Darwin" and current_arch == "arm64":
147
- command = java_path + "/mac_arm64_jdk_jre/bin/java\" -jar qps.jar " + str(args)
148
- else: # default to windows
149
- command = java_path + "\\win_amd64_jdk_jre\\bin\\java\" -jar qps.jar " + str(args)
150
-
151
138
  if isQpsRunning():
152
139
  logger.debug("QPS is already running. Not starting another instance.")
153
140
  os.chdir(current_dir)
154
- return
155
- if "-logging=ON" in str(args): #If logging to a terminal window is on then os.system should be used to keep a window open to view logging.
156
- if current_os in "Windows":
157
- process = subprocess.Popen(command,shell=True)
158
- else:
159
- # Add a hold command to keep the terminal open (useful for bash)
160
- command_with_pause = command + "; exec bash"
161
- process = subprocess.run(command_with_pause, shell=True)
162
- else:
163
- if sys.version_info[0] < 3:
164
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
165
- else:
166
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
141
+ return None
167
142
 
168
- startTime = time.time()
169
- while not isQpsRunning():
170
- time.sleep(0.2)
171
- _get_std_msg_and_err_from_QPS_process(process)
172
- if time.time() - startTime > timeout:
173
- os.chdir(current_dir)
174
- raise TimeoutError("QPS failed to launch within timelimit of " + str(timeout) + " sec.")
175
- logger.debug("QPS detected after " + str(time.time() - startTime) + "s")
143
+ try:
144
+ os.chdir(qps_dir) # Switch to QPS dir for launch dependencies
145
+ process = _launch_process(command, args)
146
+ finally:
147
+ os.chdir(current_dir) # Always return to original dir
176
148
 
177
- while not isQisRunning():
178
- if time.time() - startTime > timeout:
179
- raise TimeoutError(
180
- "QPS did launch but QIS did not respond during the timeout time of " + str(timeout) + " sec.")
181
- time.sleep(0.2)
182
- logger.debug("QIS detected after " + str(time.time() - startTime) + "s")
149
+ # 7. Wait for QPS to be ready
150
+ if not _wait_for_service(host, port, timeout, process, args):
151
+ return None
183
152
 
184
- # return current working directory
185
- os.chdir(current_dir)
186
- return
153
+ # 8. Return Connected Interface
154
+ try:
155
+ return QpsInterface(host=host, port=port)
156
+ except Exception as e:
157
+ logger.error(f"QPS started, but failed to create interface object: {e}")
158
+ return None
187
159
 
188
160
 
189
- def reader(stream, q, source, lock,stop_flag):
161
+ def reader(stream, q, source, lock, stop_flag):
190
162
  '''
191
163
  Used to read output and place it in a queue for multithreaded reading
192
164
  :param stream:
@@ -222,7 +194,7 @@ def _get_std_msg_and_err_from_QPS_process(process):
222
194
  t2.start()
223
195
  counter = 0
224
196
  # check for stderr or stdmsg from the queue
225
- while counter <= 3: # If 3 empty reads from the queue then move on to see if QPS is running.
197
+ while counter <= 3: # If 3 empty reads from the queue then move on to see if QPS is running.
226
198
  try:
227
199
  source, line = q.get(timeout=1) # Wait for 1 second for new lines
228
200
  counter = 0
@@ -232,73 +204,197 @@ def _get_std_msg_and_err_from_QPS_process(process):
232
204
  printText(f"{source}: {line}")
233
205
  except Empty:
234
206
  counter += 1
235
- stop_flag.set() #Close the threads and return to the main loop where QPS is check to see if its started yet
207
+ stop_flag.set() #Close the threads and return to the main loop where QPS is check to see if its started yet
236
208
 
237
209
 
238
210
  def closeQps(host='127.0.0.1', port=9822):
239
211
  myQps = QpsInterface(host, port)
240
212
  myQps.sendCmdVerbose("$shutdown")
241
213
  del myQps
242
- time.sleep(1) #needed as calling "isQpsRunning()" will throw an error if it ties to connect while shutdown is in progress.
214
+ time.sleep(
215
+ 1) #needed as calling "isQpsRunning()" will throw an error if it ties to connect while shutdown is in progress.
216
+
217
+
218
+ def GetQpsModuleSelection(QpsConnection: 'QpsInterface', favouriteOnly=True,
219
+ additionalOptions=['rescan', 'all con types', 'ip scan'], scan=True):
220
+ """
221
+ Deprecated: use QpsInterface.get_module_selection instead.
222
+ This function will return a module selection list from QPS.
223
+
224
+ """
225
+ return (
226
+ QpsConnection.get_qps_module_selection(preferred_connection_only=favouriteOnly,
227
+ additional_options=additionalOptions, scan=scan)
228
+ )
229
+
230
+
231
+ # ==========================================
232
+ # HELPER FUNCTIONS
233
+ # ==========================================
234
+
235
+ def _parse_ports(args: List[str]) -> Tuple[int, int, int]:
236
+ """Extracts QPS and QIS ports from the argument list."""
237
+ qps_port = 9822
238
+ qis_port = 9722
239
+ qis_rest_port = 9780
240
+
241
+ for arg in args:
242
+ arg_lower = arg.lower()
243
+ if "-port=" in arg_lower:
244
+ try:
245
+ qps_port = int(arg.split('=')[1])
246
+ except (IndexError, ValueError):
247
+ pass
248
+ elif "-qisport=" in arg_lower:
249
+ try:
250
+ qis_port = int(arg.split('=')[1])
251
+ except (IndexError, ValueError):
252
+ pass
253
+ elif "-qisrestport=" in arg_lower:
254
+ try:
255
+ qis_rest_port = int(arg.split('=')[1])
256
+ except (IndexError, ValueError):
257
+ pass
258
+
259
+ return qps_port, qis_port, qis_rest_port
260
+
261
+
262
+ def _ensure_qis_running(host: str, qis_port: int, qis_rest_port: int, timeout: int) -> bool:
263
+ """Checks if QIS is running on the target port, starts it if not."""
264
+ if _check_port_open(host, qis_port):
265
+ return True
243
266
 
244
- def GetQpsModuleSelection(QpsConnection, favouriteOnly=True, additionalOptions=['rescan', 'all con types', 'ip scan'], scan=True):
245
- favourite = favouriteOnly
246
- ip_address = None
247
- while True:
248
- printText("QPS scanning for devices")
249
- tableHeaders = ["Module"]
250
- # Request a list of all USB and LAN accessible power modules
251
- if ip_address == None:
252
- devList = QpsConnection.getDeviceList(scan=scan)
253
- else:
254
- devList = QpsConnection.getDeviceList(scan=scan, ipAddress=ip_address)
255
- if "no device" in devList[0].lower() or "no module" in devList[0].lower():
256
- favourite = False # If no device found conPref wont match and will bugout
257
-
258
- # Removes rest devices
259
- devList = [x for x in devList if "rest" not in x]
260
- message = "Select a quarch module"
261
-
262
- if (favourite):
263
- index = 0
264
- sortedDevList = []
265
- conPref = ["USB", "TCP", "SERIAL", "REST", "TELNET"]
266
- while len(sortedDevList) < len(devList):
267
- for device in devList:
268
- if conPref[index] in device.upper():
269
- sortedDevList.append(device)
270
- index += 1
271
- devList = sortedDevList
272
-
273
- # new dictionary only containing one favourite connection to each device.
274
- favConDevList = []
275
- index = 0
276
- for device in sortedDevList:
277
- if (favConDevList == [] or not device.split("::")[1] in str(favConDevList)):
278
- favConDevList.append(device)
279
- devList = favConDevList
280
-
281
- if User_interface.instance != None and User_interface.instance.selectedInterface == "testcenter":
282
- tempString = ""
283
- for module in devList:
284
- tempString+=module+"="+module+","
285
- devList = tempString[0:-1]
286
-
287
-
288
- myDeviceID = listSelection(title=message, message=message, selectionList=devList,
289
- additionalOptions=additionalOptions, nice=True, tableHeaders=tableHeaders, indexReq=True)
290
-
291
- if myDeviceID in 'rescan':
292
- ip_address = None
293
- favourite = True
294
- continue
295
- elif myDeviceID in 'all con types':
296
- printText('Displaying all conection types...')
297
- favourite = False
298
- continue
299
- elif myDeviceID in 'ip scan':
300
- ip_address = requestDialog("Please input IP Address of the module you would like to connect to: ")
301
- favourite = False
302
- continue
267
+ logger.debug(f"Starting QIS on ports {qis_port}/{qis_rest_port}...")
268
+ qis_args = [f'-port={qis_port}', f'-restport={qis_rest_port}']
269
+ startLocalQis(args=qis_args)
270
+
271
+ # Wait for QIS
272
+ start_time = time.time()
273
+ while not _check_port_open(host, qis_port):
274
+ if time.time() - start_time > timeout:
275
+ logger.error(f"QIS failed to start on port {qis_port} within timeout.")
276
+ return False
277
+ time.sleep(0.5)
278
+
279
+ while isQisRunningAndResponding():
280
+ time.sleep(0.5)
281
+
282
+ return True
283
+
284
+
285
+ def _prepare_qps_launch_env(args: List[str], startQPSMinimised: bool) -> Tuple[Optional[str], Optional[str]]:
286
+ """Resolves paths using quarchpy_binaries, checks permissions, and builds command."""
287
+
288
+ # 1. Check OS Support
289
+ current_os = platform.system()
290
+ current_arch = platform.machine().lower()
291
+
292
+ # 2. Handle Permissions
293
+ _handle_java_permissions()
294
+
295
+ # 3. Resolve Paths using quarchpy_binaries
296
+ if 'quarchpy_binaries' not in globals() and 'quarchpy_binaries' not in locals():
297
+ # Fallback if module isn't imported or available
298
+ logger.error("quarchpy_binaries module not found. Cannot locate JRE.")
299
+ return None, None
300
+
301
+ try:
302
+ java_home = quarchpy_binaries.get_jre_home()
303
+ except Exception as e:
304
+ logger.error(f"Failed to get JRE home: {e}")
305
+ return None, None
306
+
307
+ # Resolve QPS Jar Path
308
+ qps_root = os.path.dirname(os.path.abspath(__file__))
309
+ qps_root, _ = os.path.split(qps_root) # Up one level
310
+ qps_jar_path = os.path.join(qps_root, "connection_specific", "QPS", "qps.jar")
311
+ qps_dir = os.path.dirname(qps_jar_path)
312
+
313
+ # 4. Construct Command
314
+ # Determine separator based on OS (Windows uses \, Linux/Mac use /)
315
+
316
+ java_bin = "bin/java"
317
+ if current_os == "Windows":
318
+ java_bin = r"bin\java"
319
+
320
+ # Full path to java executable
321
+ java_exe = os.path.join(java_home, java_bin)
322
+
323
+ # Wrap java path in quotes for safety
324
+ java_exe_quoted = f'"{java_exe}"'
325
+
326
+ # Prepare Args String
327
+ args_str = " ".join(args) if args else " "
328
+ if startQPSMinimised and "-ccs" not in args_str.lower():
329
+ args_str += " -ccs=MIN"
330
+
331
+ # Build Final Command
332
+ command = f'{java_exe_quoted} -jar qps.jar {args_str}'
333
+
334
+ return command, qps_dir
335
+
336
+
337
+ def _handle_java_permissions() -> None:
338
+ """Checks and attempts to fix Java execution permissions."""
339
+ permissions, message = find_java_permissions()
340
+ if not permissions:
341
+ logger.warning(message)
342
+ printText("Not having correct permissions will prevent Quarch Java Programs from launching.")
343
+ printText('Run "python -m quarchpy.run permission_fix" to fix this.')
344
+ printText("Would you like quarchpy to attempt to fix the permissions now? (Y/N)")
345
+ try:
346
+ user_input = requestDialog(">>> ")
347
+ except EOFError:
348
+ user_input = "N"
349
+ if user_input.strip().lower() in ['y', 'yes']:
350
+ fix_permissions()
351
+
352
+
353
+ def _launch_process(command: str, args: List[str]) -> Union[Popen, CompletedProcess]:
354
+ """Launches the subprocess, handling logging flags."""
355
+ args_str = " ".join(args) if args else ""
356
+
357
+ if "-logconsole=ON" in args_str:
358
+ if platform.system() == "Windows":
359
+ return subprocess.Popen(command, shell=True)
303
360
  else:
304
- return myDeviceID
361
+ return subprocess.run(command + "; exec bash", shell=True)
362
+ else:
363
+ # Use text=True for Python 3.7+
364
+ text_mode = True if sys.version_info >= (3, 7) else False
365
+ # Fallback for 3.6 if needed (universal_newlines=True)
366
+ return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=text_mode, shell=True)
367
+
368
+
369
+ def _wait_for_service(host: str, port: int, timeout: int, process: Optional[subprocess.Popen], args: List[str]) -> bool:
370
+ """Polls the port until open, checking process output for errors."""
371
+ start_time = time.time()
372
+ args_str = " ".join(args) if args else ""
373
+ logging_on = "-logconsole=ON" in args_str
374
+
375
+ while True:
376
+ if _check_port_open(host, port):
377
+ logger.debug(f"QPS detected on port {port} after {time.time() - start_time:.2f}s")
378
+ return True
379
+
380
+ # If hidden, drain pipes to prevent deadlock and check for crashes
381
+ if not logging_on and process:
382
+ _get_std_msg_and_err_from_QPS_process(process)
383
+
384
+ if time.time() - start_time > timeout:
385
+ logger.error(f"QPS failed to launch on port {port} within timelimit of {timeout} sec.")
386
+ return False
387
+
388
+ time.sleep(0.2)
389
+
390
+
391
+ def _check_port_open(host: str, port: int) -> bool:
392
+ """Simple TCP connect check."""
393
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
394
+ s.settimeout(1)
395
+ try:
396
+ s.connect((host, int(port)))
397
+ s.close()
398
+ return True
399
+ except (socket.timeout, ConnectionRefusedError, OSError):
400
+ return False
quarchpy/run.py CHANGED
@@ -240,7 +240,7 @@ def _run_qps_function(args=[]):
240
240
  printText("QPS is not running")
241
241
 
242
242
  if not shutdown:
243
- startLocalQps(args=args)
243
+ startLocalQps(args=args, startQPSMinimised=False)
244
244
 
245
245
 
246
246
  def _run_calibration_function(args=[]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quarchpy
3
- Version: 2.2.17.dev1
3
+ Version: 2.2.17.dev3
4
4
  Summary: This packpage offers Python support for Quarch Technology modules.
5
5
  Author: Quarch Technology ltd
6
6
  Author-email: support@quarch.com
@@ -18,6 +18,7 @@ Classifier: Topic :: System
18
18
  Classifier: Topic :: System :: Power (UPS)
19
19
  Requires-Python: >=3.7
20
20
  Description-Content-Type: text/x-rst
21
+ License-File: LICENSE.txt
21
22
  Requires-Dist: zeroconf>=0.23.0
22
23
  Requires-Dist: numpy
23
24
  Requires-Dist: pandas
@@ -25,6 +26,8 @@ Requires-Dist: requests
25
26
  Requires-Dist: packaging
26
27
  Requires-Dist: quarchpy-binaries
27
28
  Requires-Dist: typing-extensions
29
+ Requires-Dist: libusb-package
30
+ Requires-Dist: telnetlib-313-and-up; python_version >= "3.13"
28
31
  Dynamic: author
29
32
  Dynamic: author-email
30
33
  Dynamic: classifier
@@ -32,6 +35,7 @@ Dynamic: description
32
35
  Dynamic: description-content-type
33
36
  Dynamic: keywords
34
37
  Dynamic: license
38
+ Dynamic: license-file
35
39
  Dynamic: requires-dist
36
40
  Dynamic: requires-python
37
41
  Dynamic: summary