pyForceDAQ 2.0.6__tar.gz → 2.0.7__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.6 → pyforcedaq-2.0.7}/PKG-INFO +1 -1
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/pyproject.toml +1 -1
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/data_recorder.py +23 -44
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/file_writer.py +1 -7
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/lsl.py +9 -5
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/misc.py +33 -60
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/sensor.py +8 -4
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/sensor_process.py +12 -9
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/types.py +12 -35
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/udp_connection.py +2 -23
- pyforcedaq-2.0.7/src/pyforcedaq/gui/__init__.py +7 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_gui_status.py +9 -23
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_run.py +41 -66
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/launcher.py +2 -3
- pyforcedaq-2.0.6/src/pyforcedaq/gui/__init__.py +0 -5
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/__init__.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/__main__.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/__init__.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/clock.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/process_priority_manager.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/settings.py +1 -1
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/constants.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/__init__.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/_pyATIDAQ.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/calibration_dll.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/calibration_iaftt.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/read_daq_mock_sensor.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/read_daq_nidaqmx.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/read_daq_pydaqmx.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/force.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_layout.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_level_indicator.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_pg_surface.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_plotter.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_scaling.py +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
- {pyforcedaq-2.0.6 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/rf_icon.png +0 -0
|
@@ -14,12 +14,12 @@ from typing import List
|
|
|
14
14
|
from .. import __version__ as forceDAQVersion
|
|
15
15
|
from .. import constants
|
|
16
16
|
from .file_writer import FileWriter
|
|
17
|
+
from .lsl import LSLSream, cf_string
|
|
17
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
|
|
21
|
-
from .types import
|
|
22
|
-
from .udp_connection import UDPConnectionProcess
|
|
22
|
+
from .types import ForceSensorData, PollingPriority
|
|
23
23
|
|
|
24
24
|
set_logging(data_directory="data", log_file="recording.log")
|
|
25
25
|
|
|
@@ -27,14 +27,12 @@ set_logging(data_directory="data", log_file="recording.log")
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class DataRecorder(object):
|
|
30
|
-
"""handles multiple sensors and
|
|
30
|
+
"""handles multiple sensors, file writing and process management, LSL stream for events"""
|
|
31
31
|
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
34
34
|
recording_settings: RecordingSettings,
|
|
35
|
-
force_sensor_settings: SensorSettings | List[SensorSettings]
|
|
36
|
-
poll_udp_connection: bool = False,
|
|
37
|
-
):
|
|
35
|
+
force_sensor_settings: SensorSettings | List[SensorSettings]):
|
|
38
36
|
"""queue_data will be saved
|
|
39
37
|
see sensorprocess.__init__
|
|
40
38
|
|
|
@@ -74,17 +72,23 @@ class DataRecorder(object):
|
|
|
74
72
|
fst.start()
|
|
75
73
|
event_trigger.append(fst.event_trigger)
|
|
76
74
|
self.force_sensor_processes.append(fst)
|
|
75
|
+
# LSL stream
|
|
76
|
+
self.lsl_events_stream = LSLSream()
|
|
77
|
+
if self.recording_settings.lsl_stream:
|
|
78
|
+
self.lsl_events_stream.init(
|
|
79
|
+
name="Events_forceDAQ",
|
|
80
|
+
content_type="Marker",
|
|
81
|
+
n_channels=1,
|
|
82
|
+
stream_id="FE",
|
|
83
|
+
freq=0,
|
|
84
|
+
channel_format=cf_string,
|
|
85
|
+
metadata={}
|
|
86
|
+
)
|
|
87
|
+
|
|
77
88
|
|
|
78
|
-
# create udp connection process
|
|
79
|
-
if poll_udp_connection:
|
|
80
|
-
self.udp = UDPConnectionProcess(event_trigger=event_trigger)
|
|
81
|
-
self.udp.start()
|
|
82
|
-
else:
|
|
83
|
-
self.udp = None
|
|
84
89
|
|
|
85
90
|
# process managing FIYME needed?
|
|
86
91
|
self._proc_manager = ProcessPriorityManager()
|
|
87
|
-
self._proc_manager.add_subprocess(self.udp)
|
|
88
92
|
self._proc_manager.add_subprocess(self.force_sensor_processes)
|
|
89
93
|
if self.recording_settings.priority is not None:
|
|
90
94
|
level = PollingPriority.NORMAL
|
|
@@ -96,8 +100,6 @@ class DataRecorder(object):
|
|
|
96
100
|
"Main process priority: %s", self._proc_manager.get_main_priority()
|
|
97
101
|
)
|
|
98
102
|
# logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
|
|
99
|
-
|
|
100
|
-
self._daq_event = [] # FIXME needed?
|
|
101
103
|
atexit.register(self.quit)
|
|
102
104
|
|
|
103
105
|
@property
|
|
@@ -145,37 +147,8 @@ class DataRecorder(object):
|
|
|
145
147
|
fsp.join()
|
|
146
148
|
self.close_data_file()
|
|
147
149
|
|
|
148
|
-
if self.udp is not None:
|
|
149
|
-
self.udp.quit()
|
|
150
|
-
|
|
151
150
|
logging.info("Quit recording")
|
|
152
151
|
|
|
153
|
-
def process_and_write_udp_events(self) -> list:
|
|
154
|
-
"""process udp events and return them"""
|
|
155
|
-
buffer = []
|
|
156
|
-
while True:
|
|
157
|
-
try:
|
|
158
|
-
data = self.udp.receive_queue.get_nowait() # type: ignore
|
|
159
|
-
except AttributeError:
|
|
160
|
-
# until queue empty or no udp connection
|
|
161
|
-
break
|
|
162
|
-
buffer.append(data)
|
|
163
|
-
|
|
164
|
-
if isinstance(self.file_writer, FileWriter):
|
|
165
|
-
for dat in buffer:
|
|
166
|
-
self.file_writer.queue.put(dat)
|
|
167
|
-
return buffer
|
|
168
|
-
|
|
169
|
-
def store_daq_event(
|
|
170
|
-
self, code: str | int | float, time: float | None = None
|
|
171
|
-
) -> None:
|
|
172
|
-
"""Set marker code in file
|
|
173
|
-
|
|
174
|
-
DAQEvent will be timestamps and occur in the data output
|
|
175
|
-
|
|
176
|
-
"""
|
|
177
|
-
self._daq_event.append(DAQEvents(time=time, code=code))
|
|
178
|
-
|
|
179
152
|
def start_saving(self) -> None:
|
|
180
153
|
"""Start polling process and record
|
|
181
154
|
|
|
@@ -188,6 +161,7 @@ class DataRecorder(object):
|
|
|
188
161
|
for fsp in self.force_sensor_processes:
|
|
189
162
|
fsp.flag_sensor_bias_is_determined.wait() # wait is no initial bias is set yet
|
|
190
163
|
fsp.start_saving()
|
|
164
|
+
self.lsl_events_stream.push_sample(["Start saving"])
|
|
191
165
|
|
|
192
166
|
def pause_saving(self):
|
|
193
167
|
"""Pauses all polling processes and process data
|
|
@@ -201,6 +175,7 @@ class DataRecorder(object):
|
|
|
201
175
|
# pause polling
|
|
202
176
|
for fsp in self.force_sensor_processes:
|
|
203
177
|
fsp.pause_saving()
|
|
178
|
+
self.lsl_events_stream.push_sample(["Pause saving"])
|
|
204
179
|
|
|
205
180
|
|
|
206
181
|
def determine_biases(self) -> None:
|
|
@@ -208,6 +183,8 @@ class DataRecorder(object):
|
|
|
208
183
|
x.determine_bias()
|
|
209
184
|
for x in self.force_sensor_processes:
|
|
210
185
|
x.flag_sensor_bias_is_determined.wait()
|
|
186
|
+
self.lsl_events_stream.push_sample(["New Baseline"])
|
|
187
|
+
|
|
211
188
|
|
|
212
189
|
def open_data_file(
|
|
213
190
|
self,
|
|
@@ -303,7 +280,9 @@ class DataRecorder(object):
|
|
|
303
280
|
|
|
304
281
|
"""
|
|
305
282
|
if isinstance(self.file_writer, FileWriter):
|
|
283
|
+
self.pause_saving()
|
|
306
284
|
self.file_writer.close_file()
|
|
307
285
|
if self.file_writer.is_alive():
|
|
308
286
|
self.file_writer.join()
|
|
309
287
|
self.file_writer.filepath = Path("")
|
|
288
|
+
|
|
@@ -4,8 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from queue import Empty
|
|
5
5
|
|
|
6
6
|
from .settings import RecordingSettings
|
|
7
|
-
from .types import
|
|
8
|
-
ForceSensorData, UDPData)
|
|
7
|
+
from .types import TAG_COMMENTS, ForceSensorData
|
|
9
8
|
|
|
10
9
|
NEWLINE = "\n"
|
|
11
10
|
ENCODING = "utf-8"
|
|
@@ -90,11 +89,6 @@ class FileWriter(Process):
|
|
|
90
89
|
txt += float_format.format(x)
|
|
91
90
|
txt = txt[:-1] + NEWLINE
|
|
92
91
|
|
|
93
|
-
elif isinstance(d, DAQEvents):
|
|
94
|
-
txt = f"{TAG_DAQEVENT},{d.time},{str(d.code)}{NEWLINE}"
|
|
95
|
-
|
|
96
|
-
elif isinstance(d, UDPData):
|
|
97
|
-
txt = f"{TAG_UDPDATA},{d.time},{d.unicode}{NEWLINE}"
|
|
98
92
|
elif isinstance(d, str):
|
|
99
93
|
txt = f"{TAG_COMMENTS} {d}"
|
|
100
94
|
else:
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from numpy import typing as npt
|
|
1
4
|
from pylsl import (
|
|
2
5
|
StreamInfo,
|
|
3
6
|
StreamOutlet,
|
|
@@ -24,6 +27,7 @@ class LSLSream:
|
|
|
24
27
|
def init(
|
|
25
28
|
self,
|
|
26
29
|
name: str,
|
|
30
|
+
content_type: str,
|
|
27
31
|
n_channels: int,
|
|
28
32
|
stream_id: str,
|
|
29
33
|
freq: int,
|
|
@@ -35,12 +39,13 @@ class LSLSream:
|
|
|
35
39
|
|
|
36
40
|
Args:
|
|
37
41
|
name: name 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
|
|
38
44
|
n_channels: number of channels per sample
|
|
39
45
|
channel_format: format/type of each channel (ex: string, int, ...)
|
|
40
46
|
same format for each channel
|
|
41
47
|
stream_id: unique identifier of the stream
|
|
42
|
-
|
|
43
|
-
types defined in the XDF file format specification where
|
|
48
|
+
|
|
44
49
|
applicable
|
|
45
50
|
freq: sampling rate in Hz
|
|
46
51
|
|
|
@@ -51,8 +56,7 @@ class LSLSream:
|
|
|
51
56
|
return
|
|
52
57
|
|
|
53
58
|
info = StreamInfo(
|
|
54
|
-
name,
|
|
55
|
-
"force",
|
|
59
|
+
name, content_type,
|
|
56
60
|
channel_count=n_channels,
|
|
57
61
|
nominal_srate=freq,
|
|
58
62
|
channel_format=channel_format,
|
|
@@ -70,7 +74,7 @@ class LSLSream:
|
|
|
70
74
|
self._is_init = True
|
|
71
75
|
self.outlet = StreamOutlet(info)
|
|
72
76
|
|
|
73
|
-
def push_sample(self, sample:
|
|
77
|
+
def push_sample(self, sample: List | npt.NDArray):
|
|
74
78
|
"""Push a sample to the LSL stream if it is initialized."""
|
|
75
79
|
if not self._is_init:
|
|
76
80
|
# Don't do anything
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import socket
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from subprocess import check_output
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy import typing as npt
|
|
5
10
|
|
|
6
11
|
from ..constants import SETTINGS_FILE_EXTENSION
|
|
7
12
|
from .clock import local_clock_ms
|
|
@@ -67,32 +72,26 @@ class MinMaxDetector(object):
|
|
|
67
72
|
local_clock_ms() - self._level_change_time
|
|
68
73
|
) < self._duration_ms
|
|
69
74
|
|
|
75
|
+
def get_lan_ip():
|
|
76
|
+
if os.name == "nt":
|
|
77
|
+
# Windows
|
|
78
|
+
return socket.gethostbyname(socket.gethostname())
|
|
79
|
+
else:
|
|
80
|
+
# Linux and macOS
|
|
81
|
+
try:
|
|
82
|
+
# Try Linux command first
|
|
83
|
+
rtn = check_output(["hostname", "-I"]).decode().strip()
|
|
84
|
+
return rtn.split()[0] if rtn else None
|
|
85
|
+
except:
|
|
86
|
+
try:
|
|
87
|
+
# Fallback to macOS command
|
|
88
|
+
rtn = check_output(["ipconfig", "getifaddr", "en0"]).decode().strip()
|
|
89
|
+
return rtn if rtn else None
|
|
90
|
+
except:
|
|
91
|
+
# Fallback to socket method if both commands fail
|
|
92
|
+
return socket.gethostbyname(socket.gethostname())
|
|
93
|
+
|
|
70
94
|
|
|
71
|
-
# def find_calibration_file(calibration_folder: str, device_label: str,
|
|
72
|
-
# calibration_suffix=".cal") -> str:
|
|
73
|
-
|
|
74
|
-
# needle = 'Serial="{0}"'.format(device_label)
|
|
75
|
-
# calibration_files = []
|
|
76
|
-
# for x in listdir(path.abspath(calibration_folder)):
|
|
77
|
-
# filename = path.join(calibration_folder, x)
|
|
78
|
-
# if path.isfile(filename) and filename.endswith(calibration_suffix):
|
|
79
|
-
# with open(filename, "r") as fl:
|
|
80
|
-
# for l in fl:
|
|
81
|
-
# if l.find(needle)>0:
|
|
82
|
-
# print("Found calibration file for sensor '{0}' : {1}.".format(
|
|
83
|
-
# device_label, filename))
|
|
84
|
-
# calibration_files.append(filename)
|
|
85
|
-
|
|
86
|
-
# if len(calibration_files) == 1:
|
|
87
|
-
# return calibration_files[0]
|
|
88
|
-
# elif len(calibration_files) > 1:
|
|
89
|
-
# print("Multiple calibration files found for sensor '{0}'".format(device_label))
|
|
90
|
-
# for f in calibration_files:
|
|
91
|
-
# print(" - {0}".format(f))
|
|
92
|
-
# print("Please ensure that only one calibration file exists for each sensor")
|
|
93
|
-
# else:
|
|
94
|
-
# print("No calibration file found for sensor '{0}'.".format(device_label))
|
|
95
|
-
# exit()
|
|
96
95
|
|
|
97
96
|
|
|
98
97
|
# Sensor History with moving average filtering and distance, velocity
|
|
@@ -105,10 +104,7 @@ class SensorHistory(object):
|
|
|
105
104
|
"""
|
|
106
105
|
|
|
107
106
|
def __init__(self, history_size, number_of_parameter):
|
|
108
|
-
self.history = [
|
|
109
|
-
self.moving_average = [0] * number_of_parameter
|
|
110
|
-
self._correction_cnt = 0
|
|
111
|
-
self._previous_moving_average = self.moving_average
|
|
107
|
+
self.history = [np.zeros(number_of_parameter, dtype=np.float64) for _ in range(history_size)]
|
|
112
108
|
|
|
113
109
|
def __str__(self):
|
|
114
110
|
return str(self.history)
|
|
@@ -124,35 +120,16 @@ class SensorHistory(object):
|
|
|
124
120
|
|
|
125
121
|
"""
|
|
126
122
|
|
|
127
|
-
self.
|
|
128
|
-
pop = self.history.pop(0)
|
|
123
|
+
self.history.pop(0)
|
|
129
124
|
self.history.append(values)
|
|
130
|
-
# pop first element and calc moving average
|
|
131
|
-
if self._correction_cnt > 10000:
|
|
132
|
-
self._correction_cnt = 0
|
|
133
|
-
self.moving_average = self.calc_history_average()
|
|
134
|
-
else:
|
|
135
|
-
self._correction_cnt += 1
|
|
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
|
-
)
|
|
142
|
-
|
|
143
|
-
def calc_history_average(self):
|
|
144
|
-
"""Calculate history averages for all sensor parameter.
|
|
145
|
-
|
|
146
|
-
The method is more time consuming than calling the property
|
|
147
|
-
`moving_average`. It is does however not suffer from accumulated
|
|
148
|
-
rounding-errors such as moving average.
|
|
149
125
|
|
|
150
|
-
|
|
126
|
+
def moving_averages(self) -> npt.NDArray[np.floating]:
|
|
127
|
+
"""Returns a list of moving averages for all sensor parameters."""
|
|
128
|
+
return np.mean(self.history, axis=0)
|
|
151
129
|
|
|
152
|
-
|
|
153
|
-
for
|
|
154
|
-
|
|
155
|
-
return list(map(lambda x: x / len(self.history), s))
|
|
130
|
+
def moving_average(self, sensor:int) -> np.floating:
|
|
131
|
+
"""Returns the moving average for a specific sensor parameter."""
|
|
132
|
+
return np.mean([x[sensor] for x in self.history])
|
|
156
133
|
|
|
157
134
|
@property
|
|
158
135
|
def history_size(self):
|
|
@@ -160,8 +137,4 @@ class SensorHistory(object):
|
|
|
160
137
|
|
|
161
138
|
@property
|
|
162
139
|
def number_of_parameter(self):
|
|
163
|
-
return len(self.history[0])
|
|
164
|
-
|
|
165
|
-
@property
|
|
166
|
-
def previous_moving_average(self):
|
|
167
|
-
return self._previous_moving_average
|
|
140
|
+
return len(self.history[0])
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
"""class to
|
|
1
|
+
"""Sensor class for reading data from NI devices and converting to force data.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Per default the NIDAQMX library is installed and access the NI instruments data.
|
|
4
|
+
If the PyDAQMX library is installed, this library is used instead.
|
|
5
|
+
|
|
6
|
+
For conversion to force data, the ATI dll will be used (use_aiftt=True, DEFAULT). Alternatively, you
|
|
7
|
+
might use your own complied dll and pyforceDAQ own interface to the DLL (use_aiftt=False)
|
|
4
8
|
"""
|
|
5
9
|
|
|
6
10
|
__author__ = "Oliver Lindemann"
|
|
@@ -22,7 +26,7 @@ class Sensor(object):
|
|
|
22
26
|
|
|
23
27
|
def __init__(self, s_settings: SensorSettings,
|
|
24
28
|
daq_type: int,
|
|
25
|
-
use_aiftt: bool):
|
|
29
|
+
use_aiftt: bool=True):
|
|
26
30
|
"""DOC"""
|
|
27
31
|
|
|
28
32
|
assert isinstance(s_settings, SensorSettings)
|
|
@@ -72,7 +76,7 @@ class Sensor(object):
|
|
|
72
76
|
"""sets the bias"""
|
|
73
77
|
|
|
74
78
|
if np.shape(data)[1] != len(Sensor.SENSOR_CHANNELS):
|
|
75
|
-
raise ValueError(
|
|
79
|
+
raise ValueError("Bias data should have the shape (x, n_sensor_channels)")
|
|
76
80
|
|
|
77
81
|
if self._calib_converter is not None:
|
|
78
82
|
self._calib_converter.bias(np.mean(data, axis=0))
|
|
@@ -26,7 +26,7 @@ class SensorProcess(Process):
|
|
|
26
26
|
recording_settings: RecordingSettings,
|
|
27
27
|
file_writer_queue: Optional[Queue],
|
|
28
28
|
daq_type: int,
|
|
29
|
-
use_aiftt: bool
|
|
29
|
+
use_aiftt: bool = True
|
|
30
30
|
):
|
|
31
31
|
"""ForceSensorProcess
|
|
32
32
|
|
|
@@ -62,7 +62,8 @@ class SensorProcess(Process):
|
|
|
62
62
|
self._np_dat = np.frombuffer(
|
|
63
63
|
self._dat.get_obj(), dtype=np.float64
|
|
64
64
|
) # numpy view
|
|
65
|
-
self.
|
|
65
|
+
self._saved_sample_cnt = Value(ct.c_int64, 0)
|
|
66
|
+
self._total_sample_cnt = Value(ct.c_int64, 0)
|
|
66
67
|
self.flag_sensor_bias_is_determined = Event()
|
|
67
68
|
self._flag_quit_request = Event()
|
|
68
69
|
self.__flag_is_saving = Event()
|
|
@@ -107,7 +108,10 @@ class SensorProcess(Process):
|
|
|
107
108
|
return self._np_dat[3:6]
|
|
108
109
|
|
|
109
110
|
def get_saved_sample_cnt(self) -> int:
|
|
110
|
-
return self.
|
|
111
|
+
return self._saved_sample_cnt.value
|
|
112
|
+
|
|
113
|
+
def get_total_sample_cnt(self) -> int:
|
|
114
|
+
return self._total_sample_cnt.value
|
|
111
115
|
|
|
112
116
|
def determine_bias(self):
|
|
113
117
|
self.flag_sensor_bias_is_determined.clear()
|
|
@@ -144,6 +148,7 @@ class SensorProcess(Process):
|
|
|
144
148
|
if self.recording_settings.lsl_stream:
|
|
145
149
|
lsl_data_steam.init(
|
|
146
150
|
name=f"Force_{sensor.device_label}",
|
|
151
|
+
content_type="force",
|
|
147
152
|
n_channels=sum(stream_forces),
|
|
148
153
|
stream_id=f"RF_{sensor.device_label}",
|
|
149
154
|
freq=self.sensor_settings.rate,
|
|
@@ -155,6 +160,7 @@ class SensorProcess(Process):
|
|
|
155
160
|
if n_hardware_trigger > 0:
|
|
156
161
|
lsl_hardware_trigger_stream.init(
|
|
157
162
|
name=f"Trigger_{sensor.device_label}",
|
|
163
|
+
content_type="Marker",
|
|
158
164
|
n_channels=n_hardware_trigger,
|
|
159
165
|
stream_id=f"Tr_{sensor.device_label}",
|
|
160
166
|
channel_format=cf_float32,
|
|
@@ -192,31 +198,28 @@ class SensorProcess(Process):
|
|
|
192
198
|
continue
|
|
193
199
|
|
|
194
200
|
## LSL
|
|
195
|
-
|
|
196
|
-
# stream only select forces
|
|
197
|
-
lsl_data_steam.outlet.push_sample(d.forces[stream_forces]) # type: ignore
|
|
201
|
+
lsl_data_steam.push_sample(d.forces[stream_forces])
|
|
198
202
|
if lsl_hardware_trigger_stream.is_init:
|
|
199
203
|
tr = d.trigger[stream_trigger]
|
|
200
204
|
if any(tr): # only stream if at least one trigger is active
|
|
201
205
|
lsl_hardware_trigger_stream.outlet.push_sample(tr) # type: ignore
|
|
202
206
|
|
|
203
207
|
# write to shared memory and file writer queue
|
|
208
|
+
self._total_sample_cnt.value += 1 # type: ignore
|
|
204
209
|
self._dat[:] = d.forces
|
|
205
210
|
fifo.append(d.forces) # for bias determination
|
|
206
211
|
|
|
207
212
|
if self.is_saving() and self._file_writer_queue is not None:
|
|
208
213
|
self._file_writer_queue.put(d)
|
|
209
|
-
self.
|
|
214
|
+
self._saved_sample_cnt.value += 1 # type: ignore
|
|
210
215
|
|
|
211
216
|
if not self.flag_sensor_bias_is_determined.is_set():
|
|
212
217
|
# new baseline requested
|
|
213
218
|
sensor.set_bias(np.array(fifo))
|
|
214
219
|
self.flag_sensor_bias_is_determined.set()
|
|
215
|
-
# FIXME determine bias marker event?
|
|
216
220
|
|
|
217
221
|
# stop process
|
|
218
222
|
self.pause_saving()
|
|
219
223
|
sensor.daq.stop_data_acquisition()
|
|
220
224
|
logging.info("Sensor quit, %s", sensor.device_label)
|
|
221
225
|
|
|
222
|
-
# FIXME check trigger processing and UDP connections
|
|
@@ -10,8 +10,6 @@ from .misc import MinMaxDetector as _MinMaxDetector
|
|
|
10
10
|
|
|
11
11
|
# tag in data output
|
|
12
12
|
TAG_COMMENTS = "#"
|
|
13
|
-
TAG_DAQEVENT = TAG_COMMENTS + "T"
|
|
14
|
-
TAG_UDPDATA = TAG_COMMENTS + "UDP"
|
|
15
13
|
|
|
16
14
|
CTYPE_FORCES = ct.c_double * 600
|
|
17
15
|
CTYPE_TRIGGER = ct.c_double * 2
|
|
@@ -181,6 +179,13 @@ class ForceSensorData(TimedData):
|
|
|
181
179
|
self.forces = struct.forces
|
|
182
180
|
self.trigger = struct.trigger
|
|
183
181
|
|
|
182
|
+
@classmethod
|
|
183
|
+
def force_id(cls, force_label) -> float | None:
|
|
184
|
+
"""returns the id of the force parameter with the given label or None if not found"""
|
|
185
|
+
try:
|
|
186
|
+
return cls.forces_names.index(force_label)
|
|
187
|
+
except ValueError:
|
|
188
|
+
return None
|
|
184
189
|
|
|
185
190
|
class UDPData(TimedData):
|
|
186
191
|
"""The UDP data class, used to store UDP DATA with timestamps"""
|
|
@@ -212,28 +217,6 @@ def bytes_startswith(a, b):
|
|
|
212
217
|
return a[: len(b)] == b
|
|
213
218
|
|
|
214
219
|
|
|
215
|
-
class DAQEvents(TimedData):
|
|
216
|
-
"""The DAQEvents data class, used to store trigger
|
|
217
|
-
|
|
218
|
-
See Also
|
|
219
|
-
--------
|
|
220
|
-
DataRecorder.set_daq_event()
|
|
221
|
-
|
|
222
|
-
"""
|
|
223
|
-
|
|
224
|
-
def __init__(self, time: float | None, code: str | int | float):
|
|
225
|
-
"""Create a DAQEvents object
|
|
226
|
-
|
|
227
|
-
Parameters
|
|
228
|
-
----------
|
|
229
|
-
time : float
|
|
230
|
-
code : numerical or string
|
|
231
|
-
|
|
232
|
-
"""
|
|
233
|
-
super().__init__(time)
|
|
234
|
-
self.code = code
|
|
235
|
-
|
|
236
|
-
|
|
237
220
|
class Thresholds(object):
|
|
238
221
|
def __init__(self, thresholds, n_channels=1):
|
|
239
222
|
"""Thresholds for a one or multiple channels of data"""
|
|
@@ -268,7 +251,7 @@ class Thresholds(object):
|
|
|
268
251
|
def thresholds(self):
|
|
269
252
|
return self._thresholds
|
|
270
253
|
|
|
271
|
-
def get_level(self, value):
|
|
254
|
+
def get_level(self, value) -> int:
|
|
272
255
|
"""return [int]
|
|
273
256
|
int: the level of current sensor value depending of thresholds (array)
|
|
274
257
|
|
|
@@ -298,20 +281,14 @@ class Thresholds(object):
|
|
|
298
281
|
self._minmax[channel] = None
|
|
299
282
|
return self._prev_level[channel]
|
|
300
283
|
|
|
301
|
-
def get_level_change(self, value, channel=0):
|
|
284
|
+
def get_level_change(self, value, channel=0) -> tuple[bool, int] | tuple[None, None]:
|
|
302
285
|
"""return tuple with level_change (boolean) and current level (int)
|
|
303
|
-
if level change detection is switch on
|
|
304
|
-
|
|
305
|
-
Note: after detected level change detection is switched off!
|
|
306
286
|
"""
|
|
307
287
|
|
|
308
|
-
if self._prev_level[channel] is None:
|
|
309
|
-
return None, None
|
|
310
|
-
|
|
311
288
|
current = self.get_level(value)
|
|
312
289
|
changed = current != self._prev_level[channel]
|
|
313
290
|
if changed:
|
|
314
|
-
self._prev_level[channel] =
|
|
291
|
+
self._prev_level[channel] = current
|
|
315
292
|
return changed, current
|
|
316
293
|
|
|
317
294
|
def __str__(self):
|
|
@@ -333,7 +310,7 @@ class Thresholds(object):
|
|
|
333
310
|
self._prev_level[channel] = None
|
|
334
311
|
return lv
|
|
335
312
|
|
|
336
|
-
def get_response_minmax(self, value, channel=0):
|
|
313
|
+
def get_response_minmax(self, value, channel=0) -> tuple[int, int] | tuple[None, None]:
|
|
337
314
|
"""checks for response minimum and maximum if set_response_minmax_detection is switch on
|
|
338
315
|
With this function you add a sample and check if the response can be classified. If so,
|
|
339
316
|
it returns a tuple with the minimum and maximum response level during the response period
|
|
@@ -346,7 +323,7 @@ class Thresholds(object):
|
|
|
346
323
|
"""
|
|
347
324
|
|
|
348
325
|
if self._minmax[channel] is None:
|
|
349
|
-
return None
|
|
326
|
+
return None, None
|
|
350
327
|
|
|
351
328
|
rtn = self._minmax[channel].process(self.get_level(value))
|
|
352
329
|
if rtn is not None:
|
|
@@ -5,36 +5,15 @@ __version__ = "0.5"
|
|
|
5
5
|
|
|
6
6
|
import atexit
|
|
7
7
|
import logging
|
|
8
|
-
import os
|
|
9
8
|
import socket
|
|
10
9
|
from multiprocessing import Event, Process, Queue
|
|
11
|
-
from subprocess import check_output
|
|
12
10
|
|
|
13
11
|
from .clock import local_clock, wait_ms
|
|
12
|
+
from .misc import get_lan_ip
|
|
14
13
|
from .process_priority_manager import get_priority
|
|
15
14
|
from .types import UDPData
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
def get_lan_ip():
|
|
19
|
-
if os.name == "nt":
|
|
20
|
-
# Windows
|
|
21
|
-
return socket.gethostbyname(socket.gethostname())
|
|
22
|
-
else:
|
|
23
|
-
# Linux and macOS
|
|
24
|
-
try:
|
|
25
|
-
# Try Linux command first
|
|
26
|
-
rtn = check_output(["hostname", "-I"]).decode().strip()
|
|
27
|
-
return rtn.split()[0] if rtn else None
|
|
28
|
-
except:
|
|
29
|
-
try:
|
|
30
|
-
# Fallback to macOS command
|
|
31
|
-
rtn = check_output(["ipconfig", "getifaddr", "en0"]).decode().strip()
|
|
32
|
-
return rtn if rtn else None
|
|
33
|
-
except:
|
|
34
|
-
# Fallback to socket method if both commands fail
|
|
35
|
-
return socket.gethostbyname(socket.gethostname())
|
|
36
|
-
|
|
37
|
-
|
|
38
17
|
class UDPConnection(object):
|
|
39
18
|
# DOC document the usage "connecting" "unconnecting"
|
|
40
19
|
COMMAND_CHAR = b"$"
|
|
@@ -196,7 +175,7 @@ class UDPConnectionProcess(Process):
|
|
|
196
175
|
# Server that prints each input and echos it to the client
|
|
197
176
|
# that is currently connected
|
|
198
177
|
|
|
199
|
-
from udp_connection import UDPConnectionProcess, Queue
|
|
178
|
+
from pyforcedaq._lib.udp_connection import UDPConnectionProcess, Queue
|
|
200
179
|
|
|
201
180
|
receive_queue = Queue()
|
|
202
181
|
udp_p = UDPConnectionProcess(receive_queue=receive_queue)
|
|
@@ -4,6 +4,7 @@ __author__ = "Oliver Lindemann"
|
|
|
4
4
|
from time import sleep
|
|
5
5
|
from typing import List, Tuple
|
|
6
6
|
|
|
7
|
+
import numpy as np
|
|
7
8
|
from expyriment import io, misc
|
|
8
9
|
|
|
9
10
|
from .._lib.data_recorder import DataRecorder
|
|
@@ -56,7 +57,9 @@ class GUIStatus(object):
|
|
|
56
57
|
|
|
57
58
|
self.sensor_processes = recorder.force_sensor_processes
|
|
58
59
|
self.n_sensors = len(self.sensor_processes)
|
|
59
|
-
self.
|
|
60
|
+
self.force_id_level_detect = ForceSensorData.force_id(gui_settings.level_detection_parameter)
|
|
61
|
+
|
|
62
|
+
self.history: List[SensorHistory] = []
|
|
60
63
|
for _ in range(self.n_sensors):
|
|
61
64
|
self.history.append(
|
|
62
65
|
SensorHistory(
|
|
@@ -69,9 +72,7 @@ class GUIStatus(object):
|
|
|
69
72
|
self.clear_screen = True
|
|
70
73
|
self.thresholds = None
|
|
71
74
|
self.set_marker = False
|
|
72
|
-
self.last_udp_data = None
|
|
73
75
|
self._last_processed_smpl = [0] * self.n_sensors
|
|
74
|
-
self._last_thresholds = None
|
|
75
76
|
self._clock = misc.Clock()
|
|
76
77
|
|
|
77
78
|
self.sensor_info_str = ""
|
|
@@ -159,7 +160,7 @@ class GUIStatus(object):
|
|
|
159
160
|
"""returns list of sensors with new samples"""
|
|
160
161
|
rtn = []
|
|
161
162
|
for i, cnt in enumerate(
|
|
162
|
-
map(SensorProcess.
|
|
163
|
+
map(SensorProcess.get_total_sample_cnt, self.sensor_processes)
|
|
163
164
|
):
|
|
164
165
|
if self._last_processed_smpl[i] < cnt:
|
|
165
166
|
# new sample
|
|
@@ -167,14 +168,6 @@ class GUIStatus(object):
|
|
|
167
168
|
rtn.append(i)
|
|
168
169
|
return rtn
|
|
169
170
|
|
|
170
|
-
def check_thresholds_changed(self):
|
|
171
|
-
"""returns only true if not changed between calls"""
|
|
172
|
-
if self.thresholds != self._last_thresholds:
|
|
173
|
-
# new sample
|
|
174
|
-
self._last_thresholds = self.thresholds
|
|
175
|
-
return True
|
|
176
|
-
return False
|
|
177
|
-
|
|
178
171
|
def process_key(self, key):
|
|
179
172
|
if key == misc.constants.K_q or key == misc.constants.K_ESCAPE:
|
|
180
173
|
self.quit_recording = True
|
|
@@ -229,19 +222,12 @@ class GUIStatus(object):
|
|
|
229
222
|
else:
|
|
230
223
|
self.thresholds = None
|
|
231
224
|
|
|
232
|
-
def
|
|
233
|
-
"""remote control"""
|
|
234
|
-
self.set_marker = True
|
|
235
|
-
self.last_udp_data = udp_event.byte_string
|
|
236
|
-
|
|
237
|
-
def update_history(self, sensor):
|
|
225
|
+
def update_history(self, sensor:int):
|
|
238
226
|
self.history[sensor].update(self.sensor_processes[sensor].get_Fxyz())
|
|
239
227
|
|
|
240
|
-
def
|
|
228
|
+
def get_average_level_detection_parameter(self, sensor:int) -> np.floating | None:
|
|
241
229
|
"""just a short cut"""
|
|
242
|
-
if sensor < self.n_sensors:
|
|
243
|
-
return self.history[sensor].moving_average
|
|
244
|
-
self.gs.level_detection_parameter
|
|
245
|
-
]
|
|
230
|
+
if sensor < self.n_sensors and isinstance(self.force_id_level_detect, int):
|
|
231
|
+
return self.history[sensor].moving_average(self.force_id_level_detect)
|
|
246
232
|
else:
|
|
247
233
|
return None
|
|
@@ -7,7 +7,6 @@ __author__ = "Oliver Lindemann"
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from pickle import dumps
|
|
11
10
|
from time import sleep
|
|
12
11
|
from typing import List
|
|
13
12
|
|
|
@@ -20,25 +19,21 @@ from .._lib.clock import wait_ms
|
|
|
20
19
|
from .._lib.data_recorder import DataRecorder
|
|
21
20
|
from .._lib.sensor_process import SensorProcess
|
|
22
21
|
from .._lib.settings import AppSettings, GUISettings, SensorSettings
|
|
22
|
+
from .._lib.types import ForceSensorData
|
|
23
23
|
from ..constants import DEFAULT_OUTPUT_FILENAME
|
|
24
24
|
from ._gui_status import GUIStatus
|
|
25
25
|
from ._layout import colours, get_pygame_rect, logo_text_line, make_text_line
|
|
26
26
|
from ._level_indicator import level_indicator
|
|
27
27
|
from ._plotter import PlotterThread
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
RESPONSE_MINMAX =
|
|
32
|
-
RESPONSE_MINMAX2 =
|
|
33
|
-
CHANGED_LEVEL =
|
|
34
|
-
CHANGED_LEVEL2 =
|
|
29
|
+
# Feedback
|
|
30
|
+
|
|
31
|
+
RESPONSE_MINMAX = "RM"
|
|
32
|
+
RESPONSE_MINMAX2 = "RM2"
|
|
33
|
+
CHANGED_LEVEL = "CL"
|
|
34
|
+
CHANGED_LEVEL2 = "CL2"
|
|
35
35
|
|
|
36
36
|
def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[str]):
|
|
37
|
-
"""udp command:
|
|
38
|
-
"start", "pause", "stop"
|
|
39
|
-
"thresholds = [x,...]" : start level detection for Fz parameter and set threshold
|
|
40
|
-
"thresholds stop" : stop level detection
|
|
41
|
-
"""
|
|
42
37
|
|
|
43
38
|
indicator_grid = 70 # distance between indicator center
|
|
44
39
|
plotter_width = 900
|
|
@@ -52,8 +47,10 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
52
47
|
exp.keyboard.clear()
|
|
53
48
|
|
|
54
49
|
last_recording_status = None
|
|
55
|
-
|
|
50
|
+
last_thresholds = None
|
|
51
|
+
recorder.lsl_events_stream.push_sample(["Recording started, " + forceDAQVersion])
|
|
56
52
|
s.background.stimulus().present()
|
|
53
|
+
|
|
57
54
|
while not s.quit_recording: ######## process loop
|
|
58
55
|
if s.pause_recording:
|
|
59
56
|
wait_ms(100)
|
|
@@ -61,35 +58,32 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
61
58
|
################################ process keyboard
|
|
62
59
|
s.process_key(exp.keyboard.check(check_for_control_keys=False))
|
|
63
60
|
|
|
64
|
-
############################## process udp
|
|
65
|
-
udp = recorder.process_and_write_udp_events()
|
|
66
|
-
while len(udp) > 0:
|
|
67
|
-
s.process_udp_event(udp.pop(0))
|
|
68
|
-
|
|
69
61
|
########################### process new samples
|
|
70
62
|
for x in s.check_new_samples():
|
|
71
63
|
s.update_history(sensor=x)
|
|
72
64
|
|
|
73
|
-
if s.thresholds is not None:
|
|
65
|
+
if s.thresholds is not None and isinstance(s.force_id_level_detect, int):
|
|
74
66
|
# level change detection
|
|
75
67
|
level_change, tmp = s.thresholds.get_level_change(
|
|
76
|
-
s.history[x].moving_average
|
|
68
|
+
s.history[x].moving_average(s.force_id_level_detect), channel=x
|
|
77
69
|
)
|
|
78
70
|
if level_change:
|
|
79
71
|
if x == 1:
|
|
80
|
-
|
|
81
|
-
else:
|
|
82
|
-
recorder.udp.send_queue.put(CHANGED_LEVEL + dumps(tmp)) # type: ignore
|
|
83
|
-
|
|
84
|
-
# minmax detection
|
|
85
|
-
tmp = s.thresholds.get_response_minmax(
|
|
86
|
-
s.history[x].moving_average[gs.level_detection_parameter], channel=x
|
|
87
|
-
)
|
|
88
|
-
if tmp is not None:
|
|
89
|
-
if x == 1:
|
|
90
|
-
recorder.udp.send_queue.put(RESPONSE_MINMAX2 + dumps(tmp)) # type: ignore
|
|
72
|
+
resp = f"{CHANGED_LEVEL}-{tmp}"
|
|
91
73
|
else:
|
|
92
|
-
|
|
74
|
+
resp = f"{CHANGED_LEVEL2}-{tmp}"
|
|
75
|
+
recorder.lsl_events_stream.push_sample([resp])
|
|
76
|
+
|
|
77
|
+
## minmax detection FIXME needs to call first "set_response_minmax_detection"
|
|
78
|
+
# tmp = s.thresholds.get_response_minmax(
|
|
79
|
+
# s.history[x].moving_average(s.force_id_level_detect), channel=x
|
|
80
|
+
# )
|
|
81
|
+
# if tmp[0] is not None:
|
|
82
|
+
# if x == 1:
|
|
83
|
+
# resp = f"{RESPONSE_MINMAX}-{tmp}"
|
|
84
|
+
# else:
|
|
85
|
+
# resp = f"{RESPONSE_MINMAX2}-{tmp}"
|
|
86
|
+
# recorder.lsl_events_stream.push_sample([resp])
|
|
93
87
|
|
|
94
88
|
######################## show pause or recording screen
|
|
95
89
|
if s.pause_recording != last_recording_status:
|
|
@@ -106,10 +100,12 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
106
100
|
if s.check_refresh_required(): # do not give priority to visual output
|
|
107
101
|
update_rects = []
|
|
108
102
|
|
|
109
|
-
if s.
|
|
103
|
+
if s.thresholds != last_thresholds:
|
|
104
|
+
# thresholds have changed
|
|
110
105
|
_draw_plotter_thread_thresholds(
|
|
111
106
|
plotter_thread, s.thresholds, s.scaling_plotter
|
|
112
107
|
)
|
|
108
|
+
last_thresholds = s.thresholds
|
|
113
109
|
|
|
114
110
|
if s.plot_indicator:
|
|
115
111
|
############################################ plot_indicator
|
|
@@ -132,7 +128,7 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
132
128
|
+ 0.5 * indicator_grid
|
|
133
129
|
)
|
|
134
130
|
|
|
135
|
-
if cnt ==
|
|
131
|
+
if cnt == s.force_id_level_detect:
|
|
136
132
|
thr = s.thresholds
|
|
137
133
|
else:
|
|
138
134
|
thr = None
|
|
@@ -236,11 +232,11 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
236
232
|
tmp = np.array(
|
|
237
233
|
list(
|
|
238
234
|
map(
|
|
239
|
-
lambda x: s.history[x[0]].moving_average
|
|
235
|
+
lambda x: s.history[x[0]].moving_average(x[1]),
|
|
240
236
|
s.plot_data_plotter,
|
|
241
237
|
)
|
|
242
238
|
),
|
|
243
|
-
dtype=
|
|
239
|
+
dtype=np.float64,
|
|
244
240
|
)
|
|
245
241
|
else:
|
|
246
242
|
tmp = np.array(
|
|
@@ -250,7 +246,7 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
250
246
|
s.plot_data_plotter,
|
|
251
247
|
)
|
|
252
248
|
),
|
|
253
|
-
dtype=
|
|
249
|
+
dtype=np.float64,
|
|
254
250
|
)
|
|
255
251
|
|
|
256
252
|
if s.thresholds is not None:
|
|
@@ -316,33 +312,31 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
316
312
|
txt.present(update=False, clear=False)
|
|
317
313
|
update_rects.append(get_pygame_rect(txt, exp.screen.size))
|
|
318
314
|
|
|
319
|
-
#FIXME Threshold levels down work
|
|
320
|
-
|
|
321
315
|
# Sensor info
|
|
322
316
|
pos = (200, 250)
|
|
323
317
|
tmp = stimuli.Canvas(
|
|
324
|
-
position=pos, size=(
|
|
318
|
+
position=pos, size=(600, 50), colour=misc.constants.C_BLACK
|
|
325
319
|
)
|
|
326
320
|
tmp.present(update=False, clear=False)
|
|
327
321
|
update_rects.append(get_pygame_rect(tmp, exp.screen.size))
|
|
328
322
|
if s.thresholds is not None:
|
|
329
323
|
if s.n_sensors > 1:
|
|
330
324
|
tmp = [
|
|
331
|
-
s.thresholds.get_level(s.
|
|
332
|
-
s.thresholds.get_level(s.
|
|
325
|
+
s.thresholds.get_level(s.get_average_level_detection_parameter(0)),
|
|
326
|
+
s.thresholds.get_level(s.get_average_level_detection_parameter(1)),
|
|
333
327
|
]
|
|
334
328
|
else:
|
|
335
|
-
tmp = s.thresholds.get_level(s.
|
|
329
|
+
tmp = s.thresholds.get_level(s.get_average_level_detection_parameter(0))
|
|
330
|
+
|
|
336
331
|
|
|
337
332
|
txt = stimuli.TextBox(
|
|
338
333
|
position=pos,
|
|
339
|
-
size=(
|
|
334
|
+
size=(600, 50),
|
|
340
335
|
text_size=15,
|
|
341
336
|
text="T: {0} L: {1}".format(s.thresholds, tmp),
|
|
342
337
|
text_colour=misc.constants.C_YELLOW,
|
|
343
338
|
text_justification=0,
|
|
344
339
|
)
|
|
345
|
-
|
|
346
340
|
txt.present(update=False, clear=False)
|
|
347
341
|
|
|
348
342
|
pos = (400, 250)
|
|
@@ -362,30 +356,13 @@ def _main_loop(exp, recorder: DataRecorder, gs: GUISettings, info_strings: List[
|
|
|
362
356
|
)
|
|
363
357
|
txt.present(update=False, clear=False)
|
|
364
358
|
|
|
365
|
-
# last_udp input
|
|
366
|
-
if s.last_udp_data is not None:
|
|
367
|
-
pos = (420, 250)
|
|
368
|
-
stimuli.Canvas(
|
|
369
|
-
position=pos, size=(200, 30), colour=misc.constants.C_BLACK
|
|
370
|
-
).present(update=False, clear=False)
|
|
371
|
-
txt = stimuli.TextBox(
|
|
372
|
-
position=pos,
|
|
373
|
-
size=(200, 30),
|
|
374
|
-
# background_colour=(30,30,30),
|
|
375
|
-
text_size=15,
|
|
376
|
-
text="UDP:" + str(s.last_udp_data),
|
|
377
|
-
text_colour=misc.constants.C_YELLOW,
|
|
378
|
-
text_justification=0,
|
|
379
|
-
)
|
|
380
|
-
txt.present(update=False, clear=False)
|
|
381
|
-
update_rects.append(get_pygame_rect(txt, exp.screen.size))
|
|
382
|
-
|
|
383
359
|
pygame.display.update(update_rects)
|
|
384
360
|
# end plotting screen
|
|
385
361
|
|
|
386
362
|
##### end main loop
|
|
387
363
|
|
|
388
364
|
recorder.pause_saving()
|
|
365
|
+
recorder.lsl_events_stream.push_sample(["Recording stopped"])
|
|
389
366
|
s.background.stimulus("Quitting").present()
|
|
390
367
|
if plotter_thread is not None:
|
|
391
368
|
plotter_thread.join()
|
|
@@ -435,15 +412,13 @@ def run(settings: AppSettings):
|
|
|
435
412
|
pygame.display.set_caption(f"pyforceDAQ {forceDAQVersion}")
|
|
436
413
|
|
|
437
414
|
icon_path = os.path.join(os.path.dirname(__file__), "rf_icon.png")
|
|
438
|
-
print(f"Loading icon from {icon_path}")
|
|
439
415
|
pygame.display.set_icon(pygame.image.load(icon_path))
|
|
440
416
|
|
|
441
417
|
logo_text_line("Initializing Force Recording").present()
|
|
442
418
|
show_logo_time = 0.5
|
|
443
419
|
recorder = DataRecorder(
|
|
444
420
|
recording_settings=rs,
|
|
445
|
-
force_sensor_settings=sensor_settings
|
|
446
|
-
poll_udp_connection=False, # FIXME remove UDP polling from recorder and put it in main loop
|
|
421
|
+
force_sensor_settings=sensor_settings
|
|
447
422
|
)
|
|
448
423
|
|
|
449
424
|
if rs.save_data:
|
|
@@ -6,9 +6,8 @@ from typing import List
|
|
|
6
6
|
import PySimpleGUI as _sg
|
|
7
7
|
|
|
8
8
|
from . import __version__, constants
|
|
9
|
-
from ._lib.misc import list_settings_files
|
|
9
|
+
from ._lib.misc import get_lan_ip, list_settings_files
|
|
10
10
|
from ._lib.settings import AppSettings
|
|
11
|
-
from ._lib.udp_connection import UDPConnection
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def _check_sensor_calibration_settings(
|
|
@@ -64,7 +63,7 @@ def _windows_run(settings: AppSettings):
|
|
|
64
63
|
info_settings.append([_sg.Text(f"- {labels}: {cal}", text_color=col)])
|
|
65
64
|
|
|
66
65
|
info = [[_sg.Text(f"version: {__version__}")]]
|
|
67
|
-
info.append([_sg.Text(f"IP address: {
|
|
66
|
+
info.append([_sg.Text(f"IP address: {get_lan_ip()}")])
|
|
68
67
|
|
|
69
68
|
if constants.DAQ_TYPE == constants.MOCK_SENSOR:
|
|
70
69
|
info.append([_sg.Text("!!! USING MOCK SENSORS !!!", text_color="red")])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -152,6 +152,7 @@ class RecordingSettings(ABCSettings):
|
|
|
152
152
|
|
|
153
153
|
@dataclass
|
|
154
154
|
class GUISettings(ABCSettings):
|
|
155
|
+
|
|
155
156
|
level_detection_parameter: str = "Fz"
|
|
156
157
|
window_font: str = "freemono"
|
|
157
158
|
moving_average_size: int = 5
|
|
@@ -174,7 +175,6 @@ class GUISettings(ABCSettings):
|
|
|
174
175
|
default_factory=lambda: [(0, 2), (1, 2)]
|
|
175
176
|
)
|
|
176
177
|
|
|
177
|
-
|
|
178
178
|
class AppSettings(object):
|
|
179
179
|
def __init__(self, filename: str | Path):
|
|
180
180
|
# defaults
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|