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