qlsdk2 0.4.1__py3-none-any.whl → 0.4.2__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.
@@ -0,0 +1,384 @@
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 ARSKindlingEDFHandler
9
+ from qlsdk.rsc.interface import IDevice, IParser
10
+ from qlsdk.rsc.command import *
11
+ from qlsdk.rsc.parser.base import TcpMessageParser
12
+ from qlsdk.rsc.device.base import QLBaseDevice
13
+
14
+ class ARSKindling(QLBaseDevice):
15
+
16
+ device_type = 0x60 # C64RS设备类型标识符
17
+
18
+ def __init__(self, socket):
19
+ super().__init__(socket)
20
+ # self.socket = socket
21
+
22
+ self._id = None
23
+
24
+ # 设备信息
25
+ # self.device_id = None
26
+ # self.device_name = None
27
+ self._device_no = None
28
+ # self.software_version = None
29
+ # self.hardware_version = None
30
+ # self.connect_time = None
31
+ # self.current_time = None
32
+ # # mV
33
+ # self.voltage = None
34
+ # # %
35
+ # self.battery_remain = None
36
+ # # %
37
+ # self.battery_total = None
38
+ # persist
39
+ self._recording = False
40
+ self._storage_path = None
41
+ self._file_prefix = None
42
+
43
+ # 可设置参数
44
+ # 采集:采样量程、采样率、采样通道
45
+ # 刺激:刺激电流、刺激频率、刺激时间、刺激通道
46
+ # 采样量程(mV):188、375、563、750、1125、2250、4500
47
+ self._sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188
48
+ # 采样率(Hz):250、500、1000、2000、4000、8000、16000、32000
49
+ self._sample_rate:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 500
50
+ self._physical_max = self._sample_range * 1000 # 物理最大值(uV)
51
+ self._physical_min = -self._sample_range * 1000 # 物理最小值(uV)
52
+ self._digital_max = 8388607
53
+ self._digital_min = -8388608
54
+ self._physical_range = self._physical_max - self._physical_min
55
+ self._digital_range = 16777215
56
+ self._acq_channels = None
57
+ self._acq_param = {
58
+ "sample_range": 188,
59
+ "sample_rate": 500,
60
+ "channels": [],
61
+ }
62
+
63
+ self._stim_param = {
64
+ "stim_type": 0, # 刺激类型:0-所有通道参数相同, 1: 通道参数不同
65
+ "channels": [],
66
+ "param": [{
67
+ "channel_id": 0, #通道号 从0开始 -- 必填
68
+ "waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
69
+ "current": 1, #电流强度(mA) -- 必填
70
+ "duration": 30, #平稳阶段持续时间(s) -- 必填
71
+ "ramp_up": 5, #上升时间(s) 默认0
72
+ "ramp_down": 5, #下降时间(s) 默认0
73
+ "frequency": 500, #频率(Hz) -- 非直流必填
74
+ "phase_position": 0, #相位 -- 默认0
75
+ "duration_delay": "0", #延迟启动时间(s) -- 默认0
76
+ "pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
77
+ },
78
+ {
79
+ "channel_id": 1, #通道号 从0开始 -- 必填
80
+ "waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
81
+ "current": 1, #电流强度(mA) -- 必填
82
+ "duration": 30, #平稳阶段持续时间(s) -- 必填
83
+ "ramp_up": 5, #上升时间(s) 默认0
84
+ "ramp_down": 5, #下降时间(s) 默认0
85
+ "frequency": 500, #频率(Hz) -- 非直流必填
86
+ "phase_position": 0, #相位 -- 默认0
87
+ "duration_delay": "0", #延迟启动时间(s) -- 默认0
88
+ "pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
89
+ }
90
+ ]
91
+ }
92
+
93
+ self.stim_paradigm = None
94
+
95
+ signal_info = {
96
+ "param" : None,
97
+ "start_time" : None,
98
+ "finished_time" : None,
99
+ "packet_total" : None,
100
+ "last_packet_time" : None,
101
+ "state" : 0
102
+ }
103
+ stim_info = {
104
+
105
+ }
106
+ Impedance_info = {
107
+
108
+ }
109
+ # 信号采集状态
110
+ # 信号数据包总数(一个信号采集周期内)
111
+ # 信号采集参数
112
+ # 电刺激状态
113
+ # 电刺激开始时间(最近一次)
114
+ # 电刺激结束时间(最近一次)
115
+ # 电刺激参数
116
+ # 启动数据解析线程
117
+ # 数据存储状态
118
+ # 存储目录
119
+
120
+ #
121
+ self.__signal_consumer: Dict[str, Queue[Any]]={}
122
+ self.__impedance_consumer: Dict[str, Queue[Any]]={}
123
+
124
+ # EDF文件处理器
125
+ self._edf_handler = None
126
+ self.storage_enable = True
127
+
128
+ # self.start_message_listening()
129
+ # self.start()
130
+ # self.parser.set_device(self)
131
+
132
+ self.channel_mapping = {
133
+ "A1": 55, "A2": 56, "A3": 53, "A4": 54, "A5": 51, "A6": 52, "A7": 49, "A8": 50,
134
+ "A9": 57, "A10": 58, "A11": 59, "A12": 60, "A13": 61, "A14": 62,
135
+
136
+ "B1": 39, "B2": 40, "B3": 37, "B4": 38, "B5": 35, "B6": 36, "B7": 33, "B8": 34,
137
+ "B9": 41, "B10": 42, "B11": 43, "B12": 44, "B13": 45, "B14": 46,
138
+
139
+ "C1": 23, "C2": 24, "C3": 21, "C4": 22, "C5": 19, "C6": 20, "C7": 17, "C8": 18,
140
+ "C9": 25, "C10": 26, "C11": 27, "C12": 28, "C13": 29, "C14": 30,
141
+
142
+ "D1": 7, "D2": 8, "D3": 5, "D4": 6, "D5": 3, "D6": 4, "D7": 1, "D8": 2,
143
+ "D9": 9, "D10": 10, "D11": 11, "D12": 12, "D13": 13, "D14": 14,
144
+ }
145
+
146
+ @property
147
+ def device_no(self):
148
+ return self._device_no
149
+
150
+ @device_no.setter
151
+ def device_no(self, value: str):
152
+ self._device_no = value
153
+
154
+ @property
155
+ def parser(self) -> IParser:
156
+ return self._parser
157
+
158
+ @classmethod
159
+ def from_parent(cls, parent:IDevice) -> IDevice:
160
+ rlt = cls(parent.socket)
161
+ rlt.device_id = parent.device_id
162
+ rlt._device_no = parent.device_no
163
+ return rlt
164
+
165
+ def init_edf_handler(self):
166
+ self._edf_handler = ARSKindlingEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
167
+ self._edf_handler.set_device_type(self.device_type)
168
+ self._edf_handler.set_device_no(self.device_no)
169
+ self._edf_handler.set_storage_path(self._storage_path)
170
+ self._edf_handler.set_file_prefix(self._file_prefix)
171
+
172
+ @property
173
+ def edf_handler(self):
174
+ if not self.storage_enable:
175
+ return None
176
+
177
+ if self._edf_handler is None:
178
+ self.init_edf_handler()
179
+
180
+ return self._edf_handler
181
+
182
+ @property
183
+ def acq_channels(self):
184
+ if self._acq_channels is None:
185
+ self._acq_channels = [i for i in range(1, 63)]
186
+ return self._acq_channels
187
+ @property
188
+ def sample_range(self):
189
+ return self._sample_range if self._sample_range else 188
190
+ @property
191
+ def sample_rate(self):
192
+ return self._sample_rate if self._sample_rate else 500
193
+ @property
194
+ def resolution(self):
195
+ return 24
196
+
197
+ @property
198
+ def signal_consumers(self):
199
+ return self.__signal_consumer
200
+
201
+ @property
202
+ def impedance_consumers(self):
203
+ return self.__impedance_consumer
204
+
205
+ # 设置记录文件路径
206
+ def set_storage_path(self, path):
207
+ self._storage_path = path
208
+
209
+ # 设置记录文件名称前缀
210
+ def set_file_prefix(self, prefix):
211
+ self._file_prefix = prefix
212
+
213
+ # 接收socket消息
214
+ def accept(self):
215
+ while True:
216
+ # 缓冲去4M
217
+ data = self.socket.recv(4096*1024)
218
+ if not data:
219
+ logger.warning(f"设备{self.device_no}连接结束")
220
+ break
221
+
222
+ self._parser.append(data)
223
+
224
+
225
+ # socket发送数据
226
+ def send(self, data):
227
+ self.socket.sendall(data)
228
+
229
+ # 设置刺激参数
230
+ def set_stim_param(self, param):
231
+ self.stim_paradigm = param
232
+
233
+ # 设置采集参数
234
+ def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
235
+ self._acq_param["original_channels"] = channels
236
+ for k in channels.keys():
237
+ if isinstance(channels[k], list):
238
+ temp = [k + str(i) for i in channels[k]]
239
+ channels[k] = [self.channel_mapping.get(c, 1) for c in temp]
240
+ else:
241
+ channels[k] = [k + str(channels[k])]
242
+
243
+
244
+
245
+ self._acq_param["channels"] = channels
246
+ self._acq_param["sample_rate"] = sample_rate
247
+ self._acq_param["sample_range"] = sample_range
248
+ self._acq_channels = channels
249
+ self._sample_rate = sample_rate
250
+ self._sample_range = sample_range
251
+
252
+ # 通用配置-TODO
253
+ def set_config(self, key:str, val: str):
254
+ pass
255
+
256
+ def start_impedance(self):
257
+ logger.info("启动阻抗测量")
258
+ msg = StartImpedanceCommand.build(self).pack()
259
+ logger.debug(f"start_impedance message is {msg.hex()}")
260
+ self.socket.sendall(msg)
261
+
262
+ def stop_impedance(self):
263
+ logger.info("停止阻抗测量")
264
+ msg = StopImpedanceCommand.build(self).pack()
265
+ logger.debug(f"stop_impedance message is {msg.hex()}")
266
+ self.socket.sendall(msg)
267
+
268
+ def start_stimulation(self):
269
+ if self.stim_paradigm is None:
270
+ logger.warning("刺激参数未设置,请先设置刺激参数")
271
+ return
272
+ logger.info("启动电刺激")
273
+ msg = StartStimulationCommand.build(self).pack()
274
+ logger.debug(f"start_stimulation message is {msg.hex()}")
275
+ self.socket.sendall(msg)
276
+ t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
277
+ t.start()
278
+
279
+ def _stop_stimulation_trigger(self, duration):
280
+ delay = duration
281
+ while delay > 0:
282
+ sleep(1)
283
+ delay -= 1
284
+ logger.info(f"_stop_stimulation_trigger duration: {duration}")
285
+ if self._edf_handler:
286
+ self._edf_handler.trigger("stimulation should be stopped")
287
+ else:
288
+ logger.warning("stop stim trigger fail. no edf writer alive")
289
+
290
+ def stop_stimulation(self):
291
+ logger.info("停止电刺激")
292
+ msg = StopStimulationCommand.pack()
293
+ logger.debug(f"stop_stimulation message is {msg.hex()}")
294
+ self.socket.sendall(msg)
295
+
296
+ # 启动采集
297
+ def start_acquisition(self, recording = True):
298
+ logger.info("启动信号采集")
299
+ self._recording = recording
300
+ # 设置数据采集参数
301
+ param_bytes = SetAcquisitionParamCommand.build(self).pack()
302
+ # 启动数据采集
303
+ start_bytes = StartAcquisitionCommand.build(self).pack()
304
+ msg = param_bytes + start_bytes
305
+ logger.debug(f"start_acquisition message is {msg.hex()}")
306
+ self.socket.sendall(msg)
307
+
308
+ # 停止采集
309
+ def stop_acquisition(self):
310
+ logger.info("停止信号采集")
311
+ msg = StopAcquisitionCommand.build(self).pack()
312
+ logger.debug(f"stop_acquisition message is {msg.hex()}")
313
+ self.socket.sendall(msg)
314
+ if self._edf_handler:
315
+ # 发送结束标识
316
+ self.edf_handler.write(None)
317
+
318
+ # 订阅实时数据
319
+ def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
320
+ logger.info(f"订阅{self.device_no}的数据流")
321
+ # 数据队列
322
+ if q is None:
323
+ q = Queue(maxsize=1000)
324
+
325
+ # 队列名称
326
+ if topic is None:
327
+ topic = f"{type}_{time_ns()}"
328
+
329
+ # 订阅生理电信号数据
330
+ if type == "signal":
331
+ # topic唯一,用来区分不同的订阅队列(下同)
332
+ if topic in list(self.__signal_consumer.keys()):
333
+ logger.warning(f"exists {type} subscribe of {topic}")
334
+ else:
335
+ self.__signal_consumer[topic] = q
336
+
337
+ # 订阅阻抗数据
338
+ if type == "impedance":
339
+ if topic in list(self.__signal_consumer.keys()):
340
+ logger.warning(f"exists {type} subscribe of {topic}")
341
+ else:
342
+ self.__impedance_consumer[topic] = q
343
+
344
+ return topic, q
345
+
346
+ def trigger(self, desc):
347
+ if self._edf_handler:
348
+ self.edf_handler.trigger(desc)
349
+ else:
350
+ logger.warning("no edf handler, no place to recording trigger")
351
+
352
+
353
+ def gen_set_acquirement_param(self) -> bytes:
354
+
355
+ arr = []
356
+ for k in self.acq_channels.keys():
357
+ arr = list(arr + self.acq_channels[k])
358
+
359
+ arr = list(set(arr))
360
+ body = to_bytes(arr)
361
+ body += self.sample_range.to_bytes(4, byteorder='little')
362
+ body += self.sample_rate.to_bytes(4, byteorder='little')
363
+ body += self.sample_num.to_bytes(4, byteorder='little')
364
+ body += self.resolution.to_bytes(1, byteorder='little')
365
+ body += bytes.fromhex('00')
366
+
367
+ return body
368
+
369
+ def __str__(self):
370
+ return f'''
371
+ Device:
372
+ Name: {self.device_no},
373
+ Type: {hex(self.device_type) if self.device_type else None},
374
+ ID: {self.device_id if self.device_id else None},
375
+ Software: {self.software_version},
376
+ Hardware: {self.hardware_version},
377
+ Connect Time: {self.connect_time},
378
+ Current Time: {self.current_time},
379
+ Voltage: {str(self.voltage) + "mV" if self.voltage else None},
380
+ Battery Remain: {str(self.battery_remain)+ "%" if self.battery_remain else None},
381
+ Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
382
+ '''
383
+
384
+
qlsdk/rsc/device/base.py CHANGED
@@ -5,6 +5,7 @@ from time import sleep, time_ns
5
5
  from typing import Any, Dict, Literal
6
6
 
7
7
  from loguru import logger
8
+ from qlsdk.core.utils import to_bytes
8
9
  from qlsdk.persist import RscEDFHandler
9
10
  from qlsdk.rsc.interface import IDevice, IParser
10
11
  from qlsdk.rsc.parser import TcpMessageParser
@@ -15,6 +16,9 @@ class QLBaseDevice(IDevice):
15
16
  def __init__(self, socket):
16
17
  self.socket = socket
17
18
 
19
+ # 启动数据解析线程
20
+ self._parser: IParser = None
21
+
18
22
  # 设备信息
19
23
  self.device_id = None
20
24
  self.device_name = None
@@ -119,20 +123,54 @@ class QLBaseDevice(IDevice):
119
123
  # EDF文件处理器
120
124
  self._edf_handler = None
121
125
  self.storage_enable = True
126
+ self._listening = False
127
+ # self.ready()
122
128
 
123
- # 启动数据解析线程
124
- self._parser: IParser = TcpMessageParser(self)
125
- self._parser.start()
129
+ def parser(self) -> IParser:
130
+ return self._parser
126
131
 
127
- # 启动数据接收线程
128
- self._accept = Thread(target=self.accept)
129
- self._accept.daemon = True
130
- self._accept.start()
132
+ def start_listening(self):
133
+
134
+ try:
135
+ self.start_message_parser()
136
+
137
+ self.start_message_listening()
138
+ except Exception as e:
139
+ logger.error(f"设备{self.device_no}准备失败: {str(e)}")
140
+ return False
141
+
142
+ return True
143
+
144
+ def stop_listening(self):
145
+ logger.info(f"设备{self.device_no}停止socket监听")
146
+ self._listening = False
131
147
 
132
148
  @property
133
149
  def device_type(self) -> int:
134
150
  return self._device_type
135
151
 
152
+ def start_message_parser(self) -> None:
153
+ self._parser = TcpMessageParser(self)
154
+ self._parser.start()
155
+ logger.info("TCP消息解析器已启动")
156
+
157
+ def start_message_listening(self) -> None:
158
+ def _accept():
159
+ while self._listening:
160
+ # 缓冲去4M
161
+ data = self.socket.recv(4096*1024)
162
+ if not data:
163
+ logger.warning(f"设备{self.device_name}连接结束")
164
+ break
165
+
166
+ self._parser.append(data)
167
+
168
+ # 启动数据接收线程
169
+ self._listening = True
170
+ message_accept = Thread(target=_accept, daemon=True)
171
+ message_accept.start()
172
+ logger.info(f"socket消息监听已启动")
173
+
136
174
  def set_device_type(self, type: int):
137
175
  self._device_type = type
138
176
 
@@ -168,11 +206,12 @@ class QLBaseDevice(IDevice):
168
206
  return self._digital_range
169
207
 
170
208
  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)
209
+ pass
210
+ # self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
211
+ # self._edf_handler.set_device_type(self.device_type)
212
+ # self._edf_handler.set_device_no(self.device_no)
213
+ # self._edf_handler.set_storage_path(self._storage_path)
214
+ # self._edf_handler.set_file_prefix(self._file_prefix)
176
215
 
177
216
  # eeg数字值转物理值
178
217
  def eeg2phy(self, digital:int):
@@ -221,18 +260,6 @@ class QLBaseDevice(IDevice):
221
260
  # 设置记录文件名称前缀
222
261
  def set_file_prefix(self, prefix):
223
262
  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
263
 
237
264
  # socket发送数据
238
265
  def send(self, data):
@@ -320,6 +347,8 @@ class QLBaseDevice(IDevice):
320
347
  # 订阅实时数据
321
348
  def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
322
349
 
350
+ logger.info(f"base订阅{self.device_name}的数据流")
351
+ # 数据队列
323
352
  # 数据队列
324
353
  if q is None:
325
354
  q = Queue(maxsize=1000)
@@ -351,6 +380,17 @@ class QLBaseDevice(IDevice):
351
380
  else:
352
381
  logger.warning("no edf handler, no place to recording trigger")
353
382
 
383
+ def gen_set_acquirement_param(self) -> bytes:
384
+
385
+ body = to_bytes(self.acq_channels)
386
+ body += self.sample_range.to_bytes(4, byteorder='little')
387
+ body += self.sample_rate.to_bytes(4, byteorder='little')
388
+ body += self.sample_num.to_bytes(4, byteorder='little')
389
+ body += self.resolution.to_bytes(1, byteorder='little')
390
+ body += bytes.fromhex('00')
391
+
392
+ return body
393
+
354
394
  def __str__(self):
355
395
  return f'''
356
396
  Device:
@@ -366,21 +406,6 @@ class QLBaseDevice(IDevice):
366
406
  Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
367
407
  '''
368
408
 
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
409
  def __eq__(self, other):
385
410
  return self.device_name == other.device_name and self.device_type == other.device_type and self.device_id == other.device_id
386
411