qlsdk2 0.4.2__py3-none-any.whl → 0.5.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/core/entity/__init__.py +8 -12
- qlsdk/persist/rsc_edf.py +42 -13
- qlsdk/rsc/command/__init__.py +32 -24
- qlsdk/rsc/device/__init__.py +6 -1
- qlsdk/rsc/device/arskindling.py +4 -2
- qlsdk/rsc/device/base.py +92 -39
- qlsdk/rsc/device/c16_rs.py +185 -0
- qlsdk/rsc/device/c256_rs.py +1 -0
- qlsdk/rsc/device/c64_rs.py +3 -2
- qlsdk/rsc/device/c64s1.py +3 -2
- qlsdk/rsc/device/device_factory.py +2 -1
- qlsdk/rsc/interface/device.py +13 -0
- qlsdk/rsc/manager/container.py +27 -23
- qlsdk/rsc/network/discover.py +25 -36
- qlsdk/rsc/parser/base-new.py +135 -0
- qlsdk/rsc/parser/base.py +114 -26
- qlsdk2-0.5.1.dist-info/METADATA +57 -0
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.1.dist-info}/RECORD +20 -18
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.1.dist-info}/WHEEL +1 -1
- qlsdk2-0.4.2.dist-info/METADATA +0 -121
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.1.dist-info}/top_level.txt +0 -0
qlsdk/core/entity/__init__.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
from qlsdk.core.utils import to_channels
|
|
2
2
|
from loguru import logger
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
def __init__(self, device_type, device_id, channels, data):
|
|
6
|
-
self.data = data
|
|
7
|
-
self.channels = None
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class RscPacket(object):
|
|
4
|
+
class Packet(object):
|
|
11
5
|
def __init__(self):
|
|
12
6
|
self.time_stamp = None
|
|
13
7
|
self.pkg_id = None
|
|
14
8
|
self.result = None
|
|
15
9
|
self.channels = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RscPacket(Packet):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
16
15
|
self.origin_sample_rate = None
|
|
17
16
|
self.sample_rate = None
|
|
18
17
|
self.sample_num = None
|
|
@@ -65,12 +64,9 @@ class RscPacket(object):
|
|
|
65
64
|
eeg: {self.eeg}
|
|
66
65
|
"""
|
|
67
66
|
|
|
68
|
-
class ImpedancePacket(
|
|
67
|
+
class ImpedancePacket(Packet):
|
|
69
68
|
def __init__(self):
|
|
70
|
-
|
|
71
|
-
self.pkg_id = None
|
|
72
|
-
self.result = None
|
|
73
|
-
self.channels = None
|
|
69
|
+
super().__init__()
|
|
74
70
|
self.data_len = None
|
|
75
71
|
self.impedance = None
|
|
76
72
|
|
qlsdk/persist/rsc_edf.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
import time
|
|
2
3
|
from multiprocessing import Lock, Queue
|
|
3
4
|
from time import time_ns
|
|
4
5
|
from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
|
|
@@ -14,7 +15,8 @@ EDF_FILE_TYPE = {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
class EDFStreamWriter(Thread):
|
|
17
|
-
def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path):
|
|
18
|
+
def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path, record_duration=None):
|
|
19
|
+
|
|
18
20
|
super().__init__()
|
|
19
21
|
self._writer : EdfWriter = None
|
|
20
22
|
self.data_queue : Queue = Queue()
|
|
@@ -22,6 +24,8 @@ class EDFStreamWriter(Thread):
|
|
|
22
24
|
self._points = 0
|
|
23
25
|
self._duration = 0
|
|
24
26
|
self._buffer = None
|
|
27
|
+
# 设置edf/bdf文件参数,设置[0.001, 1)可以在1秒内记录多个事件(不建议开启)
|
|
28
|
+
self.record_duration = record_duration
|
|
25
29
|
|
|
26
30
|
# signal info
|
|
27
31
|
self._channels = channels
|
|
@@ -61,9 +65,10 @@ class EDFStreamWriter(Thread):
|
|
|
61
65
|
# 数据
|
|
62
66
|
self.data_queue.put(data)
|
|
63
67
|
|
|
64
|
-
def trigger(self, onset, desc):
|
|
68
|
+
def trigger(self, onset, desc: str):
|
|
65
69
|
if self._writer:
|
|
66
|
-
|
|
70
|
+
logger.trace(f"[{onset} : {desc}]")
|
|
71
|
+
self._writer.writeAnnotation(onset, -1, desc)
|
|
67
72
|
else:
|
|
68
73
|
logger.warning("未创建文件,无法写入Trigger标记")
|
|
69
74
|
|
|
@@ -77,8 +82,9 @@ class EDFStreamWriter(Thread):
|
|
|
77
82
|
if self._writer is None:
|
|
78
83
|
self._init_writer()
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
waits = 300
|
|
86
|
+
while waits > 0:
|
|
87
|
+
if not self.data_queue.empty():
|
|
82
88
|
try:
|
|
83
89
|
data = self.data_queue.get(timeout=30)
|
|
84
90
|
if data is None:
|
|
@@ -88,13 +94,20 @@ class EDFStreamWriter(Thread):
|
|
|
88
94
|
self._points += len(data[1])
|
|
89
95
|
logger.trace(f"已处理数据点数:{self._points}")
|
|
90
96
|
self._write_file(data)
|
|
97
|
+
# 有数据重置计数器
|
|
98
|
+
waits = 100 # 重置等待计数器
|
|
91
99
|
except Exception as e:
|
|
92
100
|
logger.error(f"异常或超时(30s)结束: {str(e)}")
|
|
93
101
|
break
|
|
94
102
|
else:
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
time.sleep(0.1)
|
|
104
|
+
# 记录状态等待30s、非记录状态等待3s
|
|
105
|
+
if self._recording:
|
|
106
|
+
waits -= 1
|
|
107
|
+
else:
|
|
108
|
+
waits -= 10
|
|
97
109
|
|
|
110
|
+
logger.info(f"数据记录完成:{self.file_path}")
|
|
98
111
|
self.close()
|
|
99
112
|
|
|
100
113
|
def _init_writer(self):
|
|
@@ -126,6 +139,8 @@ class EDFStreamWriter(Thread):
|
|
|
126
139
|
})
|
|
127
140
|
|
|
128
141
|
self._writer.setSignalHeaders(signal_headers)
|
|
142
|
+
if self.record_duration:
|
|
143
|
+
self._writer.setDatarecordDuration(self.record_duration) # 每个数据块1秒
|
|
129
144
|
|
|
130
145
|
def _write_file(self, eeg_data):
|
|
131
146
|
try:
|
|
@@ -159,6 +174,9 @@ class EDFStreamWriter(Thread):
|
|
|
159
174
|
# 写入时转置为(样本数, 通道数)格式
|
|
160
175
|
self._writer.writeSamples(data_float64)
|
|
161
176
|
self._duration += 1
|
|
177
|
+
|
|
178
|
+
if self._duration % 10 == 0: # 每10秒打印一次进度
|
|
179
|
+
logger.info(f"数据记录中... 文件名:{self.file_path}, 已记录时长: {self._duration}秒")
|
|
162
180
|
|
|
163
181
|
# 用作数据结构一致化处理,通过调用公共类写入edf文件
|
|
164
182
|
# 入参包含写入edf的全部前置参数
|
|
@@ -177,7 +195,7 @@ class RscEDFHandler(object):
|
|
|
177
195
|
@author: qlsdk
|
|
178
196
|
@since: 0.4.0
|
|
179
197
|
'''
|
|
180
|
-
def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
|
|
198
|
+
def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None, record_duration=None):
|
|
181
199
|
# edf文件参数
|
|
182
200
|
self.physical_max = physical_max
|
|
183
201
|
self.physical_min = physical_min
|
|
@@ -213,6 +231,7 @@ class RscEDFHandler(object):
|
|
|
213
231
|
self._total_packets = 0
|
|
214
232
|
self._lost_packets = 0
|
|
215
233
|
self._storage_path = storage_path
|
|
234
|
+
self._record_duration = record_duration
|
|
216
235
|
self._edf_writer_thread = None
|
|
217
236
|
self._file_prefix = None
|
|
218
237
|
|
|
@@ -243,6 +262,8 @@ class RscEDFHandler(object):
|
|
|
243
262
|
self._device_type = "LJ64S1"
|
|
244
263
|
elif device_type == 0x60:
|
|
245
264
|
self._device_type = "ARSKindling"
|
|
265
|
+
elif device_type == 0x339:
|
|
266
|
+
self._device_type = "C16R"
|
|
246
267
|
else:
|
|
247
268
|
self._device_type = device_type
|
|
248
269
|
|
|
@@ -264,17 +285,18 @@ class RscEDFHandler(object):
|
|
|
264
285
|
def write(self, packet: RscPacket):
|
|
265
286
|
# logger.trace(f"packet: {packet}")
|
|
266
287
|
if packet is None:
|
|
288
|
+
logger.info(f"收到结束信号,即将停止写入数据:{self.file_name}")
|
|
267
289
|
self._edf_writer_thread.stop_recording()
|
|
268
290
|
return
|
|
269
291
|
|
|
270
292
|
with self._lock:
|
|
271
293
|
if self.channels is None:
|
|
272
|
-
logger.
|
|
294
|
+
logger.debug(f"开始记录数据到文件...")
|
|
273
295
|
self.channels = packet.channels
|
|
274
296
|
self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
|
|
275
297
|
self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
|
|
276
298
|
self._start_time = datetime.now()
|
|
277
|
-
logger.
|
|
299
|
+
logger.debug(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
|
|
278
300
|
|
|
279
301
|
if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
|
|
280
302
|
self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
|
|
@@ -284,7 +306,7 @@ class RscEDFHandler(object):
|
|
|
284
306
|
self._total_packets += 1
|
|
285
307
|
|
|
286
308
|
if self._edf_writer_thread is None:
|
|
287
|
-
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
|
|
309
|
+
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name, self._record_duration)
|
|
288
310
|
self._edf_writer_thread.set_start_time(self._start_time)
|
|
289
311
|
self._edf_writer_thread.start()
|
|
290
312
|
logger.info(f"开始写入数据: {self.file_name}")
|
|
@@ -295,9 +317,16 @@ class RscEDFHandler(object):
|
|
|
295
317
|
# trigger标记
|
|
296
318
|
# desc: 标记内容
|
|
297
319
|
# cur_time: 设备时间时间戳,非设备发出的trigger不要设置
|
|
298
|
-
def trigger(self, desc, cur_time=None):
|
|
320
|
+
def trigger(self, desc: str, cur_time=None):
|
|
321
|
+
if self._edf_writer_thread is None:
|
|
322
|
+
logger.warning(f"File writing has not started, discarding trigger {desc}")
|
|
323
|
+
return
|
|
324
|
+
|
|
299
325
|
if cur_time is None:
|
|
300
|
-
|
|
326
|
+
# 计算trigger位置
|
|
327
|
+
if self._start_time:
|
|
328
|
+
onset = datetime.now().timestamp() - self._start_time.timestamp()
|
|
329
|
+
else: onset = 0
|
|
301
330
|
else:
|
|
302
331
|
onset = cur_time - self._first_timestamp
|
|
303
332
|
self._edf_writer_thread.trigger(onset, desc)
|
qlsdk/rsc/command/__init__.py
CHANGED
|
@@ -51,15 +51,29 @@ class DeviceCommand(abc.ABC):
|
|
|
51
51
|
device_id = int(self.device.device_id) if self.device and self.device.device_id else 0
|
|
52
52
|
device_type = int(self.device.device_type) if self.device and self.device.device_type else 0
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
#兼容设计
|
|
55
|
+
b_device_type = None
|
|
56
|
+
if device_type > 0xFF:
|
|
57
|
+
b_device_type = int.to_bytes(device_type, 2, 'little')
|
|
58
|
+
|
|
59
|
+
if b_device_type is None:
|
|
60
|
+
return (
|
|
61
|
+
DeviceCommand.HEADER_PREFIX
|
|
62
|
+
+ int(2).to_bytes(1, 'little') # pkgType
|
|
63
|
+
+ device_type.to_bytes(1, 'little')
|
|
64
|
+
+ device_id.to_bytes(4, 'little')
|
|
65
|
+
+ (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
|
|
66
|
+
+ self.cmd_code.to_bytes(2, 'little')
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
return (
|
|
70
|
+
DeviceCommand.HEADER_PREFIX
|
|
71
|
+
+ b_device_type[0].to_bytes(1, 'little') # pkgType
|
|
72
|
+
+ b_device_type[1].to_bytes(1, 'little')
|
|
73
|
+
+ device_id.to_bytes(4, 'little')
|
|
74
|
+
+ (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
|
|
75
|
+
+ self.cmd_code.to_bytes(2, 'little')
|
|
76
|
+
)
|
|
63
77
|
|
|
64
78
|
def unpack(self, payload: bytes) -> bytes:
|
|
65
79
|
"""解析消息体"""
|
|
@@ -70,7 +84,7 @@ class DeviceCommand(abc.ABC):
|
|
|
70
84
|
time = int.from_bytes(body[0:8], 'little')
|
|
71
85
|
# result - 1B
|
|
72
86
|
result = body[8]
|
|
73
|
-
logger.
|
|
87
|
+
logger.debug(f"[{time}]{self.cmd_desc}{'成功' if result == 0 else '失败'}")
|
|
74
88
|
|
|
75
89
|
|
|
76
90
|
|
|
@@ -156,7 +170,7 @@ class QueryBatteryCommand(DeviceCommand):
|
|
|
156
170
|
self.device.battery_total = body[12]
|
|
157
171
|
# state - 1b
|
|
158
172
|
# state = body[13]
|
|
159
|
-
logger.
|
|
173
|
+
logger.trace(f"电量更新: {self.device}")
|
|
160
174
|
else:
|
|
161
175
|
logger.warning(f"QueryBatteryCommand message received but result is failed.")
|
|
162
176
|
|
|
@@ -200,6 +214,10 @@ class StartImpedanceCommand(DeviceCommand):
|
|
|
200
214
|
body += to_bytes(self.device.acq_channels)
|
|
201
215
|
body += bytes.fromhex('0000000000000000') # 8字节占位符
|
|
202
216
|
return body
|
|
217
|
+
|
|
218
|
+
def parse_body(self, body):
|
|
219
|
+
logger.info(f"Received StartImpedance response: {body.hex()}")
|
|
220
|
+
return super().parse_body(body)
|
|
203
221
|
|
|
204
222
|
|
|
205
223
|
# 停止阻抗测量
|
|
@@ -279,7 +297,7 @@ class ImpedanceDataCommand(DeviceCommand):
|
|
|
279
297
|
cmd_desc = "阻抗数据"
|
|
280
298
|
|
|
281
299
|
def parse_body(self, body: bytes):
|
|
282
|
-
logger.info(f"Received impedance data: {body.hex()}")
|
|
300
|
+
# logger.info(f"Received impedance data: {body.hex()}")
|
|
283
301
|
packet = ImpedancePacket().transfer(body)
|
|
284
302
|
|
|
285
303
|
# 信号数据
|
|
@@ -294,18 +312,8 @@ class SignalDataCommand(DeviceCommand):
|
|
|
294
312
|
# 解析数据包
|
|
295
313
|
packet = RscPacket()
|
|
296
314
|
packet.transfer(body)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if self.device.edf_handler:
|
|
300
|
-
self.device.edf_handler.write(packet)
|
|
301
|
-
|
|
302
|
-
if len(self.device.signal_consumers) > 0 :
|
|
303
|
-
# 信号数字值转物理值
|
|
304
|
-
packet.eeg = self.device.eeg2phy(np.array(packet.eeg))
|
|
305
|
-
|
|
306
|
-
# 发送数据包到订阅者
|
|
307
|
-
for q in list(self.device.signal_consumers.values()):
|
|
308
|
-
q.put(packet)
|
|
315
|
+
# 将数据包传递给设备
|
|
316
|
+
self.device.produce(packet)
|
|
309
317
|
|
|
310
318
|
|
|
311
319
|
|
qlsdk/rsc/device/__init__.py
CHANGED
qlsdk/rsc/device/arskindling.py
CHANGED
|
@@ -167,7 +167,7 @@ class ARSKindling(QLBaseDevice):
|
|
|
167
167
|
self._edf_handler.set_device_type(self.device_type)
|
|
168
168
|
self._edf_handler.set_device_no(self.device_no)
|
|
169
169
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
170
|
-
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
170
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'ARS')
|
|
171
171
|
|
|
172
172
|
@property
|
|
173
173
|
def edf_handler(self):
|
|
@@ -233,6 +233,8 @@ class ARSKindling(QLBaseDevice):
|
|
|
233
233
|
# 设置采集参数
|
|
234
234
|
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
235
235
|
self._acq_param["original_channels"] = channels
|
|
236
|
+
|
|
237
|
+
# 根据映射关系做通道转换
|
|
236
238
|
for k in channels.keys():
|
|
237
239
|
if isinstance(channels[k], list):
|
|
238
240
|
temp = [k + str(i) for i in channels[k]]
|
|
@@ -241,7 +243,7 @@ class ARSKindling(QLBaseDevice):
|
|
|
241
243
|
channels[k] = [k + str(channels[k])]
|
|
242
244
|
|
|
243
245
|
|
|
244
|
-
|
|
246
|
+
# 更新采集参数
|
|
245
247
|
self._acq_param["channels"] = channels
|
|
246
248
|
self._acq_param["sample_rate"] = sample_rate
|
|
247
249
|
self._acq_param["sample_range"] = sample_range
|
qlsdk/rsc/device/base.py
CHANGED
|
@@ -5,12 +5,13 @@ 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
|
|
8
10
|
from qlsdk.core.utils import to_bytes
|
|
9
11
|
from qlsdk.persist import RscEDFHandler
|
|
10
12
|
from qlsdk.rsc.interface import IDevice, IParser
|
|
11
13
|
from qlsdk.rsc.parser import TcpMessageParser
|
|
12
14
|
from qlsdk.rsc.command import StartImpedanceCommand, StopImpedanceCommand, StartStimulationCommand, StopStimulationCommand, SetAcquisitionParamCommand, StartAcquisitionCommand, StopAcquisitionCommand
|
|
13
|
-
# from qlsdk.rsc.command import *
|
|
14
15
|
|
|
15
16
|
class QLBaseDevice(IDevice):
|
|
16
17
|
def __init__(self, socket):
|
|
@@ -19,6 +20,9 @@ class QLBaseDevice(IDevice):
|
|
|
19
20
|
# 启动数据解析线程
|
|
20
21
|
self._parser: IParser = None
|
|
21
22
|
|
|
23
|
+
# 用在edf/bdf文件写入中,不建议使用
|
|
24
|
+
self._record_duration = None
|
|
25
|
+
|
|
22
26
|
# 设备信息
|
|
23
27
|
self.device_id = None
|
|
24
28
|
self.device_name = None
|
|
@@ -117,8 +121,8 @@ class QLBaseDevice(IDevice):
|
|
|
117
121
|
# 存储目录
|
|
118
122
|
|
|
119
123
|
#
|
|
120
|
-
self.
|
|
121
|
-
self.
|
|
124
|
+
self._signal_consumer: Dict[str, Queue[Any]]={}
|
|
125
|
+
self._impedance_consumer: Dict[str, Queue[Any]]={}
|
|
122
126
|
|
|
123
127
|
# EDF文件处理器
|
|
124
128
|
self._edf_handler = None
|
|
@@ -128,6 +132,45 @@ class QLBaseDevice(IDevice):
|
|
|
128
132
|
|
|
129
133
|
def parser(self) -> IParser:
|
|
130
134
|
return self._parser
|
|
135
|
+
|
|
136
|
+
def set_record_duration(self, record_duration: float):
|
|
137
|
+
self._record_duration = record_duration
|
|
138
|
+
|
|
139
|
+
# 数据包处理
|
|
140
|
+
def produce(self, data: RscPacket, type:Literal['signal', 'impedance']="signal"):
|
|
141
|
+
if data is None: return
|
|
142
|
+
|
|
143
|
+
# 处理信号数据
|
|
144
|
+
self._signal_wrapper(data)
|
|
145
|
+
|
|
146
|
+
# 存储
|
|
147
|
+
if self.storage_enable:
|
|
148
|
+
self._write_signal(data)
|
|
149
|
+
|
|
150
|
+
if len(self.signal_consumers) > 0 :
|
|
151
|
+
# 信号数字值转物理值
|
|
152
|
+
data.eeg = self.eeg2phy(np.array(data.eeg))
|
|
153
|
+
|
|
154
|
+
# 发送数据包到订阅者
|
|
155
|
+
for q in list(self.signal_consumers.values()):
|
|
156
|
+
# 队列满了就丢弃最早的数据
|
|
157
|
+
if q.full():
|
|
158
|
+
q.get()
|
|
159
|
+
|
|
160
|
+
q.put(data)
|
|
161
|
+
|
|
162
|
+
# 信号数据转换(默认不处理)
|
|
163
|
+
def _signal_wrapper(self, data: RscPacket):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def _write_signal(self, data: RscPacket):
|
|
167
|
+
# 文件写入到edf
|
|
168
|
+
if self._edf_handler is None:
|
|
169
|
+
logger.debug("Initializing EDF handler for data storage")
|
|
170
|
+
self.init_edf_handler()
|
|
171
|
+
|
|
172
|
+
if self._edf_handler:
|
|
173
|
+
self._edf_handler.write(data)
|
|
131
174
|
|
|
132
175
|
def start_listening(self):
|
|
133
176
|
|
|
@@ -142,8 +185,9 @@ class QLBaseDevice(IDevice):
|
|
|
142
185
|
return True
|
|
143
186
|
|
|
144
187
|
def stop_listening(self):
|
|
145
|
-
logger.
|
|
146
|
-
self._listening = False
|
|
188
|
+
logger.trace(f"设备{self.device_no}停止socket监听")
|
|
189
|
+
self._listening = False
|
|
190
|
+
self._parser.stop()
|
|
147
191
|
|
|
148
192
|
@property
|
|
149
193
|
def device_type(self) -> int:
|
|
@@ -152,15 +196,15 @@ class QLBaseDevice(IDevice):
|
|
|
152
196
|
def start_message_parser(self) -> None:
|
|
153
197
|
self._parser = TcpMessageParser(self)
|
|
154
198
|
self._parser.start()
|
|
155
|
-
logger.
|
|
199
|
+
logger.debug("TCP消息解析器已启动")
|
|
156
200
|
|
|
157
201
|
def start_message_listening(self) -> None:
|
|
158
202
|
def _accept():
|
|
159
203
|
while self._listening:
|
|
160
|
-
#
|
|
204
|
+
# 缓冲区4M
|
|
161
205
|
data = self.socket.recv(4096*1024)
|
|
162
206
|
if not data:
|
|
163
|
-
logger.warning(f"设备{self.device_name}连接结束")
|
|
207
|
+
logger.warning(f"设备[{self.device_name}]连接结束")
|
|
164
208
|
break
|
|
165
209
|
|
|
166
210
|
self._parser.append(data)
|
|
@@ -169,7 +213,7 @@ class QLBaseDevice(IDevice):
|
|
|
169
213
|
self._listening = True
|
|
170
214
|
message_accept = Thread(target=_accept, daemon=True)
|
|
171
215
|
message_accept.start()
|
|
172
|
-
logger.
|
|
216
|
+
logger.debug(f"socket消息监听已启动")
|
|
173
217
|
|
|
174
218
|
def set_device_type(self, type: int):
|
|
175
219
|
self._device_type = type
|
|
@@ -206,6 +250,7 @@ class QLBaseDevice(IDevice):
|
|
|
206
250
|
return self._digital_range
|
|
207
251
|
|
|
208
252
|
def init_edf_handler(self):
|
|
253
|
+
logger.warning("init_edf_handler not implemented in base class, should be overridden in subclass")
|
|
209
254
|
pass
|
|
210
255
|
# self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
211
256
|
# self._edf_handler.set_device_type(self.device_type)
|
|
@@ -221,6 +266,7 @@ class QLBaseDevice(IDevice):
|
|
|
221
266
|
@property
|
|
222
267
|
def edf_handler(self):
|
|
223
268
|
if not self.storage_enable:
|
|
269
|
+
logger.warning("EDF storage is disabled, no edf handler available")
|
|
224
270
|
return None
|
|
225
271
|
|
|
226
272
|
if self._edf_handler is None:
|
|
@@ -247,11 +293,11 @@ class QLBaseDevice(IDevice):
|
|
|
247
293
|
return 10
|
|
248
294
|
@property
|
|
249
295
|
def signal_consumers(self):
|
|
250
|
-
return self.
|
|
296
|
+
return self._signal_consumer
|
|
251
297
|
|
|
252
298
|
@property
|
|
253
299
|
def impedance_consumers(self):
|
|
254
|
-
return self.
|
|
300
|
+
return self._impedance_consumer
|
|
255
301
|
|
|
256
302
|
# 设置记录文件路径
|
|
257
303
|
def set_storage_path(self, path):
|
|
@@ -283,26 +329,26 @@ class QLBaseDevice(IDevice):
|
|
|
283
329
|
pass
|
|
284
330
|
|
|
285
331
|
def start_impedance(self):
|
|
286
|
-
logger.info("启动阻抗测量")
|
|
332
|
+
logger.info(f"[设备-{self.device_no}]启动阻抗测量")
|
|
287
333
|
msg = StartImpedanceCommand.build(self).pack()
|
|
288
|
-
logger.
|
|
334
|
+
logger.trace(f"start_impedance message is {msg.hex()}")
|
|
289
335
|
self.socket.sendall(msg)
|
|
290
336
|
|
|
291
337
|
def stop_impedance(self):
|
|
292
|
-
logger.info("停止阻抗测量")
|
|
338
|
+
logger.info(f"[设备{self.device_no}]停止阻抗测量")
|
|
293
339
|
msg = StopImpedanceCommand.build(self).pack()
|
|
294
|
-
logger.
|
|
340
|
+
logger.trace(f"stop_impedance message is {msg.hex()}")
|
|
295
341
|
self.socket.sendall(msg)
|
|
296
342
|
|
|
297
343
|
def start_stimulation(self):
|
|
298
344
|
if self.stim_paradigm is None:
|
|
299
345
|
logger.warning("刺激参数未设置,请先设置刺激参数")
|
|
300
346
|
return
|
|
301
|
-
logger.info("启动电刺激")
|
|
347
|
+
logger.info(f"[设备-{self.device_no}]启动电刺激")
|
|
302
348
|
msg = StartStimulationCommand.build(self).pack()
|
|
303
|
-
logger.
|
|
349
|
+
logger.trace(f"start_stimulation message is {msg.hex()}")
|
|
304
350
|
self.socket.sendall(msg)
|
|
305
|
-
t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
|
|
351
|
+
t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,), daemon=True)
|
|
306
352
|
t.start()
|
|
307
353
|
|
|
308
354
|
def _stop_stimulation_trigger(self, duration):
|
|
@@ -310,67 +356,74 @@ class QLBaseDevice(IDevice):
|
|
|
310
356
|
while delay > 0:
|
|
311
357
|
sleep(1)
|
|
312
358
|
delay -= 1
|
|
313
|
-
logger.
|
|
359
|
+
logger.debug(f"_stop_stimulation_trigger duration: {duration}")
|
|
314
360
|
if self._edf_handler:
|
|
315
361
|
self._edf_handler.trigger("stimulation should be stopped")
|
|
316
362
|
else:
|
|
317
363
|
logger.warning("stop stim trigger fail. no edf writer alive")
|
|
318
364
|
|
|
319
365
|
def stop_stimulation(self):
|
|
320
|
-
logger.info("停止电刺激")
|
|
366
|
+
logger.info(f"[设备-{self.device_no}]停止电刺激")
|
|
321
367
|
msg = StopStimulationCommand.pack()
|
|
322
|
-
logger.
|
|
368
|
+
logger.trace(f"stop_stimulation message is {msg.hex()}")
|
|
323
369
|
self.socket.sendall(msg)
|
|
324
370
|
|
|
325
371
|
# 启动采集
|
|
326
372
|
def start_acquisition(self, recording = True):
|
|
327
|
-
logger.info("启动信号采集")
|
|
373
|
+
logger.info(f"[设备-{self.device_no}]启动信号采集")
|
|
328
374
|
self._recording = recording
|
|
375
|
+
# 初始化EDF处理器
|
|
376
|
+
self.init_edf_handler()
|
|
329
377
|
# 设置数据采集参数
|
|
330
378
|
param_bytes = SetAcquisitionParamCommand.build(self).pack()
|
|
331
379
|
# 启动数据采集
|
|
332
380
|
start_bytes = StartAcquisitionCommand.build(self).pack()
|
|
333
381
|
msg = param_bytes + start_bytes
|
|
334
|
-
logger.
|
|
382
|
+
logger.trace(f"start_acquisition message is {msg.hex()}")
|
|
335
383
|
self.socket.sendall(msg)
|
|
336
384
|
|
|
337
385
|
# 停止采集
|
|
338
386
|
def stop_acquisition(self):
|
|
339
|
-
logger.info("停止信号采集")
|
|
387
|
+
logger.info(f"[设备-{self.device_no}]停止信号采集")
|
|
340
388
|
msg = StopAcquisitionCommand.build(self).pack()
|
|
341
|
-
logger.
|
|
389
|
+
logger.trace(f"stop_acquisition message is {msg.hex()}")
|
|
342
390
|
self.socket.sendall(msg)
|
|
343
391
|
if self._edf_handler:
|
|
344
392
|
# 发送结束标识
|
|
345
393
|
self.edf_handler.write(None)
|
|
346
394
|
|
|
347
|
-
|
|
395
|
+
'''
|
|
396
|
+
订阅数据
|
|
397
|
+
topic: 订阅主题
|
|
398
|
+
q: 数据队列
|
|
399
|
+
type: 数据类型,signal-信号数据,impedance-阻抗数据
|
|
400
|
+
'''
|
|
348
401
|
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
349
402
|
|
|
350
|
-
|
|
351
|
-
|
|
403
|
+
# 队列名称
|
|
404
|
+
if topic is None:
|
|
405
|
+
topic = f"{self.device_no}_{type}_{time_ns()}"
|
|
406
|
+
|
|
407
|
+
logger.debug(f"[设备-{self.device_no}]订阅数据流: {topic}, type: {type}")
|
|
408
|
+
|
|
352
409
|
# 数据队列
|
|
353
410
|
if q is None:
|
|
354
411
|
q = Queue(maxsize=1000)
|
|
355
|
-
|
|
356
|
-
# 队列名称
|
|
357
|
-
if topic is None:
|
|
358
|
-
topic = f"{type}_{time_ns()}"
|
|
359
412
|
|
|
360
413
|
# 订阅生理电信号数据
|
|
361
414
|
if type == "signal":
|
|
362
415
|
# topic唯一,用来区分不同的订阅队列(下同)
|
|
363
|
-
if topic in list(self.
|
|
364
|
-
logger.warning(f"
|
|
416
|
+
if topic in list(self._signal_consumer.keys()):
|
|
417
|
+
logger.warning(f"已存在主题[{topic}]的信号数据订阅!")
|
|
365
418
|
else:
|
|
366
|
-
self.
|
|
419
|
+
self._signal_consumer[topic] = q
|
|
367
420
|
|
|
368
421
|
# 订阅阻抗数据
|
|
369
422
|
if type == "impedance":
|
|
370
|
-
if topic in list(self.
|
|
371
|
-
logger.warning(f"
|
|
423
|
+
if topic in list(self._impedance_consumer.keys()):
|
|
424
|
+
logger.warning(f"已存在主题[{topic}]的阻抗数据订阅!")
|
|
372
425
|
else:
|
|
373
|
-
self.
|
|
426
|
+
self._impedance_consumer[topic] = q
|
|
374
427
|
|
|
375
428
|
return topic, q
|
|
376
429
|
|
|
@@ -378,7 +431,7 @@ class QLBaseDevice(IDevice):
|
|
|
378
431
|
if self._edf_handler:
|
|
379
432
|
self.edf_handler.trigger(desc)
|
|
380
433
|
else:
|
|
381
|
-
logger.warning("
|
|
434
|
+
logger.warning("没有开启文件记录时,无法记录trigger信息")
|
|
382
435
|
|
|
383
436
|
def gen_set_acquirement_param(self) -> bytes:
|
|
384
437
|
|