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/rsc/entity.py CHANGED
@@ -1,11 +1,12 @@
1
1
  from multiprocessing import Queue, Process
2
2
  from typing import Any, Dict, Literal
3
- # from rsc.command import *
4
- from qlsdk.core import *
5
- from qlsdk.core.device import BaseDevice
6
3
  from threading import Thread
7
4
  from loguru import logger
8
- from time import time_ns
5
+ from time import time_ns, sleep
6
+
7
+ from qlsdk.core import *
8
+ from qlsdk.core.device import BaseDevice
9
+ from qlsdk.persist import RscEDFHandler
9
10
 
10
11
  class QLDevice(BaseDevice):
11
12
  def __init__(self, socket):
@@ -25,6 +26,10 @@ class QLDevice(BaseDevice):
25
26
  self.battery_remain = None
26
27
  # %
27
28
  self.battery_total = None
29
+ # persist
30
+ self._recording = False
31
+ self._storage_path = None
32
+ self._file_prefix = None
28
33
 
29
34
  # 可设置参数
30
35
  # 采集:采样量程、采样率、采样通道
@@ -33,6 +38,12 @@ class QLDevice(BaseDevice):
33
38
  self._sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188
34
39
  # 采样率(Hz):250、500、1000、2000、4000、8000、16000、32000
35
40
  self._sample_rate:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 500
41
+ self._physical_max = 188000
42
+ self._physical_min = -188000
43
+ self._digital_max = 8388607
44
+ self._digital_min = -8388608
45
+ self._physical_range = 376000
46
+ self._digital_range = 16777215
36
47
  self._acq_channels = None
37
48
  self._acq_param = {
38
49
  "sample_range": 188,
@@ -101,6 +112,9 @@ class QLDevice(BaseDevice):
101
112
  self.__signal_consumer: Dict[str, Queue[Any]]={}
102
113
  self.__impedance_consumer: Dict[str, Queue[Any]]={}
103
114
 
115
+ # EDF文件处理器
116
+ self._edf_handler = None
117
+ self.storage_enable = True
104
118
 
105
119
  self._parser = DeviceParser(self)
106
120
  self._parser.start()
@@ -109,18 +123,45 @@ class QLDevice(BaseDevice):
109
123
  self._accept = Thread(target=self.accept)
110
124
  self._accept.daemon = True
111
125
  self._accept.start()
126
+
127
+ def init_edf_handler(self):
128
+ self._edf_handler = RscEDFHandler(self.sample_rate, self._physical_max , self._physical_min, self.resolution)
129
+ self._edf_handler.set_device_type(self.device_type)
130
+ self._edf_handler.set_device_no(self.device_name)
131
+ self._edf_handler.set_storage_path(self._storage_path)
132
+ self._edf_handler.set_file_prefix(self._file_prefix)
133
+
134
+ # eeg数字值转物理值
135
+ def eeg2phy(self, digital:int):
136
+ # 向量化计算(自动支持广播)
137
+ return ((digital - self._digital_min) / self._digital_range) * self._physical_range + self._physical_min
112
138
 
139
+ @property
140
+ def edf_handler(self):
141
+ if not self.storage_enable:
142
+ return None
143
+
144
+ if self._edf_handler is None:
145
+ self.init_edf_handler()
146
+
147
+ return self._edf_handler
148
+
149
+ # 采集通道列表, 从1开始
113
150
  @property
114
151
  def acq_channels(self):
115
152
  if self._acq_channels is None:
116
153
  self._acq_channels = [i for i in range(1, 63)]
117
154
  return self._acq_channels
155
+
156
+ # 量程范围
118
157
  @property
119
158
  def sample_range(self):
159
+
120
160
  return self._sample_range if self._sample_range else 188
121
161
  @property
122
162
  def sample_rate(self):
123
163
  return self._sample_rate if self._sample_rate else 500
164
+
124
165
  @property
125
166
  def resolution(self):
126
167
  return 24
@@ -132,11 +173,18 @@ class QLDevice(BaseDevice):
132
173
  @property
133
174
  def impedance_consumers(self):
134
175
  return self.__impedance_consumer
176
+
177
+ # 设置记录文件路径
178
+ def set_storage_path(self, path):
179
+ self._storage_path = path
180
+
181
+ # 设置记录文件名称前缀
182
+ def set_file_prefix(self, prefix):
183
+ self._file_prefix = prefix
135
184
 
136
185
  def accept(self):
137
186
  while True:
138
187
  data = self.socket.recv(4096*1024)
139
- # logger.debug(f"QLDevice接收到数据: {data.hex()}")
140
188
  if not data:
141
189
  logger.warning(f"设备{self.device_name}连接结束")
142
190
  break
@@ -155,35 +203,31 @@ class QLDevice(BaseDevice):
155
203
  self.stim_paradigm = param
156
204
 
157
205
  # 设置采集参数
158
- def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
206
+ def set_acq_param(self, channels, sample_rate:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 500, sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188):
159
207
  self._acq_param["channels"] = channels
160
208
  self._acq_param["sample_rate"] = sample_rate
161
209
  self._acq_param["sample_range"] = sample_range
162
210
  self._acq_channels = channels
163
211
  self._sample_rate = sample_rate
164
212
  self._sample_range = sample_range
213
+ # 根据量程更新信号物理值范围
214
+ self._physical_max = self._sample_range * 1000
215
+ self._physical_min = -self._sample_range * 1000
216
+ self._physical_range = self._physical_max - self._physical_min
165
217
 
166
- # 通用配置
218
+ # 通用配置-TODO
167
219
  def set_config(self, key:str, val: str):
168
220
  pass
169
221
 
170
- def set_impedance_param(self):
171
- channels = bytes.fromhex("FFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000")
172
- sample_rate = 1000
173
- sample_len = 300
174
- resolution = self.resolution
175
-
176
222
  def start_impedance(self):
177
223
  logger.info("启动阻抗测量")
178
224
  msg = StartImpedanceCommand.build(self).pack()
179
- # msg = bytes.fromhex("5aa50239320013243f0000001104ffffffffffffffff000000000000000000000000000000000000000000000000e8030000fa00000010000164000000a11e5aa50239320013243a00000012040000000000000000000000000000000000000000000000000000000000000000000001000000000000000325")
180
225
  logger.debug(f"start_impedance message is {msg.hex()}")
181
226
  self.socket.sendall(msg)
182
227
 
183
228
  def stop_impedance(self):
184
229
  logger.info("停止阻抗测量")
185
230
  msg = StopImpedanceCommand.build(self).pack()
186
- # msg = bytes.fromhex("5aa5023932001324100000001304e9df")
187
231
  logger.debug(f"stop_impedance message is {msg.hex()}")
188
232
  self.socket.sendall(msg)
189
233
 
@@ -192,20 +236,33 @@ class QLDevice(BaseDevice):
192
236
  logger.warning("刺激参数未设置,请先设置刺激参数")
193
237
  return
194
238
  logger.info("启动电刺激")
195
- # conf = SetStimulationParamCommand.build(self).pack()
196
239
  msg = StartStimulationCommand.build(self).pack()
197
240
  logger.debug(f"start_stimulation message is {msg.hex()}")
198
241
  self.socket.sendall(msg)
242
+ t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
243
+ t.start()
244
+
245
+ def _stop_stimulation_trigger(self, duration):
246
+ delay = duration
247
+ while delay > 0:
248
+ sleep(1)
249
+ delay -= 1
250
+ logger.info(f"_stop_stimulation_trigger duration: {duration}")
251
+ if self._edf_handler:
252
+ self._edf_handler.trigger("stimulation should be stopped")
253
+ else:
254
+ logger.warning("trigger fail for 'stop stim'. no recording file to write")
199
255
 
200
256
  def stop_stimulation(self):
201
257
  logger.info("停止电刺激")
202
- msg = StopStimulationCommand.pack()
258
+ msg = StopStimulationCommand.build().pack()
203
259
  logger.debug(f"stop_stimulation message is {msg.hex()}")
204
260
  self.socket.sendall(msg)
205
261
 
206
262
  # 启动采集
207
- def start_acquisition(self):
263
+ def start_acquisition(self, recording = True):
208
264
  logger.info("启动信号采集")
265
+ self._recording = recording
209
266
  # 设置数据采集参数
210
267
  param_bytes = SetAcquisitionParamCommand.build(self).pack()
211
268
  # 启动数据采集
@@ -218,15 +275,18 @@ class QLDevice(BaseDevice):
218
275
  def stop_acquisition(self):
219
276
  logger.info("停止信号采集")
220
277
  msg = StopAcquisitionCommand.build(self).pack()
221
- logger.debug(f"stop_acquisition message is {msg}")
278
+ logger.debug(f"stop_acquisition message is {msg.hex()}")
222
279
  self.socket.sendall(msg)
280
+ if self._edf_handler:
281
+ # 发送结束标识
282
+ self.edf_handler.write(None)
223
283
 
224
284
  # 订阅实时数据
225
285
  def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
226
286
 
227
287
  # 数据队列
228
288
  if q is None:
229
- q = Queue()
289
+ q = Queue(maxsize=1000)
230
290
 
231
291
  # 队列名称
232
292
  if topic is None:
@@ -248,6 +308,12 @@ class QLDevice(BaseDevice):
248
308
  self.__impedance_consumer[topic] = q
249
309
 
250
310
  return topic, q
311
+
312
+ def trigger(self, desc):
313
+ if self._edf_handler:
314
+ self.edf_handler.trigger(desc)
315
+ else:
316
+ logger.warning("no edf handler, no place to recording trigger")
251
317
 
252
318
  def __str__(self):
253
319
  return f'''
@@ -303,10 +369,6 @@ class RSC64R(QLDevice):
303
369
  class ARSKindling(QLDevice):
304
370
  def __init__(self, socket):
305
371
  super().__init__(socket)
306
-
307
-
308
-
309
-
310
372
 
311
373
  class DeviceParser(object):
312
374
  def __init__(self, device : QLDevice):
@@ -318,7 +380,7 @@ class DeviceParser(object):
318
380
 
319
381
  def append(self, buffer):
320
382
  self.cache += buffer
321
- logger.debug(f"append cache len: {len(self.cache)}")
383
+ logger.debug(f"已缓存的数据长度: {len(self.cache)}")
322
384
 
323
385
  # if not self.running:
324
386
  # self.start()
@@ -326,14 +388,13 @@ class DeviceParser(object):
326
388
  def __parser__(self):
327
389
  logger.info("数据解析开始")
328
390
  while self.running:
329
- # logger.debug(f" cache len: {len(self.cache)}")
330
391
  if len(self.cache) < 14:
331
392
  continue
332
393
  if self.cache[0] != 0x5A or self.cache[1] != 0xA5:
333
394
  self.cache = self.cache[1:]
334
395
  continue
335
396
  pkg_len = int.from_bytes(self.cache[8:12], 'little')
336
- logger.debug(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
397
+ logger.trace(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
337
398
  # 一次取整包数据
338
399
  if len(self.cache) < pkg_len:
339
400
  continue
@@ -368,11 +429,13 @@ class TCPMessage(object):
368
429
  TCPMessage._validate_packet(data)
369
430
  # 提取指令码
370
431
  cmd_code = int.from_bytes(data[TCPMessage.CMD_POS:TCPMessage.CMD_POS+2], 'little')
371
- logger.debug(f"收到指令:{hex(cmd_code)}")
372
432
  cmd_class = CommandFactory.create_command(cmd_code)
373
- logger.debug(f"Command class: {cmd_class}")
433
+ logger.debug(f"收到指令:{cmd_class.cmd_desc}[{hex(cmd_code)}]")
374
434
  instance = cmd_class(device)
435
+ start = time_ns()
436
+ logger.debug(f"开始解析: {start}")
375
437
  instance.parse_body(data[TCPMessage.HEADER_LEN:-2])
438
+ logger.debug(f"解析完成:{time_ns()}, 解析耗时:{time_ns() - start}ns")
376
439
  return instance
377
440
 
378
441
  @staticmethod
@@ -388,9 +451,9 @@ class TCPMessage(object):
388
451
  if len(data) != expected_len:
389
452
  raise ValueError(f"Length mismatch: {len(data)} vs {expected_len}")
390
453
 
391
- logger.debug(f"checksum: {int.from_bytes(data[-2:], 'little')}")
392
- checksum = crc16(data[:-2])
393
- logger.debug(f"checksum recv: {checksum}")
454
+ # logger.trace(f"checksum: {int.from_bytes(data[-2:], 'little')}")
455
+ # checksum = crc16(data[:-2])
456
+ # logger.trace(f"checksum recv: {checksum}")
394
457
 
395
458
 
396
459
 
@@ -402,150 +465,3 @@ class DataPacket(object):
402
465
 
403
466
  def parse_body(self, body: bytes):
404
467
  raise NotImplementedError("Subclasses should implement this method")
405
-
406
-
407
-
408
- class C64Channel(Enum):
409
- CH0 = 0
410
- CH1 = 1
411
- CH2 = 2
412
- CH3 = 3
413
- CH4 = 4
414
- CH5 = 5
415
- CH6 = 6
416
- CH7 = 7
417
- CH8 = 8
418
- CH9 = 9
419
- CH10 = 10
420
- CH11 = 11
421
- CH12 = 12
422
- CH13 = 13
423
- CH14 = 14
424
- CH15 = 15
425
-
426
- class WaveForm(Enum):
427
- DC = 0
428
- SQUARE = 1
429
- AC = 2
430
- CUSTOM = 3
431
- PULSE = 4
432
-
433
- # # 刺激通道
434
- # class StimulationChannel(object):
435
- # def __init__(self, channel_id: int, waveform: int, current: float, duration: float, ramp_up: float = None, ramp_down: float = None,
436
- # frequency: float = None, phase_position: int = None, duration_delay: float = None, pulse_width: int = None, pulse_width_rate: int = 1):
437
- # self.channel_id = channel_id
438
- # self.waveform = waveform
439
- # self.current_max = current
440
- # self.current_min = current
441
- # self.duration = duration
442
- # self.ramp_up = ramp_up
443
- # self.ramp_down = ramp_down
444
- # self.frequency = frequency
445
- # self.phase_position = phase_position
446
- # self.duration_delay = duration_delay
447
- # self.pulse_width = pulse_width
448
- # self.delay_time = 0
449
- # self.pulse_interval = 0
450
- # self.with_group_repeats = 1
451
- # self.pulse_width_rate = 1065353216
452
- # self.pulse_time_f = 0
453
- # self.pulse_time_out = 0
454
- # self.pulse_time_idle = 0
455
-
456
- # def to_bytes(self):
457
- # # Convert the object to bytes for transmission
458
- # result = self.channel_id.to_bytes(1, 'little')
459
- # wave_form = WaveForm.SQUARE.value if self.waveform == WaveForm.PULSE.value else self.waveform
460
- # result += wave_form.to_bytes(1, 'little')
461
- # result += int(self.current_max * 1000 * 1000).to_bytes(4, 'little')
462
- # # result += int(self.current_min * 1000).to_bytes(2, 'little')
463
- # result += int(self.frequency).to_bytes(2, 'little')
464
- # result += int(self.pulse_width).to_bytes(2, 'little')
465
- # result += int(self.pulse_width_rate).to_bytes(4, 'little')
466
-
467
- # result += int(self.pulse_interval).to_bytes(2, 'little')
468
- # result += int(self.with_group_repeats).to_bytes(2, 'little')
469
- # result += int(self.pulse_time_f).to_bytes(4, 'little')
470
- # result += int(self.pulse_time_out).to_bytes(4, 'little')
471
- # result += int(self.pulse_time_idle).to_bytes(4, 'little')
472
-
473
- # result += int(self.delay_time).to_bytes(4, 'little')
474
- # result += int(self.ramp_up * 1000).to_bytes(4, 'little')
475
- # result += int((self.duration + self.ramp_up) * 1000).to_bytes(4, 'little')
476
- # result += int(self.ramp_down * 1000).to_bytes(4, 'little')
477
-
478
- # return result
479
-
480
- # def to_json(self):
481
- # return {
482
- # "channel_id": self.channel_id,
483
- # "waveform": self.waveform,
484
- # "current_max": self.current_max,
485
- # "current_min": self.current_min,
486
- # "duration": self.duration,
487
- # "ramp_up": self.ramp_up,
488
- # "ramp_down": self.ramp_down,
489
- # "frequency": self.frequency,
490
- # "phase_position": self.phase_position,
491
- # "duration_delay": self.duration_delay,
492
- # "pulse_width": self.pulse_width,
493
- # "delay_time": self.delay_time,
494
- # "pulse_interval": self.pulse_interval,
495
- # "with_group_repeats": self.with_group_repeats
496
- # }
497
-
498
- # # 刺激范式
499
- # class StimulationParadigm(object):
500
- # def __init__(self):
501
- # self.channels = None
502
- # self.duration = None
503
- # self.interval_time = 0
504
- # self.characteristic = 0
505
- # self.mode = 0
506
- # self.repeats = 0
507
-
508
- # def add_channel(self, channel: StimulationChannel, update=False):
509
- # if self.channels is None:
510
- # self.channels = {}
511
- # channel_id = channel.channel_id + 1
512
- # if channel_id in self.channels.keys():
513
- # logger.warning(f"Channel {channel_id} already exists")
514
- # if update:
515
- # self.channels[channel_id] = channel
516
- # else:
517
- # self.channels[channel_id] = channel
518
-
519
- # # 计算刺激时间
520
- # duration = channel.duration + channel.ramp_up + channel.ramp_down
521
- # if self.duration is None or duration > self.duration:
522
- # self.duration = duration
523
-
524
-
525
- # def to_bytes(self):
526
- # result = to_bytes(list(self.channels.keys()), 64)
527
- # result += int(self.duration * 1000).to_bytes(4, 'little')
528
- # result += int(self.interval_time).to_bytes(4, 'little')
529
- # result += int(self.characteristic).to_bytes(4, 'little')
530
- # result += int(self.mode).to_bytes(1, 'little')
531
- # result += int(self.repeats).to_bytes(4, 'little')
532
- # for channel in self.channels.values():
533
- # result += channel.to_bytes()
534
- # return result
535
-
536
- # def to_json(self):
537
- # # Convert the object to JSON for transmission
538
- # return {
539
- # "channels": list(self.channels.keys()),
540
- # "duration": self.duration,
541
- # "interval_time": self.interval_time,
542
- # "characteristic": self.characteristic,
543
- # "mode": self.mode,
544
- # "repeats": self.repeats,
545
- # "stim": [channel.to_json() for channel in self.channels.values()]
546
- # }
547
-
548
- # @staticmethod
549
- # def from_json(param: Dict[str, Any]):
550
- # pass
551
-
@@ -0,0 +1,3 @@
1
+ from .device import *
2
+ from .parser import *
3
+ from .handler import *
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class ICommand(ABC):
4
+ def __init__(self):
5
+ super().__init__()
6
+
7
+
8
+ @abstractmethod
9
+ def append(self, buffer):
10
+ pass
@@ -0,0 +1,107 @@
1
+ from abc import ABC, abstractmethod
2
+ import abc
3
+
4
+ class IDevice(ABC):
5
+
6
+ @property
7
+ @abc.abstractmethod
8
+ def device_type(self) -> int:
9
+ pass
10
+
11
+ def set_device_type(self, value: int):
12
+ pass
13
+
14
+ @property
15
+ def device_no(self) -> str:
16
+ pass
17
+
18
+ def set_device_no(self, value: str):
19
+ pass
20
+
21
+ def from_parent(cls, parent) :
22
+ pass
23
+
24
+ def start_implementation(self) -> None:
25
+ pass
26
+
27
+ def stop_implementation(self) -> None:
28
+ pass
29
+
30
+ def start_acquisition(self) -> None:
31
+ pass
32
+
33
+ def stop_acquisition(self) -> None:
34
+ pass
35
+
36
+ def subscribe(self, type="signal") -> None:
37
+ pass
38
+
39
+ def unsubscribe(self, topic) -> None:
40
+ pass
41
+
42
+ def start_stimulation(self, type="signal", duration=0) -> None:
43
+ pass
44
+
45
+ def stop_stimulation(self) -> None:
46
+ pass
47
+
48
+ def __eq__(self, other):
49
+ return self.device_id == other.device_id
50
+
51
+
52
+ class RscDevice(IDevice):
53
+ def __init__(self, socket):
54
+ super().__init__(socket)
55
+
56
+ def start_implementation(self):
57
+ """
58
+ Start the device implementation
59
+ """
60
+ raise NotImplementedError("This method should be overridden by subclasses")
61
+
62
+ def stop_implementation(self):
63
+ """
64
+ Stop the device implementation
65
+ """
66
+ raise NotImplementedError("This method should be overridden by subclasses")
67
+
68
+ def start_acquisition(self):
69
+ """
70
+ Start data acquisition
71
+ """
72
+ raise NotImplementedError("This method should be overridden by subclasses")
73
+
74
+ def stop_acquisition(self):
75
+ """
76
+ Stop data acquisition
77
+ """
78
+ raise NotImplementedError("This method should be overridden by subclasses")
79
+
80
+ def subscribe(self, type="signal"):
81
+ """
82
+ Subscribe to data of a specific type (default is "signal")
83
+ """
84
+ raise NotImplementedError("This method should be overridden by subclasses")
85
+
86
+ def unsubscribe(self, topic):
87
+ """
88
+ Unsubscribe from a specific topic
89
+ """
90
+ raise NotImplementedError("This method should be overridden by subclasses")
91
+
92
+ def start_stimulation(self, type="signal", duration=0):
93
+ """
94
+ Start stimulation of a specific type (default is "signal") for a given duration
95
+ """
96
+ raise NotImplementedError("This method should be overridden by subclasses")
97
+
98
+ def stop_stimulation(self):
99
+ """
100
+ Stop stimulation
101
+ """
102
+ raise NotImplementedError("This method should be overridden by subclasses")
103
+
104
+ class ProxyDevice(IDevice):
105
+ def __init__(self, socket, proxy_socket):
106
+ super().__init__(socket)
107
+ self.proxy_socket = proxy_socket
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class IHandler(ABC):
4
+ def __init__(self):
5
+ super().__init__()
6
+
7
+ @abstractmethod
8
+ def handler(self, buffer):
9
+ pass
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class IParser(ABC):
4
+ def __init__(self):
5
+ super().__init__()
6
+
7
+ @abstractmethod
8
+ def append(self, buffer):
9
+ pass
@@ -0,0 +1,2 @@
1
+
2
+ from .container import DeviceContainer