pyForceDAQ 2.0.0__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/__init__.py +43 -0
- pyforcedaq/__main__.py +42 -0
- pyforcedaq/_lib/__init__.py +1 -0
- pyforcedaq/_lib/lsl.py +56 -0
- pyforcedaq/_lib/misc.py +126 -0
- pyforcedaq/_lib/polling_time_profile.py +52 -0
- pyforcedaq/_lib/process_priority_manager.py +148 -0
- pyforcedaq/_lib/timer.py +45 -0
- pyforcedaq/_lib/types.py +400 -0
- pyforcedaq/_lib/udp_connection.py +326 -0
- pyforcedaq/daq/__init__.py +19 -0
- pyforcedaq/daq/_daq_read_Analog_pydaqmx.py +114 -0
- pyforcedaq/daq/_daq_read_analog_nidaqmx.py +84 -0
- pyforcedaq/daq/_mock_sensor.py +80 -0
- pyforcedaq/daq/_pyATIDAQ.py +306 -0
- pyforcedaq/daq/config.py +13 -0
- pyforcedaq/extras/__init__.py +0 -0
- pyforcedaq/extras/convert.py +275 -0
- pyforcedaq/extras/expyriment_daq_control.py +246 -0
- pyforcedaq/extras/opensesame_daq_control.py +280 -0
- pyforcedaq/extras/read_force_data.py +89 -0
- pyforcedaq/extras/remote_control.py +93 -0
- pyforcedaq/force/__init__.py +13 -0
- pyforcedaq/force/_log.py +18 -0
- pyforcedaq/force/data_recorder.py +400 -0
- pyforcedaq/force/sensor.py +200 -0
- pyforcedaq/force/sensor_process.py +251 -0
- pyforcedaq/gui/__init__.py +6 -0
- pyforcedaq/gui/_gui_status.py +306 -0
- pyforcedaq/gui/_layout.py +104 -0
- pyforcedaq/gui/_level_indicator.py +59 -0
- pyforcedaq/gui/_pg_surface.py +100 -0
- pyforcedaq/gui/_plotter.py +234 -0
- pyforcedaq/gui/_run.py +522 -0
- pyforcedaq/gui/_scaling.py +71 -0
- pyforcedaq/gui/_settings.py +98 -0
- pyforcedaq/gui/forceDAQ_logo.png +0 -0
- pyforcedaq/gui/launcher.py +249 -0
- pyforcedaq-2.0.0.dist-info/METADATA +15 -0
- pyforcedaq-2.0.0.dist-info/RECORD +42 -0
- pyforcedaq-2.0.0.dist-info/WHEEL +4 -0
- pyforcedaq-2.0.0.dist-info/entry_points.txt +3 -0
pyforcedaq/_lib/types.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
__author__ = 'Oliver Lindemann'
|
|
2
|
+
|
|
3
|
+
import ctypes as ct
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from .misc import MinMaxDetector as _MinMaxDetector
|
|
7
|
+
|
|
8
|
+
# tag in data output
|
|
9
|
+
TAG_COMMENTS = "#"
|
|
10
|
+
TAG_DAQEVENT = TAG_COMMENTS + "T"
|
|
11
|
+
TAG_UDPDATA = TAG_COMMENTS + "UDP"
|
|
12
|
+
|
|
13
|
+
CTYPE_FORCES = ct.c_float * 600
|
|
14
|
+
CTYPE_TRIGGER = ct.c_float * 2
|
|
15
|
+
|
|
16
|
+
class PollingPriority(object):
|
|
17
|
+
|
|
18
|
+
NORMAL = 'normal'
|
|
19
|
+
HIGH = 'high'
|
|
20
|
+
REALTIME = 'real_time'
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def get_priority(priority_str):
|
|
24
|
+
"""returns normal or the higher priority if detected """
|
|
25
|
+
if isinstance(priority_str, str):
|
|
26
|
+
if priority_str.find("real") >= 0 and \
|
|
27
|
+
priority_str.find("time") >= 0:
|
|
28
|
+
return PollingPriority.REALTIME
|
|
29
|
+
elif priority_str.startswith("high"):
|
|
30
|
+
return PollingPriority.HIGH
|
|
31
|
+
|
|
32
|
+
return PollingPriority.NORMAL
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CTypesForceSensorData(ct.Structure):
|
|
36
|
+
_fields_ = [("device_id", ct.c_int),
|
|
37
|
+
("time", ct.c_int),
|
|
38
|
+
("forces", CTYPE_FORCES),
|
|
39
|
+
("trigger", CTYPE_TRIGGER)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ForceSensorData(object):
|
|
43
|
+
"""The Force data structure with the following properties
|
|
44
|
+
* device_id
|
|
45
|
+
* time (time stamp)
|
|
46
|
+
* aquisition delay (time it took to receive the new data)
|
|
47
|
+
* Fx, Fy, & Fz
|
|
48
|
+
* Tx, Ty, & Tz
|
|
49
|
+
* trigger1 & trigger2
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
forces_names = ["Fx", "Fy", "Fz", "Tx", "Ty", "Tz"]
|
|
54
|
+
|
|
55
|
+
def __init__(self, time=0, acquisition_delay = -1,
|
|
56
|
+
forces= [0] * 6, trigger=(0, 0),
|
|
57
|
+
device_id=0, trigger_threshold=0.9, reverse=()):
|
|
58
|
+
"""Create a ForceSensorData object
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
device_id: int, optional
|
|
62
|
+
the id of the sensor device
|
|
63
|
+
time: int, optional
|
|
64
|
+
the timestamp
|
|
65
|
+
acquisition_delay: int, optional
|
|
66
|
+
time
|
|
67
|
+
forces: array of six floats
|
|
68
|
+
array of the force data defined as [Fx, Fy, Fz, Tx, Ty, Tz]
|
|
69
|
+
trigger: array of two floats
|
|
70
|
+
two trigger values: [trigger1, trigger2]
|
|
71
|
+
|
|
72
|
+
trigger_threshold: float (default = 0.4)
|
|
73
|
+
if abs(trigger1/2) < trigger_threshold the threshold it will considered as noise
|
|
74
|
+
and set to zero
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
self.time = time
|
|
79
|
+
self.acquisition_delay = acquisition_delay
|
|
80
|
+
self.device_id = device_id
|
|
81
|
+
self.forces = forces
|
|
82
|
+
self.trigger = list(trigger)
|
|
83
|
+
if abs(self.trigger[0]) < trigger_threshold:
|
|
84
|
+
self.trigger[0] = 0
|
|
85
|
+
if abs(self.trigger[1]) < trigger_threshold:
|
|
86
|
+
self.trigger[1] = 0
|
|
87
|
+
for r in reverse:
|
|
88
|
+
forces[r] = -1*forces[r]
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
"""converts data to string. """
|
|
92
|
+
txt = "%d,%d,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f" % (self.device_id,
|
|
93
|
+
self.time,
|
|
94
|
+
self.forces[0],
|
|
95
|
+
self.forces[1],
|
|
96
|
+
self.forces[2],
|
|
97
|
+
self.forces[3],
|
|
98
|
+
self.forces[4],
|
|
99
|
+
self.forces[5])
|
|
100
|
+
txt += ",%.4f,%.4f" % (self.trigger[0], self.trigger[1])
|
|
101
|
+
return txt
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def Fx(self):
|
|
105
|
+
return self.forces[0]
|
|
106
|
+
|
|
107
|
+
@Fx.setter
|
|
108
|
+
def Fx(self, value):
|
|
109
|
+
self.forces[0] = value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def Fy(self):
|
|
113
|
+
return self.forces[1]
|
|
114
|
+
|
|
115
|
+
@Fy.setter
|
|
116
|
+
def Fy(self, value):
|
|
117
|
+
self.forces[1] = value
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def Fz(self):
|
|
121
|
+
return self.forces[2]
|
|
122
|
+
|
|
123
|
+
@Fz.setter
|
|
124
|
+
def Fz(self, value):
|
|
125
|
+
self.forces[2] = value
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def Tx(self):
|
|
129
|
+
return self.forces[3]
|
|
130
|
+
|
|
131
|
+
@Tx.setter
|
|
132
|
+
def Tx(self, value):
|
|
133
|
+
self.forces[3] = value
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def Ty(self):
|
|
137
|
+
return self.forces[4]
|
|
138
|
+
|
|
139
|
+
@Ty.setter
|
|
140
|
+
def Ty(self, value):
|
|
141
|
+
self.forces[4] = value
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def Tz(self):
|
|
145
|
+
return self.forces[5]
|
|
146
|
+
|
|
147
|
+
@Tz.setter
|
|
148
|
+
def Tz(self, value):
|
|
149
|
+
self.forces[5] = value
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def ctypes_struct(self):
|
|
153
|
+
return CTypesForceSensorData(self.device_id, self.time,
|
|
154
|
+
CTYPE_FORCES(*self.forces), CTYPE_TRIGGER(*self.trigger))
|
|
155
|
+
|
|
156
|
+
@ctypes_struct.setter
|
|
157
|
+
def ctypes_struct(self, struct):
|
|
158
|
+
self.device_id = struct.device_id
|
|
159
|
+
self.time = struct.time
|
|
160
|
+
self.force = struct.forces
|
|
161
|
+
self.trigger = struct.trigger
|
|
162
|
+
|
|
163
|
+
def selected_forces(self, select: List[bool]):
|
|
164
|
+
"""Return an iterator over selected force values."""
|
|
165
|
+
return (force for i, force in enumerate(self.forces) if select[i])
|
|
166
|
+
|
|
167
|
+
def selected_trigger(self, select: List[bool]):
|
|
168
|
+
return (trigger for i, trigger in enumerate(self.trigger) if select[i])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class UDPData(object):
|
|
172
|
+
"""The UDP data class, used to store UDP DATA with timestamps
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, string, time):
|
|
177
|
+
"""Create a UDA_DATA object
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
time : int
|
|
182
|
+
code : numerical or string
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
self.time = time
|
|
186
|
+
if isinstance(string, str):
|
|
187
|
+
self.byte_string = string.encode()
|
|
188
|
+
else:
|
|
189
|
+
self.byte_string = string
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def unicode(self):
|
|
193
|
+
return self.byte_string.decode('utf-8', 'replace')
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def is_remote_control_command(self):
|
|
197
|
+
return self.startswith(GUIRemoteControlCommands.COMMAND_STR)
|
|
198
|
+
|
|
199
|
+
def startswith(self, byte_string):
|
|
200
|
+
return self.byte_string[:len(byte_string)] == byte_string
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def bytes_startswith(a, b):
|
|
204
|
+
return a[:len(b)] == b
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class DAQEvents(object):
|
|
208
|
+
"""The DAQEvents data class, used to store trigger
|
|
209
|
+
|
|
210
|
+
See Also
|
|
211
|
+
--------
|
|
212
|
+
DataRecorder.set_daq_event()
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(self, time, code):
|
|
217
|
+
"""Create a DAQEvents object
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
time : int
|
|
222
|
+
code : numerical or string
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
self.time = time
|
|
226
|
+
self.code = code
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class GUIRemoteControlCommands(object):
|
|
230
|
+
"""
|
|
231
|
+
SET_THRESHOLDS needs to be followed by a threshold object
|
|
232
|
+
SET_RESPONSE_MINMAX_DETECTION needs to be followed an integer representing duration of sampling
|
|
233
|
+
|
|
234
|
+
feedback:
|
|
235
|
+
CHANGED_LEVEL+int from SET_LEVEL_CHANGE_DETECTION
|
|
236
|
+
RESPONSE_MINMAX+(int, int) from SET_RESPONSE_MINMAX_DETECTION
|
|
237
|
+
VALUE+float from GET_FX, GET_FY, GET_FZ, GET_TX, GET_TY, GET_TZ,
|
|
238
|
+
|
|
239
|
+
see also UDPConnection constants!
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
#DOC REMOTECONTROL
|
|
243
|
+
|
|
244
|
+
COMMAND_STR = b"$"
|
|
245
|
+
# BASIC
|
|
246
|
+
START = COMMAND_STR + b"SRT"
|
|
247
|
+
PAUSE = COMMAND_STR + b"PSE"
|
|
248
|
+
PING = COMMAND_STR + b"PNG"
|
|
249
|
+
QUIT = COMMAND_STR + b"QUT"
|
|
250
|
+
#getter
|
|
251
|
+
GET_FX1 = COMMAND_STR + b"gFX1"
|
|
252
|
+
GET_FX2 = COMMAND_STR + b"gFX2"
|
|
253
|
+
GET_FY1 = COMMAND_STR + b"gFY1"
|
|
254
|
+
GET_FY2 = COMMAND_STR + b"gFY2"
|
|
255
|
+
GET_FZ1 = COMMAND_STR + b"gFZ1"
|
|
256
|
+
GET_FZ2 = COMMAND_STR + b"gFZ2"
|
|
257
|
+
GET_TX1 = COMMAND_STR + b"gTX1"
|
|
258
|
+
GET_TX2 = COMMAND_STR + b"gTX2"
|
|
259
|
+
GET_TY1 = COMMAND_STR + b"gTY1"
|
|
260
|
+
GET_TY2 = COMMAND_STR + b"gTY2"
|
|
261
|
+
GET_TZ1 = COMMAND_STR + b"gTZ1"
|
|
262
|
+
GET_TZ2 = COMMAND_STR + b"gTZ2"
|
|
263
|
+
GET_THRESHOLD_LEVEL =COMMAND_STR + b"gTL"
|
|
264
|
+
GET_THRESHOLD_LEVEL2 = COMMAND_STR + b"gTL2"
|
|
265
|
+
GET_VERSION = COMMAND_STR + b"gVR"
|
|
266
|
+
# setter
|
|
267
|
+
FILENAME = COMMAND_STR + b"sFN"
|
|
268
|
+
SET_THRESHOLDS = COMMAND_STR + b"sTH"
|
|
269
|
+
SET_LEVEL_CHANGE_DETECTION = COMMAND_STR + b"sCD1"
|
|
270
|
+
SET_LEVEL_CHANGE_DETECTION2 = COMMAND_STR + b"sCD2"
|
|
271
|
+
SET_RESPONSE_MINMAX_DETECTION = COMMAND_STR + b"sMD1"
|
|
272
|
+
SET_RESPONSE_MINMAX_DETECTION2 = COMMAND_STR + b"sMD2"
|
|
273
|
+
#feedback
|
|
274
|
+
FEEDBACK = COMMAND_STR + b"xFB"
|
|
275
|
+
VALUE = COMMAND_STR + b"xVL"
|
|
276
|
+
RESPONSE_MINMAX = COMMAND_STR + b"xRM1"
|
|
277
|
+
RESPONSE_MINMAX2 = COMMAND_STR + b"xRM2"
|
|
278
|
+
CHANGED_LEVEL = COMMAND_STR + b"xCL1"
|
|
279
|
+
CHANGED_LEVEL2 = COMMAND_STR + b"xCL2"
|
|
280
|
+
|
|
281
|
+
FEEDBACK_PAUSED = FEEDBACK + b"paused"
|
|
282
|
+
FEEDBACK_STARTED = FEEDBACK + b"started"
|
|
283
|
+
|
|
284
|
+
class Thresholds(object):
|
|
285
|
+
|
|
286
|
+
def __init__(self, thresholds, n_channels=1):
|
|
287
|
+
"""Thresholds for a one or multiple channels of data"""
|
|
288
|
+
self._thresholds = list(thresholds)
|
|
289
|
+
self._thresholds.sort()
|
|
290
|
+
self.set_number_of_channels(n_channels=n_channels)
|
|
291
|
+
|
|
292
|
+
def is_detecting(self, channels=0):
|
|
293
|
+
return self._minmax[channels] is not None or self._prev_level[channels] is not None
|
|
294
|
+
|
|
295
|
+
def is_level_change_detecting(self, channels=0):
|
|
296
|
+
return self._prev_level[channels] is not None
|
|
297
|
+
|
|
298
|
+
def is_response_minmax_detecting(self, channels=0):
|
|
299
|
+
return self._minmax[channels] is not None
|
|
300
|
+
|
|
301
|
+
def is_detecting_anything(self):
|
|
302
|
+
"""is detecting something in at least one channel"""
|
|
303
|
+
nn = lambda x:x is not None
|
|
304
|
+
return len(list(filter(nn, self._prev_level)))>0 or len(list(filter(nn, self._minmax)))>0
|
|
305
|
+
|
|
306
|
+
def set_number_of_channels(self, n_channels):
|
|
307
|
+
self._prev_level = [None] * n_channels
|
|
308
|
+
self._minmax = [None] * n_channels
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def thresholds(self):
|
|
312
|
+
return self._thresholds
|
|
313
|
+
|
|
314
|
+
def get_level(self, value):
|
|
315
|
+
"""return [int]
|
|
316
|
+
int: the level of current sensor value depending of thresholds (array)
|
|
317
|
+
|
|
318
|
+
return:
|
|
319
|
+
0 below smallest threshold
|
|
320
|
+
1 large first but small second threshold
|
|
321
|
+
..
|
|
322
|
+
x larger highest threshold (x=n thresholds)
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
level = None
|
|
326
|
+
cnt = 0
|
|
327
|
+
for cnt, x in enumerate(self._thresholds):
|
|
328
|
+
if value < x:
|
|
329
|
+
level = cnt
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
if level is None:
|
|
333
|
+
level = cnt + 1
|
|
334
|
+
return level
|
|
335
|
+
|
|
336
|
+
def set_level_change_detection(self, value, channel=0):
|
|
337
|
+
"""sets level change detection
|
|
338
|
+
returns: current level
|
|
339
|
+
"""
|
|
340
|
+
self._prev_level[channel] = self.get_level(value)
|
|
341
|
+
self._minmax[channel] = None
|
|
342
|
+
return self._prev_level[channel]
|
|
343
|
+
|
|
344
|
+
def get_level_change(self, value, channel=0):
|
|
345
|
+
"""return tuple with level_change (boolean) and current level (int)
|
|
346
|
+
if level change detection is switch on
|
|
347
|
+
|
|
348
|
+
Note: after detected level change detection is switched off!
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
if self._prev_level[channel] is None:
|
|
352
|
+
return None, None
|
|
353
|
+
|
|
354
|
+
current = self.get_level(value)
|
|
355
|
+
changed = (current != self._prev_level[channel])
|
|
356
|
+
if changed:
|
|
357
|
+
self._prev_level[channel] = None
|
|
358
|
+
return changed, current
|
|
359
|
+
|
|
360
|
+
def __str__(self):
|
|
361
|
+
return str(self._thresholds)
|
|
362
|
+
|
|
363
|
+
def set_response_minmax_detection(self, value, duration, channel=0):
|
|
364
|
+
"""Start response detection
|
|
365
|
+
Parameters detects minimum and maximum of the response
|
|
366
|
+
after first level change (length =number_of_samples)
|
|
367
|
+
|
|
368
|
+
value: start level
|
|
369
|
+
polled samples need to feed via get_response_minmax()
|
|
370
|
+
|
|
371
|
+
returns: current level
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
lv = self.get_level(value)
|
|
375
|
+
self._minmax[channel] = _MinMaxDetector(start_value=lv,
|
|
376
|
+
duration=duration)
|
|
377
|
+
self._prev_level[channel] = None
|
|
378
|
+
return lv
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def get_response_minmax(self, value, channel=0):
|
|
382
|
+
"""checks for response minimum and maximum if set_response_minmax_detection is switch on
|
|
383
|
+
With this function you add a sample and check if the response can be classified. If so,
|
|
384
|
+
it returns a tuple with the minimum and maximum response level during the response period
|
|
385
|
+
otherwise
|
|
386
|
+
returns None
|
|
387
|
+
|
|
388
|
+
tuple with level_change (boolean) and current level (int)
|
|
389
|
+
|
|
390
|
+
Note: after response minmax has been determined once response_minmax_detection is switched off!
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
if self._minmax[channel] is None:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
rtn = self._minmax[channel].process(self.get_level(value))
|
|
397
|
+
if rtn is not None:
|
|
398
|
+
# minmax just detected
|
|
399
|
+
self._minmax[channel] = None # switch off
|
|
400
|
+
return rtn
|