qlsdk2 0.4.0a3__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,5 +1,6 @@
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
@@ -7,116 +8,188 @@ import numpy as np
7
8
  import os
8
9
  from qlsdk.core import RscPacket
9
10
 
10
- class EDFWriterThread(Thread):
11
- def __init__(self, edf_writer : EdfWriter):
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):
12
18
  super().__init__()
13
- self._edf_writer : EdfWriter = edf_writer
19
+ self._writer : EdfWriter = None
14
20
  self.data_queue : Queue = Queue()
15
- self._stop_event : bool = False
16
21
  self._recording = False
17
- self._chunk = np.array([])
18
22
  self._points = 0
19
23
  self._duration = 0
20
- self._sample_frequency = 0
21
- self._total_packets = 0
22
- self._channels = []
23
- 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"
24
45
 
25
- def stop(self):
26
- self._stop_event = True
46
+ def set_channels(self, channels):
47
+ self._channels = channels
48
+ self._n_channels = len(channels)
49
+
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
27
58
 
28
59
  def append(self, data):
29
- # 数据
30
- 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
+
31
70
 
32
71
  def run(self):
33
- logger.debug(f"开始消费数据 _consumer: {self.data_queue.qsize()}")
72
+ logger.debug(f"启动bdf文件写入线程,写入数据到文件 {self.file_path}")
73
+ # 记录状态
74
+ self._recording = True
75
+
76
+ # 初始化
77
+ if self._writer is None:
78
+ self._init_writer()
79
+
34
80
  while True:
35
81
  if self._recording or (not self.data_queue.empty()):
36
82
  try:
37
- data = self.data_queue.get(timeout=10)
83
+ data = self.data_queue.get(timeout=30)
38
84
  if data is None:
85
+ logger.debug("收到结束信号,停止写入数据")
39
86
  break
40
87
  # 处理数据
41
- self._points += len(data)
88
+ self._points += len(data[1])
89
+ logger.trace(f"已处理数据点数:{self._points}")
42
90
  self._write_file(data)
43
91
  except Exception as e:
44
- logger.error("数据队列为空,超时(10s)结束")
92
+ logger.error(f"异常或超时(30s)结束: {str(e)}")
45
93
  break
46
94
  else:
95
+ logger.debug("数据记录完成")
47
96
  break
48
97
 
49
98
  self.close()
50
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
+
51
130
  def _write_file(self, eeg_data):
52
131
  try:
53
- if (self._chunk.size == 0):
54
- self._chunk = np.asarray(eeg_data)
132
+ if self._buffer is None or self._buffer.size == 0:
133
+ self._buffer = np.asarray(eeg_data)
55
134
  else:
56
- self._chunk = np.hstack((self._chunk, eeg_data))
57
-
58
- if self._chunk.size >= self._sample_rate * self._channels:
59
- self._write_chunk(self._chunk[:self._sample_rate])
60
- 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:]
61
141
 
62
142
  except Exception as e:
63
143
  logger.error(f"写入数据异常: {str(e)}")
64
144
 
65
145
  def close(self):
66
146
  self._recording = False
67
- if self._edf_writer:
68
- self._end_time = datetime.now().timestamp()
69
- self._edf_writer.writeAnnotation(0, 1, "start recording ")
70
- self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
71
- self._edf_writer.close()
72
-
73
- # logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
74
- # logger.info(f"文件: 完成记录, 总点数: {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()
75
151
 
152
+ logger.info(f"文件: {self.file_path}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒")
76
153
 
77
-
78
- def _write_chunk(self, chunk):
79
- logger.debug(f"写入数据: {chunk}")
80
- # 转换数据类型为float64(pyedflib要求)
81
- 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)
82
159
  # 写入时转置为(样本数, 通道数)格式
83
- self._edf_writer.writeSamples(data_float64)
160
+ self._writer.writeSamples(data_float64)
84
161
  self._duration += 1
85
162
 
163
+ # 用作数据结构一致化处理,通过调用公共类写入edf文件
164
+ # 入参包含写入edf的全部前置参数
165
+ # 实时数据包为个性化数据包,含有eeg数据部分
86
166
  class RscEDFHandler(object):
87
167
  '''
88
168
  Rsc EDF Handler
89
169
  处理EDF文件的读写
90
170
  RSC设备通道数根据选择变化,不同通道采样频率相同
91
- sample_frequency: 采样频率
92
- physical_max: 物理最大值
93
- physical_min: 物理最小值
94
- digital_max: 数字最大值
95
- digital_min: 数字最小值
171
+ eeg_sample_rate: 采样频率
172
+ physical_max: 物理最大值 (uV)
173
+ physical_min: 物理最小值 (uV)
96
174
  resolution: 分辨率
97
175
  storage_path: 存储路径
98
176
 
99
177
  @author: qlsdk
100
178
  @since: 0.4.0
101
179
  '''
102
- def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=24, storage_path = None):
180
+ def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
103
181
  # edf文件参数
104
182
  self.physical_max = physical_max
105
183
  self.physical_min = physical_min
106
- self.digital_max = digital_max
107
- 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"]
108
187
  # 点分辨率
109
188
  self.resolution = resolution
110
189
  # eeg通道数
111
- self.eeg_channels = None
190
+ self.channels = None
112
191
  # eeg采样率
113
- self.eeg_sample_rate = 500
114
- self.acc_channels = None
115
- self.acc_sample_rate = 50
116
- # 缓存
117
- self._cache = Queue()
118
- # 采样频率
119
- self.sample_frequency = sample_frequency
192
+ self.sample_rate = eeg_sample_rate
120
193
  # bytes per second
121
194
  self.bytes_per_second = 0
122
195
  self._edf_writer = None
@@ -132,110 +205,96 @@ class RscEDFHandler(object):
132
205
  self._first_pkg_id = None
133
206
  self._last_pkg_id = None
134
207
  self._first_timestamp = None
208
+ self._start_time = None
135
209
  self._end_time = None
136
210
  self._patient_code = "patient_code"
137
211
  self._patient_name = "patient_name"
138
212
  self._device_type = "24130032"
213
+ self._device_no = "24130032"
139
214
  self._total_packets = 0
140
215
  self._lost_packets = 0
141
216
  self._storage_path = storage_path
142
217
  self._edf_writer_thread = None
218
+ self._file_prefix = None
143
219
 
144
220
  @property
145
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
+
146
227
  if self._storage_path:
147
228
  try:
148
- os.makedirs(self._storage_path, exist_ok=True) # 自动创建目录,存在则忽略
149
- 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}"
150
233
  except Exception as e:
151
- logger.error(f"创建目录[{self._storage_path}]失败: {e}")
152
-
153
- return f"{self._device_type}_{self._first_timestamp}.edf"
154
-
155
- @property
156
- def file_type(self):
157
- return FILETYPE_BDFPLUS if self.resolution == 24 else FILETYPE_EDFPLUS
234
+ logger.error(f"创建目录[{self._storage_path}]失败: {e}")
235
+
236
+ return file_name
158
237
 
159
238
  def set_device_type(self, device_type):
160
- 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
161
248
 
162
249
  def set_storage_path(self, storage_path):
163
250
  self._storage_path = storage_path
164
251
 
252
+ def set_file_prefix(self, file_prefix):
253
+ self._file_prefix = file_prefix
254
+
165
255
  def set_patient_code(self, patient_code):
166
256
  self._patient_code = patient_code
167
257
 
168
258
  def set_patient_name(self, patient_name):
169
259
  self._patient_name = patient_name
170
260
 
171
- def append(self, data: RscPacket):
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}")
172
274
 
173
- if data:
174
- if self.eeg_channels is None:
175
- logger.info(f"开始记录数据到文件...")
176
- self.eeg_channels = data.channels
177
- self._first_pkg_id = data.pkg_id if self._first_pkg_id is None else self._first_pkg_id
178
- self._first_timestamp = data.time_stamp if self._first_timestamp is None else self._first_timestamp
179
-
180
- if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
181
- self._lost_packets += data.pkg_id - self._last_pkg_id - 1
182
- logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
183
-
184
- self._last_pkg_id = data.pkg_id
185
- self._total_packets += 1
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}")
186
278
 
279
+ self._last_pkg_id = packet.pkg_id
280
+ self._total_packets += 1
187
281
 
282
+ if self._edf_writer_thread is None:
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)
285
+ self._edf_writer_thread.start()
286
+ logger.info(f"开始写入数据: {self.file_name}")
188
287
 
189
- # 通道数变化、采样频率、信号放大幅度变化时应生成新的edf文件,handler内部不关注,外部调用方自行控制
190
- # elif len(self.eeg_channels) != len(data.channels):
191
-
192
- if self._edf_writer_thread is None:
193
- self._edf_writer_thread = EDFWriterThread(self.init_edf_writer())
194
- self._edf_writer_thread.start()
195
- self._recording = True
196
- self._edf_writer_thread._recording = True
197
- logger.info(f"开始写入数据: {self.file_name}")
198
-
199
- self._edf_writer_thread.append(data)
288
+ self._edf_writer_thread.append(packet.eeg)
200
289
 
201
- # 数据
202
- # self._cache.put(data)
203
- # self._edf_writer_thread.append(data)
204
- # if not self._recording:
205
- # self.start()
206
290
 
207
- def trigger(self, data):
208
- pass
209
-
210
- def init_edf_writer(self):
211
- # 创建EDF+写入器
212
- edf_writer = EdfWriter(
213
- self.file_name,
214
- len(self.eeg_channels),
215
- file_type=self.file_type
216
- )
217
-
218
- # 设置头信息
219
- edf_writer.setPatientCode(self._patient_code)
220
- edf_writer.setPatientName(self._patient_name)
221
- edf_writer.setEquipment(self._device_type)
222
- edf_writer.setStartdatetime(datetime.now())
223
-
224
- # 配置通道参数
225
- signal_headers = []
226
- for ch in range(len(self.eeg_channels)):
227
- signal_headers.append({
228
- "label": f'channels {ch + 1}',
229
- "dimension": 'uV',
230
- "sample_frequency": self.sample_frequency,
231
- "physical_min": self.physical_min,
232
- "physical_max": self.physical_max,
233
- "digital_min": self.digital_min,
234
- "digital_max": self.digital_max
235
- })
236
-
237
- edf_writer.setSignalHeaders(signal_headers)
238
-
239
- return edf_writer
240
-
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)
241
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 *