qlsdk2 0.4.1__py3-none-any.whl → 0.5.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
@@ -5,16 +5,21 @@ from time import sleep, time_ns
5
5
  from typing import Any, Dict, Literal
6
6
 
7
7
  from loguru import logger
8
+ import numpy as np
9
+ from qlsdk.core.entity import RscPacket
10
+ from qlsdk.core.utils import to_bytes
8
11
  from qlsdk.persist import RscEDFHandler
9
12
  from qlsdk.rsc.interface import IDevice, IParser
10
13
  from qlsdk.rsc.parser import TcpMessageParser
11
14
  from qlsdk.rsc.command import StartImpedanceCommand, StopImpedanceCommand, StartStimulationCommand, StopStimulationCommand, SetAcquisitionParamCommand, StartAcquisitionCommand, StopAcquisitionCommand
12
- # from qlsdk.rsc.command import *
13
15
 
14
16
  class QLBaseDevice(IDevice):
15
17
  def __init__(self, socket):
16
18
  self.socket = socket
17
19
 
20
+ # 启动数据解析线程
21
+ self._parser: IParser = None
22
+
18
23
  # 设备信息
19
24
  self.device_id = None
20
25
  self.device_name = None
@@ -113,26 +118,92 @@ class QLBaseDevice(IDevice):
113
118
  # 存储目录
114
119
 
115
120
  #
116
- self.__signal_consumer: Dict[str, Queue[Any]]={}
117
- self.__impedance_consumer: Dict[str, Queue[Any]]={}
121
+ self._signal_consumer: Dict[str, Queue[Any]]={}
122
+ self._impedance_consumer: Dict[str, Queue[Any]]={}
118
123
 
119
124
  # EDF文件处理器
120
125
  self._edf_handler = None
121
126
  self.storage_enable = True
127
+ self._listening = False
128
+ # self.ready()
122
129
 
123
- # 启动数据解析线程
124
- self._parser: IParser = TcpMessageParser(self)
125
- self._parser.start()
130
+ def parser(self) -> IParser:
131
+ return self._parser
132
+
133
+ # 数据包处理
134
+ def produce(self, data: RscPacket):
135
+ if data is None: return
126
136
 
127
- # 启动数据接收线程
128
- self._accept = Thread(target=self.accept)
129
- self._accept.daemon = True
130
- self._accept.start()
137
+ # 处理信号数据
138
+ self._signal_wrapper(data)
139
+
140
+ # 存储
141
+ if self.storage_enable:
142
+ self._write_signal(data)
143
+
144
+ if len(self.signal_consumers) > 0 :
145
+ # 信号数字值转物理值
146
+ data.eeg = self.eeg2phy(np.array(data.eeg))
147
+
148
+ # 发送数据包到订阅者
149
+ for q in list(self.signal_consumers.values()):
150
+ q.put(data)
151
+
152
+ # 信号数据转换(默认不处理)
153
+ def _signal_wrapper(self, data: RscPacket):
154
+ pass
155
+
156
+ def _write_signal(self, data: RscPacket):
157
+ # 文件写入到edf
158
+ if self._edf_handler is None:
159
+ logger.debug("Initializing EDF handler for data storage")
160
+ self.init_edf_handler()
161
+
162
+ if self._edf_handler:
163
+ self._edf_handler.write(data)
164
+
165
+ def start_listening(self):
166
+
167
+ try:
168
+ self.start_message_parser()
169
+
170
+ self.start_message_listening()
171
+ except Exception as e:
172
+ logger.error(f"设备{self.device_no}准备失败: {str(e)}")
173
+ return False
174
+
175
+ return True
176
+
177
+ def stop_listening(self):
178
+ logger.trace(f"设备{self.device_no}停止socket监听")
179
+ self._listening = False
131
180
 
132
181
  @property
133
182
  def device_type(self) -> int:
134
183
  return self._device_type
135
184
 
185
+ def start_message_parser(self) -> None:
186
+ self._parser = TcpMessageParser(self)
187
+ self._parser.start()
188
+ logger.debug("TCP消息解析器已启动")
189
+
190
+ def start_message_listening(self) -> None:
191
+ def _accept():
192
+ while self._listening:
193
+ # 缓冲区4M
194
+ data = self.socket.recv(4096*1024)
195
+ if not data:
196
+ logger.warning(f"设备[{self.device_name}]连接结束")
197
+ break
198
+
199
+ self._parser.append(data)
200
+
201
+ # 启动数据接收线程
202
+ self._listening = True
203
+ message_accept = Thread(target=_accept, daemon=True)
204
+ message_accept.start()
205
+ logger.debug(f"socket消息监听已启动")
206
+
136
207
  def set_device_type(self, type: int):
137
208
  self._device_type = type
138
209
 
@@ -168,11 +239,13 @@ class QLBaseDevice(IDevice):
168
239
  return self._digital_range
169
240
 
170
241
  def init_edf_handler(self):
171
- self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
172
- self._edf_handler.set_device_type(self.device_type)
173
- self._edf_handler.set_device_no(self.device_name)
174
- self._edf_handler.set_storage_path(self._storage_path)
175
- self._edf_handler.set_file_prefix(self._file_prefix)
242
+ logger.warning("init_edf_handler not implemented in base class, should be overridden in subclass")
243
+ pass
244
+ # self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
245
+ # self._edf_handler.set_device_type(self.device_type)
246
+ # self._edf_handler.set_device_no(self.device_no)
247
+ # self._edf_handler.set_storage_path(self._storage_path)
248
+ # self._edf_handler.set_file_prefix(self._file_prefix)
176
249
 
177
250
  # eeg数字值转物理值
178
251
  def eeg2phy(self, digital:int):
@@ -182,6 +255,7 @@ class QLBaseDevice(IDevice):
182
255
  @property
183
256
  def edf_handler(self):
184
257
  if not self.storage_enable:
258
+ logger.warning("EDF storage is disabled, no edf handler available")
185
259
  return None
186
260
 
187
261
  if self._edf_handler is None:
@@ -208,11 +282,11 @@ class QLBaseDevice(IDevice):
208
282
  return 10
209
283
  @property
210
284
  def signal_consumers(self):
211
- return self.__signal_consumer
285
+ return self._signal_consumer
212
286
 
213
287
  @property
214
288
  def impedance_consumers(self):
215
- return self.__impedance_consumer
289
+ return self._impedance_consumer
216
290
 
217
291
  # 设置记录文件路径
218
292
  def set_storage_path(self, path):
@@ -221,18 +295,6 @@ class QLBaseDevice(IDevice):
221
295
  # 设置记录文件名称前缀
222
296
  def set_file_prefix(self, prefix):
223
297
  self._file_prefix = prefix
224
-
225
- # 接收socket消息
226
- def accept(self):
227
- while True:
228
- # 缓冲去4M
229
- data = self.socket.recv(4096*1024)
230
- if not data:
231
- logger.warning(f"设备{self.device_name}连接结束")
232
- break
233
-
234
- self._parser.append(data)
235
-
236
298
 
237
299
  # socket发送数据
238
300
  def send(self, data):
@@ -256,26 +318,26 @@ class QLBaseDevice(IDevice):
256
318
  pass
257
319
 
258
320
  def start_impedance(self):
259
- logger.info("启动阻抗测量")
321
+ logger.info(f"[设备-{self.device_no}]启动阻抗测量")
260
322
  msg = StartImpedanceCommand.build(self).pack()
261
- logger.debug(f"start_impedance message is {msg.hex()}")
323
+ logger.trace(f"start_impedance message is {msg.hex()}")
262
324
  self.socket.sendall(msg)
263
325
 
264
326
  def stop_impedance(self):
265
- logger.info("停止阻抗测量")
327
+ logger.info(f"[设备{self.device_no}]停止阻抗测量")
266
328
  msg = StopImpedanceCommand.build(self).pack()
267
- logger.debug(f"stop_impedance message is {msg.hex()}")
329
+ logger.trace(f"stop_impedance message is {msg.hex()}")
268
330
  self.socket.sendall(msg)
269
331
 
270
332
  def start_stimulation(self):
271
333
  if self.stim_paradigm is None:
272
334
  logger.warning("刺激参数未设置,请先设置刺激参数")
273
335
  return
274
- logger.info("启动电刺激")
336
+ logger.info(f"[设备-{self.device_no}]启动电刺激")
275
337
  msg = StartStimulationCommand.build(self).pack()
276
- logger.debug(f"start_stimulation message is {msg.hex()}")
338
+ logger.trace(f"start_stimulation message is {msg.hex()}")
277
339
  self.socket.sendall(msg)
278
- t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
340
+ t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,), daemon=True)
279
341
  t.start()
280
342
 
281
343
  def _stop_stimulation_trigger(self, duration):
@@ -283,65 +345,74 @@ class QLBaseDevice(IDevice):
283
345
  while delay > 0:
284
346
  sleep(1)
285
347
  delay -= 1
286
- logger.info(f"_stop_stimulation_trigger duration: {duration}")
348
+ logger.debug(f"_stop_stimulation_trigger duration: {duration}")
287
349
  if self._edf_handler:
288
350
  self._edf_handler.trigger("stimulation should be stopped")
289
351
  else:
290
352
  logger.warning("stop stim trigger fail. no edf writer alive")
291
353
 
292
354
  def stop_stimulation(self):
293
- logger.info("停止电刺激")
355
+ logger.info(f"[设备-{self.device_no}]停止电刺激")
294
356
  msg = StopStimulationCommand.pack()
295
- logger.debug(f"stop_stimulation message is {msg.hex()}")
357
+ logger.trace(f"stop_stimulation message is {msg.hex()}")
296
358
  self.socket.sendall(msg)
297
359
 
298
360
  # 启动采集
299
361
  def start_acquisition(self, recording = True):
300
- logger.info("启动信号采集")
362
+ logger.info(f"[设备-{self.device_no}]启动信号采集")
301
363
  self._recording = recording
364
+ # 初始化EDF处理器
365
+ self.init_edf_handler()
302
366
  # 设置数据采集参数
303
367
  param_bytes = SetAcquisitionParamCommand.build(self).pack()
304
368
  # 启动数据采集
305
369
  start_bytes = StartAcquisitionCommand.build(self).pack()
306
370
  msg = param_bytes + start_bytes
307
- logger.debug(f"start_acquisition message is {msg.hex()}")
371
+ logger.trace(f"start_acquisition message is {msg.hex()}")
308
372
  self.socket.sendall(msg)
309
373
 
310
374
  # 停止采集
311
375
  def stop_acquisition(self):
312
- logger.info("停止信号采集")
376
+ logger.info(f"[设备-{self.device_no}]停止信号采集")
313
377
  msg = StopAcquisitionCommand.build(self).pack()
314
- logger.debug(f"stop_acquisition message is {msg.hex()}")
378
+ logger.trace(f"stop_acquisition message is {msg.hex()}")
315
379
  self.socket.sendall(msg)
316
380
  if self._edf_handler:
317
381
  # 发送结束标识
318
382
  self.edf_handler.write(None)
319
383
 
320
- # 订阅实时数据
384
+ '''
385
+ 订阅数据
386
+ topic: 订阅主题
387
+ q: 数据队列
388
+ type: 数据类型,signal-信号数据,impedance-阻抗数据
389
+ '''
321
390
  def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
322
391
 
392
+ # 队列名称
393
+ if topic is None:
394
+ topic = f"{self.device_no}_{type}_{time_ns()}"
395
+
396
+ logger.debug(f"[设备-{self.device_no}]订阅数据流: {topic}, type: {type}")
397
+
323
398
  # 数据队列
324
399
  if q is None:
325
400
  q = Queue(maxsize=1000)
326
-
327
- # 队列名称
328
- if topic is None:
329
- topic = f"{type}_{time_ns()}"
330
401
 
331
402
  # 订阅生理电信号数据
332
403
  if type == "signal":
333
404
  # topic唯一,用来区分不同的订阅队列(下同)
334
- if topic in list(self.__signal_consumer.keys()):
335
- logger.warning(f"exists {type} subscribe of {topic}")
405
+ if topic in list(self._signal_consumer.keys()):
406
+ logger.warning(f"已存在主题[{topic}]的信号数据订阅!")
336
407
  else:
337
- self.__signal_consumer[topic] = q
408
+ self._signal_consumer[topic] = q
338
409
 
339
410
  # 订阅阻抗数据
340
411
  if type == "impedance":
341
- if topic in list(self.__signal_consumer.keys()):
342
- logger.warning(f"exists {type} subscribe of {topic}")
412
+ if topic in list(self._impedance_consumer.keys()):
413
+ logger.warning(f"已存在主题[{topic}]的阻抗数据订阅!")
343
414
  else:
344
- self.__impedance_consumer[topic] = q
415
+ self._impedance_consumer[topic] = q
345
416
 
346
417
  return topic, q
347
418
 
@@ -349,8 +420,19 @@ class QLBaseDevice(IDevice):
349
420
  if self._edf_handler:
350
421
  self.edf_handler.trigger(desc)
351
422
  else:
352
- logger.warning("no edf handler, no place to recording trigger")
423
+ logger.warning("没有开启文件记录时,无法记录trigger信息")
424
+
425
+ def gen_set_acquirement_param(self) -> bytes:
426
+
427
+ body = to_bytes(self.acq_channels)
428
+ body += self.sample_range.to_bytes(4, byteorder='little')
429
+ body += self.sample_rate.to_bytes(4, byteorder='little')
430
+ body += self.sample_num.to_bytes(4, byteorder='little')
431
+ body += self.resolution.to_bytes(1, byteorder='little')
432
+ body += bytes.fromhex('00')
353
433
 
434
+ return body
435
+
354
436
  def __str__(self):
355
437
  return f'''
356
438
  Device:
@@ -366,21 +448,6 @@ class QLBaseDevice(IDevice):
366
448
  Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
367
449
  '''
368
450
 
369
- def __repr__(self):
370
- return f'''
371
- Device:
372
- Name: {self.device_name},
373
- Type: {hex(self.device_type)},
374
- ID: {self.device_id},
375
- Software: {self.software_version},
376
- Hardware: {self.hardware_version},
377
- Connect Time: {self.connect_time},
378
- Current Time: {self.current_time},
379
- Voltage: {self.voltage}mV,
380
- Battery Remain: {self.battery_remain}%,
381
- Battery Total: {self.battery_total}%
382
- '''
383
-
384
451
  def __eq__(self, other):
385
452
  return self.device_name == other.device_name and self.device_type == other.device_type and self.device_id == other.device_id
386
453
 
@@ -0,0 +1,205 @@
1
+
2
+ from multiprocessing import Queue
3
+ from threading import Thread
4
+ from time import sleep, time_ns
5
+ from typing import Any, Dict, Literal
6
+
7
+ from loguru import logger
8
+ from qlsdk.persist import RscEDFHandler
9
+ from qlsdk.rsc.device.device_factory import DeviceFactory
10
+ from qlsdk.rsc.interface import IDevice
11
+ from qlsdk.rsc.command import *
12
+ from qlsdk.rsc.device.base import QLBaseDevice
13
+
14
+ '''
15
+ C16RS设备类,继承自QLBaseDevice
16
+ 提供设备特定的属性和方法,包括设备类型、采集参数设置、电刺激参
17
+ '''
18
+ class C16RS(QLBaseDevice):
19
+
20
+ device_type = 0x339 # C16RS设备类型标识符
21
+
22
+ def __init__(self, socket):
23
+
24
+ super().__init__(socket)
25
+ # 存储通道反向映射位置值
26
+ self._reverse_ch_pos = None
27
+ self.channel_name_mapping = {
28
+ "FP1": 1,
29
+ "FP2": 2,
30
+ "C3": 3,
31
+ "C4": 4,
32
+ "O1": 5,
33
+ "O2": 6,
34
+ "CZ": 7,
35
+ "T7": 8,
36
+ "T8": 9,
37
+ "M1": 10,
38
+ "M2": 11,
39
+ "F3": 12,
40
+ "F4": 13,
41
+ "FZ": 14,
42
+ "F7": 15,
43
+ "F8": 16,
44
+ "PZ": 17,
45
+ "P3": 18,
46
+ "P7": 19,
47
+ "P4": 20,
48
+ "P8": 21
49
+ }
50
+
51
+ # self.channel_mapping = {
52
+ # "1": 61,
53
+ # "2": 62,
54
+ # "3": 63,
55
+ # "4": 64,
56
+ # "5": 65,
57
+ # "6": 66,
58
+ # "7": 68,
59
+ # "8": 67,
60
+ # "9": 53,
61
+ # "10": 54,
62
+ # "11": 55,
63
+ # "12": 57,
64
+ # "13": 59,
65
+ # "14": 58,
66
+ # "15": 60,
67
+ # "16": 56,
68
+ # "17": 26,
69
+ # "18": 25,
70
+ # "19": 24,
71
+ # "20": 23,
72
+ # "21": 22
73
+ # }
74
+
75
+ self.channel_mapping = {
76
+ "1": 2,
77
+ "2": 3,
78
+ "3": 27,
79
+ "4": 31,
80
+ "5": 60,
81
+ "6": 61,
82
+ "7": 25,
83
+ "8": 29,
84
+ "9": 33,
85
+ "10": 62,
86
+ "11": 63,
87
+ "12": 10,
88
+ "13": 14,
89
+ "14": 8,
90
+ "15": 12,
91
+ "16": 16,
92
+ "17": 43,
93
+ "18": 45,
94
+ "19": 47,
95
+ "20": 49,
96
+ "21": 51
97
+ }
98
+
99
+ self.channel_display_mapping = {
100
+ 2: 1,
101
+ 3: 2,
102
+ 27: 3,
103
+ 31: 4,
104
+ 60: 5,
105
+ 61: 6,
106
+ 25: 7,
107
+ 29: 8,
108
+ 33: 9,
109
+ 62: 10,
110
+ 63: 11,
111
+ 10: 12,
112
+ 14: 13,
113
+ 8: 14,
114
+ 12: 15,
115
+ 16: 16,
116
+ 43: 17,
117
+ 45: 18,
118
+ 47: 19,
119
+ 49: 20,
120
+ 51: 21
121
+ }
122
+
123
+
124
+ @classmethod
125
+ def from_parent(cls, parent:IDevice) -> IDevice:
126
+ rlt = cls(parent.socket)
127
+ rlt.device_id = parent.device_id
128
+ rlt._device_no = parent.device_no
129
+ return rlt
130
+
131
+
132
+ def init_edf_handler(self):
133
+ self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
134
+ self._edf_handler.set_device_type(self.device_type)
135
+ self._edf_handler.set_device_no(self.device_no)
136
+ self._edf_handler.set_storage_path(self._storage_path)
137
+ self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C16R')
138
+
139
+ # 设置刺激参数
140
+ def set_stim_param(self, param):
141
+ self.stim_paradigm = param
142
+
143
+ # 设置采集参数
144
+ def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
145
+ # 保存原始通道参数
146
+ self._acq_param["original_channels"] = channels
147
+
148
+ # 名称转换为数字通道
149
+ channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
150
+
151
+ # 根据映射关系做通道转换-没有映射的默认到第一个通道
152
+ # 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
153
+ channels = [self.channel_mapping.get(str(i), -1) for i in channels]
154
+ channels = [i if i != -1 else channels[0] for i in channels]
155
+
156
+ # 更新采集参数
157
+ self._acq_param["channels"] = channels
158
+ self._acq_param["sample_rate"] = sample_rate
159
+ self._acq_param["sample_range"] = sample_range
160
+ self._acq_channels = channels
161
+ self._sample_rate = sample_rate
162
+ self._sample_range = sample_range
163
+
164
+ logger.debug(f"C16RS: set_acq_param: {self._acq_param}")
165
+
166
+ # 参数改变后,重置通道位置映射
167
+ self._reverse_ch_pos = None
168
+
169
+ # 信号数据转换(默认不处理)
170
+ def _signal_wrapper(self, data: RscPacket):
171
+ if data is None:
172
+ return
173
+ # 根据映射关系做通道转换-(注意数据和通道的一致性)
174
+ # data.channels = [self.channel_display_mapping.get(i, i) for i in data.channels]
175
+
176
+ # 升级为类变量,减少计算
177
+ if self._reverse_ch_pos is None:
178
+ self._reverse_ch_pos = map_indices(self._acq_param["channels"], data.channels)
179
+
180
+ # 更新通道(数据)顺序和输入一致
181
+ data.channels = self._acq_param["original_channels"]
182
+ data.eeg = [data.eeg[i] for i in self._reverse_ch_pos]
183
+
184
+
185
+ def map_indices(A, B):
186
+ """
187
+
188
+ 参数:
189
+ A: 源数组(无重复值)
190
+ B: 目标数组(无重复值)
191
+
192
+ 返回:
193
+ C: 与A长度相同的数组,元素为A中对应值在B中的索引(不存在则为-1)
194
+ """
195
+ # 创建B的值到索引的映射字典(O(n)操作)
196
+ b_map = {value: idx for idx, value in enumerate(B)}
197
+
198
+ # 遍历A,获取每个元素在B中的位置(O(m)操作)
199
+ return [b_map.get(a, -1) for a in A]
200
+
201
+
202
+ # Register the C16RS device with the DeviceFactory
203
+ DeviceFactory.register(C16RS.device_type, C16RS)
204
+
205
+