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.
- pyforcedaq/_lib/data_recorder.py +72 -193
- pyforcedaq/_lib/file_writer.py +110 -0
- pyforcedaq/_lib/sensor.py +12 -4
- pyforcedaq/_lib/sensor_process.py +78 -140
- pyforcedaq/_lib/types.py +8 -17
- pyforcedaq/force.py +1 -1
- pyforcedaq/gui/_gui_status.py +50 -45
- pyforcedaq/gui/_layout.py +48 -35
- pyforcedaq/gui/_level_indicator.py +4 -2
- pyforcedaq/gui/_run.py +20 -29
- {pyforcedaq-2.0.5.dev2.dist-info → pyforcedaq-2.0.5.dev3.dist-info}/METADATA +1 -1
- {pyforcedaq-2.0.5.dev2.dist-info → pyforcedaq-2.0.5.dev3.dist-info}/RECORD +14 -16
- {pyforcedaq-2.0.5.dev2.dist-info → pyforcedaq-2.0.5.dev3.dist-info}/WHEEL +1 -1
- pyforcedaq/extras/__init__.py +0 -0
- pyforcedaq/extras/convert.py +0 -275
- pyforcedaq/extras/read_force_data.py +0 -89
- {pyforcedaq-2.0.5.dev2.dist-info → pyforcedaq-2.0.5.dev3.dist-info}/entry_points.txt +0 -0
pyforcedaq/_lib/data_recorder.py
CHANGED
|
@@ -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 .
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
104
|
+
def has_file_writer(self):
|
|
113
105
|
"""Property indicates whether a data file is open"""
|
|
114
|
-
return self.
|
|
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.
|
|
121
|
-
except
|
|
112
|
+
return self.force_sensor_processes[0].is_alive()
|
|
113
|
+
except IndexError:
|
|
122
114
|
return False
|
|
123
115
|
|
|
124
116
|
@property
|
|
125
|
-
def
|
|
117
|
+
def is_saving(self):
|
|
126
118
|
"""Property indicates whether the recording is started or paused"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
179
|
+
def start_saving(self) -> None:
|
|
238
180
|
"""Start polling process and record
|
|
239
181
|
|
|
240
182
|
See Also
|
|
241
183
|
--------
|
|
242
|
-
|
|
184
|
+
is_saving
|
|
243
185
|
|
|
244
186
|
"""
|
|
245
187
|
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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.
|
|
281
|
-
fsp.
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
370
|
-
if
|
|
371
|
-
#
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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.
|
|
276
|
+
self.file_writer.queue.put(txt)
|
|
398
277
|
|
|
399
278
|
if len(comment_line) > 0:
|
|
400
|
-
self.
|
|
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,
|
|
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.
|
|
295
|
+
self.file_writer.queue.put(line[:-1] + "\n")
|
|
417
296
|
|
|
418
|
-
return
|
|
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
|
-
|
|
428
|
-
self.
|
|
429
|
-
|
|
430
|
-
self.
|
|
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
|
)
|