qlsdk2 0.4.0a2__py3-none-any.whl → 0.4.1__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/persist/rsc_edf.py CHANGED
@@ -1,124 +1,195 @@
1
1
  from datetime import datetime
2
2
  from multiprocessing import Lock, Queue
3
+ from time import time_ns
3
4
  from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
4
5
  from threading import Thread
5
6
  from loguru import logger
6
7
  import numpy as np
7
8
  import os
9
+ from qlsdk.core import RscPacket
8
10
 
9
- class EDFWriterThread(Thread):
10
- def __init__(self, edf_writer : EdfWriter):
11
- super().__init__(self)
12
- self._edf_writer : EdfWriter = edf_writer
11
+ EDF_FILE_TYPE = {
12
+ "bdf": FILETYPE_BDFPLUS,
13
+ "edf": FILETYPE_EDFPLUS
14
+ }
15
+
16
+ class EDFStreamWriter(Thread):
17
+ def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path):
18
+ super().__init__()
19
+ self._writer : EdfWriter = None
13
20
  self.data_queue : Queue = Queue()
14
- self._stop_event : bool = False
15
21
  self._recording = False
16
- self._chunk = np.array([])
17
22
  self._points = 0
18
23
  self._duration = 0
19
- self._sample_frequency = 0
20
- self._total_packets = 0
21
- self._channels = []
22
- self._sample_rate = 0
24
+ self._buffer = None
25
+
26
+ # signal info
27
+ self._channels = channels
28
+ self._n_channels = len(channels)
29
+ self.sample_frequency = sample_frequency
30
+ self.physical_max = physical_max
31
+ self.physical_min = digital_min
32
+ # 和位数相关,edf 16 bits/bdf 24 bits
33
+ self.digital_max = 8388607 if file_type == EDF_FILE_TYPE['bdf'] else 32767
34
+ self.digital_min = -8388608 if file_type == EDF_FILE_TYPE['bdf'] else -32768
35
+ self.file_type = file_type
36
+ self.file_path = file_path
37
+
38
+ # 记录开始时间
39
+ self.start_time = None
40
+
41
+ # header info
42
+ self.equipment = "equipment"
43
+ self.patient_code = "patient_code"
44
+ self.patient_name = "patient_name"
23
45
 
24
- def start(self):
25
- self._stop_event = False
26
- super().start()
46
+ def set_channels(self, channels):
47
+ self._channels = channels
48
+ self._n_channels = len(channels)
27
49
 
28
- def stop(self):
29
- self._stop_event = True
50
+ def set_sample_rate(self, sample_rate):
51
+ self._sample_rate = sample_rate
52
+
53
+ def set_start_time(self, time):
54
+ self.start_time = time
55
+
56
+ def stop_recording(self):
57
+ self._recording = False
30
58
 
31
59
  def append(self, data):
32
- # 数据
33
- self.data_queue.put(data)
60
+ if data:
61
+ # 数据
62
+ self.data_queue.put(data)
63
+
64
+ def trigger(self, onset, desc):
65
+ if self._writer:
66
+ self._writer.writeAnnotation(onset, 1, desc)
67
+ else:
68
+ logger.warning("未创建文件,无法写入Trigger标记")
69
+
70
+
71
+ def run(self):
72
+ logger.debug(f"启动bdf文件写入线程,写入数据到文件 {self.file_path}")
73
+ # 记录状态
74
+ self._recording = True
34
75
 
35
- def _consumer(self):
36
- logger.debug(f"开始消费数据 _consumer: {self.data_queue.qsize()}")
76
+ # 初始化
77
+ if self._writer is None:
78
+ self._init_writer()
79
+
37
80
  while True:
38
81
  if self._recording or (not self.data_queue.empty()):
39
82
  try:
40
- data = self.data_queue.get(timeout=10)
83
+ data = self.data_queue.get(timeout=30)
41
84
  if data is None:
85
+ logger.debug("收到结束信号,停止写入数据")
42
86
  break
43
87
  # 处理数据
44
- self._points += len(data)
88
+ self._points += len(data[1])
89
+ logger.trace(f"已处理数据点数:{self._points}")
45
90
  self._write_file(data)
46
91
  except Exception as e:
47
- logger.error("数据队列为空,超时(10s)结束")
92
+ logger.error(f"异常或超时(30s)结束: {str(e)}")
48
93
  break
49
94
  else:
95
+ logger.debug("数据记录完成")
50
96
  break
51
97
 
52
98
  self.close()
53
99
 
100
+ def _init_writer(self):
101
+
102
+ # 创建EDF+写入器
103
+ self._writer = EdfWriter(
104
+ self.file_path,
105
+ self._n_channels,
106
+ file_type=self.file_type
107
+ )
108
+
109
+ # 设置头信息
110
+ self._writer.setPatientCode(self.patient_code)
111
+ self._writer.setPatientName(self.patient_name)
112
+ self._writer.setEquipment(self.equipment)
113
+ self._writer.setStartdatetime(self.start_time if self.start_time else datetime.now())
114
+
115
+ # 配置通道参数
116
+ signal_headers = []
117
+ for ch in range(self._n_channels):
118
+ signal_headers.append({
119
+ "label": f'channels {self._channels[ch]}',
120
+ "dimension": 'uV',
121
+ "sample_frequency": self.sample_frequency,
122
+ "physical_min": self.physical_min,
123
+ "physical_max": self.physical_max,
124
+ "digital_min": self.digital_min,
125
+ "digital_max": self.digital_max
126
+ })
127
+
128
+ self._writer.setSignalHeaders(signal_headers)
129
+
54
130
  def _write_file(self, eeg_data):
55
131
  try:
56
- if (self._chunk.size == 0):
57
- self._chunk = np.asarray(eeg_data)
132
+ if self._buffer is None or self._buffer.size == 0:
133
+ self._buffer = np.asarray(eeg_data)
58
134
  else:
59
- self._chunk = np.hstack((self._chunk, eeg_data))
60
-
61
- if self._chunk.size >= self._sample_rate * self._channels:
62
- self._write_chunk(self._chunk[:self._sample_rate])
63
- self._chunk = self._chunk[self._sample_rate:]
135
+ self._buffer = np.hstack((self._buffer, eeg_data))
136
+
137
+ if self._buffer.shape[1] >= self.sample_frequency:
138
+ block = self._buffer[:, :self.sample_frequency]
139
+ self._write_block(block)
140
+ self._buffer = self._buffer[:, self.sample_frequency:]
64
141
 
65
142
  except Exception as e:
66
143
  logger.error(f"写入数据异常: {str(e)}")
67
144
 
68
145
  def close(self):
69
146
  self._recording = False
70
- if self._edf_writer:
71
- self._end_time = datetime.now().timestamp()
72
- self._edf_writer.writeAnnotation(0, 1, "start recording ")
73
- self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
74
- self._edf_writer.close()
75
-
76
- logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
147
+ if self._writer:
148
+ self._writer.writeAnnotation(0, 1, "recording start")
149
+ self._writer.writeAnnotation(self._duration, 1, "recording end")
150
+ self._writer.close()
77
151
 
152
+ logger.info(f"文件: {self.file_path}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒")
78
153
 
79
-
80
- def _write_chunk(self, chunk):
81
- logger.debug(f"写入数据: {chunk}")
82
- # 转换数据类型为float64(pyedflib要求)
83
- data_float64 = chunk.astype(np.float64)
154
+ # 写入1秒的数据
155
+ def _write_block(self, block):
156
+ logger.trace(f"写入数据: {block}")
157
+ # 转换数据类型为float64
158
+ data_float64 = block.astype(np.float64)
84
159
  # 写入时转置为(样本数, 通道数)格式
85
- self._edf_writer.writeSamples(data_float64)
160
+ self._writer.writeSamples(data_float64)
86
161
  self._duration += 1
87
162
 
163
+ # 用作数据结构一致化处理,通过调用公共类写入edf文件
164
+ # 入参包含写入edf的全部前置参数
165
+ # 实时数据包为个性化数据包,含有eeg数据部分
88
166
  class RscEDFHandler(object):
89
167
  '''
90
168
  Rsc EDF Handler
91
169
  处理EDF文件的读写
92
170
  RSC设备通道数根据选择变化,不同通道采样频率相同
93
- sample_frequency: 采样频率
94
- physical_max: 物理最大值
95
- physical_min: 物理最小值
96
- digital_max: 数字最大值
97
- digital_min: 数字最小值
171
+ eeg_sample_rate: 采样频率
172
+ physical_max: 物理最大值 (uV)
173
+ physical_min: 物理最小值 (uV)
98
174
  resolution: 分辨率
99
175
  storage_path: 存储路径
100
176
 
101
177
  @author: qlsdk
102
178
  @since: 0.4.0
103
179
  '''
104
- def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=32, storage_path = None):
180
+ def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
105
181
  # edf文件参数
106
182
  self.physical_max = physical_max
107
183
  self.physical_min = physical_min
108
- self.digital_max = digital_max
109
- self.digital_min = digital_min
184
+ self.digital_max = 8388607 if resolution == 24 else 32767
185
+ self.digital_min = -8388607 if resolution == 24 else - 32768
186
+ self.file_type = EDF_FILE_TYPE["bdf"] if resolution == 24 else EDF_FILE_TYPE["edf"]
110
187
  # 点分辨率
111
188
  self.resolution = resolution
112
189
  # eeg通道数
113
- self.eeg_channels = None
190
+ self.channels = None
114
191
  # eeg采样率
115
- self.eeg_sample_rate = 500
116
- self.acc_channels = None
117
- self.acc_sample_rate = 50
118
- # 缓存
119
- self._cache = Queue()
120
- # 采样频率
121
- self.sample_frequency = sample_frequency
192
+ self.sample_rate = eeg_sample_rate
122
193
  # bytes per second
123
194
  self.bytes_per_second = 0
124
195
  self._edf_writer = None
@@ -134,103 +205,96 @@ class RscEDFHandler(object):
134
205
  self._first_pkg_id = None
135
206
  self._last_pkg_id = None
136
207
  self._first_timestamp = None
208
+ self._start_time = None
137
209
  self._end_time = None
138
210
  self._patient_code = "patient_code"
139
211
  self._patient_name = "patient_name"
140
- self._device_type = None
212
+ self._device_type = "24130032"
213
+ self._device_no = "24130032"
141
214
  self._total_packets = 0
142
215
  self._lost_packets = 0
143
216
  self._storage_path = storage_path
144
217
  self._edf_writer_thread = None
218
+ self._file_prefix = None
145
219
 
146
220
  @property
147
221
  def file_name(self):
222
+ suffix = "bdf" if self.resolution == 24 else "edf"
223
+
224
+ # 文件名称
225
+ file_name = f"{self._file_prefix}_{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}" if self._file_prefix else f"{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}"
226
+
148
227
  if self._storage_path:
149
228
  try:
150
- os.makedirs(self._storage_path, exist_ok=True) # 自动创建目录,存在则忽略
151
- return f"{self._storage_path}/{self._device_type}_{self._first_timestamp}.edf"
229
+ # 自动创建目录,存在则忽略
230
+ os.makedirs(self._storage_path, exist_ok=True)
231
+
232
+ return f"{self._storage_path}/{file_name}"
152
233
  except Exception as e:
153
- logger.error(f"创建目录[{self._storage_path}]失败: {e}")
154
-
155
- return f"{self._device_type}_{self._first_timestamp}.edf"
156
-
157
- @property
158
- def file_type(self):
159
- return FILETYPE_BDFPLUS if self.resolution == 24 else FILETYPE_EDFPLUS
234
+ logger.error(f"创建目录[{self._storage_path}]失败: {e}")
235
+
236
+ return file_name
160
237
 
161
238
  def set_device_type(self, device_type):
162
- self._device_type = device_type
239
+ if device_type == 0x39:
240
+ self._device_type = "C64RS"
241
+ elif device_type == 0x40:
242
+ self._device_type = "LJ64S1"
243
+ else:
244
+ self._device_type = hex(device_type)
245
+
246
+ def set_device_no(self, device_no):
247
+ self._device_no = device_no
163
248
 
164
249
  def set_storage_path(self, storage_path):
165
250
  self._storage_path = storage_path
166
251
 
252
+ def set_file_prefix(self, file_prefix):
253
+ self._file_prefix = file_prefix
254
+
167
255
  def set_patient_code(self, patient_code):
168
256
  self._patient_code = patient_code
169
257
 
170
258
  def set_patient_name(self, patient_name):
171
259
  self._patient_name = patient_name
172
260
 
173
- def append(self, data, channels=None):
261
+ def write(self, packet: RscPacket):
262
+ logger.debug(f"packet: {packet}")
263
+ if packet is None:
264
+ self._edf_writer_thread.stop_recording()
265
+ return
266
+
267
+ if self.channels is None:
268
+ logger.info(f"开始记录数据到文件...")
269
+ self.channels = packet.channels
270
+ self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
271
+ self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
272
+ self._start_time = datetime.now()
273
+ logger.info(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
274
+
275
+ if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
276
+ self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
277
+ logger.warning(f"数据包丢失: {self._last_pkg_id} -> {packet.pkg_id}, 丢包数: {packet.pkg_id - self._last_pkg_id - 1}")
278
+
279
+ self._last_pkg_id = packet.pkg_id
280
+ self._total_packets += 1
281
+
174
282
  if self._edf_writer_thread is None:
175
- self._edf_writer_thread = EDFWriterThread(self.init_edf_writer())
283
+ self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
284
+ self._edf_writer_thread.set_start_time(self._start_time)
176
285
  self._edf_writer_thread.start()
177
- self._recording = True
178
- self._edf_writer_thread._recording = True
179
286
  logger.info(f"开始写入数据: {self.file_name}")
180
287
 
181
- if data:
182
- # # 通道数
183
- # if self._first_pkg_id is None:
184
- # self.eeg_channels = data.eeg_ch_count
185
- # self.acc_channels = data.acc_ch_count
186
- # self._first_pkg_id = data.pkg_id
187
- # self._first_timestamp = data.time_stamp
188
-
189
- # if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
190
- # self._lost_packets += data.pkg_id - self._last_pkg_id - 1
191
- # logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
192
-
193
- self._last_pkg_id = data.pkg_id
194
- self._total_packets += 1
288
+ self._edf_writer_thread.append(packet.eeg)
195
289
 
196
- # 数据
197
- self._cache.put(data)
198
- self._edf_writer_thread.append(data)
199
- # if not self._recording:
200
- # self.start()
201
290
 
202
- def trigger(self, data):
203
- pass
204
-
205
- def init_edf_writer(self):
206
- # 创建EDF+写入器
207
- edf_writer = EdfWriter(
208
- self.file_name,
209
- self.eeg_channels,
210
- file_type=self.file_type
211
- )
212
-
213
- # 设置头信息
214
- edf_writer.setPatientCode(self._patient_code)
215
- edf_writer.setPatientName(self._patient_name)
216
- edf_writer.setEquipment(self._device_type)
217
- edf_writer.setStartdatetime(datetime.now())
218
-
219
- # 配置通道参数
220
- signal_headers = []
221
- for ch in range(self.eeg_channels):
222
- signal_headers.append({
223
- "label": f'channels {ch + 1}',
224
- "dimension": 'uV',
225
- "sample_frequency": self.sample_frequency,
226
- "physical_min": self.physical_min,
227
- "physical_max": self.physical_max,
228
- "digital_min": self.digital_min,
229
- "digital_max": self.digital_max
230
- })
231
-
232
- edf_writer.setSignalHeaders(signal_headers)
233
-
234
- return edf_writer
235
-
291
+ # trigger标记
292
+ # desc: 标记内容
293
+ # cur_time: 设备时间时间戳,非设备发出的trigger不要设置
294
+ def trigger(self, desc, cur_time=None):
295
+ if cur_time is None:
296
+ onset = datetime.now().timestamp() - self._start_time.timestamp()
297
+ else:
298
+ onset = cur_time - self._first_timestamp
299
+ self._edf_writer_thread.trigger(onset, desc)
236
300
 
qlsdk/rsc/__init__.py CHANGED
@@ -1,7 +1,9 @@
1
- from .discover import *
1
+ from .network.discover import *
2
2
 
3
3
  from .entity import DeviceParser, QLDevice
4
4
  from .command import *
5
- from .device_manager import DeviceContainer
5
+ # from .device_manager import DeviceContainer
6
6
  from .proxy import DeviceProxy
7
7
  from .paradigm import *
8
+ from .manager import DeviceContainer
9
+ from .network import *