qlsdk2 0.5.1.1__py3-none-any.whl → 0.6.0__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/rsc/device/base.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  from multiprocessing import Queue
3
2
  from threading import Thread
4
3
  from time import sleep, time_ns
@@ -6,14 +5,30 @@ from typing import Any, Dict, Literal
6
5
 
7
6
  from loguru import logger
8
7
  import numpy as np
9
- from qlsdk.core.entity import RscPacket
8
+ from qlsdk.core.entity import RscPacket, ImpedancePacket
10
9
  from qlsdk.core.utils import to_bytes
11
- from qlsdk.persist import RscEDFHandler
12
10
  from qlsdk.rsc.interface import IDevice, IParser
13
- from qlsdk.rsc.parser import TcpMessageParser
14
- from qlsdk.rsc.command import StartImpedanceCommand, StopImpedanceCommand, StartStimulationCommand, StopStimulationCommand, SetAcquisitionParamCommand, StartAcquisitionCommand, StopAcquisitionCommand
11
+ from qlsdk.rsc.command import SetImpedanceParamCommand, StartImpedanceCommand, StopImpedanceCommand, StartStimulationCommand, StopStimulationCommand, SetAcquisitionParamCommand, StartAcquisitionCommand, StopAcquisitionCommand
12
+ from qlsdk.rsc.paradigm import StimulationParadigm
13
+ from qlsdk.rsc.parser.base import TcpMessageParser
15
14
 
15
+
16
+ def intersection_positions(A, B):
17
+ setB = set(B)
18
+ seen = set()
19
+ return [idx for idx, elem in enumerate(A)
20
+ if elem in setB and elem not in seen and not seen.add(elem)]
21
+
16
22
  class QLBaseDevice(IDevice):
23
+
24
+ __TRIGGER_MAPPING = {
25
+ 0x3E8: "Start of stimulation",
26
+ 0x3E9: "End of stimulation",
27
+ 0x3EA: "Ascending end of stimulation",
28
+ 0x3EB: "Descending start of stimulation ",
29
+ 0x3EC: "刺激参数有误",
30
+ 0x3ED: "End of stimulation (by force)",
31
+ }
17
32
  def __init__(self, socket):
18
33
  self.socket = socket
19
34
 
@@ -63,52 +78,11 @@ class QLBaseDevice(IDevice):
63
78
  "channels": [],
64
79
  }
65
80
 
66
- self._stim_param = {
67
- "stim_type": 0, # 刺激类型:0-所有通道参数相同, 1: 通道参数不同
68
- "channels": [],
69
- "param": [{
70
- "channel_id": 0, #通道号 从0开始 -- 必填
71
- "waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
72
- "current": 1, #电流强度(mA) -- 必填
73
- "duration": 30, #平稳阶段持续时间(s) -- 必填
74
- "ramp_up": 5, #上升时间(s) 默认0
75
- "ramp_down": 5, #下降时间(s) 默认0
76
- "frequency": 500, #频率(Hz) -- 非直流必填
77
- "phase_position": 0, #相位 -- 默认0
78
- "duration_delay": "0", #延迟启动时间(s) -- 默认0
79
- "pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
80
- },
81
- {
82
- "channel_id": 1, #通道号 从0开始 -- 必填
83
- "waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
84
- "current": 1, #电流强度(mA) -- 必填
85
- "duration": 30, #平稳阶段持续时间(s) -- 必填
86
- "ramp_up": 5, #上升时间(s) 默认0
87
- "ramp_down": 5, #下降时间(s) 默认0
88
- "frequency": 500, #频率(Hz) -- 非直流必填
89
- "phase_position": 0, #相位 -- 默认0
90
- "duration_delay": "0", #延迟启动时间(s) -- 默认0
91
- "pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
92
- }
93
- ]
94
- }
81
+ self._stim_param = None
95
82
 
96
- self.stim_paradigm = None
97
-
98
- signal_info = {
99
- "param" : None,
100
- "start_time" : None,
101
- "finished_time" : None,
102
- "packet_total" : None,
103
- "last_packet_time" : None,
104
- "state" : 0
105
- }
106
- stim_info = {
107
-
108
- }
109
- Impedance_info = {
110
-
111
- }
83
+ self._impedance_channels = []
84
+
85
+ self.stim_paradigm: StimulationParadigm = None
112
86
  # 信号采集状态
113
87
  # 信号数据包总数(一个信号采集周期内)
114
88
  # 信号采集参数
@@ -129,6 +103,9 @@ class QLBaseDevice(IDevice):
129
103
  self.storage_enable = True
130
104
  self._listening = False
131
105
  # self.ready()
106
+ self._signal_cache: Queue = None
107
+ self._recording = False
108
+
132
109
 
133
110
  def parser(self) -> IParser:
134
111
  return self._parser
@@ -137,19 +114,55 @@ class QLBaseDevice(IDevice):
137
114
  self._record_duration = record_duration
138
115
 
139
116
  # 数据包处理
140
- def produce(self, data: RscPacket, type:Literal['signal', 'impedance']="signal"):
141
- if data is None: return
117
+ def produce(self, body: bytes, type:Literal['signal', 'impedance']="signal"):
118
+ if body is None: return
142
119
 
143
- # 处理信号数据
144
- self._signal_wrapper(data)
120
+ if type == "signal":
121
+ self._produce_signal(body)
122
+ elif type == "impedance":
123
+ self._produce_impedance(body)
145
124
 
125
+ def _produce_impedance(self, body: bytes):
126
+ # 分发阻抗数据包给订阅者
127
+ if len(self._impedance_consumer) > 0:
128
+ packet = self._impedance_wrapper(body)
129
+ for topic, q in self._impedance_consumer.items():
130
+ try:
131
+ # 队列满了就丢弃最早的数据
132
+ if q.full():
133
+ q.get()
134
+ q.put(packet, timeout=1)
135
+ except Exception as e:
136
+ logger.error(f"impedance data put to queue exception: {str(e)}")
137
+
138
+ def _produce_signal(self, body: bytes):
139
+
140
+ # 处理信号数据
141
+ data = self._signal_wrapper(body)
142
+ # logger.debug("pkg_id: {}, eeg len: {}".format(data.pkg_id, len(data.eeg)))
143
+ #
144
+ trigger_positions = [index for index, value in enumerate(data.trigger) if value != 0]
145
+ if len(trigger_positions) > 0:
146
+ # logger.debug(f"Trigger触发点位置: {trigger_positions}, 触发点时间戳: {[data.time_stamp + int(pos * 1000 / data.sample_rate) for pos in trigger_positions]}")
147
+ for pos in trigger_positions:
148
+ self.trigger(self.trigger_info(data.trigger[pos]))
146
149
  # 存储
147
- if self.storage_enable:
148
- self._write_signal(data)
149
-
150
+ if self.storage_enable:
151
+ # 确保记录线程启动
152
+ if self._recording is False:
153
+ self._start_recording()
154
+
155
+ # 写入文件的缓存队列
156
+ if self._signal_cache is None:
157
+ self._signal_cache = Queue(256 * 1024 * 1024) # 256MB缓存
158
+ tmp = data.copy()
159
+ self._signal_cache.put(tmp)
160
+
150
161
  if len(self.signal_consumers) > 0 :
162
+ logger.trace(f"dg eeg: {data.eeg}")
151
163
  # 信号数字值转物理值
152
164
  data.eeg = self.eeg2phy(np.array(data.eeg))
165
+ logger.trace(f"ph eeg: {data.eeg}")
153
166
 
154
167
  # 发送数据包到订阅者
155
168
  for q in list(self.signal_consumers.values()):
@@ -157,26 +170,59 @@ class QLBaseDevice(IDevice):
157
170
  if q.full():
158
171
  q.get()
159
172
 
160
- q.put(data)
173
+ q.put(data)
161
174
 
162
- # 信号数据转换(默认不处理)
163
- def _signal_wrapper(self, data: RscPacket):
164
- pass
175
+ def _impedance_wrapper(self, body: bytes):
176
+ packet = ImpedancePacket().transfer(body)
177
+ if self._impedance_channels is not None and len(self._impedance_channels) > 0:
178
+ # 只保留设置的阻抗通道
179
+ channel_pos = intersection_positions(packet.channels, self._impedance_channels)
180
+ packet.impedance = [packet.impedance[i] for i in channel_pos]
181
+ packet.channels = [packet.channels[i] for i in channel_pos]
182
+
183
+ return packet
184
+
185
+ # 信号数据转换
186
+ def _signal_wrapper(self, body: bytes):
187
+ return RscPacket().transfer(body)
165
188
 
166
- def _write_signal(self, data: RscPacket):
189
+ def _write_signal(self):
167
190
  # 文件写入到edf
168
191
  if self._edf_handler is None:
169
192
  logger.debug("Initializing EDF handler for data storage")
170
193
  self.init_edf_handler()
171
194
 
172
- if self._edf_handler:
195
+ while self._recording:
196
+ data = self._signal_cache.get()
173
197
  self._edf_handler.write(data)
198
+ if data is None:
199
+ break
200
+
201
+ self._recording = False
202
+ def _start_recording(self):
203
+ if self.storage_enable is False:
204
+ logger.trace("Storage is disabled, will not start recording")
205
+ return
206
+
207
+ if self._signal_cache is None:
208
+ self._signal_cache = Queue(256 * 1024 * 1024) # 256MB缓存
209
+
210
+ try:
211
+ self._recording = True
212
+ t = Thread(target=self._write_signal, daemon=True)
213
+ t.start()
214
+ logger.info(f"开启记录")
215
+ except Exception as e:
216
+ logger.error(f"开启记录失败: {str(e)}")
217
+ return
174
218
 
175
219
  def start_listening(self):
176
220
 
177
221
  try:
222
+ # 启动消息解析器
178
223
  self.start_message_parser()
179
224
 
225
+ # 启动消息监听器
180
226
  self.start_message_listening()
181
227
  except Exception as e:
182
228
  logger.error(f"设备{self.device_no}准备失败: {str(e)}")
@@ -188,26 +234,50 @@ class QLBaseDevice(IDevice):
188
234
  logger.trace(f"设备{self.device_no}停止socket监听")
189
235
  self._listening = False
190
236
  self._parser.stop()
191
-
237
+
238
+ def read_msg(self, size: int) -> bytes:
239
+ try:
240
+ self.socket.settimeout(2.0)
241
+ return self.socket.recv(size)
242
+ except Exception as e:
243
+ logger.error(f"read_msg exception: {str(e)}")
244
+ raise ValueError("read_msg exception") from e
245
+
246
+ @classmethod
247
+ def from_parent(cls, parent:IDevice) -> IDevice:
248
+ rlt = cls(parent.socket)
249
+ rlt.device_id = parent.device_id
250
+ rlt._device_no = parent.device_no
251
+ return rlt
252
+
192
253
  @property
193
254
  def device_type(self) -> int:
194
255
  return self._device_type
195
256
 
257
+ def set_impedance_channels(self, channels):
258
+ self._impedance_channels = channels
259
+
260
+ def get_impedance_channels(self):
261
+ return self._impedance_channels
262
+
196
263
  def start_message_parser(self) -> None:
197
264
  self._parser = TcpMessageParser(self)
198
265
  self._parser.start()
199
- logger.debug("TCP消息解析器已启动")
266
+ logger.debug("RSC消息解析器已启动")
200
267
 
201
268
  def start_message_listening(self) -> None:
202
269
  def _accept():
203
270
  while self._listening:
204
- # 缓冲区4M
205
- data = self.socket.recv(4096*1024)
206
- if not data:
207
- logger.warning(f"设备[{self.device_name}]连接结束")
271
+ try:
272
+ # 缓冲区4M
273
+ data = self.socket.recv(4096*1024)
274
+ if not data:
275
+ logger.warning(f"设备[{self.device_name}]连接结束")
276
+ break
277
+ self._parser.append(data)
278
+ except Exception as e:
279
+ logger.debug(f"设备[{self.device_name}]接收数据异常: {str(e)}")
208
280
  break
209
-
210
- self._parser.append(data)
211
281
 
212
282
  # 启动数据接收线程
213
283
  self._listening = True
@@ -252,11 +322,6 @@ class QLBaseDevice(IDevice):
252
322
  def init_edf_handler(self):
253
323
  logger.warning("init_edf_handler not implemented in base class, should be overridden in subclass")
254
324
  pass
255
- # self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
256
- # self._edf_handler.set_device_type(self.device_type)
257
- # self._edf_handler.set_device_no(self.device_no)
258
- # self._edf_handler.set_storage_path(self._storage_path)
259
- # self._edf_handler.set_file_prefix(self._file_prefix)
260
325
 
261
326
  # eeg数字值转物理值
262
327
  def eeg2phy(self, digital:int):
@@ -266,7 +331,7 @@ class QLBaseDevice(IDevice):
266
331
  @property
267
332
  def edf_handler(self):
268
333
  if not self.storage_enable:
269
- logger.warning("EDF storage is disabled, no edf handler available")
334
+ logger.info("已关闭文件记录,不会生成edf/bdf文件")
270
335
  return None
271
336
 
272
337
  if self._edf_handler is None:
@@ -300,8 +365,12 @@ class QLBaseDevice(IDevice):
300
365
  return self._impedance_consumer
301
366
 
302
367
  # 设置记录文件路径
303
- def set_storage_path(self, path):
304
- self._storage_path = path
368
+ def set_storage_path(self, dir: str):
369
+ import os
370
+
371
+ abs_path = os.path.abspath(dir)
372
+ os.makedirs(abs_path, exist_ok=True)
373
+ self._storage_path = abs_path
305
374
 
306
375
  # 设置记录文件名称前缀
307
376
  def set_file_prefix(self, prefix):
@@ -316,23 +385,27 @@ class QLBaseDevice(IDevice):
316
385
  self.stim_paradigm = param
317
386
 
318
387
  # 设置采集参数
319
- def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
388
+ def set_acq_param(self, channels, sample_rate:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 500, sample_range:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 188):
320
389
  self._acq_param["channels"] = channels
321
390
  self._acq_param["sample_rate"] = sample_rate
322
391
  self._acq_param["sample_range"] = sample_range
323
392
  self._acq_channels = channels
324
393
  self._sample_rate = sample_rate
325
394
  self._sample_range = sample_range
326
-
327
- # 通用配置-TODO
328
- def set_config(self, key:str, val: str):
329
- pass
330
395
 
331
396
  def start_impedance(self):
332
397
  logger.info(f"[设备-{self.device_no}]启动阻抗测量")
333
- msg = StartImpedanceCommand.build(self).pack()
334
- logger.trace(f"start_impedance message is {msg.hex()}")
335
- self.socket.sendall(msg)
398
+ # 设置数据采集参数
399
+ # set_param_msg = SetImpedanceParamCommand.build(self).pack()
400
+ device_id = bytes.fromhex(self.device_id)[::-1].hex() if self.device_id else '00000000'
401
+ set_param_msg = bytes.fromhex(f'5aa50239{device_id}3f0000001104ffffffffffffffff000000000000000000000000000000000000000000000000e8030000fa00000010000164000000745c5aa50239390045243a00000012040000000000000000000000000000000000000000000000000000000000000000000001000000000000004c2a')
402
+ logger.debug(f"set_param_msg message is {set_param_msg.hex()}")
403
+ self.socket.sendall(set_param_msg)
404
+ sleep(0.5)
405
+
406
+ impedance_start_msg = StartImpedanceCommand.build(self).pack()
407
+ logger.debug(f"start_impedance message is {impedance_start_msg.hex()}")
408
+ self.socket.sendall(impedance_start_msg)
336
409
 
337
410
  def stop_impedance(self):
338
411
  logger.info(f"[设备{self.device_no}]停止阻抗测量")
@@ -348,8 +421,11 @@ class QLBaseDevice(IDevice):
348
421
  msg = StartStimulationCommand.build(self).pack()
349
422
  logger.trace(f"start_stimulation message is {msg.hex()}")
350
423
  self.socket.sendall(msg)
351
- t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,), daemon=True)
352
- t.start()
424
+ # t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,), daemon=True)
425
+ # t.start()
426
+
427
+ def get_stim_param(self) -> bytes:
428
+ return self.stim_paradigm.to_bytes()
353
429
 
354
430
  def _stop_stimulation_trigger(self, duration):
355
431
  delay = duration
@@ -357,23 +433,22 @@ class QLBaseDevice(IDevice):
357
433
  sleep(1)
358
434
  delay -= 1
359
435
  logger.debug(f"_stop_stimulation_trigger duration: {duration}")
360
- if self._edf_handler:
361
- self._edf_handler.trigger("stimulation should be stopped")
436
+ if self.edf_handler:
437
+ self.edf_handler.trigger("stimulation should be stopped")
362
438
  else:
363
439
  logger.warning("stop stim trigger fail. no edf writer alive")
364
440
 
365
441
  def stop_stimulation(self):
366
442
  logger.info(f"[设备-{self.device_no}]停止电刺激")
367
- msg = StopStimulationCommand.pack()
443
+ msg = StopStimulationCommand.build(self).pack()
368
444
  logger.trace(f"stop_stimulation message is {msg.hex()}")
369
445
  self.socket.sendall(msg)
370
446
 
371
447
  # 启动采集
372
- def start_acquisition(self, recording = True):
448
+ def start_acquisition(self):
373
449
  logger.info(f"[设备-{self.device_no}]启动信号采集")
374
- self._recording = recording
375
- # 初始化EDF处理器
376
- self.init_edf_handler()
450
+ # 记录准备
451
+ self._start_recording()
377
452
  # 设置数据采集参数
378
453
  param_bytes = SetAcquisitionParamCommand.build(self).pack()
379
454
  # 启动数据采集
@@ -388,9 +463,10 @@ class QLBaseDevice(IDevice):
388
463
  msg = StopAcquisitionCommand.build(self).pack()
389
464
  logger.trace(f"stop_acquisition message is {msg.hex()}")
390
465
  self.socket.sendall(msg)
391
- if self._edf_handler:
466
+ # 结束标识
467
+ if self._signal_cache:
392
468
  # 发送结束标识
393
- self.edf_handler.write(None)
469
+ self._signal_cache.put(None)
394
470
 
395
471
  '''
396
472
  订阅数据
@@ -408,7 +484,7 @@ class QLBaseDevice(IDevice):
408
484
 
409
485
  # 数据队列
410
486
  if q is None:
411
- q = Queue(maxsize=1000)
487
+ q = Queue(maxsize=100 * 1024 * 1024)
412
488
 
413
489
  # 订阅生理电信号数据
414
490
  if type == "signal":
@@ -428,11 +504,12 @@ class QLBaseDevice(IDevice):
428
504
  return topic, q
429
505
 
430
506
  def trigger(self, desc):
431
- if self._edf_handler:
507
+ if self.edf_handler:
432
508
  self.edf_handler.trigger(desc)
433
509
  else:
434
- logger.warning("没有开启文件记录时,无法记录trigger信息")
510
+ logger.info("已关闭文件记录,不会记录trigger信息")
435
511
 
512
+ # 设置信号采集参数
436
513
  def gen_set_acquirement_param(self) -> bytes:
437
514
 
438
515
  body = to_bytes(self.acq_channels)
@@ -443,13 +520,45 @@ class QLBaseDevice(IDevice):
443
520
  body += bytes.fromhex('00')
444
521
 
445
522
  return body
523
+ # 设置阻抗测量参数
524
+ def gen_set_impedance_param(self) -> bytes:
525
+
526
+ # 仅通道生效 32字节,其他不生效-272字节,实际73字节
527
+ body = to_bytes(self._impedance_channels)
528
+ # 100 bytes
529
+ # body += bytes.fromhex('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
530
+ # # 100 bytes
531
+ # body += bytes.fromhex('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
532
+ # 73 bytes
533
+ body += bytes.fromhex('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
534
+
535
+ return bytes.fromhex('ffffffffffffffff000000000000000000000000000000000000000000000000e8030000fa00000010000164000000745c5aa50239390045243a0000001204000000000000000000000000000000000000000000000000000000000000000000000100000000000000')
536
+ def disconnect(self):
537
+ logger.info(f"[断开设备-{self.device_no}]的连接...")
538
+ self._listening = False
539
+ try:
540
+ sleep(0.1)
541
+ self.socket.shutdown(2)
542
+ self.socket.close()
543
+ logger.info(f"[设备-{self.device_no}]设备连接已断开")
544
+ except Exception as e:
545
+ logger.error(f"断开设备连接异常: {str(e)}")
546
+
547
+ # 关闭解析器
548
+ self._parser.stop()
549
+
550
+ def enable_storage(self, enable: bool = True):
551
+ self.storage_enable = enable
552
+
553
+ def trigger_info(self, code: int) -> str:
554
+ return QLBaseDevice.__TRIGGER_MAPPING.get(code, hex(code))
446
555
 
447
556
  def __str__(self):
448
557
  return f'''
449
558
  Device:
450
559
  Name: {self.device_name},
451
560
  Type: {hex(self.device_type) if self.device_type else None},
452
- ID: {self.device_id if self.device_id else None},
561
+ ID: {self.device_no if self.device_no else None},
453
562
  Software: {self.software_version},
454
563
  Hardware: {self.hardware_version},
455
564
  Connect Time: {self.connect_time},
@@ -459,8 +568,8 @@ class QLBaseDevice(IDevice):
459
568
  Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
460
569
  '''
461
570
 
462
- def __eq__(self, other):
463
- return self.device_name == other.device_name and self.device_type == other.device_type and self.device_id == other.device_id
571
+ def __eq__(self, other:IDevice):
572
+ return self.device_type == other.device_type and self.device_no == other.device_no
464
573
 
465
574
  def __hash__(self):
466
- return hash((self.device_name, self.device_type, self.device_id))
575
+ return hash((self.device_type, self.device_no))
@@ -115,11 +115,7 @@ class C16RS(QLBaseDevice):
115
115
  self._edf_handler.set_device_no(self.device_no)
116
116
  self._edf_handler.set_storage_path(self._storage_path)
117
117
  self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C16R')
118
-
119
- # 设置刺激参数
120
- def set_stim_param(self, param):
121
- self.stim_paradigm = param
122
-
118
+
123
119
  # 设置采集参数
124
120
  def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
125
121
  # 保存原始通道参数