pyForceDAQ 2.0.5.dev2__py3-none-any.whl → 2.0.5.dev3__py3-none-any.whl

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.
@@ -6,34 +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
15
  from .. import constants
18
- from .clock import wait_ms
16
+ from .file_writer import FileWriter
19
17
  from .misc import set_logging
20
18
  from .process_priority_manager import ProcessPriorityManager
21
19
  from .sensor_process import SensorProcess
22
20
  from .settings import RecordingSettings, SensorSettings
23
- from .types import (
24
- TAG_COMMENTS,
25
- TAG_DAQEVENT,
26
- TAG_UDPDATA,
27
- DAQEvents,
28
- ForceSensorData,
29
- PollingPriority,
30
- UDPData,
31
- )
21
+ from .types import DAQEvents, ForceSensorData, PollingPriority
32
22
  from .udp_connection import UDPConnectionProcess
33
23
 
34
24
  set_logging(data_directory="data", log_file="recording.log")
35
25
 
36
- NEWLINE = "\n"
26
+ # FIXME LSL marker event for all events
37
27
 
38
28
 
39
29
  class DataRecorder(object):
@@ -61,9 +51,15 @@ class DataRecorder(object):
61
51
  force_sensor_settings = [force_sensor_settings]
62
52
 
63
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
64
60
 
65
61
  # create sensor processes
66
- self._force_sensor_processes = []
62
+ self.force_sensor_processes: List[SensorProcess] = []
67
63
  event_trigger = []
68
64
  for fs in force_sensor_settings:
69
65
  if not isinstance(fs, SensorSettings):
@@ -72,13 +68,12 @@ class DataRecorder(object):
72
68
  fst = SensorProcess(
73
69
  sensor_settings=fs,
74
70
  recording_settings=recording_settings,
71
+ file_writer_queue=queue,
75
72
  daq_type=constants.DAQ_TYPE,
76
- use_aiftt=constants.USE_AIFTT,
77
- pipe_buffered_data_after_pause=True,
78
- )
73
+ use_aiftt=constants.USE_AIFTT)
79
74
  fst.start()
80
75
  event_trigger.append(fst.event_trigger)
81
- self._force_sensor_processes.append(fst)
76
+ self.force_sensor_processes.append(fst)
82
77
 
83
78
  # create udp connection process
84
79
  if poll_udp_connection:
@@ -90,7 +85,7 @@ class DataRecorder(object):
90
85
  # process managing FIYME needed?
91
86
  self._proc_manager = ProcessPriorityManager()
92
87
  self._proc_manager.add_subprocess(self.udp)
93
- self._proc_manager.add_subprocess(self._force_sensor_processes)
88
+ self._proc_manager.add_subprocess(self.force_sensor_processes)
94
89
  if self.recording_settings.priority is not None:
95
90
  level = PollingPriority.NORMAL
96
91
  else:
@@ -102,37 +97,36 @@ class DataRecorder(object):
102
97
  )
103
98
  # logging.info("Subprocess priorities: {}".format(self._proc_manager.get_subprocess_priorities()))
104
99
 
105
- self._is_recording = False
106
- self._file = None
107
- self._daq_event = []
108
- self.path_open_file = Path("")
100
+ self._daq_event = [] # FIXME needed?
109
101
  atexit.register(self.quit)
110
102
 
111
103
  @property
112
- def is_saving_data(self):
104
+ def has_file_writer(self):
113
105
  """Property indicates whether a data file is open"""
114
- return self._file is not None
106
+ return isinstance(self.file_writer, FileWriter) and self.file_writer.is_alive()
115
107
 
116
108
  @property
117
109
  def is_alive(self):
118
110
  """Property indicates whether the recording processes are alive"""
119
111
  try:
120
- return self._force_sensor_processes[0].is_alive()
121
- except Exception:
112
+ return self.force_sensor_processes[0].is_alive()
113
+ except IndexError:
122
114
  return False
123
115
 
124
116
  @property
125
- def is_recording(self):
117
+ def is_saving(self):
126
118
  """Property indicates whether the recording is started or paused"""
127
- return self._is_recording
128
-
129
- @property
130
- def force_sensor_processes(self):
131
- 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
132
126
 
133
127
  @property
134
128
  def sensor_settings_list(self):
135
- 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))
136
130
 
137
131
  def quit(self) -> list | None:
138
132
  """Stop all recording processes, close data file and quit recording
@@ -146,20 +140,16 @@ class DataRecorder(object):
146
140
  if not self.is_alive:
147
141
  return
148
142
 
149
- buffer = self.pause_recording()
143
+ self.pause_saving()
144
+ for fsp in self.force_sensor_processes:
145
+ fsp.join()
150
146
  self.close_data_file()
151
147
 
152
148
  if self.udp is not None:
153
149
  self.udp.quit()
154
150
 
155
- # wait that all processes are quitted
156
- for fsp in self._force_sensor_processes:
157
- fsp.join()
158
-
159
151
  logging.info("Quit recording")
160
152
 
161
- return buffer
162
-
163
153
  def process_and_write_udp_events(self) -> list:
164
154
  """process udp events and return them"""
165
155
  buffer = []
@@ -170,59 +160,11 @@ class DataRecorder(object):
170
160
  # until queue empty or no udp connection
171
161
  break
172
162
  buffer.append(data)
173
- if len(buffer) > 0:
174
- self._write_data(buffer)
175
- return buffer
176
163
 
177
- def _write_data(
178
- self, data_buffer: list, recording_screen=None, float_decimal_places: int = 4
179
- ) -> None:
180
- """writes data to disk and set counters
181
-
182
- ignores UDP remote control commands
183
- """
184
- # DOC output format
185
-
186
- BLOCKSIZE = 10000 # for recording screen feedback only
187
- write_forces = self.recording_settings.array_write_forces()
188
- write_trigger = self.recording_settings.array_write_trigger()
189
- write_deviceid = len(self.recording_settings.device_labels) > 1
190
- float_format = "{0:." + str(float_decimal_places) + "f},"
191
- buffer_len = len(data_buffer)
192
- for c, d in enumerate(data_buffer):
193
- if self._file is not None:
194
- if isinstance(d, ForceSensorData):
195
- line = f"{d.time}, {d.acquisition_delay},"
196
- if write_deviceid:
197
- line += f"{d.sensor_id},"
198
- for x in d.forces[write_forces]:
199
- line += float_format.format(x)
200
- for x in d.trigger[write_trigger]:
201
- if isinstance(x, int):
202
- line += f"{x},"
203
- else:
204
- line += float_format.format(x)
205
- self._file_write(line[:-1] + NEWLINE)
206
-
207
- elif isinstance(d, DAQEvents):
208
- self._file_write(f"{TAG_DAQEVENT},{d.time},{str(d.code)}" + NEWLINE)
209
-
210
- elif isinstance(d, UDPData):
211
- self._file_write(f"{TAG_UDPDATA},{d.time},{d.unicode}" + NEWLINE)
212
-
213
- if recording_screen is not None and c % BLOCKSIZE == 0:
214
- recording_screen.stimulus(
215
- "Saving {0} of {1} blocks".format(
216
- c // BLOCKSIZE, buffer_len // BLOCKSIZE
217
- )
218
- ).present()
219
-
220
- def _file_write(self, s: str) -> None:
221
-
222
- if isinstance(self._file, gzip.GzipFile):
223
- self._file.write(s.encode("utf-8"))
224
- elif isinstance(self._file, TextIOWrapper):
225
- 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
226
168
 
227
169
  def store_daq_event(
228
170
  self, code: str | int | float, time: float | None = None
@@ -234,35 +176,20 @@ class DataRecorder(object):
234
176
  """
235
177
  self._daq_event.append(DAQEvents(time=time, code=code))
236
178
 
237
- def start_recording(self, determine_bias: bool = False) -> None:
179
+ def start_saving(self) -> None:
238
180
  """Start polling process and record
239
181
 
240
182
  See Also
241
183
  --------
242
- is_recording
184
+ is_saving
243
185
 
244
186
  """
245
187
 
246
- if determine_bias:
247
- self.determine_biases(n_samples=1000)
248
-
249
- if len(
250
- list(
251
- filter(
252
- lambda x: x.event_bias_is_available.is_set(),
253
- self._force_sensor_processes,
254
- )
255
- )
256
- ) != len(self._force_sensor_processes):
257
- raise RuntimeError(
258
- "Sensors can't be started before bias has been determined."
259
- )
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()
260
191
 
261
- # start polling
262
- list(map(lambda x: x.start_polling(), self._force_sensor_processes))
263
- self._is_recording = True
264
-
265
- def pause_recording(self, recording_screen=None) -> list:
192
+ def pause_saving(self):
266
193
  """Pauses all polling processes and process data
267
194
 
268
195
  returns
@@ -270,59 +197,17 @@ class DataRecorder(object):
270
197
  data : all last data
271
198
 
272
199
  """
273
- self._is_recording = False
274
-
275
- data = []
276
- if recording_screen is not None:
277
- recording_screen.stimulus("").present()
278
200
 
279
201
  # pause polling
280
- for fsp in self._force_sensor_processes:
281
- fsp.pause_polling()
282
-
283
- wait_ms(500)
284
-
285
- if recording_screen is not None:
286
- if self.is_saving_data:
287
- recording_screen.stimulus("saving data ...").present()
288
- else:
289
- recording_screen.stimulus("pause recording").present()
202
+ for fsp in self.force_sensor_processes:
203
+ fsp.pause_saving()
290
204
 
291
- # get data
292
- for fsp in self._force_sensor_processes:
293
- buffer = fsp.get_buffer()
294
- self._write_data(buffer, recording_screen)
295
- data.extend(buffer)
296
205
 
297
- # udp event
298
- data.extend(self.process_and_write_udp_events())
299
-
300
- # soft trigger
301
- self._write_data(self._daq_event)
302
- data.extend(self._daq_event)
303
- self._daq_event = []
304
- return data
305
-
306
- def determine_biases(self, n_samples: int) -> None:
307
- """Record n data samples (n_samples) to determine bias.
308
- Afterwards recording is in pause mode
309
-
310
- Notes
311
- -----
312
- The function take some time to be processed
313
-
314
- See Also
315
- --------
316
- Sensor.determine_bias()
317
-
318
- """
319
-
320
- self.pause_recording()
321
- for x in self._force_sensor_processes:
322
- x.determine_bias(n_samples=n_samples)
323
-
324
- for x in self._force_sensor_processes:
325
- 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()
326
211
 
327
212
  def open_data_file(
328
213
  self,
@@ -330,7 +215,7 @@ class DataRecorder(object):
330
215
  subdirectory: str = "data",
331
216
  varnames: bool = True,
332
217
  comment_line: str = "",
333
- ) -> Path:
218
+ ) -> Path | None:
334
219
  """Create a data file
335
220
 
336
221
  Only if data file has been opened, data will be saved!
@@ -355,20 +240,22 @@ class DataRecorder(object):
355
240
  full path the actually used file (incl. timestamp)
356
241
 
357
242
  """
358
- self.close_data_file()
243
+
244
+ if not isinstance(self.file_writer, FileWriter):
245
+ return
359
246
 
360
247
  # create filename
361
248
  data_dir = Path.cwd() / subdirectory
362
249
  data_dir.mkdir(exist_ok=True)
250
+
363
251
  if self.recording_settings.zip_data:
364
- filename = Path(filename).with_suffix(".csv.gz")
252
+ filename = Path(filename).with_suffix(".csv.bz2")
365
253
  else:
366
254
  filename = Path(filename).with_suffix(".csv")
367
-
368
255
  while True:
369
- self.path_open_file = data_dir / filename
370
- if self.path_open_file.is_file():
371
- # 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
372
259
  filename = Path(
373
260
  filename.stem
374
261
  + "_"
@@ -378,32 +265,24 @@ class DataRecorder(object):
378
265
  else:
379
266
  break
380
267
 
381
- if self.recording_settings.zip_data:
382
- self._file = gzip.open(self.path_open_file, "w")
383
- else:
384
- self._file = open(self.path_open_file, "w")
385
- print("Data file: {}".format(self.path_open_file))
386
-
387
- self._file_write(
388
- TAG_COMMENTS
389
- + "Recorded at {0} with pyForceDAQ {1}\n".format(
390
- asctime(localtime()), forceDAQVersion
391
- )
392
- )
393
- 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")
394
273
 
395
274
  for s in self.sensor_settings_list:
396
275
  txt = f" Sensor: label={s.device_label}, cal-file={s.calibration_file}\n"
397
- self._file_write(TAG_COMMENTS + txt)
276
+ self.file_writer.queue.put(txt)
398
277
 
399
278
  if len(comment_line) > 0:
400
- self._file_write(TAG_COMMENTS + comment_line + "\n")
279
+ self.file_writer.queue.put(comment_line + "\n")
401
280
 
402
281
  if varnames:
403
282
  write_forces = self.recording_settings.array_write_forces()
404
283
  write_trigger = self.recording_settings.array_write_trigger()
405
284
  write_deviceid = len(self.recording_settings.device_labels) > 1
406
- line = "time,delay,"
285
+ line = "time,"
407
286
  if write_deviceid:
408
287
  line += "device_tag,"
409
288
  for x in range(6):
@@ -413,9 +292,9 @@ class DataRecorder(object):
413
292
  line += "trigger1,"
414
293
  if write_trigger[1]:
415
294
  line += "trigger2,"
416
- self._file_write(line[:-1] + NEWLINE)
295
+ self.file_writer.queue.put(line[:-1] + "\n")
417
296
 
418
- return self.path_open_file
297
+ return file_path
419
298
 
420
299
  def close_data_file(self) -> None:
421
300
  """Close the data file
@@ -423,8 +302,8 @@ class DataRecorder(object):
423
302
  Afterwards data will not be saved anymore.
424
303
 
425
304
  """
426
-
427
- if self._file is not None:
428
- self._file.close()
429
- self._file = None
430
- 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
+
pyforcedaq/_lib/sensor.py CHANGED
@@ -6,6 +6,7 @@ See COPYING file distributed along with the pyForceDAQ copyright and license ter
6
6
  __author__ = "Oliver Lindemann"
7
7
 
8
8
  import numpy as np
9
+ from numpy import typing as npt
9
10
 
10
11
  from .. import constants
11
12
  from .clock import local_clock
@@ -67,6 +68,15 @@ class Sensor(object):
67
68
  continue
68
69
  self._reverse_vector[idx] = -1
69
70
 
71
+ def set_bias(self, data: npt.NDArray):
72
+ """sets the bias"""
73
+
74
+ if np.shape(data)[1] != len(Sensor.SENSOR_CHANNELS):
75
+ raise ValueError(f"biasdata should have the shape (x, n_sensor_channels)")
76
+
77
+ if self._calib_converter is not None:
78
+ self._calib_converter.bias(np.mean(data, axis=0))
79
+
70
80
  def determine_bias(self, n_samples=100):
71
81
  """determines the bias"""
72
82
 
@@ -102,8 +112,8 @@ class Sensor(object):
102
112
 
103
113
  """
104
114
 
105
- start = local_clock()
106
115
  data, _read_samples = self.daq.read_analog()
116
+ t = local_clock()
107
117
  if self.convert_to_FT and self._calib_converter is not None:
108
118
  forces = np.asarray(
109
119
  self._calib_converter.convertToFT(voltages=data[Sensor.SENSOR_CHANNELS])
@@ -114,12 +124,10 @@ class Sensor(object):
114
124
 
115
125
  # reverse scaling if needed
116
126
  forces = forces * self._reverse_vector
117
- t = local_clock()
118
127
 
119
128
  return ForceSensorData(
120
129
  time=t,
121
- acquisition_delay=t - start,
122
130
  sensor_id=self.sensor_id,
123
131
  forces=forces,
124
- trigger=data[Sensor.TRIGGER_CHANNELS],
132
+ trigger=data[Sensor.TRIGGER_CHANNELS]
125
133
  )