qlsdk2 0.2.0__tar.gz → 0.3.0__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.0 → qlsdk2-0.3.0}/PKG-INFO +2 -1
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/setup.py +4 -10
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/src/qlsdk/__init__.py +5 -2
- qlsdk2-0.3.0/src/qlsdk/ar4/__init__.py +128 -0
- qlsdk2-0.3.0/src/qlsdk/ar4m/__init__.py +21 -0
- qlsdk2-0.3.0/src/qlsdk/persist/__init__.py +1 -0
- qlsdk2-0.3.0/src/qlsdk/persist/edf.py +186 -0
- qlsdk2-0.3.0/src/qlsdk/sdk/__init__.py +2 -0
- {qlsdk2-0.2.0/src/qlsdk/ar4m → qlsdk2-0.3.0/src/qlsdk/sdk}/ar4sdk.py +293 -151
- qlsdk2-0.3.0/src/qlsdk/sdk/hub.py +51 -0
- qlsdk2-0.3.0/src/qlsdk/x8/__init__.py +128 -0
- qlsdk2-0.3.0/src/qlsdk/x8m/__init__.py +21 -0
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/src/qlsdk2.egg-info/PKG-INFO +2 -1
- qlsdk2-0.3.0/src/qlsdk2.egg-info/SOURCES.txt +21 -0
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/src/qlsdk2.egg-info/requires.txt +1 -0
- qlsdk2-0.2.0/src/qlsdk/ar4m/__init__.py +0 -50
- qlsdk2-0.2.0/src/qlsdk2.egg-info/SOURCES.txt +0 -14
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/README.md +0 -0
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/setup.cfg +0 -0
- {qlsdk2-0.2.0/src/qlsdk/ar4m → qlsdk2-0.3.0/src/qlsdk/sdk}/libs/libAr4SDK.dll +0 -0
- {qlsdk2-0.2.0/src/qlsdk/ar4m → qlsdk2-0.3.0/src/qlsdk/sdk}/libs/libwinpthread-1.dll +0 -0
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/src/qlsdk2.egg-info/top_level.txt +0 -0
- {qlsdk2-0.2.0 → qlsdk2-0.3.0}/test/test_ar4m.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: qlsdk2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: SDK for quanlan device
|
|
5
5
|
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
6
|
Author: hehuajun
|
|
@@ -12,6 +12,7 @@ Requires-Python: >=3.9
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: loguru>=0.6.0
|
|
14
14
|
Requires-Dist: numpy>=1.23.5
|
|
15
|
+
Requires-Dist: pyedflib>=0.1.40
|
|
15
16
|
Provides-Extra: dev
|
|
16
17
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
17
18
|
Requires-Dist: twine>=3.0; extra == "dev"
|
|
@@ -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.0",
|
|
10
10
|
author="hehuajun",
|
|
11
11
|
author_email="hehuajun@eegion.com",
|
|
12
12
|
description="SDK for quanlan device",
|
|
@@ -24,13 +24,7 @@ setuptools.setup(
|
|
|
24
24
|
install_requires=open("requirements.txt").read().splitlines(),
|
|
25
25
|
include_package_data=True,
|
|
26
26
|
package_data={
|
|
27
|
-
# "
|
|
28
|
-
"qlsdk
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
# entry_points={
|
|
32
|
-
# 'console_scripts': [
|
|
33
|
-
# 'qlsdk-cli=qlsdk.cli:main', # 如果有命令行工具
|
|
34
|
-
# ],
|
|
35
|
-
# },
|
|
27
|
+
# "qlsdk2": ["/**/*.dll"],
|
|
28
|
+
"qlsdk": ["./**/*.dll"]
|
|
29
|
+
}
|
|
36
30
|
)
|
|
@@ -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
|
|
@@ -0,0 +1,186 @@
|
|
|
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.eeg_channels = None
|
|
16
|
+
self.eeg_sample_rate = 500
|
|
17
|
+
self.acc_channels = None
|
|
18
|
+
self.acc_sample_rate = 50
|
|
19
|
+
self._cache = Queue()
|
|
20
|
+
self.resolution = resolution
|
|
21
|
+
self.sample_frequency = sample_frequency
|
|
22
|
+
# bytes per second
|
|
23
|
+
self.bytes_per_second = 0
|
|
24
|
+
self._edf_writer = None
|
|
25
|
+
self._cache2 = tuple()
|
|
26
|
+
self._recording = False
|
|
27
|
+
self._edf_writer = None
|
|
28
|
+
self.annotations = None
|
|
29
|
+
# 每个数据块大小
|
|
30
|
+
self._chunk = np.array([])
|
|
31
|
+
self._Lock = Lock()
|
|
32
|
+
self._duration = 0
|
|
33
|
+
self._points = 0
|
|
34
|
+
self._first_pkg_id = None
|
|
35
|
+
self._last_pkg_id = None
|
|
36
|
+
self._first_timestamp = None
|
|
37
|
+
self._end_time = None
|
|
38
|
+
self._patient_code = "patient_code"
|
|
39
|
+
self._patient_name = "patient_name"
|
|
40
|
+
self._device_type = None
|
|
41
|
+
self._total_packets = 0
|
|
42
|
+
self._lost_packets = 0
|
|
43
|
+
self._storage_path = storage_path
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def file_name(self):
|
|
47
|
+
if self._storage_path:
|
|
48
|
+
try:
|
|
49
|
+
os.makedirs(self._storage_path, exist_ok=True) # 自动创建目录,存在则忽略
|
|
50
|
+
return f"{self._storage_path}/{self._device_type}_{self._first_timestamp}.edf"
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"创建目录[{self._storage_path}]失败: {e}")
|
|
53
|
+
|
|
54
|
+
return f"{self._device_type}_{self._first_timestamp}.edf"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def file_type(self):
|
|
58
|
+
return FILETYPE_BDFPLUS if self.resolution == 24 else FILETYPE_EDFPLUS
|
|
59
|
+
|
|
60
|
+
def set_device_type(self, device_type):
|
|
61
|
+
self._device_type = device_type
|
|
62
|
+
|
|
63
|
+
def set_storage_path(self, storage_path):
|
|
64
|
+
self._storage_path = storage_path
|
|
65
|
+
|
|
66
|
+
def set_patient_code(self, patient_code):
|
|
67
|
+
self._patient_code = patient_code
|
|
68
|
+
|
|
69
|
+
def set_patient_name(self, patient_name):
|
|
70
|
+
self._patient_name = patient_name
|
|
71
|
+
|
|
72
|
+
def append(self, data):
|
|
73
|
+
if data:
|
|
74
|
+
# 通道数
|
|
75
|
+
if self._first_pkg_id is None:
|
|
76
|
+
self.eeg_channels = data.eeg_ch_count
|
|
77
|
+
self.acc_channels = data.acc_ch_count
|
|
78
|
+
self._first_pkg_id = data.pkg_id
|
|
79
|
+
self._first_timestamp = data.time_stamp
|
|
80
|
+
|
|
81
|
+
if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
|
|
82
|
+
self._lost_packets += data.pkg_id - self._last_pkg_id - 1
|
|
83
|
+
logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
|
|
84
|
+
|
|
85
|
+
self._last_pkg_id = data.pkg_id
|
|
86
|
+
self._total_packets += 1
|
|
87
|
+
|
|
88
|
+
# 数据
|
|
89
|
+
self._cache.put(data)
|
|
90
|
+
if not self._recording:
|
|
91
|
+
self.start()
|
|
92
|
+
|
|
93
|
+
def trigger(self, data):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def start(self):
|
|
97
|
+
self._recording = True
|
|
98
|
+
record_thread = Thread(target=self._consumer)
|
|
99
|
+
record_thread.start()
|
|
100
|
+
|
|
101
|
+
def _consumer(self):
|
|
102
|
+
logger.debug(f"开始消费数据 _consumer: {self._cache.qsize()}")
|
|
103
|
+
while True:
|
|
104
|
+
if self._recording or (not self._cache.empty()):
|
|
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)结束")
|
|
114
|
+
break
|
|
115
|
+
else:
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
self.close()
|
|
119
|
+
|
|
120
|
+
def _write_file(self, eeg_data, acc_data):
|
|
121
|
+
try:
|
|
122
|
+
if self._edf_writer is None:
|
|
123
|
+
self.initialize_edf()
|
|
124
|
+
|
|
125
|
+
if (self._chunk.size == 0):
|
|
126
|
+
self._chunk = np.asarray(eeg_data)
|
|
127
|
+
else:
|
|
128
|
+
self._chunk = np.hstack((self._chunk, eeg_data))
|
|
129
|
+
|
|
130
|
+
if self._chunk.size >= self.eeg_sample_rate * self.eeg_channels:
|
|
131
|
+
self._write_chunk(self._chunk[:self.sample_frequency])
|
|
132
|
+
self._chunk = self._chunk[self.sample_frequency:]
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"写入数据异常: {str(e)}")
|
|
136
|
+
|
|
137
|
+
def close(self):
|
|
138
|
+
self._recording = False
|
|
139
|
+
if self._edf_writer:
|
|
140
|
+
self._end_time = datetime.now().timestamp()
|
|
141
|
+
self._edf_writer.writeAnnotation(0, 1, "start recording ")
|
|
142
|
+
self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
|
|
143
|
+
self._edf_writer.close()
|
|
144
|
+
|
|
145
|
+
logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def initialize_edf(self):
|
|
150
|
+
# 创建EDF+写入器
|
|
151
|
+
self._edf_writer = EdfWriter(
|
|
152
|
+
self.file_name,
|
|
153
|
+
self.eeg_channels,
|
|
154
|
+
file_type=self.file_type
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# 设置头信息
|
|
158
|
+
self._edf_writer.setPatientCode(self._patient_code)
|
|
159
|
+
self._edf_writer.setPatientName(self._patient_name)
|
|
160
|
+
self._edf_writer.setEquipment(self._device_type)
|
|
161
|
+
self._edf_writer.setStartdatetime(datetime.now())
|
|
162
|
+
|
|
163
|
+
# 配置通道参数
|
|
164
|
+
signal_headers = []
|
|
165
|
+
for ch in range(self.eeg_channels):
|
|
166
|
+
signal_headers.append({
|
|
167
|
+
"label": f'channels {ch + 1}',
|
|
168
|
+
"dimension": 'uV',
|
|
169
|
+
"sample_frequency": self.sample_frequency,
|
|
170
|
+
"physical_min": self.physical_min,
|
|
171
|
+
"physical_max": self.physical_max,
|
|
172
|
+
"digital_min": self.digital_min,
|
|
173
|
+
"digital_max": self.digital_max
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
self._edf_writer.setSignalHeaders(signal_headers)
|
|
177
|
+
|
|
178
|
+
def _write_chunk(self, chunk):
|
|
179
|
+
logger.debug(f"写入数据: {chunk}")
|
|
180
|
+
# 转换数据类型为float64(pyedflib要求)
|
|
181
|
+
data_float64 = chunk.astype(np.float64)
|
|
182
|
+
# 写入时转置为(样本数, 通道数)格式
|
|
183
|
+
self._edf_writer.writeSamples(data_float64)
|
|
184
|
+
self._duration += 1
|
|
185
|
+
|
|
186
|
+
|
|
@@ -11,6 +11,7 @@ from loguru import logger
|
|
|
11
11
|
from time import sleep, time
|
|
12
12
|
import os
|
|
13
13
|
import numpy as np
|
|
14
|
+
# from .persist import EdfHandler
|
|
14
15
|
|
|
15
16
|
real_path = os.path.realpath(__file__)
|
|
16
17
|
dll_path = f'{os.path.dirname(real_path)}/libs/libAr4SDK.dll'
|
|
@@ -162,17 +163,64 @@ class AR4SDK:
|
|
|
162
163
|
|
|
163
164
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
164
165
|
pass
|
|
165
|
-
|
|
166
166
|
|
|
167
|
+
|
|
168
|
+
# 枚举后才能连接上
|
|
169
|
+
AR4SDK.enum_devices()
|
|
167
170
|
# 读取系统当前时间(ms)
|
|
168
171
|
def _get_time():
|
|
169
172
|
cur_time = int(round(time()) * 1000)
|
|
170
|
-
logger.debug(f"_get_time is {cur_time}")
|
|
171
173
|
return cur_time
|
|
174
|
+
|
|
175
|
+
class Packet(object):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
class LMPacket(Packet):
|
|
179
|
+
def __init__(self):
|
|
180
|
+
self.time_stamp = None
|
|
181
|
+
self.pkg_id = None
|
|
182
|
+
self.notify_id = None
|
|
183
|
+
self.eeg_ch_count = None
|
|
184
|
+
self.eeg_count = None
|
|
185
|
+
self.eeg = None
|
|
186
|
+
self.acc_ch_count = None
|
|
187
|
+
self.acc_count = None
|
|
188
|
+
self.acc = None
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
def transfer(self, data: Ar4NotifyData):
|
|
191
|
+
self.time_stamp = data.time_stamp
|
|
192
|
+
self.pkg_id = data.pkg_id
|
|
193
|
+
self.notify_id = data.notify_id
|
|
194
|
+
self.eeg_ch_count = data.eeg_ch_count
|
|
195
|
+
self.eeg_count = data.eeg_count
|
|
196
|
+
self.acc_ch_count = data.acc_ch_count
|
|
197
|
+
self.acc_count = data.acc_count
|
|
198
|
+
# 读eeg数据
|
|
199
|
+
if self.eeg_ch_count and self.eeg_count:
|
|
200
|
+
self.eeg = [data.eeg[i:self.eeg_count*self.eeg_ch_count:self.eeg_ch_count] for i in range(self.eeg_ch_count)]
|
|
201
|
+
# 读acc数据
|
|
202
|
+
if self.acc_ch_count and self.acc_count:
|
|
203
|
+
self.acc = [[] for _ in range(self.acc_ch_count)]
|
|
204
|
+
for i in range(self.acc_ch_count):
|
|
205
|
+
self.acc[i] = [data.acc[j + (i * self.acc_count)] for j in range(self.acc_count)]
|
|
206
|
+
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def __str__(self):
|
|
210
|
+
return f"""
|
|
211
|
+
time_stamp: {self.time_stamp}
|
|
212
|
+
pkg_id: {self.pkg_id}
|
|
213
|
+
notify_id: {self.notify_id}
|
|
214
|
+
eeg_ch_count: {self.eeg_ch_count}
|
|
215
|
+
eeg_count: {self.eeg_count}
|
|
216
|
+
acc_ch_count: {self.acc_ch_count}
|
|
217
|
+
acc_count: {self.acc_count}
|
|
218
|
+
eeg: {self.eeg}
|
|
219
|
+
acc: {self.acc}
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
class LMDevice(object):
|
|
223
|
+
def __init__(self, box_mac:str):
|
|
176
224
|
# 设备句柄
|
|
177
225
|
self._handle = None
|
|
178
226
|
# 设备基本信息
|
|
@@ -188,8 +236,8 @@ class AR4(object):
|
|
|
188
236
|
self._head_conn_state = None
|
|
189
237
|
self._head_soc = None
|
|
190
238
|
self._net_state = None
|
|
191
|
-
self._hub_name = hub_name
|
|
192
|
-
self._slot = slot
|
|
239
|
+
# self._hub_name = hub_name
|
|
240
|
+
# self._slot = slot
|
|
193
241
|
self._connected = False
|
|
194
242
|
self._conn_time = None
|
|
195
243
|
self._last_time = None
|
|
@@ -214,11 +262,9 @@ class AR4(object):
|
|
|
214
262
|
self._acc_phy_range = None
|
|
215
263
|
self._acc_dig_range = None
|
|
216
264
|
|
|
265
|
+
self._sample_frequency = 500
|
|
266
|
+
|
|
217
267
|
self._acq_info = {}
|
|
218
|
-
# 订阅者列表,数值为数字信号值
|
|
219
|
-
self._dig_subscriber: dict[str, Queue] = {}
|
|
220
|
-
# 订阅者列表,数值为物理信号值
|
|
221
|
-
self._phy_subscriber: dict[str, Queue] = {}
|
|
222
268
|
|
|
223
269
|
# 回调函数
|
|
224
270
|
self._data_callback = FuncAr4DataNotify(self._wrap_data_accept())
|
|
@@ -230,6 +276,16 @@ class AR4(object):
|
|
|
230
276
|
self._start_record_notify_callback = FuncAr4RecorderConnected(self._wrap_start_record_notify())
|
|
231
277
|
self._stop_record_notify_callback = FuncAr4RecorderDisconnected(self._wrap_stop_record_notify())
|
|
232
278
|
|
|
279
|
+
self.init()
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def connected(self):
|
|
283
|
+
return self._connected
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def sample_frequency(self):
|
|
287
|
+
return self._sample_frequency
|
|
288
|
+
|
|
233
289
|
@property
|
|
234
290
|
def box_mac(self):
|
|
235
291
|
return self._box_mac
|
|
@@ -239,47 +295,185 @@ class AR4(object):
|
|
|
239
295
|
@property
|
|
240
296
|
def hub_name(self):
|
|
241
297
|
return self._hub_name
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def eeg_phy_max(self):
|
|
301
|
+
return self._eeg_phy_max
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def eeg_phy_min(self):
|
|
305
|
+
return self._eeg_phy_min
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def eeg_dig_max(self):
|
|
309
|
+
return self._eeg_dig_max
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def eeg_dig_min(self):
|
|
313
|
+
return self._eeg_dig_min
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def acc_phy_max(self):
|
|
317
|
+
return self._acc_phy_max
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def acc_phy_min(self):
|
|
321
|
+
return self._acc_phy_min
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def acc_dig_max(self):
|
|
325
|
+
return self._acc_dig_max
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def acc_dig_min(self):
|
|
329
|
+
return self._acc_dig_min
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def eeg_phy_unit(self):
|
|
333
|
+
return self._eeg_phy_unit
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def acc_phy_unit(self):
|
|
337
|
+
return self._acc_phy_unit
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def eeg_phy_range(self):
|
|
341
|
+
return self._eeg_phy_range
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def eeg_dig_range(self):
|
|
345
|
+
return self._eeg_dig_range
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def acc_phy_range(self):
|
|
349
|
+
return self._acc_phy_range
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def acc_dig_range(self):
|
|
353
|
+
return self._acc_dig_range
|
|
354
|
+
|
|
355
|
+
def init(self):
|
|
356
|
+
if self.connect():
|
|
357
|
+
self.get_sample_rate()
|
|
246
358
|
## eeg 参数
|
|
247
|
-
self.
|
|
248
|
-
self.
|
|
249
|
-
self.
|
|
250
|
-
self.
|
|
359
|
+
self.get_eeg_phy_max()
|
|
360
|
+
self.get_eeg_phy_min()
|
|
361
|
+
self.get_eeg_phy_unit()
|
|
362
|
+
self.get_eeg_digital_max()
|
|
363
|
+
self.get_eeg_digital_min()
|
|
251
364
|
self._eeg_phy_range = self._eeg_phy_max - self._eeg_phy_min
|
|
252
365
|
self._eeg_dig_range = self._eeg_dig_max - self._eeg_dig_min
|
|
253
|
-
eeg_unit = _dll.ar4_sdk_get_eeg_phy_unit(self._handle)
|
|
254
|
-
if eeg_unit:
|
|
255
|
-
try:
|
|
256
|
-
self._eeg_phy_unit = eeg_unit.decode("utf-8")
|
|
257
|
-
except Exception as e:
|
|
258
|
-
logger.error(f"ar4 {self._box_mac} 获取eeg物理单位异常: {str(e)}")
|
|
259
366
|
|
|
260
367
|
## acc 参数
|
|
261
|
-
self.
|
|
262
|
-
self.
|
|
263
|
-
self.
|
|
264
|
-
self.
|
|
368
|
+
self.get_acc_phy_max()
|
|
369
|
+
self.get_acc_phy_min()
|
|
370
|
+
self.get_acc_phy_unit()
|
|
371
|
+
self.get_acc_digital_max()
|
|
372
|
+
self.get_acc_digital_min()
|
|
265
373
|
self._acc_phy_range = self._acc_phy_max - self._acc_phy_min
|
|
266
374
|
self._acc_dig_range = self._acc_dig_max - self._acc_dig_min
|
|
267
|
-
acc_unit = _dll.ar4_sdk_get_acc_phy_unit(self._handle)
|
|
268
|
-
if acc_unit:
|
|
269
|
-
try:
|
|
270
|
-
self._acc_phy_unit = eeg_unit.decode("utf-8")
|
|
271
|
-
except Exception as e:
|
|
272
|
-
logger.error(f"ar4 {self._box_mac} 获取acc物理单位异常: {str(e)}")
|
|
273
375
|
|
|
274
|
-
if self._connected:
|
|
275
|
-
self._conn_time = _get_time()
|
|
276
|
-
self.get_box_name()
|
|
277
|
-
self.get_head_mac()
|
|
278
|
-
self.get_record_conn_state()
|
|
279
|
-
self._last_time = _get_time()
|
|
280
376
|
logger.debug(self)
|
|
281
377
|
self._register_callback()
|
|
282
|
-
|
|
378
|
+
return True
|
|
379
|
+
else:
|
|
380
|
+
logger.error(f"设备[{self._box_mac}]连接失败")
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
def get_eeg_phy_max(self):
|
|
384
|
+
if self._handle:
|
|
385
|
+
try:
|
|
386
|
+
self._eeg_phy_max = _dll.ar4_sdk_get_eeg_phy_max(self._handle)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error(f"设备[{self._box_mac}]获取eeg物理最大值异常: {str(e)}")
|
|
389
|
+
|
|
390
|
+
return self._eeg_phy_max
|
|
391
|
+
|
|
392
|
+
def get_eeg_phy_min(self):
|
|
393
|
+
if self._handle:
|
|
394
|
+
try:
|
|
395
|
+
self._eeg_phy_min = _dll.ar4_sdk_get_eeg_phy_min(self._handle)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.error(f"设备[{self._box_mac}]获取eeg物理最小值异常: {str(e)}")
|
|
398
|
+
|
|
399
|
+
return self._eeg_phy_min
|
|
400
|
+
|
|
401
|
+
def get_eeg_phy_unit(self):
|
|
402
|
+
if self._handle:
|
|
403
|
+
try:
|
|
404
|
+
eeg_unit = _dll.ar4_sdk_get_eeg_phy_unit(self._handle)
|
|
405
|
+
if eeg_unit:
|
|
406
|
+
self._eeg_phy_unit = eeg_unit.decode("utf-8")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.error(f"设备[{self._box_mac}]获取eeg物理单位异常: {str(e)}")
|
|
409
|
+
|
|
410
|
+
return self._eeg_phy_unit
|
|
411
|
+
|
|
412
|
+
def get_eeg_digital_max(self):
|
|
413
|
+
if self._handle:
|
|
414
|
+
try:
|
|
415
|
+
self._eeg_dig_max = _dll.ar4_sdk_get_eeg_digtal_max(self._handle)
|
|
416
|
+
except Exception as e:
|
|
417
|
+
logger.error(f"设备[{self._box_mac}]获取eeg数字最大值异常: {str(e)}")
|
|
418
|
+
|
|
419
|
+
return self._eeg_dig_max
|
|
420
|
+
|
|
421
|
+
def get_eeg_digital_min(self):
|
|
422
|
+
if self._handle:
|
|
423
|
+
try:
|
|
424
|
+
self._eeg_dig_min = _dll.ar4_sdk_get_eeg_digtal_min(self._handle)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.error(f"设备[{self._box_mac}]获取eeg数字最小值异常: {str(e)}")
|
|
427
|
+
|
|
428
|
+
return self._eeg_dig_min
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def get_acc_phy_max(self):
|
|
432
|
+
if self._handle:
|
|
433
|
+
try:
|
|
434
|
+
self._acc_phy_max = _dll.ar4_sdk_get_acc_phy_max(self._handle)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(f"设备[{self._box_mac}]获取acc物理最大值异常: {str(e)}")
|
|
437
|
+
|
|
438
|
+
return self._acc_phy_max
|
|
439
|
+
|
|
440
|
+
def get_acc_phy_min(self):
|
|
441
|
+
if self._handle:
|
|
442
|
+
try:
|
|
443
|
+
self._acc_phy_min = _dll.ar4_sdk_get_acc_phy_min(self._handle)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
logger.error(f"设备[{self._box_mac}]获取acc物理最小值异常: {str(e)}")
|
|
446
|
+
|
|
447
|
+
return self._eeg_phy_min
|
|
448
|
+
|
|
449
|
+
def get_acc_phy_unit(self):
|
|
450
|
+
if self._handle:
|
|
451
|
+
try:
|
|
452
|
+
acc_unit = _dll.ar4_sdk_get_acc_phy_unit(self._handle)
|
|
453
|
+
if acc_unit:
|
|
454
|
+
self._acc_phy_unit = acc_unit.decode("utf-8")
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logger.error(f"设备[{self._box_mac}]获取acc物理单位异常: {str(e)}")
|
|
457
|
+
|
|
458
|
+
return self._acc_phy_unit
|
|
459
|
+
|
|
460
|
+
def get_acc_digital_max(self):
|
|
461
|
+
if self._handle:
|
|
462
|
+
try:
|
|
463
|
+
self._acc_dig_max = _dll.ar4_sdk_get_acc_digtal_max(self._handle)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
logger.error(f"设备[{self._box_mac}]获取acc数字最大值异常: {str(e)}")
|
|
466
|
+
|
|
467
|
+
return self._acc_dig_max
|
|
468
|
+
|
|
469
|
+
def get_acc_digital_min(self):
|
|
470
|
+
if self._handle:
|
|
471
|
+
try:
|
|
472
|
+
self._acc_dig_min = _dll.ar4_sdk_get_acc_digtal_min(self._handle)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.error(f"设备[{self._box_mac}]获取acc数字最小值异常: {str(e)}")
|
|
475
|
+
|
|
476
|
+
return self._acc_dig_min
|
|
283
477
|
|
|
284
478
|
def _register_callback(self):
|
|
285
479
|
try:
|
|
@@ -289,7 +483,7 @@ class AR4(object):
|
|
|
289
483
|
_dll.ar4_sdk_register_record_state_notify(self._handle, self._start_record_notify_callback, self._stop_record_notify_callback)
|
|
290
484
|
_dll.ar4_sdk_register_box_conn_notify(self._handle, self._box_connected_notify_callback, self._box_disconnected_notify_callback)
|
|
291
485
|
except Exception as e:
|
|
292
|
-
logger.error(f"回调函数注册异常: {str(e)}")
|
|
486
|
+
logger.error(f"设备[{self._box_mac}]回调函数注册异常: {str(e)}")
|
|
293
487
|
|
|
294
488
|
def update_info(self):
|
|
295
489
|
self._last_time = _get_time()
|
|
@@ -297,17 +491,28 @@ class AR4(object):
|
|
|
297
491
|
# 设备连接
|
|
298
492
|
def connect(self)-> bool:
|
|
299
493
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
494
|
+
if not self._box_mac:
|
|
495
|
+
raise Exception("设备MAC地址不能为空")
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
self._handle = _dll.ar4_sdk_connect(int(self._box_mac, 16), c_void_p(0))
|
|
499
|
+
# logger.info(f"conn handle is {self._handle}")
|
|
500
|
+
self._connected = self._handle is not None
|
|
501
|
+
if self._handle:
|
|
502
|
+
self._connected = True
|
|
503
|
+
self._handle = c_void_p(self._handle)
|
|
504
|
+
self._conn_time = _get_time()
|
|
505
|
+
self.get_box_name()
|
|
506
|
+
self.get_head_mac()
|
|
507
|
+
self.get_record_conn_state()
|
|
508
|
+
self._last_time = _get_time()
|
|
509
|
+
else:
|
|
510
|
+
raise Exception(f"设备 {self._box_mac} 连接失败: {self._handle}")
|
|
511
|
+
logger.debug(f"ar4 {self._box_mac} 连接: {self._handle}")
|
|
512
|
+
except Exception as e:
|
|
513
|
+
logger.error(f"ar4 {self._box_mac} 连接异常: {str(e)}")
|
|
514
|
+
|
|
515
|
+
return self._connected
|
|
311
516
|
|
|
312
517
|
# 读取盒子名称
|
|
313
518
|
def get_box_name(self):
|
|
@@ -333,23 +538,27 @@ class AR4(object):
|
|
|
333
538
|
|
|
334
539
|
# 数据采集启动
|
|
335
540
|
def start_acquisition(self):
|
|
336
|
-
logger.info(f"
|
|
337
|
-
|
|
541
|
+
logger.info(f"device {self._box_mac} 启动数据采集...")
|
|
542
|
+
if not self._handle:
|
|
543
|
+
self.init()
|
|
338
544
|
|
|
339
545
|
self._acq_info["start_time"] = _get_time()
|
|
340
546
|
|
|
341
547
|
if self._handle:
|
|
342
548
|
# 启动采集
|
|
343
549
|
try:
|
|
344
|
-
logger.debug(f"
|
|
550
|
+
logger.debug(f"device {self._box_mac} 启动采集: {self._handle}")
|
|
345
551
|
ret = _dll.ar4_sdk_start_acq(self._handle)
|
|
346
|
-
|
|
347
|
-
|
|
552
|
+
if ret == 0:
|
|
553
|
+
logger.info(f"device {self._box_mac} 启动数据采集成功")
|
|
554
|
+
self.start_record()
|
|
555
|
+
else:
|
|
556
|
+
logger.error(f"device {self._box_mac} 启动数据采集失败, ret: {ret}")
|
|
348
557
|
return ret == 0
|
|
349
558
|
except Exception as e:
|
|
350
|
-
logger.error(f"
|
|
559
|
+
logger.error(f"device {self._box_mac} 停止采集异常: {str(e)}")
|
|
351
560
|
else:
|
|
352
|
-
logger.info(f"
|
|
561
|
+
logger.info(f"device {self._box_mac} 启动数据采集失败, 设备未连接")
|
|
353
562
|
return False
|
|
354
563
|
|
|
355
564
|
def stop_acquisition(self):
|
|
@@ -360,11 +569,23 @@ class AR4(object):
|
|
|
360
569
|
except Exception as e:
|
|
361
570
|
logger.error(f"ar4 {self._box_mac} 获取开始采集时间异常: {str(e)}")
|
|
362
571
|
try:
|
|
363
|
-
|
|
572
|
+
ret = _dll.ar4_sdk_stop_acq(self._handle)
|
|
573
|
+
if ret == 0:
|
|
574
|
+
logger.info(f"device {self._box_mac} 停止采集成功")
|
|
575
|
+
self.stop_record()
|
|
576
|
+
else:
|
|
577
|
+
logger.error(f"device {self._box_mac} 停止采集失败, ret: {ret}")
|
|
578
|
+
|
|
579
|
+
return ret == 0
|
|
364
580
|
except Exception as e:
|
|
365
581
|
logger.error(f"ar4 {self._box_mac} 停止采集异常: {str(e)}")
|
|
366
582
|
else:
|
|
367
583
|
return False
|
|
584
|
+
|
|
585
|
+
def start_record(self):
|
|
586
|
+
pass
|
|
587
|
+
def stop_record(self):
|
|
588
|
+
pass
|
|
368
589
|
|
|
369
590
|
def disconnect(self):
|
|
370
591
|
"""断开连接"""
|
|
@@ -374,7 +595,9 @@ class AR4(object):
|
|
|
374
595
|
def get_sample_rate(self):
|
|
375
596
|
try:
|
|
376
597
|
ret = _dll.ar4_sdk_get_record_sample_rate(self._handle)
|
|
377
|
-
logger.debug(f"ar4 {self._box_mac} 获取采样率: {ret
|
|
598
|
+
logger.debug(f"ar4 {self._box_mac} 获取采样率: {ret}")
|
|
599
|
+
if ret > 1:
|
|
600
|
+
self._sample_frequency = ret
|
|
378
601
|
except Exception as e:
|
|
379
602
|
logger.error(f"ar4 {self._box_mac} 获取采样率异常: {str(e)}")
|
|
380
603
|
|
|
@@ -387,24 +610,6 @@ class AR4(object):
|
|
|
387
610
|
logger.debug(f"ar4 {self._box_mac} 获取采样开始时间: {ret}")
|
|
388
611
|
except Exception as e:
|
|
389
612
|
logger.error(f"ar4 {self._box_mac} 获取采样开始时间异常: {str(e)}")
|
|
390
|
-
|
|
391
|
-
# 订阅推送消息
|
|
392
|
-
def subscribe(self, topic: str = None, q: Queue = Queue(), value_type: Literal['phy', 'dig'] = 'phy') -> tuple[str, Queue]:
|
|
393
|
-
if topic is None:
|
|
394
|
-
topic = f"msg_{_get_time()}"
|
|
395
|
-
|
|
396
|
-
if value_type == 'dig':
|
|
397
|
-
if topic in list(self._dig_subscriber.keys()):
|
|
398
|
-
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
399
|
-
return topic, self._dig_subscriber[topic]
|
|
400
|
-
self._dig_subscriber[topic] = q
|
|
401
|
-
else:
|
|
402
|
-
if topic in list(self._phy_subscriber.keys()):
|
|
403
|
-
logger.warning(f"ar4 {self._box_mac} 订阅主题已存在: {topic}")
|
|
404
|
-
return topic, self._phy_subscriber[topic]
|
|
405
|
-
self._phy_subscriber[topic] = q
|
|
406
|
-
|
|
407
|
-
return topic, q
|
|
408
613
|
|
|
409
614
|
def _wrap_data_accept(self):
|
|
410
615
|
|
|
@@ -414,23 +619,10 @@ class AR4(object):
|
|
|
414
619
|
|
|
415
620
|
return data_accept
|
|
416
621
|
def _data_accept(self, data_ptr):
|
|
417
|
-
|
|
418
|
-
# logger.debug(f'handle:{self}, data_ptr:{data_ptr}')
|
|
419
|
-
# data = cast(data_ptr, POINTER(Ar4NotifyData)).contents
|
|
420
|
-
if len(self._dig_subscriber) > 0 or len(self._phy_subscriber) > 0:
|
|
421
|
-
packet = AR4Packet().transfer(data_ptr.contents)
|
|
422
|
-
|
|
423
|
-
for consumer in self._dig_subscriber.values():
|
|
424
|
-
consumer.put(packet)
|
|
425
|
-
|
|
426
|
-
if len(self._phy_subscriber) > 0:
|
|
427
|
-
packet.eeg = self.eeg2phy(np.array(packet.eeg))
|
|
428
|
-
packet.acc = self.acc2phy(np.array(packet.acc))
|
|
429
|
-
for consumer2 in self._phy_subscriber.values():
|
|
430
|
-
consumer2.put(packet)
|
|
431
|
-
|
|
432
|
-
# logger.debug(f"EEG数据: {packet}")
|
|
622
|
+
self.eeg_accept(LMPacket().transfer(data_ptr.contents))
|
|
433
623
|
|
|
624
|
+
def eeg_accept(self, packet: LMPacket):
|
|
625
|
+
pass
|
|
434
626
|
def eeg2phy(self, digtal):
|
|
435
627
|
# 向量化计算(自动支持广播)
|
|
436
628
|
return ((digtal - self._eeg_dig_min) / self._eeg_dig_range) * self._eeg_phy_range + self._eeg_phy_min
|
|
@@ -506,8 +698,7 @@ class AR4(object):
|
|
|
506
698
|
logger.info(f"_start_record_notify 被调用 handle: {handle}")
|
|
507
699
|
self._recording = True
|
|
508
700
|
self._record_start_time = time()
|
|
509
|
-
logger.info(self)
|
|
510
|
-
|
|
701
|
+
logger.info(self)
|
|
511
702
|
|
|
512
703
|
def _wrap_stop_record_notify(self):
|
|
513
704
|
@FuncAr4RecorderDisconnected
|
|
@@ -545,56 +736,7 @@ class AR4(object):
|
|
|
545
736
|
acc dig min: {self._acc_dig_min}
|
|
546
737
|
acc phy unit: {self._acc_phy_unit}
|
|
547
738
|
]
|
|
739
|
+
dig -> ((dig - {self._eeg_dig_min}) / {self._eeg_dig_range}) * {self._eeg_phy_range} + {self._eeg_phy_min}
|
|
740
|
+
dig -> ((dig - {self._acc_dig_min}) / {self._acc_dig_range}) * {self._acc_phy_range} + {self._acc_phy_min}
|
|
548
741
|
"""
|
|
549
|
-
|
|
550
|
-
class Packet(object):
|
|
551
|
-
pass
|
|
552
|
-
|
|
553
|
-
class AR4Packet(Packet):
|
|
554
|
-
def __init__(self):
|
|
555
|
-
self.time_stamp = None
|
|
556
|
-
self.pkg_id = None
|
|
557
|
-
self.notify_id = None
|
|
558
|
-
self.eeg_ch_count = None
|
|
559
|
-
self.eeg_count = None
|
|
560
|
-
self.eeg = None
|
|
561
|
-
self.acc_ch_count = None
|
|
562
|
-
self.acc_count = None
|
|
563
|
-
self.acc = None
|
|
564
|
-
|
|
565
|
-
def transfer(self, data: Ar4NotifyData):
|
|
566
|
-
self.time_stamp = data.time_stamp
|
|
567
|
-
self.pkg_id = data.pkg_id
|
|
568
|
-
self.notify_id = data.notify_id
|
|
569
|
-
self.eeg_ch_count = data.eeg_ch_count
|
|
570
|
-
self.eeg_count = data.eeg_count
|
|
571
|
-
self.acc_ch_count = data.acc_ch_count
|
|
572
|
-
self.acc_count = data.acc_count
|
|
573
|
-
# 读eeg数据
|
|
574
|
-
if self.eeg_ch_count and self.eeg_count:
|
|
575
|
-
# self.eeg = [[] for _ in range(self.eeg_ch_count)]
|
|
576
|
-
# for i in range(self.eeg_ch_count):
|
|
577
|
-
# self.eeg[i] = [data.eeg[j + (i * self.eeg_count)] for j in range(self.eeg_count)]
|
|
578
|
-
# tmp = data.eeg[:self.eeg_ch_count * self.eeg_count]
|
|
579
|
-
# logger.info(tmp)
|
|
580
|
-
self.eeg = [data.eeg[i:self.eeg_count*self.eeg_ch_count:self.eeg_ch_count] for i in range(self.eeg_ch_count)]
|
|
581
|
-
# 读acc数据
|
|
582
|
-
if self.acc_ch_count and self.acc_count:
|
|
583
|
-
self.acc = [[] for _ in range(self.acc_ch_count)]
|
|
584
|
-
for i in range(self.acc_ch_count):
|
|
585
|
-
self.acc[i] = [data.acc[j + (i * self.acc_count)] for j in range(self.acc_count)]
|
|
586
|
-
|
|
587
|
-
return self
|
|
588
|
-
|
|
589
|
-
def __str__(self):
|
|
590
|
-
return f"""
|
|
591
|
-
time_stamp: {self.time_stamp}
|
|
592
|
-
pkg_id: {self.pkg_id}
|
|
593
|
-
notify_id: {self.notify_id}
|
|
594
|
-
eeg_ch_count: {self.eeg_ch_count}
|
|
595
|
-
eeg_count: {self.eeg_count}
|
|
596
|
-
acc_ch_count: {self.acc_ch_count}
|
|
597
|
-
acc_count: {self.acc_count}
|
|
598
|
-
eeg: {self.eeg}
|
|
599
|
-
acc: {self.acc}
|
|
600
|
-
"""
|
|
742
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from qlsdk.sdk.ar4sdk import AR4SDK, LMDevice
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from time import sleep, time
|
|
5
|
+
from threading import Lock, Timer
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
class Hub(object):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._lock = Lock()
|
|
11
|
+
self._search_timer = None
|
|
12
|
+
self._search_running = False
|
|
13
|
+
self._devices: dict[str, LMDevice] = {}
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def devices(self):
|
|
17
|
+
return self._devices
|
|
18
|
+
|
|
19
|
+
def search(self):
|
|
20
|
+
if not self._search_running:
|
|
21
|
+
self._search_running = True
|
|
22
|
+
self._search()
|
|
23
|
+
|
|
24
|
+
def _search(self):
|
|
25
|
+
if self._search_running:
|
|
26
|
+
|
|
27
|
+
self._search_timer = Timer(2, self._search_device)
|
|
28
|
+
self._search_timer.daemon = True
|
|
29
|
+
self._search_timer.start()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _search_device(self):
|
|
33
|
+
try:
|
|
34
|
+
devices = AR4SDK.enum_devices()
|
|
35
|
+
# logger.debug(f"_search_ar4 devices size: {len(devices)}")
|
|
36
|
+
for dev in devices:
|
|
37
|
+
self.add_device(hex(dev.mac))
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.error(f"_search_device 异常: {str(e)}")
|
|
40
|
+
finally:
|
|
41
|
+
self._search()
|
|
42
|
+
|
|
43
|
+
def add_device(self, mac: str):
|
|
44
|
+
if mac in list(self._devices.keys()):
|
|
45
|
+
self._devices[mac].update_info()
|
|
46
|
+
logger.info(f"update device mac: {mac}")
|
|
47
|
+
else:
|
|
48
|
+
dev = LMDevice(mac)
|
|
49
|
+
if dev.init():
|
|
50
|
+
self._devices[mac] = dev
|
|
51
|
+
logger.info(f"add device mac: {dev}")
|
|
@@ -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 X8(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 "x8"
|
|
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.x8 import X8
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class X8M(Hub):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self._devices: dict[str, X8] = {}
|
|
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 = X8(mac)
|
|
19
|
+
if dev.connected:
|
|
20
|
+
self._devices[mac] = dev
|
|
21
|
+
logger.info(f"add x8 device mac: {dev}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: qlsdk2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: SDK for quanlan device
|
|
5
5
|
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
6
|
Author: hehuajun
|
|
@@ -12,6 +12,7 @@ Requires-Python: >=3.9
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: loguru>=0.6.0
|
|
14
14
|
Requires-Dist: numpy>=1.23.5
|
|
15
|
+
Requires-Dist: pyedflib>=0.1.40
|
|
15
16
|
Provides-Extra: dev
|
|
16
17
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
17
18
|
Requires-Dist: twine>=3.0; extra == "dev"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.cfg
|
|
3
|
+
setup.py
|
|
4
|
+
src/qlsdk/__init__.py
|
|
5
|
+
src/qlsdk2.egg-info/PKG-INFO
|
|
6
|
+
src/qlsdk2.egg-info/SOURCES.txt
|
|
7
|
+
src/qlsdk2.egg-info/dependency_links.txt
|
|
8
|
+
src/qlsdk2.egg-info/requires.txt
|
|
9
|
+
src/qlsdk2.egg-info/top_level.txt
|
|
10
|
+
src/qlsdk/ar4/__init__.py
|
|
11
|
+
src/qlsdk/ar4m/__init__.py
|
|
12
|
+
src/qlsdk/persist/__init__.py
|
|
13
|
+
src/qlsdk/persist/edf.py
|
|
14
|
+
src/qlsdk/sdk/__init__.py
|
|
15
|
+
src/qlsdk/sdk/ar4sdk.py
|
|
16
|
+
src/qlsdk/sdk/hub.py
|
|
17
|
+
src/qlsdk/sdk/libs/libAr4SDK.dll
|
|
18
|
+
src/qlsdk/sdk/libs/libwinpthread-1.dll
|
|
19
|
+
src/qlsdk/x8/__init__.py
|
|
20
|
+
src/qlsdk/x8m/__init__.py
|
|
21
|
+
test/test_ar4m.py
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
2
|
-
|
|
3
|
-
from .ar4sdk import AR4SDK, AR4, Packet, AR4Packet
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from time import sleep, time
|
|
7
|
-
from threading import Lock, Timer
|
|
8
|
-
from loguru import logger
|
|
9
|
-
|
|
10
|
-
class AR4M(object):
|
|
11
|
-
def __init__(self):
|
|
12
|
-
self._lock = Lock()
|
|
13
|
-
self._search_timer = None
|
|
14
|
-
self._search_running = False
|
|
15
|
-
self._devices: dict[str, AR4] = {}
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def devices(self):
|
|
19
|
-
return self._devices
|
|
20
|
-
def search(self):
|
|
21
|
-
if not self._search_running:
|
|
22
|
-
self._search_running = True
|
|
23
|
-
self._search()
|
|
24
|
-
|
|
25
|
-
def _search(self):
|
|
26
|
-
if self._search_running:
|
|
27
|
-
|
|
28
|
-
self._search_timer = Timer(2, self._search_ar4)
|
|
29
|
-
self._search_timer.daemon = True
|
|
30
|
-
self._search_timer.start()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _search_ar4(self):
|
|
34
|
-
try:
|
|
35
|
-
devices = AR4SDK.enum_devices()
|
|
36
|
-
# logger.debug(f"_search_ar4 devices size: {len(devices)}")
|
|
37
|
-
for dev in devices:
|
|
38
|
-
# logger.debug(f"slot: {dev.slot}, mac: {dev.mac}-{hex(dev.mac)}, hub_name: {dev.hub_name.str}")
|
|
39
|
-
if dev.mac in list(self._devices.keys()):
|
|
40
|
-
ar4 = self._devices[dev.mac]
|
|
41
|
-
ar4.update_info()
|
|
42
|
-
else:
|
|
43
|
-
ar4 = AR4(hex(dev.mac), dev.slot, dev.hub_name.str.decode("utf-8"))
|
|
44
|
-
if ar4.init():
|
|
45
|
-
self._devices[dev.mac] = ar4
|
|
46
|
-
logger.info(f"add device mac: {ar4.box_mac} slot: {ar4.slot} hub_name: {ar4.hub_name}")
|
|
47
|
-
except Exception as e:
|
|
48
|
-
logger.error(f"_search_ar4 异常: {str(e)}")
|
|
49
|
-
finally:
|
|
50
|
-
self._search()
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
README.md
|
|
2
|
-
setup.cfg
|
|
3
|
-
setup.py
|
|
4
|
-
src/qlsdk/__init__.py
|
|
5
|
-
src/qlsdk2.egg-info/PKG-INFO
|
|
6
|
-
src/qlsdk2.egg-info/SOURCES.txt
|
|
7
|
-
src/qlsdk2.egg-info/dependency_links.txt
|
|
8
|
-
src/qlsdk2.egg-info/requires.txt
|
|
9
|
-
src/qlsdk2.egg-info/top_level.txt
|
|
10
|
-
src/qlsdk/ar4m/__init__.py
|
|
11
|
-
src/qlsdk/ar4m/ar4sdk.py
|
|
12
|
-
src/qlsdk/ar4m/libs/libAr4SDK.dll
|
|
13
|
-
src/qlsdk/ar4m/libs/libwinpthread-1.dll
|
|
14
|
-
test/test_ar4m.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|