pyForceDAQ 2.0.5.dev4__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.
Files changed (36) hide show
  1. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/PKG-INFO +1 -1
  2. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/pyproject.toml +1 -1
  3. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/__main__.py +4 -9
  4. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/data_recorder.py +23 -44
  5. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/file_writer.py +1 -7
  6. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/lsl.py +9 -5
  7. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/misc.py +33 -60
  8. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/sensor.py +8 -4
  9. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/sensor_process.py +12 -9
  10. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/types.py +12 -35
  11. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/udp_connection.py +2 -23
  12. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/__init__.py +2 -0
  13. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_gui_status.py +9 -23
  14. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_layout.py +1 -5
  15. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_run.py +46 -65
  16. pyforcedaq-2.0.7/src/pyforcedaq/gui/rf_icon.png +0 -0
  17. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/launcher.py +2 -4
  18. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/__init__.py +0 -0
  19. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/__init__.py +0 -0
  20. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/clock.py +0 -0
  21. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/process_priority_manager.py +0 -0
  22. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/_lib/settings.py +1 -1
  23. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/constants.py +0 -0
  24. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/__init__.py +0 -0
  25. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/_pyATIDAQ.py +0 -0
  26. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/calibration_dll.py +0 -0
  27. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/calibration_iaftt.py +0 -0
  28. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/read_daq_mock_sensor.py +0 -0
  29. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/read_daq_nidaqmx.py +0 -0
  30. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/daq/read_daq_pydaqmx.py +0 -0
  31. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/force.py +0 -0
  32. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_level_indicator.py +0 -0
  33. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_pg_surface.py +0 -0
  34. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_plotter.py +0 -0
  35. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/_scaling.py +0 -0
  36. {pyforcedaq-2.0.5.dev4 → pyforcedaq-2.0.7}/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyForceDAQ
3
- Version: 2.0.5.dev4
3
+ Version: 2.0.7
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyForceDAQ"
3
- version = "2.0.5-dev4"
3
+ version = "2.0.7"
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" },
@@ -21,14 +21,6 @@ def cli():
21
21
 
22
22
  parser.add_argument("SETTINGS_FILE", nargs="?", default="", help="settings file")
23
23
 
24
- parser.add_argument(
25
- "-l",
26
- "--launcher",
27
- action="store_true",
28
- default=False,
29
- help="Run with launcher GUI to edit settings and start recording",
30
- )
31
-
32
24
  parser.add_argument(
33
25
  "-o",
34
26
  "--omit-launcher",
@@ -70,9 +62,12 @@ def cli():
70
62
  else:
71
63
  from .gui import run_settings_file
72
64
 
65
+ if len(args.SETTINGS_FILE) == 0:
66
+ print("No settings file provided, can't start recording")
67
+ exit()
68
+
73
69
  run_settings_file(args.SETTINGS_FILE)
74
70
 
75
71
 
76
72
  if __name__ == "__main__": # required because of threading
77
73
  cli()
78
- cli()
@@ -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 DAQEvents, ForceSensorData, PollingPriority
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 udp connection"""
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 (TAG_COMMENTS, TAG_DAQEVENT, TAG_UDPDATA, DAQEvents,
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
- content_type: content type of stream. By convention LSL uses the content
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: list):
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 = [[0] * number_of_parameter] * history_size
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._previous_moving_average = self.moving_average
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
- s = [float(0)] * self.number_of_parameter
153
- for t in self.history:
154
- s = list(map(lambda x: x[0] + x[1], zip(s, t)))
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 record force sensor data
1
+ """Sensor class for reading data from NI devices and converting to force data.
2
2
 
3
- See COPYING file distributed along with the pyForceDAQ copyright and license terms.
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(f"biasdata should have the shape (x, n_sensor_channels)")
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._sample_cnt = Value(ct.c_int64, 0)
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._sample_cnt.value
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
- if lsl_data_steam.is_init:
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._sample_cnt.value += 1 # type: ignore
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] = None
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)
@@ -1,3 +1,5 @@
1
+
2
+ print("\nStarting pyforceDAQ GUI...")
1
3
  from .._lib import settings # # read settings
2
4
  from ._run import (
3
5
  run, # for running with options from Python script
@@ -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.history = []
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.get_saved_sample_cnt, self.sensor_processes)
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 process_udp_event(self, udp_event):
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 level_detection_parameter_average(self, sensor):
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
@@ -2,7 +2,6 @@ __author__ = "Oliver Lindemann"
2
2
 
3
3
  # helper functions
4
4
  import os
5
- from copy import copy
6
5
  from typing import List
7
6
 
8
7
  import pygame
@@ -20,7 +19,6 @@ colours = [
20
19
  expy_constants.C_EXPYRIMENT_PURPLE,
21
20
  ]
22
21
 
23
-
24
22
  def get_pygame_rect(stimulus, screen_size):
25
23
  """little helper function that returns the pygame rect from stimuli"""
26
24
  half_screen_size = (screen_size[0] / 2, screen_size[1] / 2)
@@ -75,9 +73,7 @@ class RecordingScreen(object):
75
73
  self.text_colour = text_colour
76
74
  self.elements = []
77
75
 
78
- txt_list = copy(txt_top_left)
79
- txt_list.insert(0, f"Version {forceDAQVersion}")
80
- for cnt, txt in enumerate(txt_list):
76
+ for cnt, txt in enumerate(txt_top_left):
81
77
  self.add_text_line_left(
82
78
  txt,
83
79
  [self.left, self.top - cnt * 20],
@@ -5,8 +5,8 @@ See COPYING file distributed along with the pyForceDAQ copyright and license ter
5
5
  __author__ = "Oliver Lindemann"
6
6
 
7
7
  import logging
8
+ import os
8
9
  from pathlib import Path
9
- from pickle import dumps
10
10
  from time import sleep
11
11
  from typing import List
12
12
 
@@ -19,26 +19,21 @@ from .._lib.clock import wait_ms
19
19
  from .._lib.data_recorder import DataRecorder
20
20
  from .._lib.sensor_process import SensorProcess
21
21
  from .._lib.settings import AppSettings, GUISettings, SensorSettings
22
+ from .._lib.types import ForceSensorData
22
23
  from ..constants import DEFAULT_OUTPUT_FILENAME
23
24
  from ._gui_status import GUIStatus
24
25
  from ._layout import colours, get_pygame_rect, logo_text_line, make_text_line
25
26
  from ._level_indicator import level_indicator
26
27
  from ._plotter import PlotterThread
27
28
 
28
- # eedback
29
- COMMAND_STR = b"$"
30
- RESPONSE_MINMAX = COMMAND_STR + b"xRM1"
31
- RESPONSE_MINMAX2 = COMMAND_STR + b"xRM2"
32
- CHANGED_LEVEL = COMMAND_STR + b"xCL1"
33
- CHANGED_LEVEL2 = COMMAND_STR + b"xCL2"
29
+ # Feedback
34
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[gs.level_detection_parameter], channel=x
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
- recorder.udp.send_queue.put(CHANGED_LEVEL2 + dumps(tmp)) # type: ignore
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
- recorder.udp.send_queue.put(RESPONSE_MINMAX + dumps(tmp)) # type: ignore
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.check_thresholds_changed():
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 == gs.level_detection_parameter:
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[x[1]],
235
+ lambda x: s.history[x[0]].moving_average(x[1]),
240
236
  s.plot_data_plotter,
241
237
  )
242
238
  ),
243
- dtype=float,
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=float,
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=(400, 50), colour=misc.constants.C_BLACK
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.level_detection_parameter_average(0)),
332
- s.thresholds.get_level(s.level_detection_parameter_average(1)),
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.level_detection_parameter_average(0))
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=(400, 50),
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()
@@ -432,12 +409,16 @@ def run(settings: AppSettings):
432
409
 
433
410
  control.initialize(exp)
434
411
  exp.mouse.show_cursor() # type: ignore #
412
+ pygame.display.set_caption(f"pyforceDAQ {forceDAQVersion}")
413
+
414
+ icon_path = os.path.join(os.path.dirname(__file__), "rf_icon.png")
415
+ pygame.display.set_icon(pygame.image.load(icon_path))
416
+
435
417
  logo_text_line("Initializing Force Recording").present()
436
418
  show_logo_time = 0.5
437
419
  recorder = DataRecorder(
438
420
  recording_settings=rs,
439
- force_sensor_settings=sensor_settings,
440
- poll_udp_connection=False, # FIXME remove UDP polling from recorder and put it in main loop
421
+ force_sensor_settings=sensor_settings
441
422
  )
442
423
 
443
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: {UDPConnection.MY_IP}")])
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")])
@@ -168,7 +167,6 @@ def run_launcher():
168
167
  if ch == "No":
169
168
  return # quit
170
169
  from . import gui
171
-
172
170
  gui.run(settings)
173
171
  else:
174
172
  pass
@@ -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