pyForceDAQ 2.0.4__tar.gz → 2.0.5.dev3__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 (44) hide show
  1. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/PKG-INFO +1 -4
  2. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/pyproject.toml +21 -4
  3. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/__main__.py +27 -4
  4. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/data_recorder.py +79 -192
  5. pyforcedaq-2.0.5.dev3/src/pyforcedaq/_lib/file_writer.py +110 -0
  6. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/process_priority_manager.py +1 -1
  7. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/sensor.py +44 -21
  8. pyforcedaq-2.0.5.dev3/src/pyforcedaq/_lib/sensor_process.py +222 -0
  9. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/settings.py +2 -2
  10. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/types.py +8 -17
  11. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/udp_connection.py +1 -5
  12. pyforcedaq-2.0.5.dev3/src/pyforcedaq/constants.py +12 -0
  13. pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/__init__.py +68 -0
  14. pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_dll.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/calibration_dll.py +5 -3
  15. pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/calibration_iaftt.py +17 -0
  16. pyforcedaq-2.0.4/src/pyforcedaq/daq/_mock_sensor.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/read_daq_mock_sensor.py +4 -2
  17. pyforcedaq-2.0.4/src/pyforcedaq/daq/_use_nidaqmx.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/read_daq_nidaqmx.py +9 -6
  18. pyforcedaq-2.0.4/src/pyforcedaq/daq/_use_pydaqmx.py → pyforcedaq-2.0.5.dev3/src/pyforcedaq/daq/read_daq_pydaqmx.py +4 -2
  19. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/force.py +1 -1
  20. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_gui_status.py +50 -45
  21. pyforcedaq-2.0.5.dev3/src/pyforcedaq/gui/_layout.py +133 -0
  22. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_level_indicator.py +4 -2
  23. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_run.py +20 -29
  24. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/launcher.py +4 -5
  25. pyforcedaq-2.0.4/src/pyforcedaq/_lib/polling_time_profile.py +0 -57
  26. pyforcedaq-2.0.4/src/pyforcedaq/_lib/sensor_process.py +0 -277
  27. pyforcedaq-2.0.4/src/pyforcedaq/constants.py +0 -9
  28. pyforcedaq-2.0.4/src/pyforcedaq/daq/__init__.py +0 -28
  29. pyforcedaq-2.0.4/src/pyforcedaq/daq/_calibration_iaftt.py +0 -17
  30. pyforcedaq-2.0.4/src/pyforcedaq/extras/__init__.py +0 -0
  31. pyforcedaq-2.0.4/src/pyforcedaq/extras/convert.py +0 -275
  32. pyforcedaq-2.0.4/src/pyforcedaq/extras/read_force_data.py +0 -89
  33. pyforcedaq-2.0.4/src/pyforcedaq/gui/_layout.py +0 -120
  34. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/__init__.py +0 -0
  35. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/__init__.py +0 -0
  36. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/clock.py +0 -0
  37. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/lsl.py +0 -0
  38. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/_lib/misc.py +0 -0
  39. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/daq/_pyATIDAQ.py +0 -0
  40. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/__init__.py +0 -0
  41. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_pg_surface.py +0 -0
  42. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_plotter.py +0 -0
  43. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/_scaling.py +0 -0
  44. {pyforcedaq-2.0.4 → pyforcedaq-2.0.5.dev3}/src/pyforcedaq/gui/forceDAQ_logo.png +0 -0
@@ -1,18 +1,15 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyForceDAQ
3
- Version: 2.0.4
3
+ Version: 2.0.5.dev3
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: icecream>=2.2.0
10
9
  Requires-Dist: nidaqmx>=1.5.0
11
10
  Requires-Dist: numpy>=2.4.4
12
11
  Requires-Dist: psutil>=7.2.2
13
- Requires-Dist: pydaqmx>=1.4.7
14
12
  Requires-Dist: pylsl>=1.18.2
15
13
  Requires-Dist: pysimplegui>=6.0
16
- Requires-Dist: pyxdf>=1.17.4
17
14
  Requires-Dist: tomlkit>=0.15.0
18
15
  Requires-Python: >=3.12, <3.14
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyForceDAQ"
3
- version = "2.0.4"
3
+ version = "2.0.5-dev3"
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,14 +9,11 @@ requires-python = ">=3.12, <3.14"
9
9
  dependencies = [
10
10
  "atiiaftt>=0.1.1",
11
11
  "expyriment>=1.0.1",
12
- "icecream>=2.2.0",
13
12
  "nidaqmx>=1.5.0",
14
13
  "numpy>=2.4.4",
15
14
  "psutil>=7.2.2",
16
- "pydaqmx>=1.4.7",
17
15
  "pylsl>=1.18.2",
18
16
  "pysimplegui>=6.0",
19
- "pyxdf>=1.17.4",
20
17
  "tomlkit>=0.15.0",
21
18
  ]
22
19
 
@@ -27,5 +24,25 @@ dependencies = [
27
24
  requires = ["uv_build>=0.11.15,<0.12.0"]
28
25
  build-backend = "uv_build"
29
26
 
27
+ [dependency-groups]
28
+ dev = [
29
+ "icecream>=2.2.0",
30
+ "ipython>=9.14.0",
31
+ "ruff>=0.15.15",
32
+ ]
33
+ pydaqmx = [
34
+ "pydaqmx>=1.4.7",
35
+ ]
36
+ test = [
37
+ "pytest>=9.0.3",
38
+ ]
39
+
30
40
  [project.scripts]
31
41
  pyforcedaq = "pyforcedaq.__main__:cli"
42
+
43
+ [tool.ruff.lint]
44
+ ignore = ["F401", "F405"]
45
+
46
+ [tool.ruff.format]
47
+ line-ending = "lf"
48
+ indent-style = "space"
@@ -1,8 +1,13 @@
1
1
  import argparse
2
2
 
3
- from . import __author__, __version__
3
+ from . import __author__, __version__, constants
4
4
 
5
5
 
6
+ def print_version():
7
+ print("+" + "-" * 23 + "+")
8
+ print(f"| pyforceDAQ {__version__}".ljust(24) + "|")
9
+ print("+" + "-" * 23 + "+")
10
+
6
11
  def cli():
7
12
 
8
13
  parser = argparse.ArgumentParser(
@@ -31,12 +36,29 @@ def cli():
31
36
  default=False,
32
37
  help="Omit launcher GUI and start recording directly",
33
38
  )
34
- print("+" + "-" * 23 + "+")
35
- print(f"| pyforceDAQ {__version__}".ljust(24) + "|")
36
- print("+" + "-" * 23 + "+")
39
+
40
+ parser.add_argument(
41
+ "--mock",
42
+ action="store_true",
43
+ default=False,
44
+ help="Use mock sensor",
45
+ )
46
+
47
+ parser.add_argument(
48
+ "--dll",
49
+ action="store_true",
50
+ default=False,
51
+ help="Use self compiled ATI DLL",
52
+ )
37
53
 
38
54
  args = parser.parse_args()
55
+ if args.mock:
56
+ constants.DAQ_TYPE = constants.MOCK_SENSOR
57
+ else:
58
+ constants.DAQ_TYPE = constants.NIDAQMX # use NI-DAQmx
59
+ constants.USE_AIFTT = not args.dll
39
60
 
61
+ print_version()
40
62
  if not args.omit_launcher:
41
63
  if len(args.SETTINGS_FILE) > 0:
42
64
  print("Can't use launcher and settings file")
@@ -53,3 +75,4 @@ def cli():
53
75
 
54
76
  if __name__ == "__main__": # required because of threading
55
77
  cli()
78
+ cli()
@@ -6,33 +6,24 @@ See COPYING file distributed along with the pyForceDAQ copyright and license ter
6
6
  __author__ = "Oliver Lindemann"
7
7
 
8
8
  import atexit
9
- import gzip
10
9
  import logging
11
- from io import TextIOWrapper
12
10
  from pathlib import Path
13
11
  from time import asctime, localtime, strftime
14
12
  from typing import List
15
13
 
16
14
  from .. import __version__ as forceDAQVersion
17
- from .clock import wait_ms
15
+ from .. import constants
16
+ from .file_writer import FileWriter
18
17
  from .misc import set_logging
19
18
  from .process_priority_manager import ProcessPriorityManager
20
19
  from .sensor_process import SensorProcess
21
20
  from .settings import RecordingSettings, SensorSettings
22
- from .types import (
23
- TAG_COMMENTS,
24
- TAG_DAQEVENT,
25
- TAG_UDPDATA,
26
- DAQEvents,
27
- ForceSensorData,
28
- PollingPriority,
29
- UDPData,
30
- )
21
+ from .types import DAQEvents, ForceSensorData, PollingPriority
31
22
  from .udp_connection import UDPConnectionProcess
32
23
 
33
24
  set_logging(data_directory="data", log_file="recording.log")
34
25
 
35
- NEWLINE = "\n"
26
+ # FIXME LSL marker event for all events
36
27
 
37
28
 
38
29
  class DataRecorder(object):
@@ -49,15 +40,26 @@ class DataRecorder(object):
49
40
 
50
41
  polling_priority has to be types.PollingPriority.{HIGH},
51
42
  {REALTIME} or {NORMAL} or None
43
+
44
+ You can change the used modules by settings the following constants before creating the
45
+ DataRecorder instance:
46
+ * set constants.DAQ_TYPE to constants.PYDAQMX, constants.NIDAQMX or constants.MOCK_SENSOR
47
+ * set constants.USE_AIFTT to True or False
52
48
  """
53
49
 
54
50
  if not isinstance(force_sensor_settings, list):
55
51
  force_sensor_settings = [force_sensor_settings]
56
52
 
57
53
  self.recording_settings = recording_settings
54
+ if recording_settings.save_data:
55
+ self.file_writer = FileWriter(recording_settings)
56
+ queue = self.file_writer.queue
57
+ else:
58
+ self.file_writer = None
59
+ queue = None
58
60
 
59
61
  # create sensor processes
60
- self._force_sensor_processes = []
62
+ self.force_sensor_processes: List[SensorProcess] = []
61
63
  event_trigger = []
62
64
  for fs in force_sensor_settings:
63
65
  if not isinstance(fs, SensorSettings):
@@ -66,11 +68,12 @@ class DataRecorder(object):
66
68
  fst = SensorProcess(
67
69
  sensor_settings=fs,
68
70
  recording_settings=recording_settings,
69
- pipe_buffered_data_after_pause=True,
70
- )
71
+ file_writer_queue=queue,
72
+ daq_type=constants.DAQ_TYPE,
73
+ use_aiftt=constants.USE_AIFTT)
71
74
  fst.start()
72
75
  event_trigger.append(fst.event_trigger)
73
- self._force_sensor_processes.append(fst)
76
+ self.force_sensor_processes.append(fst)
74
77
 
75
78
  # create udp connection process
76
79
  if poll_udp_connection:
@@ -82,7 +85,7 @@ class DataRecorder(object):
82
85
  # process managing FIYME needed?
83
86
  self._proc_manager = ProcessPriorityManager()
84
87
  self._proc_manager.add_subprocess(self.udp)
85
- self._proc_manager.add_subprocess(self._force_sensor_processes)
88
+ self._proc_manager.add_subprocess(self.force_sensor_processes)
86
89
  if self.recording_settings.priority is not None:
87
90
  level = PollingPriority.NORMAL
88
91
  else:
@@ -94,37 +97,36 @@ class DataRecorder(object):
94
97
  )
95
98
  # logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
96
99
 
97
- self._is_recording = False
98
- self._file = None
99
- self._daq_event = []
100
- self.path_open_file = Path("")
100
+ self._daq_event = [] # FIXME needed?
101
101
  atexit.register(self.quit)
102
102
 
103
103
  @property
104
- def is_saving_data(self):
104
+ def has_file_writer(self):
105
105
  """Property indicates whether a data file is open"""
106
- return self._file is not None
106
+ return isinstance(self.file_writer, FileWriter) and self.file_writer.is_alive()
107
107
 
108
108
  @property
109
109
  def is_alive(self):
110
110
  """Property indicates whether the recording processes are alive"""
111
111
  try:
112
- return self._force_sensor_processes[0].is_alive()
113
- except Exception:
112
+ return self.force_sensor_processes[0].is_alive()
113
+ except IndexError:
114
114
  return False
115
115
 
116
116
  @property
117
- def is_recording(self):
117
+ def is_saving(self):
118
118
  """Property indicates whether the recording is started or paused"""
119
- return self._is_recording
120
-
121
- @property
122
- def force_sensor_processes(self):
123
- return self._force_sensor_processes
119
+ if self.has_file_writer:
120
+ for fsp in self.force_sensor_processes:
121
+ if not fsp.is_saving:
122
+ return False
123
+ return True # all sensor processes are saving, file writer is alive
124
+ else:
125
+ return False
124
126
 
125
127
  @property
126
128
  def sensor_settings_list(self):
127
- return list(map(lambda x: x.sensor_settings, self._force_sensor_processes))
129
+ return list(map(lambda x: x.sensor_settings, self.force_sensor_processes))
128
130
 
129
131
  def quit(self) -> list | None:
130
132
  """Stop all recording processes, close data file and quit recording
@@ -138,20 +140,16 @@ class DataRecorder(object):
138
140
  if not self.is_alive:
139
141
  return
140
142
 
141
- buffer = self.pause_recording()
143
+ self.pause_saving()
144
+ for fsp in self.force_sensor_processes:
145
+ fsp.join()
142
146
  self.close_data_file()
143
147
 
144
148
  if self.udp is not None:
145
149
  self.udp.quit()
146
150
 
147
- # wait that all processes are quitted
148
- for fsp in self._force_sensor_processes:
149
- fsp.join()
150
-
151
151
  logging.info("Quit recording")
152
152
 
153
- return buffer
154
-
155
153
  def process_and_write_udp_events(self) -> list:
156
154
  """process udp events and return them"""
157
155
  buffer = []
@@ -162,59 +160,11 @@ class DataRecorder(object):
162
160
  # until queue empty or no udp connection
163
161
  break
164
162
  buffer.append(data)
165
- if len(buffer) > 0:
166
- self._write_data(buffer)
167
- return buffer
168
-
169
- def _write_data(
170
- self, data_buffer: list, recording_screen=None, float_decimal_places: int = 4
171
- ) -> None:
172
- """writes data to disk and set counters
173
163
 
174
- ignores UDP remote control commands
175
- """
176
- # DOC output format
177
-
178
- BLOCKSIZE = 10000 # for recording screen feedback only
179
- write_forces = self.recording_settings.array_write_forces()
180
- write_trigger = self.recording_settings.array_write_trigger()
181
- write_deviceid = len(self.recording_settings.device_labels) > 1
182
- float_format = "{0:." + str(float_decimal_places) + "f},"
183
- buffer_len = len(data_buffer)
184
- for c, d in enumerate(data_buffer):
185
- if self._file is not None:
186
- if isinstance(d, ForceSensorData):
187
- line = f"{d.time}, {d.acquisition_delay},"
188
- if write_deviceid:
189
- line += f"{d.sensor_id},"
190
- for x in d.forces[write_forces]:
191
- line += float_format.format(x)
192
- for x in d.trigger[write_trigger]:
193
- if isinstance(x, int):
194
- line += f"{x},"
195
- else:
196
- line += float_format.format(x)
197
- self._file_write(line[:-1] + NEWLINE)
198
-
199
- elif isinstance(d, DAQEvents):
200
- self._file_write(f"{TAG_DAQEVENT},{d.time},{str(d.code)}" + NEWLINE)
201
-
202
- elif isinstance(d, UDPData):
203
- self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
204
-
205
- if recording_screen is not None and c % BLOCKSIZE == 0:
206
- recording_screen.stimulus(
207
- "Saving {0} of {1} blocks".format(
208
- c // BLOCKSIZE, buffer_len // BLOCKSIZE
209
- )
210
- ).present()
211
-
212
- def _file_write(self, s: str) -> None:
213
-
214
- if isinstance(self._file, gzip.GzipFile):
215
- self._file.write(s.encode("utf-8"))
216
- elif isinstance(self._file, TextIOWrapper):
217
- self._file.write(s)
164
+ if isinstance(self.file_writer, FileWriter):
165
+ for dat in buffer:
166
+ self.file_writer.queue.put(dat)
167
+ return buffer
218
168
 
219
169
  def store_daq_event(
220
170
  self, code: str | int | float, time: float | None = None
@@ -226,35 +176,20 @@ class DataRecorder(object):
226
176
  """
227
177
  self._daq_event.append(DAQEvents(time=time, code=code))
228
178
 
229
- def start_recording(self, determine_bias: bool = False) -> None:
179
+ def start_saving(self) -> None:
230
180
  """Start polling process and record
231
181
 
232
182
  See Also
233
183
  --------
234
- is_recording
184
+ is_saving
235
185
 
236
186
  """
237
187
 
238
- if determine_bias:
239
- self.determine_biases(n_samples=1000)
188
+ for fsp in self.force_sensor_processes:
189
+ fsp.flag_sensor_bias_is_determined.wait() # wait is no initial bias is set yet
190
+ fsp.start_saving()
240
191
 
241
- if len(
242
- list(
243
- filter(
244
- lambda x: x.event_bias_is_available.is_set(),
245
- self._force_sensor_processes,
246
- )
247
- )
248
- ) != len(self._force_sensor_processes):
249
- raise RuntimeError(
250
- "Sensors can't be started before bias has been determined."
251
- )
252
-
253
- # start polling
254
- list(map(lambda x: x.start_polling(), self._force_sensor_processes))
255
- self._is_recording = True
256
-
257
- def pause_recording(self, recording_screen=None) -> list:
192
+ def pause_saving(self):
258
193
  """Pauses all polling processes and process data
259
194
 
260
195
  returns
@@ -262,59 +197,17 @@ class DataRecorder(object):
262
197
  data : all last data
263
198
 
264
199
  """
265
- self._is_recording = False
266
-
267
- data = []
268
- if recording_screen is not None:
269
- recording_screen.stimulus("").present()
270
200
 
271
201
  # pause polling
272
- for fsp in self._force_sensor_processes:
273
- fsp.pause_polling()
274
-
275
- wait_ms(500)
276
-
277
- if recording_screen is not None:
278
- if self.is_saving_data:
279
- recording_screen.stimulus("saving data ...").present()
280
- else:
281
- recording_screen.stimulus("pause recording").present()
282
-
283
- # get data
284
- for fsp in self._force_sensor_processes:
285
- buffer = fsp.get_buffer()
286
- self._write_data(buffer, recording_screen)
287
- data.extend(buffer)
288
-
289
- # udp event
290
- data.extend(self.process_and_write_udp_events())
291
-
292
- # soft trigger
293
- self._write_data(self._daq_event)
294
- data.extend(self._daq_event)
295
- self._daq_event = []
296
- return data
297
-
298
- def determine_biases(self, n_samples: int) -> None:
299
- """Record n data samples (n_samples) to determine bias.
300
- Afterwards recording is in pause mode
301
-
302
- Notes
303
- -----
304
- The function take some time to be processed
305
-
306
- See Also
307
- --------
308
- Sensor.determine_bias()
202
+ for fsp in self.force_sensor_processes:
203
+ fsp.pause_saving()
309
204
 
310
- """
311
205
 
312
- self.pause_recording()
313
- for x in self._force_sensor_processes:
314
- x.determine_bias(n_samples=n_samples)
315
-
316
- for x in self._force_sensor_processes:
317
- x.event_bias_is_available.wait()
206
+ def determine_biases(self) -> None:
207
+ for x in self.force_sensor_processes:
208
+ x.determine_bias()
209
+ for x in self.force_sensor_processes:
210
+ x.flag_sensor_bias_is_determined.wait()
318
211
 
319
212
  def open_data_file(
320
213
  self,
@@ -322,7 +215,7 @@ class DataRecorder(object):
322
215
  subdirectory: str = "data",
323
216
  varnames: bool = True,
324
217
  comment_line: str = "",
325
- ) -> Path:
218
+ ) -> Path | None:
326
219
  """Create a data file
327
220
 
328
221
  Only if data file has been opened, data will be saved!
@@ -347,20 +240,22 @@ class DataRecorder(object):
347
240
  full path the actually used file (incl. timestamp)
348
241
 
349
242
  """
350
- self.close_data_file()
243
+
244
+ if not isinstance(self.file_writer, FileWriter):
245
+ return
351
246
 
352
247
  # create filename
353
248
  data_dir = Path.cwd() / subdirectory
354
249
  data_dir.mkdir(exist_ok=True)
250
+
355
251
  if self.recording_settings.zip_data:
356
- filename = Path(filename).with_suffix(".csv.gz")
252
+ filename = Path(filename).with_suffix(".csv.bz2")
357
253
  else:
358
254
  filename = Path(filename).with_suffix(".csv")
359
-
360
255
  while True:
361
- self.path_open_file = data_dir / filename
362
- if self.path_open_file.is_file():
363
- # print "data file already exists, adding counter"
256
+ file_path = data_dir / filename
257
+ if file_path.is_file():
258
+ #get unique filename by adding timestamp if file already exists
364
259
  filename = Path(
365
260
  filename.stem
366
261
  + "_"
@@ -370,32 +265,24 @@ class DataRecorder(object):
370
265
  else:
371
266
  break
372
267
 
373
- if self.recording_settings.zip_data:
374
- self._file = gzip.open(self.path_open_file, "w")
375
- else:
376
- self._file = open(self.path_open_file, "w")
377
- print("Data file: {}".format(self.path_open_file))
378
-
379
- self._file_write(
380
- TAG_COMMENTS
381
- + "Recorded at {0} with pyForceDAQ {1}\n".format(
382
- asctime(localtime()), forceDAQVersion
383
- )
384
- )
385
- logging.info("new file: {}".format(self.path_open_file))
268
+ self.file_writer.start_recording(file_path=file_path, append_mode=False)
269
+ logging.info("new file: %s", file_path)
270
+
271
+ self.file_writer.queue.put(
272
+ f"Recorded at {asctime(localtime())} with pyForceDAQ {forceDAQVersion}\n")
386
273
 
387
274
  for s in self.sensor_settings_list:
388
275
  txt = f" Sensor: label={s.device_label}, cal-file={s.calibration_file}\n"
389
- self._file_write(TAG_COMMENTS + txt)
276
+ self.file_writer.queue.put(txt)
390
277
 
391
278
  if len(comment_line) > 0:
392
- self._file_write(TAG_COMMENTS + comment_line + "\n")
279
+ self.file_writer.queue.put(comment_line + "\n")
393
280
 
394
281
  if varnames:
395
282
  write_forces = self.recording_settings.array_write_forces()
396
283
  write_trigger = self.recording_settings.array_write_trigger()
397
284
  write_deviceid = len(self.recording_settings.device_labels) > 1
398
- line = "time,delay,"
285
+ line = "time,"
399
286
  if write_deviceid:
400
287
  line += "device_tag,"
401
288
  for x in range(6):
@@ -405,9 +292,9 @@ class DataRecorder(object):
405
292
  line += "trigger1,"
406
293
  if write_trigger[1]:
407
294
  line += "trigger2,"
408
- self._file_write(line[:-1] + NEWLINE)
295
+ self.file_writer.queue.put(line[:-1] + "\n")
409
296
 
410
- return self.path_open_file
297
+ return file_path
411
298
 
412
299
  def close_data_file(self) -> None:
413
300
  """Close the data file
@@ -415,8 +302,8 @@ class DataRecorder(object):
415
302
  Afterwards data will not be saved anymore.
416
303
 
417
304
  """
418
-
419
- if self._file is not None:
420
- self._file.close()
421
- self._file = None
422
- self.path_open_file = Path("")
305
+ if isinstance(self.file_writer, FileWriter):
306
+ self.file_writer.close_file()
307
+ if self.file_writer.is_alive():
308
+ self.file_writer.join()
309
+ self.file_writer.filepath = Path("")
@@ -0,0 +1,110 @@
1
+ import bz2
2
+ from multiprocessing import Event, Process, Queue
3
+ from pathlib import Path
4
+ from queue import Empty
5
+
6
+ from .settings import RecordingSettings
7
+ from .types import (TAG_COMMENTS, TAG_DAQEVENT, TAG_UDPDATA, DAQEvents,
8
+ ForceSensorData, UDPData)
9
+
10
+ NEWLINE = "\n"
11
+ ENCODING = "utf-8"
12
+
13
+ class FileWriter(Process):
14
+ def __init__(
15
+ self, recording_settings: RecordingSettings, float_decimal_places: int = 4
16
+ ):
17
+ """To write to a file from multiple processes. Use FileWriter.queue.put(str) to write file"""
18
+
19
+ super().__init__()
20
+ self.filepath: Path = Path("")
21
+ self.append_mode = False
22
+ self.queue = Queue()
23
+ self._force_quit = Event()
24
+ self._close_file = Event()
25
+ self._write_forces = recording_settings.array_write_forces()
26
+ self._write_trigger = recording_settings.array_write_trigger()
27
+ self._write_deviceid = len(recording_settings.device_labels) > 1
28
+ self._decimal_places = float_decimal_places
29
+
30
+ def close_file(self):
31
+ """closes file after all pending writes are done and no further write occurred
32
+ for close_timeout seconds
33
+ """
34
+ self._close_file.set()
35
+
36
+ def force_quit(self):
37
+ """forces the process to quit immediately, even if there are pending writes in the queue"""
38
+ self._force_quit.set()
39
+
40
+ def start_recording(self, file_path: Path, append_mode: bool = False):
41
+ """opens file for writing, if file already exists, it will be overwritten (or appended if append_mode is True)"""
42
+ self.filepath = file_path
43
+ self.append_mode = append_mode
44
+ self.start()
45
+
46
+ # def join(self, timeout=None):
47
+ # super(FileWriter, self).join(timeout)
48
+
49
+ def run(self):
50
+
51
+ if self.filepath is None:
52
+ raise ValueError("File path is not set. Call start_recording() with a valid file path before running the process.")
53
+
54
+ float_format = "{0:." + str(self._decimal_places) + "f},"
55
+ if self.append_mode:
56
+ mode = "a"
57
+ else:
58
+ mode = "w"
59
+ if self.filepath.suffix.endswith("bz2"):
60
+ fl = bz2.open(self.filepath, mode)
61
+ else:
62
+ fl = open(self.filepath, mode, encoding=ENCODING)
63
+
64
+ self._close_file.clear()
65
+ self._force_quit.clear()
66
+
67
+ while not self._force_quit.is_set():
68
+
69
+ if self._close_file.is_set():
70
+ try:
71
+ d = self.queue.get_nowait()
72
+ except Empty:
73
+ break # quit process
74
+ else:
75
+ try:
76
+ d = self.queue.get(timeout=0.5)
77
+ except Empty:
78
+ continue # wait again for events
79
+
80
+ if isinstance(d, ForceSensorData):
81
+ txt = f"{d.time},"
82
+ if self._write_deviceid:
83
+ txt += f"{d.sensor_id},"
84
+ for x in d.forces[self._write_forces]:
85
+ txt += float_format.format(x)
86
+ for x in d.trigger[self._write_trigger]:
87
+ if isinstance(x, int):
88
+ txt += f"{x},"
89
+ else:
90
+ txt += float_format.format(x)
91
+ txt = txt[:-1] + NEWLINE
92
+
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
+ elif isinstance(d, str):
99
+ txt = f"{TAG_COMMENTS} {d}"
100
+ else:
101
+ continue # ignore unknown data types or maybe raise error (TODO)
102
+
103
+ if isinstance(fl, bz2.BZ2File):
104
+ fl.write(txt.encode(ENCODING))
105
+ else:
106
+ fl.write(txt)
107
+
108
+ fl.flush()
109
+ fl.close()
110
+
@@ -16,7 +16,7 @@ class ProcessPriorityManager(object):
16
16
  platform = sys.platform
17
17
  pybits = 32 + int(sys.maxsize > 2**32) * 32
18
18
  main_process_id = psutil.Process().pid
19
- _normal_nice_value = psutil.Process().nice() # usied on Linux
19
+ _normal_nice_value = psutil.Process().nice() # used on Linux
20
20
 
21
21
  def __init__(self):
22
22
  self._subprocs = []