qlsdk2 0.2.0__py3-none-any.whl → 0.3.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/__init__.py +5 -2
- qlsdk/ar4/__init__.py +128 -0
- qlsdk/ar4m/__init__.py +16 -45
- qlsdk/ar4m/ar4sdk.py +421 -10
- qlsdk/ar4m/persist.py +178 -0
- qlsdk/persist/__init__.py +1 -0
- qlsdk/persist/edf.py +186 -0
- qlsdk/sdk/__init__.py +2 -0
- qlsdk/sdk/ar4sdk.py +742 -0
- qlsdk/sdk/hub.py +51 -0
- qlsdk/x8/__init__.py +128 -0
- qlsdk/x8m/__init__.py +21 -0
- {qlsdk2-0.2.0.dist-info → qlsdk2-0.3.0a1.dist-info}/METADATA +2 -1
- qlsdk2-0.3.0a1.dist-info/RECORD +18 -0
- qlsdk2-0.2.0.dist-info/RECORD +0 -9
- {qlsdk2-0.2.0.dist-info → qlsdk2-0.3.0a1.dist-info}/WHEEL +0 -0
- {qlsdk2-0.2.0.dist-info → qlsdk2-0.3.0a1.dist-info}/top_level.txt +0 -0
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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .edf import EdfHandler
|
qlsdk/persist/edf.py
ADDED
|
@@ -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
|
+
|
qlsdk/sdk/__init__.py
ADDED