pyForceDAQ 2.0.0.dev0__tar.gz → 2.0.1__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.0.dev0 → pyforcedaq-2.0.1}/PKG-INFO +2 -2
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/pyproject.toml +2 -2
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/__init__.py +1 -1
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/__main__.py +10 -3
- {pyforcedaq-2.0.0.dev0/src/pyforcedaq/force → pyforcedaq-2.0.1/src/pyforcedaq/_lib}/_log.py +2 -1
- pyforcedaq-2.0.1/src/pyforcedaq/_lib/clock.py +40 -0
- {pyforcedaq-2.0.0.dev0/src/pyforcedaq/force → pyforcedaq-2.0.1/src/pyforcedaq/_lib}/data_recorder.py +41 -46
- pyforcedaq-2.0.1/src/pyforcedaq/_lib/lsl.py +75 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/_lib/misc.py +32 -23
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/_lib/polling_time_profile.py +10 -8
- {pyforcedaq-2.0.0.dev0/src/pyforcedaq/force → pyforcedaq-2.0.1/src/pyforcedaq/_lib}/sensor.py +28 -34
- {pyforcedaq-2.0.0.dev0/src/pyforcedaq/force → pyforcedaq-2.0.1/src/pyforcedaq/_lib}/sensor_process.py +27 -19
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/_lib/types.py +34 -22
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/_lib/udp_connection.py +13 -19
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/daq/__init__.py +5 -4
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/daq/_mock_sensor.py +7 -7
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/daq/_pyATIDAQ.py +1 -3
- pyforcedaq-2.0.1/src/pyforcedaq/force.py +10 -0
- pyforcedaq-2.0.1/src/pyforcedaq/gui/__init__.py +6 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_gui_status.py +1 -1
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_run.py +48 -44
- pyforcedaq-2.0.1/src/pyforcedaq/gui/_settings.py +101 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/launcher.py +24 -23
- pyforcedaq-2.0.0.dev0/src/pyforcedaq/_lib/lsl.py +0 -56
- pyforcedaq-2.0.0.dev0/src/pyforcedaq/_lib/timer.py +0 -45
- pyforcedaq-2.0.0.dev0/src/pyforcedaq/force/__init__.py +0 -13
- pyforcedaq-2.0.0.dev0/src/pyforcedaq/gui/__init__.py +0 -6
- pyforcedaq-2.0.0.dev0/src/pyforcedaq/gui/_settings.py +0 -98
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/_lib/process_priority_manager.py +0 -0
- /pyforcedaq-2.0.0.dev0/src/pyforcedaq/daq/_daq_read_analog_nidaqmx.py → /pyforcedaq-2.0.1/src/pyforcedaq/daq/_use_nidaqmx.py +0 -0
- /pyforcedaq-2.0.0.dev0/src/pyforcedaq/daq/_daq_read_Analog_pydaqmx.py → /pyforcedaq-2.0.1/src/pyforcedaq/daq/_use_pydaqmx.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/daq/config.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/extras/__init__.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/extras/convert.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/extras/expyriment_daq_control.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/extras/opensesame_daq_control.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/extras/read_force_data.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/extras/remote_control.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_layout.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_level_indicator.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_pg_surface.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_plotter.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/src/pyforcedaq/gui/_scaling.py +0 -0
- {pyforcedaq-2.0.0.dev0 → pyforcedaq-2.0.1}/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.1
|
|
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.1"
|
|
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
|
]
|
|
@@ -24,9 +24,16 @@ 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()
|
|
@@ -34,9 +41,9 @@ def cli():
|
|
|
34
41
|
from .gui import launcher
|
|
35
42
|
return launcher.run()
|
|
36
43
|
else:
|
|
37
|
-
gui.
|
|
44
|
+
gui.run_settings(args.SETTINGS_FILE)
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
if __name__ == "__main__": # required because of threading
|
|
42
|
-
cli()
|
|
49
|
+
cli()
|
|
@@ -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
|
{pyforcedaq-2.0.0.dev0/src/pyforcedaq/force → pyforcedaq-2.0.1/src/pyforcedaq/_lib}/data_recorder.py
RENAMED
|
@@ -7,16 +7,16 @@ __author__ = "Oliver Lindemann"
|
|
|
7
7
|
import atexit
|
|
8
8
|
import gzip
|
|
9
9
|
import logging
|
|
10
|
-
import
|
|
11
|
-
import sys
|
|
10
|
+
from pathlib import Path
|
|
12
11
|
from time import asctime, localtime, strftime
|
|
13
12
|
|
|
14
|
-
from pyforcedaq._lib import timer
|
|
15
|
-
|
|
16
13
|
from .. import __version__ as forceDAQVersion
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
14
|
+
from . import _log
|
|
15
|
+
from .clock import wait_ms
|
|
16
|
+
from .process_priority_manager import ProcessPriorityManager
|
|
17
|
+
from .sensor import SensorSettings
|
|
18
|
+
from .sensor_process import SensorProcess
|
|
19
|
+
from .types import (
|
|
20
20
|
TAG_COMMENTS,
|
|
21
21
|
TAG_DAQEVENT,
|
|
22
22
|
TAG_UDPDATA,
|
|
@@ -25,10 +25,10 @@ from .._lib.types import (
|
|
|
25
25
|
PollingPriority,
|
|
26
26
|
UDPData,
|
|
27
27
|
)
|
|
28
|
-
from
|
|
29
|
-
from
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
from .types import GUIRemoteControlCommands as RemoteCmd
|
|
29
|
+
from .udp_connection import UDPConnectionProcess
|
|
30
|
+
|
|
31
|
+
_log.set_logging(data_directory="data", log_file="recording.log")
|
|
32
32
|
|
|
33
33
|
NEWLINE = "\n"
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ class DataRecorder(object):
|
|
|
38
38
|
def __init__(self, force_sensor_settings:SensorSettings | list,
|
|
39
39
|
poll_udp_connection=False,
|
|
40
40
|
write_deviceid = False,
|
|
41
|
-
polling_priority=None):
|
|
41
|
+
polling_priority:str | None = None):
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
"""queue_data will be saved
|
|
@@ -82,18 +82,18 @@ class DataRecorder(object):
|
|
|
82
82
|
self._proc_manager.add_subprocess(self.udp)
|
|
83
83
|
self._proc_manager.add_subprocess(self._force_sensor_processes)
|
|
84
84
|
if polling_priority is not None:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
level = PollingPriority.NORMAL
|
|
86
|
+
else:
|
|
87
|
+
level = PollingPriority.get_priority(polling_priority)
|
|
88
|
+
self._proc_manager.set_subprocess_priorities(level=level, disable_gc=False)
|
|
88
89
|
|
|
89
|
-
logging.info("Main process priority:
|
|
90
|
-
self._proc_manager.get_main_priority()))
|
|
90
|
+
logging.info("Main process priority: %s", self._proc_manager.get_main_priority())
|
|
91
91
|
#logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
|
|
92
92
|
|
|
93
93
|
self._is_recording = False
|
|
94
94
|
self._file = None
|
|
95
95
|
self._daq_event = []
|
|
96
|
-
self.filename = None
|
|
96
|
+
self.filename: str | None = None
|
|
97
97
|
atexit.register(self.quit)
|
|
98
98
|
|
|
99
99
|
|
|
@@ -102,7 +102,7 @@ class DataRecorder(object):
|
|
|
102
102
|
"""Property indicates whether the recording processes are alive"""
|
|
103
103
|
try:
|
|
104
104
|
return self._force_sensor_processes[0].is_alive()
|
|
105
|
-
except:
|
|
105
|
+
except Exception:
|
|
106
106
|
return False
|
|
107
107
|
|
|
108
108
|
@property
|
|
@@ -119,7 +119,7 @@ class DataRecorder(object):
|
|
|
119
119
|
return list(map(lambda x:x.sensor_settings,
|
|
120
120
|
self._force_sensor_processes))
|
|
121
121
|
|
|
122
|
-
def quit(self):
|
|
122
|
+
def quit(self) -> list | None:
|
|
123
123
|
"""Stop all recording processes, close data file and quit recording
|
|
124
124
|
|
|
125
125
|
Notes
|
|
@@ -145,7 +145,7 @@ class DataRecorder(object):
|
|
|
145
145
|
|
|
146
146
|
return buffer
|
|
147
147
|
|
|
148
|
-
def process_and_write_udp_events(self):
|
|
148
|
+
def process_and_write_udp_events(self) -> list:
|
|
149
149
|
"""process udp events and return them"""
|
|
150
150
|
buffer = []
|
|
151
151
|
while True:
|
|
@@ -159,9 +159,9 @@ class DataRecorder(object):
|
|
|
159
159
|
self._save_data(buffer)
|
|
160
160
|
return buffer
|
|
161
161
|
|
|
162
|
-
def _save_data(self, data_buffer,
|
|
162
|
+
def _save_data(self, data_buffer: list,
|
|
163
163
|
recording_screen=None,
|
|
164
|
-
float_decimal_places=4):
|
|
164
|
+
float_decimal_places: int = 4) -> None:
|
|
165
165
|
""" writes data to disk and set counters
|
|
166
166
|
|
|
167
167
|
ignores UDP remote control commands
|
|
@@ -199,21 +199,19 @@ class DataRecorder(object):
|
|
|
199
199
|
"Writing {0} of {1} blocks".format(c//BLOCKSIZE,
|
|
200
200
|
buffer_len//BLOCKSIZE)).present()
|
|
201
201
|
|
|
202
|
-
def _file_write(self, str):
|
|
203
|
-
self._file.write(
|
|
202
|
+
def _file_write(self, s: str) -> None:
|
|
203
|
+
self._file.write(s.encode())
|
|
204
204
|
|
|
205
|
-
def save_daq_event(self, code, time=None):
|
|
205
|
+
def save_daq_event(self, code: str | int | float, time: float | None = None) -> None:
|
|
206
206
|
"""Set marker code in file
|
|
207
207
|
|
|
208
208
|
DAQEvent will be timestamps and occur in the data output
|
|
209
209
|
|
|
210
210
|
"""
|
|
211
|
-
if time is None:
|
|
212
|
-
time = app_clock.time
|
|
213
211
|
self._daq_event.append(DAQEvents(time = time, code = code))
|
|
214
212
|
|
|
215
213
|
|
|
216
|
-
def start_recording(self, determine_bias=False):
|
|
214
|
+
def start_recording(self, determine_bias: bool = False) -> None:
|
|
217
215
|
"""Start polling process and record
|
|
218
216
|
|
|
219
217
|
See Also
|
|
@@ -233,7 +231,7 @@ class DataRecorder(object):
|
|
|
233
231
|
list(map(lambda x:x.start_polling(), self._force_sensor_processes))
|
|
234
232
|
self._is_recording = True
|
|
235
233
|
|
|
236
|
-
def pause_recording(self, recording_screen=None):
|
|
234
|
+
def pause_recording(self, recording_screen=None) -> list:
|
|
237
235
|
"""Pauses all polling processes and process data
|
|
238
236
|
|
|
239
237
|
returns
|
|
@@ -251,7 +249,7 @@ class DataRecorder(object):
|
|
|
251
249
|
for fsp in self._force_sensor_processes:
|
|
252
250
|
fsp.pause_polling()
|
|
253
251
|
|
|
254
|
-
|
|
252
|
+
wait_ms(500)
|
|
255
253
|
|
|
256
254
|
# get data
|
|
257
255
|
for fsp in self._force_sensor_processes:
|
|
@@ -268,7 +266,7 @@ class DataRecorder(object):
|
|
|
268
266
|
self._daq_event = []
|
|
269
267
|
return data
|
|
270
268
|
|
|
271
|
-
def determine_biases(self, n_samples):
|
|
269
|
+
def determine_biases(self, n_samples: int) -> None:
|
|
272
270
|
"""Record n data samples (n_samples) to determine bias.
|
|
273
271
|
Afterwards recording is in pause mode
|
|
274
272
|
|
|
@@ -289,12 +287,12 @@ class DataRecorder(object):
|
|
|
289
287
|
for x in self._force_sensor_processes:
|
|
290
288
|
x.event_bias_is_available.wait()
|
|
291
289
|
|
|
292
|
-
def open_data_file(self, filename,
|
|
293
|
-
subdirectory="data",
|
|
294
|
-
time_stamp_filename=False,
|
|
295
|
-
varnames = True,
|
|
296
|
-
comment_line="",
|
|
297
|
-
zipped=False):
|
|
290
|
+
def open_data_file(self, filename: str,
|
|
291
|
+
subdirectory: str = "data",
|
|
292
|
+
time_stamp_filename: bool = False,
|
|
293
|
+
varnames: bool = True,
|
|
294
|
+
comment_line: str = "",
|
|
295
|
+
zipped: bool = False) -> Path:
|
|
298
296
|
"""Create a data file
|
|
299
297
|
|
|
300
298
|
Only if data file has been opened, data will be saved!
|
|
@@ -322,11 +320,8 @@ class DataRecorder(object):
|
|
|
322
320
|
full path the actually used file (incl. timestamp)
|
|
323
321
|
|
|
324
322
|
"""
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
data_dir = os.path.join(base_dir, subdirectory)
|
|
328
|
-
if not os.path.isdir(data_dir):
|
|
329
|
-
os.mkdir(data_dir)
|
|
323
|
+
data_dir = Path.cwd() / subdirectory
|
|
324
|
+
data_dir.mkdir(exist_ok=True)
|
|
330
325
|
self.close_data_file()
|
|
331
326
|
|
|
332
327
|
if filename is None or len(filename) == 0:
|
|
@@ -352,8 +347,8 @@ class DataRecorder(object):
|
|
|
352
347
|
else:
|
|
353
348
|
self.filename = flname + suffix
|
|
354
349
|
|
|
355
|
-
full_path_file =
|
|
356
|
-
if
|
|
350
|
+
full_path_file = data_dir / self.filename
|
|
351
|
+
if full_path_file.is_file():
|
|
357
352
|
# print "data file already exists, adding counter"
|
|
358
353
|
cnt += 1
|
|
359
354
|
else:
|
|
@@ -388,7 +383,7 @@ class DataRecorder(object):
|
|
|
388
383
|
|
|
389
384
|
return full_path_file
|
|
390
385
|
|
|
391
|
-
def close_data_file(self):
|
|
386
|
+
def close_data_file(self) -> None:
|
|
392
387
|
"""Close the data file
|
|
393
388
|
|
|
394
389
|
Afterwards data will not be saved anymore.
|
|
@@ -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,22 +35,33 @@ 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
|
-
(
|
|
41
|
-
|
|
42
|
-
def find_calibration_file(calibration_folder, sensor_name,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
(local_clock_ms() - self._level_change_time) < self._duration_ms
|
|
39
|
+
|
|
40
|
+
# def find_calibration_file(calibration_folder: str, sensor_name: str,
|
|
41
|
+
# calibration_suffix=".cal") -> str:
|
|
42
|
+
|
|
43
|
+
# needle = 'Serial="{0}"'.format(sensor_name)
|
|
44
|
+
# calibration_files = []
|
|
45
|
+
# for x in listdir(path.abspath(calibration_folder)):
|
|
46
|
+
# filename = path.join(calibration_folder, x)
|
|
47
|
+
# if path.isfile(filename) and filename.endswith(calibration_suffix):
|
|
48
|
+
# with open(filename, "r") as fl:
|
|
49
|
+
# for l in fl:
|
|
50
|
+
# if l.find(needle)>0:
|
|
51
|
+
# print("Found calibration file for sensor '{0}' : {1}.".format(
|
|
52
|
+
# sensor_name, filename))
|
|
53
|
+
# calibration_files.append(filename)
|
|
54
|
+
|
|
55
|
+
# if len(calibration_files) == 1:
|
|
56
|
+
# return calibration_files[0]
|
|
57
|
+
# elif len(calibration_files) > 1:
|
|
58
|
+
# print("Multiple calibration files found for sensor '{0}'".format(sensor_name))
|
|
59
|
+
# for f in calibration_files:
|
|
60
|
+
# print(" - {0}".format(f))
|
|
61
|
+
# print("Please ensure that only one calibration file exists for each sensor")
|
|
62
|
+
# else:
|
|
63
|
+
# print("No calibration file found for sensor '{0}'.".format(sensor_name))
|
|
64
|
+
# exit()
|
|
56
65
|
|
|
57
66
|
#Sensor History with moving average filtering and distance, velocity
|
|
58
67
|
class SensorHistory(object):
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
|
|
3
|
-
from .
|
|
3
|
+
from .clock import local_clock
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class PollingTimeProfile(object):
|
|
7
7
|
|
|
8
|
-
def __init__(self,
|
|
8
|
+
def __init__(self, timing_range_ms=10):
|
|
9
9
|
self._last = None
|
|
10
|
-
self.
|
|
10
|
+
self._timing_range_ms = 10
|
|
11
11
|
self._zero_cnt = 0
|
|
12
12
|
|
|
13
13
|
#self._zero_time_polling_frequency = {}
|
|
14
|
-
self.profile_frequency = np.array([0] * (
|
|
14
|
+
self.profile_frequency = np.array([0] * (timing_range_ms + 1))
|
|
15
15
|
|
|
16
16
|
def stop(self):
|
|
17
17
|
self._last = None
|
|
18
18
|
|
|
19
|
-
def update(self,
|
|
19
|
+
def update(self, time: float):
|
|
20
|
+
|
|
21
|
+
time_ms = int(time * 1000)
|
|
20
22
|
if self._last is not None:
|
|
21
23
|
d = time_ms - self._last
|
|
22
|
-
if d > self.
|
|
23
|
-
d = self.
|
|
24
|
+
if d > self._timing_range_ms:
|
|
25
|
+
d = self._timing_range_ms
|
|
24
26
|
self.profile_frequency[d] += 1
|
|
25
27
|
|
|
26
28
|
#if d == 0:
|
|
@@ -35,7 +37,7 @@ class PollingTimeProfile(object):
|
|
|
35
37
|
self._last = time_ms
|
|
36
38
|
|
|
37
39
|
def tick(self):
|
|
38
|
-
self.update(
|
|
40
|
+
self.update(local_clock())
|
|
39
41
|
|
|
40
42
|
@property
|
|
41
43
|
def profile_percent(self):
|