qlsdk2 0.3.0a3__py3-none-any.whl → 0.4.0a1__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/__init__.py +1 -0
- qlsdk/core/__init__.py +4 -0
- qlsdk/core/crc/__init__.py +5 -0
- qlsdk/core/crc/crctools.py +95 -0
- qlsdk/core/device.py +25 -0
- qlsdk/core/entity/__init__.py +92 -0
- qlsdk/core/exception.py +0 -0
- qlsdk/core/filter/__init__.py +1 -0
- qlsdk/core/filter/norch.py +59 -0
- qlsdk/core/local.py +34 -0
- qlsdk/core/message/__init__.py +2 -0
- qlsdk/core/message/command.py +293 -0
- qlsdk/core/message/tcp.py +0 -0
- qlsdk/core/message/udp.py +96 -0
- qlsdk/core/utils.py +68 -0
- qlsdk/persist/__init__.py +2 -1
- qlsdk/persist/rsc_edf.py +236 -0
- qlsdk/rsc/__init__.py +7 -0
- qlsdk/rsc/command/__init__.py +214 -0
- qlsdk/rsc/command/message.py +239 -0
- qlsdk/rsc/device_manager.py +96 -0
- qlsdk/rsc/discover.py +86 -0
- qlsdk/rsc/eegion.py +360 -0
- qlsdk/rsc/entity.py +552 -0
- qlsdk/rsc/paradigm.py +310 -0
- qlsdk/rsc/proxy.py +76 -0
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0a1.dist-info}/METADATA +1 -1
- qlsdk2-0.4.0a1.dist-info/RECORD +40 -0
- qlsdk2-0.3.0a3.dist-info/RECORD +0 -16
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0a1.dist-info}/WHEEL +0 -0
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0a1.dist-info}/top_level.txt +0 -0
qlsdk/rsc/entity.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
from multiprocessing import Queue, Process
|
|
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
|
+
from threading import Thread
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from time import time_ns
|
|
9
|
+
|
|
10
|
+
class QLDevice(BaseDevice):
|
|
11
|
+
def __init__(self, socket):
|
|
12
|
+
self.socket = socket
|
|
13
|
+
|
|
14
|
+
# 设备信息
|
|
15
|
+
self.device_id = None
|
|
16
|
+
self.device_name = None
|
|
17
|
+
self.device_type = None
|
|
18
|
+
self.software_version = None
|
|
19
|
+
self.hardware_version = None
|
|
20
|
+
self.connect_time = None
|
|
21
|
+
self.current_time = None
|
|
22
|
+
# mV
|
|
23
|
+
self.voltage = None
|
|
24
|
+
# %
|
|
25
|
+
self.battery_remain = None
|
|
26
|
+
# %
|
|
27
|
+
self.battery_total = None
|
|
28
|
+
|
|
29
|
+
# 可设置参数
|
|
30
|
+
# 采集:采样量程、采样率、采样通道
|
|
31
|
+
# 刺激:刺激电流、刺激频率、刺激时间、刺激通道
|
|
32
|
+
# 采样量程(mV):188、375、563、750、1125、2250、4500
|
|
33
|
+
self._sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188
|
|
34
|
+
# 采样率(Hz):250、500、1000、2000、4000、8000、16000、32000
|
|
35
|
+
self._sample_rate:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 500
|
|
36
|
+
self._acq_channels = None
|
|
37
|
+
self._acq_param = {
|
|
38
|
+
"sample_range": 188,
|
|
39
|
+
"sample_rate": 500,
|
|
40
|
+
"channels": [],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
self._stim_param = {
|
|
44
|
+
"stim_type": 0, # 刺激类型:0-所有通道参数相同, 1: 通道参数不同
|
|
45
|
+
"channels": [],
|
|
46
|
+
"param": [{
|
|
47
|
+
"channel_id": 0, #通道号 从0开始 -- 必填
|
|
48
|
+
"waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
|
|
49
|
+
"current": 1, #电流强度(mA) -- 必填
|
|
50
|
+
"duration": 30, #平稳阶段持续时间(s) -- 必填
|
|
51
|
+
"ramp_up": 5, #上升时间(s) 默认0
|
|
52
|
+
"ramp_down": 5, #下降时间(s) 默认0
|
|
53
|
+
"frequency": 500, #频率(Hz) -- 非直流必填
|
|
54
|
+
"phase_position": 0, #相位 -- 默认0
|
|
55
|
+
"duration_delay": "0", #延迟启动时间(s) -- 默认0
|
|
56
|
+
"pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"channel_id": 1, #通道号 从0开始 -- 必填
|
|
60
|
+
"waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
|
|
61
|
+
"current": 1, #电流强度(mA) -- 必填
|
|
62
|
+
"duration": 30, #平稳阶段持续时间(s) -- 必填
|
|
63
|
+
"ramp_up": 5, #上升时间(s) 默认0
|
|
64
|
+
"ramp_down": 5, #下降时间(s) 默认0
|
|
65
|
+
"frequency": 500, #频率(Hz) -- 非直流必填
|
|
66
|
+
"phase_position": 0, #相位 -- 默认0
|
|
67
|
+
"duration_delay": "0", #延迟启动时间(s) -- 默认0
|
|
68
|
+
"pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
self.stim_paradigm = None
|
|
74
|
+
|
|
75
|
+
signal_info = {
|
|
76
|
+
"param" : None,
|
|
77
|
+
"start_time" : None,
|
|
78
|
+
"finished_time" : None,
|
|
79
|
+
"packet_total" : None,
|
|
80
|
+
"last_packet_time" : None,
|
|
81
|
+
"state" : 0
|
|
82
|
+
}
|
|
83
|
+
stim_info = {
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
Impedance_info = {
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
# 信号采集状态
|
|
90
|
+
# 信号数据包总数(一个信号采集周期内)
|
|
91
|
+
# 信号采集参数
|
|
92
|
+
# 电刺激状态
|
|
93
|
+
# 电刺激开始时间(最近一次)
|
|
94
|
+
# 电刺激结束时间(最近一次)
|
|
95
|
+
# 电刺激参数
|
|
96
|
+
# 启动数据解析线程
|
|
97
|
+
# 数据存储状态
|
|
98
|
+
# 存储目录
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
self.__signal_consumer: Dict[str, Queue[Any]]={}
|
|
102
|
+
self.__impedance_consumer: Dict[str, Queue[Any]]={}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
self._parser = DeviceParser(self)
|
|
106
|
+
self._parser.start()
|
|
107
|
+
|
|
108
|
+
# 启动数据接收线程
|
|
109
|
+
self._accept = Thread(target=self.accept)
|
|
110
|
+
self._accept.daemon = True
|
|
111
|
+
self._accept.start()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def acq_channels(self):
|
|
115
|
+
if self._acq_channels is None:
|
|
116
|
+
self._acq_channels = [i for i in range(1, 63)]
|
|
117
|
+
return self._acq_channels
|
|
118
|
+
@property
|
|
119
|
+
def sample_range(self):
|
|
120
|
+
return self._sample_range if self._sample_range else 188
|
|
121
|
+
@property
|
|
122
|
+
def sample_rate(self):
|
|
123
|
+
return self._sample_rate if self._sample_rate else 500
|
|
124
|
+
@property
|
|
125
|
+
def resolution(self):
|
|
126
|
+
return 24
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def signal_consumers(self):
|
|
130
|
+
return self.__signal_consumer
|
|
131
|
+
return self.__signal_consumer
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def impedance_consumers(self):
|
|
135
|
+
return self.__impedance_consumer
|
|
136
|
+
|
|
137
|
+
def accept(self):
|
|
138
|
+
while True:
|
|
139
|
+
data = self.socket.recv(4096*1024)
|
|
140
|
+
# logger.debug(f"QLDevice接收到数据: {data.hex()}")
|
|
141
|
+
if not data:
|
|
142
|
+
logger.warning(f"设备{self.device_name}连接结束")
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
self._parser.append(data)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def send(self, data):
|
|
149
|
+
self.socket.sendall(data)
|
|
150
|
+
|
|
151
|
+
def add_param(self, key:str, val:str):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# 设置刺激参数
|
|
155
|
+
def set_stim_param(self, param):
|
|
156
|
+
self.stim_paradigm = param
|
|
157
|
+
|
|
158
|
+
# 设置采集参数
|
|
159
|
+
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
160
|
+
self._acq_param["channels"] = channels
|
|
161
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
162
|
+
self._acq_param["sample_range"] = sample_range
|
|
163
|
+
self._acq_channels = channels
|
|
164
|
+
self._sample_rate = sample_rate
|
|
165
|
+
self._sample_range = sample_range
|
|
166
|
+
|
|
167
|
+
# 通用配置
|
|
168
|
+
def set_config(self, key:str, val: str):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def set_impedance_param(self):
|
|
172
|
+
channels = bytes.fromhex("FFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000")
|
|
173
|
+
sample_rate = 1000
|
|
174
|
+
sample_len = 300
|
|
175
|
+
resolution = self.resolution
|
|
176
|
+
|
|
177
|
+
def start_impedance(self):
|
|
178
|
+
logger.info("启动阻抗测量")
|
|
179
|
+
msg = StartImpedanceCommand.build(self).pack()
|
|
180
|
+
# msg = bytes.fromhex("5aa50239320013243f0000001104ffffffffffffffff000000000000000000000000000000000000000000000000e8030000fa00000010000164000000a11e5aa50239320013243a00000012040000000000000000000000000000000000000000000000000000000000000000000001000000000000000325")
|
|
181
|
+
logger.debug(f"start_impedance message is {msg.hex()}")
|
|
182
|
+
self.socket.sendall(msg)
|
|
183
|
+
|
|
184
|
+
def stop_impedance(self):
|
|
185
|
+
logger.info("停止阻抗测量")
|
|
186
|
+
msg = StopImpedanceCommand.build(self).pack()
|
|
187
|
+
# msg = bytes.fromhex("5aa5023932001324100000001304e9df")
|
|
188
|
+
logger.debug(f"stop_impedance message is {msg.hex()}")
|
|
189
|
+
self.socket.sendall(msg)
|
|
190
|
+
|
|
191
|
+
def start_stimulation(self):
|
|
192
|
+
if self.stim_paradigm is None:
|
|
193
|
+
logger.warning("刺激参数未设置,请先设置刺激参数")
|
|
194
|
+
return
|
|
195
|
+
logger.info("启动电刺激")
|
|
196
|
+
# conf = SetStimulationParamCommand.build(self).pack()
|
|
197
|
+
msg = StartStimulationCommand.build(self).pack()
|
|
198
|
+
logger.debug(f"start_stimulation message is {msg.hex()}")
|
|
199
|
+
self.socket.sendall(msg)
|
|
200
|
+
|
|
201
|
+
def stop_stimulation(self):
|
|
202
|
+
logger.info("停止电刺激")
|
|
203
|
+
msg = StopStimulationCommand.pack()
|
|
204
|
+
logger.debug(f"stop_stimulation message is {msg.hex()}")
|
|
205
|
+
self.socket.sendall(msg)
|
|
206
|
+
|
|
207
|
+
# 启动采集
|
|
208
|
+
def start_acquisition(self):
|
|
209
|
+
logger.info("启动信号采集")
|
|
210
|
+
# 设置数据采集参数
|
|
211
|
+
param_bytes = SetAcquisitionParamCommand.build(self).pack()
|
|
212
|
+
# 启动数据采集
|
|
213
|
+
start_bytes = StartAcquisitionCommand.build(self).pack()
|
|
214
|
+
msg = param_bytes + start_bytes
|
|
215
|
+
logger.debug(f"start_acquisition message is {msg.hex()}")
|
|
216
|
+
self.socket.sendall(msg)
|
|
217
|
+
|
|
218
|
+
# 停止采集
|
|
219
|
+
def stop_acquisition(self):
|
|
220
|
+
logger.info("停止信号采集")
|
|
221
|
+
msg = StopAcquisitionCommand.build(self).pack()
|
|
222
|
+
logger.debug(f"stop_acquisition message is {msg}")
|
|
223
|
+
self.socket.sendall(msg)
|
|
224
|
+
|
|
225
|
+
# 订阅实时数据
|
|
226
|
+
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
227
|
+
|
|
228
|
+
# 数据队列
|
|
229
|
+
if q is None:
|
|
230
|
+
q = Queue()
|
|
231
|
+
|
|
232
|
+
# 队列名称
|
|
233
|
+
if topic is None:
|
|
234
|
+
topic = f"{type}_{time_ns()}"
|
|
235
|
+
|
|
236
|
+
# 订阅生理电信号数据
|
|
237
|
+
if type == "signal":
|
|
238
|
+
# topic唯一,用来区分不同的订阅队列(下同)
|
|
239
|
+
if topic in list(self.__signal_consumer.keys()):
|
|
240
|
+
logger.warning(f"exists {type} subscribe of {topic}")
|
|
241
|
+
else:
|
|
242
|
+
self.__signal_consumer[topic] = q
|
|
243
|
+
|
|
244
|
+
# 订阅阻抗数据
|
|
245
|
+
if type == "impedance":
|
|
246
|
+
if topic in list(self.__signal_consumer.keys()):
|
|
247
|
+
logger.warning(f"exists {type} subscribe of {topic}")
|
|
248
|
+
else:
|
|
249
|
+
self.__impedance_consumer[topic] = q
|
|
250
|
+
|
|
251
|
+
return topic, q
|
|
252
|
+
|
|
253
|
+
def __str__(self):
|
|
254
|
+
return f'''
|
|
255
|
+
Device:
|
|
256
|
+
Name: {self.device_name},
|
|
257
|
+
Type: {hex(self.device_type) if self.device_type else None},
|
|
258
|
+
ID: {hex(self.device_id) if self.device_id else None},
|
|
259
|
+
Software: {self.software_version},
|
|
260
|
+
Hardware: {self.hardware_version},
|
|
261
|
+
Connect Time: {self.connect_time},
|
|
262
|
+
Current Time: {self.current_time},
|
|
263
|
+
Voltage: {str(self.voltage) + "mV" if self.voltage else None},
|
|
264
|
+
Battery Remain: {str(self.battery_remain)+ "%" if self.battery_remain else None},
|
|
265
|
+
Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
|
|
266
|
+
'''
|
|
267
|
+
|
|
268
|
+
def __repr__(self):
|
|
269
|
+
return f'''
|
|
270
|
+
Device:
|
|
271
|
+
Name: {self.device_name},
|
|
272
|
+
Type: {hex(self.device_type)},
|
|
273
|
+
ID: {hex(self.device_id)},
|
|
274
|
+
Software: {self.software_version},
|
|
275
|
+
Hardware: {self.hardware_version},
|
|
276
|
+
Connect Time: {self.connect_time},
|
|
277
|
+
Current Time: {self.current_time},
|
|
278
|
+
Voltage: {self.voltage}mV,
|
|
279
|
+
Battery Remain: {self.battery_remain}%,
|
|
280
|
+
Battery Total: {self.battery_total}%
|
|
281
|
+
'''
|
|
282
|
+
|
|
283
|
+
def __eq__(self, other):
|
|
284
|
+
return self.device_name == other.device_name and self.device_type == other.device_type and self.device_id == other.device_id
|
|
285
|
+
|
|
286
|
+
def __hash__(self):
|
|
287
|
+
return hash((self.device_name, self.device_type, self.device_id))
|
|
288
|
+
|
|
289
|
+
class RSC64RS(QLDevice):
|
|
290
|
+
def __init__(self, socket):
|
|
291
|
+
super().__init__(socket)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class LJS1(QLDevice):
|
|
295
|
+
def __init__(self, socket):
|
|
296
|
+
super().__init__(socket)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class RSC64R(QLDevice):
|
|
300
|
+
def __init__(self, socket):
|
|
301
|
+
super().__init__(socket)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ARSKindling(QLDevice):
|
|
305
|
+
def __init__(self, socket):
|
|
306
|
+
super().__init__(socket)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class DeviceParser(object):
|
|
313
|
+
def __init__(self, device : QLDevice):
|
|
314
|
+
# 待解析的数据来源于该设备
|
|
315
|
+
self.device = device
|
|
316
|
+
self.running = False
|
|
317
|
+
|
|
318
|
+
self.cache = b''
|
|
319
|
+
|
|
320
|
+
def append(self, buffer):
|
|
321
|
+
self.cache += buffer
|
|
322
|
+
logger.debug(f"append cache len: {len(self.cache)}")
|
|
323
|
+
|
|
324
|
+
# if not self.running:
|
|
325
|
+
# self.start()
|
|
326
|
+
|
|
327
|
+
def __parser__(self):
|
|
328
|
+
logger.info("数据解析开始")
|
|
329
|
+
while self.running:
|
|
330
|
+
# logger.debug(f" cache len: {len(self.cache)}")
|
|
331
|
+
if len(self.cache) < 14:
|
|
332
|
+
continue
|
|
333
|
+
if self.cache[0] != 0x5A or self.cache[1] != 0xA5:
|
|
334
|
+
self.cache = self.cache[1:]
|
|
335
|
+
continue
|
|
336
|
+
pkg_len = int.from_bytes(self.cache[8:12], 'little')
|
|
337
|
+
logger.debug(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
|
|
338
|
+
# 一次取整包数据
|
|
339
|
+
if len(self.cache) < pkg_len:
|
|
340
|
+
continue
|
|
341
|
+
pkg = self.cache[:pkg_len]
|
|
342
|
+
self.cache = self.cache[pkg_len:]
|
|
343
|
+
self.unpack(pkg)
|
|
344
|
+
|
|
345
|
+
def unpack(self, packet):
|
|
346
|
+
TCPMessage.parse(packet, self.device)
|
|
347
|
+
# logger.debug(self.device)
|
|
348
|
+
# logger.debug(f'packet len: {len(packet)}, {packet.hex()}')
|
|
349
|
+
|
|
350
|
+
def start(self):
|
|
351
|
+
self.running = True
|
|
352
|
+
parser = Thread(target=self.__parser__,)
|
|
353
|
+
parser.daemon = True
|
|
354
|
+
parser.start()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class TCPMessage(object):
|
|
359
|
+
# 消息头
|
|
360
|
+
HEADER_PREFIX = b'\x5A\xA5'
|
|
361
|
+
# 消息头总长度 2(prefix) +1(pkgType) +1(deviceType) +4(deviceId) +4(len) +2(cmd)
|
|
362
|
+
HEADER_LEN = 14
|
|
363
|
+
# 消息指令码位置
|
|
364
|
+
CMD_POS = 12
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def parse(data: bytes, device : QLDevice) -> 'DeviceCommand':
|
|
368
|
+
# 数据包校验
|
|
369
|
+
TCPMessage._validate_packet(data)
|
|
370
|
+
# 提取指令码
|
|
371
|
+
cmd_code = int.from_bytes(data[TCPMessage.CMD_POS:TCPMessage.CMD_POS+2], 'little')
|
|
372
|
+
logger.debug(f"收到指令:{hex(cmd_code)}")
|
|
373
|
+
cmd_class = CommandFactory.create_command(cmd_code)
|
|
374
|
+
logger.debug(f"Command class: {cmd_class}")
|
|
375
|
+
instance = cmd_class(device)
|
|
376
|
+
instance.parse_body(data[TCPMessage.HEADER_LEN:-2])
|
|
377
|
+
return instance
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _validate_packet(data: bytes):
|
|
381
|
+
"""Perform full packet validation"""
|
|
382
|
+
if len(data) < TCPMessage.HEADER_LEN + 2: # Header + min body + checksum
|
|
383
|
+
raise ValueError("Packet too short")
|
|
384
|
+
|
|
385
|
+
if data[0:2] != TCPMessage.HEADER_PREFIX:
|
|
386
|
+
raise ValueError("Invalid header prefix")
|
|
387
|
+
|
|
388
|
+
expected_len = int.from_bytes(data[8:12], 'little')
|
|
389
|
+
if len(data) != expected_len:
|
|
390
|
+
raise ValueError(f"Length mismatch: {len(data)} vs {expected_len}")
|
|
391
|
+
|
|
392
|
+
logger.debug(f"checksum: {int.from_bytes(data[-2:], 'little')}")
|
|
393
|
+
checksum = crc16(data[:-2])
|
|
394
|
+
logger.debug(f"checksum recv: {checksum}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class DataPacket(object):
|
|
399
|
+
def __init__(self, device: QLDevice):
|
|
400
|
+
self.device = device
|
|
401
|
+
self.header = None
|
|
402
|
+
self.data = None
|
|
403
|
+
|
|
404
|
+
def parse_body(self, body: bytes):
|
|
405
|
+
raise NotImplementedError("Subclasses should implement this method")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class C64Channel(Enum):
|
|
410
|
+
CH0 = 0
|
|
411
|
+
CH1 = 1
|
|
412
|
+
CH2 = 2
|
|
413
|
+
CH3 = 3
|
|
414
|
+
CH4 = 4
|
|
415
|
+
CH5 = 5
|
|
416
|
+
CH6 = 6
|
|
417
|
+
CH7 = 7
|
|
418
|
+
CH8 = 8
|
|
419
|
+
CH9 = 9
|
|
420
|
+
CH10 = 10
|
|
421
|
+
CH11 = 11
|
|
422
|
+
CH12 = 12
|
|
423
|
+
CH13 = 13
|
|
424
|
+
CH14 = 14
|
|
425
|
+
CH15 = 15
|
|
426
|
+
|
|
427
|
+
class WaveForm(Enum):
|
|
428
|
+
DC = 0
|
|
429
|
+
SQUARE = 1
|
|
430
|
+
AC = 2
|
|
431
|
+
CUSTOM = 3
|
|
432
|
+
PULSE = 4
|
|
433
|
+
|
|
434
|
+
# # 刺激通道
|
|
435
|
+
# class StimulationChannel(object):
|
|
436
|
+
# def __init__(self, channel_id: int, waveform: int, current: float, duration: float, ramp_up: float = None, ramp_down: float = None,
|
|
437
|
+
# frequency: float = None, phase_position: int = None, duration_delay: float = None, pulse_width: int = None, pulse_width_rate: int = 1):
|
|
438
|
+
# self.channel_id = channel_id
|
|
439
|
+
# self.waveform = waveform
|
|
440
|
+
# self.current_max = current
|
|
441
|
+
# self.current_min = current
|
|
442
|
+
# self.duration = duration
|
|
443
|
+
# self.ramp_up = ramp_up
|
|
444
|
+
# self.ramp_down = ramp_down
|
|
445
|
+
# self.frequency = frequency
|
|
446
|
+
# self.phase_position = phase_position
|
|
447
|
+
# self.duration_delay = duration_delay
|
|
448
|
+
# self.pulse_width = pulse_width
|
|
449
|
+
# self.delay_time = 0
|
|
450
|
+
# self.pulse_interval = 0
|
|
451
|
+
# self.with_group_repeats = 1
|
|
452
|
+
# self.pulse_width_rate = 1065353216
|
|
453
|
+
# self.pulse_time_f = 0
|
|
454
|
+
# self.pulse_time_out = 0
|
|
455
|
+
# self.pulse_time_idle = 0
|
|
456
|
+
|
|
457
|
+
# def to_bytes(self):
|
|
458
|
+
# # Convert the object to bytes for transmission
|
|
459
|
+
# result = self.channel_id.to_bytes(1, 'little')
|
|
460
|
+
# wave_form = WaveForm.SQUARE.value if self.waveform == WaveForm.PULSE.value else self.waveform
|
|
461
|
+
# result += wave_form.to_bytes(1, 'little')
|
|
462
|
+
# result += int(self.current_max * 1000 * 1000).to_bytes(4, 'little')
|
|
463
|
+
# # result += int(self.current_min * 1000).to_bytes(2, 'little')
|
|
464
|
+
# result += int(self.frequency).to_bytes(2, 'little')
|
|
465
|
+
# result += int(self.pulse_width).to_bytes(2, 'little')
|
|
466
|
+
# result += int(self.pulse_width_rate).to_bytes(4, 'little')
|
|
467
|
+
|
|
468
|
+
# result += int(self.pulse_interval).to_bytes(2, 'little')
|
|
469
|
+
# result += int(self.with_group_repeats).to_bytes(2, 'little')
|
|
470
|
+
# result += int(self.pulse_time_f).to_bytes(4, 'little')
|
|
471
|
+
# result += int(self.pulse_time_out).to_bytes(4, 'little')
|
|
472
|
+
# result += int(self.pulse_time_idle).to_bytes(4, 'little')
|
|
473
|
+
|
|
474
|
+
# result += int(self.delay_time).to_bytes(4, 'little')
|
|
475
|
+
# result += int(self.ramp_up * 1000).to_bytes(4, 'little')
|
|
476
|
+
# result += int((self.duration + self.ramp_up) * 1000).to_bytes(4, 'little')
|
|
477
|
+
# result += int(self.ramp_down * 1000).to_bytes(4, 'little')
|
|
478
|
+
|
|
479
|
+
# return result
|
|
480
|
+
|
|
481
|
+
# def to_json(self):
|
|
482
|
+
# return {
|
|
483
|
+
# "channel_id": self.channel_id,
|
|
484
|
+
# "waveform": self.waveform,
|
|
485
|
+
# "current_max": self.current_max,
|
|
486
|
+
# "current_min": self.current_min,
|
|
487
|
+
# "duration": self.duration,
|
|
488
|
+
# "ramp_up": self.ramp_up,
|
|
489
|
+
# "ramp_down": self.ramp_down,
|
|
490
|
+
# "frequency": self.frequency,
|
|
491
|
+
# "phase_position": self.phase_position,
|
|
492
|
+
# "duration_delay": self.duration_delay,
|
|
493
|
+
# "pulse_width": self.pulse_width,
|
|
494
|
+
# "delay_time": self.delay_time,
|
|
495
|
+
# "pulse_interval": self.pulse_interval,
|
|
496
|
+
# "with_group_repeats": self.with_group_repeats
|
|
497
|
+
# }
|
|
498
|
+
|
|
499
|
+
# # 刺激范式
|
|
500
|
+
# class StimulationParadigm(object):
|
|
501
|
+
# def __init__(self):
|
|
502
|
+
# self.channels = None
|
|
503
|
+
# self.duration = None
|
|
504
|
+
# self.interval_time = 0
|
|
505
|
+
# self.characteristic = 0
|
|
506
|
+
# self.mode = 0
|
|
507
|
+
# self.repeats = 0
|
|
508
|
+
|
|
509
|
+
# def add_channel(self, channel: StimulationChannel, update=False):
|
|
510
|
+
# if self.channels is None:
|
|
511
|
+
# self.channels = {}
|
|
512
|
+
# channel_id = channel.channel_id + 1
|
|
513
|
+
# if channel_id in self.channels.keys():
|
|
514
|
+
# logger.warning(f"Channel {channel_id} already exists")
|
|
515
|
+
# if update:
|
|
516
|
+
# self.channels[channel_id] = channel
|
|
517
|
+
# else:
|
|
518
|
+
# self.channels[channel_id] = channel
|
|
519
|
+
|
|
520
|
+
# # 计算刺激时间
|
|
521
|
+
# duration = channel.duration + channel.ramp_up + channel.ramp_down
|
|
522
|
+
# if self.duration is None or duration > self.duration:
|
|
523
|
+
# self.duration = duration
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# def to_bytes(self):
|
|
527
|
+
# result = to_bytes(list(self.channels.keys()), 64)
|
|
528
|
+
# result += int(self.duration * 1000).to_bytes(4, 'little')
|
|
529
|
+
# result += int(self.interval_time).to_bytes(4, 'little')
|
|
530
|
+
# result += int(self.characteristic).to_bytes(4, 'little')
|
|
531
|
+
# result += int(self.mode).to_bytes(1, 'little')
|
|
532
|
+
# result += int(self.repeats).to_bytes(4, 'little')
|
|
533
|
+
# for channel in self.channels.values():
|
|
534
|
+
# result += channel.to_bytes()
|
|
535
|
+
# return result
|
|
536
|
+
|
|
537
|
+
# def to_json(self):
|
|
538
|
+
# # Convert the object to JSON for transmission
|
|
539
|
+
# return {
|
|
540
|
+
# "channels": list(self.channels.keys()),
|
|
541
|
+
# "duration": self.duration,
|
|
542
|
+
# "interval_time": self.interval_time,
|
|
543
|
+
# "characteristic": self.characteristic,
|
|
544
|
+
# "mode": self.mode,
|
|
545
|
+
# "repeats": self.repeats,
|
|
546
|
+
# "stim": [channel.to_json() for channel in self.channels.values()]
|
|
547
|
+
# }
|
|
548
|
+
|
|
549
|
+
# @staticmethod
|
|
550
|
+
# def from_json(param: Dict[str, Any]):
|
|
551
|
+
# pass
|
|
552
|
+
|