pyForceDAQ 2.0.1.dev1__tar.gz → 2.0.2__tar.gz

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