pyForceDAQ 2.0.1.dev2__tar.gz → 2.0.2.dev0__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.1.dev2 → pyforcedaq-2.0.2.dev0}/PKG-INFO +2 -2
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/pyproject.toml +2 -2
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/__init__.py +2 -7
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/__main__.py +10 -3
- {pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib}/_log.py +2 -1
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib/clock.py +40 -0
- {pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib}/data_recorder.py +82 -86
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib/lsl.py +75 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/_lib/misc.py +11 -13
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/_lib/polling_time_profile.py +10 -8
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib/sensor.py +107 -0
- {pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib}/sensor_process.py +58 -43
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/_lib/settings.py +211 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/_lib/types.py +33 -80
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/_lib/udp_connection.py +13 -19
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/daq/__init__.py +1 -1
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/daq/_mock_sensor.py +8 -7
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/daq/_use_nidaqmx.py +3 -1
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/daq/_use_pydaqmx.py +2 -1
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/force.py +11 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/__init__.py +2 -2
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/gui/_gui_status.py +223 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/_layout.py +5 -7
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/_run.py +69 -172
- pyforcedaq-2.0.2.dev0/src/pyforcedaq/gui/launcher.py +117 -0
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/_lib/lsl.py +0 -56
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/_lib/timer.py +0 -45
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/daq/config.py +0 -13
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/extras/expyriment_daq_control.py +0 -246
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/extras/opensesame_daq_control.py +0 -280
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/extras/remote_control.py +0 -93
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/force/__init__.py +0 -13
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/force/sensor.py +0 -195
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/gui/_gui_status.py +0 -306
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/gui/_settings.py +0 -101
- pyforcedaq-2.0.1.dev2/src/pyforcedaq/gui/launcher.py +0 -250
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/_lib/process_priority_manager.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/daq/_pyATIDAQ.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/extras/__init__.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/extras/convert.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/extras/read_force_data.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/_level_indicator.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/_pg_surface.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/_plotter.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/_scaling.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2.dev0}/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyForceDAQ
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2.dev0
|
|
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: freesimplegui>=5.2.0.post1
|
|
10
9
|
Requires-Dist: icecream>=2.2.0
|
|
11
10
|
Requires-Dist: nidaqmx>=1.5.0
|
|
12
11
|
Requires-Dist: numpy>=2.4.4
|
|
13
12
|
Requires-Dist: psutil>=7.2.2
|
|
14
13
|
Requires-Dist: pydaqmx>=1.4.7
|
|
15
14
|
Requires-Dist: pylsl>=1.18.2
|
|
15
|
+
Requires-Dist: pysimplegui>=6.0
|
|
16
16
|
Requires-Dist: pyxdf>=1.17.4
|
|
17
17
|
Requires-Dist: tomlkit>=0.15.0
|
|
18
18
|
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.2-dev0"
|
|
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,13 +9,13 @@ requires-python = ">=3.12, <3.14"
|
|
|
9
9
|
dependencies = [
|
|
10
10
|
"atiiaftt>=0.1.1",
|
|
11
11
|
"expyriment>=1.0.1",
|
|
12
|
-
"freesimplegui>=5.2.0.post1",
|
|
13
12
|
"icecream>=2.2.0",
|
|
14
13
|
"nidaqmx>=1.5.0",
|
|
15
14
|
"numpy>=2.4.4",
|
|
16
15
|
"psutil>=7.2.2",
|
|
17
16
|
"pydaqmx>=1.4.7",
|
|
18
17
|
"pylsl>=1.18.2",
|
|
18
|
+
"pysimplegui>=6.0",
|
|
19
19
|
"pyxdf>=1.17.4",
|
|
20
20
|
"tomlkit>=0.15.0",
|
|
21
21
|
]
|
|
@@ -7,9 +7,8 @@ launch the GUI force from your Python program:
|
|
|
7
7
|
``
|
|
8
8
|
from pyforcedaq import gui
|
|
9
9
|
|
|
10
|
-
gui.run(
|
|
11
|
-
|
|
12
|
-
calibration_file="FT_sensor1.cal")
|
|
10
|
+
gui.run(ask_filename=True,
|
|
11
|
+
calibration_file="FT_sensor1.cal") TODO
|
|
13
12
|
``
|
|
14
13
|
|
|
15
14
|
|
|
@@ -18,10 +17,6 @@ import relevant stuff to program your own force:
|
|
|
18
17
|
from pyforcedaq import force
|
|
19
18
|
``
|
|
20
19
|
|
|
21
|
-
import relevant stuff for remote control of the GUI force:
|
|
22
|
-
``
|
|
23
|
-
from pyforcedaq import remote_control
|
|
24
|
-
``
|
|
25
20
|
|
|
26
21
|
For function to support data handling see the folder pyForceDAQ/analysis
|
|
27
22
|
|
|
@@ -24,17 +24,24 @@ def cli():
|
|
|
24
24
|
help="Run with launcher GUI to edit settings and start recording",
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-o", "--omit-launcher",
|
|
29
|
+
action="store_true",
|
|
30
|
+
default=False,
|
|
31
|
+
help="Omit launcher GUI and start recording directly",
|
|
32
|
+
)
|
|
33
|
+
|
|
27
34
|
args = parser.parse_args()
|
|
28
35
|
|
|
29
|
-
if args.
|
|
36
|
+
if not args.omit_launcher:
|
|
30
37
|
if len(args.SETTINGS_FILE) > 0:
|
|
31
38
|
print("Can't use launcher and settings file")
|
|
32
39
|
exit()
|
|
33
40
|
|
|
34
41
|
from .gui import launcher
|
|
35
|
-
return launcher.
|
|
42
|
+
return launcher.run_launcher()
|
|
36
43
|
else:
|
|
37
|
-
gui.
|
|
44
|
+
gui.run_settings_file(args.SETTINGS_FILE)
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""A high-resolution monotonic timer based on LSL's local_clock() function.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from time import sleep
|
|
6
|
+
|
|
7
|
+
from pylsl import local_clock
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def local_clock_ms():
|
|
11
|
+
"""Returns the current time in milliseconds, based on LSL's local_clock()"""
|
|
12
|
+
return local_clock() * 1000
|
|
13
|
+
|
|
14
|
+
def wait_ms(waiting_time: int | float, looptime : int = 200) -> None:
|
|
15
|
+
"""Wait for a certain amount of milliseconds.
|
|
16
|
+
"""
|
|
17
|
+
start = local_clock_ms()
|
|
18
|
+
if waiting_time > looptime:
|
|
19
|
+
sleep((waiting_time - looptime) / 1000)
|
|
20
|
+
while local_clock_ms() < start + waiting_time:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
class StopWatch(object):#
|
|
24
|
+
"""A simple timer"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._init_time = local_clock()
|
|
28
|
+
|
|
29
|
+
def reset_stopwatch(self):
|
|
30
|
+
"""Reset the stopwatch time to zero.
|
|
31
|
+
"""
|
|
32
|
+
self._init_time = local_clock()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def time(self) -> float:
|
|
36
|
+
return local_clock() - self._init_time
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def time_ms(self) -> float:
|
|
40
|
+
return self.time * 1000
|
|
@@ -7,15 +7,21 @@ __author__ = "Oliver Lindemann"
|
|
|
7
7
|
import atexit
|
|
8
8
|
import gzip
|
|
9
9
|
import logging
|
|
10
|
+
from fileinput import filename
|
|
11
|
+
from io import TextIOWrapper
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from time import asctime, localtime, strftime
|
|
14
|
+
from typing import List
|
|
12
15
|
|
|
13
|
-
from
|
|
16
|
+
from icecream import ic
|
|
14
17
|
|
|
15
18
|
from .. import __version__ as forceDAQVersion
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
+
from . import _log
|
|
20
|
+
from .clock import wait_ms
|
|
21
|
+
from .process_priority_manager import ProcessPriorityManager
|
|
22
|
+
from .sensor_process import SensorProcess
|
|
23
|
+
from .settings import RecordingSettings, SensorSettings
|
|
24
|
+
from .types import (
|
|
19
25
|
TAG_COMMENTS,
|
|
20
26
|
TAG_DAQEVENT,
|
|
21
27
|
TAG_UDPDATA,
|
|
@@ -24,20 +30,19 @@ from .._lib.types import (
|
|
|
24
30
|
PollingPriority,
|
|
25
31
|
UDPData,
|
|
26
32
|
)
|
|
27
|
-
from
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
from .sensor_process import SensorProcess
|
|
33
|
+
from .udp_connection import UDPConnectionProcess
|
|
34
|
+
|
|
35
|
+
_log.set_logging(data_directory="data", log_file="recording.log")
|
|
31
36
|
|
|
32
37
|
NEWLINE = "\n"
|
|
33
38
|
|
|
34
39
|
class DataRecorder(object):
|
|
35
40
|
"""handles multiple sensors and udp connection"""
|
|
36
41
|
|
|
37
|
-
def __init__(self,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
def __init__(self,
|
|
43
|
+
recording_settings: RecordingSettings,
|
|
44
|
+
force_sensor_settings:SensorSettings | List[SensorSettings],
|
|
45
|
+
poll_udp_connection: bool = False):
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
"""queue_data will be saved
|
|
@@ -47,13 +52,10 @@ class DataRecorder(object):
|
|
|
47
52
|
{REALTIME} or {NORMAL} or None
|
|
48
53
|
"""
|
|
49
54
|
|
|
50
|
-
self._write_deviceid = write_deviceid
|
|
51
|
-
|
|
52
55
|
if not isinstance(force_sensor_settings, list):
|
|
53
56
|
force_sensor_settings = [force_sensor_settings]
|
|
54
|
-
|
|
55
|
-
self.
|
|
56
|
-
self._write_trigger = force_sensor_settings[0].array_write_trigger()
|
|
57
|
+
|
|
58
|
+
self.recording_settings = recording_settings
|
|
57
59
|
|
|
58
60
|
# create sensor processes
|
|
59
61
|
self._force_sensor_processes =[]
|
|
@@ -62,7 +64,8 @@ class DataRecorder(object):
|
|
|
62
64
|
if not isinstance(fs, SensorSettings):
|
|
63
65
|
raise RuntimeError("Recorder needs a list of Force Sensor Settings!")
|
|
64
66
|
else:
|
|
65
|
-
fst = SensorProcess(
|
|
67
|
+
fst = SensorProcess(sensor_settings = fs,
|
|
68
|
+
recording_settings=recording_settings,
|
|
66
69
|
pipe_buffered_data_after_pause=True)
|
|
67
70
|
fst.start()
|
|
68
71
|
event_trigger.append(fst.event_trigger)
|
|
@@ -70,20 +73,19 @@ class DataRecorder(object):
|
|
|
70
73
|
|
|
71
74
|
# create udp connection process
|
|
72
75
|
if poll_udp_connection:
|
|
73
|
-
self.udp = UDPConnectionProcess(event_trigger=event_trigger
|
|
74
|
-
event_ignore_tag = RemoteCmd.COMMAND_STR)
|
|
76
|
+
self.udp = UDPConnectionProcess(event_trigger=event_trigger)
|
|
75
77
|
self.udp.start()
|
|
76
78
|
else:
|
|
77
79
|
self.udp = None
|
|
78
80
|
|
|
79
|
-
# process managing
|
|
81
|
+
# process managing FIYME needed?
|
|
80
82
|
self._proc_manager = ProcessPriorityManager()
|
|
81
83
|
self._proc_manager.add_subprocess(self.udp)
|
|
82
84
|
self._proc_manager.add_subprocess(self._force_sensor_processes)
|
|
83
|
-
if
|
|
85
|
+
if self.recording_settings.priority is not None:
|
|
84
86
|
level = PollingPriority.NORMAL
|
|
85
87
|
else:
|
|
86
|
-
level = PollingPriority.get_priority(
|
|
88
|
+
level = PollingPriority.get_priority(self.recording_settings.priority)
|
|
87
89
|
self._proc_manager.set_subprocess_priorities(level=level, disable_gc=False)
|
|
88
90
|
|
|
89
91
|
logging.info("Main process priority: %s", self._proc_manager.get_main_priority())
|
|
@@ -92,9 +94,13 @@ class DataRecorder(object):
|
|
|
92
94
|
self._is_recording = False
|
|
93
95
|
self._file = None
|
|
94
96
|
self._daq_event = []
|
|
95
|
-
self.
|
|
97
|
+
self.path_open_file = Path("")
|
|
96
98
|
atexit.register(self.quit)
|
|
97
99
|
|
|
100
|
+
@property
|
|
101
|
+
def is_saving_data(self):
|
|
102
|
+
"""Property indicates whether a data file is open"""
|
|
103
|
+
return self._file is not None
|
|
98
104
|
|
|
99
105
|
@property
|
|
100
106
|
def is_alive(self):
|
|
@@ -149,16 +155,16 @@ class DataRecorder(object):
|
|
|
149
155
|
buffer = []
|
|
150
156
|
while True:
|
|
151
157
|
try:
|
|
152
|
-
data = self.udp.receive_queue.get_nowait()
|
|
153
|
-
except:
|
|
158
|
+
data = self.udp.receive_queue.get_nowait() # type: ignore
|
|
159
|
+
except AttributeError:
|
|
154
160
|
# until queue empty or no udp connection
|
|
155
161
|
break
|
|
156
162
|
buffer.append(data)
|
|
157
163
|
if len(buffer)>0:
|
|
158
|
-
self.
|
|
164
|
+
self._write_data(buffer)
|
|
159
165
|
return buffer
|
|
160
166
|
|
|
161
|
-
def
|
|
167
|
+
def _write_data(self, data_buffer: list,
|
|
162
168
|
recording_screen=None,
|
|
163
169
|
float_decimal_places: int = 4) -> None:
|
|
164
170
|
""" writes data to disk and set counters
|
|
@@ -168,18 +174,20 @@ class DataRecorder(object):
|
|
|
168
174
|
#DOC output format
|
|
169
175
|
|
|
170
176
|
BLOCKSIZE = 10000 # for recording screen feedback only
|
|
171
|
-
|
|
177
|
+
write_forces = self.recording_settings.array_write_forces()
|
|
178
|
+
write_trigger = self.recording_settings.array_write_trigger()
|
|
179
|
+
write_deviceid = len(self.recording_settings.device_labels)>1
|
|
172
180
|
float_format = "{0:." + str(float_decimal_places) + "f},"
|
|
173
181
|
buffer_len = len(data_buffer)
|
|
174
182
|
for c, d in enumerate(data_buffer):
|
|
175
183
|
if self._file is not None:
|
|
176
184
|
if isinstance(d, ForceSensorData):
|
|
177
185
|
line = f"{d.time}, {d.acquisition_delay},"
|
|
178
|
-
if
|
|
179
|
-
line += f"{d.
|
|
180
|
-
for x in d.selected_forces(select=
|
|
186
|
+
if write_deviceid:
|
|
187
|
+
line += f"{d.sensor_id},"
|
|
188
|
+
for x in d.selected_forces(select=write_forces):
|
|
181
189
|
line += float_format.format(x)
|
|
182
|
-
for x in d.selected_trigger(select=
|
|
190
|
+
for x in d.selected_trigger(select=write_trigger):
|
|
183
191
|
if isinstance(x, int):
|
|
184
192
|
line += f"{x},"
|
|
185
193
|
else:
|
|
@@ -190,8 +198,7 @@ class DataRecorder(object):
|
|
|
190
198
|
self._file_write(f"{TAG_DAQEVENT},{d.time},{str(d.code)}" + NEWLINE)
|
|
191
199
|
|
|
192
200
|
elif isinstance(d, UDPData):
|
|
193
|
-
|
|
194
|
-
self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
|
|
201
|
+
self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
|
|
195
202
|
|
|
196
203
|
if recording_screen is not None and c % BLOCKSIZE == 0:
|
|
197
204
|
recording_screen.stimulus(
|
|
@@ -199,16 +206,19 @@ class DataRecorder(object):
|
|
|
199
206
|
buffer_len//BLOCKSIZE)).present()
|
|
200
207
|
|
|
201
208
|
def _file_write(self, s: str) -> None:
|
|
202
|
-
self._file.write(s.encode())
|
|
203
209
|
|
|
204
|
-
|
|
210
|
+
if isinstance(self._file, gzip.GzipFile):
|
|
211
|
+
self._file.write(s.encode("utf-8"))
|
|
212
|
+
elif isinstance(self._file, TextIOWrapper):
|
|
213
|
+
self._file.write(s)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def store_daq_event(self, code: str | int | float, time: float | None = None) -> None:
|
|
205
217
|
"""Set marker code in file
|
|
206
218
|
|
|
207
219
|
DAQEvent will be timestamps and occur in the data output
|
|
208
220
|
|
|
209
221
|
"""
|
|
210
|
-
if time is None:
|
|
211
|
-
time = app_clock.time
|
|
212
222
|
self._daq_event.append(DAQEvents(time = time, code = code))
|
|
213
223
|
|
|
214
224
|
|
|
@@ -250,19 +260,19 @@ class DataRecorder(object):
|
|
|
250
260
|
for fsp in self._force_sensor_processes:
|
|
251
261
|
fsp.pause_polling()
|
|
252
262
|
|
|
253
|
-
|
|
263
|
+
wait_ms(500)
|
|
254
264
|
|
|
255
265
|
# get data
|
|
256
266
|
for fsp in self._force_sensor_processes:
|
|
257
267
|
buffer = fsp.get_buffer()
|
|
258
|
-
self.
|
|
268
|
+
self._write_data(buffer, recording_screen)
|
|
259
269
|
data.extend(buffer)
|
|
260
270
|
|
|
261
271
|
# udp event
|
|
262
272
|
data.extend(self.process_and_write_udp_events())
|
|
263
273
|
|
|
264
274
|
# soft trigger
|
|
265
|
-
self.
|
|
275
|
+
self._write_data(self._daq_event)
|
|
266
276
|
data.extend(self._daq_event)
|
|
267
277
|
self._daq_event = []
|
|
268
278
|
return data
|
|
@@ -288,12 +298,11 @@ class DataRecorder(object):
|
|
|
288
298
|
for x in self._force_sensor_processes:
|
|
289
299
|
x.event_bias_is_available.wait()
|
|
290
300
|
|
|
291
|
-
def open_data_file(self,
|
|
301
|
+
def open_data_file(self,
|
|
302
|
+
filename: str | Path,
|
|
292
303
|
subdirectory: str = "data",
|
|
293
|
-
time_stamp_filename: bool = False,
|
|
294
304
|
varnames: bool = True,
|
|
295
|
-
comment_line: str = ""
|
|
296
|
-
zipped: bool = False) -> Path:
|
|
305
|
+
comment_line: str = "") -> Path:
|
|
297
306
|
"""Create a data file
|
|
298
307
|
|
|
299
308
|
Only if data file has been opened, data will be saved!
|
|
@@ -311,9 +320,6 @@ class DataRecorder(object):
|
|
|
311
320
|
write variable names in first line of data output
|
|
312
321
|
comment_line : string, optional
|
|
313
322
|
add some comments at the beginning of the data output file
|
|
314
|
-
zippers : boolean, optional
|
|
315
|
-
are the data zipped or not. Note: Saving zipped data after pause
|
|
316
|
-
takes much longer.
|
|
317
323
|
|
|
318
324
|
Returns
|
|
319
325
|
-------
|
|
@@ -321,68 +327,57 @@ class DataRecorder(object):
|
|
|
321
327
|
full path the actually used file (incl. timestamp)
|
|
322
328
|
|
|
323
329
|
"""
|
|
324
|
-
data_dir = Path.cwd() / subdirectory
|
|
325
|
-
data_dir.mkdir(exist_ok=True)
|
|
326
330
|
self.close_data_file()
|
|
327
331
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if
|
|
332
|
-
|
|
332
|
+
# create filename
|
|
333
|
+
data_dir = Path.cwd() / subdirectory
|
|
334
|
+
data_dir.mkdir(exist_ok=True)
|
|
335
|
+
if self.recording_settings.zip_data:
|
|
336
|
+
filename = Path(filename).with_suffix(".csv.gz")
|
|
333
337
|
else:
|
|
334
|
-
|
|
338
|
+
filename = Path(filename).with_suffix(".csv")
|
|
335
339
|
|
|
336
|
-
cnt = 0
|
|
337
340
|
while True:
|
|
338
|
-
|
|
339
|
-
if
|
|
340
|
-
x = flname.find(".")
|
|
341
|
-
if x<0:
|
|
342
|
-
x = len(flname)
|
|
343
|
-
flname = flname[:x] + "_{0}".format(cnt) + flname[x:]
|
|
344
|
-
|
|
345
|
-
if time_stamp_filename:
|
|
346
|
-
self.filename = flname + "_" + \
|
|
347
|
-
strftime("%Y%m%d%H%M", localtime()) + suffix
|
|
348
|
-
else:
|
|
349
|
-
self.filename = flname + suffix
|
|
350
|
-
|
|
351
|
-
full_path_file = data_dir / self.filename
|
|
352
|
-
if full_path_file.is_file():
|
|
341
|
+
self.path_open_file = data_dir / filename
|
|
342
|
+
if self.path_open_file.is_file():
|
|
353
343
|
# print "data file already exists, adding counter"
|
|
354
|
-
|
|
344
|
+
filename = Path(filename.stem + "_" + strftime("%m%d%H%M", localtime()) + \
|
|
345
|
+
filename.suffix)
|
|
355
346
|
else:
|
|
356
347
|
break
|
|
357
348
|
|
|
358
|
-
if
|
|
359
|
-
self._file = gzip.open(
|
|
349
|
+
if self.recording_settings.zip_data:
|
|
350
|
+
self._file = gzip.open(self.path_open_file, 'w')
|
|
360
351
|
else:
|
|
361
|
-
self._file = open(
|
|
362
|
-
print("Data file: {}".format(
|
|
352
|
+
self._file = open(self.path_open_file, 'w')
|
|
353
|
+
print("Data file: {}".format(self.path_open_file))
|
|
363
354
|
|
|
364
355
|
self._file_write(TAG_COMMENTS + "Recorded at {0} with pyForceDAQ {1}\n".format(
|
|
365
356
|
asctime(localtime()), forceDAQVersion))
|
|
366
|
-
logging.info("new file: {}".format(
|
|
357
|
+
logging.info("new file: {}".format(self.path_open_file))
|
|
367
358
|
|
|
368
359
|
for s in self.sensor_settings_list:
|
|
369
|
-
txt = " Sensor:
|
|
370
|
-
s.sensor_name, s.calibration_file)
|
|
360
|
+
txt = f" Sensor: label={s.device_label}, cal-file={s.calibration_file}\n"
|
|
371
361
|
self._file_write(TAG_COMMENTS + txt)
|
|
372
362
|
|
|
373
363
|
if len(comment_line)>0:
|
|
374
364
|
self._file_write(TAG_COMMENTS + comment_line + "\n")
|
|
365
|
+
|
|
375
366
|
if varnames:
|
|
367
|
+
write_forces = self.recording_settings.array_write_forces()
|
|
368
|
+
write_trigger = self.recording_settings.array_write_trigger()
|
|
369
|
+
write_deviceid = len(self.recording_settings.device_labels)>1
|
|
376
370
|
line = "time,delay,"
|
|
377
|
-
if
|
|
371
|
+
if write_deviceid:
|
|
372
|
+
line += "device_tag,"
|
|
378
373
|
for x in range(6):
|
|
379
|
-
if
|
|
374
|
+
if write_forces[x]:
|
|
380
375
|
line += ForceSensorData.forces_names[x] + ","
|
|
381
|
-
if
|
|
382
|
-
if
|
|
376
|
+
if write_trigger[0]: line += "trigger1,"
|
|
377
|
+
if write_trigger[1]: line += "trigger2,"
|
|
383
378
|
self._file_write(line[:-1] + NEWLINE)
|
|
384
379
|
|
|
385
|
-
return
|
|
380
|
+
return self.path_open_file
|
|
386
381
|
|
|
387
382
|
def close_data_file(self) -> None:
|
|
388
383
|
"""Close the data file
|
|
@@ -394,3 +389,4 @@ class DataRecorder(object):
|
|
|
394
389
|
if self._file is not None:
|
|
395
390
|
self._file.close()
|
|
396
391
|
self._file = None
|
|
392
|
+
self.path_open_file = Path("")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pylsl import (
|
|
2
|
+
StreamInfo,
|
|
3
|
+
StreamOutlet,
|
|
4
|
+
cf_double64,
|
|
5
|
+
cf_float32,
|
|
6
|
+
cf_int8,
|
|
7
|
+
cf_int16,
|
|
8
|
+
cf_int32,
|
|
9
|
+
cf_int64,
|
|
10
|
+
cf_string,
|
|
11
|
+
cf_undefined,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LSLSream():
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.outlet = None
|
|
19
|
+
self._is_init = False
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def is_init(self):
|
|
23
|
+
return self._is_init
|
|
24
|
+
|
|
25
|
+
def init(self,
|
|
26
|
+
name: str,
|
|
27
|
+
n_channels: int,
|
|
28
|
+
stream_id: str,
|
|
29
|
+
freq: int,
|
|
30
|
+
channel_format: int,
|
|
31
|
+
metadata: dict | None = None,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialise a LSL stream
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
name: name of the stream
|
|
38
|
+
n_channels: number of channels per sample
|
|
39
|
+
channel_format: format/type of each channel (ex: string, int, ...)
|
|
40
|
+
same format for each channel
|
|
41
|
+
stream_id: unique identifier of the stream
|
|
42
|
+
content_type: content type of stream. By convention LSL uses the content
|
|
43
|
+
types defined in the XDF file format specification where
|
|
44
|
+
applicable
|
|
45
|
+
freq: sampling rate in Hz
|
|
46
|
+
|
|
47
|
+
Return:
|
|
48
|
+
outlet: StreamOulet to push samples with LSL
|
|
49
|
+
"""
|
|
50
|
+
if self._is_init:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
info = StreamInfo(name, "force",
|
|
54
|
+
channel_count=n_channels,
|
|
55
|
+
nominal_srate=freq,
|
|
56
|
+
channel_format=channel_format,
|
|
57
|
+
source_id=stream_id)
|
|
58
|
+
|
|
59
|
+
# Check if there is metadata to add to the lsl stream
|
|
60
|
+
if metadata:
|
|
61
|
+
# Get xml object of the stream created earlier
|
|
62
|
+
xml_info = info.desc()
|
|
63
|
+
# Add meta data to xml object
|
|
64
|
+
for key, data in metadata.items():
|
|
65
|
+
xml_info.append_child_value(key, str(data))
|
|
66
|
+
|
|
67
|
+
self._is_init = True
|
|
68
|
+
self.outlet = StreamOutlet(info)
|
|
69
|
+
|
|
70
|
+
def push_sample(self, sample: list):
|
|
71
|
+
"""Push a sample to the LSL stream if it is initialized."""
|
|
72
|
+
if not self._is_init:
|
|
73
|
+
# Don't do anything
|
|
74
|
+
return
|
|
75
|
+
self.outlet.push_sample(sample) # type: ignore
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from .timer import get_time_ms
|
|
1
|
+
from .clock import local_clock_ms
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
def N2g(N):
|
|
@@ -9,9 +7,9 @@ def N2g(N):
|
|
|
9
7
|
|
|
10
8
|
class MinMaxDetector(object):
|
|
11
9
|
|
|
12
|
-
def __init__(self, start_value,
|
|
10
|
+
def __init__(self, start_value, duration_ms):
|
|
13
11
|
self._minmax = [start_value, start_value]
|
|
14
|
-
self.
|
|
12
|
+
self._duration_ms = duration_ms
|
|
15
13
|
self._level_change_time = None
|
|
16
14
|
|
|
17
15
|
def process(self, value):
|
|
@@ -19,7 +17,7 @@ class MinMaxDetector(object):
|
|
|
19
17
|
level change has occurred, otherwise None"""
|
|
20
18
|
|
|
21
19
|
if self._level_change_time is not None:
|
|
22
|
-
if (
|
|
20
|
+
if (local_clock_ms() - self._level_change_time) >= self._duration_ms:
|
|
23
21
|
return tuple(self._minmax)
|
|
24
22
|
|
|
25
23
|
if value > self._minmax[1]:
|
|
@@ -28,7 +26,7 @@ class MinMaxDetector(object):
|
|
|
28
26
|
self._minmax[0] = value
|
|
29
27
|
|
|
30
28
|
elif self._minmax[0] != value: # level change just occurred
|
|
31
|
-
self._level_change_time =
|
|
29
|
+
self._level_change_time = local_clock_ms()
|
|
32
30
|
return self.process(value)
|
|
33
31
|
|
|
34
32
|
return None
|
|
@@ -37,12 +35,12 @@ class MinMaxDetector(object):
|
|
|
37
35
|
def is_sampling_for_minmax(self):
|
|
38
36
|
"""true true if currently sampling for minmax"""
|
|
39
37
|
return (self._level_change_time is not None) and \
|
|
40
|
-
(
|
|
38
|
+
(local_clock_ms() - self._level_change_time) < self._duration_ms
|
|
41
39
|
|
|
42
|
-
# def find_calibration_file(calibration_folder: str,
|
|
40
|
+
# def find_calibration_file(calibration_folder: str, device_label: str,
|
|
43
41
|
# calibration_suffix=".cal") -> str:
|
|
44
42
|
|
|
45
|
-
# needle = 'Serial="{0}"'.format(
|
|
43
|
+
# needle = 'Serial="{0}"'.format(device_label)
|
|
46
44
|
# calibration_files = []
|
|
47
45
|
# for x in listdir(path.abspath(calibration_folder)):
|
|
48
46
|
# filename = path.join(calibration_folder, x)
|
|
@@ -51,18 +49,18 @@ class MinMaxDetector(object):
|
|
|
51
49
|
# for l in fl:
|
|
52
50
|
# if l.find(needle)>0:
|
|
53
51
|
# print("Found calibration file for sensor '{0}' : {1}.".format(
|
|
54
|
-
#
|
|
52
|
+
# device_label, filename))
|
|
55
53
|
# calibration_files.append(filename)
|
|
56
54
|
|
|
57
55
|
# if len(calibration_files) == 1:
|
|
58
56
|
# return calibration_files[0]
|
|
59
57
|
# elif len(calibration_files) > 1:
|
|
60
|
-
# print("Multiple calibration files found for sensor '{0}'".format(
|
|
58
|
+
# print("Multiple calibration files found for sensor '{0}'".format(device_label))
|
|
61
59
|
# for f in calibration_files:
|
|
62
60
|
# print(" - {0}".format(f))
|
|
63
61
|
# print("Please ensure that only one calibration file exists for each sensor")
|
|
64
62
|
# else:
|
|
65
|
-
# print("No calibration file found for sensor '{0}'.".format(
|
|
63
|
+
# print("No calibration file found for sensor '{0}'.".format(device_label))
|
|
66
64
|
# exit()
|
|
67
65
|
|
|
68
66
|
#Sensor History with moving average filtering and distance, velocity
|