pyForceDAQ 2.0.2.dev0__tar.gz → 2.0.4__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.2.dev0 → pyforcedaq-2.0.4}/PKG-INFO +1 -1
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/pyproject.toml +1 -1
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/__init__.py +8 -8
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/__main__.py +14 -8
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/clock.py +8 -10
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/data_recorder.py +88 -58
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/lsl.py +11 -8
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/misc.py +45 -13
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/polling_time_profile.py +12 -9
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/process_priority_manager.py +28 -21
- pyforcedaq-2.0.4/src/pyforcedaq/_lib/sensor.py +110 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/sensor_process.py +86 -80
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/settings.py +60 -48
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/types.py +77 -67
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/udp_connection.py +33 -26
- pyforcedaq-2.0.4/src/pyforcedaq/constants.py +9 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/__init__.py +28 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_dll.py +20 -0
- pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_iaftt.py +17 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/daq/_pyATIDAQ.py +2 -9
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/daq/_use_nidaqmx.py +25 -23
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/daq/_use_pydaqmx.py +38 -36
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/force.py +1 -1
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/__init__.py +1 -2
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/_gui_status.py +54 -39
- pyforcedaq-2.0.4/src/pyforcedaq/gui/_layout.py +120 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/_level_indicator.py +28 -18
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/_pg_surface.py +1 -2
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/_plotter.py +90 -67
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/_run.py +217 -151
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/_scaling.py +13 -12
- pyforcedaq-2.0.4/src/pyforcedaq/launcher.py +168 -0
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib/_log.py +0 -19
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib/sensor.py +0 -107
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/daq/__init__.py +0 -20
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/gui/_layout.py +0 -102
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/gui/launcher.py +0 -117
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/daq/_mock_sensor.py +0 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/extras/__init__.py +0 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/extras/convert.py +0 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/extras/read_force_data.py +0 -0
- {pyforcedaq-2.0.2.dev0 → pyforcedaq-2.0.4}/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
|
@@ -30,11 +30,11 @@ __author__ = "Oliver Lindemann"
|
|
|
30
30
|
|
|
31
31
|
import sys as _sys
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
if _sys.version_info[0] != 3 or _sys.version_info[1] < 12:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
"pyForceDAQ {0} ".format(__version__)
|
|
36
|
+
+ "is not compatible with Python {0}.{1}. ".format(
|
|
37
|
+
_sys.version_info[0], _sys.version_info[1]
|
|
38
|
+
)
|
|
39
|
+
+ "Please use Python 3.12+."
|
|
40
|
+
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
import argparse
|
|
3
2
|
|
|
4
|
-
from . import __author__, __version__
|
|
3
|
+
from . import __author__, __version__
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
def cli():
|
|
@@ -18,18 +17,23 @@ def cli():
|
|
|
18
17
|
parser.add_argument("SETTINGS_FILE", nargs="?", default="", help="settings file")
|
|
19
18
|
|
|
20
19
|
parser.add_argument(
|
|
21
|
-
"-l",
|
|
20
|
+
"-l",
|
|
21
|
+
"--launcher",
|
|
22
22
|
action="store_true",
|
|
23
23
|
default=False,
|
|
24
24
|
help="Run with launcher GUI to edit settings and start recording",
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
parser.add_argument(
|
|
28
|
-
"-o",
|
|
28
|
+
"-o",
|
|
29
|
+
"--omit-launcher",
|
|
29
30
|
action="store_true",
|
|
30
31
|
default=False,
|
|
31
32
|
help="Omit launcher GUI and start recording directly",
|
|
32
33
|
)
|
|
34
|
+
print("+" + "-" * 23 + "+")
|
|
35
|
+
print(f"| pyforceDAQ {__version__}".ljust(24) + "|")
|
|
36
|
+
print("+" + "-" * 23 + "+")
|
|
33
37
|
|
|
34
38
|
args = parser.parse_args()
|
|
35
39
|
|
|
@@ -38,12 +42,14 @@ def cli():
|
|
|
38
42
|
print("Can't use launcher and settings file")
|
|
39
43
|
exit()
|
|
40
44
|
|
|
41
|
-
from .
|
|
42
|
-
|
|
45
|
+
from .launcher import run_launcher
|
|
46
|
+
|
|
47
|
+
return run_launcher()
|
|
43
48
|
else:
|
|
44
|
-
gui
|
|
49
|
+
from .gui import run_settings_file
|
|
45
50
|
|
|
51
|
+
run_settings_file(args.SETTINGS_FILE)
|
|
46
52
|
|
|
47
53
|
|
|
48
|
-
if __name__ == "__main__":
|
|
54
|
+
if __name__ == "__main__": # required because of threading
|
|
49
55
|
cli()
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""A high-resolution monotonic timer based on LSL's local_clock() function.
|
|
2
|
-
"""
|
|
3
|
-
|
|
1
|
+
"""A high-resolution monotonic timer based on LSL's local_clock() function."""
|
|
4
2
|
|
|
5
3
|
from time import sleep
|
|
6
4
|
|
|
@@ -11,24 +9,24 @@ def local_clock_ms():
|
|
|
11
9
|
"""Returns the current time in milliseconds, based on LSL's local_clock()"""
|
|
12
10
|
return local_clock() * 1000
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"""
|
|
12
|
+
|
|
13
|
+
def wait_ms(waiting_time: int | float, looptime: int = 200) -> None:
|
|
14
|
+
"""Wait for a certain amount of milliseconds."""
|
|
17
15
|
start = local_clock_ms()
|
|
18
16
|
if waiting_time > looptime:
|
|
19
17
|
sleep((waiting_time - looptime) / 1000)
|
|
20
18
|
while local_clock_ms() < start + waiting_time:
|
|
21
19
|
pass
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
|
|
22
|
+
class StopWatch(object): #
|
|
24
23
|
"""A simple timer"""
|
|
25
24
|
|
|
26
25
|
def __init__(self):
|
|
27
26
|
self._init_time = local_clock()
|
|
28
27
|
|
|
29
28
|
def reset_stopwatch(self):
|
|
30
|
-
"""Reset the stopwatch time to zero.
|
|
31
|
-
"""
|
|
29
|
+
"""Reset the stopwatch time to zero."""
|
|
32
30
|
self._init_time = local_clock()
|
|
33
31
|
|
|
34
32
|
@property
|
|
@@ -37,4 +35,4 @@ class StopWatch(object):#
|
|
|
37
35
|
|
|
38
36
|
@property
|
|
39
37
|
def time_ms(self) -> float:
|
|
40
|
-
return self.time * 1000
|
|
38
|
+
return self.time * 1000
|
|
@@ -2,22 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
See COPYING file distributed along with the pyForceDAQ copyright and license terms.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
__author__ = "Oliver Lindemann"
|
|
6
7
|
|
|
7
8
|
import atexit
|
|
8
9
|
import gzip
|
|
9
10
|
import logging
|
|
10
|
-
from fileinput import filename
|
|
11
11
|
from io import TextIOWrapper
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from time import asctime, localtime, strftime
|
|
14
14
|
from typing import List
|
|
15
15
|
|
|
16
|
-
from icecream import ic
|
|
17
|
-
|
|
18
16
|
from .. import __version__ as forceDAQVersion
|
|
19
|
-
from . import _log
|
|
20
17
|
from .clock import wait_ms
|
|
18
|
+
from .misc import set_logging
|
|
21
19
|
from .process_priority_manager import ProcessPriorityManager
|
|
22
20
|
from .sensor_process import SensorProcess
|
|
23
21
|
from .settings import RecordingSettings, SensorSettings
|
|
@@ -32,19 +30,20 @@ from .types import (
|
|
|
32
30
|
)
|
|
33
31
|
from .udp_connection import UDPConnectionProcess
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
set_logging(data_directory="data", log_file="recording.log")
|
|
36
34
|
|
|
37
35
|
NEWLINE = "\n"
|
|
38
36
|
|
|
37
|
+
|
|
39
38
|
class DataRecorder(object):
|
|
40
39
|
"""handles multiple sensors and udp connection"""
|
|
41
40
|
|
|
42
|
-
def __init__(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
recording_settings: RecordingSettings,
|
|
44
|
+
force_sensor_settings: SensorSettings | List[SensorSettings],
|
|
45
|
+
poll_udp_connection: bool = False,
|
|
46
|
+
):
|
|
48
47
|
"""queue_data will be saved
|
|
49
48
|
see sensorprocess.__init__
|
|
50
49
|
|
|
@@ -58,15 +57,17 @@ class DataRecorder(object):
|
|
|
58
57
|
self.recording_settings = recording_settings
|
|
59
58
|
|
|
60
59
|
# create sensor processes
|
|
61
|
-
self._force_sensor_processes =[]
|
|
60
|
+
self._force_sensor_processes = []
|
|
62
61
|
event_trigger = []
|
|
63
62
|
for fs in force_sensor_settings:
|
|
64
63
|
if not isinstance(fs, SensorSettings):
|
|
65
64
|
raise RuntimeError("Recorder needs a list of Force Sensor Settings!")
|
|
66
65
|
else:
|
|
67
|
-
fst = SensorProcess(
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
fst = SensorProcess(
|
|
67
|
+
sensor_settings=fs,
|
|
68
|
+
recording_settings=recording_settings,
|
|
69
|
+
pipe_buffered_data_after_pause=True,
|
|
70
|
+
)
|
|
70
71
|
fst.start()
|
|
71
72
|
event_trigger.append(fst.event_trigger)
|
|
72
73
|
self._force_sensor_processes.append(fst)
|
|
@@ -83,13 +84,15 @@ class DataRecorder(object):
|
|
|
83
84
|
self._proc_manager.add_subprocess(self.udp)
|
|
84
85
|
self._proc_manager.add_subprocess(self._force_sensor_processes)
|
|
85
86
|
if self.recording_settings.priority is not None:
|
|
86
|
-
|
|
87
|
+
level = PollingPriority.NORMAL
|
|
87
88
|
else:
|
|
88
89
|
level = PollingPriority.get_priority(self.recording_settings.priority)
|
|
89
90
|
self._proc_manager.set_subprocess_priorities(level=level, disable_gc=False)
|
|
90
91
|
|
|
91
|
-
logging.info(
|
|
92
|
-
|
|
92
|
+
logging.info(
|
|
93
|
+
"Main process priority: %s", self._proc_manager.get_main_priority()
|
|
94
|
+
)
|
|
95
|
+
# logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
|
|
93
96
|
|
|
94
97
|
self._is_recording = False
|
|
95
98
|
self._file = None
|
|
@@ -121,8 +124,7 @@ class DataRecorder(object):
|
|
|
121
124
|
|
|
122
125
|
@property
|
|
123
126
|
def sensor_settings_list(self):
|
|
124
|
-
return list(map(lambda x:x.sensor_settings,
|
|
125
|
-
self._force_sensor_processes))
|
|
127
|
+
return list(map(lambda x: x.sensor_settings, self._force_sensor_processes))
|
|
126
128
|
|
|
127
129
|
def quit(self) -> list | None:
|
|
128
130
|
"""Stop all recording processes, close data file and quit recording
|
|
@@ -155,28 +157,28 @@ class DataRecorder(object):
|
|
|
155
157
|
buffer = []
|
|
156
158
|
while True:
|
|
157
159
|
try:
|
|
158
|
-
data = self.udp.receive_queue.get_nowait()
|
|
160
|
+
data = self.udp.receive_queue.get_nowait() # type: ignore
|
|
159
161
|
except AttributeError:
|
|
160
162
|
# until queue empty or no udp connection
|
|
161
163
|
break
|
|
162
164
|
buffer.append(data)
|
|
163
|
-
if len(buffer)>0:
|
|
165
|
+
if len(buffer) > 0:
|
|
164
166
|
self._write_data(buffer)
|
|
165
167
|
return buffer
|
|
166
168
|
|
|
167
|
-
def _write_data(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"""
|
|
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
|
|
171
173
|
|
|
172
174
|
ignores UDP remote control commands
|
|
173
175
|
"""
|
|
174
|
-
#DOC output format
|
|
176
|
+
# DOC output format
|
|
175
177
|
|
|
176
|
-
BLOCKSIZE = 10000
|
|
178
|
+
BLOCKSIZE = 10000 # for recording screen feedback only
|
|
177
179
|
write_forces = self.recording_settings.array_write_forces()
|
|
178
180
|
write_trigger = self.recording_settings.array_write_trigger()
|
|
179
|
-
write_deviceid = len(self.recording_settings.device_labels)>1
|
|
181
|
+
write_deviceid = len(self.recording_settings.device_labels) > 1
|
|
180
182
|
float_format = "{0:." + str(float_decimal_places) + "f},"
|
|
181
183
|
buffer_len = len(data_buffer)
|
|
182
184
|
for c, d in enumerate(data_buffer):
|
|
@@ -185,9 +187,9 @@ class DataRecorder(object):
|
|
|
185
187
|
line = f"{d.time}, {d.acquisition_delay},"
|
|
186
188
|
if write_deviceid:
|
|
187
189
|
line += f"{d.sensor_id},"
|
|
188
|
-
for x in d.
|
|
190
|
+
for x in d.forces[write_forces]:
|
|
189
191
|
line += float_format.format(x)
|
|
190
|
-
for x in d.
|
|
192
|
+
for x in d.trigger[write_trigger]:
|
|
191
193
|
if isinstance(x, int):
|
|
192
194
|
line += f"{x},"
|
|
193
195
|
else:
|
|
@@ -202,8 +204,10 @@ class DataRecorder(object):
|
|
|
202
204
|
|
|
203
205
|
if recording_screen is not None and c % BLOCKSIZE == 0:
|
|
204
206
|
recording_screen.stimulus(
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
+
"Saving {0} of {1} blocks".format(
|
|
208
|
+
c // BLOCKSIZE, buffer_len // BLOCKSIZE
|
|
209
|
+
)
|
|
210
|
+
).present()
|
|
207
211
|
|
|
208
212
|
def _file_write(self, s: str) -> None:
|
|
209
213
|
|
|
@@ -212,15 +216,15 @@ class DataRecorder(object):
|
|
|
212
216
|
elif isinstance(self._file, TextIOWrapper):
|
|
213
217
|
self._file.write(s)
|
|
214
218
|
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
def store_daq_event(
|
|
220
|
+
self, code: str | int | float, time: float | None = None
|
|
221
|
+
) -> None:
|
|
217
222
|
"""Set marker code in file
|
|
218
223
|
|
|
219
224
|
DAQEvent will be timestamps and occur in the data output
|
|
220
225
|
|
|
221
226
|
"""
|
|
222
|
-
self._daq_event.append(DAQEvents(time
|
|
223
|
-
|
|
227
|
+
self._daq_event.append(DAQEvents(time=time, code=code))
|
|
224
228
|
|
|
225
229
|
def start_recording(self, determine_bias: bool = False) -> None:
|
|
226
230
|
"""Start polling process and record
|
|
@@ -234,12 +238,20 @@ class DataRecorder(object):
|
|
|
234
238
|
if determine_bias:
|
|
235
239
|
self.determine_biases(n_samples=1000)
|
|
236
240
|
|
|
237
|
-
if len(
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
if len(
|
|
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
|
+
)
|
|
240
252
|
|
|
241
253
|
# start polling
|
|
242
|
-
list(map(lambda x:x.start_polling(), self._force_sensor_processes))
|
|
254
|
+
list(map(lambda x: x.start_polling(), self._force_sensor_processes))
|
|
243
255
|
self._is_recording = True
|
|
244
256
|
|
|
245
257
|
def pause_recording(self, recording_screen=None) -> list:
|
|
@@ -254,14 +266,20 @@ class DataRecorder(object):
|
|
|
254
266
|
|
|
255
267
|
data = []
|
|
256
268
|
if recording_screen is not None:
|
|
257
|
-
recording_screen.stimulus("
|
|
269
|
+
recording_screen.stimulus("").present()
|
|
258
270
|
|
|
259
|
-
#pause polling
|
|
271
|
+
# pause polling
|
|
260
272
|
for fsp in self._force_sensor_processes:
|
|
261
273
|
fsp.pause_polling()
|
|
262
274
|
|
|
263
275
|
wait_ms(500)
|
|
264
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
|
+
|
|
265
283
|
# get data
|
|
266
284
|
for fsp in self._force_sensor_processes:
|
|
267
285
|
buffer = fsp.get_buffer()
|
|
@@ -298,11 +316,13 @@ class DataRecorder(object):
|
|
|
298
316
|
for x in self._force_sensor_processes:
|
|
299
317
|
x.event_bias_is_available.wait()
|
|
300
318
|
|
|
301
|
-
def open_data_file(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
319
|
+
def open_data_file(
|
|
320
|
+
self,
|
|
321
|
+
filename: str | Path,
|
|
322
|
+
subdirectory: str = "data",
|
|
323
|
+
varnames: bool = True,
|
|
324
|
+
comment_line: str = "",
|
|
325
|
+
) -> Path:
|
|
306
326
|
"""Create a data file
|
|
307
327
|
|
|
308
328
|
Only if data file has been opened, data will be saved!
|
|
@@ -341,40 +361,50 @@ class DataRecorder(object):
|
|
|
341
361
|
self.path_open_file = data_dir / filename
|
|
342
362
|
if self.path_open_file.is_file():
|
|
343
363
|
# print "data file already exists, adding counter"
|
|
344
|
-
filename = Path(
|
|
345
|
-
|
|
364
|
+
filename = Path(
|
|
365
|
+
filename.stem
|
|
366
|
+
+ "_"
|
|
367
|
+
+ strftime("%m%d%H%M", localtime())
|
|
368
|
+
+ filename.suffix
|
|
369
|
+
)
|
|
346
370
|
else:
|
|
347
371
|
break
|
|
348
372
|
|
|
349
373
|
if self.recording_settings.zip_data:
|
|
350
|
-
self._file = gzip.open(self.path_open_file,
|
|
374
|
+
self._file = gzip.open(self.path_open_file, "w")
|
|
351
375
|
else:
|
|
352
|
-
self._file = open(self.path_open_file,
|
|
376
|
+
self._file = open(self.path_open_file, "w")
|
|
353
377
|
print("Data file: {}".format(self.path_open_file))
|
|
354
378
|
|
|
355
|
-
self._file_write(
|
|
356
|
-
|
|
379
|
+
self._file_write(
|
|
380
|
+
TAG_COMMENTS
|
|
381
|
+
+ "Recorded at {0} with pyForceDAQ {1}\n".format(
|
|
382
|
+
asctime(localtime()), forceDAQVersion
|
|
383
|
+
)
|
|
384
|
+
)
|
|
357
385
|
logging.info("new file: {}".format(self.path_open_file))
|
|
358
386
|
|
|
359
387
|
for s in self.sensor_settings_list:
|
|
360
388
|
txt = f" Sensor: label={s.device_label}, cal-file={s.calibration_file}\n"
|
|
361
389
|
self._file_write(TAG_COMMENTS + txt)
|
|
362
390
|
|
|
363
|
-
if len(comment_line)>0:
|
|
391
|
+
if len(comment_line) > 0:
|
|
364
392
|
self._file_write(TAG_COMMENTS + comment_line + "\n")
|
|
365
393
|
|
|
366
394
|
if varnames:
|
|
367
395
|
write_forces = self.recording_settings.array_write_forces()
|
|
368
396
|
write_trigger = self.recording_settings.array_write_trigger()
|
|
369
|
-
write_deviceid = len(self.recording_settings.device_labels)>1
|
|
397
|
+
write_deviceid = len(self.recording_settings.device_labels) > 1
|
|
370
398
|
line = "time,delay,"
|
|
371
399
|
if write_deviceid:
|
|
372
400
|
line += "device_tag,"
|
|
373
401
|
for x in range(6):
|
|
374
402
|
if write_forces[x]:
|
|
375
403
|
line += ForceSensorData.forces_names[x] + ","
|
|
376
|
-
if write_trigger[0]:
|
|
377
|
-
|
|
404
|
+
if write_trigger[0]:
|
|
405
|
+
line += "trigger1,"
|
|
406
|
+
if write_trigger[1]:
|
|
407
|
+
line += "trigger2,"
|
|
378
408
|
self._file_write(line[:-1] + NEWLINE)
|
|
379
409
|
|
|
380
410
|
return self.path_open_file
|
|
@@ -12,8 +12,7 @@ from pylsl import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class LSLSream
|
|
16
|
-
|
|
15
|
+
class LSLSream:
|
|
17
16
|
def __init__(self):
|
|
18
17
|
self.outlet = None
|
|
19
18
|
self._is_init = False
|
|
@@ -22,7 +21,8 @@ class LSLSream():
|
|
|
22
21
|
def is_init(self):
|
|
23
22
|
return self._is_init
|
|
24
23
|
|
|
25
|
-
def init(
|
|
24
|
+
def init(
|
|
25
|
+
self,
|
|
26
26
|
name: str,
|
|
27
27
|
n_channels: int,
|
|
28
28
|
stream_id: str,
|
|
@@ -50,11 +50,14 @@ class LSLSream():
|
|
|
50
50
|
if self._is_init:
|
|
51
51
|
return
|
|
52
52
|
|
|
53
|
-
info = StreamInfo(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
info = StreamInfo(
|
|
54
|
+
name,
|
|
55
|
+
"force",
|
|
56
|
+
channel_count=n_channels,
|
|
57
|
+
nominal_srate=freq,
|
|
58
|
+
channel_format=channel_format,
|
|
59
|
+
source_id=stream_id,
|
|
60
|
+
)
|
|
58
61
|
|
|
59
62
|
# Check if there is metadata to add to the lsl stream
|
|
60
63
|
if metadata:
|
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..constants import SETTINGS_FILE_EXTENSION
|
|
1
7
|
from .clock import local_clock_ms
|
|
2
8
|
|
|
3
9
|
|
|
10
|
+
def set_logging(data_directory, log_file):
|
|
11
|
+
base_dir = os.path.split(sys.argv[0])[0]
|
|
12
|
+
log_dir = os.path.join(base_dir, data_directory)
|
|
13
|
+
try:
|
|
14
|
+
os.mkdir(log_dir)
|
|
15
|
+
except:
|
|
16
|
+
pass
|
|
17
|
+
log_file = os.path.abspath(os.path.join(log_dir, log_file))
|
|
18
|
+
logging.basicConfig(
|
|
19
|
+
level=logging.INFO,
|
|
20
|
+
format="[%(asctime)s] %(message)s",
|
|
21
|
+
datefmt="%m-%d %H:%M:%S",
|
|
22
|
+
filename=log_file,
|
|
23
|
+
filemode="a",
|
|
24
|
+
)
|
|
25
|
+
return log_file
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_settings_files():
|
|
29
|
+
"""Returns a list of all settings files in the current directory."""
|
|
30
|
+
return [f.name for f in Path(".").glob(f"*{SETTINGS_FILE_EXTENSION}")]
|
|
31
|
+
|
|
32
|
+
|
|
4
33
|
def N2g(N):
|
|
5
|
-
kg = N/9.81
|
|
6
|
-
return kg*1000
|
|
34
|
+
kg = N / 9.81
|
|
35
|
+
return kg * 1000
|
|
7
36
|
|
|
8
|
-
class MinMaxDetector(object):
|
|
9
37
|
|
|
38
|
+
class MinMaxDetector(object):
|
|
10
39
|
def __init__(self, start_value, duration_ms):
|
|
11
40
|
self._minmax = [start_value, start_value]
|
|
12
41
|
self._duration_ms = duration_ms
|
|
@@ -25,7 +54,7 @@ class MinMaxDetector(object):
|
|
|
25
54
|
elif value < self._minmax[0]:
|
|
26
55
|
self._minmax[0] = value
|
|
27
56
|
|
|
28
|
-
elif self._minmax[0] != value:
|
|
57
|
+
elif self._minmax[0] != value: # level change just occurred
|
|
29
58
|
self._level_change_time = local_clock_ms()
|
|
30
59
|
return self.process(value)
|
|
31
60
|
|
|
@@ -34,8 +63,10 @@ class MinMaxDetector(object):
|
|
|
34
63
|
@property
|
|
35
64
|
def is_sampling_for_minmax(self):
|
|
36
65
|
"""true true if currently sampling for minmax"""
|
|
37
|
-
return (self._level_change_time is not None) and
|
|
38
|
-
|
|
66
|
+
return (self._level_change_time is not None) and (
|
|
67
|
+
local_clock_ms() - self._level_change_time
|
|
68
|
+
) < self._duration_ms
|
|
69
|
+
|
|
39
70
|
|
|
40
71
|
# def find_calibration_file(calibration_folder: str, device_label: str,
|
|
41
72
|
# calibration_suffix=".cal") -> str:
|
|
@@ -63,7 +94,8 @@ class MinMaxDetector(object):
|
|
|
63
94
|
# print("No calibration file found for sensor '{0}'.".format(device_label))
|
|
64
95
|
# exit()
|
|
65
96
|
|
|
66
|
-
|
|
97
|
+
|
|
98
|
+
# Sensor History with moving average filtering and distance, velocity
|
|
67
99
|
class SensorHistory(object):
|
|
68
100
|
"""The Sensory History keeps track of the last n recorded sample and
|
|
69
101
|
calculates online the moving average (running mean).
|
|
@@ -101,10 +133,12 @@ class SensorHistory(object):
|
|
|
101
133
|
self.moving_average = self.calc_history_average()
|
|
102
134
|
else:
|
|
103
135
|
self._correction_cnt += 1
|
|
104
|
-
self.moving_average = list(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
self.moving_average = list(
|
|
137
|
+
map(
|
|
138
|
+
lambda x: x[0] + (float(x[1] - x[2]) / len(self.history)),
|
|
139
|
+
zip(self.moving_average, values, pop),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
108
142
|
|
|
109
143
|
def calc_history_average(self):
|
|
110
144
|
"""Calculate history averages for all sensor parameter.
|
|
@@ -120,7 +154,6 @@ class SensorHistory(object):
|
|
|
120
154
|
s = list(map(lambda x: x[0] + x[1], zip(s, t)))
|
|
121
155
|
return list(map(lambda x: x / len(self.history), s))
|
|
122
156
|
|
|
123
|
-
|
|
124
157
|
@property
|
|
125
158
|
def history_size(self):
|
|
126
159
|
return len(self.history)
|
|
@@ -132,4 +165,3 @@ class SensorHistory(object):
|
|
|
132
165
|
@property
|
|
133
166
|
def previous_moving_average(self):
|
|
134
167
|
return self._previous_moving_average
|
|
135
|
-
|
|
@@ -4,13 +4,12 @@ from .clock import local_clock
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class PollingTimeProfile(object):
|
|
7
|
-
|
|
8
7
|
def __init__(self, timing_range_ms=10):
|
|
9
8
|
self._last = None
|
|
10
9
|
self._timing_range_ms = 10
|
|
11
10
|
self._zero_cnt = 0
|
|
12
11
|
|
|
13
|
-
#self._zero_time_polling_frequency = {}
|
|
12
|
+
# self._zero_time_polling_frequency = {}
|
|
14
13
|
self.profile_frequency = np.array([0] * (timing_range_ms + 1))
|
|
15
14
|
|
|
16
15
|
def stop(self):
|
|
@@ -25,9 +24,9 @@ class PollingTimeProfile(object):
|
|
|
25
24
|
d = self._timing_range_ms
|
|
26
25
|
self.profile_frequency[d] += 1
|
|
27
26
|
|
|
28
|
-
#if d == 0:
|
|
27
|
+
# if d == 0:
|
|
29
28
|
# self._zero_cnt += 1
|
|
30
|
-
#elif self._zero_cnt > 0:
|
|
29
|
+
# elif self._zero_cnt > 0:
|
|
31
30
|
# try:
|
|
32
31
|
# self._zero_time_polling_frequency[self._zero_cnt] += 1
|
|
33
32
|
# except:
|
|
@@ -45,10 +44,14 @@ class PollingTimeProfile(object):
|
|
|
45
44
|
return self.profile_frequency / n
|
|
46
45
|
|
|
47
46
|
def get_profile_str(self):
|
|
48
|
-
rtn =
|
|
49
|
-
|
|
47
|
+
rtn = (
|
|
48
|
+
str(list(self.profile_frequency))
|
|
49
|
+
.replace("[", "")
|
|
50
|
+
.replace("]", "")
|
|
51
|
+
.replace(" ", "")
|
|
52
|
+
)
|
|
50
53
|
return "polling profile [{}]".format(rtn)
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
#def zero_time_polling_frequency(self):
|
|
54
|
-
# return np.array(list(self._zero_time_polling_frequency.items()))
|
|
55
|
+
# @property
|
|
56
|
+
# def zero_time_polling_frequency(self):
|
|
57
|
+
# return np.array(list(self._zero_time_polling_frequency.items()))
|