pyForceDAQ 2.0.1.dev0__py3-none-any.whl → 2.0.1.dev1__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.
- pyforcedaq/__init__.py +1 -1
- pyforcedaq/__main__.py +2 -2
- pyforcedaq/_lib/misc.py +25 -14
- pyforcedaq/_lib/types.py +1 -1
- pyforcedaq/daq/__init__.py +5 -4
- pyforcedaq/daq/_pyATIDAQ.py +1 -3
- pyforcedaq/force/data_recorder.py +30 -34
- pyforcedaq/force/sensor.py +6 -10
- pyforcedaq/gui/__init__.py +2 -2
- pyforcedaq/gui/_run.py +38 -36
- pyforcedaq/gui/_settings.py +73 -60
- pyforcedaq/gui/launcher.py +23 -22
- {pyforcedaq-2.0.1.dev0.dist-info → pyforcedaq-2.0.1.dev1.dist-info}/METADATA +1 -1
- {pyforcedaq-2.0.1.dev0.dist-info → pyforcedaq-2.0.1.dev1.dist-info}/RECORD +18 -18
- /pyforcedaq/daq/{_daq_read_analog_nidaqmx.py → _use_nidaqmx.py} +0 -0
- /pyforcedaq/daq/{_daq_read_Analog_pydaqmx.py → _use_pydaqmx.py} +0 -0
- {pyforcedaq-2.0.1.dev0.dist-info → pyforcedaq-2.0.1.dev1.dist-info}/WHEEL +0 -0
- {pyforcedaq-2.0.1.dev0.dist-info → pyforcedaq-2.0.1.dev1.dist-info}/entry_points.txt +0 -0
pyforcedaq/__init__.py
CHANGED
pyforcedaq/__main__.py
CHANGED
|
@@ -34,9 +34,9 @@ def cli():
|
|
|
34
34
|
from .gui import launcher
|
|
35
35
|
return launcher.run()
|
|
36
36
|
else:
|
|
37
|
-
gui.
|
|
37
|
+
gui.run_settings(args.SETTINGS_FILE)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
if __name__ == "__main__": # required because of threading
|
|
42
|
-
cli()
|
|
42
|
+
cli()
|
pyforcedaq/_lib/misc.py
CHANGED
|
@@ -39,20 +39,31 @@ class MinMaxDetector(object):
|
|
|
39
39
|
return (self._level_change_time is not None) and \
|
|
40
40
|
(get_time_ms() - self._level_change_time) < self._duration
|
|
41
41
|
|
|
42
|
-
def find_calibration_file(calibration_folder, sensor_name,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
# def find_calibration_file(calibration_folder: str, sensor_name: str,
|
|
43
|
+
# calibration_suffix=".cal") -> str:
|
|
44
|
+
|
|
45
|
+
# needle = 'Serial="{0}"'.format(sensor_name)
|
|
46
|
+
# calibration_files = []
|
|
47
|
+
# for x in listdir(path.abspath(calibration_folder)):
|
|
48
|
+
# filename = path.join(calibration_folder, x)
|
|
49
|
+
# if path.isfile(filename) and filename.endswith(calibration_suffix):
|
|
50
|
+
# with open(filename, "r") as fl:
|
|
51
|
+
# for l in fl:
|
|
52
|
+
# if l.find(needle)>0:
|
|
53
|
+
# print("Found calibration file for sensor '{0}' : {1}.".format(
|
|
54
|
+
# sensor_name, filename))
|
|
55
|
+
# calibration_files.append(filename)
|
|
56
|
+
|
|
57
|
+
# if len(calibration_files) == 1:
|
|
58
|
+
# return calibration_files[0]
|
|
59
|
+
# elif len(calibration_files) > 1:
|
|
60
|
+
# print("Multiple calibration files found for sensor '{0}'".format(sensor_name))
|
|
61
|
+
# for f in calibration_files:
|
|
62
|
+
# print(" - {0}".format(f))
|
|
63
|
+
# print("Please ensure that only one calibration file exists for each sensor")
|
|
64
|
+
# else:
|
|
65
|
+
# print("No calibration file found for sensor '{0}'.".format(sensor_name))
|
|
66
|
+
# exit()
|
|
56
67
|
|
|
57
68
|
#Sensor History with moving average filtering and distance, velocity
|
|
58
69
|
class SensorHistory(object):
|
pyforcedaq/_lib/types.py
CHANGED
pyforcedaq/daq/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
__author__ = "Oliver Lindemann"
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.5"
|
|
3
3
|
|
|
4
4
|
from .. import USE_MOCK_SENSOR
|
|
5
5
|
from ._pyATIDAQ import ATI_CDLL
|
|
@@ -11,9 +11,10 @@ if USE_MOCK_SENSOR:
|
|
|
11
11
|
else:
|
|
12
12
|
#### change import here if you want to use nidaqmx instead of pydaymx ####
|
|
13
13
|
try:
|
|
14
|
-
from .
|
|
15
|
-
#from .
|
|
16
|
-
except (ImportError, ModuleNotFoundError):
|
|
14
|
+
from ._use_pydaqmx import DAQReadAnalog
|
|
15
|
+
#from ._use_nidaqmx import DAQReadAnalog
|
|
16
|
+
except (ImportError, ModuleNotFoundError, NotImplementedError) as e:
|
|
17
|
+
print("Error importing DAQReadAnalog: {0}".format(e))
|
|
17
18
|
print("Warning: PyDAQmx or nidaqmx not found. Using mock sensor instead.")
|
|
18
19
|
from ._mock_sensor import DAQReadAnalog
|
|
19
20
|
|
pyforcedaq/daq/_pyATIDAQ.py
CHANGED
|
@@ -12,8 +12,6 @@ __author__ = "Oliver Lindemann"
|
|
|
12
12
|
from ctypes import *
|
|
13
13
|
from sys import platform
|
|
14
14
|
|
|
15
|
-
from .._lib.misc import find_calibration_file
|
|
16
|
-
|
|
17
15
|
# ### DATA TYPES ####
|
|
18
16
|
VOLTAGE_SAMPLE_TYPE = c_float * 7
|
|
19
17
|
FT_SAMPLE_TYPE = c_float * 6
|
|
@@ -292,7 +290,7 @@ if __name__ == "__main__":
|
|
|
292
290
|
# -0.065867 0.123803 111.156731 0.039974 0.040417 0.079049
|
|
293
291
|
|
|
294
292
|
#filename = raw_input("Calibration file: ")
|
|
295
|
-
filename =
|
|
293
|
+
filename = "calibration/FT30436.cal"
|
|
296
294
|
atidaq = ATI_CDLL()
|
|
297
295
|
# get calibration
|
|
298
296
|
index = c_short(1)
|
|
@@ -7,8 +7,7 @@ __author__ = "Oliver Lindemann"
|
|
|
7
7
|
import atexit
|
|
8
8
|
import gzip
|
|
9
9
|
import logging
|
|
10
|
-
import
|
|
11
|
-
import sys
|
|
10
|
+
from pathlib import Path
|
|
12
11
|
from time import asctime, localtime, strftime
|
|
13
12
|
|
|
14
13
|
from pyforcedaq._lib import timer
|
|
@@ -38,7 +37,7 @@ class DataRecorder(object):
|
|
|
38
37
|
def __init__(self, force_sensor_settings:SensorSettings | list,
|
|
39
38
|
poll_udp_connection=False,
|
|
40
39
|
write_deviceid = False,
|
|
41
|
-
polling_priority=None):
|
|
40
|
+
polling_priority:str | None = None):
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
"""queue_data will be saved
|
|
@@ -82,18 +81,18 @@ class DataRecorder(object):
|
|
|
82
81
|
self._proc_manager.add_subprocess(self.udp)
|
|
83
82
|
self._proc_manager.add_subprocess(self._force_sensor_processes)
|
|
84
83
|
if polling_priority is not None:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
level = PollingPriority.NORMAL
|
|
85
|
+
else:
|
|
86
|
+
level = PollingPriority.get_priority(polling_priority)
|
|
87
|
+
self._proc_manager.set_subprocess_priorities(level=level, disable_gc=False)
|
|
88
88
|
|
|
89
|
-
logging.info("Main process priority:
|
|
90
|
-
self._proc_manager.get_main_priority()))
|
|
89
|
+
logging.info("Main process priority: %s", self._proc_manager.get_main_priority())
|
|
91
90
|
#logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
|
|
92
91
|
|
|
93
92
|
self._is_recording = False
|
|
94
93
|
self._file = None
|
|
95
94
|
self._daq_event = []
|
|
96
|
-
self.filename = None
|
|
95
|
+
self.filename: str | None = None
|
|
97
96
|
atexit.register(self.quit)
|
|
98
97
|
|
|
99
98
|
|
|
@@ -102,7 +101,7 @@ class DataRecorder(object):
|
|
|
102
101
|
"""Property indicates whether the recording processes are alive"""
|
|
103
102
|
try:
|
|
104
103
|
return self._force_sensor_processes[0].is_alive()
|
|
105
|
-
except:
|
|
104
|
+
except Exception:
|
|
106
105
|
return False
|
|
107
106
|
|
|
108
107
|
@property
|
|
@@ -119,7 +118,7 @@ class DataRecorder(object):
|
|
|
119
118
|
return list(map(lambda x:x.sensor_settings,
|
|
120
119
|
self._force_sensor_processes))
|
|
121
120
|
|
|
122
|
-
def quit(self):
|
|
121
|
+
def quit(self) -> list | None:
|
|
123
122
|
"""Stop all recording processes, close data file and quit recording
|
|
124
123
|
|
|
125
124
|
Notes
|
|
@@ -145,7 +144,7 @@ class DataRecorder(object):
|
|
|
145
144
|
|
|
146
145
|
return buffer
|
|
147
146
|
|
|
148
|
-
def process_and_write_udp_events(self):
|
|
147
|
+
def process_and_write_udp_events(self) -> list:
|
|
149
148
|
"""process udp events and return them"""
|
|
150
149
|
buffer = []
|
|
151
150
|
while True:
|
|
@@ -159,9 +158,9 @@ class DataRecorder(object):
|
|
|
159
158
|
self._save_data(buffer)
|
|
160
159
|
return buffer
|
|
161
160
|
|
|
162
|
-
def _save_data(self, data_buffer,
|
|
161
|
+
def _save_data(self, data_buffer: list,
|
|
163
162
|
recording_screen=None,
|
|
164
|
-
float_decimal_places=4):
|
|
163
|
+
float_decimal_places: int = 4) -> None:
|
|
165
164
|
""" writes data to disk and set counters
|
|
166
165
|
|
|
167
166
|
ignores UDP remote control commands
|
|
@@ -199,10 +198,10 @@ class DataRecorder(object):
|
|
|
199
198
|
"Writing {0} of {1} blocks".format(c//BLOCKSIZE,
|
|
200
199
|
buffer_len//BLOCKSIZE)).present()
|
|
201
200
|
|
|
202
|
-
def _file_write(self, str):
|
|
203
|
-
self._file.write(
|
|
201
|
+
def _file_write(self, s: str) -> None:
|
|
202
|
+
self._file.write(s.encode())
|
|
204
203
|
|
|
205
|
-
def save_daq_event(self, code, time=None):
|
|
204
|
+
def save_daq_event(self, code, time: float | None = None) -> None:
|
|
206
205
|
"""Set marker code in file
|
|
207
206
|
|
|
208
207
|
DAQEvent will be timestamps and occur in the data output
|
|
@@ -213,7 +212,7 @@ class DataRecorder(object):
|
|
|
213
212
|
self._daq_event.append(DAQEvents(time = time, code = code))
|
|
214
213
|
|
|
215
214
|
|
|
216
|
-
def start_recording(self, determine_bias=False):
|
|
215
|
+
def start_recording(self, determine_bias: bool = False) -> None:
|
|
217
216
|
"""Start polling process and record
|
|
218
217
|
|
|
219
218
|
See Also
|
|
@@ -233,7 +232,7 @@ class DataRecorder(object):
|
|
|
233
232
|
list(map(lambda x:x.start_polling(), self._force_sensor_processes))
|
|
234
233
|
self._is_recording = True
|
|
235
234
|
|
|
236
|
-
def pause_recording(self, recording_screen=None):
|
|
235
|
+
def pause_recording(self, recording_screen=None) -> list:
|
|
237
236
|
"""Pauses all polling processes and process data
|
|
238
237
|
|
|
239
238
|
returns
|
|
@@ -268,7 +267,7 @@ class DataRecorder(object):
|
|
|
268
267
|
self._daq_event = []
|
|
269
268
|
return data
|
|
270
269
|
|
|
271
|
-
def determine_biases(self, n_samples):
|
|
270
|
+
def determine_biases(self, n_samples: int) -> None:
|
|
272
271
|
"""Record n data samples (n_samples) to determine bias.
|
|
273
272
|
Afterwards recording is in pause mode
|
|
274
273
|
|
|
@@ -289,12 +288,12 @@ class DataRecorder(object):
|
|
|
289
288
|
for x in self._force_sensor_processes:
|
|
290
289
|
x.event_bias_is_available.wait()
|
|
291
290
|
|
|
292
|
-
def open_data_file(self, filename,
|
|
293
|
-
subdirectory="data",
|
|
294
|
-
time_stamp_filename=False,
|
|
295
|
-
varnames = True,
|
|
296
|
-
comment_line="",
|
|
297
|
-
zipped=False):
|
|
291
|
+
def open_data_file(self, filename: str,
|
|
292
|
+
subdirectory: str = "data",
|
|
293
|
+
time_stamp_filename: bool = False,
|
|
294
|
+
varnames: bool = True,
|
|
295
|
+
comment_line: str = "",
|
|
296
|
+
zipped: bool = False) -> Path:
|
|
298
297
|
"""Create a data file
|
|
299
298
|
|
|
300
299
|
Only if data file has been opened, data will be saved!
|
|
@@ -322,11 +321,8 @@ class DataRecorder(object):
|
|
|
322
321
|
full path the actually used file (incl. timestamp)
|
|
323
322
|
|
|
324
323
|
"""
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
data_dir = os.path.join(base_dir, subdirectory)
|
|
328
|
-
if not os.path.isdir(data_dir):
|
|
329
|
-
os.mkdir(data_dir)
|
|
324
|
+
data_dir = Path.cwd() / subdirectory
|
|
325
|
+
data_dir.mkdir(exist_ok=True)
|
|
330
326
|
self.close_data_file()
|
|
331
327
|
|
|
332
328
|
if filename is None or len(filename) == 0:
|
|
@@ -352,8 +348,8 @@ class DataRecorder(object):
|
|
|
352
348
|
else:
|
|
353
349
|
self.filename = flname + suffix
|
|
354
350
|
|
|
355
|
-
full_path_file =
|
|
356
|
-
if
|
|
351
|
+
full_path_file = data_dir / self.filename
|
|
352
|
+
if full_path_file.is_file():
|
|
357
353
|
# print "data file already exists, adding counter"
|
|
358
354
|
cnt += 1
|
|
359
355
|
else:
|
|
@@ -388,7 +384,7 @@ class DataRecorder(object):
|
|
|
388
384
|
|
|
389
385
|
return full_path_file
|
|
390
386
|
|
|
391
|
-
def close_data_file(self):
|
|
387
|
+
def close_data_file(self) -> None:
|
|
392
388
|
"""Close the data file
|
|
393
389
|
|
|
394
390
|
Afterwards data will not be saved anymore.
|
pyforcedaq/force/sensor.py
CHANGED
|
@@ -7,11 +7,12 @@ __author__ = 'Oliver Lindemann'
|
|
|
7
7
|
|
|
8
8
|
import ctypes as ct
|
|
9
9
|
from copy import copy
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
import numpy as np
|
|
13
|
+
from icecream import ic
|
|
12
14
|
|
|
13
15
|
#from .._lib import lsl
|
|
14
|
-
from .._lib.misc import find_calibration_file
|
|
15
16
|
from .._lib.timer import Timer, app_clock
|
|
16
17
|
from .._lib.types import ForceSensorData
|
|
17
18
|
from ..daq import ATI_CDLL, DAQConfiguration, DAQReadAnalog
|
|
@@ -22,7 +23,7 @@ class SensorSettings(DAQConfiguration):
|
|
|
22
23
|
def __init__(self,
|
|
23
24
|
device_id:int,
|
|
24
25
|
sensor_name:str,
|
|
25
|
-
|
|
26
|
+
calibration_file:str | Path,
|
|
26
27
|
channels="ai0:7",
|
|
27
28
|
device_name_prefix = "Dev",
|
|
28
29
|
rate:int=1000,
|
|
@@ -62,13 +63,7 @@ class SensorSettings(DAQConfiguration):
|
|
|
62
63
|
self.write_Tz = write_Tz
|
|
63
64
|
self.write_trigger1 = write_trigger1
|
|
64
65
|
self.write_trigger2 = write_trigger2
|
|
65
|
-
|
|
66
|
-
if self.convert_to_FT:
|
|
67
|
-
self.calibration_file = find_calibration_file(
|
|
68
|
-
calibration_folder=calibration_folder,
|
|
69
|
-
sensor_name=sensor_name)
|
|
70
|
-
else:
|
|
71
|
-
self.calibration_file = None
|
|
66
|
+
self.calibration_file = calibration_file
|
|
72
67
|
|
|
73
68
|
self.reverse_parameters = []
|
|
74
69
|
if not isinstance(reverse_parameter_names, (tuple, list)):
|
|
@@ -105,7 +100,8 @@ class Sensor(DAQReadAnalog):
|
|
|
105
100
|
self.name = settings.sensor_name
|
|
106
101
|
self.convert_to_FT = settings.convert_to_FT
|
|
107
102
|
self.timer = Timer(sync_timer=app_clock) # own timer, because this class is used in own process
|
|
108
|
-
|
|
103
|
+
ic(self.DAQ_TYPE)
|
|
104
|
+
if self.DAQ_TYPE == "mock_sensor":
|
|
109
105
|
self._atidaq = None
|
|
110
106
|
self.convert_to_FT = False
|
|
111
107
|
else:
|
pyforcedaq/gui/__init__.py
CHANGED
pyforcedaq/gui/_run.py
CHANGED
|
@@ -5,7 +5,9 @@ See COPYING file distributed along with the pyForceDAQ copyright and license ter
|
|
|
5
5
|
__author__ = "Oliver Lindemann"
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
from os import path
|
|
8
9
|
from pickle import dumps
|
|
10
|
+
from typing import List
|
|
9
11
|
|
|
10
12
|
import numpy as np
|
|
11
13
|
import pygame
|
|
@@ -342,16 +344,16 @@ def _main_loop(exp, recorder, remote_control=False):
|
|
|
342
344
|
plotter_thread.join()
|
|
343
345
|
recorder.pause_recording(s.background)
|
|
344
346
|
|
|
345
|
-
def
|
|
347
|
+
def run_settings(settings_file: str | None = None):
|
|
346
348
|
|
|
347
349
|
if settings_file is not None and len(settings_file) > 0:
|
|
348
350
|
# load different settings file if specified
|
|
349
351
|
settings.load(settings_file)
|
|
350
352
|
|
|
351
|
-
return
|
|
353
|
+
return run(remote_control = settings.recording.remote_control,
|
|
352
354
|
ask_filename = settings.recording.ask_filename,
|
|
353
355
|
device_ids = settings.recording.device_ids,
|
|
354
|
-
|
|
356
|
+
calibration_files = settings.recording.calibration_files,
|
|
355
357
|
calibration_folder = settings.recording.calibration_folder,
|
|
356
358
|
device_name_prefix = settings.recording.device_name_prefix,
|
|
357
359
|
write_Fx = settings.recording.write_Fx,
|
|
@@ -368,25 +370,25 @@ def run(settings_file: str | None = None):
|
|
|
368
370
|
has_lsl_stream=settings.recording.has_lsl_stream,
|
|
369
371
|
polling_priority=settings.recording.priority)
|
|
370
372
|
|
|
371
|
-
def
|
|
373
|
+
def run(remote_control,
|
|
372
374
|
ask_filename,
|
|
373
|
-
device_ids,
|
|
374
|
-
|
|
375
|
-
calibration_folder,
|
|
376
|
-
device_name_prefix="Dev",
|
|
377
|
-
write_Fx = True,
|
|
378
|
-
write_Fy = True,
|
|
379
|
-
write_Fz = True,
|
|
380
|
-
write_Tx = False,
|
|
381
|
-
write_Ty = False,
|
|
382
|
-
write_Tz = False,
|
|
383
|
-
write_trigger1 = True,
|
|
384
|
-
write_trigger2 = False,
|
|
385
|
-
zip_data=False,
|
|
386
|
-
reverse_scaling = None,
|
|
387
|
-
convert_to_forces = True,
|
|
388
|
-
has_lsl_stream = False,
|
|
389
|
-
polling_priority = None):
|
|
375
|
+
device_ids : int | List[int],
|
|
376
|
+
calibration_files : str | List[str],
|
|
377
|
+
calibration_folder: str,
|
|
378
|
+
device_name_prefix: str="Dev",
|
|
379
|
+
write_Fx: bool = True,
|
|
380
|
+
write_Fy: bool = True,
|
|
381
|
+
write_Fz: bool = True,
|
|
382
|
+
write_Tx: bool = False,
|
|
383
|
+
write_Ty: bool = False,
|
|
384
|
+
write_Tz: bool = False,
|
|
385
|
+
write_trigger1 : bool = True,
|
|
386
|
+
write_trigger2: bool = False,
|
|
387
|
+
zip_data: bool = False,
|
|
388
|
+
reverse_scaling: dict | None = None,
|
|
389
|
+
convert_to_forces: bool = True,
|
|
390
|
+
has_lsl_stream: bool = False,
|
|
391
|
+
polling_priority: str | None = None):
|
|
390
392
|
|
|
391
393
|
"""start gui
|
|
392
394
|
remote_control should be None (ask) or True or False
|
|
@@ -401,27 +403,30 @@ def run_with_options(remote_control,
|
|
|
401
403
|
"""
|
|
402
404
|
#
|
|
403
405
|
|
|
404
|
-
logging.info("New Recording with forceDAQ
|
|
405
|
-
logging.info("Sensors
|
|
406
|
-
logging.info("Settings "
|
|
406
|
+
logging.info("New Recording with forceDAQ %s", forceDAQVersion)
|
|
407
|
+
logging.info("Sensors %s", calibration_files)
|
|
408
|
+
logging.info("Settings %s", settings.recording_as_json())
|
|
407
409
|
|
|
408
410
|
|
|
409
411
|
if not isinstance(device_ids, (list, tuple)):
|
|
410
412
|
device_ids = [device_ids]
|
|
411
|
-
if not isinstance(
|
|
412
|
-
|
|
413
|
+
if not isinstance(calibration_files, (list, tuple)):
|
|
414
|
+
calibration_files = [calibration_files]
|
|
413
415
|
|
|
414
416
|
sensors = []
|
|
415
|
-
for d_id,
|
|
416
|
-
|
|
417
|
-
reverse_parameter_names = reverse_scaling[str(d_id)]
|
|
418
|
-
except:
|
|
417
|
+
for d_id, cal_file in zip(device_ids, calibration_files):
|
|
418
|
+
if reverse_scaling is None:
|
|
419
419
|
reverse_parameter_names = []
|
|
420
|
+
else:
|
|
421
|
+
try:
|
|
422
|
+
reverse_parameter_names = reverse_scaling[str(d_id)]
|
|
423
|
+
except KeyError:
|
|
424
|
+
reverse_parameter_names = []
|
|
420
425
|
|
|
421
426
|
ss = SensorSettings(device_id = d_id,
|
|
422
427
|
device_name_prefix=device_name_prefix,
|
|
423
|
-
sensor_name =
|
|
424
|
-
|
|
428
|
+
sensor_name = cal_file.split(".")[0],
|
|
429
|
+
calibration_file=path.join(calibration_folder, cal_file),
|
|
425
430
|
reverse_parameter_names=reverse_parameter_names,
|
|
426
431
|
rate = settings.gui.sampling_rate,
|
|
427
432
|
convert_to_FT=convert_to_forces,
|
|
@@ -436,11 +441,8 @@ def run_with_options(remote_control,
|
|
|
436
441
|
write_trigger2= write_trigger2)
|
|
437
442
|
sensors.append(ss)
|
|
438
443
|
|
|
439
|
-
|
|
440
|
-
|
|
441
444
|
# expyriment
|
|
442
|
-
control.defaults.
|
|
443
|
-
control.defaults.pause_key = None
|
|
445
|
+
control.defaults.initialise_delay = 0
|
|
444
446
|
control.defaults.window_mode = True
|
|
445
447
|
control.defaults.window_size = (1000, 700)
|
|
446
448
|
control.defaults.fast_quit = True
|
pyforcedaq/gui/_settings.py
CHANGED
|
@@ -1,65 +1,80 @@
|
|
|
1
|
-
import collections
|
|
2
1
|
import json
|
|
3
2
|
import os
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List
|
|
4
6
|
|
|
5
7
|
import tomlkit
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
from icecream import ic
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class _GUISettings:
|
|
13
|
+
sampling_rate: int = 1000
|
|
14
|
+
level_detection_parameter: str = "Fz"
|
|
15
|
+
window_font: str = "freemono"
|
|
16
|
+
moving_average_size: int = 5
|
|
17
|
+
screen_refresh_interval_indicator: int = 300
|
|
18
|
+
gui_screen_refresh_interval_plotter: int = 50
|
|
19
|
+
data_min_max: list = field(default_factory=lambda: [-5, 30])
|
|
20
|
+
plotter_pixel_min_max: list = field(default_factory=lambda: [-250, 250])
|
|
21
|
+
indicator_pixel_min_max: list = field(default_factory=lambda: [-150, 150])
|
|
22
|
+
plot_axis: bool = False
|
|
23
|
+
plot_data_indicator_for_single_sensor: list = field(
|
|
24
|
+
default_factory=lambda: [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)])
|
|
25
|
+
plot_data_plotter_for_single_sensor: list = field(
|
|
26
|
+
default_factory=lambda: [(0, 0), (0, 1), (0, 2)])
|
|
27
|
+
plot_data_indicator_for_two_sensors: list = field(
|
|
28
|
+
default_factory=lambda: [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)])
|
|
29
|
+
plot_data_plotter_for_two_sensors: list = field(
|
|
30
|
+
default_factory=lambda: [(0, 2), (1, 2)])
|
|
31
|
+
|
|
32
|
+
def update_from_dict(self, d):
|
|
33
|
+
for key, value in d.items():
|
|
34
|
+
setattr(self, key, value)
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class _RecordingSetting:
|
|
38
|
+
device_name_prefix: str = "Dev"
|
|
39
|
+
device_ids: List[int] = field(default_factory=lambda: [1])
|
|
40
|
+
ask_filename: bool = False
|
|
41
|
+
remote_control: bool = False
|
|
42
|
+
calibration_folder: str = "calibration"
|
|
43
|
+
calibration_files: List[str] = field(default_factory=lambda: ["FT9334.cal"])
|
|
44
|
+
zip_data: bool = True
|
|
45
|
+
write_Fx: bool = True
|
|
46
|
+
write_Fy: bool = True
|
|
47
|
+
write_Fz: bool = True
|
|
48
|
+
write_Tx: bool = False
|
|
49
|
+
write_Ty: bool = False
|
|
50
|
+
write_Tz: bool = False
|
|
51
|
+
write_trigger1: bool = True
|
|
52
|
+
write_trigger2: bool = False
|
|
53
|
+
has_lsl_stream: bool = False
|
|
54
|
+
reverse_scaling: dict = field(default_factory=lambda: {"1": ["Fz"], "2": ["Fz"]})
|
|
55
|
+
convert_to_forces: bool = True
|
|
56
|
+
priority: str = "normal"
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
if isinstance(self.device_ids, int):
|
|
60
|
+
self.device_ids = [self.device_ids]
|
|
61
|
+
if isinstance(self.calibration_files, str):
|
|
62
|
+
self.calibration_files = [self.calibration_files]
|
|
63
|
+
|
|
64
|
+
def update_from_dict(self, d):
|
|
65
|
+
for key, value in d.items():
|
|
66
|
+
ic(key, value)
|
|
67
|
+
setattr(self, key, value)
|
|
20
68
|
|
|
21
69
|
|
|
22
70
|
class GUISettings(object):
|
|
23
71
|
|
|
24
72
|
def __init__(self, filename):
|
|
25
|
-
|
|
26
73
|
# defaults
|
|
27
|
-
self.gui = _GUISettings(
|
|
28
|
-
sampling_rate = 1000,
|
|
29
|
-
level_detection_parameter = "Fz",
|
|
30
|
-
window_font = "freemono",
|
|
31
|
-
moving_average_size = 5,
|
|
32
|
-
screen_refresh_interval_indicator = 300,
|
|
33
|
-
gui_screen_refresh_interval_plotter = 50,
|
|
34
|
-
data_min_max = [-5, 30],
|
|
35
|
-
plotter_pixel_min_max = [-250, 250],
|
|
36
|
-
indicator_pixel_min_max = [-150, 150],
|
|
37
|
-
plot_axis = False,
|
|
38
|
-
plot_data_indicator_for_single_sensor = [(0, 0), (0, 1), (0, 2), (0, 3),
|
|
39
|
-
(0, 4), (0, 5)], # sensor, parameter
|
|
40
|
-
plot_data_plotter_for_single_sensor = [(0, 0), (0, 1), (0, 2)],
|
|
41
|
-
# plotter can't plot torques # TODO
|
|
42
|
-
|
|
43
|
-
plot_data_indicator_for_two_sensors = [(0, 0), (0, 1), (0, 2), (1, 0),
|
|
44
|
-
(1, 1), (1, 2)], # sensor,
|
|
45
|
-
# parameter
|
|
46
|
-
plot_data_plotter_for_two_sensors = [(0, 2),
|
|
47
|
-
(1, 2)],
|
|
48
|
-
# plotter can't plot torques
|
|
49
|
-
)
|
|
74
|
+
self.gui = _GUISettings()
|
|
50
75
|
self.gui_section = "GUI"
|
|
51
76
|
|
|
52
|
-
self.recording = _RecordingSetting(
|
|
53
|
-
device_ids = [1],
|
|
54
|
-
sensor_names = ["FT9334"],
|
|
55
|
-
calibration_folder="calibration",
|
|
56
|
-
reverse_scaling = {"1": ["Fz"], "2":["Fz"]}, # key: device_id, parameter. E.g.:if x & z dimension of sensor 1 and z dimension of sensor 2 has to be flipped use {1: ["Fx", "Fz"], 2: ["Fz"]}
|
|
57
|
-
remote_control=False, ask_filename= False, write_Fx=True,
|
|
58
|
-
write_Fy=True, write_Fz=True, write_Tx=False, write_Ty=False,
|
|
59
|
-
write_Tz=False, write_trigger1=True, write_trigger2=False,
|
|
60
|
-
has_lsl_stream=False,
|
|
61
|
-
zip_data=True, convert_to_forces=True,
|
|
62
|
-
priority='normal') # default recording settings
|
|
77
|
+
self.recording = _RecordingSetting()
|
|
63
78
|
self.recording_section = "Recording"
|
|
64
79
|
|
|
65
80
|
self.filename = filename
|
|
@@ -69,30 +84,28 @@ class GUISettings(object):
|
|
|
69
84
|
self.save() # defaults
|
|
70
85
|
|
|
71
86
|
def _asdict(self):
|
|
72
|
-
return {self.recording_section: self.recording.
|
|
73
|
-
self.gui_section: self.gui.
|
|
87
|
+
return {self.recording_section: self.recording.__dict__,
|
|
88
|
+
self.gui_section: self.gui.__dict__}
|
|
74
89
|
|
|
75
90
|
def set_gui_settings(self, gui_setting_dict):
|
|
76
91
|
self.gui = _GUISettings(**gui_setting_dict)
|
|
77
92
|
|
|
78
|
-
def
|
|
93
|
+
def set_recording_setting(self, recording_setting_dict):
|
|
79
94
|
self.recording = _RecordingSetting(**recording_setting_dict)
|
|
80
95
|
|
|
81
96
|
def load(self, filename=None):
|
|
82
97
|
if filename is not None:
|
|
83
98
|
self.filename = filename
|
|
84
|
-
with open(self.filename, 'r') as fl:
|
|
99
|
+
with open(self.filename, 'r', encoding='utf-8') as fl:
|
|
85
100
|
d = tomlkit.load(fl)
|
|
86
101
|
self.set_gui_settings(d[self.gui_section])
|
|
87
|
-
self.
|
|
88
|
-
|
|
102
|
+
self.set_recording_setting(d[self.recording_section])
|
|
89
103
|
|
|
90
104
|
def save(self):
|
|
91
|
-
with open(self.filename, 'w') as fl:
|
|
105
|
+
with open(self.filename, 'w', encoding='utf-8') as fl:
|
|
92
106
|
tomlkit.dump(self._asdict(), fl)
|
|
93
107
|
|
|
94
|
-
|
|
95
108
|
def recording_as_json(self):
|
|
96
|
-
return json.dumps(self.recording.
|
|
109
|
+
return json.dumps(self.recording.__dict__)
|
|
97
110
|
|
|
98
|
-
settings = GUISettings(filename="pyForceDAQ.defaults.settings.toml")
|
|
111
|
+
settings = GUISettings(filename="pyForceDAQ.defaults.settings.toml")
|
pyforcedaq/gui/launcher.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from os import path
|
|
3
|
+
from typing import List
|
|
3
4
|
|
|
4
5
|
import FreeSimpleGUI as _sg
|
|
5
6
|
|
|
6
7
|
from .. import USE_MOCK_SENSOR, __version__
|
|
7
|
-
from .._lib.misc import find_calibration_file
|
|
8
8
|
from .._lib.types import PollingPriority
|
|
9
9
|
from .._lib.udp_connection import UDPConnection
|
|
10
|
-
from ._run import
|
|
10
|
+
from ._run import run_settings as _gui_run
|
|
11
11
|
from ._settings import settings
|
|
12
12
|
|
|
13
13
|
|
|
@@ -37,23 +37,25 @@ def _s2l(csv_string, is_integer=False, is_float=False): # convert csv string to
|
|
|
37
37
|
return rtn
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def _check_sensor_calibration_settings(device_ids
|
|
41
|
-
|
|
40
|
+
def _check_sensor_calibration_settings(device_ids: List[int],
|
|
41
|
+
calibrations_files : List[str],
|
|
42
|
+
calibration_folder :str):
|
|
42
43
|
rtn = []
|
|
43
44
|
for x, d_id in enumerate(device_ids):
|
|
44
45
|
error = False
|
|
45
46
|
try:
|
|
46
|
-
|
|
47
|
-
except:
|
|
48
|
-
|
|
49
|
-
error = True
|
|
50
|
-
try:
|
|
51
|
-
cal = find_calibration_file(calibration_folder=calibration_folder,
|
|
52
|
-
sensor_name=sensor_name)
|
|
53
|
-
except:
|
|
54
|
-
cal = "NO CALIBRATION"
|
|
47
|
+
cal_file = calibrations_files[x]
|
|
48
|
+
except KeyError:
|
|
49
|
+
cal_file = "??"
|
|
55
50
|
error = True
|
|
56
|
-
|
|
51
|
+
|
|
52
|
+
if not error:
|
|
53
|
+
cal = path.join(calibration_folder, cal_file)
|
|
54
|
+
if not path.isfile(cal):
|
|
55
|
+
cal = "NOT FOUND"
|
|
56
|
+
error = True
|
|
57
|
+
|
|
58
|
+
rtn.append([d_id, cal_file, cal, error])
|
|
57
59
|
|
|
58
60
|
return rtn
|
|
59
61
|
|
|
@@ -68,7 +70,7 @@ def _windows_run():
|
|
|
68
70
|
|
|
69
71
|
for d_id, name, cal, error in _check_sensor_calibration_settings(
|
|
70
72
|
s.device_ids,
|
|
71
|
-
s.
|
|
73
|
+
s.calibration_files,
|
|
72
74
|
s.calibration_folder):
|
|
73
75
|
if error:
|
|
74
76
|
col = "red"
|
|
@@ -135,8 +137,8 @@ def _window_settings():
|
|
|
135
137
|
[[_sg.Text("Folder:", size=(5, 1)), _sg.InputText(
|
|
136
138
|
s.calibration_folder, size=(23, 1), key="cal_dir"),
|
|
137
139
|
_sg.FolderBrowse(size=(6, 1))],
|
|
138
|
-
_input_text_list("
|
|
139
|
-
key="
|
|
140
|
+
_input_text_list("Cal-files:", s.calibration_files,
|
|
141
|
+
key="calibration_files")
|
|
140
142
|
])])
|
|
141
143
|
|
|
142
144
|
layout.append([_sg.Frame('Record Forces & Torques',
|
|
@@ -190,7 +192,7 @@ def _window_settings():
|
|
|
190
192
|
except:
|
|
191
193
|
event = "Error"
|
|
192
194
|
|
|
193
|
-
key = "
|
|
195
|
+
key = "calibration_files"
|
|
194
196
|
try:
|
|
195
197
|
d[key] = _s2l(values[key])
|
|
196
198
|
except:
|
|
@@ -207,7 +209,7 @@ def _window_settings():
|
|
|
207
209
|
main_path = path.split(sys.modules['__main__'].__file__)[0] + path.sep
|
|
208
210
|
d["calibration_folder"] = values["cal_dir"].replace(main_path, "")
|
|
209
211
|
|
|
210
|
-
settings.
|
|
212
|
+
settings.set_recording_setting(d)
|
|
211
213
|
settings.save()
|
|
212
214
|
|
|
213
215
|
window.close()
|
|
@@ -220,9 +222,8 @@ def run():
|
|
|
220
222
|
s = settings.recording
|
|
221
223
|
settings_error = False
|
|
222
224
|
n_sensor = len(s.device_ids)
|
|
223
|
-
if n_sensor != len(s.
|
|
224
|
-
_sg.PopupError("Number of devices IDs and
|
|
225
|
-
"not equal.")
|
|
225
|
+
if n_sensor != len(s.calibration_files):
|
|
226
|
+
_sg.PopupError("Number of devices IDs and calibration files are not equal.")
|
|
226
227
|
settings_error = True
|
|
227
228
|
|
|
228
229
|
if not path.isdir(s.calibration_folder):
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
pyforcedaq/__init__.py,sha256=
|
|
2
|
-
pyforcedaq/__main__.py,sha256=
|
|
1
|
+
pyforcedaq/__init__.py,sha256=esQLa14fkmOHX5vEjv9AGAB6x5ETZ-EbTSDSZNSSoEw,1151
|
|
2
|
+
pyforcedaq/__main__.py,sha256=TkcXcjMoypKTpi5cmG5yfJRaRHzpAGuMyRysl1bPLZ4,1002
|
|
3
3
|
pyforcedaq/_lib/__init__.py,sha256=GBiextmC_Xz9iMQlLBnU1qsw7YFVQaiZ8nV5ln3B7Wk,32
|
|
4
4
|
pyforcedaq/_lib/lsl.py,sha256=6bLniyIy2hWB36-uclrO-9WJZogV2PoY1Lt2qq5JaR4,1520
|
|
5
|
-
pyforcedaq/_lib/misc.py,sha256=
|
|
5
|
+
pyforcedaq/_lib/misc.py,sha256=YiG2fTac9YFYgWZac4iQ1j7BBhFo-y80-231xk1Gzds,4622
|
|
6
6
|
pyforcedaq/_lib/polling_time_profile.py,sha256=5EHSWMVii-bEKk6dkuClH6OZnbSSJjrvW6hIfizS988,1487
|
|
7
7
|
pyforcedaq/_lib/process_priority_manager.py,sha256=3povqR0hT54K9TRQtxk-rc8JPakT3K6PH4Np8pNgydo,4196
|
|
8
8
|
pyforcedaq/_lib/timer.py,sha256=MN192NvqxezW-C5-znse9p-sv0DdoPV6jVN3X8d6CYY,1111
|
|
9
|
-
pyforcedaq/_lib/types.py,sha256=
|
|
9
|
+
pyforcedaq/_lib/types.py,sha256=haJORth-LE38G2bUpZJ7oudd_UUjH6HVRANiAbBNABs,11978
|
|
10
10
|
pyforcedaq/_lib/udp_connection.py,sha256=k6Ww8n9r7PdOzC0AJmzs-sC9eW8Jnij3EyUR0ASJHas,10254
|
|
11
|
-
pyforcedaq/daq/__init__.py,sha256=
|
|
12
|
-
pyforcedaq/daq/_daq_read_Analog_pydaqmx.py,sha256=0jtLoY82CBq1UxoOQMaCylKAOs_AzpK6mL2UUIdkhsw,3568
|
|
13
|
-
pyforcedaq/daq/_daq_read_analog_nidaqmx.py,sha256=MHUC83UnKbHYAlNWr5iuTFoGYJpSfB21cxkm41qS_Vg,2459
|
|
11
|
+
pyforcedaq/daq/__init__.py,sha256=ZrRK8xFcoi523Rk9I7DQcY-LyCm21hFcmQOXP7VwwYw,716
|
|
14
12
|
pyforcedaq/daq/_mock_sensor.py,sha256=GsNKDz87lAUIF8eRyP18s9b3gQy6WzL1r3sbKa1fur8,2076
|
|
15
|
-
pyforcedaq/daq/_pyATIDAQ.py,sha256=
|
|
13
|
+
pyforcedaq/daq/_pyATIDAQ.py,sha256=IqTdz2nAxEC8ea_u9OQvvCmT-fbOwkP85FBHV3gyIfQ,11142
|
|
14
|
+
pyforcedaq/daq/_use_nidaqmx.py,sha256=MHUC83UnKbHYAlNWr5iuTFoGYJpSfB21cxkm41qS_Vg,2459
|
|
15
|
+
pyforcedaq/daq/_use_pydaqmx.py,sha256=0jtLoY82CBq1UxoOQMaCylKAOs_AzpK6mL2UUIdkhsw,3568
|
|
16
16
|
pyforcedaq/daq/config.py,sha256=JinPGtz4WYBj8uLT9LZkzQq-fSe11st8A0acrjJ015o,481
|
|
17
17
|
pyforcedaq/extras/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
pyforcedaq/extras/convert.py,sha256=N8r5gl0Te3GHafkggeC4btFD94nUwBzM9cctH1jiSEA,8601
|
|
@@ -22,21 +22,21 @@ pyforcedaq/extras/read_force_data.py,sha256=ZjrEHexz40OW8L8JlRFgAFxQuBAS8c66t2-j
|
|
|
22
22
|
pyforcedaq/extras/remote_control.py,sha256=2Ka9Y6edTAnsIY-P46rzyLDj9ddOc51n4cJ2Mma_cKg,2204
|
|
23
23
|
pyforcedaq/force/__init__.py,sha256=h9pQxFkwt_xOs1hrTYCiEPFr1Cg2XblxwyKEDuKH8rs,346
|
|
24
24
|
pyforcedaq/force/_log.py,sha256=h2JbbLLJsrrh1U9HwEbY7iFdRW7FYxDbiZuIq5Z3sE0,541
|
|
25
|
-
pyforcedaq/force/data_recorder.py,sha256=
|
|
26
|
-
pyforcedaq/force/sensor.py,sha256
|
|
25
|
+
pyforcedaq/force/data_recorder.py,sha256=7hWM7WKD94uEv57_R1RydJwf7HAQBRyNN3gDRcN9cRc,12939
|
|
26
|
+
pyforcedaq/force/sensor.py,sha256=ZdeK6pEedEosrk_4SlG3mEOji6zBRaopSg0oEV6ZBM8,6810
|
|
27
27
|
pyforcedaq/force/sensor_process.py,sha256=gBZ4B3Z7CDz87QISUbMJ0tsSf-SBT1rbl3dxc0Eo-To,9319
|
|
28
|
-
pyforcedaq/gui/__init__.py,sha256=
|
|
28
|
+
pyforcedaq/gui/__init__.py,sha256=bB1kI51Oi3fK-x_J7ncaN8yZpvai-5-3sp7UHqDr-GY,147
|
|
29
29
|
pyforcedaq/gui/_gui_status.py,sha256=0YJcxVX2G8VtbgRn0H9avZi1wY4R-1A8re3cJjojkl0,13987
|
|
30
30
|
pyforcedaq/gui/_layout.py,sha256=obptKZgw9CgygiqDCDgbXu4Esyfb8btOZgxxdNzgXiE,4616
|
|
31
31
|
pyforcedaq/gui/_level_indicator.py,sha256=cBB-5DOlvdc95rQQ12Gt5dvhNxSYQTbaq19FSdz4YnQ,1905
|
|
32
32
|
pyforcedaq/gui/_pg_surface.py,sha256=TD4rQJsBMMiKoQDZTon35LvsEmyayCWfklJbm0iRl6c,2902
|
|
33
33
|
pyforcedaq/gui/_plotter.py,sha256=KvSxg3M3mDRj0_oNtn6mll8D2LBrBoboNoUs9hJJSUs,8451
|
|
34
|
-
pyforcedaq/gui/_run.py,sha256=
|
|
34
|
+
pyforcedaq/gui/_run.py,sha256=9IsWsnVVcRmk12aBxMPDVmNB0QcFLHVivpVIHBVQ8yg,23368
|
|
35
35
|
pyforcedaq/gui/_scaling.py,sha256=XgdB7CXqzrBXNFT1Kul8t7mebqO9gojNxGzNIppj4Bg,1905
|
|
36
|
-
pyforcedaq/gui/_settings.py,sha256=
|
|
36
|
+
pyforcedaq/gui/_settings.py,sha256=rSn3OgOx7mTkS7RCH_v7YPhugTKsDivmKqgzE1FvPOI,3772
|
|
37
37
|
pyforcedaq/gui/forceDAQ_logo.png,sha256=-bn6ZQQjKf2Pxd9mJ4r-wulibtjuv_bEkhG567GR1vc,15624
|
|
38
|
-
pyforcedaq/gui/launcher.py,sha256=
|
|
39
|
-
pyforcedaq-2.0.1.
|
|
40
|
-
pyforcedaq-2.0.1.
|
|
41
|
-
pyforcedaq-2.0.1.
|
|
42
|
-
pyforcedaq-2.0.1.
|
|
38
|
+
pyforcedaq/gui/launcher.py,sha256=5IjHTCU65bsoctr52k-w9L6MBTJOW4gN6Wm0PItUwX0,8753
|
|
39
|
+
pyforcedaq-2.0.1.dev1.dist-info/WHEEL,sha256=i9aSRDivn5iP9LaR1BLQX2GNAuriQWPsFwbbWygTX2k,81
|
|
40
|
+
pyforcedaq-2.0.1.dev1.dist-info/entry_points.txt,sha256=0VDGqAldPs3uUCun3y9v4ue8Uae8n-D6-R5YpamgJwQ,56
|
|
41
|
+
pyforcedaq-2.0.1.dev1.dist-info/METADATA,sha256=4RVrVMtGkSX5ibhek7BBHpWW4P25Qikz70QB2hk0VxQ,601
|
|
42
|
+
pyforcedaq-2.0.1.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|