sensor-sdk 1.3.3__py3-none-win_amd64.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.
sensor/sensor_data.py ADDED
@@ -0,0 +1,294 @@
1
+ import ctypes
2
+ import struct
3
+ from enum import IntEnum
4
+
5
+ from ._dll import sen_data_info_t
6
+
7
+ SAMPLE_SIZE = 40
8
+ _OFF_ABS_TIMESTAMP = 0
9
+ _OFF_CHANNEL_INDEX = 8
10
+ _OFF_SAMPLE_INDEX = 12
11
+ _OFF_RAW_DATA = 16
12
+ _OFF_DATA = 20
13
+ _OFF_IMPEDANCE = 24
14
+ _OFF_SATURATION = 28
15
+ _OFF_IS_LOST = 32
16
+ _SAMPLE_FMT = "<d3i3fB"
17
+
18
+ class Sample:
19
+ __slots__ = [
20
+ "rawData",
21
+ "data",
22
+ "impedance",
23
+ "saturation",
24
+ "sampleIndex",
25
+ "isLost",
26
+ "absTimeStampInSec",
27
+ "channelIndex",
28
+ ]
29
+
30
+ def __init__(self):
31
+ self.rawData = 0
32
+ self.data = 0.0
33
+ self.impedance = 0.0
34
+ self.saturation = 0.0
35
+ self.sampleIndex = 0
36
+ self.isLost = False
37
+ self.absTimeStampInSec = 0.0
38
+ self.channelIndex = 0
39
+
40
+ class DataType(IntEnum):
41
+ NTF_ACC = 0x1
42
+ NTF_GYRO = 0x2
43
+ NTF_EULER_DATA = 0x4
44
+ NTF_QUATERNION = 0x5
45
+ NTF_GEST = 0x07
46
+ NTF_EMG = 0x8
47
+ NTF_MAG_ANGLE_DATA = 0x0D
48
+ NTF_EEG = 0x10
49
+ NTF_ECG = 0x11
50
+ NTF_IMPEDANCE = 0x12
51
+ NTF_IMU = 0x13
52
+ NTF_ADS = 0x14
53
+ NTF_BRTH = 0x15
54
+ NTF_IMPEDANCE_EXT = 0x16
55
+ NTF_SPO2 = 0x17
56
+ NTF_PPG = 0x18
57
+
58
+ def _info_field(field_name, default):
59
+
60
+ @property
61
+ def prop(self):
62
+ i = self._read_info()
63
+ return getattr(i, field_name) if i is not None else default
64
+
65
+ @prop.setter
66
+ def prop(self, value):
67
+ setattr(self._ensure_own_info(), field_name, value)
68
+
69
+ return prop
70
+
71
+ class SensorData:
72
+ __slots__ = [
73
+ "_startSampleIndex",
74
+ "_info",
75
+ "_own_info",
76
+ "_buf",
77
+ "_view_start_time_stamp",
78
+ ]
79
+
80
+ def __init__(self):
81
+ self._startSampleIndex = 0
82
+ self._info = None
83
+ self._own_info = None
84
+ self._buf = b""
85
+ self._view_start_time_stamp = 0
86
+
87
+ @property
88
+ def startSampleIndex(self) -> int:
89
+ return self._startSampleIndex
90
+
91
+ def _read_info(self):
92
+ if self._own_info is not None:
93
+ return self._own_info
94
+ if self._info:
95
+ return self._info.contents
96
+ return None
97
+
98
+ def _ensure_own_info(self):
99
+ if self._own_info is None:
100
+ own = sen_data_info_t()
101
+ src = self._read_info()
102
+ if src is not None:
103
+ ctypes.memmove(ctypes.byref(own), ctypes.byref(src),
104
+ ctypes.sizeof(own))
105
+ self._own_info = own
106
+ return self._own_info
107
+
108
+ @property
109
+ def deviceMac(self) -> str:
110
+ i = self._read_info()
111
+ if i is None:
112
+ return ""
113
+ return bytes(i.deviceMac).split(b"\0", 1)[0].decode("utf-8", errors="replace")
114
+
115
+ @deviceMac.setter
116
+ def deviceMac(self, value: str):
117
+ raw = str(value).encode("utf-8", errors="replace")[:17]
118
+ mac = self._ensure_own_info().deviceMac
119
+ for idx, b in enumerate(raw):
120
+ mac[idx] = b
121
+
122
+ @property
123
+ def deviceName(self) -> str:
124
+ i = self._read_info()
125
+ if i is None:
126
+ return ""
127
+ return bytes(i.deviceName).split(b"\0", 1)[0].decode("utf-8", errors="replace")
128
+
129
+ @deviceName.setter
130
+ def deviceName(self, value: str):
131
+ raw = str(value).encode("utf-8", errors="replace")[:31]
132
+ name = self._ensure_own_info().deviceName
133
+ for idx, b in enumerate(raw):
134
+ name[idx] = b
135
+
136
+ @property
137
+ def dataType(self):
138
+ i = self._read_info()
139
+ if i is None:
140
+ return DataType.NTF_EEG
141
+ try:
142
+ return DataType(i.dataType)
143
+ except ValueError:
144
+ return i.dataType
145
+
146
+ @dataType.setter
147
+ def dataType(self, value):
148
+ self._ensure_own_info().dataType = int(value)
149
+
150
+ lostPackageCount = _info_field("lostPackageCount", 0)
151
+ sampleRate = _info_field("sampleRate", 0.0)
152
+ channelCount = _info_field("channelCount", 0)
153
+ channelMask = _info_field("channelMask", 0)
154
+ sampleCount = _info_field("sampleCount", 0)
155
+ startTimeStamp = _info_field("startTimeStamp", 0)
156
+ delay = _info_field("delay", 0)
157
+ startTimeSec = _info_field("startTimeSec", 0.0)
158
+
159
+ def getDeviceMac(self) -> str:
160
+ return self.deviceMac
161
+
162
+ def getDeviceName(self) -> str:
163
+ return self.deviceName
164
+
165
+ def getDataType(self):
166
+ return self.dataType
167
+
168
+ def getLostPackageCount(self) -> int:
169
+ return self.lostPackageCount
170
+
171
+ def getSampleRate(self) -> float:
172
+ return self.sampleRate
173
+
174
+ def getChannelCount(self) -> int:
175
+ return self.channelCount
176
+
177
+ def getChannelMask(self) -> int:
178
+ return self.channelMask
179
+
180
+ def getSampleCount(self) -> int:
181
+ return self.sampleCount
182
+
183
+ def getStartTimeStamp(self) -> int:
184
+ return self.startTimeStamp
185
+
186
+ def getDelay(self) -> int:
187
+ return self.delay
188
+
189
+ def getStartTimeSec(self) -> float:
190
+ return self.startTimeSec
191
+
192
+ def _slot_offset(self, channelIndex: int, sampleIndex: int) -> int:
193
+ return (channelIndex * self.sampleCount + sampleIndex) * SAMPLE_SIZE
194
+
195
+ def _check_slot(self, channelIndex: int, sampleIndex: int) -> int:
196
+ if (not self._buf
197
+ or channelIndex < 0 or channelIndex >= self.channelCount
198
+ or sampleIndex < 0 or sampleIndex >= self.sampleCount):
199
+ raise IndexError(
200
+ "sample (%d, %d) out of range (%d channels x %d samples)"
201
+ % (channelIndex, sampleIndex, self.channelCount, self.sampleCount))
202
+ return self._slot_offset(channelIndex, sampleIndex)
203
+
204
+ def isDataValid(self, channelIndex: int = 0, sampleIndex: int = 0) -> bool:
205
+ if (not self._buf
206
+ or channelIndex < 0 or channelIndex >= self.channelCount
207
+ or sampleIndex < 0 or sampleIndex >= self.sampleCount):
208
+ return False
209
+ if self.startTimeStamp != self._view_start_time_stamp:
210
+ return False
211
+ off = self._slot_offset(channelIndex, sampleIndex)
212
+ return (struct.unpack_from("<i", self._buf, off + _OFF_SAMPLE_INDEX)[0]
213
+ == self.startSampleIndex + sampleIndex)
214
+
215
+ def isChannelEnabled(self, channelIndex: int) -> bool:
216
+ if channelIndex < 0 or channelIndex >= 64:
217
+ return False
218
+ return ((self.channelMask >> channelIndex) & 1) != 0
219
+
220
+ def getChannelSample(self, channelIndex: int, sampleIndex: int) -> Sample:
221
+ off = self._check_slot(channelIndex, sampleIndex)
222
+ (abs_ts, ch, idx, raw, data, impedance, saturation, is_lost) = \
223
+ struct.unpack_from(_SAMPLE_FMT, self._buf, off)
224
+ s = Sample()
225
+ s.rawData = raw
226
+ s.data = data
227
+ s.impedance = impedance
228
+ s.saturation = saturation
229
+ s.sampleIndex = idx
230
+ s.isLost = is_lost != 0
231
+ s.absTimeStampInSec = abs_ts
232
+ s.channelIndex = ch
233
+ return s
234
+
235
+ def _read(self, fmt, field_off, channelIndex, sampleIndex):
236
+ off = self._check_slot(channelIndex, sampleIndex)
237
+ return struct.unpack_from(fmt, self._buf, off + field_off)[0]
238
+
239
+ def getTimeStampInMs(self, channelIndex: int, sampleIndex: int) -> int:
240
+ idx = self.getSampleIndex(channelIndex, sampleIndex)
241
+ rate = self.sampleRate
242
+ if rate <= 0:
243
+ return 0
244
+ return int(idx * 1000.0 / rate)
245
+
246
+ def getAbsTimeStampInSec(self, channelIndex: int, sampleIndex: int) -> float:
247
+ return self._read("<d", _OFF_ABS_TIMESTAMP, channelIndex, sampleIndex)
248
+
249
+ def getSampleIndex(self, channelIndex: int, sampleIndex: int) -> int:
250
+ return self._read("<i", _OFF_SAMPLE_INDEX, channelIndex, sampleIndex)
251
+
252
+ def getRawData(self, channelIndex: int, sampleIndex: int) -> int:
253
+ return self._read("<i", _OFF_RAW_DATA, channelIndex, sampleIndex)
254
+
255
+ def getData(self, channelIndex: int, sampleIndex: int) -> float:
256
+ return self._read("<f", _OFF_DATA, channelIndex, sampleIndex)
257
+
258
+ def getImpedance(self, channelIndex: int, sampleIndex: int) -> float:
259
+ return self._read("<f", _OFF_IMPEDANCE, channelIndex, sampleIndex)
260
+
261
+ def getSaturation(self, channelIndex: int, sampleIndex: int) -> float:
262
+ return self._read("<f", _OFF_SATURATION, channelIndex, sampleIndex)
263
+
264
+ def isLost(self, channelIndex: int, sampleIndex: int) -> bool:
265
+ return self._read("<B", _OFF_IS_LOST, channelIndex, sampleIndex) != 0
266
+
267
+ def as_numpy(self):
268
+ try:
269
+ import numpy as np
270
+ except ImportError as e:
271
+ raise ImportError(
272
+ "SensorData.as_numpy() requires numpy (pip install numpy); "
273
+ "the sensor package itself only needs the stdlib") from e
274
+ dtype = np.dtype({
275
+ "names": ["absTimeStampInSec", "channelIndex", "sampleIndex", "rawData",
276
+ "data", "impedance", "saturation", "isLost", "_pad"],
277
+ "formats": ["<f8", "<i4", "<i4", "<i4",
278
+ "<f4", "<f4", "<f4", "<u1", "V7"],
279
+ "offsets": [0, 8, 12, 16, 20, 24, 28, 32, 33],
280
+ "itemsize": SAMPLE_SIZE,
281
+ })
282
+ arr = np.frombuffer(self._buf, dtype=dtype)
283
+ return arr.reshape(self.channelCount, self.sampleCount)
284
+
285
+ def clone(self) -> "SensorData":
286
+ out = SensorData()
287
+ src = self._read_info()
288
+ if src is not None:
289
+ ctypes.memmove(ctypes.byref(out._ensure_own_info()),
290
+ ctypes.byref(src), ctypes.sizeof(sen_data_info_t))
291
+ out._startSampleIndex = self._startSampleIndex
292
+ out._view_start_time_stamp = self._view_start_time_stamp
293
+ out._buf = bytes(self._buf)
294
+ return out
@@ -0,0 +1,120 @@
1
+ from enum import Enum
2
+
3
+ from . import _dll
4
+
5
+ class DeviceInfo:
6
+ def __init__(self):
7
+ self.DeviceName = ""
8
+ self.ModelName = ""
9
+ self.HardwareVersion = ""
10
+ self.FirmwareVersion = ""
11
+ self.PpgChannelCount = 0
12
+ self.PpgSampleRate = 0
13
+ self.Spo2ChannelCount = 0
14
+ self.Spo2SampleRate = 0
15
+ self.ImpeChannelCount = 0
16
+ self.ImpeSampleRate = 0
17
+ self.EmgChannelCount = 0
18
+ self.EmgSampleRate = 0
19
+ self.EegChannelCount = 0
20
+ self.EegSampleRate = 0
21
+ self.EcgChannelCount = 0
22
+ self.EcgSampleRate = 0
23
+ self.EmgMaxSampleRate = 0
24
+ self.EegMaxSampleRate = 0
25
+ self.EcgMaxSampleRate = 0
26
+ self.ConnectionIntervalMs = 0.0
27
+ self.PeripheralLatency = -1
28
+ self.SupervisionTimeoutMs = 0
29
+ self.Backend = ""
30
+ self.AccChannelCount = 0
31
+ self.AccSampleRate = 0
32
+ self.GyroChannelCount = 0
33
+ self.GyroSampleRate = 0
34
+ self.BrthChannelCount = 0
35
+ self.BrthSampleRate = 0
36
+ self.MagAngleChannelCount = 0
37
+ self.MagAngleSampleRate = 0
38
+ self.EulerChannelCount = 0
39
+ self.EulerSampleRate = 0
40
+ self.QuatChannelCount = 0
41
+ self.QuatSampleRate = 0
42
+ self.ImuChannelCount = 0
43
+ self.ImuSampleRate = 0
44
+ self.MTUSize = 0
45
+ self.IsMTUFine = 0
46
+ self.EmgGain = 0
47
+ self.EegGain = 0
48
+ self.EcgGain = 0
49
+ self.GestChannelCount = 0
50
+ self.GestSampleRate = 0
51
+
52
+ @classmethod
53
+ def _from_c(cls, info: "_dll.sen_device_info_t") -> "DeviceInfo":
54
+ d = cls()
55
+ d.DeviceName = _decode(info.deviceName)
56
+ d.ModelName = _decode(info.modelName)
57
+ d.HardwareVersion = _decode(info.hardwareVersion)
58
+ d.FirmwareVersion = _decode(info.firmwareVersion)
59
+ d.PpgChannelCount = info.PpgChannelCount
60
+ d.PpgSampleRate = info.PpgSampleRate
61
+ d.Spo2ChannelCount = info.Spo2ChannelCount
62
+ d.Spo2SampleRate = info.Spo2SampleRate
63
+ d.ImpeChannelCount = info.ImpeChannelCount
64
+ d.ImpeSampleRate = info.ImpeSampleRate
65
+ d.EmgChannelCount = info.EMGChannelCount
66
+ d.EmgSampleRate = info.EMGSampleRate
67
+ d.EegChannelCount = info.EEGChannelCount
68
+ d.EegSampleRate = info.EEGSampleRate
69
+ d.EcgChannelCount = info.ECGChannelCount
70
+ d.EcgSampleRate = info.ECGSampleRate
71
+ d.EmgMaxSampleRate = info.EmgMaxSampleRate
72
+ d.EegMaxSampleRate = info.EegMaxSampleRate
73
+ d.EcgMaxSampleRate = info.EcgMaxSampleRate
74
+ d.ConnectionIntervalMs = info.ConnectionIntervalMs
75
+ d.PeripheralLatency = info.PeripheralLatency
76
+ d.SupervisionTimeoutMs = info.SupervisionTimeoutMs
77
+ d.Backend = _decode(info.backend)
78
+ d.AccChannelCount = info.AccChannelCount
79
+ d.AccSampleRate = info.AccSampleRate
80
+ d.GyroChannelCount = info.GyroChannelCount
81
+ d.GyroSampleRate = info.GyroSampleRate
82
+ d.BrthChannelCount = info.BRTHChannelCount
83
+ d.BrthSampleRate = info.BRTHSampleRate
84
+ d.MagAngleChannelCount = info.MagAngleChannelCount
85
+ d.MagAngleSampleRate = info.MagAngleSampleRate
86
+ d.EulerChannelCount = info.EulerChannelCount
87
+ d.EulerSampleRate = info.EulerSampleRate
88
+ d.QuatChannelCount = info.QuatChannelCount
89
+ d.QuatSampleRate = info.QuatSampleRate
90
+ d.ImuChannelCount = info.ImuChannelCount
91
+ d.ImuSampleRate = info.ImuSampleRate
92
+ d.MTUSize = info.MTUSize
93
+ d.IsMTUFine = info.isMTUFine
94
+ d.EmgGain = info.EMGGain
95
+ d.EegGain = info.EEGGain
96
+ d.EcgGain = info.ECGGain
97
+ d.GestChannelCount = info.GestChannelCount
98
+ d.GestSampleRate = info.GestSampleRate
99
+ return d
100
+
101
+ class DeviceStateEx(Enum):
102
+ Disconnected = 0
103
+ Connecting = 1
104
+ Connected = 2
105
+ Ready = 3
106
+ Disconnecting = 4
107
+ Invalid = 5
108
+
109
+ class BLEDevice:
110
+ def __init__(self, name: str, address: str, rssi: int):
111
+ self.Name = name
112
+ self.Address = address
113
+ self.RSSI = rssi
114
+
115
+ @classmethod
116
+ def _from_c(cls, dev: "_dll.sen_ble_device_t") -> "BLEDevice":
117
+ return cls(_decode(dev.name), _decode(dev.mac), dev.rssi)
118
+
119
+ def _decode(raw) -> str:
120
+ return bytes(raw).split(b"\0", 1)[0].decode("utf-8", errors="replace")