quarchpy 2.1.10.dev5__py3-none-any.whl → 2.1.10.dev6__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/_version.py +1 -1
- quarchpy/config_files/quarch_config_parser.py +4 -0
- quarchpy/config_files/quarch_config_parser.py.bak +766 -0
- {quarchpy-2.1.10.dev5.dist-info → quarchpy-2.1.10.dev6.dist-info}/METADATA +1 -1
- {quarchpy-2.1.10.dev5.dist-info → quarchpy-2.1.10.dev6.dist-info}/RECORD +7 -6
- {quarchpy-2.1.10.dev5.dist-info → quarchpy-2.1.10.dev6.dist-info}/WHEEL +0 -0
- {quarchpy-2.1.10.dev5.dist-info → quarchpy-2.1.10.dev6.dist-info}/top_level.txt +0 -0
quarchpy/_version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "2.1.10.
|
1
|
+
__version__ = "2.1.10.dev6"
|
@@ -241,6 +241,10 @@ def get_config_path_for_module (idn_string = None, module_connection = None):
|
|
241
241
|
# Split the string into lines and run through them to locate the parts we need
|
242
242
|
idn_lines = idn_string.upper().split("\n")
|
243
243
|
for i in idn_lines:
|
244
|
+
|
245
|
+
if i.count(":") > 1:
|
246
|
+
i = i[i.index(":") + 1:]
|
247
|
+
|
244
248
|
# Part number of the module
|
245
249
|
if "PART#:" in i:
|
246
250
|
device_number = i[6:].strip()
|
@@ -0,0 +1,766 @@
|
|
1
|
+
import logging
|
2
|
+
import os, os.path, sys
|
3
|
+
import inspect
|
4
|
+
import quarchpy.config_files
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
'''
|
8
|
+
Describes a unit for time measurement
|
9
|
+
'''
|
10
|
+
class TimeUnit(Enum):
|
11
|
+
"UNSPECIFIED",
|
12
|
+
"nS",
|
13
|
+
"uS",
|
14
|
+
"mS",
|
15
|
+
"S",
|
16
|
+
|
17
|
+
'''
|
18
|
+
Described a precise time duration for settings
|
19
|
+
'''
|
20
|
+
class TimeValue:
|
21
|
+
time_value = None
|
22
|
+
time_unit = None
|
23
|
+
|
24
|
+
def __init__ (self):
|
25
|
+
time_value = 0
|
26
|
+
time_unit = TimeUnit.UNSPECIFIED
|
27
|
+
|
28
|
+
'''
|
29
|
+
Describes a single range element if a module range parameter
|
30
|
+
'''
|
31
|
+
class ModuleRangeItem:
|
32
|
+
min_value = 0
|
33
|
+
max_value = 0
|
34
|
+
step_value = 0
|
35
|
+
unit = None
|
36
|
+
|
37
|
+
def __init__ (self):
|
38
|
+
min_value = 0
|
39
|
+
max_value = 0
|
40
|
+
step_value = 0
|
41
|
+
unit = None
|
42
|
+
|
43
|
+
'''
|
44
|
+
Describes a range of values which can be applied to the settings on a module
|
45
|
+
This is generally a time value, but can be anything
|
46
|
+
'''
|
47
|
+
class ModuleRangeParam:
|
48
|
+
ranges = None
|
49
|
+
range_unit = None
|
50
|
+
|
51
|
+
def __init__ (self):
|
52
|
+
self.ranges = list()
|
53
|
+
self.range_unit = None
|
54
|
+
|
55
|
+
def __repr__ (self):
|
56
|
+
str = ""
|
57
|
+
for item in self.ranges:
|
58
|
+
str = str + item.min_value + "," + item.max_value + "," + item.step_value + "," + item.unit + "|"
|
59
|
+
return str
|
60
|
+
|
61
|
+
'''
|
62
|
+
Adds a new range item, which is verified to match any existing items
|
63
|
+
'''
|
64
|
+
def add_range (self, new_range_item):
|
65
|
+
valid = True
|
66
|
+
|
67
|
+
if (self.range_unit is None):
|
68
|
+
self.range_unit = new_range_item.unit
|
69
|
+
# For additional ranges, verify the unit is the same
|
70
|
+
else:
|
71
|
+
if (new_range_item.unit != self.range_unit):
|
72
|
+
valid = False
|
73
|
+
|
74
|
+
# Add the new range if all is OK
|
75
|
+
if (valid):
|
76
|
+
self.ranges.append (new_range_item)
|
77
|
+
return True
|
78
|
+
else:
|
79
|
+
return False
|
80
|
+
|
81
|
+
'''
|
82
|
+
Internal method to find the closest value within a given range
|
83
|
+
'''
|
84
|
+
def _get_closest_value (self, range_item, value):
|
85
|
+
|
86
|
+
# Check for out of rang values
|
87
|
+
if (value < range_item.min_value):
|
88
|
+
result = range_item.min_value
|
89
|
+
elif (value > range_item.max_value):
|
90
|
+
result = range_item.max_value
|
91
|
+
# Else value is in range
|
92
|
+
else:
|
93
|
+
low_steps = int((float(value) / float(range_item.step_value)) + 0.5)
|
94
|
+
low_value = int(low_steps * range_item.step_value)
|
95
|
+
high_value = int(low_value + range_item.step_value)
|
96
|
+
|
97
|
+
# Get the closest step
|
98
|
+
if (abs(low_value - value) < abs(high_value - value)):
|
99
|
+
result - low_value
|
100
|
+
else:
|
101
|
+
result - high_value
|
102
|
+
|
103
|
+
return result
|
104
|
+
|
105
|
+
'''
|
106
|
+
Returns the closest allowable value to the given number
|
107
|
+
'''
|
108
|
+
def get_closest_value (self, value):
|
109
|
+
valid_value = -sys.maxsize -1
|
110
|
+
in_range = list()
|
111
|
+
running_error = sys.maxsize
|
112
|
+
curr_error = 0
|
113
|
+
possible_value = 0
|
114
|
+
|
115
|
+
if (self.ranges is None or len(self.ranges) == 0):
|
116
|
+
raise ValueError ("No ranges available to check against")
|
117
|
+
else:
|
118
|
+
# Loop through the ranges
|
119
|
+
for i in self.ranges:
|
120
|
+
# Find the closest value in this range
|
121
|
+
possible_value = self._get_closest_value (i, value)
|
122
|
+
curr_error = abs(possible_value - value)
|
123
|
+
|
124
|
+
# Store if it is closer than the previous tests
|
125
|
+
if (curr_error < running_error):
|
126
|
+
running_error = curr_error
|
127
|
+
valid_value = possible_value
|
128
|
+
|
129
|
+
return possible_value
|
130
|
+
|
131
|
+
'''
|
132
|
+
Returns the largest allowable value
|
133
|
+
'''
|
134
|
+
def get_max_value (self):
|
135
|
+
valid_value = -sys.maxsize -1
|
136
|
+
|
137
|
+
for i in self.ranges:
|
138
|
+
if (i.max_value > valid_value):
|
139
|
+
valid_value = i.max_value
|
140
|
+
|
141
|
+
return valid_value
|
142
|
+
|
143
|
+
'''
|
144
|
+
Returns the smallest allowable value
|
145
|
+
'''
|
146
|
+
def get_max_value (self):
|
147
|
+
valid_value = sys.maxsize
|
148
|
+
|
149
|
+
for i in self.ranges:
|
150
|
+
if (i.min_value < valid_value):
|
151
|
+
valid_value = i.min_value
|
152
|
+
|
153
|
+
return valid_value
|
154
|
+
|
155
|
+
'''
|
156
|
+
Describes a switched signal on a breaaker module
|
157
|
+
'''
|
158
|
+
class BreakerModuleSignal:
|
159
|
+
name = None
|
160
|
+
parameters = None
|
161
|
+
|
162
|
+
def __init__ (self):
|
163
|
+
self.name = None
|
164
|
+
self.parameters = dict ()
|
165
|
+
|
166
|
+
'''
|
167
|
+
Describes a signal group on a breaker module
|
168
|
+
'''
|
169
|
+
class BreakerSignalGroup:
|
170
|
+
name = None
|
171
|
+
signals = None
|
172
|
+
|
173
|
+
def __init__ (self):
|
174
|
+
self.name = None
|
175
|
+
self.signals = list ()
|
176
|
+
|
177
|
+
'''
|
178
|
+
Describes control sources on a breaker module
|
179
|
+
'''
|
180
|
+
class BreakerSource:
|
181
|
+
name = None
|
182
|
+
parameters = None
|
183
|
+
|
184
|
+
def __init__ (self):
|
185
|
+
self.name = None
|
186
|
+
self.parameters = dict ()
|
187
|
+
|
188
|
+
'''
|
189
|
+
Describes a Torridon hot-swap/breaker module and all its capabilities
|
190
|
+
'''
|
191
|
+
class TorridonBreakerModule:
|
192
|
+
config_data = None
|
193
|
+
|
194
|
+
def get_signals(self):
|
195
|
+
return self.config_data["SIGNALS"]
|
196
|
+
|
197
|
+
def get_signal_groups(self):
|
198
|
+
return self.config_data["SIGNAL_GROUPS"]
|
199
|
+
|
200
|
+
def get_sources(self):
|
201
|
+
return self.config_data["SOURCES"]
|
202
|
+
|
203
|
+
def get_general_capabilities(self):
|
204
|
+
return self.config_data["GENERAL"]
|
205
|
+
|
206
|
+
def __init__ (self):
|
207
|
+
self.config_data = dict ()
|
208
|
+
|
209
|
+
|
210
|
+
'''
|
211
|
+
Tries to locate configuration data for the given module and return a structure of device capabilities to help the user
|
212
|
+
control the module and understand what its features are
|
213
|
+
'''
|
214
|
+
def get_device_capabilities (idn_string = None, module_connection = None):
|
215
|
+
# Get config data and fail if none is found
|
216
|
+
file_path = get_config_path_for_module (idn_string = idn_string, module_connection = module_connection)
|
217
|
+
if (file_path is None):
|
218
|
+
raise ValueError ("No configuration data found for the given module")
|
219
|
+
|
220
|
+
return parse_config_file (file_path)
|
221
|
+
|
222
|
+
'''
|
223
|
+
Returns the path to the most recent config file that is compatible with the given module. Module information can be passed in as
|
224
|
+
an existing IDN string from the "*IDN?" command or via an open connection to the module
|
225
|
+
'''
|
226
|
+
def get_config_path_for_module (idn_string = None, module_connection = None):
|
227
|
+
device_number = None
|
228
|
+
device_fw = None
|
229
|
+
device_fpga = None
|
230
|
+
result = True
|
231
|
+
|
232
|
+
# Check for invalid parameters
|
233
|
+
if (idn_string is None and module_connection is None):
|
234
|
+
logging.error("Invalid parameters, no module information given")
|
235
|
+
result = False
|
236
|
+
else:
|
237
|
+
# Prefer IDN string, otherwise use the module connection to get it now
|
238
|
+
if (idn_string is None):
|
239
|
+
idn_string = module_connection.sendCommand ("*IDN?")
|
240
|
+
|
241
|
+
# Split the string into lines and run through them to locate the parts we need
|
242
|
+
idn_lines = idn_string.upper().split("\n")
|
243
|
+
for i in idn_lines:
|
244
|
+
# Part number of the module
|
245
|
+
if "PART#:" in i:
|
246
|
+
device_number = i[6:].strip()
|
247
|
+
# Firmware version
|
248
|
+
if "PROCESSOR:" in i:
|
249
|
+
device_fw = i[10:].strip()
|
250
|
+
pos = device_fw.find (",")
|
251
|
+
if (pos == -1):
|
252
|
+
device_fw = None
|
253
|
+
else:
|
254
|
+
device_fw = device_fw[pos+1:].strip()
|
255
|
+
# FPGA version
|
256
|
+
if "FPGA 1:" in i:
|
257
|
+
device_fpga = i[7:].strip()
|
258
|
+
pos = device_fpga.find (",")
|
259
|
+
if (pos == -1):
|
260
|
+
device_fpga = None
|
261
|
+
else:
|
262
|
+
device_fpga = device_fpga[pos+1:].strip()
|
263
|
+
|
264
|
+
# Log the failure if we did not get all the info needed
|
265
|
+
if (device_number is None):
|
266
|
+
result = False
|
267
|
+
logging.error("Unable to indentify module - no module number")
|
268
|
+
if (device_fw is None):
|
269
|
+
logging.error("Unable to indentify module - no firmware version")
|
270
|
+
result = False
|
271
|
+
if (device_fpga is None):
|
272
|
+
logging.error("Unable to indentify module - no FPGA version")
|
273
|
+
result = False
|
274
|
+
|
275
|
+
# If we got all the data, use it to find the config file
|
276
|
+
config_matches = list()
|
277
|
+
if (result == False):
|
278
|
+
logging.error(f"Error finding an element from *IDN, output was : {str(idn_string)}")
|
279
|
+
return None
|
280
|
+
else:
|
281
|
+
# Get all config files as a dictionary of their basic header information
|
282
|
+
config_file_header = get_config_file_headers ()
|
283
|
+
|
284
|
+
# Loop through each config file header
|
285
|
+
for i in config_file_header:
|
286
|
+
# If the part number can be used with this config file
|
287
|
+
if (check_part_number_matches(i, device_number)):
|
288
|
+
logging.debug("Matched part number")
|
289
|
+
# Check if the part number is not seperately excluded
|
290
|
+
if (check_part_exclude_matches(i, device_number) == False):
|
291
|
+
logging.debug("Config file does not exclude module number")
|
292
|
+
# Check Firmware can be used with this config file
|
293
|
+
if (check_fw_version_matches(i, device_fw)):
|
294
|
+
logging.debug("Matched fw versions")
|
295
|
+
# Check if FPGA version matches
|
296
|
+
if (check_fpga_version_matches(i, device_fpga)):
|
297
|
+
# Store this as a matching config file for the device
|
298
|
+
logging.debug("Located matching config file: " + i["Config_Path"])
|
299
|
+
config_matches.append (i)
|
300
|
+
|
301
|
+
# Sort the config files into preferred order
|
302
|
+
if (len(config_matches) > 0):
|
303
|
+
config_matches = sort_config_headers (config_matches)
|
304
|
+
return config_matches[0]["Config_Path"]
|
305
|
+
else:
|
306
|
+
logging.error("No matching config files were found for this module:"
|
307
|
+
f"device number: {device_number}\n"
|
308
|
+
f"device fw: {device_fw}"
|
309
|
+
f"device fpga {device_fpga}")
|
310
|
+
return None
|
311
|
+
|
312
|
+
# Attempts to parse every file on the system to check for errors in the config files or the parser
|
313
|
+
def test_config_parser (level=1):
|
314
|
+
# Get all config files as a dictionary of their basic header information
|
315
|
+
config_file_header = get_config_file_headers ()
|
316
|
+
for i in config_file_header:
|
317
|
+
print ("CONFIG:" + i["Config_Path"])
|
318
|
+
dev_caps = parse_config_file (i["Config_Path"])
|
319
|
+
if (dev_caps is None):
|
320
|
+
print ("Module not parsed!")
|
321
|
+
else:
|
322
|
+
if (type(dev_caps) is TorridonBreakerModule):
|
323
|
+
print (dev_caps.config_data["HEADER"]["DeviceDescription"])
|
324
|
+
print("")
|
325
|
+
|
326
|
+
|
327
|
+
'''
|
328
|
+
Processes all config files to get a list of dictionaries of basic header information
|
329
|
+
'''
|
330
|
+
def get_config_file_headers ():
|
331
|
+
|
332
|
+
# Get the path to the config files folder
|
333
|
+
# folder_path = os.path.dirname (os.path.abspath(quarchpy.config_files.__file__))
|
334
|
+
folder_path = os.path.dirname(os.path.realpath(__file__))
|
335
|
+
files_found = list()
|
336
|
+
config_headers = list()
|
337
|
+
|
338
|
+
# Iterate through all files, including and recursive folders
|
339
|
+
for search_path, search_subdirs, search_files in os.walk(folder_path):
|
340
|
+
for name in search_files:
|
341
|
+
if (".qfg" in name.lower()):
|
342
|
+
files_found.append (os.path.join(search_path, name))
|
343
|
+
|
344
|
+
# Now parse the header section of each file into the list of dictionaries
|
345
|
+
for i in files_found:
|
346
|
+
read_file = open (i, "r")
|
347
|
+
next_line, read_point = read_config_line (read_file)
|
348
|
+
if ("@CONFIG" in next_line):
|
349
|
+
# Read until we find the @HEADER section
|
350
|
+
while ("@HEADER" not in next_line):
|
351
|
+
next_line, read_point = read_config_line (read_file)
|
352
|
+
# Parse the header section data
|
353
|
+
new_config = parse_section_to_dictionary (read_file)
|
354
|
+
# Store the file path as an item
|
355
|
+
new_config["Config_Path"] = i
|
356
|
+
config_headers.append (new_config)
|
357
|
+
else:
|
358
|
+
logging.error("Config file parse failed, @CONFIG section not found")
|
359
|
+
|
360
|
+
return config_headers
|
361
|
+
|
362
|
+
'''
|
363
|
+
Reads the next line of the file which contains usable data, skips blank lines and comments
|
364
|
+
'''
|
365
|
+
def read_config_line (read_file):
|
366
|
+
while(True):
|
367
|
+
start_pos = read_file.tell()
|
368
|
+
line = read_file.readline ()
|
369
|
+
|
370
|
+
if (line == ''):
|
371
|
+
return None,0
|
372
|
+
else:
|
373
|
+
line = line.strip()
|
374
|
+
if (len(line) > 0):
|
375
|
+
if (line[0] != '#'):
|
376
|
+
return line, start_pos
|
377
|
+
|
378
|
+
'''
|
379
|
+
Reads the next section of the file (up to the next @ line) into a dictionary
|
380
|
+
'''
|
381
|
+
def parse_section_to_dictionary (read_file):
|
382
|
+
elements = dict()
|
383
|
+
|
384
|
+
# Read until we find the end
|
385
|
+
while(True):
|
386
|
+
# Read a line and the read point in the file
|
387
|
+
line, start_pos = read_config_line (read_file)
|
388
|
+
# If this is the start of a new section, set the file back one line and stop
|
389
|
+
if (line.find ('@') == 0):
|
390
|
+
read_file.seek (start_pos)
|
391
|
+
break
|
392
|
+
# Else we parse the line into a new dictionary item
|
393
|
+
else:
|
394
|
+
pos = line.find ('=')
|
395
|
+
if (pos == -1):
|
396
|
+
logging.error("Config line does not meet required format of x=y: " + line)
|
397
|
+
return None
|
398
|
+
else:
|
399
|
+
elements[line[:pos]] = line[pos+1:]
|
400
|
+
|
401
|
+
return elements
|
402
|
+
|
403
|
+
|
404
|
+
'''
|
405
|
+
Returns true of the config header is allowed for use on a module with the given part number
|
406
|
+
'''
|
407
|
+
def check_part_number_matches(config_header, device_number):
|
408
|
+
|
409
|
+
# Strip down to the main part number, removing the version
|
410
|
+
pos = device_number.find ("-")
|
411
|
+
if (pos != -1):
|
412
|
+
pos = len(device_number) - pos
|
413
|
+
short_device_number = device_number[:-pos]
|
414
|
+
# Fail on part number not including the version section
|
415
|
+
else:
|
416
|
+
logging.debug("Part number did not contain the version :" + device_number)
|
417
|
+
return False
|
418
|
+
|
419
|
+
# Loop through the allowed part numbers
|
420
|
+
allowed_device_numbers = config_header["DeviceNumbers"].split(",")
|
421
|
+
for dev in allowed_device_numbers:
|
422
|
+
pos = dev.find('-');
|
423
|
+
if (pos != -1):
|
424
|
+
pos = len(dev) - pos
|
425
|
+
short_config_number = dev[:-pos]
|
426
|
+
if ("xx" in dev):
|
427
|
+
any_version = True;
|
428
|
+
else:
|
429
|
+
any_version = False;
|
430
|
+
# Fail if config number is invalid
|
431
|
+
else:
|
432
|
+
logging.debug("Part number in config file is not in the right format: " + dev)
|
433
|
+
return False;
|
434
|
+
|
435
|
+
# Return true if we find a number that matches in full, or one which matches in part and the any_version flag was present in the config file
|
436
|
+
if (device_number == dev or (short_device_number == short_config_number and any_version)):
|
437
|
+
return True
|
438
|
+
|
439
|
+
# False as the fallback if no matching part numbers were found
|
440
|
+
return False
|
441
|
+
|
442
|
+
'''
|
443
|
+
Returns true of the config header does not contain an exclusion for the given device number
|
444
|
+
'''
|
445
|
+
def check_part_exclude_matches(config_header, device_number):
|
446
|
+
|
447
|
+
# Strip down to the main part number, removing the version
|
448
|
+
pos = device_number.find ("-")
|
449
|
+
if (pos != -1):
|
450
|
+
pos = len(device_number) - pos
|
451
|
+
short_device_number = device_number[:-pos]
|
452
|
+
# Fail on part number not including the version section
|
453
|
+
else:
|
454
|
+
logging.debug("Part number did not contain the version :" + device_number)
|
455
|
+
return False
|
456
|
+
|
457
|
+
# Check that the part number is fully qualified (will not be the case if the serial number is not set)
|
458
|
+
if ("?" in device_number):
|
459
|
+
logging.debug("Part number is not fully qualified :" + device_number)
|
460
|
+
return False
|
461
|
+
|
462
|
+
# Loop through the excluded part numbers
|
463
|
+
allowed_device_numbers = config_header["DeviceNumbersExclude"].split(",")
|
464
|
+
for dev in allowed_device_numbers:
|
465
|
+
# Skip blanks (normally due to no part numbers specified)
|
466
|
+
if (len(dev) == 0):
|
467
|
+
continue
|
468
|
+
|
469
|
+
pos = dev.find('-');
|
470
|
+
if (pos != -1):
|
471
|
+
pos = len(dev) - pos
|
472
|
+
short_config_number = dev[:-pos]
|
473
|
+
if ("xx" in dev):
|
474
|
+
any_version = True;
|
475
|
+
else:
|
476
|
+
any_version = False;
|
477
|
+
# Fail if config number is invalid
|
478
|
+
else:
|
479
|
+
logging.debug("Exclude part number in config file is not in the right format: " + dev)
|
480
|
+
return False;
|
481
|
+
|
482
|
+
# Return true if we find a number that matches in full, or one which matches in part and the any_version flag was present in the config file
|
483
|
+
if (device_number == dev or (short_device_number == short_config_number and any_version)):
|
484
|
+
return True
|
485
|
+
|
486
|
+
# False as the fallback if no matching part numbers were found
|
487
|
+
return False
|
488
|
+
|
489
|
+
'''
|
490
|
+
Checks that the firmware version on the config header allows the version on the device
|
491
|
+
'''
|
492
|
+
def check_fw_version_matches(config_header, device_fw):
|
493
|
+
if float(device_fw) >= float(config_header["MinFirmwareRequired"]):
|
494
|
+
return True
|
495
|
+
else:
|
496
|
+
return False
|
497
|
+
|
498
|
+
'''
|
499
|
+
Checks that the FPGA version on the config header allows the version on the device
|
500
|
+
'''
|
501
|
+
def check_fpga_version_matches(config_header, device_fpga):
|
502
|
+
if (float(device_fpga) >= float(config_header["MinFpgaRequired"])):
|
503
|
+
return True
|
504
|
+
else:
|
505
|
+
return False
|
506
|
+
|
507
|
+
'''
|
508
|
+
Sorts a list of config headers into order, where the highest firmware version file is at the top of the list
|
509
|
+
This is the one which would normally be chosen
|
510
|
+
'''
|
511
|
+
def sort_config_headers (config_matches):
|
512
|
+
return sorted (config_matches, key=lambda i: i["MinFirmwareRequired"], reverse=True)
|
513
|
+
|
514
|
+
def parse_config_file (file):
|
515
|
+
|
516
|
+
config_dict = dict ()
|
517
|
+
section_dict = dict ()
|
518
|
+
section_name = None
|
519
|
+
read_file = open (file, "r")
|
520
|
+
|
521
|
+
# Start with the first line, as this is not useful info anyway
|
522
|
+
line, read_pos = read_config_line (read_file)
|
523
|
+
# Loop through the file, reading all lines
|
524
|
+
while (True):
|
525
|
+
line, read_pos = read_config_line (read_file)
|
526
|
+
if (line is None):
|
527
|
+
break
|
528
|
+
|
529
|
+
# If this is the start of the first section, store its name
|
530
|
+
if ("@" in line and section_name is None):
|
531
|
+
section_name = line[1:]
|
532
|
+
# If a new section, store the old one first
|
533
|
+
elif ("@" in line):
|
534
|
+
config_dict[section_name] = section_dict
|
535
|
+
section_name = line[1:]
|
536
|
+
section_dict = dict ()
|
537
|
+
# Else this must be a standard data line
|
538
|
+
else:
|
539
|
+
# Special case for module signals, create as a BreakerSignal class type
|
540
|
+
if ("SIGNALS" in section_name):
|
541
|
+
|
542
|
+
# Change to a list for signals
|
543
|
+
if (len(section_dict) == 0):
|
544
|
+
section_dict = list()
|
545
|
+
|
546
|
+
signal = BreakerModuleSignal ()
|
547
|
+
line_value = line.split(',')
|
548
|
+
# Loop to add the optional parameters
|
549
|
+
for i in line_value:
|
550
|
+
pos = i.find('=')
|
551
|
+
line_param = i[pos+1:]
|
552
|
+
line_name = i[:pos]
|
553
|
+
if ("Name" in line_name):
|
554
|
+
signal.name = line_param
|
555
|
+
else:
|
556
|
+
signal.parameters[line_name] = line_param
|
557
|
+
# Add signal to the section
|
558
|
+
section_dict.append(signal)
|
559
|
+
# Special case for module signal groups
|
560
|
+
elif ("SIGNAL_GROUPS" in section_name):
|
561
|
+
|
562
|
+
# Change to a list for signals groups
|
563
|
+
if (len(section_dict) == 0):
|
564
|
+
section_dict = list()
|
565
|
+
|
566
|
+
group = BreakerSignalGroup ()
|
567
|
+
# Get the name of the group
|
568
|
+
pos = line.find(',')
|
569
|
+
line_group = line[pos+1:]
|
570
|
+
line_header = line[:pos]
|
571
|
+
pos = line_header.find('=')
|
572
|
+
line_param = line_header[pos+1:]
|
573
|
+
line_name = line_header[:pos]
|
574
|
+
group.name = line_param
|
575
|
+
# Get the list of signals
|
576
|
+
pos = line_group.find('=')
|
577
|
+
line_param = line_group[pos+1:].strip('\"')
|
578
|
+
group.signals = line_param.split (',')
|
579
|
+
# Add group to the section
|
580
|
+
section_dict.append(group)
|
581
|
+
# Special case for module sources
|
582
|
+
elif ("SOURCE_START" in section_name):
|
583
|
+
read_file.seek(read_pos)
|
584
|
+
sources = parse_breaker_sources_section(read_file)
|
585
|
+
config_dict["SOURCES"] = sources
|
586
|
+
else:
|
587
|
+
pos = line.find('=')
|
588
|
+
line_value = line[pos+1:]
|
589
|
+
line_name = line[:pos]
|
590
|
+
section_dict[line_name] = line_value
|
591
|
+
|
592
|
+
# Now build the appropriate module class
|
593
|
+
# TODO: Assuming breaker for testing
|
594
|
+
if (config_dict["HEADER"]["DeviceClass"] == "TorridonModule"):
|
595
|
+
dev_caps = TorridonBreakerModule ()
|
596
|
+
dev_caps.config_data = config_dict
|
597
|
+
return dev_caps
|
598
|
+
else:
|
599
|
+
logging.error("Only 'TorridonModule' class devices are currently supported")
|
600
|
+
return None
|
601
|
+
|
602
|
+
# Parses the sources section of a Torridon breaker module, returning a list of sources
|
603
|
+
def parse_breaker_sources_section(file_access):
|
604
|
+
new_source = None
|
605
|
+
sources = list()
|
606
|
+
first_source = True
|
607
|
+
|
608
|
+
while(True):
|
609
|
+
line, read_pos = read_config_line (file_access)
|
610
|
+
|
611
|
+
# If the start of a source
|
612
|
+
if ("@SOURCE_START" in line or first_source):
|
613
|
+
# If first source, step back on line as we have consumed the first param already
|
614
|
+
if (first_source):
|
615
|
+
file_access.seek (read_pos)
|
616
|
+
first_source = False
|
617
|
+
# Parse the basic sections of the source info
|
618
|
+
new_source = BreakerSource ()
|
619
|
+
parse_source_basic_section (file_access, new_source)
|
620
|
+
continue
|
621
|
+
# Bounce sections describe pin-bounce abilities
|
622
|
+
elif ("@SOURCE_BOUNCE" in line):
|
623
|
+
parse_source_bounce_section (file_access, new_source)
|
624
|
+
continue
|
625
|
+
elif ("@SOURCE_END" in line):
|
626
|
+
if (new_source is not None):
|
627
|
+
sources.append (new_source)
|
628
|
+
new_source = None
|
629
|
+
# If the start of a new section
|
630
|
+
elif ("@" in line):
|
631
|
+
# Return the file to the line as if we had not read it
|
632
|
+
file_access.seek(read_pos)
|
633
|
+
# Exit the source parsing loop
|
634
|
+
break
|
635
|
+
|
636
|
+
return sources
|
637
|
+
|
638
|
+
# Parses the 'general' section, present for all sources
|
639
|
+
def parse_source_basic_section(file_access, source):
|
640
|
+
|
641
|
+
while(True):
|
642
|
+
line, read_pos = read_config_line (file_access)
|
643
|
+
|
644
|
+
if ("@" not in line):
|
645
|
+
# Limits sections need parsed into a limits object
|
646
|
+
if ("_Limits" in line):
|
647
|
+
pos = line.find('=')
|
648
|
+
line_param = line[pos+1:]
|
649
|
+
line_name = line[:pos]
|
650
|
+
line_param = parse_limits_string (line_param)
|
651
|
+
# If we've seen a limit section for this already
|
652
|
+
if (line_name in source.parameters):
|
653
|
+
# Combine the limits
|
654
|
+
source.parameters[line_name].add_range (line_param)
|
655
|
+
# Else start a new range
|
656
|
+
else:
|
657
|
+
new_range = ModuleRangeParam ()
|
658
|
+
new_range.add_range (line_param)
|
659
|
+
source.parameters[line_name] = new_range
|
660
|
+
# Else add as dictionary item
|
661
|
+
else:
|
662
|
+
pos = line.find('=')
|
663
|
+
line_param = line[pos+1:]
|
664
|
+
line_name = line[:pos]
|
665
|
+
|
666
|
+
if ("Name" in line_name):
|
667
|
+
source.name = line_param
|
668
|
+
else:
|
669
|
+
source.parameters[line_name] = line_param
|
670
|
+
else:
|
671
|
+
break
|
672
|
+
# Jump file back a line
|
673
|
+
file_access.seek (read_pos)
|
674
|
+
|
675
|
+
# Parses a limit string into a ModuleRangeValue class
|
676
|
+
def parse_limits_string (limit_text):
|
677
|
+
new_item = ModuleRangeItem ()
|
678
|
+
|
679
|
+
parts = limit_text.split(',')
|
680
|
+
new_item.unit = parts[0]
|
681
|
+
new_item.min_value = parts[1]
|
682
|
+
new_item.max_value = parts[2]
|
683
|
+
new_item.step_value = parts[3]
|
684
|
+
|
685
|
+
return new_item
|
686
|
+
|
687
|
+
# Parse out the bounce parameter section, for sources with pin-bounce
|
688
|
+
def parse_source_bounce_section (file_access, source):
|
689
|
+
while(True):
|
690
|
+
line, read_pos = read_config_line (file_access)
|
691
|
+
|
692
|
+
if ("@" not in line):
|
693
|
+
# Limits sections need parsed into a limits object
|
694
|
+
if ("_Limits" in line):
|
695
|
+
pos = line.find('=')
|
696
|
+
line_param = line[pos+1:]
|
697
|
+
line_name = line[:pos]
|
698
|
+
line_param = parse_limits_string (line_param)
|
699
|
+
# If we've seen a limit section for this already
|
700
|
+
if (line_name in source.parameters):
|
701
|
+
# Combine the limits
|
702
|
+
source.parameters[line_name].add_range (line_param)
|
703
|
+
# Else start a new range
|
704
|
+
else:
|
705
|
+
new_range = ModuleRangeParam ()
|
706
|
+
new_range.add_range (line_param)
|
707
|
+
source.parameters[line_name] = new_range
|
708
|
+
# Else add as dictionary item
|
709
|
+
else:
|
710
|
+
pos = line.find('=')
|
711
|
+
line_param = line[pos+1:]
|
712
|
+
line_name = line[:pos]
|
713
|
+
source.parameters[line_name] = line_param
|
714
|
+
else:
|
715
|
+
break
|
716
|
+
|
717
|
+
# Jump file back a line
|
718
|
+
file_access.seek(read_pos)
|
719
|
+
|
720
|
+
|
721
|
+
# will return a list of QTLXXXX numbers for each module type
|
722
|
+
def return_module_type_list(module_type=None):
|
723
|
+
current_path = os.path.dirname(os.path.abspath(__file__))
|
724
|
+
only_dirs = [f for f in os.listdir(current_path) if os.path.isdir(os.path.join(current_path, f))]
|
725
|
+
# only_dirs = ['Cable_Modules', 'Card_Modules', 'Drive_Modules', 'Power_Margining', 'Switch_Modules']
|
726
|
+
|
727
|
+
if any(str(module_type).lower() in str(s).lower() for s in only_dirs):
|
728
|
+
|
729
|
+
# capitalizing first letter - Linux path is case sensitive
|
730
|
+
module_type = str(module_type).capitalize()
|
731
|
+
post_fix = "_Modules"
|
732
|
+
if module_type in "power":
|
733
|
+
post_fix = "_Margining"
|
734
|
+
|
735
|
+
# e.g. "card" + "_modules" > Not case sensitive
|
736
|
+
module_type_path = os.path.join(current_path, module_type + post_fix)
|
737
|
+
|
738
|
+
# getting all files from specified directory
|
739
|
+
onlyfiles = [os.path.join(module_type_path, f) for f in os.listdir(module_type_path) if os.path.isfile(os.path.join(module_type_path, f))]
|
740
|
+
|
741
|
+
filtered_modules = []
|
742
|
+
# Grabbing first 7 character (QTLXXXX) for list without duplicates
|
743
|
+
# [filtered_modules.append(f[:7]) for f in onlyfiles if f[:7] not in filtered_modules]
|
744
|
+
|
745
|
+
for item in onlyfiles:
|
746
|
+
x = parse_config_file(item)
|
747
|
+
if x.config_data['HEADER']['DeviceNumbers'][:7] not in filtered_modules:
|
748
|
+
filtered_modules.append(x.config_data['HEADER']['DeviceNumbers'][:7])
|
749
|
+
|
750
|
+
return filtered_modules
|
751
|
+
|
752
|
+
def return_module_signals(module_connection=None, idn_string=None):
|
753
|
+
|
754
|
+
config_path = get_config_path_for_module(idn_string=idn_string, module_connection=module_connection)
|
755
|
+
parsed_file = parse_config_file(config_path)
|
756
|
+
|
757
|
+
signals = []
|
758
|
+
for item in parsed_file.config_data["SIGNALS"]:
|
759
|
+
signals.append(item.name)
|
760
|
+
|
761
|
+
# Future may need signal groups too
|
762
|
+
# signal_groups = []
|
763
|
+
# for item in parsed_file.config_data["SIGNAL_GROUPS"]:
|
764
|
+
# signal_groups.append(item)
|
765
|
+
|
766
|
+
return signals
|
@@ -2,7 +2,7 @@ quarchpy/LICENSE.rst,sha256=SmYK6o5Xs2xRaUwYT-HqNDRu9eygu6y9QwweXNttiRc,3690
|
|
2
2
|
quarchpy/QuarchPy Function Listing.xlsx,sha256=NE68cZPn7b9P3wcnCsnQr3H6yBkt6D5S6dH6457X520,31245
|
3
3
|
quarchpy/README.txt,sha256=-LbrJ5rCPGSxoBmvr9CxJVokQUDwZSjoHa43eN8bWck,980
|
4
4
|
quarchpy/__init__.py,sha256=NQsPKeV7AkmRNKqRsVRXBhzzx0TV0eE3GnmofJEwpTg,3002
|
5
|
-
quarchpy/_version.py,sha256=
|
5
|
+
quarchpy/_version.py,sha256=tfeFT6hXEFiIcb31XwNOeAsWBTy1zmtr6ig_zqUOMhM,27
|
6
6
|
quarchpy/connection.py,sha256=9lPIUP4LCWkFAedc8CWorDY-3ujdAA-cyeNkSBdIv9E,2226
|
7
7
|
quarchpy/run.py,sha256=FS9sa8Z09NLNYhm4ZYtJuZBfMDAPzSB8QHFK2DZnfZA,8702
|
8
8
|
quarchpy/__pycache__/__init__.cpython-38.pyc,sha256=9HU6ut0FlgdY1atAWxzdke2mg6Jgm_pgq8TUuBvIoDA,2142
|
@@ -10,7 +10,8 @@ quarchpy/__pycache__/_version.cpython-38.pyc,sha256=UMEBhGFNnnj6QDVy5VwDzyjftNMZ
|
|
10
10
|
quarchpy/__pycache__/connection.cpython-38.pyc,sha256=ir7cO3aYWjVJJw8H42AJxBcPnrZ5z2q5P9vNk3f-6ws,2118
|
11
11
|
quarchpy/__pycache__/run.cpython-38.pyc,sha256=9Gh8Ppc0qsNaz-PofzISrCtuvE4JP5n8lK_zENxAkVc,7201
|
12
12
|
quarchpy/config_files/__init__.py,sha256=bP9FM5hjP0r7s8qmWqcsVjAfHM0B4XY7cz5sTpe2HDA,290
|
13
|
-
quarchpy/config_files/quarch_config_parser.py,sha256=
|
13
|
+
quarchpy/config_files/quarch_config_parser.py,sha256=RA6KJ3UqDIlCCARMGfqTEZTr-giL7pJ3KdRWUTN6NB8,28342
|
14
|
+
quarchpy/config_files/quarch_config_parser.py.bak,sha256=KgBnDnJIkRHM72lsz5Aerl6h6_1dk6jIibks_Q04La0,28262
|
14
15
|
quarchpy/config_files/Cable_Modules/QTL1253-01 - Mini SAS Module Config v3.5 c1.3.qfg,sha256=hJnFfjsqiNZXYWHx2kn43Gnr2xqmJth4GGVd8VKU5_k,4923
|
15
16
|
quarchpy/config_files/Cable_Modules/QTL1253-01 - Mini SAS Module Config v4.000 c1.3.qfg,sha256=yqU30RxHOo6ltRvK5MhnAhJuEzDpPnt1hQwEC2iQCA8,4989
|
16
17
|
quarchpy/config_files/Cable_Modules/QTL1253-02 - Mini SAS Module Config v3.5 c1.3.qfg,sha256=bpdt3f6sc5wmDNn6g1L-KzhoSB7AwUtDHad2jYgtD-A,4924
|
@@ -718,7 +719,7 @@ quarchpy/utilities/__pycache__/TimeValue.cpython-38.pyc,sha256=4GIB56U829vIZZlXC
|
|
718
719
|
quarchpy/utilities/__pycache__/__init__.cpython-37.pyc,sha256=kqRsK20ckULi5CPPWZW8gDE162DCmVi2uLz968a8z4U,183
|
719
720
|
quarchpy/utilities/__pycache__/__init__.cpython-38.pyc,sha256=nijt9QMW1V99GuRUUOgpVawrPoKQA3WSyjOcz5E4oq0,150
|
720
721
|
quarchpy/utilities/__pycache__/__init__.cpython-39.pyc,sha256=ZE-6REfAg8i2Gw4x6YrmDrBn-RhVipmJpw793mzuaVE,155
|
721
|
-
quarchpy-2.1.10.
|
722
|
-
quarchpy-2.1.10.
|
723
|
-
quarchpy-2.1.10.
|
724
|
-
quarchpy-2.1.10.
|
722
|
+
quarchpy-2.1.10.dev6.dist-info/METADATA,sha256=KIXl_YTfFCl0hpaORYeJlCchAbXB-HOWuTnoV5b81XA,8124
|
723
|
+
quarchpy-2.1.10.dev6.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
|
724
|
+
quarchpy-2.1.10.dev6.dist-info/top_level.txt,sha256=Vc7qsvkVax7oeBaBy_e7kvJvqb_VAAJcW_MuDMmi5W8,9
|
725
|
+
quarchpy-2.1.10.dev6.dist-info/RECORD,,
|
File without changes
|
File without changes
|