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/__init__.py +34 -0
- sensor/_dll.py +381 -0
- sensor/_sync.py +85 -0
- sensor/bin/sensor.dll +0 -0
- sensor/bin/sensor_ble_helper.exe +0 -0
- sensor/pyinstaller_hooks/__init__.py +4 -0
- sensor/pyinstaller_hooks/hook-sensor.py +3 -0
- sensor/sensor_controller.py +666 -0
- sensor/sensor_data.py +294 -0
- sensor/sensor_device.py +120 -0
- sensor/sensor_profile.py +674 -0
- sensor_sdk-1.3.3.dist-info/METADATA +23 -0
- sensor_sdk-1.3.3.dist-info/RECORD +16 -0
- sensor_sdk-1.3.3.dist-info/WHEEL +5 -0
- sensor_sdk-1.3.3.dist-info/entry_points.txt +2 -0
- sensor_sdk-1.3.3.dist-info/top_level.txt +1 -0
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
|
sensor/sensor_device.py
ADDED
|
@@ -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")
|