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/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
@@ -0,0 +1,2 @@
1
+ from .ar4sdk import LMDevice, LMPacket, AR4SDK
2
+ from .hub import Hub