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/device/device.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from typing import Optional, Tuple, Any, Union # Added for type hinting in docstrings
3
3
  import logging
4
- logger = logging.getLogger(__name__)
5
4
  import os
6
5
  import re
7
6
  import sys
@@ -9,8 +8,9 @@ import time
9
8
 
10
9
  from quarchpy.qps import isQpsRunning
11
10
  from quarchpy.qis import isQisRunning
12
-
13
11
  from quarchpy.connection import QISConnection, PYConnection, QPSConnection
12
+
13
+ logger = logging.getLogger(__name__)
14
14
  # Check Python version and set timeout exception
15
15
  if sys.version_info.major == 2:
16
16
  try:
@@ -23,6 +23,14 @@ if sys.version_info.major == 2:
23
23
  else:
24
24
  timeout_exception = TimeoutError # Python 3: Use built-in TimeoutError
25
25
 
26
+ class _InstanceWrapper:
27
+ """Helper class to wrap external instances to match expected connectionObj structure."""
28
+ def __init__(self, interface_obj, mode: str):
29
+ if mode == "QPS":
30
+ self.qps = interface_obj
31
+ elif mode == "QIS":
32
+ self.qis = interface_obj
33
+
26
34
  # --- Main Device Class ---
27
35
  class quarchDevice:
28
36
  """
@@ -43,19 +51,26 @@ class quarchDevice:
43
51
  connectionTypeName (Optional[str]): Alias for ConCommsType. Set for PY type.
44
52
  """
45
53
 
46
- def __init__(self, ConString: str, ConType: str = "PY", timeout: str = "5", host=None, port=None):
54
+ def __init__(self,
55
+ ConString: str,
56
+ ConType: str = "PY",
57
+ timeout: str = "5",
58
+ qps_instance: Optional['QpsInterface'] = None,
59
+ qis_instance: Optional['QisInterface'] = None):
47
60
  """
48
61
  Initializes the quarchDevice, establishes the connection.
49
62
 
50
63
  Performs initial parameter validation, determines the connection type,
51
64
  delegates to specific helper methods to create the underlying connection
52
65
  object (PYConnection, QISConnection, or QPSConnection), and verifies
53
- the connection.
66
+ the connection. Supports injection of existing QIS/QPS instances.
54
67
 
55
68
  Args:
56
69
  ConString (str): The connection string (e.g., "USB:ID", "TCP:IP", "QIS:ID").
57
70
  ConType (str, optional): The connection mode ('PY', 'QIS', 'QPS'). Defaults to "PY".
58
71
  timeout (str, optional): Communication timeout in seconds. Defaults to "5".
72
+ qps_instance (Optional[QpsInterface], optional): An existing QPS instance to use. Defaults to None.
73
+ qis_instance (Optional[QisInterface], optional): An existing QIS instance to use. Defaults to None.
59
74
 
60
75
  Raises:
61
76
  ValueError: If ConString format is invalid or timeout is not numeric.
@@ -74,6 +89,12 @@ class quarchDevice:
74
89
  self.timeout = 5 # Default int timeout
75
90
  self.is_module_resetting = False
76
91
 
92
+ # Determine ConType based on provided instances
93
+ if qps_instance:
94
+ self.ConType = "QPS"
95
+ if qis_instance:
96
+ self.ConType = "QIS"
97
+
77
98
  # Call helper to store and validate parameters
78
99
  self._store_and_validate_params(ConString, ConType, timeout)
79
100
 
@@ -84,9 +105,9 @@ class quarchDevice:
84
105
  if con_type_upper == "PY":
85
106
  self._initialize_py_connection()
86
107
  elif con_type_upper.startswith("QIS"):
87
- self._initialize_qis_connection()
108
+ self._initialize_qis_connection(existing_instance=qis_instance)
88
109
  elif con_type_upper.startswith("QPS"):
89
- self._initialize_qps_connection()
110
+ self._initialize_qps_connection(existing_instance=qps_instance)
90
111
  else:
91
112
  # Invalid ConType should have been caught by check_module_format
92
113
  raise ValueError(f"Invalid connection type '{self.ConType}'.")
@@ -401,7 +422,7 @@ class quarchDevice:
401
422
  logger.warning(f"scanIP method not found on {server_type} connection object.")
402
423
  return None # Cannot scan
403
424
 
404
- scan_response = scan_method(target_ip) # Scan using the target IP
425
+ scan_response = scan_method(server_conn_obj, target_ip) # Scan using the target IP
405
426
  if "located" not in str(scan_response).lower():
406
427
  logger.debug(f"{server_type} scan for {target_ip} did not locate the device.")
407
428
  return None # Scan didn't find it
@@ -442,13 +463,16 @@ class quarchDevice:
442
463
  return True
443
464
  return False
444
465
 
445
- def _initialize_qis_connection(self):
466
+ def _initialize_qis_connection(self, existing_instance: Optional['QisInterface'] = None):
446
467
  """
447
468
  Initializes the connection using the QIS method.
448
469
 
449
470
  Parses host/port, prepares connection string, creates QISConnection object,
450
471
  verifies the device presence on the server using the common helper, and
451
- sets the device as default on the QIS server.
472
+ sets the device as default on the QIS server. Also supports injection of an existing QIS instance.
473
+
474
+ Args:
475
+ qis_instance (Optional[QisInterface], optional): An existing QIS instance to use. Defaults to None.
452
476
 
453
477
  Sets:
454
478
  self.connectionObj, self.ConString.
@@ -459,27 +483,33 @@ class quarchDevice:
459
483
  ImportError: If QISConnection class is missing.
460
484
  """
461
485
  logger.debug("Attempting QIS connection...")
462
- host, port = self._parse_server_details(default_port=9722)
463
486
  self._prepare_server_con_string()
464
487
 
465
- # Create QISConnection object
466
- try:
467
- # Assumes QISConnection is imported
468
- self.connectionObj = QISConnection(self.ConString, host, port)
469
- logger.debug(f"QISConnection object created for '{self.ConString}' via {host}:{port}")
470
- except Exception as e_qisconn:
471
- logger.error(f"Failed to create QISConnection: {e_qisconn}", exc_info=True)
472
- raise ConnectionError("Failed to establish QIS connection.") from e_qisconn
473
-
474
- # Verify device presence on the QIS server
488
+ if existing_instance:
489
+ # Extract details directly from the provided instance
490
+ host = getattr(existing_instance, 'host', 'unknown')
491
+ port = getattr(existing_instance, 'port', 'unknown')
492
+
493
+ # Wrap it to match self.connectionObj.qis structure
494
+ self.connectionObj = _InstanceWrapper(existing_instance, "QIS")
495
+ logger.debug(f"Using provided QIS instance for '{self.ConString}' at {host}:{port}")
496
+ else:
497
+ # Legacy creation
498
+ host, port = self._parse_server_details(default_port=9722)
499
+ try:
500
+ self.connectionObj = QISConnection(self.ConString, host, port)
501
+ logger.debug(f"QISConnection object created for '{self.ConString}' via {host}:{port}")
502
+ except Exception as e_qisconn:
503
+ raise ConnectionError("Failed to establish QIS connection.") from e_qisconn
504
+
505
+ # Verify device presence (uses the connection object we just set up)
475
506
  try:
476
- # Pass the QIS-specific sub-object (self.connectionObj.qis) to the helper
477
507
  self._verify_server_device(self.connectionObj.qis, "QIS")
478
508
  except TimeoutError as e_timeout:
479
- self.close_connection() # Close object if verification failed
480
- raise e_timeout # Re-raise timeout
509
+ self.close_connection()
510
+ raise e_timeout
481
511
  except Exception as e_qis_conn:
482
- self.close_connection() # Close object if verification failed
512
+ self.close_connection()
483
513
  raise ConnectionError(f"Failed QIS device verification: {e_qis_conn}") from e_qis_conn
484
514
 
485
515
  # Set QIS default device
@@ -493,13 +523,18 @@ class quarchDevice:
493
523
  logger.warning(f"QIS command '$default {self.ConString}' failed.")
494
524
  except Exception as e_def:
495
525
  logger.warning(f"Error setting QIS default device: {e_def}")
526
+ pass
496
527
 
497
- def _initialize_qps_connection(self, host=None, port=None):
528
+ def _initialize_qps_connection(self, existing_instance: Optional['QpsInterface'] = None):
498
529
  """
499
530
  Initializes the connection using the QPS method.
500
531
 
501
532
  Parses host/port, prepares connection string, creates QPSConnection object,
502
533
  and verifies the device presence on the server using the common helper.
534
+ Also supports injection of an existing QPS instance.
535
+
536
+ Args:
537
+ existing_instance (Optional[QpsInterface], optional): An existing QPS instance to use. Defaults to None.
503
538
 
504
539
  Sets:
505
540
  self.connectionObj, self.ConString (potentially updated).
@@ -510,29 +545,34 @@ class quarchDevice:
510
545
  ImportError: If QPSConnection class is missing.
511
546
  """
512
547
  logger.debug("Attempting QPS connection...")
513
- host, port = self._parse_server_details(default_port=9822) # type: ignore[misc] # Private call ok
514
- self._prepare_server_con_string() # type: ignore[misc] # Private call ok
548
+ self._prepare_server_con_string()
515
549
 
516
- # Create QPSConnection object
517
- try:
518
- # Assumes QPSConnection is imported
519
- self.connectionObj = QPSConnection(host, port)
520
- logger.debug(f"QPSConnection object created via {host}:{port}")
521
- except Exception as e_qpsconn:
522
- logger.error(f"Failed to create QPSConnection: {e_qpsconn}", exc_info=True)
523
- raise ConnectionError("Failed to establish QPS connection.") from e_qpsconn
524
-
525
- # Verify device presence on the QPS server
550
+ if existing_instance:
551
+ # Extract details directly from the provided instance
552
+ host = getattr(existing_instance, 'host', 'unknown')
553
+ port = getattr(existing_instance, 'port', 'unknown')
554
+
555
+ # Wrap it to match self.connectionObj.qps structure
556
+ self.connectionObj = _InstanceWrapper(existing_instance, "QPS")
557
+ logger.debug(f"Using provided QPS instance for '{self.ConString}' at {host}:{port}")
558
+ else:
559
+ # Legacy creation
560
+ host, port = self._parse_server_details(default_port=9822)
561
+ try:
562
+ self.connectionObj = QPSConnection(host, port)
563
+ logger.debug(f"QPSConnection object created via {host}:{port}")
564
+ except Exception as e_qpsconn:
565
+ raise ConnectionError("Failed to establish QPS connection.") from e_qpsconn
566
+
567
+ # Verify device presence
526
568
  try:
527
- # Pass the QPS-specific sub-object (self.connectionObj.qps) to the helper
528
- self._verify_server_device(self.connectionObj.qps, "QPS") # type: ignore[misc] # Private call ok
569
+ self._verify_server_device(self.connectionObj.qps, "QPS")
529
570
  except TimeoutError as e_timeout:
530
- self.close_connection() # Close object if verification failed
531
- raise e_timeout # Re-raise timeout
571
+ self.close_connection()
572
+ raise e_timeout
532
573
  except Exception as e_qps_conn:
533
- self.close_connection() # Close object if verification failed
574
+ self.close_connection()
534
575
  raise ConnectionError(f"Failed QPS device verification: {e_qps_conn}") from e_qps_conn
535
- # QPS typically doesn't use/need a '$default' command
536
576
 
537
577
  def _verify_connection_object(self):
538
578
  """
@@ -1254,7 +1294,11 @@ def checkModuleFormat(ConString: str) -> bool:
1254
1294
 
1255
1295
 
1256
1296
  # --- getQuarchDevice / get_quarch_device ---
1257
- def get_quarch_device(connectionTarget: str, ConType: str = "PY", timeout: str = "5") -> 'Union[quarchDevice, subDevice]':
1297
+ def get_quarch_device(connectionTarget: str,
1298
+ ConType: str = "PY",
1299
+ timeout: str = "5",
1300
+ qps_instance: Optional['QpsInterface'] = None,
1301
+ qis_instance: Optional['QisInterface'] = None) -> 'Union[quarchDevice, subDevice]':
1258
1302
  """
1259
1303
  Creates and returns a quarchDevice or subDevice instance.
1260
1304
 
@@ -1271,6 +1315,10 @@ def get_quarch_device(connectionTarget: str, ConType: str = "PY", timeout: str =
1271
1315
  currently defaults to "PY" internally based on original logic.
1272
1316
  timeout (str, optional): The connection timeout in seconds as a string.
1273
1317
  Defaults to "5".
1318
+ qps_instance (Optional[QpsInterface], optional): An optional QpsInterface instance
1319
+ to use for QPS connections. Defaults to None.
1320
+ qis_instance (Optional[QisInterface], optional): An optional QisInterface instance
1321
+ to use for QIS connections. Defaults to None.
1274
1322
 
1275
1323
  Returns:
1276
1324
  quarchDevice | subDevice | Any: An instance representing the connected device.
@@ -1328,7 +1376,7 @@ def get_quarch_device(connectionTarget: str, ConType: str = "PY", timeout: str =
1328
1376
  # Standard device connection
1329
1377
  logger.debug(f"Standard device connection for: {connectionTarget}")
1330
1378
  # Use passed ConType and timeout
1331
- myDevice = quarchDevice(connectionTarget, ConType=ConType, timeout=timeout)
1379
+ myDevice = quarchDevice(connectionTarget, ConType=ConType, timeout=timeout, qps_instance=qps_instance, qis_instance=qis_instance)
1332
1380
  logger.info(f"Successfully connected to standard device: {connectionTarget}")
1333
1381
 
1334
1382
  return myDevice
@@ -249,25 +249,40 @@ def list_network(target_conn="all", debugPrint=False, lanTimeout=1, ipAddressLoo
249
249
  network_modules = {}
250
250
  msg_received = None
251
251
  counter += 1
252
- # Receive raw message until timeout, then break.
252
+
253
253
  try:
254
+ # recvfrom returns a tuple: (bytes_data, (address, port))
254
255
  msg_received = mySocket.recvfrom(1024)
255
256
  except Exception as e:
256
257
  logger.debug(str(e))
257
- # check if any a device was targeted directly and allow parse
258
258
  if specifiedDevice is not None:
259
259
  msg_received = specifiedDevice
260
260
  specifiedDevice = None
261
261
  else:
262
262
  break
263
- cont = 0
264
263
 
265
- # Used split \r\n since values of 13 or 10 were looked at as /r and /n when using splitlines
266
- # This fixes for all cases except if 13 is followed by 10.
267
- splits = msg_received[0].split(b"\r\n")
268
- del splits[-1]
264
+ # --- ROBUST DATA EXTRACTION ---
265
+ # If msg_received is a tuple (data, addr), split them out
266
+ if isinstance(msg_received, tuple) and len(msg_received) >= 2:
267
+ raw_payload = msg_received[0]
268
+ addr_info = msg_received[1] # This is (ip, port)
269
+ else:
270
+ # Fallback if data is raw bytes or something else
271
+ raw_payload = msg_received
272
+ addr_info = ("0.0.0.0", 0)
273
+
274
+ # Ensure we have bytes to split
275
+ if isinstance(raw_payload, bytes):
276
+ splits = raw_payload.split(b"\r\n")
277
+ else:
278
+ continue # Skip this iteration if we don't have valid data
279
+
280
+ # Remove empty trailing element if it exists
281
+ if splits and splits[-1] == b"":
282
+ del splits[-1]
283
+ # ------------------------------
269
284
 
270
- # Decode the discovered device and extend the provided discovered_devices list if required
285
+ # Decode and extend discovered_devices
271
286
  if discovered_devices is not None:
272
287
  idn_info = IDNInfo()
273
288
  fix_idn_info = FixtureIDNInfo()
@@ -277,18 +292,23 @@ def list_network(target_conn="all", debugPrint=False, lanTimeout=1, ipAddressLoo
277
292
  discovered_device.populate_device_info()
278
293
  discovered_devices.append(discovered_device)
279
294
 
295
+ cont = 0
280
296
  for lines in splits:
281
297
  if cont <= 1:
282
298
  index = cont
283
299
  data = repr(lines).replace("'", "").replace("b", "")
284
300
  cont += 1
285
301
  else:
286
- index = repr(lines[0]).replace("'", "")
302
+ # lines[0] returns an int, so we use slicing lines[0:1] to keep it bytes
303
+ index = repr(lines[0:1]).replace("'", "").replace("b", "")
287
304
  data = repr(lines[1:]).replace("'", "").replace("b", "")
288
305
  network_modules[index] = data
306
+
289
307
  module_name = get_user_level_serial_number(network_modules)
290
- logger.debug("Found UDP response: " + module_name)
291
- ip_module = msg_received[1][0].strip()
308
+ logger.debug("Found UDP response: " + str(module_name))
309
+
310
+ # FIXED: Safely get the IP from our extracted addr_info
311
+ ip_module = addr_info[0].strip() if isinstance(addr_info, (tuple, list)) else "Unknown"
292
312
  try:
293
313
  # Add a QTL before modules without it.
294
314
  if "QTL" not in module_name.decode("utf-8"):
@@ -298,32 +318,43 @@ def list_network(target_conn="all", debugPrint=False, lanTimeout=1, ipAddressLoo
298
318
  if "QTL" not in module_name:
299
319
  module_name = "QTL" + module_name
300
320
 
321
+ # Helper to check if this module already exists in our list with a proper IP
322
+ def is_duplicate_ghost(name, current_modules, protocol):
323
+ if ip_module != "0.0.0.0":
324
+ return False
325
+ # Check if any existing entry for this protocol has the same module name
326
+ return any(m_name == name for key, m_name in current_modules.items() if key.startswith(protocol))
327
+
328
+ # Ensure module_name is a string for dictionary values and logging
329
+ if isinstance(module_name, bytes):
330
+ module_name = module_name.decode("utf-8", errors="ignore")
331
+
301
332
  # Checks if there's a value in the TELNET key.
302
- if target_conn.lower() == "all" or target_conn.lower() == "telnet":
333
+ if target_conn.lower() in ["all", "telnet"]:
303
334
  if network_modules.get("\\x8a") or network_modules.get("138"):
304
- # Append the information to the list.
305
- lan_modules["TELNET:" + ip_module] = module_name
306
- logger.debug("Found Telnet module: " + module_name)
335
+ if not is_duplicate_ghost(module_name, lan_modules, "TELNET:"):
336
+ lan_modules[f"TELNET:{ip_module}"] = module_name
337
+ logger.debug(f"Found Telnet module: {module_name} at {ip_module}")
307
338
 
308
339
  # Checks if there's a value in the REST key.
309
- if target_conn.lower() == "all" or target_conn.lower() == "rest":
340
+ if target_conn.lower() in ["all", "rest"]:
310
341
  if network_modules.get("\\x84") or network_modules.get("132"):
311
- # Append the information to the list.
312
- lan_modules["REST:" + ip_module] = module_name
313
- logger.debug("Found REST module: " + module_name)
342
+ if not is_duplicate_ghost(module_name, lan_modules, "REST:"):
343
+ lan_modules[f"REST:{ip_module}"] = module_name
344
+ logger.debug(f"Found REST module: {module_name} at {ip_module}")
314
345
 
315
346
  # Checks if there's a value in the TCP key.
316
- if target_conn.lower() == "all" or target_conn.lower() == "tcp":
347
+ if target_conn.lower() in ["all", "tcp"]:
317
348
  if network_modules.get("\\x85") or network_modules.get("133"):
318
- # Append the information to the list.
319
- lan_modules["TCP:" + ip_module] = module_name
320
- logger.debug("Found TCP module: " + module_name)
349
+ if not is_duplicate_ghost(module_name, lan_modules, "TCP:"):
350
+ lan_modules[f"TCP:{ip_module}"] = module_name
351
+ logger.debug(f"Found TCP module: {module_name} at {ip_module}")
321
352
  mySocket.close()
322
353
  if ipAddressLookup is not None:
323
354
  if moduleFound is None:
324
355
  printText("IP Scan failed, no module found.")
325
356
  else:
326
- printText("IP Scan succeeded, module found: " + moduleFound)
357
+ printText("IP Scan succeeded, module found: " + str(moduleFound))
327
358
  logger.debug("Finished UDP scan")
328
359
  retVal.update(lan_modules)
329
360
  return retVal