pyForceDAQ 2.0.1.dev2__tar.gz → 2.0.2__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}/PKG-INFO +2 -2
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/pyproject.toml +2 -2
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/__init__.py +2 -9
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/__main__.py +16 -6
- {pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2/src/pyforcedaq/_lib}/_log.py +2 -1
- pyforcedaq-2.0.2/src/pyforcedaq/_lib/clock.py +38 -0
- pyforcedaq-2.0.2/src/pyforcedaq/_lib/constants.py +9 -0
- {pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2/src/pyforcedaq/_lib}/data_recorder.py +88 -89
- pyforcedaq-2.0.2/src/pyforcedaq/_lib/lsl.py +75 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/_lib/misc.py +18 -12
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/_lib/polling_time_profile.py +10 -8
- pyforcedaq-2.0.2/src/pyforcedaq/_lib/sensor.py +97 -0
- {pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2/src/pyforcedaq/_lib}/sensor_process.py +60 -43
- pyforcedaq-2.0.2/src/pyforcedaq/_lib/settings.py +209 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/_lib/types.py +55 -91
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/_lib/udp_connection.py +13 -19
- pyforcedaq-2.0.2/src/pyforcedaq/daq/__init__.py +29 -0
- pyforcedaq-2.0.2/src/pyforcedaq/daq/_calibration_dll.py +21 -0
- pyforcedaq-2.0.2/src/pyforcedaq/daq/_calibration_iaftt.py +17 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/daq/_mock_sensor.py +8 -7
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/daq/_use_nidaqmx.py +7 -5
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/daq/_use_pydaqmx.py +4 -3
- pyforcedaq-2.0.2/src/pyforcedaq/force.py +11 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/__init__.py +2 -2
- pyforcedaq-2.0.2/src/pyforcedaq/gui/_gui_status.py +222 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/_layout.py +5 -8
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/_run.py +66 -173
- pyforcedaq-2.0.2/src/pyforcedaq/launcher.py +136 -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/__init__.py +0 -20
- 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}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/_lib/process_priority_manager.py +0 -0
- /pyforcedaq-2.0.1.dev2/src/pyforcedaq/daq/_pyATIDAQ.py → /pyforcedaq-2.0.2/src/pyforcedaq/daq/pyATIDAQ.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/extras/__init__.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/extras/convert.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/extras/read_force_data.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/_level_indicator.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/_pg_surface.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/_plotter.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/src/pyforcedaq/gui/_scaling.py +0 -0
- {pyforcedaq-2.0.1.dev2 → pyforcedaq-2.0.2}/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
|
|
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"
|
|
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
|
|
|
@@ -35,8 +30,6 @@ __author__ = "Oliver Lindemann"
|
|
|
35
30
|
|
|
36
31
|
import sys as _sys
|
|
37
32
|
|
|
38
|
-
USE_MOCK_SENSOR = False # <-- change for usage in lab to False
|
|
39
|
-
|
|
40
33
|
if _sys.version_info[0] != 3 or _sys.version_info[1]<12:
|
|
41
34
|
raise RuntimeError("pyForceDAQ {0} ".format(__version__) +
|
|
42
35
|
"is not compatible with Python {0}.{1}. ".format(
|
|
@@ -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():
|
|
@@ -24,17 +23,28 @@ def cli():
|
|
|
24
23
|
help="Run with launcher GUI to edit settings and start recording",
|
|
25
24
|
)
|
|
26
25
|
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-o", "--omit-launcher",
|
|
28
|
+
action="store_true",
|
|
29
|
+
default=False,
|
|
30
|
+
help="Omit launcher GUI and start recording directly",
|
|
31
|
+
)
|
|
32
|
+
print("+" + "-" * 23 + "+")
|
|
33
|
+
print(f"| pyforceDAQ {__version__}".ljust(24) + "|")
|
|
34
|
+
print("+" + "-" * 23 + "+")
|
|
35
|
+
|
|
27
36
|
args = parser.parse_args()
|
|
28
37
|
|
|
29
|
-
if args.
|
|
38
|
+
if not args.omit_launcher:
|
|
30
39
|
if len(args.SETTINGS_FILE) > 0:
|
|
31
40
|
print("Can't use launcher and settings file")
|
|
32
41
|
exit()
|
|
33
42
|
|
|
34
|
-
from .
|
|
35
|
-
return
|
|
43
|
+
from .launcher import run_launcher
|
|
44
|
+
return run_launcher()
|
|
36
45
|
else:
|
|
37
|
-
gui
|
|
46
|
+
from .gui import run_settings_file
|
|
47
|
+
run_settings_file(args.SETTINGS_FILE)
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""A high-resolution monotonic timer based on LSL's local_clock() function.
|
|
2
|
+
"""
|
|
3
|
+
from time import sleep
|
|
4
|
+
|
|
5
|
+
from pylsl import local_clock
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def local_clock_ms():
|
|
9
|
+
"""Returns the current time in milliseconds, based on LSL's local_clock()"""
|
|
10
|
+
return local_clock() * 1000
|
|
11
|
+
|
|
12
|
+
def wait_ms(waiting_time: int | float, looptime : int = 200) -> None:
|
|
13
|
+
"""Wait for a certain amount of milliseconds.
|
|
14
|
+
"""
|
|
15
|
+
start = local_clock_ms()
|
|
16
|
+
if waiting_time > looptime:
|
|
17
|
+
sleep((waiting_time - looptime) / 1000)
|
|
18
|
+
while local_clock_ms() < start + waiting_time:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class StopWatch(object):#
|
|
22
|
+
"""A simple timer"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._init_time = local_clock()
|
|
26
|
+
|
|
27
|
+
def reset_stopwatch(self):
|
|
28
|
+
"""Reset the stopwatch time to zero.
|
|
29
|
+
"""
|
|
30
|
+
self._init_time = local_clock()
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def time(self) -> float:
|
|
34
|
+
return local_clock() - self._init_time
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def time_ms(self) -> float:
|
|
38
|
+
return self.time * 1000
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
USE_MOCK_SENSOR = False # <-- change for usage in lab to False
|
|
2
|
+
USE_PYDAQMX = False # <-- change to True to use pyDAQmx instead of nidaqmx, requires pyDAQmx installation
|
|
3
|
+
USE_ATI_DLL = False # <-- change to True to use ATI DLL for calibration conversion, otherwise use atiiaftt
|
|
4
|
+
|
|
5
|
+
SETTINGS_FILE_EXTENSION = ".settings.toml"
|
|
6
|
+
DEFAULT_SETTINGS_FILE = "pyForceDAQ" + SETTINGS_FILE_EXTENSION
|
|
7
|
+
DEFAULT_OUTPUT_FILENAME = None
|
|
8
|
+
CALIBRATION_FOLDER = "calibration"
|
|
9
|
+
DATA_FOLDER = "data"
|
{pyforcedaq-2.0.1.dev2/src/pyforcedaq/force → pyforcedaq-2.0.2/src/pyforcedaq/_lib}/data_recorder.py
RENAMED
|
@@ -7,15 +7,18 @@ __author__ = "Oliver Lindemann"
|
|
|
7
7
|
import atexit
|
|
8
8
|
import gzip
|
|
9
9
|
import logging
|
|
10
|
+
from io import TextIOWrapper
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from time import asctime, localtime, strftime
|
|
12
|
-
|
|
13
|
-
from pyforcedaq._lib import timer
|
|
13
|
+
from typing import List
|
|
14
14
|
|
|
15
15
|
from .. import __version__ as forceDAQVersion
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
16
|
+
from . import _log
|
|
17
|
+
from .clock import wait_ms
|
|
18
|
+
from .process_priority_manager import ProcessPriorityManager
|
|
19
|
+
from .sensor_process import SensorProcess
|
|
20
|
+
from .settings import RecordingSettings, SensorSettings
|
|
21
|
+
from .types import (
|
|
19
22
|
TAG_COMMENTS,
|
|
20
23
|
TAG_DAQEVENT,
|
|
21
24
|
TAG_UDPDATA,
|
|
@@ -24,20 +27,19 @@ from .._lib.types import (
|
|
|
24
27
|
PollingPriority,
|
|
25
28
|
UDPData,
|
|
26
29
|
)
|
|
27
|
-
from
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
from .sensor_process import SensorProcess
|
|
30
|
+
from .udp_connection import UDPConnectionProcess
|
|
31
|
+
|
|
32
|
+
_log.set_logging(data_directory="data", log_file="recording.log")
|
|
31
33
|
|
|
32
34
|
NEWLINE = "\n"
|
|
33
35
|
|
|
34
36
|
class DataRecorder(object):
|
|
35
37
|
"""handles multiple sensors and udp connection"""
|
|
36
38
|
|
|
37
|
-
def __init__(self,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
def __init__(self,
|
|
40
|
+
recording_settings: RecordingSettings,
|
|
41
|
+
force_sensor_settings:SensorSettings | List[SensorSettings],
|
|
42
|
+
poll_udp_connection: bool = False):
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
"""queue_data will be saved
|
|
@@ -47,13 +49,10 @@ class DataRecorder(object):
|
|
|
47
49
|
{REALTIME} or {NORMAL} or None
|
|
48
50
|
"""
|
|
49
51
|
|
|
50
|
-
self._write_deviceid = write_deviceid
|
|
51
|
-
|
|
52
52
|
if not isinstance(force_sensor_settings, list):
|
|
53
53
|
force_sensor_settings = [force_sensor_settings]
|
|
54
|
-
|
|
55
|
-
self.
|
|
56
|
-
self._write_trigger = force_sensor_settings[0].array_write_trigger()
|
|
54
|
+
|
|
55
|
+
self.recording_settings = recording_settings
|
|
57
56
|
|
|
58
57
|
# create sensor processes
|
|
59
58
|
self._force_sensor_processes =[]
|
|
@@ -62,7 +61,8 @@ class DataRecorder(object):
|
|
|
62
61
|
if not isinstance(fs, SensorSettings):
|
|
63
62
|
raise RuntimeError("Recorder needs a list of Force Sensor Settings!")
|
|
64
63
|
else:
|
|
65
|
-
fst = SensorProcess(
|
|
64
|
+
fst = SensorProcess(sensor_settings = fs,
|
|
65
|
+
recording_settings=recording_settings,
|
|
66
66
|
pipe_buffered_data_after_pause=True)
|
|
67
67
|
fst.start()
|
|
68
68
|
event_trigger.append(fst.event_trigger)
|
|
@@ -70,20 +70,19 @@ class DataRecorder(object):
|
|
|
70
70
|
|
|
71
71
|
# create udp connection process
|
|
72
72
|
if poll_udp_connection:
|
|
73
|
-
self.udp = UDPConnectionProcess(event_trigger=event_trigger
|
|
74
|
-
event_ignore_tag = RemoteCmd.COMMAND_STR)
|
|
73
|
+
self.udp = UDPConnectionProcess(event_trigger=event_trigger)
|
|
75
74
|
self.udp.start()
|
|
76
75
|
else:
|
|
77
76
|
self.udp = None
|
|
78
77
|
|
|
79
|
-
# process managing
|
|
78
|
+
# process managing FIYME needed?
|
|
80
79
|
self._proc_manager = ProcessPriorityManager()
|
|
81
80
|
self._proc_manager.add_subprocess(self.udp)
|
|
82
81
|
self._proc_manager.add_subprocess(self._force_sensor_processes)
|
|
83
|
-
if
|
|
82
|
+
if self.recording_settings.priority is not None:
|
|
84
83
|
level = PollingPriority.NORMAL
|
|
85
84
|
else:
|
|
86
|
-
level = PollingPriority.get_priority(
|
|
85
|
+
level = PollingPriority.get_priority(self.recording_settings.priority)
|
|
87
86
|
self._proc_manager.set_subprocess_priorities(level=level, disable_gc=False)
|
|
88
87
|
|
|
89
88
|
logging.info("Main process priority: %s", self._proc_manager.get_main_priority())
|
|
@@ -92,9 +91,13 @@ class DataRecorder(object):
|
|
|
92
91
|
self._is_recording = False
|
|
93
92
|
self._file = None
|
|
94
93
|
self._daq_event = []
|
|
95
|
-
self.
|
|
94
|
+
self.path_open_file = Path("")
|
|
96
95
|
atexit.register(self.quit)
|
|
97
96
|
|
|
97
|
+
@property
|
|
98
|
+
def is_saving_data(self):
|
|
99
|
+
"""Property indicates whether a data file is open"""
|
|
100
|
+
return self._file is not None
|
|
98
101
|
|
|
99
102
|
@property
|
|
100
103
|
def is_alive(self):
|
|
@@ -149,16 +152,16 @@ class DataRecorder(object):
|
|
|
149
152
|
buffer = []
|
|
150
153
|
while True:
|
|
151
154
|
try:
|
|
152
|
-
data = self.udp.receive_queue.get_nowait()
|
|
153
|
-
except:
|
|
155
|
+
data = self.udp.receive_queue.get_nowait() # type: ignore
|
|
156
|
+
except AttributeError:
|
|
154
157
|
# until queue empty or no udp connection
|
|
155
158
|
break
|
|
156
159
|
buffer.append(data)
|
|
157
160
|
if len(buffer)>0:
|
|
158
|
-
self.
|
|
161
|
+
self._write_data(buffer)
|
|
159
162
|
return buffer
|
|
160
163
|
|
|
161
|
-
def
|
|
164
|
+
def _write_data(self, data_buffer: list,
|
|
162
165
|
recording_screen=None,
|
|
163
166
|
float_decimal_places: int = 4) -> None:
|
|
164
167
|
""" writes data to disk and set counters
|
|
@@ -168,18 +171,20 @@ class DataRecorder(object):
|
|
|
168
171
|
#DOC output format
|
|
169
172
|
|
|
170
173
|
BLOCKSIZE = 10000 # for recording screen feedback only
|
|
171
|
-
|
|
174
|
+
write_forces = self.recording_settings.array_write_forces()
|
|
175
|
+
write_trigger = self.recording_settings.array_write_trigger()
|
|
176
|
+
write_deviceid = len(self.recording_settings.device_labels)>1
|
|
172
177
|
float_format = "{0:." + str(float_decimal_places) + "f},"
|
|
173
178
|
buffer_len = len(data_buffer)
|
|
174
179
|
for c, d in enumerate(data_buffer):
|
|
175
180
|
if self._file is not None:
|
|
176
181
|
if isinstance(d, ForceSensorData):
|
|
177
182
|
line = f"{d.time}, {d.acquisition_delay},"
|
|
178
|
-
if
|
|
179
|
-
line += f"{d.
|
|
180
|
-
for x in d.selected_forces(select=
|
|
183
|
+
if write_deviceid:
|
|
184
|
+
line += f"{d.sensor_id},"
|
|
185
|
+
for x in d.selected_forces(select=write_forces):
|
|
181
186
|
line += float_format.format(x)
|
|
182
|
-
for x in d.selected_trigger(select=
|
|
187
|
+
for x in d.selected_trigger(select=write_trigger):
|
|
183
188
|
if isinstance(x, int):
|
|
184
189
|
line += f"{x},"
|
|
185
190
|
else:
|
|
@@ -190,25 +195,27 @@ class DataRecorder(object):
|
|
|
190
195
|
self._file_write(f"{TAG_DAQEVENT},{d.time},{str(d.code)}" + NEWLINE)
|
|
191
196
|
|
|
192
197
|
elif isinstance(d, UDPData):
|
|
193
|
-
|
|
194
|
-
self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
|
|
198
|
+
self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
|
|
195
199
|
|
|
196
200
|
if recording_screen is not None and c % BLOCKSIZE == 0:
|
|
197
201
|
recording_screen.stimulus(
|
|
198
|
-
"
|
|
202
|
+
"Saving {0} of {1} blocks".format(c//BLOCKSIZE,
|
|
199
203
|
buffer_len//BLOCKSIZE)).present()
|
|
200
204
|
|
|
201
205
|
def _file_write(self, s: str) -> None:
|
|
202
|
-
self._file.write(s.encode())
|
|
203
206
|
|
|
204
|
-
|
|
207
|
+
if isinstance(self._file, gzip.GzipFile):
|
|
208
|
+
self._file.write(s.encode("utf-8"))
|
|
209
|
+
elif isinstance(self._file, TextIOWrapper):
|
|
210
|
+
self._file.write(s)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def store_daq_event(self, code: str | int | float, time: float | None = None) -> None:
|
|
205
214
|
"""Set marker code in file
|
|
206
215
|
|
|
207
216
|
DAQEvent will be timestamps and occur in the data output
|
|
208
217
|
|
|
209
218
|
"""
|
|
210
|
-
if time is None:
|
|
211
|
-
time = app_clock.time
|
|
212
219
|
self._daq_event.append(DAQEvents(time = time, code = code))
|
|
213
220
|
|
|
214
221
|
|
|
@@ -244,25 +251,31 @@ class DataRecorder(object):
|
|
|
244
251
|
|
|
245
252
|
data = []
|
|
246
253
|
if recording_screen is not None:
|
|
247
|
-
recording_screen.stimulus("
|
|
254
|
+
recording_screen.stimulus("").present()
|
|
248
255
|
|
|
249
256
|
#pause polling
|
|
250
257
|
for fsp in self._force_sensor_processes:
|
|
251
258
|
fsp.pause_polling()
|
|
252
259
|
|
|
253
|
-
|
|
260
|
+
wait_ms(500)
|
|
261
|
+
|
|
262
|
+
if recording_screen is not None:
|
|
263
|
+
if self.is_saving_data:
|
|
264
|
+
recording_screen.stimulus("saving data ...").present()
|
|
265
|
+
else:
|
|
266
|
+
recording_screen.stimulus("pause recording").present()
|
|
254
267
|
|
|
255
268
|
# get data
|
|
256
269
|
for fsp in self._force_sensor_processes:
|
|
257
270
|
buffer = fsp.get_buffer()
|
|
258
|
-
self.
|
|
271
|
+
self._write_data(buffer, recording_screen)
|
|
259
272
|
data.extend(buffer)
|
|
260
273
|
|
|
261
274
|
# udp event
|
|
262
275
|
data.extend(self.process_and_write_udp_events())
|
|
263
276
|
|
|
264
277
|
# soft trigger
|
|
265
|
-
self.
|
|
278
|
+
self._write_data(self._daq_event)
|
|
266
279
|
data.extend(self._daq_event)
|
|
267
280
|
self._daq_event = []
|
|
268
281
|
return data
|
|
@@ -288,12 +301,11 @@ class DataRecorder(object):
|
|
|
288
301
|
for x in self._force_sensor_processes:
|
|
289
302
|
x.event_bias_is_available.wait()
|
|
290
303
|
|
|
291
|
-
def open_data_file(self,
|
|
304
|
+
def open_data_file(self,
|
|
305
|
+
filename: str | Path,
|
|
292
306
|
subdirectory: str = "data",
|
|
293
|
-
time_stamp_filename: bool = False,
|
|
294
307
|
varnames: bool = True,
|
|
295
|
-
comment_line: str = ""
|
|
296
|
-
zipped: bool = False) -> Path:
|
|
308
|
+
comment_line: str = "") -> Path:
|
|
297
309
|
"""Create a data file
|
|
298
310
|
|
|
299
311
|
Only if data file has been opened, data will be saved!
|
|
@@ -311,9 +323,6 @@ class DataRecorder(object):
|
|
|
311
323
|
write variable names in first line of data output
|
|
312
324
|
comment_line : string, optional
|
|
313
325
|
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
326
|
|
|
318
327
|
Returns
|
|
319
328
|
-------
|
|
@@ -321,68 +330,57 @@ class DataRecorder(object):
|
|
|
321
330
|
full path the actually used file (incl. timestamp)
|
|
322
331
|
|
|
323
332
|
"""
|
|
324
|
-
data_dir = Path.cwd() / subdirectory
|
|
325
|
-
data_dir.mkdir(exist_ok=True)
|
|
326
333
|
self.close_data_file()
|
|
327
334
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if
|
|
332
|
-
|
|
335
|
+
# create filename
|
|
336
|
+
data_dir = Path.cwd() / subdirectory
|
|
337
|
+
data_dir.mkdir(exist_ok=True)
|
|
338
|
+
if self.recording_settings.zip_data:
|
|
339
|
+
filename = Path(filename).with_suffix(".csv.gz")
|
|
333
340
|
else:
|
|
334
|
-
|
|
341
|
+
filename = Path(filename).with_suffix(".csv")
|
|
335
342
|
|
|
336
|
-
cnt = 0
|
|
337
343
|
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():
|
|
344
|
+
self.path_open_file = data_dir / filename
|
|
345
|
+
if self.path_open_file.is_file():
|
|
353
346
|
# print "data file already exists, adding counter"
|
|
354
|
-
|
|
347
|
+
filename = Path(filename.stem + "_" + strftime("%m%d%H%M", localtime()) + \
|
|
348
|
+
filename.suffix)
|
|
355
349
|
else:
|
|
356
350
|
break
|
|
357
351
|
|
|
358
|
-
if
|
|
359
|
-
self._file = gzip.open(
|
|
352
|
+
if self.recording_settings.zip_data:
|
|
353
|
+
self._file = gzip.open(self.path_open_file, 'w')
|
|
360
354
|
else:
|
|
361
|
-
self._file = open(
|
|
362
|
-
print("Data file: {}".format(
|
|
355
|
+
self._file = open(self.path_open_file, 'w')
|
|
356
|
+
print("Data file: {}".format(self.path_open_file))
|
|
363
357
|
|
|
364
358
|
self._file_write(TAG_COMMENTS + "Recorded at {0} with pyForceDAQ {1}\n".format(
|
|
365
359
|
asctime(localtime()), forceDAQVersion))
|
|
366
|
-
logging.info("new file: {}".format(
|
|
360
|
+
logging.info("new file: {}".format(self.path_open_file))
|
|
367
361
|
|
|
368
362
|
for s in self.sensor_settings_list:
|
|
369
|
-
txt = " Sensor:
|
|
370
|
-
s.sensor_name, s.calibration_file)
|
|
363
|
+
txt = f" Sensor: label={s.device_label}, cal-file={s.calibration_file}\n"
|
|
371
364
|
self._file_write(TAG_COMMENTS + txt)
|
|
372
365
|
|
|
373
366
|
if len(comment_line)>0:
|
|
374
367
|
self._file_write(TAG_COMMENTS + comment_line + "\n")
|
|
368
|
+
|
|
375
369
|
if varnames:
|
|
370
|
+
write_forces = self.recording_settings.array_write_forces()
|
|
371
|
+
write_trigger = self.recording_settings.array_write_trigger()
|
|
372
|
+
write_deviceid = len(self.recording_settings.device_labels)>1
|
|
376
373
|
line = "time,delay,"
|
|
377
|
-
if
|
|
374
|
+
if write_deviceid:
|
|
375
|
+
line += "device_tag,"
|
|
378
376
|
for x in range(6):
|
|
379
|
-
if
|
|
377
|
+
if write_forces[x]:
|
|
380
378
|
line += ForceSensorData.forces_names[x] + ","
|
|
381
|
-
if
|
|
382
|
-
if
|
|
379
|
+
if write_trigger[0]: line += "trigger1,"
|
|
380
|
+
if write_trigger[1]: line += "trigger2,"
|
|
383
381
|
self._file_write(line[:-1] + NEWLINE)
|
|
384
382
|
|
|
385
|
-
return
|
|
383
|
+
return self.path_open_file
|
|
386
384
|
|
|
387
385
|
def close_data_file(self) -> None:
|
|
388
386
|
"""Close the data file
|
|
@@ -394,3 +392,4 @@ class DataRecorder(object):
|
|
|
394
392
|
if self._file is not None:
|
|
395
393
|
self._file.close()
|
|
396
394
|
self._file = None
|
|
395
|
+
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
|