qlsdk2 0.4.0a3__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/ar4/__init__.py +9 -5
- qlsdk/core/device.py +10 -1
- qlsdk/core/message/command.py +56 -35
- qlsdk/persist/edf.py +21 -2
- qlsdk/persist/rsc_edf.py +187 -128
- qlsdk/rsc/__init__.py +4 -2
- qlsdk/rsc/command/__init__.py +183 -61
- qlsdk/rsc/command/message.py +171 -74
- qlsdk/rsc/device/__init__.py +2 -0
- qlsdk/rsc/device/base.py +388 -0
- qlsdk/rsc/device/c64_rs.py +364 -0
- qlsdk/rsc/device/device_factory.py +29 -0
- qlsdk/rsc/entity.py +63 -24
- qlsdk/rsc/interface/__init__.py +3 -0
- qlsdk/rsc/interface/command.py +10 -0
- qlsdk/rsc/interface/device.py +107 -0
- qlsdk/rsc/interface/handler.py +9 -0
- qlsdk/rsc/interface/parser.py +9 -0
- qlsdk/rsc/manager/__init__.py +2 -0
- qlsdk/rsc/manager/container.py +121 -0
- qlsdk/rsc/manager/search.py +0 -0
- qlsdk/rsc/network/__init__.py +1 -0
- qlsdk/rsc/network/discover.py +87 -0
- qlsdk/rsc/paradigm.py +1 -1
- qlsdk/rsc/parser/__init__.py +1 -0
- qlsdk/rsc/parser/base.py +66 -0
- qlsdk/sdk/ar4sdk.py +13 -4
- qlsdk/x8/__init__.py +4 -0
- {qlsdk2-0.4.0a3.dist-info → qlsdk2-0.4.1.dist-info}/METADATA +1 -1
- qlsdk2-0.4.1.dist-info/RECORD +58 -0
- qlsdk2-0.4.0a3.dist-info/RECORD +0 -42
- {qlsdk2-0.4.0a3.dist-info → qlsdk2-0.4.1.dist-info}/WHEEL +0 -0
- {qlsdk2-0.4.0a3.dist-info → qlsdk2-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
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.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 C64RS(QLBaseDevice):
|
|
15
|
+
|
|
16
|
+
device_type = 0x39 # C64RS设备类型标识符
|
|
17
|
+
|
|
18
|
+
def __init__(self, socket):
|
|
19
|
+
self.socket = socket
|
|
20
|
+
|
|
21
|
+
self._id = None
|
|
22
|
+
|
|
23
|
+
# 设备信息
|
|
24
|
+
self.device_id = None
|
|
25
|
+
self.device_name = None
|
|
26
|
+
self._device_no = None
|
|
27
|
+
self.software_version = None
|
|
28
|
+
self.hardware_version = None
|
|
29
|
+
self.connect_time = None
|
|
30
|
+
self.current_time = None
|
|
31
|
+
# mV
|
|
32
|
+
self.voltage = None
|
|
33
|
+
# %
|
|
34
|
+
self.battery_remain = None
|
|
35
|
+
# %
|
|
36
|
+
self.battery_total = None
|
|
37
|
+
# persist
|
|
38
|
+
self._recording = False
|
|
39
|
+
self._storage_path = None
|
|
40
|
+
self._file_prefix = None
|
|
41
|
+
|
|
42
|
+
# 可设置参数
|
|
43
|
+
# 采集:采样量程、采样率、采样通道
|
|
44
|
+
# 刺激:刺激电流、刺激频率、刺激时间、刺激通道
|
|
45
|
+
# 采样量程(mV):188、375、563、750、1125、2250、4500
|
|
46
|
+
self._sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188
|
|
47
|
+
# 采样率(Hz):250、500、1000、2000、4000、8000、16000、32000
|
|
48
|
+
self._sample_rate:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 500
|
|
49
|
+
self._physical_max = self._sample_range * 1000 # 物理最大值(uV)
|
|
50
|
+
self._physical_min = -self._sample_range * 1000 # 物理最小值(uV)
|
|
51
|
+
self._digital_max = 8388607
|
|
52
|
+
self._digital_min = -8388608
|
|
53
|
+
self._physical_range = self._physical_max - self._physical_min
|
|
54
|
+
self._digital_range = 16777215
|
|
55
|
+
self._acq_channels = None
|
|
56
|
+
self._acq_param = {
|
|
57
|
+
"sample_range": 188,
|
|
58
|
+
"sample_rate": 500,
|
|
59
|
+
"channels": [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
self._stim_param = {
|
|
63
|
+
"stim_type": 0, # 刺激类型:0-所有通道参数相同, 1: 通道参数不同
|
|
64
|
+
"channels": [],
|
|
65
|
+
"param": [{
|
|
66
|
+
"channel_id": 0, #通道号 从0开始 -- 必填
|
|
67
|
+
"waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
|
|
68
|
+
"current": 1, #电流强度(mA) -- 必填
|
|
69
|
+
"duration": 30, #平稳阶段持续时间(s) -- 必填
|
|
70
|
+
"ramp_up": 5, #上升时间(s) 默认0
|
|
71
|
+
"ramp_down": 5, #下降时间(s) 默认0
|
|
72
|
+
"frequency": 500, #频率(Hz) -- 非直流必填
|
|
73
|
+
"phase_position": 0, #相位 -- 默认0
|
|
74
|
+
"duration_delay": "0", #延迟启动时间(s) -- 默认0
|
|
75
|
+
"pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"channel_id": 1, #通道号 从0开始 -- 必填
|
|
79
|
+
"waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
|
|
80
|
+
"current": 1, #电流强度(mA) -- 必填
|
|
81
|
+
"duration": 30, #平稳阶段持续时间(s) -- 必填
|
|
82
|
+
"ramp_up": 5, #上升时间(s) 默认0
|
|
83
|
+
"ramp_down": 5, #下降时间(s) 默认0
|
|
84
|
+
"frequency": 500, #频率(Hz) -- 非直流必填
|
|
85
|
+
"phase_position": 0, #相位 -- 默认0
|
|
86
|
+
"duration_delay": "0", #延迟启动时间(s) -- 默认0
|
|
87
|
+
"pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
self.stim_paradigm = None
|
|
93
|
+
|
|
94
|
+
signal_info = {
|
|
95
|
+
"param" : None,
|
|
96
|
+
"start_time" : None,
|
|
97
|
+
"finished_time" : None,
|
|
98
|
+
"packet_total" : None,
|
|
99
|
+
"last_packet_time" : None,
|
|
100
|
+
"state" : 0
|
|
101
|
+
}
|
|
102
|
+
stim_info = {
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
Impedance_info = {
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
# 信号采集状态
|
|
109
|
+
# 信号数据包总数(一个信号采集周期内)
|
|
110
|
+
# 信号采集参数
|
|
111
|
+
# 电刺激状态
|
|
112
|
+
# 电刺激开始时间(最近一次)
|
|
113
|
+
# 电刺激结束时间(最近一次)
|
|
114
|
+
# 电刺激参数
|
|
115
|
+
# 启动数据解析线程
|
|
116
|
+
# 数据存储状态
|
|
117
|
+
# 存储目录
|
|
118
|
+
|
|
119
|
+
#
|
|
120
|
+
self.__signal_consumer: Dict[str, Queue[Any]]={}
|
|
121
|
+
self.__impedance_consumer: Dict[str, Queue[Any]]={}
|
|
122
|
+
|
|
123
|
+
# EDF文件处理器
|
|
124
|
+
self._edf_handler = None
|
|
125
|
+
self.storage_enable = True
|
|
126
|
+
|
|
127
|
+
self._parser: IParser = TcpMessageParser(self)
|
|
128
|
+
self._parser.start()
|
|
129
|
+
|
|
130
|
+
# 启动数据接收线程
|
|
131
|
+
self._accept = Thread(target=self.accept)
|
|
132
|
+
self._accept.daemon = True
|
|
133
|
+
self._accept.start()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def device_no(self):
|
|
137
|
+
return self._device_no
|
|
138
|
+
|
|
139
|
+
@device_no.setter
|
|
140
|
+
def device_no(self, value: str):
|
|
141
|
+
self._device_no = value
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_parent(cls, parent:IDevice) -> IDevice:
|
|
145
|
+
rlt = cls(parent.socket)
|
|
146
|
+
rlt.device_id = parent.device_id
|
|
147
|
+
rlt._device_no = parent.device_no
|
|
148
|
+
return rlt
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def init_edf_handler(self):
|
|
152
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
153
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
154
|
+
self._edf_handler.set_device_no(self.device_name)
|
|
155
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
156
|
+
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def edf_handler(self):
|
|
160
|
+
if not self.storage_enable:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if self._edf_handler is None:
|
|
164
|
+
self.init_edf_handler()
|
|
165
|
+
|
|
166
|
+
return self._edf_handler
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def acq_channels(self):
|
|
170
|
+
if self._acq_channels is None:
|
|
171
|
+
self._acq_channels = [i for i in range(1, 63)]
|
|
172
|
+
return self._acq_channels
|
|
173
|
+
@property
|
|
174
|
+
def sample_range(self):
|
|
175
|
+
return self._sample_range if self._sample_range else 188
|
|
176
|
+
@property
|
|
177
|
+
def sample_rate(self):
|
|
178
|
+
return self._sample_rate if self._sample_rate else 500
|
|
179
|
+
@property
|
|
180
|
+
def resolution(self):
|
|
181
|
+
return 24
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def signal_consumers(self):
|
|
185
|
+
return self.__signal_consumer
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def impedance_consumers(self):
|
|
189
|
+
return self.__impedance_consumer
|
|
190
|
+
|
|
191
|
+
# 设置记录文件路径
|
|
192
|
+
def set_storage_path(self, path):
|
|
193
|
+
self._storage_path = path
|
|
194
|
+
|
|
195
|
+
# 设置记录文件名称前缀
|
|
196
|
+
def set_file_prefix(self, prefix):
|
|
197
|
+
self._file_prefix = prefix
|
|
198
|
+
|
|
199
|
+
# 接收socket消息
|
|
200
|
+
def accept(self):
|
|
201
|
+
while True:
|
|
202
|
+
# 缓冲去4M
|
|
203
|
+
data = self.socket.recv(4096*1024)
|
|
204
|
+
if not data:
|
|
205
|
+
logger.warning(f"设备{self.device_name}连接结束")
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
self._parser.append(data)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# socket发送数据
|
|
212
|
+
def send(self, data):
|
|
213
|
+
self.socket.sendall(data)
|
|
214
|
+
|
|
215
|
+
# 设置刺激参数
|
|
216
|
+
def set_stim_param(self, param):
|
|
217
|
+
self.stim_paradigm = param
|
|
218
|
+
|
|
219
|
+
# 设置采集参数
|
|
220
|
+
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
221
|
+
self._acq_param["channels"] = channels
|
|
222
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
223
|
+
self._acq_param["sample_range"] = sample_range
|
|
224
|
+
self._acq_channels = channels
|
|
225
|
+
self._sample_rate = sample_rate
|
|
226
|
+
self._sample_range = sample_range
|
|
227
|
+
|
|
228
|
+
# 通用配置-TODO
|
|
229
|
+
def set_config(self, key:str, val: str):
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
def start_impedance(self):
|
|
233
|
+
logger.info("启动阻抗测量")
|
|
234
|
+
msg = StartImpedanceCommand.build(self).pack()
|
|
235
|
+
logger.debug(f"start_impedance message is {msg.hex()}")
|
|
236
|
+
self.socket.sendall(msg)
|
|
237
|
+
|
|
238
|
+
def stop_impedance(self):
|
|
239
|
+
logger.info("停止阻抗测量")
|
|
240
|
+
msg = StopImpedanceCommand.build(self).pack()
|
|
241
|
+
logger.debug(f"stop_impedance message is {msg.hex()}")
|
|
242
|
+
self.socket.sendall(msg)
|
|
243
|
+
|
|
244
|
+
def start_stimulation(self):
|
|
245
|
+
if self.stim_paradigm is None:
|
|
246
|
+
logger.warning("刺激参数未设置,请先设置刺激参数")
|
|
247
|
+
return
|
|
248
|
+
logger.info("启动电刺激")
|
|
249
|
+
msg = StartStimulationCommand.build(self).pack()
|
|
250
|
+
logger.debug(f"start_stimulation message is {msg.hex()}")
|
|
251
|
+
self.socket.sendall(msg)
|
|
252
|
+
t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
|
|
253
|
+
t.start()
|
|
254
|
+
|
|
255
|
+
def _stop_stimulation_trigger(self, duration):
|
|
256
|
+
delay = duration
|
|
257
|
+
while delay > 0:
|
|
258
|
+
sleep(1)
|
|
259
|
+
delay -= 1
|
|
260
|
+
logger.info(f"_stop_stimulation_trigger duration: {duration}")
|
|
261
|
+
if self._edf_handler:
|
|
262
|
+
self._edf_handler.trigger("stimulation should be stopped")
|
|
263
|
+
else:
|
|
264
|
+
logger.warning("stop stim trigger fail. no edf writer alive")
|
|
265
|
+
|
|
266
|
+
def stop_stimulation(self):
|
|
267
|
+
logger.info("停止电刺激")
|
|
268
|
+
msg = StopStimulationCommand.pack()
|
|
269
|
+
logger.debug(f"stop_stimulation message is {msg.hex()}")
|
|
270
|
+
self.socket.sendall(msg)
|
|
271
|
+
|
|
272
|
+
# 启动采集
|
|
273
|
+
def start_acquisition(self, recording = True):
|
|
274
|
+
logger.info("启动信号采集")
|
|
275
|
+
self._recording = recording
|
|
276
|
+
# 设置数据采集参数
|
|
277
|
+
param_bytes = SetAcquisitionParamCommand.build(self).pack()
|
|
278
|
+
# 启动数据采集
|
|
279
|
+
start_bytes = StartAcquisitionCommand.build(self).pack()
|
|
280
|
+
msg = param_bytes + start_bytes
|
|
281
|
+
logger.debug(f"start_acquisition message is {msg.hex()}")
|
|
282
|
+
self.socket.sendall(msg)
|
|
283
|
+
|
|
284
|
+
# 停止采集
|
|
285
|
+
def stop_acquisition(self):
|
|
286
|
+
logger.info("停止信号采集")
|
|
287
|
+
msg = StopAcquisitionCommand.build(self).pack()
|
|
288
|
+
logger.debug(f"stop_acquisition message is {msg.hex()}")
|
|
289
|
+
self.socket.sendall(msg)
|
|
290
|
+
if self._edf_handler:
|
|
291
|
+
# 发送结束标识
|
|
292
|
+
self.edf_handler.write(None)
|
|
293
|
+
|
|
294
|
+
# 订阅实时数据
|
|
295
|
+
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
296
|
+
|
|
297
|
+
# 数据队列
|
|
298
|
+
if q is None:
|
|
299
|
+
q = Queue(maxsize=1000)
|
|
300
|
+
|
|
301
|
+
# 队列名称
|
|
302
|
+
if topic is None:
|
|
303
|
+
topic = f"{type}_{time_ns()}"
|
|
304
|
+
|
|
305
|
+
# 订阅生理电信号数据
|
|
306
|
+
if type == "signal":
|
|
307
|
+
# topic唯一,用来区分不同的订阅队列(下同)
|
|
308
|
+
if topic in list(self.__signal_consumer.keys()):
|
|
309
|
+
logger.warning(f"exists {type} subscribe of {topic}")
|
|
310
|
+
else:
|
|
311
|
+
self.__signal_consumer[topic] = q
|
|
312
|
+
|
|
313
|
+
# 订阅阻抗数据
|
|
314
|
+
if type == "impedance":
|
|
315
|
+
if topic in list(self.__signal_consumer.keys()):
|
|
316
|
+
logger.warning(f"exists {type} subscribe of {topic}")
|
|
317
|
+
else:
|
|
318
|
+
self.__impedance_consumer[topic] = q
|
|
319
|
+
|
|
320
|
+
return topic, q
|
|
321
|
+
|
|
322
|
+
def trigger(self, desc):
|
|
323
|
+
if self._edf_handler:
|
|
324
|
+
self.edf_handler.trigger(desc)
|
|
325
|
+
else:
|
|
326
|
+
logger.warning("no edf handler, no place to recording trigger")
|
|
327
|
+
|
|
328
|
+
def __str__(self):
|
|
329
|
+
return f'''
|
|
330
|
+
Device:
|
|
331
|
+
Name: {self.device_name},
|
|
332
|
+
Type: {hex(self.device_type) if self.device_type else None},
|
|
333
|
+
ID: {self.device_id if self.device_id else None},
|
|
334
|
+
Software: {self.software_version},
|
|
335
|
+
Hardware: {self.hardware_version},
|
|
336
|
+
Connect Time: {self.connect_time},
|
|
337
|
+
Current Time: {self.current_time},
|
|
338
|
+
Voltage: {str(self.voltage) + "mV" if self.voltage else None},
|
|
339
|
+
Battery Remain: {str(self.battery_remain)+ "%" if self.battery_remain else None},
|
|
340
|
+
Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
|
|
341
|
+
'''
|
|
342
|
+
|
|
343
|
+
def __repr__(self):
|
|
344
|
+
return f'''
|
|
345
|
+
Device:
|
|
346
|
+
Name: {self.device_name},
|
|
347
|
+
Type: {hex(self.device_type)},
|
|
348
|
+
ID: {self.device_id},
|
|
349
|
+
Software: {self.software_version},
|
|
350
|
+
Hardware: {self.hardware_version},
|
|
351
|
+
Connect Time: {self.connect_time},
|
|
352
|
+
Current Time: {self.current_time},
|
|
353
|
+
Voltage: {self.voltage}mV,
|
|
354
|
+
Battery Remain: {self.battery_remain}%,
|
|
355
|
+
Battery Total: {self.battery_total}%
|
|
356
|
+
'''
|
|
357
|
+
|
|
358
|
+
def __eq__(self, other):
|
|
359
|
+
return self.device_name == other.device_name and self.device_type == other.device_type and self.device_id == other.device_id
|
|
360
|
+
|
|
361
|
+
def __hash__(self):
|
|
362
|
+
return hash((self.device_name, self.device_type, self.device_id))
|
|
363
|
+
|
|
364
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Dict, Type
|
|
2
|
+
from qlsdk.rsc.interface import IDevice
|
|
3
|
+
|
|
4
|
+
from qlsdk.rsc.device.c64_rs import C64RS
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
class DeviceFactory(object):
|
|
9
|
+
"""Registry for device implementations"""
|
|
10
|
+
_devices: Dict[str, Type[IDevice]] = {}
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def register(cls, device_type: int, device: Type[IDevice]):
|
|
14
|
+
cls._devices[device_type] = device
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def create_device(cls, device: IDevice) -> Type[IDevice]:
|
|
18
|
+
logger.debug(f"Creating device for device type: {hex(device.device_type)}, support types: {cls._devices.keys()}")
|
|
19
|
+
if device.device_type not in cls._devices.keys():
|
|
20
|
+
logger.warning(f"不支持的设备类型: {hex(device.device_type)}")
|
|
21
|
+
raise ValueError(f"Unsupported device type: {hex(device.device_type)}")
|
|
22
|
+
|
|
23
|
+
instance = cls._devices[device.device_type]
|
|
24
|
+
return instance.from_parent(device) if hasattr(instance, 'from_parent') else instance(device.socket)
|
|
25
|
+
|
|
26
|
+
# Register the C64RS device with the DeviceFactory
|
|
27
|
+
DeviceFactory.register(C64RS.device_type, C64RS)
|
|
28
|
+
|
|
29
|
+
|
qlsdk/rsc/entity.py
CHANGED
|
@@ -2,7 +2,7 @@ from multiprocessing import Queue, Process
|
|
|
2
2
|
from typing import Any, Dict, Literal
|
|
3
3
|
from threading import Thread
|
|
4
4
|
from loguru import logger
|
|
5
|
-
from time import time_ns
|
|
5
|
+
from time import time_ns, sleep
|
|
6
6
|
|
|
7
7
|
from qlsdk.core import *
|
|
8
8
|
from qlsdk.core.device import BaseDevice
|
|
@@ -26,6 +26,10 @@ class QLDevice(BaseDevice):
|
|
|
26
26
|
self.battery_remain = None
|
|
27
27
|
# %
|
|
28
28
|
self.battery_total = None
|
|
29
|
+
# persist
|
|
30
|
+
self._recording = False
|
|
31
|
+
self._storage_path = None
|
|
32
|
+
self._file_prefix = None
|
|
29
33
|
|
|
30
34
|
# 可设置参数
|
|
31
35
|
# 采集:采样量程、采样率、采样通道
|
|
@@ -34,6 +38,12 @@ class QLDevice(BaseDevice):
|
|
|
34
38
|
self._sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188
|
|
35
39
|
# 采样率(Hz):250、500、1000、2000、4000、8000、16000、32000
|
|
36
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
|
|
37
47
|
self._acq_channels = None
|
|
38
48
|
self._acq_param = {
|
|
39
49
|
"sample_range": 188,
|
|
@@ -115,8 +125,17 @@ class QLDevice(BaseDevice):
|
|
|
115
125
|
self._accept.start()
|
|
116
126
|
|
|
117
127
|
def init_edf_handler(self):
|
|
118
|
-
self._edf_handler = RscEDFHandler(self.sample_rate, self.
|
|
119
|
-
|
|
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
|
|
138
|
+
|
|
120
139
|
@property
|
|
121
140
|
def edf_handler(self):
|
|
122
141
|
if not self.storage_enable:
|
|
@@ -127,17 +146,22 @@ class QLDevice(BaseDevice):
|
|
|
127
146
|
|
|
128
147
|
return self._edf_handler
|
|
129
148
|
|
|
149
|
+
# 采集通道列表, 从1开始
|
|
130
150
|
@property
|
|
131
151
|
def acq_channels(self):
|
|
132
152
|
if self._acq_channels is None:
|
|
133
153
|
self._acq_channels = [i for i in range(1, 63)]
|
|
134
154
|
return self._acq_channels
|
|
155
|
+
|
|
156
|
+
# 量程范围
|
|
135
157
|
@property
|
|
136
158
|
def sample_range(self):
|
|
159
|
+
|
|
137
160
|
return self._sample_range if self._sample_range else 188
|
|
138
161
|
@property
|
|
139
162
|
def sample_rate(self):
|
|
140
163
|
return self._sample_rate if self._sample_rate else 500
|
|
164
|
+
|
|
141
165
|
@property
|
|
142
166
|
def resolution(self):
|
|
143
167
|
return 24
|
|
@@ -150,13 +174,17 @@ class QLDevice(BaseDevice):
|
|
|
150
174
|
def impedance_consumers(self):
|
|
151
175
|
return self.__impedance_consumer
|
|
152
176
|
|
|
177
|
+
# 设置记录文件路径
|
|
153
178
|
def set_storage_path(self, path):
|
|
154
|
-
|
|
179
|
+
self._storage_path = path
|
|
180
|
+
|
|
181
|
+
# 设置记录文件名称前缀
|
|
182
|
+
def set_file_prefix(self, prefix):
|
|
183
|
+
self._file_prefix = prefix
|
|
155
184
|
|
|
156
185
|
def accept(self):
|
|
157
186
|
while True:
|
|
158
187
|
data = self.socket.recv(4096*1024)
|
|
159
|
-
# logger.debug(f"QLDevice接收到数据: {data.hex()}")
|
|
160
188
|
if not data:
|
|
161
189
|
logger.warning(f"设备{self.device_name}连接结束")
|
|
162
190
|
break
|
|
@@ -175,35 +203,31 @@ class QLDevice(BaseDevice):
|
|
|
175
203
|
self.stim_paradigm = param
|
|
176
204
|
|
|
177
205
|
# 设置采集参数
|
|
178
|
-
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):
|
|
179
207
|
self._acq_param["channels"] = channels
|
|
180
208
|
self._acq_param["sample_rate"] = sample_rate
|
|
181
209
|
self._acq_param["sample_range"] = sample_range
|
|
182
210
|
self._acq_channels = channels
|
|
183
211
|
self._sample_rate = sample_rate
|
|
184
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
|
|
185
217
|
|
|
186
|
-
#
|
|
218
|
+
# 通用配置-TODO
|
|
187
219
|
def set_config(self, key:str, val: str):
|
|
188
220
|
pass
|
|
189
221
|
|
|
190
|
-
def set_impedance_param(self):
|
|
191
|
-
channels = bytes.fromhex("FFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000")
|
|
192
|
-
sample_rate = 1000
|
|
193
|
-
sample_len = 300
|
|
194
|
-
resolution = self.resolution
|
|
195
|
-
|
|
196
222
|
def start_impedance(self):
|
|
197
223
|
logger.info("启动阻抗测量")
|
|
198
224
|
msg = StartImpedanceCommand.build(self).pack()
|
|
199
|
-
# msg = bytes.fromhex("5aa50239320013243f0000001104ffffffffffffffff000000000000000000000000000000000000000000000000e8030000fa00000010000164000000a11e5aa50239320013243a00000012040000000000000000000000000000000000000000000000000000000000000000000001000000000000000325")
|
|
200
225
|
logger.debug(f"start_impedance message is {msg.hex()}")
|
|
201
226
|
self.socket.sendall(msg)
|
|
202
227
|
|
|
203
228
|
def stop_impedance(self):
|
|
204
229
|
logger.info("停止阻抗测量")
|
|
205
230
|
msg = StopImpedanceCommand.build(self).pack()
|
|
206
|
-
# msg = bytes.fromhex("5aa5023932001324100000001304e9df")
|
|
207
231
|
logger.debug(f"stop_impedance message is {msg.hex()}")
|
|
208
232
|
self.socket.sendall(msg)
|
|
209
233
|
|
|
@@ -212,20 +236,33 @@ class QLDevice(BaseDevice):
|
|
|
212
236
|
logger.warning("刺激参数未设置,请先设置刺激参数")
|
|
213
237
|
return
|
|
214
238
|
logger.info("启动电刺激")
|
|
215
|
-
# conf = SetStimulationParamCommand.build(self).pack()
|
|
216
239
|
msg = StartStimulationCommand.build(self).pack()
|
|
217
240
|
logger.debug(f"start_stimulation message is {msg.hex()}")
|
|
218
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")
|
|
219
255
|
|
|
220
256
|
def stop_stimulation(self):
|
|
221
257
|
logger.info("停止电刺激")
|
|
222
|
-
msg = StopStimulationCommand.pack()
|
|
258
|
+
msg = StopStimulationCommand.build().pack()
|
|
223
259
|
logger.debug(f"stop_stimulation message is {msg.hex()}")
|
|
224
260
|
self.socket.sendall(msg)
|
|
225
261
|
|
|
226
262
|
# 启动采集
|
|
227
|
-
def start_acquisition(self):
|
|
263
|
+
def start_acquisition(self, recording = True):
|
|
228
264
|
logger.info("启动信号采集")
|
|
265
|
+
self._recording = recording
|
|
229
266
|
# 设置数据采集参数
|
|
230
267
|
param_bytes = SetAcquisitionParamCommand.build(self).pack()
|
|
231
268
|
# 启动数据采集
|
|
@@ -238,11 +275,11 @@ class QLDevice(BaseDevice):
|
|
|
238
275
|
def stop_acquisition(self):
|
|
239
276
|
logger.info("停止信号采集")
|
|
240
277
|
msg = StopAcquisitionCommand.build(self).pack()
|
|
241
|
-
logger.debug(f"stop_acquisition message is {msg}")
|
|
278
|
+
logger.debug(f"stop_acquisition message is {msg.hex()}")
|
|
242
279
|
self.socket.sendall(msg)
|
|
243
280
|
if self._edf_handler:
|
|
244
281
|
# 发送结束标识
|
|
245
|
-
self.edf_handler.
|
|
282
|
+
self.edf_handler.write(None)
|
|
246
283
|
|
|
247
284
|
# 订阅实时数据
|
|
248
285
|
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
@@ -271,6 +308,12 @@ class QLDevice(BaseDevice):
|
|
|
271
308
|
self.__impedance_consumer[topic] = q
|
|
272
309
|
|
|
273
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")
|
|
274
317
|
|
|
275
318
|
def __str__(self):
|
|
276
319
|
return f'''
|
|
@@ -326,10 +369,6 @@ class RSC64R(QLDevice):
|
|
|
326
369
|
class ARSKindling(QLDevice):
|
|
327
370
|
def __init__(self, socket):
|
|
328
371
|
super().__init__(socket)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
372
|
|
|
334
373
|
class DeviceParser(object):
|
|
335
374
|
def __init__(self, device : QLDevice):
|