pyForceDAQ 2.0.4__tar.gz → 2.0.5.dev2__tar.gz
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-2.0.4 → pyforcedaq-2.0.5.dev2}/PKG-INFO +1 -4
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/pyproject.toml +14 -4
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/__main__.py +27 -4
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/data_recorder.py +8 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/process_priority_manager.py +1 -1
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/sensor.py +32 -17
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/sensor_process.py +17 -10
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/settings.py +2 -2
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/udp_connection.py +1 -5
- pyforcedaq-2.0.5.dev2/src/pyforcedaq/constants.py +12 -0
- pyforcedaq-2.0.5.dev2/src/pyforcedaq/daq/__init__.py +68 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_dll.py → pyforcedaq-2.0.5.dev2/src/pyforcedaq/daq/calibration_dll.py +5 -3
- pyforcedaq-2.0.5.dev2/src/pyforcedaq/daq/calibration_iaftt.py +17 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_mock_sensor.py → pyforcedaq-2.0.5.dev2/src/pyforcedaq/daq/read_daq_mock_sensor.py +4 -2
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_use_nidaqmx.py → pyforcedaq-2.0.5.dev2/src/pyforcedaq/daq/read_daq_nidaqmx.py +9 -6
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_use_pydaqmx.py → pyforcedaq-2.0.5.dev2/src/pyforcedaq/daq/read_daq_pydaqmx.py +4 -2
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/launcher.py +4 -5
- pyforcedaq-2.0.4/src/pyforcedaq/_lib/polling_time_profile.py +0 -57
- pyforcedaq-2.0.4/src/pyforcedaq/constants.py +0 -9
- pyforcedaq-2.0.4/src/pyforcedaq/daq/__init__.py +0 -28
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_iaftt.py +0 -17
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/clock.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/lsl.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/misc.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/_lib/types.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/daq/_pyATIDAQ.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/extras/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/extras/convert.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/extras/read_force_data.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/force.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_gui_status.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_layout.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_level_indicator.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_pg_surface.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_plotter.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_run.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/_scaling.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev2}/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyForceDAQ
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5.dev2
|
|
4
4
|
Summary: A Python package for data acquisition and analysis in force-based experiments.
|
|
5
5
|
Author: Oliver Lindemann
|
|
6
6
|
Author-email: Oliver Lindemann <lindemann@essb.eur.nl>
|
|
7
7
|
Requires-Dist: atiiaftt>=0.1.1
|
|
8
8
|
Requires-Dist: expyriment>=1.0.1
|
|
9
|
-
Requires-Dist: icecream>=2.2.0
|
|
10
9
|
Requires-Dist: nidaqmx>=1.5.0
|
|
11
10
|
Requires-Dist: numpy>=2.4.4
|
|
12
11
|
Requires-Dist: psutil>=7.2.2
|
|
13
|
-
Requires-Dist: pydaqmx>=1.4.7
|
|
14
12
|
Requires-Dist: pylsl>=1.18.2
|
|
15
13
|
Requires-Dist: pysimplegui>=6.0
|
|
16
|
-
Requires-Dist: pyxdf>=1.17.4
|
|
17
14
|
Requires-Dist: tomlkit>=0.15.0
|
|
18
15
|
Requires-Python: >=3.12, <3.14
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyForceDAQ"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.5-dev2"
|
|
4
4
|
description = "A Python package for data acquisition and analysis in force-based experiments."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Oliver Lindemann", email = "lindemann@essb.eur.nl" },
|
|
@@ -9,14 +9,11 @@ requires-python = ">=3.12, <3.14"
|
|
|
9
9
|
dependencies = [
|
|
10
10
|
"atiiaftt>=0.1.1",
|
|
11
11
|
"expyriment>=1.0.1",
|
|
12
|
-
"icecream>=2.2.0",
|
|
13
12
|
"nidaqmx>=1.5.0",
|
|
14
13
|
"numpy>=2.4.4",
|
|
15
14
|
"psutil>=7.2.2",
|
|
16
|
-
"pydaqmx>=1.4.7",
|
|
17
15
|
"pylsl>=1.18.2",
|
|
18
16
|
"pysimplegui>=6.0",
|
|
19
|
-
"pyxdf>=1.17.4",
|
|
20
17
|
"tomlkit>=0.15.0",
|
|
21
18
|
]
|
|
22
19
|
|
|
@@ -27,5 +24,18 @@ dependencies = [
|
|
|
27
24
|
requires = ["uv_build>=0.11.15,<0.12.0"]
|
|
28
25
|
build-backend = "uv_build"
|
|
29
26
|
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"icecream>=2.2.0",
|
|
30
|
+
"ipython>=9.14.0",
|
|
31
|
+
"ruff>=0.15.15",
|
|
32
|
+
]
|
|
33
|
+
pydaqmx = [
|
|
34
|
+
"pydaqmx>=1.4.7",
|
|
35
|
+
]
|
|
36
|
+
test = [
|
|
37
|
+
"pytest>=9.0.3",
|
|
38
|
+
]
|
|
39
|
+
|
|
30
40
|
[project.scripts]
|
|
31
41
|
pyforcedaq = "pyforcedaq.__main__:cli"
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
|
|
3
|
-
from . import __author__, __version__
|
|
3
|
+
from . import __author__, __version__, constants
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
def print_version():
|
|
7
|
+
print("+" + "-" * 23 + "+")
|
|
8
|
+
print(f"| pyforceDAQ {__version__}".ljust(24) + "|")
|
|
9
|
+
print("+" + "-" * 23 + "+")
|
|
10
|
+
|
|
6
11
|
def cli():
|
|
7
12
|
|
|
8
13
|
parser = argparse.ArgumentParser(
|
|
@@ -31,12 +36,29 @@ def cli():
|
|
|
31
36
|
default=False,
|
|
32
37
|
help="Omit launcher GUI and start recording directly",
|
|
33
38
|
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--mock",
|
|
42
|
+
action="store_true",
|
|
43
|
+
default=False,
|
|
44
|
+
help="Use mock sensor",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--dll",
|
|
49
|
+
action="store_true",
|
|
50
|
+
default=False,
|
|
51
|
+
help="Use self compiled ATI DLL",
|
|
52
|
+
)
|
|
37
53
|
|
|
38
54
|
args = parser.parse_args()
|
|
55
|
+
if args.mock:
|
|
56
|
+
constants.DAQ_TYPE = constants.MOCK_SENSOR
|
|
57
|
+
else:
|
|
58
|
+
constants.DAQ_TYPE = constants.NIDAQMX # use NI-DAQmx
|
|
59
|
+
constants.USE_AIFTT = not args.dll
|
|
39
60
|
|
|
61
|
+
print_version()
|
|
40
62
|
if not args.omit_launcher:
|
|
41
63
|
if len(args.SETTINGS_FILE) > 0:
|
|
42
64
|
print("Can't use launcher and settings file")
|
|
@@ -53,3 +75,4 @@ def cli():
|
|
|
53
75
|
|
|
54
76
|
if __name__ == "__main__": # required because of threading
|
|
55
77
|
cli()
|
|
78
|
+
cli()
|
|
@@ -14,6 +14,7 @@ from time import asctime, localtime, strftime
|
|
|
14
14
|
from typing import List
|
|
15
15
|
|
|
16
16
|
from .. import __version__ as forceDAQVersion
|
|
17
|
+
from .. import constants
|
|
17
18
|
from .clock import wait_ms
|
|
18
19
|
from .misc import set_logging
|
|
19
20
|
from .process_priority_manager import ProcessPriorityManager
|
|
@@ -49,6 +50,11 @@ class DataRecorder(object):
|
|
|
49
50
|
|
|
50
51
|
polling_priority has to be types.PollingPriority.{HIGH},
|
|
51
52
|
{REALTIME} or {NORMAL} or None
|
|
53
|
+
|
|
54
|
+
You can change the used modules by settings the following constants before creating the
|
|
55
|
+
DataRecorder instance:
|
|
56
|
+
* set constants.DAQ_TYPE to constants.PYDAQMX, constants.NIDAQMX or constants.MOCK_SENSOR
|
|
57
|
+
* set constants.USE_AIFTT to True or False
|
|
52
58
|
"""
|
|
53
59
|
|
|
54
60
|
if not isinstance(force_sensor_settings, list):
|
|
@@ -66,6 +72,8 @@ class DataRecorder(object):
|
|
|
66
72
|
fst = SensorProcess(
|
|
67
73
|
sensor_settings=fs,
|
|
68
74
|
recording_settings=recording_settings,
|
|
75
|
+
daq_type=constants.DAQ_TYPE,
|
|
76
|
+
use_aiftt=constants.USE_AIFTT,
|
|
69
77
|
pipe_buffered_data_after_pause=True,
|
|
70
78
|
)
|
|
71
79
|
fst.start()
|
|
@@ -16,7 +16,7 @@ class ProcessPriorityManager(object):
|
|
|
16
16
|
platform = sys.platform
|
|
17
17
|
pybits = 32 + int(sys.maxsize > 2**32) * 32
|
|
18
18
|
main_process_id = psutil.Process().pid
|
|
19
|
-
_normal_nice_value = psutil.Process().nice() #
|
|
19
|
+
_normal_nice_value = psutil.Process().nice() # used on Linux
|
|
20
20
|
|
|
21
21
|
def __init__(self):
|
|
22
22
|
self._subprocs = []
|
|
@@ -7,38 +7,53 @@ __author__ = "Oliver Lindemann"
|
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
|
-
from ..
|
|
10
|
+
from .. import constants
|
|
11
11
|
from .clock import local_clock
|
|
12
12
|
from .settings import SensorSettings
|
|
13
13
|
from .types import ForceSensorData
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class Sensor(
|
|
16
|
+
class Sensor(object):
|
|
17
17
|
# channel 0:5 for FT sensor, channel 6 for trigger
|
|
18
18
|
SENSOR_CHANNELS = range(0, 5 + 1)
|
|
19
19
|
# channel 7 for trigger synchronization validation
|
|
20
20
|
TRIGGER_CHANNELS = range(5, 6 + 1)
|
|
21
21
|
|
|
22
|
-
def __init__(self, s_settings: SensorSettings
|
|
22
|
+
def __init__(self, s_settings: SensorSettings,
|
|
23
|
+
daq_type: int,
|
|
24
|
+
use_aiftt: bool):
|
|
23
25
|
"""DOC"""
|
|
24
26
|
|
|
25
27
|
assert isinstance(s_settings, SensorSettings)
|
|
26
28
|
assert len(self.SENSOR_CHANNELS) == len(ForceSensorData.forces_names)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
if daq_type == constants.NIDAQMX:
|
|
31
|
+
from ..daq.read_daq_nidaqmx import DAQReadAnalog
|
|
32
|
+
elif daq_type == constants.PYDAQMX:
|
|
33
|
+
from ..daq.read_daq_pydaqmx import DAQReadAnalog
|
|
34
|
+
elif daq_type == constants.MOCK_SENSOR:
|
|
35
|
+
from ..daq.read_daq_mock_sensor import DAQReadAnalog
|
|
36
|
+
else:
|
|
37
|
+
raise RuntimeError(f"Unsupported daq_type: {daq_type}")
|
|
38
|
+
|
|
39
|
+
if use_aiftt:
|
|
40
|
+
from ..daq.calibration_iaftt import CalibrationConverter
|
|
41
|
+
else:
|
|
42
|
+
from ..daq.calibration_dll import CalibrationConverter
|
|
43
|
+
|
|
44
|
+
self.daq = DAQReadAnalog(configuration=s_settings,
|
|
30
45
|
read_array_size_in_samples=len(Sensor.SENSOR_CHANNELS)
|
|
31
|
-
+ len(Sensor.TRIGGER_CHANNELS)
|
|
32
|
-
)
|
|
46
|
+
+ len(Sensor.TRIGGER_CHANNELS))
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
self.device_label = s_settings.device_label
|
|
36
|
-
self.convert_to_FT = s_settings.convert_to_FT
|
|
37
|
-
if self.DAQ_TYPE == "mock_sensor":
|
|
48
|
+
if daq_type == constants.MOCK_SENSOR:
|
|
38
49
|
self._calib_converter = None
|
|
39
50
|
else:
|
|
40
51
|
self._calib_converter = CalibrationConverter(s_settings.calibration_file)
|
|
41
52
|
|
|
53
|
+
self.sensor_id = s_settings.sensor_id
|
|
54
|
+
self.device_label = s_settings.device_label
|
|
55
|
+
self.convert_to_FT = s_settings.convert_to_FT
|
|
56
|
+
|
|
42
57
|
self._reverse_vector = np.ones(len(ForceSensorData.forces_names))
|
|
43
58
|
if s_settings.reverse_parameter_names is not None:
|
|
44
59
|
if isinstance(s_settings.reverse_parameter_names, str):
|
|
@@ -55,11 +70,11 @@ class Sensor(DAQReadAnalog):
|
|
|
55
70
|
def determine_bias(self, n_samples=100):
|
|
56
71
|
"""determines the bias"""
|
|
57
72
|
|
|
58
|
-
task_was_running = self.
|
|
59
|
-
self.start_data_acquisition()
|
|
73
|
+
task_was_running = self.daq.is_acquiring_data
|
|
74
|
+
self.daq.start_data_acquisition()
|
|
60
75
|
data = None
|
|
61
76
|
for _ in range(n_samples):
|
|
62
|
-
read_buffer,
|
|
77
|
+
read_buffer, _ = self.daq.read_analog()
|
|
63
78
|
sample = read_buffer[Sensor.SENSOR_CHANNELS]
|
|
64
79
|
if data is None:
|
|
65
80
|
data = sample
|
|
@@ -67,7 +82,7 @@ class Sensor(DAQReadAnalog):
|
|
|
67
82
|
data = np.vstack((data, sample))
|
|
68
83
|
|
|
69
84
|
if not task_was_running:
|
|
70
|
-
self.stop_data_acquisition()
|
|
85
|
+
self.daq.stop_data_acquisition()
|
|
71
86
|
|
|
72
87
|
if self._calib_converter is not None and isinstance(data, np.ndarray):
|
|
73
88
|
self._calib_converter.bias(np.mean(data, axis=0))
|
|
@@ -88,7 +103,7 @@ class Sensor(DAQReadAnalog):
|
|
|
88
103
|
"""
|
|
89
104
|
|
|
90
105
|
start = local_clock()
|
|
91
|
-
data, _read_samples = self.read_analog()
|
|
106
|
+
data, _read_samples = self.daq.read_analog()
|
|
92
107
|
if self.convert_to_FT and self._calib_converter is not None:
|
|
93
108
|
forces = np.asarray(
|
|
94
109
|
self._calib_converter.convertToFT(voltages=data[Sensor.SENSOR_CHANNELS])
|
|
@@ -107,4 +122,4 @@ class Sensor(DAQReadAnalog):
|
|
|
107
122
|
sensor_id=self.sensor_id,
|
|
108
123
|
forces=forces,
|
|
109
124
|
trigger=data[Sensor.TRIGGER_CHANNELS],
|
|
110
|
-
)
|
|
125
|
+
)
|
|
@@ -8,9 +8,9 @@ from multiprocessing import Array, Event, Pipe, Process, Value
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from numpy import typing as npt
|
|
10
10
|
|
|
11
|
+
from .. import constants
|
|
11
12
|
from .clock import local_clock, wait_ms
|
|
12
13
|
from .lsl import LSLSream, cf_float32
|
|
13
|
-
from .polling_time_profile import PollingTimeProfile
|
|
14
14
|
from .process_priority_manager import get_priority
|
|
15
15
|
from .sensor import Sensor
|
|
16
16
|
from .settings import RecordingSettings, SensorSettings
|
|
@@ -22,6 +22,8 @@ class SensorProcess(Process):
|
|
|
22
22
|
self,
|
|
23
23
|
sensor_settings: SensorSettings,
|
|
24
24
|
recording_settings: RecordingSettings,
|
|
25
|
+
daq_type: int,
|
|
26
|
+
use_aiftt: bool,
|
|
25
27
|
pipe_buffered_data_after_pause=True,
|
|
26
28
|
chunk_size=10000,
|
|
27
29
|
):
|
|
@@ -43,6 +45,12 @@ class SensorProcess(Process):
|
|
|
43
45
|
)
|
|
44
46
|
|
|
45
47
|
super(SensorProcess, self).__init__()
|
|
48
|
+
|
|
49
|
+
if daq_type not in [constants.NIDAQMX, constants.PYDAQMX, constants.MOCK_SENSOR]:
|
|
50
|
+
raise RuntimeError(f"Unsupported daq_type: {daq_type}")
|
|
51
|
+
|
|
52
|
+
self._daq_type = daq_type
|
|
53
|
+
self._use_aiftt = use_aiftt
|
|
46
54
|
self.sensor_settings = sensor_settings
|
|
47
55
|
self.recording_settings = recording_settings
|
|
48
56
|
self._pipe_buffer_after_pause = pipe_buffered_data_after_pause
|
|
@@ -152,14 +160,16 @@ class SensorProcess(Process):
|
|
|
152
160
|
def run(self):
|
|
153
161
|
buffer = []
|
|
154
162
|
self._buffer_size.value = 0
|
|
155
|
-
sensor = Sensor(self.sensor_settings
|
|
163
|
+
sensor = Sensor(self.sensor_settings,
|
|
164
|
+
daq_type=self._daq_type,
|
|
165
|
+
use_aiftt=self._use_aiftt)
|
|
166
|
+
|
|
156
167
|
stream_forces = self.recording_settings.array_write_forces()
|
|
157
168
|
stream_trigger = self.recording_settings.array_write_trigger()
|
|
158
169
|
|
|
159
170
|
self._event_is_polling.clear()
|
|
160
171
|
self._event_sending_data.clear()
|
|
161
172
|
is_polling = False
|
|
162
|
-
ptp = PollingTimeProfile() # TODO just for testing?
|
|
163
173
|
|
|
164
174
|
## create init LSL
|
|
165
175
|
lsl_data_steam = LSLSream()
|
|
@@ -190,7 +200,7 @@ class SensorProcess(Process):
|
|
|
190
200
|
if not is_polling:
|
|
191
201
|
# start NI device and acquire one first sample to
|
|
192
202
|
# ensure good timing
|
|
193
|
-
sensor.start_data_acquisition()
|
|
203
|
+
sensor.daq.start_data_acquisition()
|
|
194
204
|
buffer.append(
|
|
195
205
|
DAQEvents(
|
|
196
206
|
time=local_clock(), code="started:" + sensor.device_label
|
|
@@ -215,7 +225,6 @@ class SensorProcess(Process):
|
|
|
215
225
|
if any(tr): # only stream if at least one trigger is active
|
|
216
226
|
lsl_hardware_trigger_stream.outlet.push_sample(tr) # type: ignore
|
|
217
227
|
|
|
218
|
-
ptp.update(d.time) # needed? TODO
|
|
219
228
|
self._dat[:] = d.forces
|
|
220
229
|
self._sample_cnt.value += 1 # type: ignore
|
|
221
230
|
|
|
@@ -230,7 +239,7 @@ class SensorProcess(Process):
|
|
|
230
239
|
else:
|
|
231
240
|
# pause: not polling
|
|
232
241
|
if is_polling:
|
|
233
|
-
sensor.stop_data_acquisition()
|
|
242
|
+
sensor.daq.stop_data_acquisition()
|
|
234
243
|
buffer.append(
|
|
235
244
|
DAQEvents(
|
|
236
245
|
time=local_clock(), code="pause:" + sensor.device_label
|
|
@@ -244,7 +253,6 @@ class SensorProcess(Process):
|
|
|
244
253
|
get_priority(self.pid),
|
|
245
254
|
)
|
|
246
255
|
is_polling = False
|
|
247
|
-
ptp.stop()
|
|
248
256
|
|
|
249
257
|
if self._pipe_buffer_after_pause and self._buffer_size.value > 0:
|
|
250
258
|
# sending data to force
|
|
@@ -268,10 +276,9 @@ class SensorProcess(Process):
|
|
|
268
276
|
self._event_is_polling.wait(timeout=0.1)
|
|
269
277
|
|
|
270
278
|
# stop process
|
|
271
|
-
sensor.stop_data_acquisition()
|
|
279
|
+
sensor.daq.stop_data_acquisition()
|
|
272
280
|
self._buffer_size.value = 0
|
|
273
281
|
|
|
274
|
-
logging.info("Sensor quit, %s
|
|
275
|
-
|
|
282
|
+
logging.info("Sensor quit, %s", sensor.device_label)
|
|
276
283
|
|
|
277
284
|
# FIXME check trigger processing and UDP connections#FIXME check trigger processing and UDP connections
|
|
@@ -86,8 +86,8 @@ class RecordingSettings(ABCSettings):
|
|
|
86
86
|
calibration_files: List[str] = field(default_factory=lambda: ["FT9334.cal"])
|
|
87
87
|
calibration_folder: str = CALIBRATION_FOLDER
|
|
88
88
|
|
|
89
|
-
lsl_stream: bool =
|
|
90
|
-
save_data: bool =
|
|
89
|
+
lsl_stream: bool = True
|
|
90
|
+
save_data: bool = False
|
|
91
91
|
sampling_rate: int = 1000
|
|
92
92
|
|
|
93
93
|
write_Fx: bool = True
|
|
@@ -11,7 +11,6 @@ from multiprocessing import Event, Process, Queue
|
|
|
11
11
|
from subprocess import check_output
|
|
12
12
|
|
|
13
13
|
from .clock import local_clock, wait_ms
|
|
14
|
-
from .polling_time_profile import PollingTimeProfile
|
|
15
14
|
from .process_priority_manager import get_priority
|
|
16
15
|
from .types import UDPData
|
|
17
16
|
|
|
@@ -275,7 +274,6 @@ class UDPConnectionProcess(Process):
|
|
|
275
274
|
udp_connection = UDPConnection(udp_port=5005)
|
|
276
275
|
self.start_polling()
|
|
277
276
|
|
|
278
|
-
ptp = PollingTimeProfile()
|
|
279
277
|
prev_event_polling = None
|
|
280
278
|
|
|
281
279
|
while not self._event_quit_request.is_set():
|
|
@@ -290,14 +288,12 @@ class UDPConnectionProcess(Process):
|
|
|
290
288
|
)
|
|
291
289
|
else:
|
|
292
290
|
logging.warning("UDP stop")
|
|
293
|
-
ptp.stop()
|
|
294
291
|
|
|
295
292
|
if not self._event_is_polling.is_set():
|
|
296
293
|
self._event_is_polling.wait(timeout=0.1)
|
|
297
294
|
else:
|
|
298
295
|
data = udp_connection.poll()
|
|
299
296
|
t = local_clock()
|
|
300
|
-
ptp.update(t)
|
|
301
297
|
if data is not None:
|
|
302
298
|
d = UDPData(string=data, time=t)
|
|
303
299
|
self.receive_queue.put(d)
|
|
@@ -324,4 +320,4 @@ class UDPConnectionProcess(Process):
|
|
|
324
320
|
|
|
325
321
|
udp_connection.unconnect_peer()
|
|
326
322
|
|
|
327
|
-
logging.warning("UDP quit
|
|
323
|
+
logging.warning("UDP quit")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
NIDAQMX = 1
|
|
2
|
+
PYDAQMX = 2
|
|
3
|
+
MOCK_SENSOR = 9
|
|
4
|
+
|
|
5
|
+
DAQ_TYPE = NIDAQMX # default to nidaqmx, use constants.MOCK_SENSOR for mock sensor, constants.PYDAQMX for PyDAQmx
|
|
6
|
+
USE_AIFTT = True # <-- change to False to use ATI DLL for calibration conversion, otherwise use atiiaftt
|
|
7
|
+
|
|
8
|
+
SETTINGS_FILE_EXTENSION = ".settings.toml"
|
|
9
|
+
DEFAULT_SETTINGS_FILE = "pyForceDAQ" + SETTINGS_FILE_EXTENSION
|
|
10
|
+
DEFAULT_OUTPUT_FILENAME = None
|
|
11
|
+
CALIBRATION_FOLDER = "calibration"
|
|
12
|
+
DATA_FOLDER = "data"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
__author__ = "Oliver Lindemann"
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
|
|
8
|
+
from .._lib.settings import DAQConfiguration
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DAQReadAnalogABC(ABC):
|
|
12
|
+
"""Abstract base class for DAQ analog reading."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def __init__(self,
|
|
16
|
+
configuration: DAQConfiguration,
|
|
17
|
+
read_array_size_in_samples: int):
|
|
18
|
+
"""Initialize the DAQ device."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def is_acquiring_data(self) -> bool:
|
|
24
|
+
"""Return whether data acquisition is in progress."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def start_data_acquisition(self) -> None:
|
|
30
|
+
"""Start data acquisition."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def stop_data_acquisition(self) -> None:
|
|
35
|
+
"""Stop data acquisition."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def read_analog(self) -> Tuple[npt.NDArray, int]:
|
|
40
|
+
"""Read analog data.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
read_buffer : numpy array
|
|
45
|
+
The read data.
|
|
46
|
+
read_samples : int
|
|
47
|
+
The number of samples actually read.
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CalibrationConverterABC(ABC):
|
|
53
|
+
"""Abstract base class for Calibration Converters."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def __init__(self, calibration_file: str):
|
|
57
|
+
"""Initialize the calibration converter with a calibration file."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def convertToFT(self, voltages: npt.NDArray) -> list:
|
|
62
|
+
"""Convert voltages to force and torque values."""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def bias(self, bias_values: npt.NDArray) -> None:
|
|
67
|
+
"""Set the bias for the calibration."""
|
|
68
|
+
pass
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import ctypes as ct
|
|
2
|
-
from typing import List
|
|
3
2
|
|
|
4
3
|
from numpy.typing import NDArray
|
|
5
4
|
|
|
5
|
+
from . import CalibrationConverterABC
|
|
6
6
|
from ._pyATIDAQ import ATI_CDLL
|
|
7
7
|
|
|
8
|
+
print("Using self compiled ATI DLL for calibration conversion.")
|
|
9
|
+
|
|
10
|
+
class CalibrationConverter(CalibrationConverterABC):
|
|
8
11
|
|
|
9
|
-
class CalibrationConverter(object):
|
|
10
12
|
def __init__(self, calibration_file: str):
|
|
11
13
|
self._atidaq = ATI_CDLL()
|
|
12
14
|
self._atidaq.createCalibration(calibration_file, ct.c_short(1))
|
|
13
15
|
self._atidaq.setForceUnits("N")
|
|
14
16
|
self._atidaq.setTorqueUnits("N-m")
|
|
15
17
|
|
|
16
|
-
def convertToFT(self, voltages: NDArray):
|
|
18
|
+
def convertToFT(self, voltages: NDArray) -> list:
|
|
17
19
|
return self._atidaq.convertToFT(voltages)
|
|
18
20
|
|
|
19
21
|
def bias(self, bias_values: NDArray):
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import atiiaftt
|
|
2
|
+
from numpy.typing import NDArray
|
|
3
|
+
|
|
4
|
+
from . import CalibrationConverterABC
|
|
5
|
+
|
|
6
|
+
print("Using ATI IAFTT for calibration conversion.")
|
|
7
|
+
|
|
8
|
+
class CalibrationConverter(CalibrationConverterABC): # type: ignore
|
|
9
|
+
|
|
10
|
+
def __init__(self, calibration_file:str):
|
|
11
|
+
self._ftsensor = atiiaftt.FTSensor(calibration_file, index=1)
|
|
12
|
+
|
|
13
|
+
def convertToFT(self, voltages:NDArray) -> list:
|
|
14
|
+
return self._ftsensor.convertToFt(voltages.tolist()) #TODO: to list needed?
|
|
15
|
+
|
|
16
|
+
def bias(self, bias_values: NDArray) -> None:
|
|
17
|
+
self._ftsensor.bias(bias_values.tolist())
|
|
@@ -6,9 +6,11 @@ from typing import Tuple
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
8
|
from .._lib.clock import StopWatch
|
|
9
|
+
from . import DAQReadAnalogABC
|
|
9
10
|
|
|
11
|
+
print("Using mock sensor!!")
|
|
10
12
|
|
|
11
|
-
class DAQReadAnalog(
|
|
13
|
+
class DAQReadAnalog(DAQReadAnalogABC):
|
|
12
14
|
NUM_SAMPS_PER_CHAN = 1
|
|
13
15
|
TIMEOUT = 1.0
|
|
14
16
|
NI_DAQ_BUFFER_SIZE = 1000
|
|
@@ -69,7 +71,7 @@ class DAQReadAnalog(object):
|
|
|
69
71
|
|
|
70
72
|
# fill in data
|
|
71
73
|
if not self._task_is_started:
|
|
72
|
-
return
|
|
74
|
+
return np.array([]), 0
|
|
73
75
|
|
|
74
76
|
n_new_samples = int(self._simulation_timer.time_ms) - self._sample_cnt
|
|
75
77
|
while n_new_samples <= 0: # wait until new sample is available
|
|
@@ -5,15 +5,18 @@ import numpy as np
|
|
|
5
5
|
from nidaqmx import constants as nidaq_consts
|
|
6
6
|
|
|
7
7
|
from .._lib.settings import DAQConfiguration
|
|
8
|
+
from . import DAQReadAnalogABC
|
|
8
9
|
|
|
10
|
+
print("Using nidaqmx for DAQ access.")
|
|
9
11
|
|
|
10
|
-
class DAQReadAnalog(nidaqmx.Task):
|
|
12
|
+
class DAQReadAnalog(nidaqmx.Task, DAQReadAnalogABC):
|
|
11
13
|
NUM_SAMPS_PER_CHAN = 1
|
|
12
14
|
TIMEOUT = 1
|
|
13
15
|
DAQ_TYPE = "nidaqmx"
|
|
14
16
|
|
|
15
17
|
def __init__(
|
|
16
|
-
self, configuration: DAQConfiguration,
|
|
18
|
+
self, configuration: DAQConfiguration,
|
|
19
|
+
read_array_size_in_samples: int
|
|
17
20
|
):
|
|
18
21
|
"""DOC
|
|
19
22
|
read_array_size_in_samples for ReadAnalogF64 call
|
|
@@ -43,10 +46,10 @@ class DAQReadAnalog(nidaqmx.Task):
|
|
|
43
46
|
self.read_array_size_in_samples = read_array_size_in_samples
|
|
44
47
|
|
|
45
48
|
@property
|
|
46
|
-
def is_acquiring_data(self):
|
|
49
|
+
def is_acquiring_data(self) -> bool:
|
|
47
50
|
return self._task_is_started
|
|
48
51
|
|
|
49
|
-
def start_data_acquisition(self):
|
|
52
|
+
def start_data_acquisition(self) -> None:
|
|
50
53
|
"""Start data acquisition of the NI device
|
|
51
54
|
call always before polling
|
|
52
55
|
|
|
@@ -56,7 +59,7 @@ class DAQReadAnalog(nidaqmx.Task):
|
|
|
56
59
|
self.start()
|
|
57
60
|
self._task_is_started = True
|
|
58
61
|
|
|
59
|
-
def stop_data_acquisition(self):
|
|
62
|
+
def stop_data_acquisition(self) -> None:
|
|
60
63
|
"""Stop data acquisition of the NI device"""
|
|
61
64
|
|
|
62
65
|
if self._task_is_started:
|
|
@@ -83,6 +86,6 @@ class DAQReadAnalog(nidaqmx.Task):
|
|
|
83
86
|
"""
|
|
84
87
|
|
|
85
88
|
# fill in data
|
|
86
|
-
data = self.read(self.NUM_SAMPS_PER_CHAN, self.TIMEOUT)
|
|
89
|
+
data = self.read(self.NUM_SAMPS_PER_CHAN, self.TIMEOUT) # type: ignore
|
|
87
90
|
np_data = np.reshape(np.array(data), (-1,)) # reshape to vector
|
|
88
91
|
return np_data, len(np_data)
|
|
@@ -16,9 +16,11 @@ import numpy as np
|
|
|
16
16
|
import PyDAQmx
|
|
17
17
|
|
|
18
18
|
from .._lib.settings import DAQConfiguration
|
|
19
|
+
from . import DAQReadAnalogABC
|
|
19
20
|
|
|
21
|
+
print("Using PyDAQmx for DAQ access.")
|
|
20
22
|
|
|
21
|
-
class DAQReadAnalog(PyDAQmx.Task):
|
|
23
|
+
class DAQReadAnalog(PyDAQmx.Task, DAQReadAnalogABC):
|
|
22
24
|
NUM_SAMPS_PER_CHAN = ct.c_int32(1)
|
|
23
25
|
TIMEOUT = ct.c_longdouble(1.0) # one second
|
|
24
26
|
NI_DAQ_BUFFER_SIZE = 1000
|
|
@@ -100,7 +102,7 @@ class DAQReadAnalog(PyDAQmx.Task):
|
|
|
100
102
|
"""
|
|
101
103
|
|
|
102
104
|
# fill in data
|
|
103
|
-
read_samples = ct.c_int32()
|
|
105
|
+
read_samples = ct.c_int32(0)
|
|
104
106
|
read_buffer = np.zeros((self.read_array_size_in_samples,), dtype=np.float64)
|
|
105
107
|
|
|
106
108
|
error = self.ReadAnalogF64(
|
|
@@ -4,11 +4,10 @@ from typing import List
|
|
|
4
4
|
|
|
5
5
|
import PySimpleGUI as _sg
|
|
6
6
|
|
|
7
|
-
from . import __version__
|
|
7
|
+
from . import __version__, constants
|
|
8
8
|
from ._lib.misc import list_settings_files
|
|
9
9
|
from ._lib.settings import AppSettings
|
|
10
10
|
from ._lib.udp_connection import UDPConnection
|
|
11
|
-
from .constants import DEFAULT_SETTINGS_FILE, USE_MOCK_SENSOR
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def _check_sensor_calibration_settings(
|
|
@@ -66,7 +65,7 @@ def _windows_run(settings: AppSettings):
|
|
|
66
65
|
info = [[_sg.Text(f"version: {__version__}")]]
|
|
67
66
|
info.append([_sg.Text(f"IP address: {UDPConnection.MY_IP}")])
|
|
68
67
|
|
|
69
|
-
if
|
|
68
|
+
if constants.DAQ_TYPE == constants.MOCK_SENSOR:
|
|
70
69
|
info.append([_sg.Text("!!! USING MOCK SENSORS !!!", text_color="red")])
|
|
71
70
|
|
|
72
71
|
layout = [
|
|
@@ -78,7 +77,7 @@ def _windows_run(settings: AppSettings):
|
|
|
78
77
|
key="Start",
|
|
79
78
|
)
|
|
80
79
|
],
|
|
81
|
-
[_sg.Frame("Info", size=(280,
|
|
80
|
+
[_sg.Frame("Info", size=(280, 150), layout=info)],
|
|
82
81
|
[_sg.Frame("Settings", size=(280, 140), expand_y=True, layout=info_settings)],
|
|
83
82
|
]
|
|
84
83
|
layout.append(
|
|
@@ -140,7 +139,7 @@ def load_settings_file(settings_file: str | Path) -> AppSettings:
|
|
|
140
139
|
|
|
141
140
|
def run_launcher():
|
|
142
141
|
_sg.theme("DarkBlue14") # please make your windows colorful
|
|
143
|
-
settings = load_settings_file(DEFAULT_SETTINGS_FILE)
|
|
142
|
+
settings = load_settings_file(constants.DEFAULT_SETTINGS_FILE)
|
|
144
143
|
|
|
145
144
|
while True:
|
|
146
145
|
event, values, settings = _windows_run(settings)
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
from .clock import local_clock
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class PollingTimeProfile(object):
|
|
7
|
-
def __init__(self, timing_range_ms=10):
|
|
8
|
-
self._last = None
|
|
9
|
-
self._timing_range_ms = 10
|
|
10
|
-
self._zero_cnt = 0
|
|
11
|
-
|
|
12
|
-
# self._zero_time_polling_frequency = {}
|
|
13
|
-
self.profile_frequency = np.array([0] * (timing_range_ms + 1))
|
|
14
|
-
|
|
15
|
-
def stop(self):
|
|
16
|
-
self._last = None
|
|
17
|
-
|
|
18
|
-
def update(self, time: float):
|
|
19
|
-
|
|
20
|
-
time_ms = int(time * 1000)
|
|
21
|
-
if self._last is not None:
|
|
22
|
-
d = time_ms - self._last
|
|
23
|
-
if d > self._timing_range_ms:
|
|
24
|
-
d = self._timing_range_ms
|
|
25
|
-
self.profile_frequency[d] += 1
|
|
26
|
-
|
|
27
|
-
# if d == 0:
|
|
28
|
-
# self._zero_cnt += 1
|
|
29
|
-
# elif self._zero_cnt > 0:
|
|
30
|
-
# try:
|
|
31
|
-
# self._zero_time_polling_frequency[self._zero_cnt] += 1
|
|
32
|
-
# except:
|
|
33
|
-
# self._zero_time_polling_frequency[self._zero_cnt] = 1
|
|
34
|
-
# self._zero_cnt = 0
|
|
35
|
-
|
|
36
|
-
self._last = time_ms
|
|
37
|
-
|
|
38
|
-
def tick(self):
|
|
39
|
-
self.update(local_clock())
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def profile_percent(self):
|
|
43
|
-
n = np.sum(self.profile_frequency)
|
|
44
|
-
return self.profile_frequency / n
|
|
45
|
-
|
|
46
|
-
def get_profile_str(self):
|
|
47
|
-
rtn = (
|
|
48
|
-
str(list(self.profile_frequency))
|
|
49
|
-
.replace("[", "")
|
|
50
|
-
.replace("]", "")
|
|
51
|
-
.replace(" ", "")
|
|
52
|
-
)
|
|
53
|
-
return "polling profile [{}]".format(rtn)
|
|
54
|
-
|
|
55
|
-
# @property
|
|
56
|
-
# def zero_time_polling_frequency(self):
|
|
57
|
-
# return np.array(list(self._zero_time_polling_frequency.items()))
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
USE_MOCK_SENSOR = False # <-- change for usage in lab to False
|
|
2
|
-
USE_PYDAQMX = False # <-- change to True to use pyDAQmx instead of nidaqmx, requires pyDAQmx installation
|
|
3
|
-
USE_ATI_DLL = False # <-- change to True to use ATI DLL for calibration conversion, otherwise use atiiaftt
|
|
4
|
-
|
|
5
|
-
SETTINGS_FILE_EXTENSION = ".settings.toml"
|
|
6
|
-
DEFAULT_SETTINGS_FILE = "pyForceDAQ" + SETTINGS_FILE_EXTENSION
|
|
7
|
-
DEFAULT_OUTPUT_FILENAME = None
|
|
8
|
-
CALIBRATION_FOLDER = "calibration"
|
|
9
|
-
DATA_FOLDER = "data"
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
__author__ = "Oliver Lindemann"
|
|
2
|
-
|
|
3
|
-
from ..constants import USE_ATI_DLL, USE_MOCK_SENSOR, USE_PYDAQMX
|
|
4
|
-
|
|
5
|
-
if USE_MOCK_SENSOR:
|
|
6
|
-
print("Using mock sensor instead.")
|
|
7
|
-
from ._mock_sensor import DAQReadAnalog
|
|
8
|
-
|
|
9
|
-
else:
|
|
10
|
-
try:
|
|
11
|
-
if USE_PYDAQMX:
|
|
12
|
-
print("Using PyDAQmx for DAQ access.")
|
|
13
|
-
from ._use_pydaqmx import DAQReadAnalog
|
|
14
|
-
else:
|
|
15
|
-
print("Using nidaqmx for DAQ access.")
|
|
16
|
-
from ._use_nidaqmx import DAQReadAnalog
|
|
17
|
-
except (ImportError, ModuleNotFoundError, NotImplementedError) as e:
|
|
18
|
-
print("Error importing DAQReadAnalog: {0}".format(e))
|
|
19
|
-
print("Warning: PyDAQmx or nidaqmx not found. Using mock sensor instead.")
|
|
20
|
-
from ._mock_sensor import DAQReadAnalog
|
|
21
|
-
|
|
22
|
-
if not USE_ATI_DLL:
|
|
23
|
-
print("Using ATI IAFTT for calibration conversion.")
|
|
24
|
-
from ._calibration_iaftt import CalibrationConverter
|
|
25
|
-
|
|
26
|
-
else:
|
|
27
|
-
print("ATI_CDLL for calibration conversion.")
|
|
28
|
-
from ._calibration_dll import CalibrationConverter
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
|
|
3
|
-
import atiiaftt
|
|
4
|
-
from numpy.typing import NDArray
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class CalibrationConverter(object): # type: ignore
|
|
8
|
-
|
|
9
|
-
def __init__(self, calibration_file:str):
|
|
10
|
-
self._ftsensor = atiiaftt.FTSensor(calibration_file, index=1)
|
|
11
|
-
|
|
12
|
-
def convertToFT(self, voltages:NDArray):
|
|
13
|
-
return self._ftsensor.convertToFt(voltages.tolist()) # FIXME reverse parameter
|
|
14
|
-
#TODO: to list needed?
|
|
15
|
-
|
|
16
|
-
def bias(self, bias_values: NDArray):
|
|
17
|
-
self._ftsensor.bias(bias_values.tolist())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|