qlsdk2 0.6.0a2__tar.gz → 0.6.0a4__tar.gz
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.
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/PKG-INFO +1 -1
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/setup.py +1 -1
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/entity/__init__.py +59 -12
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/persist/rsc_edf.py +17 -11
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/command/__init__.py +1 -2
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/base.py +24 -5
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/c256_rs.py +1 -22
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/c64_rs.py +1 -29
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk2.egg-info/PKG-INFO +1 -1
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk2.egg-info/SOURCES.txt +2 -1
- qlsdk2-0.6.0a4/test/test_bdf.py +212 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/README.md +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/setup.cfg +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/ar4/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/ar4m/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/crc/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/crc/crctools.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/device.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/exception.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/filter/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/filter/norch.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/local.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/message/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/message/command.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/message/tcp.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/message/udp.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/network/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/network/monitor.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/core/utils.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/entity/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/entity/message.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/entity/signal.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/analyzer.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/collector.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/device.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/parser.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/stimulator.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/interface/store.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/persist/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/persist/ars_edf.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/persist/edf.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/persist/stream.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/arskindling.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/c16_rs.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/c64s1.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/device/device_factory.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/eegion.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/entity.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/interface/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/interface/command.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/interface/device.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/interface/handler.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/interface/parser.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/manager/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/manager/container.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/manager/search.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/network/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/network/discover.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/paradigm.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/parser/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/parser/base-new.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/parser/base.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/parser/rsc.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/rsc/proxy.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/sdk/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/sdk/ar4sdk.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/sdk/hub.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/x8/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk/x8m/__init__.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk2.egg-info/requires.txt +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/src/qlsdk2.egg-info/top_level.txt +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/test/test.222.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/test/test.py +0 -0
- {qlsdk2-0.6.0a2 → qlsdk2-0.6.0a4}/test/test_ar4m.py +0 -0
|
@@ -50,7 +50,8 @@ class RscPacket(Packet):
|
|
|
50
50
|
for i in range(ch_num)
|
|
51
51
|
]
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
logger.trace(packet)
|
|
54
|
+
|
|
54
55
|
return packet
|
|
55
56
|
|
|
56
57
|
def __str__(self):
|
|
@@ -71,24 +72,34 @@ class RscPacket(Packet):
|
|
|
71
72
|
class ImpedancePacket(Packet):
|
|
72
73
|
def __init__(self):
|
|
73
74
|
super().__init__()
|
|
74
|
-
self.data_len = None
|
|
75
75
|
self.impedance = None
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
@staticmethod
|
|
78
|
+
def transfer(body:bytes) -> 'ImpedancePacket':
|
|
79
|
+
packet = ImpedancePacket()
|
|
80
|
+
packet.time_stamp = int.from_bytes(body[0:8], 'little')
|
|
81
|
+
# packet.result = body[8]
|
|
82
|
+
packet.pkg_id = int.from_bytes(body[9: 13], 'little')
|
|
83
|
+
packet.channels = to_channels(body[13: 45])
|
|
84
|
+
# packet.sample_rate = int.from_bytes(body[45: 49], 'little')
|
|
85
|
+
# packet.sample_len = int.from_bytes(body[49: 53], 'little')
|
|
86
|
+
# packet.resolution = int(int(body[53]) / 8)
|
|
87
|
+
# packet.filter = int(int(body[54]) / 8)
|
|
88
|
+
# packet.wave_type = int(int(body[55]) / 8)
|
|
89
|
+
# packet.wave_freq = int.from_bytes(body[56: 60], 'little')
|
|
90
|
+
# packet.data_len = int.from_bytes(body[60: 64], 'little')
|
|
91
|
+
b_impedance = body[64:]
|
|
92
|
+
packet.impedance = [int.from_bytes(b_impedance[j * 4 : j * 4 + 4], 'little', signed=False) for j in range(len(packet.channels))]
|
|
93
|
+
|
|
94
|
+
logger.trace(f"impedance: {packet}")
|
|
84
95
|
|
|
96
|
+
return packet
|
|
97
|
+
|
|
85
98
|
def __str__(self):
|
|
86
99
|
return f"""
|
|
87
100
|
time_stamp: {self.time_stamp}
|
|
88
101
|
pkg_id: {self.pkg_id}
|
|
89
|
-
result: {self.result}
|
|
90
102
|
channels: {self.channels}
|
|
91
|
-
data len: {self.data_len}
|
|
92
103
|
impedance: {self.impedance}
|
|
93
104
|
"""
|
|
94
105
|
|
|
@@ -161,4 +172,40 @@ class C256RSPacket(Packet):
|
|
|
161
172
|
offset += 4 + channel_size * packet.resolution
|
|
162
173
|
|
|
163
174
|
logger.trace(packet)
|
|
164
|
-
return packet
|
|
175
|
+
return packet
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class C256ImpedancePacket(Packet):
|
|
179
|
+
def __init__(self):
|
|
180
|
+
super().__init__()
|
|
181
|
+
self.impedance = None
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def transfer(body:bytes) -> 'ImpedancePacket':
|
|
185
|
+
packet = ImpedancePacket()
|
|
186
|
+
packet.time_stamp = int.from_bytes(body[0:8], 'little')
|
|
187
|
+
# packet.result = body[8]
|
|
188
|
+
packet.pkg_id = int.from_bytes(body[9: 13], 'little')
|
|
189
|
+
packet.channels = to_channels(body[13: 45])
|
|
190
|
+
# packet.sample_rate = int.from_bytes(body[45: 49], 'little')
|
|
191
|
+
# packet.sample_len = int.from_bytes(body[49: 53], 'little')
|
|
192
|
+
# packet.resolution = int(int(body[53]) / 8)
|
|
193
|
+
# packet.filter = int(int(body[54]) / 8)
|
|
194
|
+
# packet.wave_type = int(int(body[55]) / 8)
|
|
195
|
+
# packet.wave_freq = int.from_bytes(body[56: 60], 'little')
|
|
196
|
+
# packet.data_len = int.from_bytes(body[60: 64], 'little')
|
|
197
|
+
b_impedance = body[64:]
|
|
198
|
+
packet.impedance = [int.from_bytes(b_impedance[j : j + 4], 'little', signed=False) for j in range(len(packet.channels))]
|
|
199
|
+
|
|
200
|
+
logger.trace(f"impedance: {packet}")
|
|
201
|
+
|
|
202
|
+
def __str__(self):
|
|
203
|
+
return f"""
|
|
204
|
+
time_stamp: {self.time_stamp}
|
|
205
|
+
pkg_id: {self.pkg_id}
|
|
206
|
+
result: {self.result}
|
|
207
|
+
channels: {self.channels}
|
|
208
|
+
data len: {self.data_len}
|
|
209
|
+
impedance: {self.impedance}
|
|
210
|
+
"""
|
|
211
|
+
|
|
@@ -15,7 +15,7 @@ EDF_FILE_TYPE = {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
class EDFStreamWriter(Thread):
|
|
18
|
-
def __init__(self, channels, sample_frequency, physical_max,
|
|
18
|
+
def __init__(self, channels, sample_frequency, physical_max, physical_min, file_type, file_path, record_duration=None):
|
|
19
19
|
|
|
20
20
|
super().__init__()
|
|
21
21
|
self._writer : EdfWriter = None
|
|
@@ -32,7 +32,7 @@ class EDFStreamWriter(Thread):
|
|
|
32
32
|
self._n_channels = len(channels)
|
|
33
33
|
self.sample_frequency = sample_frequency
|
|
34
34
|
self.physical_max = physical_max
|
|
35
|
-
self.physical_min =
|
|
35
|
+
self.physical_min = physical_min
|
|
36
36
|
# 和位数相关,edf 16 bits/bdf 24 bits
|
|
37
37
|
self.digital_max = 8388607 if file_type == EDF_FILE_TYPE['bdf'] else 32767
|
|
38
38
|
self.digital_min = -8388608 if file_type == EDF_FILE_TYPE['bdf'] else -32768
|
|
@@ -46,6 +46,7 @@ class EDFStreamWriter(Thread):
|
|
|
46
46
|
self.equipment = "equipment"
|
|
47
47
|
self.patient_code = "patient_code"
|
|
48
48
|
self.patient_name = "patient_name"
|
|
49
|
+
logger.info(f'digital_max:{self.digital_max}, digital_min:{self.digital_min}, physical_max:{self.physical_max}, physical_min:{self.physical_min}')
|
|
49
50
|
|
|
50
51
|
def set_channels(self, channels):
|
|
51
52
|
self._channels = channels
|
|
@@ -127,6 +128,7 @@ class EDFStreamWriter(Thread):
|
|
|
127
128
|
|
|
128
129
|
# 配置通道参数
|
|
129
130
|
signal_headers = []
|
|
131
|
+
logger.trace(f"sf: {self.sample_frequency}, pm: {self.physical_max}, pn: {self.physical_min}, dm: {self.digital_max}, dn: {self.digital_min}")
|
|
130
132
|
for ch in range(self._n_channels):
|
|
131
133
|
signal_headers.append({
|
|
132
134
|
"label": f'channels {self._channels[ch]}',
|
|
@@ -169,14 +171,16 @@ class EDFStreamWriter(Thread):
|
|
|
169
171
|
# 写入1秒的数据
|
|
170
172
|
def _write_block(self, block):
|
|
171
173
|
logger.trace(f"写入数据: {block}")
|
|
172
|
-
# 转换数据类型为float64
|
|
173
|
-
|
|
174
|
+
# 转换数据类型为float64-物理信号uV int32-数字信号
|
|
175
|
+
data_input = block.astype(np.int32)
|
|
176
|
+
logger.trace(f"写入数据-real: {data_input}")
|
|
174
177
|
# 写入时转置为(样本数, 通道数)格式
|
|
175
|
-
self._writer.writeSamples(
|
|
178
|
+
self._writer.writeSamples(data_input, digital=True)
|
|
176
179
|
self._duration += 1
|
|
177
180
|
|
|
178
181
|
if self._duration % 10 == 0: # 每10秒打印一次进度
|
|
179
182
|
logger.info(f"数据记录中... 文件名:{self.file_path}, 已记录时长: {self._duration}秒")
|
|
183
|
+
|
|
180
184
|
|
|
181
185
|
# 用作数据结构一致化处理,通过调用公共类写入edf文件
|
|
182
186
|
# 入参包含写入edf的全部前置参数
|
|
@@ -200,7 +204,7 @@ class RscEDFHandler(object):
|
|
|
200
204
|
self.physical_max = physical_max
|
|
201
205
|
self.physical_min = physical_min
|
|
202
206
|
self.digital_max = 8388607 if resolution == 24 else 32767
|
|
203
|
-
self.digital_min = -
|
|
207
|
+
self.digital_min = -8388608 if resolution == 24 else - 32768
|
|
204
208
|
self.file_type = EDF_FILE_TYPE["bdf"] if resolution == 24 else EDF_FILE_TYPE["edf"]
|
|
205
209
|
# 点分辨率
|
|
206
210
|
self.resolution = resolution
|
|
@@ -226,8 +230,8 @@ class RscEDFHandler(object):
|
|
|
226
230
|
self._end_time = None
|
|
227
231
|
self._patient_code = "patient_code"
|
|
228
232
|
self._patient_name = "patient_name"
|
|
229
|
-
self._device_type = "
|
|
230
|
-
self._device_no = "
|
|
233
|
+
self._device_type = "0000"
|
|
234
|
+
self._device_no = "00000000"
|
|
231
235
|
self._total_packets = 0
|
|
232
236
|
self._lost_packets = 0
|
|
233
237
|
self._storage_path = storage_path
|
|
@@ -242,7 +246,7 @@ class RscEDFHandler(object):
|
|
|
242
246
|
suffix = "bdf" if self.resolution == 24 else "edf"
|
|
243
247
|
|
|
244
248
|
# 文件名称
|
|
245
|
-
file_name = f"{self._file_prefix}_{self._device_no}_{self._start_time.strftime('%y%m%d%H%
|
|
249
|
+
file_name = f"{self._file_prefix}_{self._device_no}_{self._start_time.strftime('%y%m%d%H%M%S')}.{suffix}" if self._file_prefix else f"{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}"
|
|
246
250
|
|
|
247
251
|
if self._storage_path:
|
|
248
252
|
try:
|
|
@@ -260,6 +264,8 @@ class RscEDFHandler(object):
|
|
|
260
264
|
self._device_type = "C64RS"
|
|
261
265
|
elif device_type == 0x40:
|
|
262
266
|
self._device_type = "LJ64S1"
|
|
267
|
+
elif device_type == 0x45:
|
|
268
|
+
self._device_type = "C256RS"
|
|
263
269
|
elif device_type == 0x51:
|
|
264
270
|
self._device_type = "C256RS"
|
|
265
271
|
elif device_type == 0x60:
|
|
@@ -312,6 +318,7 @@ class RscEDFHandler(object):
|
|
|
312
318
|
self._edf_writer_thread.set_start_time(self._start_time)
|
|
313
319
|
self._edf_writer_thread.start()
|
|
314
320
|
logger.info(f"开始写入数据: {self.file_name}")
|
|
321
|
+
self._edf_writer_thread.equipment = f'{self._device_type}_{self._device_no}'
|
|
315
322
|
|
|
316
323
|
self._edf_writer_thread.append(packet.eeg)
|
|
317
324
|
|
|
@@ -331,5 +338,4 @@ class RscEDFHandler(object):
|
|
|
331
338
|
else: onset = 0
|
|
332
339
|
else:
|
|
333
340
|
onset = cur_time - self._first_timestamp
|
|
334
|
-
self._edf_writer_thread.trigger(onset, desc)
|
|
335
|
-
|
|
341
|
+
self._edf_writer_thread.trigger(onset, desc)
|
|
@@ -297,8 +297,7 @@ class ImpedanceDataCommand(DeviceCommand):
|
|
|
297
297
|
cmd_desc = "阻抗数据"
|
|
298
298
|
|
|
299
299
|
def parse_body(self, body: bytes):
|
|
300
|
-
|
|
301
|
-
packet = ImpedancePacket().transfer(body)
|
|
300
|
+
self.device.produce(body, type="impedance")
|
|
302
301
|
|
|
303
302
|
# 信号数据
|
|
304
303
|
class SignalDataCommand(DeviceCommand):
|
|
@@ -5,7 +5,7 @@ from typing import Any, Dict, Literal
|
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
7
|
import numpy as np
|
|
8
|
-
from qlsdk.core.entity import RscPacket
|
|
8
|
+
from qlsdk.core.entity import RscPacket, ImpedancePacket
|
|
9
9
|
from qlsdk.core.utils import to_bytes
|
|
10
10
|
from qlsdk.rsc.interface import IDevice, IParser
|
|
11
11
|
from qlsdk.rsc.command import StartImpedanceCommand, StopImpedanceCommand, StartStimulationCommand, StopStimulationCommand, SetAcquisitionParamCommand, StartAcquisitionCommand, StopAcquisitionCommand
|
|
@@ -136,7 +136,24 @@ class QLBaseDevice(IDevice):
|
|
|
136
136
|
|
|
137
137
|
# 数据包处理
|
|
138
138
|
def produce(self, body: bytes, type:Literal['signal', 'impedance']="signal"):
|
|
139
|
-
if body is None: return
|
|
139
|
+
if body is None: return
|
|
140
|
+
|
|
141
|
+
if type == "signal":
|
|
142
|
+
self._produce_signal(body)
|
|
143
|
+
elif type == "impedance":
|
|
144
|
+
self._produce_impedance(body)
|
|
145
|
+
|
|
146
|
+
def _produce_impedance(self, body: bytes):
|
|
147
|
+
packet = ImpedancePacket.transfer(body)
|
|
148
|
+
# 分发阻抗数据包给订阅者
|
|
149
|
+
if len(self._impedance_consumer) > 0:
|
|
150
|
+
for topic, q in self._impedance_consumer.items():
|
|
151
|
+
try:
|
|
152
|
+
q.put(packet, timeout=1)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"impedance data put to queue exception: {str(e)}")
|
|
155
|
+
|
|
156
|
+
def _produce_signal(self, body: bytes):
|
|
140
157
|
|
|
141
158
|
# 处理信号数据
|
|
142
159
|
data = self._signal_wrapper(body)
|
|
@@ -152,8 +169,10 @@ class QLBaseDevice(IDevice):
|
|
|
152
169
|
self._write_signal(data)
|
|
153
170
|
|
|
154
171
|
if len(self.signal_consumers) > 0 :
|
|
172
|
+
logger.trace(f"dg eeg: {data.eeg}")
|
|
155
173
|
# 信号数字值转物理值
|
|
156
174
|
data.eeg = self.eeg2phy(np.array(data.eeg))
|
|
175
|
+
logger.trace(f"ph eeg: {data.eeg}")
|
|
157
176
|
|
|
158
177
|
# 发送数据包到订阅者
|
|
159
178
|
for q in list(self.signal_consumers.values()):
|
|
@@ -161,7 +180,7 @@ class QLBaseDevice(IDevice):
|
|
|
161
180
|
if q.full():
|
|
162
181
|
q.get()
|
|
163
182
|
|
|
164
|
-
q.put(data)
|
|
183
|
+
q.put(data)
|
|
165
184
|
|
|
166
185
|
# 信号数据转换
|
|
167
186
|
def _signal_wrapper(self, body: bytes):
|
|
@@ -335,7 +354,7 @@ class QLBaseDevice(IDevice):
|
|
|
335
354
|
self.stim_paradigm = param
|
|
336
355
|
|
|
337
356
|
# 设置采集参数
|
|
338
|
-
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
357
|
+
def set_acq_param(self, channels, sample_rate:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 500, sample_range:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 188):
|
|
339
358
|
self._acq_param["channels"] = channels
|
|
340
359
|
self._acq_param["sample_rate"] = sample_rate
|
|
341
360
|
self._acq_param["sample_range"] = sample_range
|
|
@@ -427,7 +446,7 @@ class QLBaseDevice(IDevice):
|
|
|
427
446
|
|
|
428
447
|
# 数据队列
|
|
429
448
|
if q is None:
|
|
430
|
-
q = Queue(maxsize=
|
|
449
|
+
q = Queue(maxsize=1024 * 1024)
|
|
431
450
|
|
|
432
451
|
# 订阅生理电信号数据
|
|
433
452
|
if type == "signal":
|
|
@@ -189,19 +189,7 @@ class C256RS(QLBaseDevice):
|
|
|
189
189
|
|
|
190
190
|
# 设置记录文件名称前缀
|
|
191
191
|
def set_file_prefix(self, prefix):
|
|
192
|
-
self._file_prefix = prefix
|
|
193
|
-
|
|
194
|
-
# 接收socket消息
|
|
195
|
-
def accept(self):
|
|
196
|
-
pass
|
|
197
|
-
# while True:
|
|
198
|
-
# # 缓冲去4M
|
|
199
|
-
# data = self.socket.recv(4096*1024)
|
|
200
|
-
# if not data:
|
|
201
|
-
# logger.warning(f"设备{self.device_name}连接结束")
|
|
202
|
-
# break
|
|
203
|
-
|
|
204
|
-
# self._parser.append(data)
|
|
192
|
+
self._file_prefix = prefix
|
|
205
193
|
|
|
206
194
|
# socket发送数据
|
|
207
195
|
def send(self, data):
|
|
@@ -211,15 +199,6 @@ class C256RS(QLBaseDevice):
|
|
|
211
199
|
def set_stim_param(self, param):
|
|
212
200
|
self.stim_paradigm = param
|
|
213
201
|
|
|
214
|
-
# 设置采集参数
|
|
215
|
-
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
216
|
-
self._acq_param["channels"] = channels
|
|
217
|
-
self._acq_param["sample_rate"] = sample_rate
|
|
218
|
-
self._acq_param["sample_range"] = sample_range
|
|
219
|
-
self._acq_channels = channels
|
|
220
|
-
self._sample_rate = sample_rate
|
|
221
|
-
self._sample_range = sample_range
|
|
222
|
-
|
|
223
202
|
def _signal_wrapper(self, body: bytes):
|
|
224
203
|
return C256RSPacket().transfer(body)
|
|
225
204
|
|
|
@@ -285,35 +285,7 @@ class C64RS(QLBaseDevice):
|
|
|
285
285
|
if self._edf_handler:
|
|
286
286
|
# 发送结束标识
|
|
287
287
|
self.edf_handler.write(None)
|
|
288
|
-
|
|
289
|
-
# 订阅实时数据
|
|
290
|
-
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
291
|
-
|
|
292
|
-
# 数据队列
|
|
293
|
-
if q is None:
|
|
294
|
-
q = Queue(maxsize=1000)
|
|
295
|
-
|
|
296
|
-
# 队列名称
|
|
297
|
-
if topic is None:
|
|
298
|
-
topic = f"{type}_{time_ns()}"
|
|
299
|
-
|
|
300
|
-
# 订阅生理电信号数据
|
|
301
|
-
if type == "signal":
|
|
302
|
-
# topic唯一,用来区分不同的订阅队列(下同)
|
|
303
|
-
if topic in list(self.__signal_consumer.keys()):
|
|
304
|
-
logger.warning(f"exists {type} subscribe of {topic}")
|
|
305
|
-
else:
|
|
306
|
-
self.__signal_consumer[topic] = q
|
|
307
|
-
|
|
308
|
-
# 订阅阻抗数据
|
|
309
|
-
if type == "impedance":
|
|
310
|
-
if topic in list(self.__signal_consumer.keys()):
|
|
311
|
-
logger.warning(f"exists {type} subscribe of {topic}")
|
|
312
|
-
else:
|
|
313
|
-
self.__impedance_consumer[topic] = q
|
|
314
|
-
|
|
315
|
-
return topic, q
|
|
316
|
-
|
|
288
|
+
|
|
317
289
|
def trigger(self, desc):
|
|
318
290
|
if self._edf_handler:
|
|
319
291
|
self.edf_handler.trigger(desc)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# 使用示例
|
|
2
|
+
from read_bdf import BDFEDFReader
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
"""主函数示例"""
|
|
7
|
+
|
|
8
|
+
# 1. 创建读取器
|
|
9
|
+
filepath = r"E:\project\qlsdk-project\C256RS_450025430001_251223182657.bdf" # 或 "sample.edf"
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
# 使用上下文管理器自动管理文件
|
|
13
|
+
with BDFEDFReader(filepath) as reader:
|
|
14
|
+
|
|
15
|
+
# 2. 获取文件信息
|
|
16
|
+
file_info = reader.get_file_info()
|
|
17
|
+
print("=== 文件信息 ===")
|
|
18
|
+
for key, value in file_info.items():
|
|
19
|
+
print(f"{key}: {value}")
|
|
20
|
+
print()
|
|
21
|
+
|
|
22
|
+
# 3. 获取通道信息
|
|
23
|
+
channel_info = reader.get_channel_info()
|
|
24
|
+
print("=== 通道信息 (前3个) ===")
|
|
25
|
+
for i in range(min(3, len(channel_info))):
|
|
26
|
+
info = channel_info[i]
|
|
27
|
+
print(f"通道 {i} ({info['label']}):")
|
|
28
|
+
print(f" 采样率: {info['sample_frequency']} Hz")
|
|
29
|
+
print(f" 物理范围: [{info['physical_min']}, {info['physical_max']}] {info['physical_dimension']}")
|
|
30
|
+
print(f" 数字范围: [{info['digital_min']}, {info['digital_max']}]")
|
|
31
|
+
print(f" 增益: {info['gain']:.6f}, 偏移: {info['offset']:.6f}")
|
|
32
|
+
print()
|
|
33
|
+
|
|
34
|
+
# 4. 读取指定时间段的数据
|
|
35
|
+
start_time = 20.0 # 10秒
|
|
36
|
+
end_time = 30.0 # 12秒
|
|
37
|
+
|
|
38
|
+
print(f"=== 读取 {start_time}s 到 {end_time}s 的数据 ===")
|
|
39
|
+
|
|
40
|
+
# 读取所有通道的物理值
|
|
41
|
+
data_physical = reader.read_data(
|
|
42
|
+
start_time=start_time,
|
|
43
|
+
end_time=end_time,
|
|
44
|
+
output_type='digital', # 或 'digital'/'physical'
|
|
45
|
+
return_as_dataframe=True
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# 5. 显示统计信息
|
|
49
|
+
stats = reader.get_channel_statistics(data_physical)
|
|
50
|
+
print("=== 统计信息 ===")
|
|
51
|
+
for channel_idx, stat in stats.items():
|
|
52
|
+
print(f"通道 {channel_idx} ({stat['label']}):")
|
|
53
|
+
print(f" 样本数: {stat['count']}")
|
|
54
|
+
print(f" 范围: [{stat['min']:.2f}, {stat['max']:.2f}]")
|
|
55
|
+
print(f" 均值: {stat['mean']:.2f} ± {stat['std']:.2f}")
|
|
56
|
+
print()
|
|
57
|
+
|
|
58
|
+
# 6. 获取DataFrame
|
|
59
|
+
if 'dataframe' in data_physical:
|
|
60
|
+
df = data_physical['dataframe']
|
|
61
|
+
print("=== DataFrame 前5行 ===")
|
|
62
|
+
print(df.head())
|
|
63
|
+
print()
|
|
64
|
+
print("=== DataFrame 信息 ===")
|
|
65
|
+
print(df.info())
|
|
66
|
+
|
|
67
|
+
# 7. 导出为CSV
|
|
68
|
+
reader.export_to_csv(data_physical, "output_data.csv")
|
|
69
|
+
|
|
70
|
+
# 8. 读取特定通道
|
|
71
|
+
print("\n=== 读取特定通道 ===")
|
|
72
|
+
channel_indices = [0, 1, 2] # 前3个通道
|
|
73
|
+
data_channels = reader.read_data(
|
|
74
|
+
start_time=15.0,
|
|
75
|
+
end_time=16.0,
|
|
76
|
+
channel_indices=channel_indices,
|
|
77
|
+
output_type='digital' # 数字值
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
for channel_idx, channel_data in data_channels['channels'].items():
|
|
81
|
+
print(f"通道 {channel_idx} 前5个数字值: {channel_data['data'][:5]}")
|
|
82
|
+
|
|
83
|
+
# 9. 通过标签获取通道
|
|
84
|
+
try:
|
|
85
|
+
eeg_channel_idx = reader.get_channel_by_label("EEG")
|
|
86
|
+
print(f"\nEEG通道索引: {eeg_channel_idx}")
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
print(f"未找到EEG通道: {e}")
|
|
89
|
+
|
|
90
|
+
# 10. 读取标注
|
|
91
|
+
annotations = reader.read_annotations()
|
|
92
|
+
if annotations:
|
|
93
|
+
print("\n=== 文件标注 ===")
|
|
94
|
+
for i, annot in enumerate(annotations[:5]): # 显示前5个
|
|
95
|
+
print(f"{i+1}. 时间: {annot['onset']:.2f}s, "
|
|
96
|
+
f"时长: {annot['duration']:.2f}s, "
|
|
97
|
+
f"描述: {annot['description']}")
|
|
98
|
+
|
|
99
|
+
# 11. 绘制图形
|
|
100
|
+
try:
|
|
101
|
+
reader.plot_segment(
|
|
102
|
+
data_dict=data_physical,
|
|
103
|
+
channel_indices=[0, 1, 2],
|
|
104
|
+
figsize=(12, 8),
|
|
105
|
+
save_path="plot.png"
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"绘图失败: {e}")
|
|
109
|
+
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
print(f"文件不存在: {filepath}")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"处理文件时出错: {e}")
|
|
114
|
+
|
|
115
|
+
def batch_process_files(file_list, output_dir="output"):
|
|
116
|
+
"""
|
|
117
|
+
批量处理多个文件
|
|
118
|
+
"""
|
|
119
|
+
import os
|
|
120
|
+
|
|
121
|
+
if not os.path.exists(output_dir):
|
|
122
|
+
os.makedirs(output_dir)
|
|
123
|
+
|
|
124
|
+
for filepath in file_list:
|
|
125
|
+
if not os.path.exists(filepath):
|
|
126
|
+
print(f"文件不存在: {filepath}")
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
print(f"\n处理文件: {filepath}")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
reader = BDFEDFReader(filepath)
|
|
133
|
+
reader.open()
|
|
134
|
+
|
|
135
|
+
# 获取文件名
|
|
136
|
+
filename = os.path.basename(filepath)
|
|
137
|
+
base_name = os.path.splitext(filename)[0]
|
|
138
|
+
|
|
139
|
+
# 读取前30秒的数据
|
|
140
|
+
data = reader.read_data(
|
|
141
|
+
start_time=0,
|
|
142
|
+
end_time=30,
|
|
143
|
+
output_type='physical',
|
|
144
|
+
return_as_dataframe=True
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 导出CSV
|
|
148
|
+
csv_path = os.path.join(output_dir, f"{base_name}.csv")
|
|
149
|
+
reader.export_to_csv(data, csv_path)
|
|
150
|
+
|
|
151
|
+
# 导出统计信息
|
|
152
|
+
stats = reader.get_channel_statistics(data)
|
|
153
|
+
stats_path = os.path.join(output_dir, f"{base_name}_stats.json")
|
|
154
|
+
|
|
155
|
+
import json
|
|
156
|
+
with open(stats_path, 'w') as f:
|
|
157
|
+
json.dump(stats, f, indent=2, default=str)
|
|
158
|
+
|
|
159
|
+
print(f" 已保存: {csv_path}")
|
|
160
|
+
print(f" 已保存: {stats_path}")
|
|
161
|
+
|
|
162
|
+
reader.close()
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f" 处理失败: {e}")
|
|
166
|
+
|
|
167
|
+
# 命令行接口
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
|
170
|
+
# import argparse
|
|
171
|
+
|
|
172
|
+
# parser = argparse.ArgumentParser(description='读取BDF/EDF文件并提取数据')
|
|
173
|
+
# parser.add_argument('filepath', help='BDF/EDF文件路径')
|
|
174
|
+
# parser.add_argument('--start', type=float, default=0, help='开始时间(秒)')
|
|
175
|
+
# parser.add_argument('--end', type=float, default=10, help='结束时间(秒)')
|
|
176
|
+
# parser.add_argument('--channels', type=str, default=1 help='通道索引(逗号分隔)')
|
|
177
|
+
# parser.add_argument('--output-type', choices=['physical', 'digital'], default='physical',
|
|
178
|
+
# help='输出类型: physical(物理值) 或 digital(数字值)')
|
|
179
|
+
# parser.add_argument('--output', help='输出CSV文件路径')
|
|
180
|
+
# parser.add_argument('--plot', action='store_true', help='绘制图表')
|
|
181
|
+
|
|
182
|
+
# args = parser.parse_args()
|
|
183
|
+
|
|
184
|
+
# # 解析通道参数
|
|
185
|
+
# channel_indices = None
|
|
186
|
+
# if args.channels:
|
|
187
|
+
# channel_indices = [int(c.strip()) for c in args.channels.split(',')]
|
|
188
|
+
|
|
189
|
+
# # 处理文件
|
|
190
|
+
# with BDFEDFReader(args.filepath) as reader:
|
|
191
|
+
# data = reader.read_data(
|
|
192
|
+
# start_time=args.start,
|
|
193
|
+
# end_time=args.end,
|
|
194
|
+
# channel_indices=channel_indices,
|
|
195
|
+
# output_type=args.output_type,
|
|
196
|
+
# return_as_dataframe=True
|
|
197
|
+
# )
|
|
198
|
+
|
|
199
|
+
# # 输出到控制台
|
|
200
|
+
# if 'dataframe' in data:
|
|
201
|
+
# print(f"读取了 {len(data['dataframe'])} 个样本")
|
|
202
|
+
# print("\n前10个样本:")
|
|
203
|
+
# print(data['dataframe'].head(10))
|
|
204
|
+
|
|
205
|
+
# # 导出到CSV
|
|
206
|
+
# if args.output:
|
|
207
|
+
# reader.export_to_csv(data, args.output)
|
|
208
|
+
# print(f"\n数据已导出到: {args.output}")
|
|
209
|
+
|
|
210
|
+
# # 绘制图表
|
|
211
|
+
# if args.plot:
|
|
212
|
+
# reader.plot_segment(data, channel_indices=channel_indices)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|