pyForceDAQ 2.0.4__tar.gz → 2.0.5.dev3__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.dev3}/PKG-INFO +1 -4
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/pyproject.toml +21 -4
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/__main__.py +27 -4
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/data_recorder.py +79 -192
- pyforcedaq-2.0.5.dev3/src/pyforcedaq/_lib/file_writer.py +110 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/process_priority_manager.py +1 -1
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/sensor.py +44 -21
- pyforcedaq-2.0.5.dev3/src/pyforcedaq/_lib/sensor_process.py +222 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/settings.py +2 -2
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/types.py +8 -17
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/udp_connection.py +1 -5
- pyforcedaq-2.0.5.dev3/src/pyforcedaq/constants.py +12 -0
- pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/__init__.py +68 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_dll.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/calibration_dll.py +5 -3
- pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/calibration_iaftt.py +17 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_mock_sensor.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/read_daq_mock_sensor.py +4 -2
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_use_nidaqmx.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/read_daq_nidaqmx.py +9 -6
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_use_pydaqmx.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/read_daq_pydaqmx.py +4 -2
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/force.py +1 -1
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_gui_status.py +50 -45
- pyforcedaq-2.0.5.dev3/src/pyforcedaq/gui/_layout.py +133 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_level_indicator.py +4 -2
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_run.py +20 -29
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/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/_lib/sensor_process.py +0 -277
- 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/src/pyforcedaq/extras/__init__.py +0 -0
- pyforcedaq-2.0.4/src/pyforcedaq/extras/convert.py +0 -275
- pyforcedaq-2.0.4/src/pyforcedaq/extras/read_force_data.py +0 -89
- pyforcedaq-2.0.4/src/pyforcedaq/gui/_layout.py +0 -120
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/clock.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/lsl.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/misc.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/daq/_pyATIDAQ.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/__init__.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_pg_surface.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_plotter.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_scaling.py +0 -0
- {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/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.dev3
|
|
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-dev3"
|
|
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,25 @@ 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"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
ignore = ["F401", "F405"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.format]
|
|
47
|
+
line-ending = "lf"
|
|
48
|
+
indent-style = "space"
|
|
@@ -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()
|
|
@@ -6,33 +6,24 @@ See COPYING file distributed along with the pyForceDAQ copyright and license ter
|
|
|
6
6
|
__author__ = "Oliver Lindemann"
|
|
7
7
|
|
|
8
8
|
import atexit
|
|
9
|
-
import gzip
|
|
10
9
|
import logging
|
|
11
|
-
from io import TextIOWrapper
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from time import asctime, localtime, strftime
|
|
14
12
|
from typing import List
|
|
15
13
|
|
|
16
14
|
from .. import __version__ as forceDAQVersion
|
|
17
|
-
from
|
|
15
|
+
from .. import constants
|
|
16
|
+
from .file_writer import FileWriter
|
|
18
17
|
from .misc import set_logging
|
|
19
18
|
from .process_priority_manager import ProcessPriorityManager
|
|
20
19
|
from .sensor_process import SensorProcess
|
|
21
20
|
from .settings import RecordingSettings, SensorSettings
|
|
22
|
-
from .types import
|
|
23
|
-
TAG_COMMENTS,
|
|
24
|
-
TAG_DAQEVENT,
|
|
25
|
-
TAG_UDPDATA,
|
|
26
|
-
DAQEvents,
|
|
27
|
-
ForceSensorData,
|
|
28
|
-
PollingPriority,
|
|
29
|
-
UDPData,
|
|
30
|
-
)
|
|
21
|
+
from .types import DAQEvents, ForceSensorData, PollingPriority
|
|
31
22
|
from .udp_connection import UDPConnectionProcess
|
|
32
23
|
|
|
33
24
|
set_logging(data_directory="data", log_file="recording.log")
|
|
34
25
|
|
|
35
|
-
|
|
26
|
+
# FIXME LSL marker event for all events
|
|
36
27
|
|
|
37
28
|
|
|
38
29
|
class DataRecorder(object):
|
|
@@ -49,15 +40,26 @@ class DataRecorder(object):
|
|
|
49
40
|
|
|
50
41
|
polling_priority has to be types.PollingPriority.{HIGH},
|
|
51
42
|
{REALTIME} or {NORMAL} or None
|
|
43
|
+
|
|
44
|
+
You can change the used modules by settings the following constants before creating the
|
|
45
|
+
DataRecorder instance:
|
|
46
|
+
* set constants.DAQ_TYPE to constants.PYDAQMX, constants.NIDAQMX or constants.MOCK_SENSOR
|
|
47
|
+
* set constants.USE_AIFTT to True or False
|
|
52
48
|
"""
|
|
53
49
|
|
|
54
50
|
if not isinstance(force_sensor_settings, list):
|
|
55
51
|
force_sensor_settings = [force_sensor_settings]
|
|
56
52
|
|
|
57
53
|
self.recording_settings = recording_settings
|
|
54
|
+
if recording_settings.save_data:
|
|
55
|
+
self.file_writer = FileWriter(recording_settings)
|
|
56
|
+
queue = self.file_writer.queue
|
|
57
|
+
else:
|
|
58
|
+
self.file_writer = None
|
|
59
|
+
queue = None
|
|
58
60
|
|
|
59
61
|
# create sensor processes
|
|
60
|
-
self.
|
|
62
|
+
self.force_sensor_processes: List[SensorProcess] = []
|
|
61
63
|
event_trigger = []
|
|
62
64
|
for fs in force_sensor_settings:
|
|
63
65
|
if not isinstance(fs, SensorSettings):
|
|
@@ -66,11 +68,12 @@ class DataRecorder(object):
|
|
|
66
68
|
fst = SensorProcess(
|
|
67
69
|
sensor_settings=fs,
|
|
68
70
|
recording_settings=recording_settings,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
file_writer_queue=queue,
|
|
72
|
+
daq_type=constants.DAQ_TYPE,
|
|
73
|
+
use_aiftt=constants.USE_AIFTT)
|
|
71
74
|
fst.start()
|
|
72
75
|
event_trigger.append(fst.event_trigger)
|
|
73
|
-
self.
|
|
76
|
+
self.force_sensor_processes.append(fst)
|
|
74
77
|
|
|
75
78
|
# create udp connection process
|
|
76
79
|
if poll_udp_connection:
|
|
@@ -82,7 +85,7 @@ class DataRecorder(object):
|
|
|
82
85
|
# process managing FIYME needed?
|
|
83
86
|
self._proc_manager = ProcessPriorityManager()
|
|
84
87
|
self._proc_manager.add_subprocess(self.udp)
|
|
85
|
-
self._proc_manager.add_subprocess(self.
|
|
88
|
+
self._proc_manager.add_subprocess(self.force_sensor_processes)
|
|
86
89
|
if self.recording_settings.priority is not None:
|
|
87
90
|
level = PollingPriority.NORMAL
|
|
88
91
|
else:
|
|
@@ -94,37 +97,36 @@ class DataRecorder(object):
|
|
|
94
97
|
)
|
|
95
98
|
# logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
|
|
96
99
|
|
|
97
|
-
self.
|
|
98
|
-
self._file = None
|
|
99
|
-
self._daq_event = []
|
|
100
|
-
self.path_open_file = Path("")
|
|
100
|
+
self._daq_event = [] # FIXME needed?
|
|
101
101
|
atexit.register(self.quit)
|
|
102
102
|
|
|
103
103
|
@property
|
|
104
|
-
def
|
|
104
|
+
def has_file_writer(self):
|
|
105
105
|
"""Property indicates whether a data file is open"""
|
|
106
|
-
return self.
|
|
106
|
+
return isinstance(self.file_writer, FileWriter) and self.file_writer.is_alive()
|
|
107
107
|
|
|
108
108
|
@property
|
|
109
109
|
def is_alive(self):
|
|
110
110
|
"""Property indicates whether the recording processes are alive"""
|
|
111
111
|
try:
|
|
112
|
-
return self.
|
|
113
|
-
except
|
|
112
|
+
return self.force_sensor_processes[0].is_alive()
|
|
113
|
+
except IndexError:
|
|
114
114
|
return False
|
|
115
115
|
|
|
116
116
|
@property
|
|
117
|
-
def
|
|
117
|
+
def is_saving(self):
|
|
118
118
|
"""Property indicates whether the recording is started or paused"""
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
if self.has_file_writer:
|
|
120
|
+
for fsp in self.force_sensor_processes:
|
|
121
|
+
if not fsp.is_saving:
|
|
122
|
+
return False
|
|
123
|
+
return True # all sensor processes are saving, file writer is alive
|
|
124
|
+
else:
|
|
125
|
+
return False
|
|
124
126
|
|
|
125
127
|
@property
|
|
126
128
|
def sensor_settings_list(self):
|
|
127
|
-
return list(map(lambda x: x.sensor_settings, self.
|
|
129
|
+
return list(map(lambda x: x.sensor_settings, self.force_sensor_processes))
|
|
128
130
|
|
|
129
131
|
def quit(self) -> list | None:
|
|
130
132
|
"""Stop all recording processes, close data file and quit recording
|
|
@@ -138,20 +140,16 @@ class DataRecorder(object):
|
|
|
138
140
|
if not self.is_alive:
|
|
139
141
|
return
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
self.pause_saving()
|
|
144
|
+
for fsp in self.force_sensor_processes:
|
|
145
|
+
fsp.join()
|
|
142
146
|
self.close_data_file()
|
|
143
147
|
|
|
144
148
|
if self.udp is not None:
|
|
145
149
|
self.udp.quit()
|
|
146
150
|
|
|
147
|
-
# wait that all processes are quitted
|
|
148
|
-
for fsp in self._force_sensor_processes:
|
|
149
|
-
fsp.join()
|
|
150
|
-
|
|
151
151
|
logging.info("Quit recording")
|
|
152
152
|
|
|
153
|
-
return buffer
|
|
154
|
-
|
|
155
153
|
def process_and_write_udp_events(self) -> list:
|
|
156
154
|
"""process udp events and return them"""
|
|
157
155
|
buffer = []
|
|
@@ -162,59 +160,11 @@ class DataRecorder(object):
|
|
|
162
160
|
# until queue empty or no udp connection
|
|
163
161
|
break
|
|
164
162
|
buffer.append(data)
|
|
165
|
-
if len(buffer) > 0:
|
|
166
|
-
self._write_data(buffer)
|
|
167
|
-
return buffer
|
|
168
|
-
|
|
169
|
-
def _write_data(
|
|
170
|
-
self, data_buffer: list, recording_screen=None, float_decimal_places: int = 4
|
|
171
|
-
) -> None:
|
|
172
|
-
"""writes data to disk and set counters
|
|
173
163
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
BLOCKSIZE = 10000 # for recording screen feedback only
|
|
179
|
-
write_forces = self.recording_settings.array_write_forces()
|
|
180
|
-
write_trigger = self.recording_settings.array_write_trigger()
|
|
181
|
-
write_deviceid = len(self.recording_settings.device_labels) > 1
|
|
182
|
-
float_format = "{0:." + str(float_decimal_places) + "f},"
|
|
183
|
-
buffer_len = len(data_buffer)
|
|
184
|
-
for c, d in enumerate(data_buffer):
|
|
185
|
-
if self._file is not None:
|
|
186
|
-
if isinstance(d, ForceSensorData):
|
|
187
|
-
line = f"{d.time}, {d.acquisition_delay},"
|
|
188
|
-
if write_deviceid:
|
|
189
|
-
line += f"{d.sensor_id},"
|
|
190
|
-
for x in d.forces[write_forces]:
|
|
191
|
-
line += float_format.format(x)
|
|
192
|
-
for x in d.trigger[write_trigger]:
|
|
193
|
-
if isinstance(x, int):
|
|
194
|
-
line += f"{x},"
|
|
195
|
-
else:
|
|
196
|
-
line += float_format.format(x)
|
|
197
|
-
self._file_write(line[:-1] + NEWLINE)
|
|
198
|
-
|
|
199
|
-
elif isinstance(d, DAQEvents):
|
|
200
|
-
self._file_write(f"{TAG_DAQEVENT},{d.time},{str(d.code)}" + NEWLINE)
|
|
201
|
-
|
|
202
|
-
elif isinstance(d, UDPData):
|
|
203
|
-
self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
|
|
204
|
-
|
|
205
|
-
if recording_screen is not None and c % BLOCKSIZE == 0:
|
|
206
|
-
recording_screen.stimulus(
|
|
207
|
-
"Saving {0} of {1} blocks".format(
|
|
208
|
-
c // BLOCKSIZE, buffer_len // BLOCKSIZE
|
|
209
|
-
)
|
|
210
|
-
).present()
|
|
211
|
-
|
|
212
|
-
def _file_write(self, s: str) -> None:
|
|
213
|
-
|
|
214
|
-
if isinstance(self._file, gzip.GzipFile):
|
|
215
|
-
self._file.write(s.encode("utf-8"))
|
|
216
|
-
elif isinstance(self._file, TextIOWrapper):
|
|
217
|
-
self._file.write(s)
|
|
164
|
+
if isinstance(self.file_writer, FileWriter):
|
|
165
|
+
for dat in buffer:
|
|
166
|
+
self.file_writer.queue.put(dat)
|
|
167
|
+
return buffer
|
|
218
168
|
|
|
219
169
|
def store_daq_event(
|
|
220
170
|
self, code: str | int | float, time: float | None = None
|
|
@@ -226,35 +176,20 @@ class DataRecorder(object):
|
|
|
226
176
|
"""
|
|
227
177
|
self._daq_event.append(DAQEvents(time=time, code=code))
|
|
228
178
|
|
|
229
|
-
def
|
|
179
|
+
def start_saving(self) -> None:
|
|
230
180
|
"""Start polling process and record
|
|
231
181
|
|
|
232
182
|
See Also
|
|
233
183
|
--------
|
|
234
|
-
|
|
184
|
+
is_saving
|
|
235
185
|
|
|
236
186
|
"""
|
|
237
187
|
|
|
238
|
-
|
|
239
|
-
|
|
188
|
+
for fsp in self.force_sensor_processes:
|
|
189
|
+
fsp.flag_sensor_bias_is_determined.wait() # wait is no initial bias is set yet
|
|
190
|
+
fsp.start_saving()
|
|
240
191
|
|
|
241
|
-
|
|
242
|
-
list(
|
|
243
|
-
filter(
|
|
244
|
-
lambda x: x.event_bias_is_available.is_set(),
|
|
245
|
-
self._force_sensor_processes,
|
|
246
|
-
)
|
|
247
|
-
)
|
|
248
|
-
) != len(self._force_sensor_processes):
|
|
249
|
-
raise RuntimeError(
|
|
250
|
-
"Sensors can't be started before bias has been determined."
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
# start polling
|
|
254
|
-
list(map(lambda x: x.start_polling(), self._force_sensor_processes))
|
|
255
|
-
self._is_recording = True
|
|
256
|
-
|
|
257
|
-
def pause_recording(self, recording_screen=None) -> list:
|
|
192
|
+
def pause_saving(self):
|
|
258
193
|
"""Pauses all polling processes and process data
|
|
259
194
|
|
|
260
195
|
returns
|
|
@@ -262,59 +197,17 @@ class DataRecorder(object):
|
|
|
262
197
|
data : all last data
|
|
263
198
|
|
|
264
199
|
"""
|
|
265
|
-
self._is_recording = False
|
|
266
|
-
|
|
267
|
-
data = []
|
|
268
|
-
if recording_screen is not None:
|
|
269
|
-
recording_screen.stimulus("").present()
|
|
270
200
|
|
|
271
201
|
# pause polling
|
|
272
|
-
for fsp in self.
|
|
273
|
-
fsp.
|
|
274
|
-
|
|
275
|
-
wait_ms(500)
|
|
276
|
-
|
|
277
|
-
if recording_screen is not None:
|
|
278
|
-
if self.is_saving_data:
|
|
279
|
-
recording_screen.stimulus("saving data ...").present()
|
|
280
|
-
else:
|
|
281
|
-
recording_screen.stimulus("pause recording").present()
|
|
282
|
-
|
|
283
|
-
# get data
|
|
284
|
-
for fsp in self._force_sensor_processes:
|
|
285
|
-
buffer = fsp.get_buffer()
|
|
286
|
-
self._write_data(buffer, recording_screen)
|
|
287
|
-
data.extend(buffer)
|
|
288
|
-
|
|
289
|
-
# udp event
|
|
290
|
-
data.extend(self.process_and_write_udp_events())
|
|
291
|
-
|
|
292
|
-
# soft trigger
|
|
293
|
-
self._write_data(self._daq_event)
|
|
294
|
-
data.extend(self._daq_event)
|
|
295
|
-
self._daq_event = []
|
|
296
|
-
return data
|
|
297
|
-
|
|
298
|
-
def determine_biases(self, n_samples: int) -> None:
|
|
299
|
-
"""Record n data samples (n_samples) to determine bias.
|
|
300
|
-
Afterwards recording is in pause mode
|
|
301
|
-
|
|
302
|
-
Notes
|
|
303
|
-
-----
|
|
304
|
-
The function take some time to be processed
|
|
305
|
-
|
|
306
|
-
See Also
|
|
307
|
-
--------
|
|
308
|
-
Sensor.determine_bias()
|
|
202
|
+
for fsp in self.force_sensor_processes:
|
|
203
|
+
fsp.pause_saving()
|
|
309
204
|
|
|
310
|
-
"""
|
|
311
205
|
|
|
312
|
-
|
|
313
|
-
for x in self.
|
|
314
|
-
x.determine_bias(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
x.event_bias_is_available.wait()
|
|
206
|
+
def determine_biases(self) -> None:
|
|
207
|
+
for x in self.force_sensor_processes:
|
|
208
|
+
x.determine_bias()
|
|
209
|
+
for x in self.force_sensor_processes:
|
|
210
|
+
x.flag_sensor_bias_is_determined.wait()
|
|
318
211
|
|
|
319
212
|
def open_data_file(
|
|
320
213
|
self,
|
|
@@ -322,7 +215,7 @@ class DataRecorder(object):
|
|
|
322
215
|
subdirectory: str = "data",
|
|
323
216
|
varnames: bool = True,
|
|
324
217
|
comment_line: str = "",
|
|
325
|
-
) -> Path:
|
|
218
|
+
) -> Path | None:
|
|
326
219
|
"""Create a data file
|
|
327
220
|
|
|
328
221
|
Only if data file has been opened, data will be saved!
|
|
@@ -347,20 +240,22 @@ class DataRecorder(object):
|
|
|
347
240
|
full path the actually used file (incl. timestamp)
|
|
348
241
|
|
|
349
242
|
"""
|
|
350
|
-
|
|
243
|
+
|
|
244
|
+
if not isinstance(self.file_writer, FileWriter):
|
|
245
|
+
return
|
|
351
246
|
|
|
352
247
|
# create filename
|
|
353
248
|
data_dir = Path.cwd() / subdirectory
|
|
354
249
|
data_dir.mkdir(exist_ok=True)
|
|
250
|
+
|
|
355
251
|
if self.recording_settings.zip_data:
|
|
356
|
-
filename = Path(filename).with_suffix(".csv.
|
|
252
|
+
filename = Path(filename).with_suffix(".csv.bz2")
|
|
357
253
|
else:
|
|
358
254
|
filename = Path(filename).with_suffix(".csv")
|
|
359
|
-
|
|
360
255
|
while True:
|
|
361
|
-
|
|
362
|
-
if
|
|
363
|
-
#
|
|
256
|
+
file_path = data_dir / filename
|
|
257
|
+
if file_path.is_file():
|
|
258
|
+
#get unique filename by adding timestamp if file already exists
|
|
364
259
|
filename = Path(
|
|
365
260
|
filename.stem
|
|
366
261
|
+ "_"
|
|
@@ -370,32 +265,24 @@ class DataRecorder(object):
|
|
|
370
265
|
else:
|
|
371
266
|
break
|
|
372
267
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
self._file_write(
|
|
380
|
-
TAG_COMMENTS
|
|
381
|
-
+ "Recorded at {0} with pyForceDAQ {1}\n".format(
|
|
382
|
-
asctime(localtime()), forceDAQVersion
|
|
383
|
-
)
|
|
384
|
-
)
|
|
385
|
-
logging.info("new file: {}".format(self.path_open_file))
|
|
268
|
+
self.file_writer.start_recording(file_path=file_path, append_mode=False)
|
|
269
|
+
logging.info("new file: %s", file_path)
|
|
270
|
+
|
|
271
|
+
self.file_writer.queue.put(
|
|
272
|
+
f"Recorded at {asctime(localtime())} with pyForceDAQ {forceDAQVersion}\n")
|
|
386
273
|
|
|
387
274
|
for s in self.sensor_settings_list:
|
|
388
275
|
txt = f" Sensor: label={s.device_label}, cal-file={s.calibration_file}\n"
|
|
389
|
-
self.
|
|
276
|
+
self.file_writer.queue.put(txt)
|
|
390
277
|
|
|
391
278
|
if len(comment_line) > 0:
|
|
392
|
-
self.
|
|
279
|
+
self.file_writer.queue.put(comment_line + "\n")
|
|
393
280
|
|
|
394
281
|
if varnames:
|
|
395
282
|
write_forces = self.recording_settings.array_write_forces()
|
|
396
283
|
write_trigger = self.recording_settings.array_write_trigger()
|
|
397
284
|
write_deviceid = len(self.recording_settings.device_labels) > 1
|
|
398
|
-
line = "time,
|
|
285
|
+
line = "time,"
|
|
399
286
|
if write_deviceid:
|
|
400
287
|
line += "device_tag,"
|
|
401
288
|
for x in range(6):
|
|
@@ -405,9 +292,9 @@ class DataRecorder(object):
|
|
|
405
292
|
line += "trigger1,"
|
|
406
293
|
if write_trigger[1]:
|
|
407
294
|
line += "trigger2,"
|
|
408
|
-
self.
|
|
295
|
+
self.file_writer.queue.put(line[:-1] + "\n")
|
|
409
296
|
|
|
410
|
-
return
|
|
297
|
+
return file_path
|
|
411
298
|
|
|
412
299
|
def close_data_file(self) -> None:
|
|
413
300
|
"""Close the data file
|
|
@@ -415,8 +302,8 @@ class DataRecorder(object):
|
|
|
415
302
|
Afterwards data will not be saved anymore.
|
|
416
303
|
|
|
417
304
|
"""
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
self.
|
|
421
|
-
|
|
422
|
-
self.
|
|
305
|
+
if isinstance(self.file_writer, FileWriter):
|
|
306
|
+
self.file_writer.close_file()
|
|
307
|
+
if self.file_writer.is_alive():
|
|
308
|
+
self.file_writer.join()
|
|
309
|
+
self.file_writer.filepath = Path("")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import bz2
|
|
2
|
+
from multiprocessing import Event, Process, Queue
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from queue import Empty
|
|
5
|
+
|
|
6
|
+
from .settings import RecordingSettings
|
|
7
|
+
from .types import (TAG_COMMENTS, TAG_DAQEVENT, TAG_UDPDATA, DAQEvents,
|
|
8
|
+
ForceSensorData, UDPData)
|
|
9
|
+
|
|
10
|
+
NEWLINE = "\n"
|
|
11
|
+
ENCODING = "utf-8"
|
|
12
|
+
|
|
13
|
+
class FileWriter(Process):
|
|
14
|
+
def __init__(
|
|
15
|
+
self, recording_settings: RecordingSettings, float_decimal_places: int = 4
|
|
16
|
+
):
|
|
17
|
+
"""To write to a file from multiple processes. Use FileWriter.queue.put(str) to write file"""
|
|
18
|
+
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.filepath: Path = Path("")
|
|
21
|
+
self.append_mode = False
|
|
22
|
+
self.queue = Queue()
|
|
23
|
+
self._force_quit = Event()
|
|
24
|
+
self._close_file = Event()
|
|
25
|
+
self._write_forces = recording_settings.array_write_forces()
|
|
26
|
+
self._write_trigger = recording_settings.array_write_trigger()
|
|
27
|
+
self._write_deviceid = len(recording_settings.device_labels) > 1
|
|
28
|
+
self._decimal_places = float_decimal_places
|
|
29
|
+
|
|
30
|
+
def close_file(self):
|
|
31
|
+
"""closes file after all pending writes are done and no further write occurred
|
|
32
|
+
for close_timeout seconds
|
|
33
|
+
"""
|
|
34
|
+
self._close_file.set()
|
|
35
|
+
|
|
36
|
+
def force_quit(self):
|
|
37
|
+
"""forces the process to quit immediately, even if there are pending writes in the queue"""
|
|
38
|
+
self._force_quit.set()
|
|
39
|
+
|
|
40
|
+
def start_recording(self, file_path: Path, append_mode: bool = False):
|
|
41
|
+
"""opens file for writing, if file already exists, it will be overwritten (or appended if append_mode is True)"""
|
|
42
|
+
self.filepath = file_path
|
|
43
|
+
self.append_mode = append_mode
|
|
44
|
+
self.start()
|
|
45
|
+
|
|
46
|
+
# def join(self, timeout=None):
|
|
47
|
+
# super(FileWriter, self).join(timeout)
|
|
48
|
+
|
|
49
|
+
def run(self):
|
|
50
|
+
|
|
51
|
+
if self.filepath is None:
|
|
52
|
+
raise ValueError("File path is not set. Call start_recording() with a valid file path before running the process.")
|
|
53
|
+
|
|
54
|
+
float_format = "{0:." + str(self._decimal_places) + "f},"
|
|
55
|
+
if self.append_mode:
|
|
56
|
+
mode = "a"
|
|
57
|
+
else:
|
|
58
|
+
mode = "w"
|
|
59
|
+
if self.filepath.suffix.endswith("bz2"):
|
|
60
|
+
fl = bz2.open(self.filepath, mode)
|
|
61
|
+
else:
|
|
62
|
+
fl = open(self.filepath, mode, encoding=ENCODING)
|
|
63
|
+
|
|
64
|
+
self._close_file.clear()
|
|
65
|
+
self._force_quit.clear()
|
|
66
|
+
|
|
67
|
+
while not self._force_quit.is_set():
|
|
68
|
+
|
|
69
|
+
if self._close_file.is_set():
|
|
70
|
+
try:
|
|
71
|
+
d = self.queue.get_nowait()
|
|
72
|
+
except Empty:
|
|
73
|
+
break # quit process
|
|
74
|
+
else:
|
|
75
|
+
try:
|
|
76
|
+
d = self.queue.get(timeout=0.5)
|
|
77
|
+
except Empty:
|
|
78
|
+
continue # wait again for events
|
|
79
|
+
|
|
80
|
+
if isinstance(d, ForceSensorData):
|
|
81
|
+
txt = f"{d.time},"
|
|
82
|
+
if self._write_deviceid:
|
|
83
|
+
txt += f"{d.sensor_id},"
|
|
84
|
+
for x in d.forces[self._write_forces]:
|
|
85
|
+
txt += float_format.format(x)
|
|
86
|
+
for x in d.trigger[self._write_trigger]:
|
|
87
|
+
if isinstance(x, int):
|
|
88
|
+
txt += f"{x},"
|
|
89
|
+
else:
|
|
90
|
+
txt += float_format.format(x)
|
|
91
|
+
txt = txt[:-1] + NEWLINE
|
|
92
|
+
|
|
93
|
+
elif isinstance(d, DAQEvents):
|
|
94
|
+
txt = f"{TAG_DAQEVENT},{d.time},{str(d.code)}{NEWLINE}"
|
|
95
|
+
|
|
96
|
+
elif isinstance(d, UDPData):
|
|
97
|
+
txt = f"{TAG_UDPDATA},{d.time},{d.unicode}{NEWLINE}"
|
|
98
|
+
elif isinstance(d, str):
|
|
99
|
+
txt = f"{TAG_COMMENTS} {d}"
|
|
100
|
+
else:
|
|
101
|
+
continue # ignore unknown data types or maybe raise error (TODO)
|
|
102
|
+
|
|
103
|
+
if isinstance(fl, bz2.BZ2File):
|
|
104
|
+
fl.write(txt.encode(ENCODING))
|
|
105
|
+
else:
|
|
106
|
+
fl.write(txt)
|
|
107
|
+
|
|
108
|
+
fl.flush()
|
|
109
|
+
fl.close()
|
|
110
|
+
|
|
@@ -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 = []
|