qlsdk2 0.1.7__py3-none-any.whl → 0.2.0a1__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.
- qlsdk/ar4m/ar4sdk.py +635 -35
- qlsdk/ar4m/persist.py +178 -0
- {qlsdk2-0.1.7.dist-info → qlsdk2-0.2.0a1.dist-info}/METADATA +3 -1
- qlsdk2-0.2.0a1.dist-info/RECORD +10 -0
- qlsdk2-0.1.7.dist-info/RECORD +0 -9
- {qlsdk2-0.1.7.dist-info → qlsdk2-0.2.0a1.dist-info}/WHEEL +0 -0
- {qlsdk2-0.1.7.dist-info → qlsdk2-0.2.0a1.dist-info}/top_level.txt +0 -0
qlsdk/ar4m/ar4sdk.py
CHANGED
|
@@ -3,12 +3,15 @@ from multiprocessing import Queue
|
|
|
3
3
|
import platform
|
|
4
4
|
from ctypes import (
|
|
5
5
|
c_int, c_int32, c_int64, c_uint32, c_uint64,
|
|
6
|
-
c_char, c_char_p, c_void_p,
|
|
6
|
+
c_char, c_char_p, c_void_p, c_double,
|
|
7
7
|
Structure, POINTER, CFUNCTYPE
|
|
8
8
|
)
|
|
9
|
+
from typing import Literal
|
|
9
10
|
from loguru import logger
|
|
10
11
|
from time import sleep, time
|
|
11
12
|
import os
|
|
13
|
+
import numpy as np
|
|
14
|
+
from .persist import EdfHandler
|
|
12
15
|
|
|
13
16
|
real_path = os.path.realpath(__file__)
|
|
14
17
|
dll_path = f'{os.path.dirname(real_path)}/libs/libAr4SDK.dll'
|
|
@@ -89,10 +92,34 @@ _dll.ar4_sdk_start_acq.restype = c_int
|
|
|
89
92
|
_dll.ar4_sdk_stop_acq.argtypes = [c_void_p]
|
|
90
93
|
_dll.ar4_sdk_stop_acq.restype = c_int
|
|
91
94
|
_dll.ar4_sdk_get_record_sample_rate.argtypes = [c_void_p]
|
|
92
|
-
_dll.ar4_sdk_get_record_sample_rate.restype =
|
|
95
|
+
_dll.ar4_sdk_get_record_sample_rate.restype = c_double
|
|
93
96
|
_dll.ar4_sdk_get_acq_start_time.argtypes = [c_void_p]
|
|
94
97
|
_dll.ar4_sdk_get_acq_start_time.restype = c_int64
|
|
95
98
|
|
|
99
|
+
# eeg信号物理量转换
|
|
100
|
+
_dll.ar4_sdk_get_eeg_phy_max.argtypes = [c_void_p]
|
|
101
|
+
_dll.ar4_sdk_get_eeg_phy_max.restype = c_double
|
|
102
|
+
_dll.ar4_sdk_get_eeg_phy_min.argtypes = [c_void_p]
|
|
103
|
+
_dll.ar4_sdk_get_eeg_phy_min.restype = c_double
|
|
104
|
+
_dll.ar4_sdk_get_eeg_digtal_max.argtypes = [c_void_p]
|
|
105
|
+
_dll.ar4_sdk_get_eeg_digtal_max.restype = c_int32
|
|
106
|
+
_dll.ar4_sdk_get_eeg_digtal_min.argtypes = [c_void_p]
|
|
107
|
+
_dll.ar4_sdk_get_eeg_digtal_min.restype = c_int32
|
|
108
|
+
_dll.ar4_sdk_get_eeg_phy_unit.argtypes = [c_void_p]
|
|
109
|
+
_dll.ar4_sdk_get_eeg_phy_unit.restype = c_char_p
|
|
110
|
+
|
|
111
|
+
# acc信号物理量转换
|
|
112
|
+
_dll.ar4_sdk_get_acc_phy_max.argtypes = [c_void_p]
|
|
113
|
+
_dll.ar4_sdk_get_acc_phy_max.restype = c_double
|
|
114
|
+
_dll.ar4_sdk_get_acc_phy_min.argtypes = [c_void_p]
|
|
115
|
+
_dll.ar4_sdk_get_acc_phy_min.restype = c_double
|
|
116
|
+
_dll.ar4_sdk_get_acc_digtal_max.argtypes = [c_void_p]
|
|
117
|
+
_dll.ar4_sdk_get_acc_digtal_max.restype = c_int32
|
|
118
|
+
_dll.ar4_sdk_get_acc_digtal_min.argtypes = [c_void_p]
|
|
119
|
+
_dll.ar4_sdk_get_acc_digtal_min.restype = c_int32
|
|
120
|
+
_dll.ar4_sdk_get_acc_phy_unit.argtypes = [c_void_p]
|
|
121
|
+
_dll.ar4_sdk_get_acc_phy_unit.restype = c_char_p
|
|
122
|
+
|
|
96
123
|
# 回调注册
|
|
97
124
|
_dll.ar4_sdk_register_data_notify.argtypes = [c_void_p, FuncAr4DataNotify]
|
|
98
125
|
_dll.ar4_sdk_register_data_notify.restype = None
|
|
@@ -136,18 +163,17 @@ class AR4SDK:
|
|
|
136
163
|
|
|
137
164
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
138
165
|
pass
|
|
139
|
-
|
|
140
166
|
|
|
141
167
|
# 读取系统当前时间(ms)
|
|
142
168
|
def _get_time():
|
|
143
169
|
cur_time = int(round(time()) * 1000)
|
|
144
|
-
logger.debug(f"_get_time is {cur_time}")
|
|
145
170
|
return cur_time
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class AR4(object):
|
|
171
|
+
|
|
172
|
+
class LMDevice(object):
|
|
149
173
|
def __init__(self, box_mac:str, slot:int, hub_name:str):
|
|
174
|
+
# 设备句柄
|
|
150
175
|
self._handle = None
|
|
176
|
+
# 设备基本信息
|
|
151
177
|
self._box_type = None
|
|
152
178
|
self._box_mac = box_mac
|
|
153
179
|
self._box_id = None
|
|
@@ -166,13 +192,41 @@ class AR4(object):
|
|
|
166
192
|
self._conn_time = None
|
|
167
193
|
self._last_time = None
|
|
168
194
|
|
|
195
|
+
self._recording = False
|
|
196
|
+
self._record_start_time = None
|
|
197
|
+
|
|
198
|
+
## eeg 数据
|
|
199
|
+
self._eeg_phy_max = None
|
|
200
|
+
self._eeg_phy_min = None
|
|
201
|
+
self._eeg_dig_max = None
|
|
202
|
+
self._eeg_dig_min = None
|
|
203
|
+
self._eeg_phy_unit = 'uV'
|
|
204
|
+
self._eeg_phy_range = None
|
|
205
|
+
self._eeg_dig_range = None
|
|
206
|
+
## acc 数据
|
|
207
|
+
self._acc_phy_max = None
|
|
208
|
+
self._acc_phy_min = None
|
|
209
|
+
self._acc_dig_max = None
|
|
210
|
+
self._acc_dig_min = None
|
|
211
|
+
self._acc_phy_unit = 'mG'
|
|
212
|
+
self._acc_phy_range = None
|
|
213
|
+
self._acc_dig_range = None
|
|
214
|
+
|
|
169
215
|
self._acq_info = {}
|
|
216
|
+
# 订阅者列表,数值为数字信号值
|
|
217
|
+
self._dig_subscriber: dict[str, Queue] = {}
|
|
218
|
+
# 订阅者列表,数值为物理信号值
|
|
219
|
+
self._phy_subscriber: dict[str, Queue] = {}
|
|
220
|
+
|
|
170
221
|
# 回调函数
|
|
171
222
|
self._data_callback = FuncAr4DataNotify(self._wrap_data_accept())
|
|
172
223
|
self._trigger_callback = FuncAr4TriggerNotify(self._wrap_trigger_accept())
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
224
|
+
self._connected_notify_callback = FuncAr4RecorderConnected(self._wrap_connected_notify())
|
|
225
|
+
self._disconnected_notify_callback = FuncAr4RecorderDisconnected(self._wrap_disconnected_notify())
|
|
226
|
+
self._box_connected_notify_callback = FuncAr4RecorderConnected(self._wrap_box_connected_notify())
|
|
227
|
+
self._box_disconnected_notify_callback = FuncAr4RecorderDisconnected(self._wrap_box_disconnected_notify())
|
|
228
|
+
self._start_record_notify_callback = FuncAr4RecorderConnected(self._wrap_start_record_notify())
|
|
229
|
+
self._stop_record_notify_callback = FuncAr4RecorderDisconnected(self._wrap_stop_record_notify())
|
|
176
230
|
|
|
177
231
|
@property
|
|
178
232
|
def box_mac(self):
|
|
@@ -187,6 +241,34 @@ class AR4(object):
|
|
|
187
241
|
# logger.info(f"init ar4 {self.box_mac}")
|
|
188
242
|
if not self._handle:
|
|
189
243
|
self._connected = self.connect()
|
|
244
|
+
## eeg 参数
|
|
245
|
+
self._eeg_phy_max = _dll.ar4_sdk_get_eeg_phy_max(self._handle)
|
|
246
|
+
self._eeg_phy_min = _dll.ar4_sdk_get_eeg_phy_min(self._handle)
|
|
247
|
+
self._eeg_dig_max = _dll.ar4_sdk_get_eeg_digtal_max(self._handle)
|
|
248
|
+
self._eeg_dig_min = _dll.ar4_sdk_get_eeg_digtal_min(self._handle)
|
|
249
|
+
self._eeg_phy_range = self._eeg_phy_max - self._eeg_phy_min
|
|
250
|
+
self._eeg_dig_range = self._eeg_dig_max - self._eeg_dig_min
|
|
251
|
+
eeg_unit = _dll.ar4_sdk_get_eeg_phy_unit(self._handle)
|
|
252
|
+
if eeg_unit:
|
|
253
|
+
try:
|
|
254
|
+
self._eeg_phy_unit = eeg_unit.decode("utf-8")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"ar4 {self._box_mac} 获取eeg物理单位异常: {str(e)}")
|
|
257
|
+
|
|
258
|
+
## acc 参数
|
|
259
|
+
self._acc_phy_max = _dll.ar4_sdk_get_acc_phy_max(self._handle)
|
|
260
|
+
self._acc_phy_min = _dll.ar4_sdk_get_acc_phy_min(self._handle)
|
|
261
|
+
self._acc_dig_max = _dll.ar4_sdk_get_acc_digtal_max(self._handle)
|
|
262
|
+
self._acc_dig_min = _dll.ar4_sdk_get_acc_digtal_min(self._handle)
|
|
263
|
+
self._acc_phy_range = self._acc_phy_max - self._acc_phy_min
|
|
264
|
+
self._acc_dig_range = self._acc_dig_max - self._acc_dig_min
|
|
265
|
+
acc_unit = _dll.ar4_sdk_get_acc_phy_unit(self._handle)
|
|
266
|
+
if acc_unit:
|
|
267
|
+
try:
|
|
268
|
+
self._acc_phy_unit = eeg_unit.decode("utf-8")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"ar4 {self._box_mac} 获取acc物理单位异常: {str(e)}")
|
|
271
|
+
|
|
190
272
|
if self._connected:
|
|
191
273
|
self._conn_time = _get_time()
|
|
192
274
|
self.get_box_name()
|
|
@@ -194,7 +276,18 @@ class AR4(object):
|
|
|
194
276
|
self.get_record_conn_state()
|
|
195
277
|
self._last_time = _get_time()
|
|
196
278
|
logger.debug(self)
|
|
279
|
+
self._register_callback()
|
|
197
280
|
return True
|
|
281
|
+
|
|
282
|
+
def _register_callback(self):
|
|
283
|
+
try:
|
|
284
|
+
_dll.ar4_sdk_register_data_notify(self._handle, self._data_callback)
|
|
285
|
+
_dll.ar4_sdk_register_trigger_notify(self._handle, self._trigger_callback)
|
|
286
|
+
_dll.ar4_sdk_register_conn_notify(self._handle, self._connected_notify_callback, self._disconnected_notify_callback)
|
|
287
|
+
_dll.ar4_sdk_register_record_state_notify(self._handle, self._start_record_notify_callback, self._stop_record_notify_callback)
|
|
288
|
+
_dll.ar4_sdk_register_box_conn_notify(self._handle, self._box_connected_notify_callback, self._box_disconnected_notify_callback)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"回调函数注册异常: {str(e)}")
|
|
198
291
|
|
|
199
292
|
def update_info(self):
|
|
200
293
|
self._last_time = _get_time()
|
|
@@ -244,17 +337,6 @@ class AR4(object):
|
|
|
244
337
|
self._acq_info["start_time"] = _get_time()
|
|
245
338
|
|
|
246
339
|
if self._handle:
|
|
247
|
-
# 设置信号数据回调
|
|
248
|
-
try:
|
|
249
|
-
_dll.ar4_sdk_register_data_notify(self._handle, self._data_callback)
|
|
250
|
-
except Exception as e:
|
|
251
|
-
logger.error(f"ar4 {self._box_mac} 停止采集异常: {str(e)}")
|
|
252
|
-
|
|
253
|
-
# 设置trigger数据回调
|
|
254
|
-
# try:
|
|
255
|
-
# _dll.ar4_sdk_register_trigger_notify(self._handle, self._trigger_callback)
|
|
256
|
-
# except Exception as e:
|
|
257
|
-
# logger.error(f"ar4 {self._box_mac} 停止采集异常: {str(e)}")
|
|
258
340
|
# 启动采集
|
|
259
341
|
try:
|
|
260
342
|
logger.debug(f"ar4 {self._box_mac} 启动采集: {self._handle}")
|
|
@@ -305,15 +387,22 @@ class AR4(object):
|
|
|
305
387
|
logger.error(f"ar4 {self._box_mac} 获取采样开始时间异常: {str(e)}")
|
|
306
388
|
|
|
307
389
|
# 订阅推送消息
|
|
308
|
-
def subscribe(self, topic: str = None, q: Queue = Queue()):
|
|
390
|
+
def subscribe(self, topic: str = None, q: Queue = Queue(), value_type: Literal['phy', 'dig'] = 'phy') -> tuple[str, Queue]:
|
|
309
391
|
if topic is None:
|
|
310
392
|
topic = f"msg_{_get_time()}"
|
|
311
|
-
if topic in list(self._consumers.keys()):
|
|
312
|
-
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
313
|
-
return topic, self._consumers[topic]
|
|
314
|
-
self._consumers[topic] = q
|
|
315
393
|
|
|
316
|
-
|
|
394
|
+
if value_type == 'dig':
|
|
395
|
+
if topic in list(self._dig_subscriber.keys()):
|
|
396
|
+
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
397
|
+
return topic, self._dig_subscriber[topic]
|
|
398
|
+
self._dig_subscriber[topic] = q
|
|
399
|
+
else:
|
|
400
|
+
if topic in list(self._phy_subscriber.keys()):
|
|
401
|
+
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
402
|
+
return topic, self._phy_subscriber[topic]
|
|
403
|
+
self._phy_subscriber[topic] = q
|
|
404
|
+
|
|
405
|
+
return topic, q
|
|
317
406
|
|
|
318
407
|
def _wrap_data_accept(self):
|
|
319
408
|
|
|
@@ -322,17 +411,34 @@ class AR4(object):
|
|
|
322
411
|
self._data_accept(data_ptr)
|
|
323
412
|
|
|
324
413
|
return data_accept
|
|
325
|
-
|
|
326
414
|
def _data_accept(self, data_ptr):
|
|
327
415
|
# logger.info(f"_eeg_accept 被调用")
|
|
328
416
|
# logger.debug(f'handle:{self}, data_ptr:{data_ptr}')
|
|
329
417
|
# data = cast(data_ptr, POINTER(Ar4NotifyData)).contents
|
|
330
|
-
if len(self.
|
|
418
|
+
if len(self._dig_subscriber) > 0 or len(self._phy_subscriber) > 0:
|
|
331
419
|
packet = AR4Packet().transfer(data_ptr.contents)
|
|
332
|
-
|
|
420
|
+
|
|
421
|
+
for consumer in self._dig_subscriber.values():
|
|
333
422
|
consumer.put(packet)
|
|
423
|
+
|
|
424
|
+
if len(self._phy_subscriber) > 0:
|
|
425
|
+
logger.info(f"dig data eeg: {packet.eeg}")
|
|
426
|
+
logger.info(f"dig data acc: {packet.acc}")
|
|
427
|
+
packet.eeg = self.eeg2phy(np.array(packet.eeg))
|
|
428
|
+
packet.acc = self.acc2phy(np.array(packet.acc))
|
|
429
|
+
logger.info(f"phy data eeg: {packet.eeg}")
|
|
430
|
+
logger.info(f"phy data acc: {packet.acc}")
|
|
431
|
+
for consumer2 in self._phy_subscriber.values():
|
|
432
|
+
consumer2.put(packet)
|
|
433
|
+
|
|
334
434
|
# logger.debug(f"EEG数据: {packet}")
|
|
335
435
|
|
|
436
|
+
def eeg2phy(self, digtal):
|
|
437
|
+
# 向量化计算(自动支持广播)
|
|
438
|
+
return ((digtal - self._eeg_dig_min) / self._eeg_dig_range) * self._eeg_phy_range + self._eeg_phy_min
|
|
439
|
+
|
|
440
|
+
def acc2phy(self, digtal):
|
|
441
|
+
return ((digtal - self._acc_dig_min) / self._acc_dig_range) * self._acc_phy_range + self._acc_phy_min
|
|
336
442
|
def _wrap_trigger_accept(self):
|
|
337
443
|
|
|
338
444
|
@FuncAr4DataNotify
|
|
@@ -344,7 +450,490 @@ class AR4(object):
|
|
|
344
450
|
def _trigger_accept(self, time_ms, trigger_type, trigger_value):
|
|
345
451
|
logger.info(f"_trigger_accept 被调用")
|
|
346
452
|
logger.info(f"触发时间: {time_ms}, 触发类型: {trigger_type}, 触发值: {trigger_value}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _wrap_connected_notify(self):
|
|
456
|
+
|
|
457
|
+
@FuncAr4RecorderConnected
|
|
458
|
+
def connected_notify(handle):
|
|
459
|
+
self._connected_notify(handle)
|
|
460
|
+
|
|
461
|
+
return connected_notify
|
|
462
|
+
|
|
463
|
+
def _connected_notify(self, handle):
|
|
464
|
+
logger.info(f"_connected_notify 被调用 handle: {handle}")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _wrap_disconnected_notify(self):
|
|
468
|
+
@FuncAr4RecorderDisconnected
|
|
469
|
+
def disconnected_notify(handle):
|
|
470
|
+
self._disconnected_notify(handle)
|
|
471
|
+
|
|
472
|
+
return disconnected_notify
|
|
473
|
+
|
|
474
|
+
def _disconnected_notify(self, handle):
|
|
475
|
+
logger.info(f"_disconnected_notify 被调用 handle: {handle}")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _wrap_box_connected_notify(self):
|
|
479
|
+
|
|
480
|
+
@FuncAr4RecorderConnected
|
|
481
|
+
def box_connected_notify(handle):
|
|
482
|
+
self._box_connected_notify(handle)
|
|
483
|
+
|
|
484
|
+
return box_connected_notify
|
|
485
|
+
|
|
486
|
+
def _box_connected_notify(self, handle):
|
|
487
|
+
logger.info(f"_box_connected_notify 被调用 handle: {handle}")
|
|
488
|
+
|
|
489
|
+
def _wrap_box_disconnected_notify(self):
|
|
490
|
+
@FuncAr4RecorderDisconnected
|
|
491
|
+
def box_disconnected_notify(handle):
|
|
492
|
+
self._box_disconnected_notify(handle)
|
|
493
|
+
|
|
494
|
+
return box_disconnected_notify
|
|
495
|
+
|
|
496
|
+
def _box_disconnected_notify(self, handle):
|
|
497
|
+
logger.info(f"_box_disconnected_notify 被调用 handle: {handle}")
|
|
498
|
+
|
|
499
|
+
def _wrap_start_record_notify(self):
|
|
500
|
+
|
|
501
|
+
@FuncAr4RecorderConnected
|
|
502
|
+
def start_record_notify(handle):
|
|
503
|
+
self._start_record_notify(handle)
|
|
504
|
+
|
|
505
|
+
return start_record_notify
|
|
506
|
+
|
|
507
|
+
def _start_record_notify(self, handle):
|
|
508
|
+
logger.info(f"_start_record_notify 被调用 handle: {handle}")
|
|
509
|
+
self._recording = True
|
|
510
|
+
self._record_start_time = time()
|
|
511
|
+
logger.info(self)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _wrap_stop_record_notify(self):
|
|
515
|
+
@FuncAr4RecorderDisconnected
|
|
516
|
+
def stop_record_notify(handle):
|
|
517
|
+
self._stop_record_notify(handle)
|
|
518
|
+
|
|
519
|
+
return stop_record_notify
|
|
520
|
+
|
|
521
|
+
def _stop_record_notify(self, handle):
|
|
522
|
+
logger.info(f"_stop_record_notify 被调用 handle: {handle}")
|
|
523
|
+
self._recording = False
|
|
524
|
+
|
|
525
|
+
def __str__(self):
|
|
526
|
+
return f"""
|
|
527
|
+
box mac: {self._box_mac},
|
|
528
|
+
box name: {self._box_name},
|
|
529
|
+
box soc: {self._box_soc}
|
|
530
|
+
head conn state: {self._head_conn_state}
|
|
531
|
+
head mac: {self._head_mac},
|
|
532
|
+
head soc: {self._head_soc}
|
|
533
|
+
connected: {self._connected}
|
|
534
|
+
connect time: {self._conn_time}
|
|
535
|
+
last time: {self._last_time}
|
|
536
|
+
[
|
|
537
|
+
eeg phy max: {self._eeg_phy_max}
|
|
538
|
+
eeg phy min: {self._eeg_phy_min}
|
|
539
|
+
eeg dig max: {self._eeg_dig_max}
|
|
540
|
+
eeg dig min: {self._eeg_dig_min}
|
|
541
|
+
eeg phy unit: {self._eeg_phy_unit}
|
|
542
|
+
]
|
|
543
|
+
[
|
|
544
|
+
acc phy max: {self._acc_phy_max}
|
|
545
|
+
acc phy min: {self._acc_phy_min}
|
|
546
|
+
acc dig max: {self._acc_dig_max}
|
|
547
|
+
acc dig min: {self._acc_dig_min}
|
|
548
|
+
acc phy unit: {self._acc_phy_unit}
|
|
549
|
+
]
|
|
550
|
+
dig -> ((dig - {self._eeg_dig_min}) / {self._eeg_dig_range}) * {self._eeg_phy_range} + {self._eeg_phy_min}
|
|
551
|
+
dig -> ((dig - {self._acc_dig_min}) / {self._acc_dig_range}) * {self._acc_phy_range} + {self._acc_phy_min}
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# ar4设备对象
|
|
557
|
+
class AR4(object):
|
|
558
|
+
def __init__(self, box_mac:str, slot:int=None, hub_name:str=None, is_persist:bool=True, storage_path:str=None):
|
|
559
|
+
# 是否持久化-保存为文件
|
|
560
|
+
self._is_persist = is_persist
|
|
561
|
+
self._storage_path = storage_path
|
|
562
|
+
self._edf_handler = None
|
|
563
|
+
# 设备句柄
|
|
564
|
+
self._handle = None
|
|
565
|
+
|
|
566
|
+
self.box_connected = False
|
|
567
|
+
self.sample_frequency = 500
|
|
568
|
+
# 设备基本信息
|
|
569
|
+
self._box_type = None
|
|
570
|
+
self._box_mac = box_mac
|
|
571
|
+
self._box_id = None
|
|
572
|
+
self._box_soc = None
|
|
573
|
+
self._box_name = None
|
|
574
|
+
self._box_version = None
|
|
575
|
+
self._head_type = None
|
|
576
|
+
self._head_mac = None
|
|
577
|
+
self._head_version = None
|
|
578
|
+
self._head_conn_state = None
|
|
579
|
+
self._head_soc = None
|
|
580
|
+
self._net_state = None
|
|
581
|
+
self._hub_name = hub_name
|
|
582
|
+
self._slot = slot
|
|
583
|
+
self._connected = False
|
|
584
|
+
self._conn_time = None
|
|
585
|
+
self._last_time = None
|
|
586
|
+
|
|
587
|
+
self._recording = False
|
|
588
|
+
self._record_start_time = None
|
|
589
|
+
|
|
590
|
+
## eeg 数据
|
|
591
|
+
self._eeg_phy_max = None
|
|
592
|
+
self._eeg_phy_min = None
|
|
593
|
+
self._eeg_dig_max = None
|
|
594
|
+
self._eeg_dig_min = None
|
|
595
|
+
self._eeg_phy_unit = 'uV'
|
|
596
|
+
self._eeg_phy_range = None
|
|
597
|
+
self._eeg_dig_range = None
|
|
598
|
+
## acc 数据
|
|
599
|
+
self._acc_phy_max = None
|
|
600
|
+
self._acc_phy_min = None
|
|
601
|
+
self._acc_dig_max = None
|
|
602
|
+
self._acc_dig_min = None
|
|
603
|
+
self._acc_phy_unit = 'mG'
|
|
604
|
+
self._acc_phy_range = None
|
|
605
|
+
self._acc_dig_range = None
|
|
606
|
+
|
|
607
|
+
self._acq_info = {}
|
|
608
|
+
# 订阅者列表,数值为数字信号值
|
|
609
|
+
self._dig_subscriber: dict[str, Queue] = {}
|
|
610
|
+
# 订阅者列表,数值为物理信号值
|
|
611
|
+
self._phy_subscriber: dict[str, Queue] = {}
|
|
612
|
+
|
|
613
|
+
# 回调函数
|
|
614
|
+
self._data_callback = FuncAr4DataNotify(self._wrap_data_accept())
|
|
615
|
+
self._trigger_callback = FuncAr4TriggerNotify(self._wrap_trigger_accept())
|
|
616
|
+
self._connected_notify_callback = FuncAr4RecorderConnected(self._wrap_connected_notify())
|
|
617
|
+
self._disconnected_notify_callback = FuncAr4RecorderDisconnected(self._wrap_disconnected_notify())
|
|
618
|
+
self._box_connected_notify_callback = FuncAr4RecorderConnected(self._wrap_box_connected_notify())
|
|
619
|
+
self._box_disconnected_notify_callback = FuncAr4RecorderDisconnected(self._wrap_box_disconnected_notify())
|
|
620
|
+
self._start_record_notify_callback = FuncAr4RecorderConnected(self._wrap_start_record_notify())
|
|
621
|
+
self._stop_record_notify_callback = FuncAr4RecorderDisconnected(self._wrap_stop_record_notify())
|
|
622
|
+
|
|
623
|
+
@property
|
|
624
|
+
def box_mac(self):
|
|
625
|
+
return self._box_mac
|
|
626
|
+
@property
|
|
627
|
+
def slot(self):
|
|
628
|
+
return self._slot
|
|
629
|
+
@property
|
|
630
|
+
def hub_name(self):
|
|
631
|
+
return self._hub_name
|
|
632
|
+
|
|
633
|
+
def set_storage_path(self, storage_path):
|
|
634
|
+
self._storage_path = storage_path
|
|
635
|
+
|
|
636
|
+
def init(self):
|
|
637
|
+
# logger.info(f"init ar4 {self.box_mac}")
|
|
638
|
+
if not self._handle:
|
|
639
|
+
self._connected = self.connect()
|
|
640
|
+
## eeg 参数
|
|
641
|
+
self._eeg_phy_max = _dll.ar4_sdk_get_eeg_phy_max(self._handle)
|
|
642
|
+
self._eeg_phy_min = _dll.ar4_sdk_get_eeg_phy_min(self._handle)
|
|
643
|
+
self._eeg_dig_max = _dll.ar4_sdk_get_eeg_digtal_max(self._handle)
|
|
644
|
+
self._eeg_dig_min = _dll.ar4_sdk_get_eeg_digtal_min(self._handle)
|
|
645
|
+
self._eeg_phy_range = self._eeg_phy_max - self._eeg_phy_min
|
|
646
|
+
self._eeg_dig_range = self._eeg_dig_max - self._eeg_dig_min
|
|
647
|
+
eeg_unit = _dll.ar4_sdk_get_eeg_phy_unit(self._handle)
|
|
648
|
+
if eeg_unit:
|
|
649
|
+
try:
|
|
650
|
+
self._eeg_phy_unit = eeg_unit.decode("utf-8")
|
|
651
|
+
except Exception as e:
|
|
652
|
+
logger.error(f"ar4 {self._box_mac} 获取eeg物理单位异常: {str(e)}")
|
|
653
|
+
|
|
654
|
+
## acc 参数
|
|
655
|
+
self._acc_phy_max = _dll.ar4_sdk_get_acc_phy_max(self._handle)
|
|
656
|
+
self._acc_phy_min = _dll.ar4_sdk_get_acc_phy_min(self._handle)
|
|
657
|
+
self._acc_dig_max = _dll.ar4_sdk_get_acc_digtal_max(self._handle)
|
|
658
|
+
self._acc_dig_min = _dll.ar4_sdk_get_acc_digtal_min(self._handle)
|
|
659
|
+
self._acc_phy_range = self._acc_phy_max - self._acc_phy_min
|
|
660
|
+
self._acc_dig_range = self._acc_dig_max - self._acc_dig_min
|
|
661
|
+
acc_unit = _dll.ar4_sdk_get_acc_phy_unit(self._handle)
|
|
662
|
+
if acc_unit:
|
|
663
|
+
try:
|
|
664
|
+
self._acc_phy_unit = eeg_unit.decode("utf-8")
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.error(f"ar4 {self._box_mac} 获取acc物理单位异常: {str(e)}")
|
|
667
|
+
|
|
668
|
+
if self._connected:
|
|
669
|
+
self._conn_time = _get_time()
|
|
670
|
+
self.get_box_name()
|
|
671
|
+
self.get_head_mac()
|
|
672
|
+
self.get_record_conn_state()
|
|
673
|
+
self._last_time = _get_time()
|
|
674
|
+
logger.debug(self)
|
|
675
|
+
self._register_callback()
|
|
676
|
+
return True
|
|
677
|
+
|
|
678
|
+
def _register_callback(self):
|
|
679
|
+
try:
|
|
680
|
+
_dll.ar4_sdk_register_data_notify(self._handle, self._data_callback)
|
|
681
|
+
_dll.ar4_sdk_register_trigger_notify(self._handle, self._trigger_callback)
|
|
682
|
+
_dll.ar4_sdk_register_conn_notify(self._handle, self._connected_notify_callback, self._disconnected_notify_callback)
|
|
683
|
+
_dll.ar4_sdk_register_record_state_notify(self._handle, self._start_record_notify_callback, self._stop_record_notify_callback)
|
|
684
|
+
_dll.ar4_sdk_register_box_conn_notify(self._handle, self._box_connected_notify_callback, self._box_disconnected_notify_callback)
|
|
685
|
+
except Exception as e:
|
|
686
|
+
logger.error(f"回调函数注册异常: {str(e)}")
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def device_type(self):
|
|
690
|
+
return "ar4" if self._box_type == 4 else "x8" if self._box_type == 8 else "lm"
|
|
691
|
+
|
|
692
|
+
def update_info(self):
|
|
693
|
+
logger.debug(f"update info {self._box_mac}")
|
|
694
|
+
self._last_time = _get_time()
|
|
695
|
+
|
|
696
|
+
# 设备连接
|
|
697
|
+
def connect(self)-> bool:
|
|
698
|
+
|
|
699
|
+
if self._box_mac:
|
|
700
|
+
"""连接设备"""
|
|
701
|
+
# callback = FuncAr4GetTime(self._get_time)
|
|
702
|
+
try:
|
|
703
|
+
self._handle = _dll.ar4_sdk_connect(int(self._box_mac, 16), c_void_p(0))
|
|
704
|
+
# logger.info(f"conn handle is {self._handle}")
|
|
705
|
+
self._handle = c_void_p(self._handle)
|
|
706
|
+
logger.debug(f"ar4 {self._box_mac} 连接: {self._handle}")
|
|
707
|
+
except Exception as e:
|
|
708
|
+
logger.error(f"ar4 {self._box_mac} 连接异常: {str(e)}")
|
|
709
|
+
return self._handle is not None
|
|
710
|
+
|
|
711
|
+
# 读取盒子名称
|
|
712
|
+
def get_box_name(self):
|
|
713
|
+
if self._handle:
|
|
714
|
+
try:
|
|
715
|
+
self._head_mac = _dll.ar4_sdk_get_recoder_mac(self._handle)
|
|
716
|
+
except Exception as e:
|
|
717
|
+
logger.error(f"ar4 {self._box_mac} 获取盒子记录子mac异常: {str(e)}")
|
|
718
|
+
# 读取记录子mac
|
|
719
|
+
def get_head_mac(self):
|
|
720
|
+
if self._handle:
|
|
721
|
+
try:
|
|
722
|
+
self._box_name = _dll.ar4_sdk_get_record_conn_state(self._handle)
|
|
723
|
+
except Exception as e:
|
|
724
|
+
logger.error(f"ar4 {self._box_mac} 获取盒子名称异常: {str(e)}")
|
|
725
|
+
# 读取记录子连接状态
|
|
726
|
+
def get_record_conn_state(self):
|
|
727
|
+
if self._handle:
|
|
728
|
+
try:
|
|
729
|
+
self._head_conn_state = _dll.ar4_sdk_get_record_conn_state(self._handle)
|
|
730
|
+
except Exception as e:
|
|
731
|
+
logger.error(f"ar4 {self._box_mac} 获取盒子记录子连接状态异常: {str(e)}")
|
|
732
|
+
|
|
733
|
+
# 数据采集启动
|
|
734
|
+
def start_acquisition(self):
|
|
735
|
+
logger.info(f"ar4 {self._box_mac} 启动数据采集...")
|
|
736
|
+
"""启动数据采集"""
|
|
737
|
+
|
|
738
|
+
self._acq_info["start_time"] = _get_time()
|
|
739
|
+
|
|
740
|
+
if self._handle:
|
|
741
|
+
# 启动采集
|
|
742
|
+
try:
|
|
743
|
+
logger.debug(f"ar4 {self._box_mac} 启动采集: {self._handle}")
|
|
744
|
+
ret = _dll.ar4_sdk_start_acq(self._handle)
|
|
745
|
+
logger.debug(f"ar4 {self._box_mac} 启动采集结果: {ret}")
|
|
746
|
+
if (self._is_persist):
|
|
747
|
+
self._edf_handler = EdfHandler(self.sample_frequency, self._eeg_phy_max, self._eeg_phy_min, self._eeg_dig_max, self._eeg_dig_min, storage_path = self._storage_path)
|
|
748
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
749
|
+
|
|
750
|
+
return ret == 0
|
|
751
|
+
except Exception as e:
|
|
752
|
+
logger.error(f"ar4 {self._box_mac} 启动数据采集异常: {str(e)}")
|
|
753
|
+
else:
|
|
754
|
+
logger.info(f"ar4 {self._box_mac} 启动数据采集失败, 设备未连接")
|
|
755
|
+
return False
|
|
756
|
+
|
|
757
|
+
def stop_acquisition(self):
|
|
758
|
+
"""停止采集"""
|
|
759
|
+
if self._handle:
|
|
760
|
+
try:
|
|
761
|
+
self.get_acq_start_time()
|
|
762
|
+
except Exception as e:
|
|
763
|
+
logger.error(f"ar4 {self._box_mac} 获取开始采集时间异常: {str(e)}")
|
|
764
|
+
try:
|
|
765
|
+
ret = _dll.ar4_sdk_stop_acq(self._handle)
|
|
766
|
+
if ret == 0 and self._edf_handler:
|
|
767
|
+
self._edf_handler.append(None)
|
|
347
768
|
|
|
769
|
+
return ret == 0
|
|
770
|
+
except Exception as e:
|
|
771
|
+
logger.error(f"ar4 {self._box_mac} 停止采集异常: {str(e)}")
|
|
772
|
+
else:
|
|
773
|
+
return False
|
|
774
|
+
|
|
775
|
+
def disconnect(self):
|
|
776
|
+
"""断开连接"""
|
|
777
|
+
if self._handle:
|
|
778
|
+
_dll.ar4_sdk_disconnect(self._handle)
|
|
779
|
+
|
|
780
|
+
def get_sample_rate(self):
|
|
781
|
+
try:
|
|
782
|
+
ret = _dll.ar4_sdk_get_record_sample_rate(self._handle)
|
|
783
|
+
logger.debug(f"ar4 {self._box_mac} 获取采样率: {ret.contents.decode('utf-8')}")
|
|
784
|
+
except Exception as e:
|
|
785
|
+
logger.error(f"ar4 {self._box_mac} 获取采样率异常: {str(e)}")
|
|
786
|
+
|
|
787
|
+
def get_acq_start_time(self):
|
|
788
|
+
try:
|
|
789
|
+
ret = _dll.ar4_sdk_get_acq_start_time(self._handle)
|
|
790
|
+
# 更新采样开始时间
|
|
791
|
+
if ret:
|
|
792
|
+
self._acq_info["start_time"] = ret
|
|
793
|
+
logger.debug(f"ar4 {self._box_mac} 获取采样开始时间: {ret}")
|
|
794
|
+
except Exception as e:
|
|
795
|
+
logger.error(f"ar4 {self._box_mac} 获取采样开始时间异常: {str(e)}")
|
|
796
|
+
|
|
797
|
+
# 订阅推送消息
|
|
798
|
+
def subscribe(self, topic: str = None, q: Queue = Queue(), value_type: Literal['phy', 'dig'] = 'phy') -> tuple[str, Queue]:
|
|
799
|
+
if topic is None:
|
|
800
|
+
topic = f"msg_{_get_time()}"
|
|
801
|
+
|
|
802
|
+
if value_type == 'dig':
|
|
803
|
+
if topic in list(self._dig_subscriber.keys()):
|
|
804
|
+
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
805
|
+
return topic, self._dig_subscriber[topic]
|
|
806
|
+
self._dig_subscriber[topic] = q
|
|
807
|
+
else:
|
|
808
|
+
if topic in list(self._phy_subscriber.keys()):
|
|
809
|
+
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
810
|
+
return topic, self._phy_subscriber[topic]
|
|
811
|
+
self._phy_subscriber[topic] = q
|
|
812
|
+
|
|
813
|
+
return topic, q
|
|
814
|
+
|
|
815
|
+
def _wrap_data_accept(self):
|
|
816
|
+
|
|
817
|
+
@FuncAr4DataNotify
|
|
818
|
+
def data_accept(handle, data_ptr):
|
|
819
|
+
self._data_accept(data_ptr)
|
|
820
|
+
|
|
821
|
+
return data_accept
|
|
822
|
+
def _data_accept(self, data_ptr):
|
|
823
|
+
# logger.info(f"_eeg_accept 被调用")
|
|
824
|
+
# logger.debug(f'handle:{self}, data_ptr:{data_ptr}')
|
|
825
|
+
# data = cast(data_ptr, POINTER(Ar4NotifyData)).contents
|
|
826
|
+
# if len(self._dig_subscriber) > 0 or len(self._phy_subscriber) > 0:
|
|
827
|
+
packet = AR4Packet().transfer(data_ptr.contents)
|
|
828
|
+
if self._edf_handler:
|
|
829
|
+
self._edf_handler.append(packet)
|
|
830
|
+
|
|
831
|
+
for consumer in self._dig_subscriber.values():
|
|
832
|
+
consumer.put(packet)
|
|
833
|
+
|
|
834
|
+
if len(self._phy_subscriber) > 0:
|
|
835
|
+
logger.info(f"dig data eeg: {packet.eeg}")
|
|
836
|
+
logger.info(f"dig data acc: {packet.acc}")
|
|
837
|
+
packet.eeg = self.eeg2phy(np.array(packet.eeg))
|
|
838
|
+
packet.acc = self.acc2phy(np.array(packet.acc))
|
|
839
|
+
logger.info(f"phy data eeg: {packet.eeg}")
|
|
840
|
+
logger.info(f"phy data acc: {packet.acc}")
|
|
841
|
+
for consumer2 in self._phy_subscriber.values():
|
|
842
|
+
consumer2.put(packet)
|
|
843
|
+
|
|
844
|
+
# logger.debug(f"EEG数据: {packet}")
|
|
845
|
+
|
|
846
|
+
def eeg2phy(self, digtal):
|
|
847
|
+
# 向量化计算(自动支持广播)
|
|
848
|
+
return ((digtal - self._eeg_dig_min) / self._eeg_dig_range) * self._eeg_phy_range + self._eeg_phy_min
|
|
849
|
+
|
|
850
|
+
def acc2phy(self, digtal):
|
|
851
|
+
return ((digtal - self._acc_dig_min) / self._acc_dig_range) * self._acc_phy_range + self._acc_phy_min
|
|
852
|
+
def _wrap_trigger_accept(self):
|
|
853
|
+
|
|
854
|
+
@FuncAr4DataNotify
|
|
855
|
+
def trigger_accept(handle, time_ms, trigger_type, trigger_value):
|
|
856
|
+
self._trigger_accept(time_ms, trigger_type, trigger_value)
|
|
857
|
+
|
|
858
|
+
return trigger_accept
|
|
859
|
+
|
|
860
|
+
def _trigger_accept(self, time_ms, trigger_type, trigger_value):
|
|
861
|
+
logger.info(f"_trigger_accept 被调用")
|
|
862
|
+
logger.info(f"触发时间: {time_ms}, 触发类型: {trigger_type}, 触发值: {trigger_value}")
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _wrap_connected_notify(self):
|
|
866
|
+
|
|
867
|
+
@FuncAr4RecorderConnected
|
|
868
|
+
def connected_notify(handle):
|
|
869
|
+
self._connected_notify(handle)
|
|
870
|
+
|
|
871
|
+
return connected_notify
|
|
872
|
+
|
|
873
|
+
def _connected_notify(self, handle):
|
|
874
|
+
logger.info(f"_connected_notify 被调用 handle: {handle}")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _wrap_disconnected_notify(self):
|
|
878
|
+
@FuncAr4RecorderDisconnected
|
|
879
|
+
def disconnected_notify(handle):
|
|
880
|
+
self._disconnected_notify(handle)
|
|
881
|
+
|
|
882
|
+
return disconnected_notify
|
|
883
|
+
|
|
884
|
+
def _disconnected_notify(self, handle):
|
|
885
|
+
logger.info(f"_disconnected_notify 被调用 handle: {handle}")
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _wrap_box_connected_notify(self):
|
|
889
|
+
|
|
890
|
+
@FuncAr4RecorderConnected
|
|
891
|
+
def box_connected_notify(handle):
|
|
892
|
+
self._box_connected_notify(handle)
|
|
893
|
+
|
|
894
|
+
return box_connected_notify
|
|
895
|
+
|
|
896
|
+
def _box_connected_notify(self, handle):
|
|
897
|
+
self.box_connected = True
|
|
898
|
+
logger.info(f"_box_connected_notify 被调用 handle: {handle}")
|
|
899
|
+
|
|
900
|
+
def _wrap_box_disconnected_notify(self):
|
|
901
|
+
@FuncAr4RecorderDisconnected
|
|
902
|
+
def box_disconnected_notify(handle):
|
|
903
|
+
self._box_disconnected_notify(handle)
|
|
904
|
+
|
|
905
|
+
return box_disconnected_notify
|
|
906
|
+
|
|
907
|
+
def _box_disconnected_notify(self, handle):
|
|
908
|
+
self.box_connected = False
|
|
909
|
+
logger.info(f"_box_disconnected_notify 被调用 handle: {handle}")
|
|
910
|
+
|
|
911
|
+
def _wrap_start_record_notify(self):
|
|
912
|
+
|
|
913
|
+
@FuncAr4RecorderConnected
|
|
914
|
+
def start_record_notify(handle):
|
|
915
|
+
self._start_record_notify(handle)
|
|
916
|
+
|
|
917
|
+
return start_record_notify
|
|
918
|
+
|
|
919
|
+
def _start_record_notify(self, handle):
|
|
920
|
+
logger.info(f"_start_record_notify 被调用 handle: {handle}")
|
|
921
|
+
self._recording = True
|
|
922
|
+
self._record_start_time = time()
|
|
923
|
+
logger.info(self)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _wrap_stop_record_notify(self):
|
|
927
|
+
@FuncAr4RecorderDisconnected
|
|
928
|
+
def stop_record_notify(handle):
|
|
929
|
+
self._stop_record_notify(handle)
|
|
930
|
+
|
|
931
|
+
return stop_record_notify
|
|
932
|
+
|
|
933
|
+
def _stop_record_notify(self, handle):
|
|
934
|
+
logger.info(f"_stop_record_notify 被调用 handle: {handle}")
|
|
935
|
+
self._recording = False
|
|
936
|
+
|
|
348
937
|
def __str__(self):
|
|
349
938
|
return f"""
|
|
350
939
|
box mac: {self._box_mac},
|
|
@@ -356,6 +945,22 @@ class AR4(object):
|
|
|
356
945
|
connected: {self._connected}
|
|
357
946
|
connect time: {self._conn_time}
|
|
358
947
|
last time: {self._last_time}
|
|
948
|
+
[
|
|
949
|
+
eeg phy max: {self._eeg_phy_max}
|
|
950
|
+
eeg phy min: {self._eeg_phy_min}
|
|
951
|
+
eeg dig max: {self._eeg_dig_max}
|
|
952
|
+
eeg dig min: {self._eeg_dig_min}
|
|
953
|
+
eeg phy unit: {self._eeg_phy_unit}
|
|
954
|
+
]
|
|
955
|
+
[
|
|
956
|
+
acc phy max: {self._acc_phy_max}
|
|
957
|
+
acc phy min: {self._acc_phy_min}
|
|
958
|
+
acc dig max: {self._acc_dig_max}
|
|
959
|
+
acc dig min: {self._acc_dig_min}
|
|
960
|
+
acc phy unit: {self._acc_phy_unit}
|
|
961
|
+
]
|
|
962
|
+
dig -> ((dig - {self._eeg_dig_min}) / {self._eeg_dig_range}) * {self._eeg_phy_range} + {self._eeg_phy_min}
|
|
963
|
+
dig -> ((dig - {self._acc_dig_min}) / {self._acc_dig_range}) * {self._acc_phy_range} + {self._acc_phy_min}
|
|
359
964
|
"""
|
|
360
965
|
|
|
361
966
|
class Packet(object):
|
|
@@ -383,11 +988,6 @@ class AR4Packet(Packet):
|
|
|
383
988
|
self.acc_count = data.acc_count
|
|
384
989
|
# 读eeg数据
|
|
385
990
|
if self.eeg_ch_count and self.eeg_count:
|
|
386
|
-
# self.eeg = [[] for _ in range(self.eeg_ch_count)]
|
|
387
|
-
# for i in range(self.eeg_ch_count):
|
|
388
|
-
# self.eeg[i] = [data.eeg[j + (i * self.eeg_count)] for j in range(self.eeg_count)]
|
|
389
|
-
# tmp = data.eeg[:self.eeg_ch_count * self.eeg_count]
|
|
390
|
-
# logger.info(tmp)
|
|
391
991
|
self.eeg = [data.eeg[i:self.eeg_count*self.eeg_ch_count:self.eeg_ch_count] for i in range(self.eeg_ch_count)]
|
|
392
992
|
# 读acc数据
|
|
393
993
|
if self.acc_ch_count and self.acc_count:
|
qlsdk/ar4m/persist.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from multiprocessing import Lock, Queue
|
|
3
|
+
from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from loguru import logger
|
|
6
|
+
import numpy as np
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
class EdfHandler(object):
|
|
10
|
+
def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=16, storage_path = None):
|
|
11
|
+
self.physical_max = physical_max
|
|
12
|
+
self.physical_min = physical_min
|
|
13
|
+
self.digital_max = digital_max
|
|
14
|
+
self.digital_min = digital_min
|
|
15
|
+
self.channels = None
|
|
16
|
+
self._cache = Queue()
|
|
17
|
+
self.resolution = resolution
|
|
18
|
+
self.sample_frequency = sample_frequency
|
|
19
|
+
# bytes per second
|
|
20
|
+
self.bytes_per_second = 0
|
|
21
|
+
self._edf_writer = None
|
|
22
|
+
self._cache2 = tuple()
|
|
23
|
+
self._recording = False
|
|
24
|
+
self._edf_writer = None
|
|
25
|
+
self.annotations = None
|
|
26
|
+
# 每个数据块大小
|
|
27
|
+
self._chunk = np.array([])
|
|
28
|
+
self._Lock = Lock()
|
|
29
|
+
self._duration = 0
|
|
30
|
+
self._points = 0
|
|
31
|
+
self._first_pkg_id = None
|
|
32
|
+
self._last_pkg_id = None
|
|
33
|
+
self._first_timestamp = None
|
|
34
|
+
self._end_time = None
|
|
35
|
+
self._patient_code = "patient_code"
|
|
36
|
+
self._patient_name = "patient_name"
|
|
37
|
+
self._device_type = None
|
|
38
|
+
self._total_packets = 0
|
|
39
|
+
self._lost_packets = 0
|
|
40
|
+
self._storage_path = storage_path
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def file_name(self):
|
|
44
|
+
if self._storage_path:
|
|
45
|
+
try:
|
|
46
|
+
os.makedirs(self._storage_path, exist_ok=True) # 自动创建目录,存在则忽略
|
|
47
|
+
return f"{self._storage_path}/{self._device_type}_{self._first_timestamp}.edf"
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"创建目录[{self._storage_path}]失败: {e}")
|
|
50
|
+
|
|
51
|
+
return f"{self._device_type}_{self._first_timestamp}.edf"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def file_type(self):
|
|
55
|
+
return FILETYPE_BDFPLUS if self.resolution == 24 else FILETYPE_EDFPLUS
|
|
56
|
+
|
|
57
|
+
def set_device_type(self, device_type):
|
|
58
|
+
self._device_type = device_type
|
|
59
|
+
|
|
60
|
+
def set_storage_path(self, storage_path):
|
|
61
|
+
self._storage_path = storage_path
|
|
62
|
+
|
|
63
|
+
def set_patient_code(self, patient_code):
|
|
64
|
+
self._patient_code = patient_code
|
|
65
|
+
|
|
66
|
+
def set_patient_name(self, patient_name):
|
|
67
|
+
self._patient_name = patient_name
|
|
68
|
+
|
|
69
|
+
def append(self, data):
|
|
70
|
+
if data:
|
|
71
|
+
# 通道数
|
|
72
|
+
if self._first_pkg_id is None:
|
|
73
|
+
self.channels = data.eeg_ch_count
|
|
74
|
+
self._first_pkg_id = data.pkg_id
|
|
75
|
+
self._first_timestamp = data.time_stamp
|
|
76
|
+
|
|
77
|
+
if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
|
|
78
|
+
self._lost_packets += data.pkg_id - self._last_pkg_id - 1
|
|
79
|
+
logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
|
|
80
|
+
|
|
81
|
+
self._last_pkg_id = data.pkg_id
|
|
82
|
+
self._total_packets += 1
|
|
83
|
+
|
|
84
|
+
# 数据
|
|
85
|
+
self._cache.put(data)
|
|
86
|
+
if not self._recording:
|
|
87
|
+
self.start()
|
|
88
|
+
|
|
89
|
+
def trigger(self, data):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def start(self):
|
|
93
|
+
self._recording = True
|
|
94
|
+
record_thread = Thread(target=self._consumer)
|
|
95
|
+
record_thread.start()
|
|
96
|
+
|
|
97
|
+
def _consumer(self):
|
|
98
|
+
logger.debug(f"开始消费数据 _consumer: {self._cache.qsize()}")
|
|
99
|
+
while True:
|
|
100
|
+
if self._recording or (not self._cache.empty()):
|
|
101
|
+
data = self._cache.get()
|
|
102
|
+
if data is None:
|
|
103
|
+
break
|
|
104
|
+
# 处理数据
|
|
105
|
+
self._points += len(data.eeg[0])
|
|
106
|
+
self._write_file(data.eeg)
|
|
107
|
+
else:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
self.close()
|
|
111
|
+
|
|
112
|
+
def _write_file(self, data):
|
|
113
|
+
try:
|
|
114
|
+
if self._edf_writer is None:
|
|
115
|
+
self.initialize_edf()
|
|
116
|
+
|
|
117
|
+
if (self._chunk.size == 0):
|
|
118
|
+
self._chunk = np.asarray(data)
|
|
119
|
+
else:
|
|
120
|
+
self._chunk = np.hstack((self._chunk, data))
|
|
121
|
+
|
|
122
|
+
if self._chunk.size >= self.sample_frequency * self.channels:
|
|
123
|
+
self._write_chunk(self._chunk[:self.sample_frequency])
|
|
124
|
+
self._chunk = self._chunk[self.sample_frequency:]
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"写入数据异常: {str(e)}")
|
|
128
|
+
|
|
129
|
+
def close(self):
|
|
130
|
+
self._recording = False
|
|
131
|
+
if self._edf_writer:
|
|
132
|
+
self._end_time = datetime.now().timestamp()
|
|
133
|
+
self._edf_writer.writeAnnotation(0, 1, "start recording ")
|
|
134
|
+
self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
|
|
135
|
+
self._edf_writer.close()
|
|
136
|
+
|
|
137
|
+
logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def initialize_edf(self):
|
|
142
|
+
# 创建EDF+写入器
|
|
143
|
+
self._edf_writer = EdfWriter(
|
|
144
|
+
self.file_name,
|
|
145
|
+
self.channels,
|
|
146
|
+
file_type=self.file_type
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# 设置头信息
|
|
150
|
+
self._edf_writer.setPatientCode(self._patient_code)
|
|
151
|
+
self._edf_writer.setPatientName(self._patient_name)
|
|
152
|
+
self._edf_writer.setEquipment(self._device_type)
|
|
153
|
+
self._edf_writer.setStartdatetime(datetime.now())
|
|
154
|
+
|
|
155
|
+
# 配置通道参数
|
|
156
|
+
signal_headers = []
|
|
157
|
+
for ch in range(self.channels):
|
|
158
|
+
signal_headers.append({
|
|
159
|
+
"label": f'channels {ch + 1}',
|
|
160
|
+
"dimension": 'uV',
|
|
161
|
+
"sample_frequency": self.sample_frequency,
|
|
162
|
+
"physical_min": self.physical_min,
|
|
163
|
+
"physical_max": self.physical_max,
|
|
164
|
+
"digital_min": self.digital_min,
|
|
165
|
+
"digital_max": self.digital_max
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
self._edf_writer.setSignalHeaders(signal_headers)
|
|
169
|
+
|
|
170
|
+
def _write_chunk(self, chunk):
|
|
171
|
+
logger.debug(f"写入数据: {chunk}")
|
|
172
|
+
# 转换数据类型为float64(pyedflib要求)
|
|
173
|
+
data_float64 = chunk.astype(np.float64)
|
|
174
|
+
# 写入时转置为(样本数, 通道数)格式
|
|
175
|
+
self._edf_writer.writeSamples(data_float64)
|
|
176
|
+
self._duration += 1
|
|
177
|
+
|
|
178
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: qlsdk2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0a1
|
|
4
4
|
Summary: SDK for quanlan device
|
|
5
5
|
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
6
|
Author: hehuajun
|
|
@@ -11,6 +11,8 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: loguru>=0.6.0
|
|
14
|
+
Requires-Dist: numpy>=1.23.5
|
|
15
|
+
Requires-Dist: pyedflib>=0.1.40
|
|
14
16
|
Provides-Extra: dev
|
|
15
17
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
16
18
|
Requires-Dist: twine>=3.0; extra == "dev"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
qlsdk/__init__.py,sha256=pHMMwFiz7AGYlWvxo-81Obd7gIkojhLAEUGe0yuiAJI,372
|
|
2
|
+
qlsdk/ar4m/__init__.py,sha256=HCFv58t9vYoH2nNN9EiswwdRngiDmh_7h-uwhW-id1I,1784
|
|
3
|
+
qlsdk/ar4m/ar4sdk.py,sha256=OtmlvbO7XAgk5FNPFYIv_rXGj14maaWtgi2lQ7zQkvY,41418
|
|
4
|
+
qlsdk/ar4m/persist.py,sha256=YrISKVfY-r1m2GZOw-JLuqTbEajA5wYa31MejGlz14I,6696
|
|
5
|
+
qlsdk/ar4m/libs/libAr4SDK.dll,sha256=kZp9_DRwPdAJ5OgTFQSqS8tEETxUs7YmmETuBP2g60U,15402132
|
|
6
|
+
qlsdk/ar4m/libs/libwinpthread-1.dll,sha256=W77ySaDQDi0yxpnQu-ifcU6-uHKzmQpcvsyx2J9j5eg,52224
|
|
7
|
+
qlsdk2-0.2.0a1.dist-info/METADATA,sha256=r8hXXxFJJkgVi6W7XiYlTdMu3FjnP8vk1rYBUjLHX9k,7066
|
|
8
|
+
qlsdk2-0.2.0a1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
9
|
+
qlsdk2-0.2.0a1.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
|
|
10
|
+
qlsdk2-0.2.0a1.dist-info/RECORD,,
|
qlsdk2-0.1.7.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
qlsdk/__init__.py,sha256=pHMMwFiz7AGYlWvxo-81Obd7gIkojhLAEUGe0yuiAJI,372
|
|
2
|
-
qlsdk/ar4m/__init__.py,sha256=HCFv58t9vYoH2nNN9EiswwdRngiDmh_7h-uwhW-id1I,1784
|
|
3
|
-
qlsdk/ar4m/ar4sdk.py,sha256=M244in6ytN8f-PI7lIbt8BfEE_DcjAsE06iLxI7xNeY,15358
|
|
4
|
-
qlsdk/ar4m/libs/libAr4SDK.dll,sha256=kZp9_DRwPdAJ5OgTFQSqS8tEETxUs7YmmETuBP2g60U,15402132
|
|
5
|
-
qlsdk/ar4m/libs/libwinpthread-1.dll,sha256=W77ySaDQDi0yxpnQu-ifcU6-uHKzmQpcvsyx2J9j5eg,52224
|
|
6
|
-
qlsdk2-0.1.7.dist-info/METADATA,sha256=ynZ7zTy2_H7Hney0pXE-GgVERk9dgvaLoCierdAim8U,7001
|
|
7
|
-
qlsdk2-0.1.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
8
|
-
qlsdk2-0.1.7.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
|
|
9
|
-
qlsdk2-0.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|