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/__init__.py +57 -49
- quarchpy/_version.py +1 -1
- quarchpy/connection_specific/connection_QIS.py +3 -8
- quarchpy/connection_specific/connection_QPS.py +116 -8
- quarchpy/connection_specific/connection_Telnet.py +19 -5
- quarchpy/connection_specific/jdk_jres/fix_permissions.py +0 -3
- quarchpy/connection_specific/usb_libs/libusb1.py +21 -0
- quarchpy/debug/SystemTest.py +65 -7
- quarchpy/debug/module_debug.py +11 -3
- quarchpy/device/device.py +93 -45
- quarchpy/device/scanDevices.py +55 -24
- quarchpy/qis/qisFuncs.py +236 -127
- quarchpy/qps/qpsFuncs.py +267 -171
- quarchpy/run.py +1 -1
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/METADATA +5 -1
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/RECORD +19 -18
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/WHEEL +1 -1
- quarchpy-2.2.17.dev3.dist-info/licenses/LICENSE.txt +21 -0
- {quarchpy-2.2.17.dev1.dist-info → quarchpy-2.2.17.dev3.dist-info}/top_level.txt +0 -0
quarchpy/__init__.py
CHANGED
|
@@ -6,16 +6,34 @@ import logging
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from logging.handlers import RotatingFileHandler
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
# --- Dynamic Level Synchronization ---
|
|
11
|
+
class SyncWithRootFilter(logging.Filter):
|
|
12
|
+
"""
|
|
13
|
+
A filter that dynamically checks the root logger's level.
|
|
14
|
+
This allows the quarchpy console handler to 'point' to the
|
|
15
|
+
global python log level even if it changes mid-script.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def filter(self, record):
|
|
19
|
+
# Dynamically fetch the root logger's effective level
|
|
20
|
+
root_level = logging.getLogger().getEffectiveLevel()
|
|
21
|
+
# Only allow the log record if its level is >= the current root level
|
|
22
|
+
return record.levelno >= root_level
|
|
23
|
+
|
|
24
|
+
|
|
9
25
|
logger = logging.getLogger("quarchpy")
|
|
26
|
+
|
|
27
|
+
# Master Valve: Must stay DEBUG so the File Handler captures everything.
|
|
10
28
|
logger.setLevel(logging.DEBUG)
|
|
11
29
|
|
|
12
|
-
#
|
|
30
|
+
# Propagate to root is False because we provide our own console handler.
|
|
31
|
+
# If True, you would see duplicate logs if the user also has a root handler.
|
|
13
32
|
logger.propagate = False
|
|
14
33
|
|
|
15
34
|
# Only add handlers once
|
|
16
35
|
if not logger.handlers:
|
|
17
|
-
|
|
18
|
-
# File handler (always debug)
|
|
36
|
+
# 1. File handler (Always capture DEBUG for the 'permanent record')
|
|
19
37
|
log_dir = Path.home() / ".quarchpy"
|
|
20
38
|
log_dir.mkdir(exist_ok=True)
|
|
21
39
|
logfile = log_dir / "quarchpy.log"
|
|
@@ -28,9 +46,15 @@ if not logger.handlers:
|
|
|
28
46
|
))
|
|
29
47
|
logger.addHandler(file_handler)
|
|
30
48
|
|
|
31
|
-
# Console:
|
|
49
|
+
# 2. Console: Dynamically mirrors the python root log level
|
|
32
50
|
console_handler = logging.StreamHandler()
|
|
33
|
-
|
|
51
|
+
|
|
52
|
+
# We set this to NOTSET (0) so the handler itself doesn't block anything...
|
|
53
|
+
console_handler.setLevel(logging.NOTSET)
|
|
54
|
+
|
|
55
|
+
# ...instead, we use the custom filter to do the dynamic level check.
|
|
56
|
+
console_handler.addFilter(SyncWithRootFilter())
|
|
57
|
+
|
|
34
58
|
console_handler.setFormatter(logging.Formatter(
|
|
35
59
|
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
36
60
|
"%Y-%m-%d %H:%M:%S"
|
|
@@ -42,9 +66,14 @@ if not logger.handlers:
|
|
|
42
66
|
def configure_logging(console_level=None, file_level=None, file_path=None):
|
|
43
67
|
"""Reconfigure quarchpy logging safely."""
|
|
44
68
|
for handler in logger.handlers:
|
|
45
|
-
# Change console level
|
|
46
|
-
if isinstance(handler, logging.StreamHandler):
|
|
69
|
+
# Change console level (Note: Setting this overrides the SyncWithRootFilter behavior)
|
|
70
|
+
if isinstance(handler, logging.StreamHandler) and not isinstance(handler, RotatingFileHandler):
|
|
47
71
|
if console_level is not None:
|
|
72
|
+
# If the user explicitly sets a level, we remove the dynamic filter
|
|
73
|
+
# so it behaves exactly as the user requested.
|
|
74
|
+
for f in handler.filters:
|
|
75
|
+
if isinstance(f, SyncWithRootFilter):
|
|
76
|
+
handler.removeFilter(f)
|
|
48
77
|
handler.setLevel(console_level)
|
|
49
78
|
|
|
50
79
|
# Change file level and path
|
|
@@ -54,61 +83,40 @@ def configure_logging(console_level=None, file_level=None, file_path=None):
|
|
|
54
83
|
if file_path is not None:
|
|
55
84
|
handler.baseFilename = str(file_path)
|
|
56
85
|
|
|
57
|
-
logger.info(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Adds / to the path.
|
|
61
|
-
folder2add = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0]) + "//")
|
|
62
|
-
if folder2add not in sys.path:
|
|
63
|
-
sys.path.insert(0, folder2add)
|
|
64
|
-
|
|
65
|
-
# Adds /disk_test to the path.
|
|
66
|
-
folder2add = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0]) + "//disk_test")
|
|
67
|
-
if folder2add not in sys.path:
|
|
68
|
-
sys.path.insert(0, folder2add)
|
|
86
|
+
logger.info(
|
|
87
|
+
f"quarchpy logging reconfigured console_level:{console_level} file_level:{file_level} file_path:{file_path}")
|
|
69
88
|
|
|
70
|
-
# Adds /connection_specific to the path.
|
|
71
|
-
folder2add = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0]) + "//connection_specific")
|
|
72
|
-
if folder2add not in sys.path:
|
|
73
|
-
sys.path.insert(0, folder2add)
|
|
74
89
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
# --- Path Management (Maintain existing legacy behavior) ---
|
|
91
|
+
base_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
92
|
+
folders = [
|
|
93
|
+
"",
|
|
94
|
+
"disk_test",
|
|
95
|
+
"connection_specific",
|
|
96
|
+
os.path.join("connection_specific", "serial"),
|
|
97
|
+
os.path.join("connection_specific", "QIS"),
|
|
98
|
+
os.path.join("connection_specific", "usb_libs"),
|
|
99
|
+
"config_files"
|
|
100
|
+
]
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
folder2add = os.path.realpath(os.path.
|
|
82
|
-
if folder2add not in sys.path:
|
|
83
|
-
|
|
102
|
+
for folder in folders:
|
|
103
|
+
folder2add = os.path.realpath(os.path.join(base_path, folder))
|
|
104
|
+
if folder2add not in sys.path:
|
|
105
|
+
sys.path.insert(0, folder2add)
|
|
84
106
|
|
|
85
|
-
#
|
|
86
|
-
folder2add = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0]) + "//connection_specific//usb_libs")
|
|
87
|
-
if folder2add not in sys.path:
|
|
88
|
-
sys.path.insert(0, folder2add)
|
|
89
|
-
|
|
90
|
-
# Adds /usb_libs to the path.
|
|
91
|
-
folder2add = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0]) + "//config_files")
|
|
92
|
-
if folder2add not in sys.path:
|
|
93
|
-
sys.path.insert(0, folder2add)
|
|
94
|
-
|
|
95
|
-
# Basic functions imported up to the root module in the package
|
|
107
|
+
# --- Public API Imports ---
|
|
96
108
|
from debug.versionCompare import requiredQuarchpyVersion
|
|
97
|
-
|
|
98
|
-
#importing legacy API functions to the root module in the package. This is to avoid
|
|
99
|
-
#breacking back-compatibility with old scripts. Avoid using these direct imports
|
|
100
|
-
#and use the managed sub module format instead (from quarchpy.device import *)
|
|
101
109
|
from device import quarchDevice, getQuarchDevice, get_quarch_device
|
|
102
110
|
from connection_specific.connection_QIS import QisInterface, QisInterface as qisInterface
|
|
103
111
|
from connection_specific.connection_QPS import QpsInterface, QpsInterface as qpsInterface
|
|
104
112
|
from qis.qisFuncs import isQisRunning, startLocalQis, GetQisModuleSelection
|
|
105
113
|
from qis.qisFuncs import closeQis, closeQis as closeQIS
|
|
106
114
|
from device.quarchPPM import quarchPPM
|
|
107
|
-
from iometer.iometerFuncs import generateIcfFromCsvLineData, readIcfCsvLineData, generateIcfFromConf, runIOMeter,
|
|
115
|
+
from iometer.iometerFuncs import generateIcfFromCsvLineData, readIcfCsvLineData, generateIcfFromConf, runIOMeter, \
|
|
116
|
+
processIometerInstResults
|
|
108
117
|
from device.quarchQPS import quarchQPS
|
|
109
118
|
from qps.qpsFuncs import isQpsRunning, startLocalQps, GetQpsModuleSelection
|
|
110
119
|
from qps.qpsFuncs import closeQps, closeQps as closeQPS
|
|
111
120
|
from disk_test.DiskTargetSelection import getDiskTargetSelection, getDiskTargetSelection as GetDiskTargetSelection
|
|
112
121
|
from fio.FIO_interface import runFIO
|
|
113
|
-
from device.scanDevices import scanDevices
|
|
114
|
-
|
|
122
|
+
from device.scanDevices import scanDevices
|
quarchpy/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.2.17.
|
|
1
|
+
__version__ = "2.2.17.dev3"
|
|
@@ -609,8 +609,7 @@ class QisInterface:
|
|
|
609
609
|
logger.warning(e)
|
|
610
610
|
return False
|
|
611
611
|
|
|
612
|
-
|
|
613
|
-
def get_qis_module_selection(self, preferred_connection_only=True, additional_options="DEF_ARGS", scan=True) -> str:
|
|
612
|
+
def get_qis_module_selection(self, preferred_connection_only=True, additional_options=['rescan', 'all con types', 'ip scan'], scan=True) -> str:
|
|
614
613
|
"""
|
|
615
614
|
Scans for available modules and allows the user to select one through an interactive selection process.
|
|
616
615
|
Can also handle additional custom options and some built-in ones such as rescanning
|
|
@@ -634,10 +633,6 @@ class QisInterface:
|
|
|
634
633
|
ValueError: Raised if no valid selection is made or the provided IP address is invalid.
|
|
635
634
|
"""
|
|
636
635
|
|
|
637
|
-
# Avoid mutable warning by adding the argument list in the function rather than the header
|
|
638
|
-
if additional_options == "DEF_ARGS":
|
|
639
|
-
additional_options = ['rescan', 'all con types', 'ip scan']
|
|
640
|
-
|
|
641
636
|
table_headers = ["Modules"]
|
|
642
637
|
ip_address = None
|
|
643
638
|
favourite = preferred_connection_only
|
|
@@ -650,8 +645,8 @@ class QisInterface:
|
|
|
650
645
|
found_devices = self.qis_scan_devices(scan=scan, preferred_connection_only=favourite, ip_address=ip_address)
|
|
651
646
|
|
|
652
647
|
my_device_id = listSelection(title="Select a module",message="Select a module",
|
|
653
|
-
|
|
654
|
-
|
|
648
|
+
selectionList=found_devices, additionalOptions= additional_options,
|
|
649
|
+
nice=True, tableHeaders=table_headers, indexReq=True)
|
|
655
650
|
|
|
656
651
|
if my_device_id.lower() == 'rescan':
|
|
657
652
|
favourite = True
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import socket
|
|
3
|
-
import time
|
|
4
|
-
import datetime
|
|
5
|
-
import subprocess
|
|
6
|
-
import os
|
|
7
|
-
import random
|
|
8
3
|
import logging
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
4
|
import time
|
|
11
5
|
import re
|
|
12
|
-
from
|
|
6
|
+
from typing import Union, List, Optional, Any
|
|
7
|
+
|
|
8
|
+
from quarchpy.user_interface import user_interface, User_interface, printText, listSelection, requestDialog
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
13
11
|
|
|
14
12
|
class QpsInterface:
|
|
15
13
|
def __init__(self, host='127.0.0.1', port=9822):
|
|
@@ -113,7 +111,6 @@ class QpsInterface:
|
|
|
113
111
|
time.sleep(0.3)
|
|
114
112
|
return retVal
|
|
115
113
|
|
|
116
|
-
|
|
117
114
|
def disconnect(self, targetDevice):
|
|
118
115
|
self.sendCmdVerbose("$disconnect")
|
|
119
116
|
|
|
@@ -122,6 +119,7 @@ class QpsInterface:
|
|
|
122
119
|
return self.sendCmdVerbose("close")
|
|
123
120
|
else:
|
|
124
121
|
return self.sendCmdVerbose(conString+" close")
|
|
122
|
+
|
|
125
123
|
def scanIP(self, ipAddress, sleep=10):
|
|
126
124
|
"""
|
|
127
125
|
Triggers QPS to look at a specific IP address for a quarch module
|
|
@@ -195,6 +193,71 @@ class QpsInterface:
|
|
|
195
193
|
#return list of devices
|
|
196
194
|
return deviceList
|
|
197
195
|
|
|
196
|
+
def get_qps_module_selection(
|
|
197
|
+
self,
|
|
198
|
+
preferred_connection_only: bool = True,
|
|
199
|
+
additional_options: Optional[List[str]] = None,
|
|
200
|
+
scan: bool = True
|
|
201
|
+
) -> Optional[Any]:
|
|
202
|
+
"""
|
|
203
|
+
Scans for QPS devices and prompts the user to select one.
|
|
204
|
+
"""
|
|
205
|
+
if additional_options is None:
|
|
206
|
+
additional_options = ['rescan', 'all con types', 'ip scan']
|
|
207
|
+
|
|
208
|
+
# State variables
|
|
209
|
+
favourite = preferred_connection_only
|
|
210
|
+
ip_address = None
|
|
211
|
+
|
|
212
|
+
while True:
|
|
213
|
+
printText("QPS scanning for devices")
|
|
214
|
+
|
|
215
|
+
# 1. Fetch raw list from QPS
|
|
216
|
+
dev_list = self.getDeviceList(scan=scan, ipAddress=ip_address)
|
|
217
|
+
|
|
218
|
+
# 2. Check for empty results and sanitize
|
|
219
|
+
# If no devices found, force favorite mode off to prevent sorting bugs
|
|
220
|
+
if self._is_list_empty_or_error(dev_list):
|
|
221
|
+
favourite = False
|
|
222
|
+
|
|
223
|
+
# Remove REST devices (unsupported here)
|
|
224
|
+
dev_list = [x for x in dev_list if "rest" not in x]
|
|
225
|
+
|
|
226
|
+
# 3. Apply Sorting
|
|
227
|
+
# We always sort by type (USB > TCP), but we only deduplicate
|
|
228
|
+
# (hide extra connections) if 'favourite' is True.
|
|
229
|
+
dev_list = self._apply_favourite_sorting(dev_list, one_conn_per_mod=favourite)
|
|
230
|
+
|
|
231
|
+
# 4. Show UI
|
|
232
|
+
selection = listSelection(
|
|
233
|
+
title="Select a Quarch module",
|
|
234
|
+
message="Select a Quarch module",
|
|
235
|
+
selectionList=dev_list,
|
|
236
|
+
additionalOptions=additional_options,
|
|
237
|
+
nice=True,
|
|
238
|
+
tableHeaders=["Module"],
|
|
239
|
+
indexReq=True
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# 5. Handle User Response
|
|
243
|
+
if selection == 'rescan':
|
|
244
|
+
ip_address = None
|
|
245
|
+
favourite = True
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
elif selection == 'all con types':
|
|
249
|
+
printText('Displaying all connection types...')
|
|
250
|
+
favourite = False
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
elif selection == 'ip scan':
|
|
254
|
+
ip_address = requestDialog("Please input IP Address of the module you would like to connect to: ")
|
|
255
|
+
favourite = False
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
# If it's not an option, it must be a device
|
|
260
|
+
return selection
|
|
198
261
|
|
|
199
262
|
def open_recording(self, file_path, cmdTimeout=5, pollInterval=3, startOpenTimout=5):
|
|
200
263
|
"""
|
|
@@ -239,5 +302,50 @@ class QpsInterface:
|
|
|
239
302
|
time.sleep(1) # sleep outside the loop as there is a
|
|
240
303
|
return message
|
|
241
304
|
|
|
305
|
+
def _is_list_empty_or_error(self, dev_list: List[str]) -> bool:
|
|
306
|
+
"""Checks if the returned list is empty or contains error messages."""
|
|
307
|
+
if not dev_list:
|
|
308
|
+
return True
|
|
309
|
+
first_item = dev_list[0].lower()
|
|
310
|
+
return "no device" in first_item or "no module" in first_item
|
|
311
|
+
|
|
312
|
+
def _apply_favourite_sorting(self, dev_list: List[str], one_conn_per_mod: bool = True) -> List[str]:
|
|
313
|
+
"""
|
|
314
|
+
Sorts devices by connection preference (USB > TCP > ...).
|
|
315
|
+
If one_conn_per_mod is True, it also removes duplicate physical devices,
|
|
316
|
+
keeping only the highest priority connection type found.
|
|
317
|
+
"""
|
|
318
|
+
sorted_list = []
|
|
319
|
+
seen_ids = set()
|
|
320
|
+
con_pref = ["USB", "TCP", "SERIAL", "REST", "TELNET"]
|
|
321
|
+
|
|
322
|
+
# Helper to process a device and decide if it should be added
|
|
323
|
+
def try_add_device(device_str):
|
|
324
|
+
if device_str in sorted_list:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
if one_conn_per_mod:
|
|
328
|
+
# Extract ID (e.g., 'QTL1234' from 'USB::QTL1234')
|
|
329
|
+
if "::" in device_str:
|
|
330
|
+
dev_id = device_str.split("::")[1]
|
|
331
|
+
if dev_id in seen_ids:
|
|
332
|
+
return # Skip: We already have a preferred connection for this ID
|
|
333
|
+
seen_ids.add(dev_id)
|
|
334
|
+
|
|
335
|
+
sorted_list.append(device_str)
|
|
336
|
+
|
|
337
|
+
# Pass 1: Add devices that match our specific preferences, in order
|
|
338
|
+
for pref in con_pref:
|
|
339
|
+
for device in dev_list:
|
|
340
|
+
if pref in device.upper():
|
|
341
|
+
try_add_device(device)
|
|
342
|
+
|
|
343
|
+
# Pass 2: Add any remaining devices that didn't match preferences (Safety net)
|
|
344
|
+
# This ensures devices with weird/new connection types don't disappear.
|
|
345
|
+
for device in dev_list:
|
|
346
|
+
if device not in sorted_list:
|
|
347
|
+
try_add_device(device)
|
|
348
|
+
|
|
349
|
+
return sorted_list
|
|
242
350
|
|
|
243
351
|
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
2
|
import time
|
|
3
|
-
|
|
3
|
+
import sys
|
|
4
4
|
|
|
5
|
+
# -----------------------------------------------------------
|
|
6
|
+
# Import Logic: Handles Python 3.13+ deprecation of telnetlib
|
|
7
|
+
# -----------------------------------------------------------
|
|
8
|
+
try:
|
|
9
|
+
from telnetlib import Telnet
|
|
10
|
+
except ImportError:
|
|
11
|
+
logging.debug("Standard telnetlib not found (Python 3.13+ detected). Using drop-in replacement.")
|
|
12
|
+
try:
|
|
13
|
+
from telnetlib313andup import Telnet
|
|
14
|
+
except ImportError as e:
|
|
15
|
+
logging.error("CRITICAL: 'telnetlib313andup' is not installed.")
|
|
16
|
+
logging.error("Please run: pip install telnetlib-313-and-up")
|
|
17
|
+
raise e
|
|
18
|
+
# -----------------------------------------------------------
|
|
5
19
|
|
|
6
20
|
class TelnetConn:
|
|
7
21
|
def __init__(self, ConnTarget):
|
|
@@ -14,14 +28,14 @@ class TelnetConn:
|
|
|
14
28
|
self.Connection.close()
|
|
15
29
|
# The closed device reports as in use if a connection is opened to it within 0.05s.
|
|
16
30
|
# This happens during scanning as rest detects the device and shows it as "in use" Putting a sleep here
|
|
17
|
-
#
|
|
31
|
+
# allows time for the connection to be close,
|
|
18
32
|
time.sleep(0.15)
|
|
19
33
|
return True
|
|
20
34
|
|
|
21
35
|
def sendCommand(self, Command, expectedResponse = True):
|
|
22
36
|
self.Connection.write((Command + "\r\n").encode('latin-1'))
|
|
23
|
-
self.Connection.read_until(b"\r\n",3)
|
|
24
|
-
Result = self.Connection.read_until(b">",3)[:-1]
|
|
37
|
+
self.Connection.read_until(b"\r\n", 3)
|
|
38
|
+
Result = self.Connection.read_until(b">", 3)[:-1]
|
|
25
39
|
Result = Result.decode()
|
|
26
40
|
Result = Result.strip('> \t\n\r')
|
|
27
41
|
return Result.strip()
|
|
@@ -11,11 +11,8 @@ def main():
|
|
|
11
11
|
current_os = platform.system()
|
|
12
12
|
if current_os != "Windows":
|
|
13
13
|
print("Fixing Permissions")
|
|
14
|
-
# Check the current OS
|
|
15
|
-
current_os = platform.system()
|
|
16
14
|
# JRE path
|
|
17
15
|
java_path = quarchpy_binaries.get_jre_home()
|
|
18
|
-
|
|
19
16
|
# Ensure the jres folder has the required permissions
|
|
20
17
|
subprocess.call(['chmod', '-R', '+rwx', java_path])
|
|
21
18
|
|
|
@@ -48,6 +48,7 @@ import ctypes.util
|
|
|
48
48
|
import platform
|
|
49
49
|
import os.path
|
|
50
50
|
import sys
|
|
51
|
+
import libusb_package
|
|
51
52
|
|
|
52
53
|
class Enum(object):
|
|
53
54
|
def __init__(self, member_dict, scope_dict=None):
|
|
@@ -170,6 +171,26 @@ def _loadLibrary():
|
|
|
170
171
|
if sys.version_info[:2] >= (2, 6):
|
|
171
172
|
loader_kw['use_errno'] = True
|
|
172
173
|
loader_kw['use_last_error'] = True
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
bundled_path = libusb_package.find_library("usb-1.0")
|
|
177
|
+
|
|
178
|
+
# If that failed (macOS/Linux), find the file manually
|
|
179
|
+
import os
|
|
180
|
+
# Get the folder where libus_package is located
|
|
181
|
+
wrapper_dir = os.path.dirname(libusb_package.__file__)
|
|
182
|
+
|
|
183
|
+
# Look for the dylib/so in that folder
|
|
184
|
+
potential_path = os.path.join(wrapper_dir, "libusb-1.0" + suffix)
|
|
185
|
+
|
|
186
|
+
if os.path.exists(potential_path):
|
|
187
|
+
bundled_path = potential_path
|
|
188
|
+
|
|
189
|
+
if bundled_path:
|
|
190
|
+
return dll_loader(bundled_path, **loader_kw)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass # If bundled fails, proceed to system search below
|
|
193
|
+
|
|
173
194
|
try:
|
|
174
195
|
return dll_loader('libusb-1.0' + suffix, **loader_kw)
|
|
175
196
|
except OSError:
|
quarchpy/debug/SystemTest.py
CHANGED
|
@@ -236,24 +236,82 @@ def _get_logging_directory(app_type: AppType = "QuarchPy") -> str:
|
|
|
236
236
|
|
|
237
237
|
return log_dir
|
|
238
238
|
|
|
239
|
+
|
|
240
|
+
import os
|
|
241
|
+
import sys
|
|
242
|
+
import platform
|
|
243
|
+
|
|
244
|
+
|
|
239
245
|
def fix_usb():
|
|
246
|
+
print("\n--- USB Permission Setup ---")
|
|
247
|
+
|
|
248
|
+
rule_file_path = "/etc/udev/rules.d/20-quarchmodules.rules"
|
|
249
|
+
|
|
250
|
+
# --- CHECK EXISTING INSTALLATION ---
|
|
251
|
+
# We check if the file exists AND if it contains the correct settings
|
|
252
|
+
if os.path.exists(rule_file_path):
|
|
253
|
+
try:
|
|
254
|
+
with open(rule_file_path, 'r') as f:
|
|
255
|
+
content = f.read()
|
|
256
|
+
# We look for the Vendor ID (16d0) and the writable mode (0666)
|
|
257
|
+
if 'idVendor' in content and '16d0' in content and '0666' in content:
|
|
258
|
+
print("USB permissions appear to be correctly set up already. No changes needed.")
|
|
259
|
+
return # Exit the function here
|
|
260
|
+
except IOError:
|
|
261
|
+
# If we can't read the file for some reason, we assume we need to fix it
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# --- IF WE REACH HERE, PERMISSIONS ARE MISSING ---
|
|
265
|
+
|
|
266
|
+
print("This script needs to update your system permissions to allow access to the device.")
|
|
267
|
+
print(f"We will add a rule file to: {rule_file_path}")
|
|
268
|
+
|
|
269
|
+
# Link provided before any action is taken
|
|
270
|
+
print("\nFor full details on why this is required, please read:")
|
|
271
|
+
print("https://quarch.com/support/faqs/usb/#linux-permissions")
|
|
272
|
+
print("----------------------------")
|
|
273
|
+
|
|
274
|
+
# Ask for permission before proceeding
|
|
275
|
+
response = input("Do you want to proceed with these changes? (y/n): ")
|
|
276
|
+
if response.lower() != 'y':
|
|
277
|
+
print("Operation cancelled by user.")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# Check if the user is root (UID 0)
|
|
281
|
+
print("\nStep 1: Checking for administrator privileges...")
|
|
282
|
+
if os.geteuid() != 0:
|
|
283
|
+
print(">> Current user is not admin. Elevating permissions now...")
|
|
284
|
+
|
|
285
|
+
# Re-run the script with sudo
|
|
286
|
+
args = ['sudo', sys.executable] + sys.argv
|
|
287
|
+
os.execvp('sudo', args)
|
|
288
|
+
|
|
289
|
+
# If we reach here, we are root
|
|
290
|
+
print(">> Administrator privileges confirmed.")
|
|
291
|
+
|
|
292
|
+
print("Step 2: Defining the access rules (Vendor ID: 16d0)...")
|
|
240
293
|
content_to_write = "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"16d0\", MODE=\"0666\"\n" \
|
|
241
294
|
"SUBSYSTEM==\"usb_device\", ATTRS{idVendor}==\"16d0\", MODE=\"0666\""
|
|
242
295
|
|
|
243
296
|
if "centos" in str(platform.platform()).lower():
|
|
297
|
+
print(">> CentOS detected. Applying group permissions fix...")
|
|
244
298
|
content_to_write = "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"16d0\", MODE=\"0666\", GROUP=*\n " \
|
|
245
299
|
"SUBSYSTEM==\"usb_device\", ATTRS{idVendor}==\"16d0\", MODE=\"0666\", GROUP=*"
|
|
246
300
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
301
|
+
print("Step 3: Saving the rule file...")
|
|
302
|
+
try:
|
|
303
|
+
f = open(rule_file_path, "w")
|
|
304
|
+
f.write(content_to_write)
|
|
305
|
+
f.close()
|
|
306
|
+
except IOError as e:
|
|
307
|
+
print(f"!! Failed to write file: {e}")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
print("Step 4: Reloading system USB drivers...")
|
|
253
311
|
os.system("udevadm control --reload")
|
|
254
312
|
os.system("udevadm trigger")
|
|
255
313
|
|
|
256
|
-
print("USB
|
|
314
|
+
print("\nSUCCESS: USB Setup Complete.")
|
|
257
315
|
|
|
258
316
|
def _check_fw():
|
|
259
317
|
print("")
|
quarchpy/debug/module_debug.py
CHANGED
|
@@ -76,15 +76,23 @@ def main(filepath=None):
|
|
|
76
76
|
except FileNotFoundError as err:
|
|
77
77
|
logger.error(f"Config file not found for module : {moduleStr}\nExiting Script")
|
|
78
78
|
my_device.close_connection()
|
|
79
|
-
return
|
|
79
|
+
return None
|
|
80
80
|
|
|
81
81
|
# Parse the file to get the device capabilities
|
|
82
|
-
|
|
82
|
+
try:
|
|
83
|
+
dev_caps = parse_config_file(file)
|
|
84
|
+
except Exception as err:
|
|
85
|
+
logger.error(f"Could not parse config file for {moduleStr}\nExiting Script"
|
|
86
|
+
f"\nError details: {err}")
|
|
87
|
+
print(f"Could not parse config file for {moduleStr}\nPlease note only breaker and hot-plug modules are currently supported."
|
|
88
|
+
f"\nExiting Script")
|
|
89
|
+
my_device.close_connection()
|
|
90
|
+
return None
|
|
83
91
|
|
|
84
92
|
if not dev_caps:
|
|
85
93
|
logger.error(f"Could not parse config file for {moduleStr}\nExiting Script")
|
|
86
94
|
my_device.close_connection()
|
|
87
|
-
return
|
|
95
|
+
return None
|
|
88
96
|
logText("\nCONFIG FILE LOCATED:")
|
|
89
97
|
logText(file)
|
|
90
98
|
logText("\n")
|