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/core/entity/__init__.py +1 -0
- qlsdk/core/utils.py +20 -15
- qlsdk/persist/__init__.py +2 -1
- qlsdk/persist/ars_edf.py +278 -0
- qlsdk/persist/rsc_edf.py +47 -28
- qlsdk/persist/stream.py +161 -0
- qlsdk/rsc/command/__init__.py +28 -30
- qlsdk/rsc/device/__init__.py +6 -1
- qlsdk/rsc/device/arskindling.py +386 -0
- qlsdk/rsc/device/base.py +137 -70
- qlsdk/rsc/device/c16_rs.py +205 -0
- qlsdk/rsc/device/c256_rs.py +365 -0
- qlsdk/rsc/device/c64_rs.py +9 -14
- qlsdk/rsc/device/c64s1.py +365 -0
- qlsdk/rsc/device/device_factory.py +6 -1
- qlsdk/rsc/interface/device.py +12 -2
- qlsdk/rsc/interface/parser.py +4 -1
- qlsdk/rsc/manager/container.py +21 -17
- qlsdk/rsc/network/discover.py +25 -36
- qlsdk/rsc/parser/base.py +106 -22
- qlsdk2-0.5.0.dist-info/METADATA +40 -0
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.5.0.dist-info}/RECORD +24 -18
- qlsdk2-0.4.1.dist-info/METADATA +0 -121
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.5.0.dist-info}/WHEEL +0 -0
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.5.0.dist-info}/top_level.txt +0 -0
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.
|
|
117
|
-
self.
|
|
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
|
|
125
|
-
|
|
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.
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
285
|
+
return self._signal_consumer
|
|
212
286
|
|
|
213
287
|
@property
|
|
214
288
|
def impedance_consumers(self):
|
|
215
|
-
return self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
335
|
-
logger.warning(f"
|
|
405
|
+
if topic in list(self._signal_consumer.keys()):
|
|
406
|
+
logger.warning(f"已存在主题[{topic}]的信号数据订阅!")
|
|
336
407
|
else:
|
|
337
|
-
self.
|
|
408
|
+
self._signal_consumer[topic] = q
|
|
338
409
|
|
|
339
410
|
# 订阅阻抗数据
|
|
340
411
|
if type == "impedance":
|
|
341
|
-
if topic in list(self.
|
|
342
|
-
logger.warning(f"
|
|
412
|
+
if topic in list(self._impedance_consumer.keys()):
|
|
413
|
+
logger.warning(f"已存在主题[{topic}]的阻抗数据订阅!")
|
|
343
414
|
else:
|
|
344
|
-
self.
|
|
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("
|
|
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
|
+
|