qlsdk2 0.2.0a1__tar.gz → 0.3.0a2__tar.gz
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.
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/PKG-INFO +1 -1
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/setup.py +2 -2
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/src/qlsdk/__init__.py +5 -2
- qlsdk2-0.3.0a2/src/qlsdk/ar4/__init__.py +128 -0
- qlsdk2-0.3.0a2/src/qlsdk/ar4m/__init__.py +21 -0
- qlsdk2-0.3.0a2/src/qlsdk/persist/__init__.py +1 -0
- qlsdk2-0.2.0a1/src/qlsdk/ar4m/persist.py → qlsdk2-0.3.0a2/src/qlsdk/persist/edf.py +21 -13
- qlsdk2-0.3.0a2/src/qlsdk/sdk/__init__.py +2 -0
- qlsdk2-0.3.0a2/src/qlsdk/sdk/ar4sdk.py +742 -0
- qlsdk2-0.3.0a2/src/qlsdk/sdk/hub.py +51 -0
- qlsdk2-0.3.0a2/src/qlsdk/x8/__init__.py +128 -0
- qlsdk2-0.3.0a2/src/qlsdk/x8m/__init__.py +21 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/src/qlsdk2.egg-info/PKG-INFO +1 -1
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/src/qlsdk2.egg-info/SOURCES.txt +8 -4
- qlsdk2-0.2.0a1/src/qlsdk/ar4m/__init__.py +0 -50
- qlsdk2-0.2.0a1/src/qlsdk/ar4m/ar4sdk.py +0 -1011
- qlsdk2-0.2.0a1/src/qlsdk/ar4m/libs/libAr4SDK.dll +0 -0
- qlsdk2-0.2.0a1/src/qlsdk/ar4m/libs/libwinpthread-1.dll +0 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/README.md +0 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/setup.cfg +0 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/src/qlsdk2.egg-info/requires.txt +0 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/src/qlsdk2.egg-info/top_level.txt +0 -0
- {qlsdk2-0.2.0a1 → qlsdk2-0.3.0a2}/test/test_ar4m.py +0 -0
|
@@ -6,7 +6,7 @@ with open("README.md", "r") as fh:
|
|
|
6
6
|
|
|
7
7
|
setuptools.setup(
|
|
8
8
|
name="qlsdk2",
|
|
9
|
-
version="0.
|
|
9
|
+
version="0.3.0a2",
|
|
10
10
|
author="hehuajun",
|
|
11
11
|
author_email="hehuajun@eegion.com",
|
|
12
12
|
description="SDK for quanlan device",
|
|
@@ -25,7 +25,7 @@ setuptools.setup(
|
|
|
25
25
|
include_package_data=True,
|
|
26
26
|
package_data={
|
|
27
27
|
# "src/qlsdk/ar4m/": ["libs/*.dll"],
|
|
28
|
-
"qlsdk/
|
|
28
|
+
"qlsdk/sdk": ["libs/*.dll"],
|
|
29
29
|
"":["*.txt", "*.md"]
|
|
30
30
|
},
|
|
31
31
|
# entry_points={
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# __version__ = "0.1.1"
|
|
2
2
|
|
|
3
3
|
# 暴露公共接口
|
|
4
|
-
from .ar4m import AR4M
|
|
4
|
+
from .ar4m import AR4M
|
|
5
|
+
from .ar4 import AR4
|
|
6
|
+
from .x8 import X8
|
|
7
|
+
from .x8m import X8M
|
|
5
8
|
|
|
6
|
-
__all__ = ['AR4M', 'AR4', 'AR4Packet']
|
|
9
|
+
__all__ = ['AR4M', 'AR4', 'AR4Packet', 'X8']
|
|
7
10
|
|
|
8
11
|
# from importlib import import_module
|
|
9
12
|
# from pathlib import Path
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from multiprocessing import Queue
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from qlsdk.sdk import LMDevice, LMPacket
|
|
5
|
+
import numpy as np
|
|
6
|
+
from qlsdk.persist import EdfHandler
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
# 设备对象
|
|
10
|
+
class AR4(LMDevice):
|
|
11
|
+
def __init__(self, box_mac:str, is_persist:bool=True, storage_path:str=None):
|
|
12
|
+
# 是否持久化-保存为文件
|
|
13
|
+
self._is_persist = is_persist
|
|
14
|
+
self._storage_path = storage_path
|
|
15
|
+
self._edf_handler = None
|
|
16
|
+
|
|
17
|
+
self._recording = False
|
|
18
|
+
self._record_start_time = None
|
|
19
|
+
|
|
20
|
+
self._acq_info = {}
|
|
21
|
+
# 订阅者列表,数值为数字信号值
|
|
22
|
+
self._dig_subscriber: dict[str, Queue] = {}
|
|
23
|
+
# 订阅者列表,数值为物理信号值
|
|
24
|
+
self._phy_subscriber: dict[str, Queue] = {}
|
|
25
|
+
|
|
26
|
+
super().__init__(box_mac)
|
|
27
|
+
|
|
28
|
+
def set_storage_path(self, storage_path):
|
|
29
|
+
self._storage_path = storage_path
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def device_type(self):
|
|
33
|
+
return "AR4"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def eeg_accept(self, packet):
|
|
37
|
+
if len(self._dig_subscriber) > 0 or len(self._phy_subscriber) > 0:
|
|
38
|
+
for consumer in self._dig_subscriber.values():
|
|
39
|
+
consumer.put(packet)
|
|
40
|
+
|
|
41
|
+
if len(self._phy_subscriber) > 0:
|
|
42
|
+
logger.info(f"dig data eeg: {packet.eeg}")
|
|
43
|
+
logger.info(f"dig data acc: {packet.acc}")
|
|
44
|
+
packet.eeg = self.eeg2phy(np.array(packet.eeg))
|
|
45
|
+
packet.acc = self.acc2phy(np.array(packet.acc))
|
|
46
|
+
logger.info(f"phy data eeg: {packet.eeg}")
|
|
47
|
+
logger.info(f"phy data acc: {packet.acc}")
|
|
48
|
+
for consumer2 in self._phy_subscriber.values():
|
|
49
|
+
consumer2.put(packet)
|
|
50
|
+
|
|
51
|
+
if self._is_persist:
|
|
52
|
+
if self._edf_handler is None:
|
|
53
|
+
self.start_record()
|
|
54
|
+
self._recording = True
|
|
55
|
+
self._record_start_time = packet.time_stamp
|
|
56
|
+
logger.info(f"开始记录数据: {self.box_mac}")
|
|
57
|
+
|
|
58
|
+
# 处理数据包
|
|
59
|
+
self._edf_handler.append(packet)
|
|
60
|
+
|
|
61
|
+
def start_record(self):
|
|
62
|
+
if self._is_persist:
|
|
63
|
+
if self._edf_handler is None:
|
|
64
|
+
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)
|
|
65
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
66
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
67
|
+
|
|
68
|
+
def stop_record(self):
|
|
69
|
+
if self._edf_handler:
|
|
70
|
+
# 等待设备端数据传输完成
|
|
71
|
+
time.sleep(0.5)
|
|
72
|
+
# 添加结束标识
|
|
73
|
+
self._edf_handler.append(None)
|
|
74
|
+
self._edf_handler = None
|
|
75
|
+
self._recording = False
|
|
76
|
+
logger.info(f"停止记录数据: {self.box_mac}")
|
|
77
|
+
|
|
78
|
+
# 订阅推送消息
|
|
79
|
+
def subscribe(self, topic: str = None, q: Queue = Queue(), value_type: Literal['phy', 'dig'] = 'phy') -> tuple[str, Queue]:
|
|
80
|
+
if topic is None:
|
|
81
|
+
topic = f"msg_{self.box_mac}"
|
|
82
|
+
|
|
83
|
+
if value_type == 'dig':
|
|
84
|
+
if topic in list(self._dig_subscriber.keys()):
|
|
85
|
+
logger.warning(f"ar4 {self.box_mac} 订阅主题已存在: {topic}")
|
|
86
|
+
return topic, self._dig_subscriber[topic]
|
|
87
|
+
self._dig_subscriber[topic] = q
|
|
88
|
+
else:
|
|
89
|
+
if topic in list(self._phy_subscriber.keys()):
|
|
90
|
+
logger.warning(f"ar4 {self.box_mac} 订阅主题已存在: {topic}")
|
|
91
|
+
return topic, self._phy_subscriber[topic]
|
|
92
|
+
self._phy_subscriber[topic] = q
|
|
93
|
+
|
|
94
|
+
return topic, q
|
|
95
|
+
|
|
96
|
+
def eeg2phy(self, digtal):
|
|
97
|
+
# 向量化计算(自动支持广播)
|
|
98
|
+
return super().eeg2phy(digtal)
|
|
99
|
+
|
|
100
|
+
def acc2phy(self, digtal):
|
|
101
|
+
return super().eeg2phy(digtal)
|
|
102
|
+
|
|
103
|
+
class X8Packet(LMPacket):
|
|
104
|
+
def __init__(self, data: bytes):
|
|
105
|
+
super().__init__(data)
|
|
106
|
+
self._data = data
|
|
107
|
+
self._head = None
|
|
108
|
+
self._body = None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def head(self):
|
|
112
|
+
return self._head
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def body(self):
|
|
116
|
+
return self._body
|
|
117
|
+
|
|
118
|
+
def parse(self):
|
|
119
|
+
# 解析数据包头部和数据体
|
|
120
|
+
if len(self._data) < 4:
|
|
121
|
+
logger.error(f"数据包长度不足: {len(self._data)}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
# 解析头部和数据体
|
|
125
|
+
self._head = self._data[:4]
|
|
126
|
+
self._body = self._data[4:]
|
|
127
|
+
|
|
128
|
+
return True
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from qlsdk.sdk import Hub
|
|
2
|
+
from qlsdk.ar4 import AR4
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AR4M(Hub):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self._devices: dict[str, AR4] = {}
|
|
10
|
+
self._search_running = False
|
|
11
|
+
self._search_timer = None
|
|
12
|
+
|
|
13
|
+
def add_device(self, mac: str):
|
|
14
|
+
if mac in list(self._devices.keys()):
|
|
15
|
+
self._devices[mac].update_info()
|
|
16
|
+
logger.debug(f"update x8 device mac: {mac}")
|
|
17
|
+
else:
|
|
18
|
+
dev = AR4(mac)
|
|
19
|
+
if dev.connected:
|
|
20
|
+
self._devices[mac] = dev
|
|
21
|
+
logger.info(f"add x8 device mac: {dev}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .edf import EdfHandler
|
|
@@ -12,7 +12,10 @@ class EdfHandler(object):
|
|
|
12
12
|
self.physical_min = physical_min
|
|
13
13
|
self.digital_max = digital_max
|
|
14
14
|
self.digital_min = digital_min
|
|
15
|
-
self.
|
|
15
|
+
self.eeg_channels = None
|
|
16
|
+
self.eeg_sample_rate = 500
|
|
17
|
+
self.acc_channels = None
|
|
18
|
+
self.acc_sample_rate = 50
|
|
16
19
|
self._cache = Queue()
|
|
17
20
|
self.resolution = resolution
|
|
18
21
|
self.sample_frequency = sample_frequency
|
|
@@ -70,7 +73,8 @@ class EdfHandler(object):
|
|
|
70
73
|
if data:
|
|
71
74
|
# 通道数
|
|
72
75
|
if self._first_pkg_id is None:
|
|
73
|
-
self.
|
|
76
|
+
self.eeg_channels = data.eeg_ch_count
|
|
77
|
+
self.acc_channels = data.acc_ch_count
|
|
74
78
|
self._first_pkg_id = data.pkg_id
|
|
75
79
|
self._first_timestamp = data.time_stamp
|
|
76
80
|
|
|
@@ -98,28 +102,32 @@ class EdfHandler(object):
|
|
|
98
102
|
logger.debug(f"开始消费数据 _consumer: {self._cache.qsize()}")
|
|
99
103
|
while True:
|
|
100
104
|
if self._recording or (not self._cache.empty()):
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
try:
|
|
106
|
+
data = self._cache.get(timeout=10)
|
|
107
|
+
if data is None:
|
|
108
|
+
break
|
|
109
|
+
# 处理数据
|
|
110
|
+
self._points += len(data.eeg[0])
|
|
111
|
+
self._write_file(data.eeg, data.acc)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error("数据队列为空,超时(10s)结束")
|
|
103
114
|
break
|
|
104
|
-
# 处理数据
|
|
105
|
-
self._points += len(data.eeg[0])
|
|
106
|
-
self._write_file(data.eeg)
|
|
107
115
|
else:
|
|
108
116
|
break
|
|
109
117
|
|
|
110
118
|
self.close()
|
|
111
119
|
|
|
112
|
-
def _write_file(self,
|
|
120
|
+
def _write_file(self, eeg_data, acc_data):
|
|
113
121
|
try:
|
|
114
122
|
if self._edf_writer is None:
|
|
115
123
|
self.initialize_edf()
|
|
116
124
|
|
|
117
125
|
if (self._chunk.size == 0):
|
|
118
|
-
self._chunk = np.asarray(
|
|
126
|
+
self._chunk = np.asarray(eeg_data)
|
|
119
127
|
else:
|
|
120
|
-
self._chunk = np.hstack((self._chunk,
|
|
128
|
+
self._chunk = np.hstack((self._chunk, eeg_data))
|
|
121
129
|
|
|
122
|
-
if self._chunk.size >= self.
|
|
130
|
+
if self._chunk.size >= self.eeg_sample_rate * self.eeg_channels:
|
|
123
131
|
self._write_chunk(self._chunk[:self.sample_frequency])
|
|
124
132
|
self._chunk = self._chunk[self.sample_frequency:]
|
|
125
133
|
|
|
@@ -142,7 +150,7 @@ class EdfHandler(object):
|
|
|
142
150
|
# 创建EDF+写入器
|
|
143
151
|
self._edf_writer = EdfWriter(
|
|
144
152
|
self.file_name,
|
|
145
|
-
self.
|
|
153
|
+
self.eeg_channels,
|
|
146
154
|
file_type=self.file_type
|
|
147
155
|
)
|
|
148
156
|
|
|
@@ -154,7 +162,7 @@ class EdfHandler(object):
|
|
|
154
162
|
|
|
155
163
|
# 配置通道参数
|
|
156
164
|
signal_headers = []
|
|
157
|
-
for ch in range(self.
|
|
165
|
+
for ch in range(self.eeg_channels):
|
|
158
166
|
signal_headers.append({
|
|
159
167
|
"label": f'channels {ch + 1}',
|
|
160
168
|
"dimension": 'uV',
|