qlsdk2 0.4.0a3__tar.gz → 0.4.1__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.4.0a3 → qlsdk2-0.4.1}/PKG-INFO +1 -1
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/setup.py +1 -1
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/ar4/__init__.py +9 -5
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/device.py +10 -1
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/command.py +56 -35
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/persist/edf.py +21 -2
- qlsdk2-0.4.1/src/qlsdk/persist/rsc_edf.py +300 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/__init__.py +9 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/command/__init__.py +336 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/command/message.py +336 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/device/__init__.py +2 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/device/base.py +388 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/device/c64_rs.py +364 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/device/device_factory.py +29 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/entity.py +63 -24
- qlsdk2-0.4.1/src/qlsdk/rsc/interface/__init__.py +3 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/interface/command.py +10 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/interface/device.py +107 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/interface/handler.py +9 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/interface/parser.py +9 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/manager/__init__.py +2 -0
- qlsdk2-0.4.0a3/src/qlsdk/rsc/device_manager.py → qlsdk2-0.4.1/src/qlsdk/rsc/manager/container.py +15 -13
- qlsdk2-0.4.1/src/qlsdk/rsc/manager/search.py +0 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/network/__init__.py +1 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/paradigm.py +1 -1
- qlsdk2-0.4.1/src/qlsdk/rsc/parser/__init__.py +1 -0
- qlsdk2-0.4.1/src/qlsdk/rsc/parser/base.py +66 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/ar4sdk.py +13 -4
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/x8/__init__.py +4 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/PKG-INFO +1 -1
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/SOURCES.txt +16 -2
- qlsdk2-0.4.0a3/src/qlsdk/persist/rsc_edf.py +0 -241
- qlsdk2-0.4.0a3/src/qlsdk/rsc/__init__.py +0 -7
- qlsdk2-0.4.0a3/src/qlsdk/rsc/command/__init__.py +0 -214
- qlsdk2-0.4.0a3/src/qlsdk/rsc/command/message.py +0 -239
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/README.md +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/setup.cfg +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/ar4m/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/crc/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/crc/crctools.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/entity/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/exception.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/filter/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/filter/norch.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/local.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/tcp.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/udp.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/network/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/network/monitor.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/utils.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/persist/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/eegion.py +0 -0
- {qlsdk2-0.4.0a3/src/qlsdk/rsc → qlsdk2-0.4.1/src/qlsdk/rsc/network}/discover.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/proxy.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/hub.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/x8m/__init__.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/requires.txt +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/top_level.txt +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/test/test.py +0 -0
- {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/test/test_ar4m.py +0 -0
|
@@ -39,12 +39,12 @@ class AR4(LMDevice):
|
|
|
39
39
|
consumer.put(packet)
|
|
40
40
|
|
|
41
41
|
if len(self._phy_subscriber) > 0:
|
|
42
|
-
logger.
|
|
43
|
-
logger.
|
|
42
|
+
logger.debug(f"dig data eeg: {packet.eeg}")
|
|
43
|
+
logger.debug(f"dig data acc: {packet.acc}")
|
|
44
44
|
packet.eeg = self.eeg2phy(np.array(packet.eeg))
|
|
45
45
|
packet.acc = self.acc2phy(np.array(packet.acc))
|
|
46
|
-
logger.
|
|
47
|
-
logger.
|
|
46
|
+
logger.debug(f"phy data eeg: {packet.eeg}")
|
|
47
|
+
logger.debug(f"phy data acc: {packet.acc}")
|
|
48
48
|
for consumer2 in self._phy_subscriber.values():
|
|
49
49
|
consumer2.put(packet)
|
|
50
50
|
|
|
@@ -74,7 +74,11 @@ class AR4(LMDevice):
|
|
|
74
74
|
self._edf_handler = None
|
|
75
75
|
self._recording = False
|
|
76
76
|
logger.info(f"停止记录数据: {self.box_mac}")
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
def trigger(self, desc):
|
|
79
|
+
if self._edf_handler:
|
|
80
|
+
self._edf_handler.trigger(desc)
|
|
81
|
+
|
|
78
82
|
# 订阅推送消息
|
|
79
83
|
def subscribe(self, topic: str = None, q: Queue = Queue(), value_type: Literal['phy', 'dig'] = 'phy') -> tuple[str, Queue]:
|
|
80
84
|
if topic is None:
|
|
@@ -4,9 +4,18 @@ class BaseDevice(object):
|
|
|
4
4
|
def __init__(self, socket = None):
|
|
5
5
|
self.socket = socket
|
|
6
6
|
self.device_name = None
|
|
7
|
-
self.
|
|
7
|
+
self._device_type = None
|
|
8
8
|
self.device_id = None
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def device_type(self) -> int:
|
|
13
|
+
return self._device_type
|
|
14
|
+
|
|
15
|
+
@device_type.setter
|
|
16
|
+
def device_type(self, value: int):
|
|
17
|
+
self._device_type = value
|
|
18
|
+
|
|
10
19
|
@property
|
|
11
20
|
def acq_channels(self) :
|
|
12
21
|
return None
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
import numpy as np
|
|
2
3
|
from time import time_ns
|
|
3
4
|
from typing import Dict, Type
|
|
4
|
-
from enum import Enum
|
|
5
5
|
from loguru import logger
|
|
6
|
+
|
|
7
|
+
|
|
6
8
|
from qlsdk.core.crc import crc16
|
|
7
9
|
from qlsdk.core.device import BaseDevice
|
|
8
10
|
from qlsdk.core.entity import RscPacket, ImpedancePacket
|
|
@@ -64,6 +66,13 @@ class DeviceCommand(abc.ABC):
|
|
|
64
66
|
# 解析消息体
|
|
65
67
|
body = payload[self.HEADER_LEN:-2]
|
|
66
68
|
|
|
69
|
+
def parse_body(self, body: bytes):
|
|
70
|
+
time = int.from_bytes(body[0:8], 'little')
|
|
71
|
+
# result - 1B
|
|
72
|
+
result = body[8]
|
|
73
|
+
logger.info(f"[{time}]{self.cmd_desc}{'成功' if result == 0 else '失败'}")
|
|
74
|
+
|
|
75
|
+
|
|
67
76
|
|
|
68
77
|
class CommandFactory:
|
|
69
78
|
"""Registry for command implementations"""
|
|
@@ -114,6 +123,8 @@ class GetDeviceInfoCommand(DeviceCommand):
|
|
|
114
123
|
flag = int.from_bytes(body[41:45], 'little')
|
|
115
124
|
logger.debug(f"Received device info: {result}, {flag}, {self.device}")
|
|
116
125
|
|
|
126
|
+
# 创建设备对象
|
|
127
|
+
|
|
117
128
|
|
|
118
129
|
# 握手
|
|
119
130
|
class HandshakeCommand(DeviceCommand):
|
|
@@ -160,8 +171,6 @@ class SetAcquisitionParamCommand(DeviceCommand):
|
|
|
160
171
|
body += bytes.fromhex('00')
|
|
161
172
|
|
|
162
173
|
return body
|
|
163
|
-
def parse_body(self, body: bytes):
|
|
164
|
-
logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
|
|
165
174
|
|
|
166
175
|
# 启动采集
|
|
167
176
|
class StartAcquisitionCommand(DeviceCommand):
|
|
@@ -170,8 +179,6 @@ class StartAcquisitionCommand(DeviceCommand):
|
|
|
170
179
|
|
|
171
180
|
def pack_body(self):
|
|
172
181
|
return bytes.fromhex('0000')
|
|
173
|
-
def parse_body(self, body: bytes):
|
|
174
|
-
logger.info(f"Received acquisition start response: {body.hex()}")
|
|
175
182
|
|
|
176
183
|
# 停止采集
|
|
177
184
|
class StopAcquisitionCommand(DeviceCommand):
|
|
@@ -180,17 +187,14 @@ class StopAcquisitionCommand(DeviceCommand):
|
|
|
180
187
|
|
|
181
188
|
def pack_body(self):
|
|
182
189
|
return b''
|
|
183
|
-
|
|
184
|
-
logger.info(f"Received acquisition stop response: {body.hex()}")
|
|
190
|
+
|
|
185
191
|
|
|
186
192
|
# 设置阻抗采集参数
|
|
187
193
|
class SetImpedanceParamCommand(DeviceCommand):
|
|
188
194
|
cmd_code = 0x411
|
|
189
195
|
cmd_desc = "设置阻抗测量参数"
|
|
190
|
-
def parse_body(self, body: bytes):
|
|
191
|
-
logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
|
|
192
196
|
|
|
193
|
-
#
|
|
197
|
+
# 启动阻抗测量
|
|
194
198
|
class StartImpedanceCommand(DeviceCommand):
|
|
195
199
|
cmd_code = 0x412
|
|
196
200
|
cmd_desc = "启动阻抗测量"
|
|
@@ -200,19 +204,14 @@ class StartImpedanceCommand(DeviceCommand):
|
|
|
200
204
|
body += bytes.fromhex('0000000000000000') # 8字节占位符
|
|
201
205
|
return body
|
|
202
206
|
|
|
203
|
-
def parse_body(self, body: bytes):
|
|
204
|
-
logger.info(f"Received StartImpedanceCommand response: {body.hex()}")
|
|
205
207
|
|
|
206
|
-
#
|
|
208
|
+
# 停止阻抗测量
|
|
207
209
|
class StopImpedanceCommand(DeviceCommand):
|
|
208
210
|
cmd_code = 0x413
|
|
209
211
|
cmd_desc = "停止阻抗测量"
|
|
210
212
|
|
|
211
213
|
def pack_body(self):
|
|
212
214
|
return b''
|
|
213
|
-
|
|
214
|
-
def parse_body(self, body: bytes):
|
|
215
|
-
logger.info(f"Received StopImpedanceCommand response: {body.hex()}")
|
|
216
215
|
|
|
217
216
|
# 启动刺激
|
|
218
217
|
class StartStimulationCommand(DeviceCommand):
|
|
@@ -229,19 +228,36 @@ class StartStimulationCommand(DeviceCommand):
|
|
|
229
228
|
# error_channel - 8B
|
|
230
229
|
# error_channel= int.from_bytes(body[9:17], 'big')
|
|
231
230
|
channels = to_channels(body[9:17])
|
|
232
|
-
logger.
|
|
231
|
+
logger.success(f"通道 {channels} 刺激开始")
|
|
232
|
+
self.device.trigger(f"通道 {channels} 刺激开始")
|
|
233
233
|
# error_type - 1B
|
|
234
234
|
error_type = body[17]
|
|
235
235
|
|
|
236
|
-
#
|
|
236
|
+
# 停止刺激
|
|
237
237
|
class StopStimulationCommand(DeviceCommand):
|
|
238
238
|
cmd_code = 0x488
|
|
239
239
|
cmd_desc = "停止刺激"
|
|
240
|
-
|
|
240
|
+
|
|
241
|
+
# 启动刺激
|
|
242
|
+
class StopStimulationNotifyCommand(DeviceCommand):
|
|
243
|
+
cmd_code = 0x48D
|
|
244
|
+
cmd_desc = "停止刺激通知"
|
|
245
|
+
def pack_body(self):
|
|
246
|
+
return self.device.stim_paradigm.to_bytes()
|
|
247
|
+
# return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
|
|
241
248
|
def parse_body(self, body: bytes):
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
#
|
|
249
|
+
# time - 8B
|
|
250
|
+
time = int.from_bytes(body[0:8], 'little')
|
|
251
|
+
# result - 1B
|
|
252
|
+
result = body[8]
|
|
253
|
+
# error_channel - 8B
|
|
254
|
+
# error_channel= int.from_bytes(body[9:17], 'big')
|
|
255
|
+
channels = to_channels(body[9:17])
|
|
256
|
+
logger.success(f"通道 {channels} 刺激结束")
|
|
257
|
+
self.device.trigger(f"通道 {channels} 刺激结束", time)
|
|
258
|
+
# error_type - 1B
|
|
259
|
+
error_type = body[17]
|
|
260
|
+
# 刺激信息
|
|
245
261
|
class StimulationInfoCommand(DeviceCommand):
|
|
246
262
|
cmd_code = 0x48e
|
|
247
263
|
cmd_desc = "刺激告警信息"
|
|
@@ -254,9 +270,10 @@ class StimulationInfoCommand(DeviceCommand):
|
|
|
254
270
|
channels = to_channels(body[9:17])
|
|
255
271
|
# 保留位-8B
|
|
256
272
|
# error_type - 1B
|
|
273
|
+
err_type = body[17]
|
|
257
274
|
# 特征位-4B
|
|
258
|
-
|
|
259
|
-
logger.warning(f"[{
|
|
275
|
+
# errType = int.from_bytes(body[25:29], 'little')
|
|
276
|
+
logger.warning(f"刺激告警信息[{err_type}],通道 {channels} 刺激驱动不足")
|
|
260
277
|
|
|
261
278
|
|
|
262
279
|
# 阻抗数据
|
|
@@ -276,24 +293,28 @@ class SignalDataCommand(DeviceCommand):
|
|
|
276
293
|
def unpack(self, payload):
|
|
277
294
|
return super().unpack(payload)
|
|
278
295
|
|
|
279
|
-
def parse_body(self, body: bytes):
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
296
|
+
def parse_body(self, body: bytes):
|
|
297
|
+
# 解析数据包
|
|
298
|
+
packet = RscPacket()
|
|
299
|
+
packet.transfer(body)
|
|
300
|
+
|
|
301
|
+
# 文件写入到edf
|
|
302
|
+
if self.device.edf_handler:
|
|
303
|
+
self.device.edf_handler.write(packet)
|
|
304
|
+
|
|
305
|
+
if len(self.device.signal_consumers) > 0 :
|
|
306
|
+
# 信号数字值转物理值
|
|
307
|
+
packet.eeg = self.device.eeg2phy(np.array(packet.eeg))
|
|
284
308
|
|
|
285
309
|
# 发送数据包到订阅者
|
|
286
310
|
for q in list(self.device.signal_consumers.values()):
|
|
287
|
-
q.put(
|
|
311
|
+
q.put(packet)
|
|
288
312
|
|
|
289
|
-
|
|
290
|
-
if self.device.edf_handler:
|
|
291
|
-
self.device.edf_handler.append(rsc)
|
|
313
|
+
|
|
292
314
|
|
|
293
315
|
# =============================================================================
|
|
294
|
-
#
|
|
316
|
+
# 指令实现类注册到指令工厂
|
|
295
317
|
# =============================================================================
|
|
296
|
-
|
|
297
318
|
CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
|
|
298
319
|
CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
|
|
299
320
|
CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
|
|
@@ -34,6 +34,7 @@ class EdfHandler(object):
|
|
|
34
34
|
self._first_pkg_id = None
|
|
35
35
|
self._last_pkg_id = None
|
|
36
36
|
self._first_timestamp = None
|
|
37
|
+
self._first_time = None
|
|
37
38
|
self._end_time = None
|
|
38
39
|
self._patient_code = "patient_code"
|
|
39
40
|
self._patient_name = "patient_name"
|
|
@@ -77,6 +78,7 @@ class EdfHandler(object):
|
|
|
77
78
|
self.acc_channels = data.acc_ch_count
|
|
78
79
|
self._first_pkg_id = data.pkg_id
|
|
79
80
|
self._first_timestamp = data.time_stamp
|
|
81
|
+
self._first_time = datetime.now()
|
|
80
82
|
|
|
81
83
|
if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
|
|
82
84
|
self._lost_packets += data.pkg_id - self._last_pkg_id - 1
|
|
@@ -90,8 +92,25 @@ class EdfHandler(object):
|
|
|
90
92
|
if not self._recording:
|
|
91
93
|
self.start()
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
# trigger标记
|
|
96
|
+
# desc: 标记内容
|
|
97
|
+
# cur_time: 设备时间时间戳,非设备发出的trigger不要设置
|
|
98
|
+
def trigger(self, desc, cur_time=None):
|
|
99
|
+
if self._edf_writer is None:
|
|
100
|
+
logger.warning("EDF writer未初始化,无法写入trigger标记")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if cur_time is None:
|
|
104
|
+
onset = datetime.now().timestamp() - self._first_time.timestamp()
|
|
105
|
+
else:
|
|
106
|
+
onset = cur_time - self._first_timestamp
|
|
107
|
+
|
|
108
|
+
self._edf_writer.writeAnnotation(onset, 1, desc)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# def trigger(self, desc):
|
|
112
|
+
# if self._edf_writer:
|
|
113
|
+
# self._edf_writer.writeAnnotation(0, 1, desc)
|
|
95
114
|
|
|
96
115
|
def start(self):
|
|
97
116
|
self._recording = True
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from multiprocessing import Lock, Queue
|
|
3
|
+
from time import time_ns
|
|
4
|
+
from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
|
|
5
|
+
from threading import Thread
|
|
6
|
+
from loguru import logger
|
|
7
|
+
import numpy as np
|
|
8
|
+
import os
|
|
9
|
+
from qlsdk.core import RscPacket
|
|
10
|
+
|
|
11
|
+
EDF_FILE_TYPE = {
|
|
12
|
+
"bdf": FILETYPE_BDFPLUS,
|
|
13
|
+
"edf": FILETYPE_EDFPLUS
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class EDFStreamWriter(Thread):
|
|
17
|
+
def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._writer : EdfWriter = None
|
|
20
|
+
self.data_queue : Queue = Queue()
|
|
21
|
+
self._recording = False
|
|
22
|
+
self._points = 0
|
|
23
|
+
self._duration = 0
|
|
24
|
+
self._buffer = None
|
|
25
|
+
|
|
26
|
+
# signal info
|
|
27
|
+
self._channels = channels
|
|
28
|
+
self._n_channels = len(channels)
|
|
29
|
+
self.sample_frequency = sample_frequency
|
|
30
|
+
self.physical_max = physical_max
|
|
31
|
+
self.physical_min = digital_min
|
|
32
|
+
# 和位数相关,edf 16 bits/bdf 24 bits
|
|
33
|
+
self.digital_max = 8388607 if file_type == EDF_FILE_TYPE['bdf'] else 32767
|
|
34
|
+
self.digital_min = -8388608 if file_type == EDF_FILE_TYPE['bdf'] else -32768
|
|
35
|
+
self.file_type = file_type
|
|
36
|
+
self.file_path = file_path
|
|
37
|
+
|
|
38
|
+
# 记录开始时间
|
|
39
|
+
self.start_time = None
|
|
40
|
+
|
|
41
|
+
# header info
|
|
42
|
+
self.equipment = "equipment"
|
|
43
|
+
self.patient_code = "patient_code"
|
|
44
|
+
self.patient_name = "patient_name"
|
|
45
|
+
|
|
46
|
+
def set_channels(self, channels):
|
|
47
|
+
self._channels = channels
|
|
48
|
+
self._n_channels = len(channels)
|
|
49
|
+
|
|
50
|
+
def set_sample_rate(self, sample_rate):
|
|
51
|
+
self._sample_rate = sample_rate
|
|
52
|
+
|
|
53
|
+
def set_start_time(self, time):
|
|
54
|
+
self.start_time = time
|
|
55
|
+
|
|
56
|
+
def stop_recording(self):
|
|
57
|
+
self._recording = False
|
|
58
|
+
|
|
59
|
+
def append(self, data):
|
|
60
|
+
if data:
|
|
61
|
+
# 数据
|
|
62
|
+
self.data_queue.put(data)
|
|
63
|
+
|
|
64
|
+
def trigger(self, onset, desc):
|
|
65
|
+
if self._writer:
|
|
66
|
+
self._writer.writeAnnotation(onset, 1, desc)
|
|
67
|
+
else:
|
|
68
|
+
logger.warning("未创建文件,无法写入Trigger标记")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(self):
|
|
72
|
+
logger.debug(f"启动bdf文件写入线程,写入数据到文件 {self.file_path}")
|
|
73
|
+
# 记录状态
|
|
74
|
+
self._recording = True
|
|
75
|
+
|
|
76
|
+
# 初始化
|
|
77
|
+
if self._writer is None:
|
|
78
|
+
self._init_writer()
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
if self._recording or (not self.data_queue.empty()):
|
|
82
|
+
try:
|
|
83
|
+
data = self.data_queue.get(timeout=30)
|
|
84
|
+
if data is None:
|
|
85
|
+
logger.debug("收到结束信号,停止写入数据")
|
|
86
|
+
break
|
|
87
|
+
# 处理数据
|
|
88
|
+
self._points += len(data[1])
|
|
89
|
+
logger.trace(f"已处理数据点数:{self._points}")
|
|
90
|
+
self._write_file(data)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"异常或超时(30s)结束: {str(e)}")
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
95
|
+
logger.debug("数据记录完成")
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
self.close()
|
|
99
|
+
|
|
100
|
+
def _init_writer(self):
|
|
101
|
+
|
|
102
|
+
# 创建EDF+写入器
|
|
103
|
+
self._writer = EdfWriter(
|
|
104
|
+
self.file_path,
|
|
105
|
+
self._n_channels,
|
|
106
|
+
file_type=self.file_type
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 设置头信息
|
|
110
|
+
self._writer.setPatientCode(self.patient_code)
|
|
111
|
+
self._writer.setPatientName(self.patient_name)
|
|
112
|
+
self._writer.setEquipment(self.equipment)
|
|
113
|
+
self._writer.setStartdatetime(self.start_time if self.start_time else datetime.now())
|
|
114
|
+
|
|
115
|
+
# 配置通道参数
|
|
116
|
+
signal_headers = []
|
|
117
|
+
for ch in range(self._n_channels):
|
|
118
|
+
signal_headers.append({
|
|
119
|
+
"label": f'channels {self._channels[ch]}',
|
|
120
|
+
"dimension": 'uV',
|
|
121
|
+
"sample_frequency": self.sample_frequency,
|
|
122
|
+
"physical_min": self.physical_min,
|
|
123
|
+
"physical_max": self.physical_max,
|
|
124
|
+
"digital_min": self.digital_min,
|
|
125
|
+
"digital_max": self.digital_max
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
self._writer.setSignalHeaders(signal_headers)
|
|
129
|
+
|
|
130
|
+
def _write_file(self, eeg_data):
|
|
131
|
+
try:
|
|
132
|
+
if self._buffer is None or self._buffer.size == 0:
|
|
133
|
+
self._buffer = np.asarray(eeg_data)
|
|
134
|
+
else:
|
|
135
|
+
self._buffer = np.hstack((self._buffer, eeg_data))
|
|
136
|
+
|
|
137
|
+
if self._buffer.shape[1] >= self.sample_frequency:
|
|
138
|
+
block = self._buffer[:, :self.sample_frequency]
|
|
139
|
+
self._write_block(block)
|
|
140
|
+
self._buffer = self._buffer[:, self.sample_frequency:]
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"写入数据异常: {str(e)}")
|
|
144
|
+
|
|
145
|
+
def close(self):
|
|
146
|
+
self._recording = False
|
|
147
|
+
if self._writer:
|
|
148
|
+
self._writer.writeAnnotation(0, 1, "recording start")
|
|
149
|
+
self._writer.writeAnnotation(self._duration, 1, "recording end")
|
|
150
|
+
self._writer.close()
|
|
151
|
+
|
|
152
|
+
logger.info(f"文件: {self.file_path}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒")
|
|
153
|
+
|
|
154
|
+
# 写入1秒的数据
|
|
155
|
+
def _write_block(self, block):
|
|
156
|
+
logger.trace(f"写入数据: {block}")
|
|
157
|
+
# 转换数据类型为float64
|
|
158
|
+
data_float64 = block.astype(np.float64)
|
|
159
|
+
# 写入时转置为(样本数, 通道数)格式
|
|
160
|
+
self._writer.writeSamples(data_float64)
|
|
161
|
+
self._duration += 1
|
|
162
|
+
|
|
163
|
+
# 用作数据结构一致化处理,通过调用公共类写入edf文件
|
|
164
|
+
# 入参包含写入edf的全部前置参数
|
|
165
|
+
# 实时数据包为个性化数据包,含有eeg数据部分
|
|
166
|
+
class RscEDFHandler(object):
|
|
167
|
+
'''
|
|
168
|
+
Rsc EDF Handler
|
|
169
|
+
处理EDF文件的读写
|
|
170
|
+
RSC设备通道数根据选择变化,不同通道采样频率相同
|
|
171
|
+
eeg_sample_rate: 采样频率
|
|
172
|
+
physical_max: 物理最大值 (uV)
|
|
173
|
+
physical_min: 物理最小值 (uV)
|
|
174
|
+
resolution: 分辨率
|
|
175
|
+
storage_path: 存储路径
|
|
176
|
+
|
|
177
|
+
@author: qlsdk
|
|
178
|
+
@since: 0.4.0
|
|
179
|
+
'''
|
|
180
|
+
def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
|
|
181
|
+
# edf文件参数
|
|
182
|
+
self.physical_max = physical_max
|
|
183
|
+
self.physical_min = physical_min
|
|
184
|
+
self.digital_max = 8388607 if resolution == 24 else 32767
|
|
185
|
+
self.digital_min = -8388607 if resolution == 24 else - 32768
|
|
186
|
+
self.file_type = EDF_FILE_TYPE["bdf"] if resolution == 24 else EDF_FILE_TYPE["edf"]
|
|
187
|
+
# 点分辨率
|
|
188
|
+
self.resolution = resolution
|
|
189
|
+
# eeg通道数
|
|
190
|
+
self.channels = None
|
|
191
|
+
# eeg采样率
|
|
192
|
+
self.sample_rate = eeg_sample_rate
|
|
193
|
+
# bytes per second
|
|
194
|
+
self.bytes_per_second = 0
|
|
195
|
+
self._edf_writer = None
|
|
196
|
+
self._cache2 = tuple()
|
|
197
|
+
self._recording = False
|
|
198
|
+
self._edf_writer = None
|
|
199
|
+
self.annotations = None
|
|
200
|
+
# 每个数据块大小
|
|
201
|
+
self._chunk = np.array([])
|
|
202
|
+
self._Lock = Lock()
|
|
203
|
+
self._duration = 0
|
|
204
|
+
self._points = 0
|
|
205
|
+
self._first_pkg_id = None
|
|
206
|
+
self._last_pkg_id = None
|
|
207
|
+
self._first_timestamp = None
|
|
208
|
+
self._start_time = None
|
|
209
|
+
self._end_time = None
|
|
210
|
+
self._patient_code = "patient_code"
|
|
211
|
+
self._patient_name = "patient_name"
|
|
212
|
+
self._device_type = "24130032"
|
|
213
|
+
self._device_no = "24130032"
|
|
214
|
+
self._total_packets = 0
|
|
215
|
+
self._lost_packets = 0
|
|
216
|
+
self._storage_path = storage_path
|
|
217
|
+
self._edf_writer_thread = None
|
|
218
|
+
self._file_prefix = None
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def file_name(self):
|
|
222
|
+
suffix = "bdf" if self.resolution == 24 else "edf"
|
|
223
|
+
|
|
224
|
+
# 文件名称
|
|
225
|
+
file_name = f"{self._file_prefix}_{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}" if self._file_prefix else f"{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}"
|
|
226
|
+
|
|
227
|
+
if self._storage_path:
|
|
228
|
+
try:
|
|
229
|
+
# 自动创建目录,存在则忽略
|
|
230
|
+
os.makedirs(self._storage_path, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
return f"{self._storage_path}/{file_name}"
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"创建目录[{self._storage_path}]失败: {e}")
|
|
235
|
+
|
|
236
|
+
return file_name
|
|
237
|
+
|
|
238
|
+
def set_device_type(self, device_type):
|
|
239
|
+
if device_type == 0x39:
|
|
240
|
+
self._device_type = "C64RS"
|
|
241
|
+
elif device_type == 0x40:
|
|
242
|
+
self._device_type = "LJ64S1"
|
|
243
|
+
else:
|
|
244
|
+
self._device_type = hex(device_type)
|
|
245
|
+
|
|
246
|
+
def set_device_no(self, device_no):
|
|
247
|
+
self._device_no = device_no
|
|
248
|
+
|
|
249
|
+
def set_storage_path(self, storage_path):
|
|
250
|
+
self._storage_path = storage_path
|
|
251
|
+
|
|
252
|
+
def set_file_prefix(self, file_prefix):
|
|
253
|
+
self._file_prefix = file_prefix
|
|
254
|
+
|
|
255
|
+
def set_patient_code(self, patient_code):
|
|
256
|
+
self._patient_code = patient_code
|
|
257
|
+
|
|
258
|
+
def set_patient_name(self, patient_name):
|
|
259
|
+
self._patient_name = patient_name
|
|
260
|
+
|
|
261
|
+
def write(self, packet: RscPacket):
|
|
262
|
+
logger.debug(f"packet: {packet}")
|
|
263
|
+
if packet is None:
|
|
264
|
+
self._edf_writer_thread.stop_recording()
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
if self.channels is None:
|
|
268
|
+
logger.info(f"开始记录数据到文件...")
|
|
269
|
+
self.channels = packet.channels
|
|
270
|
+
self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
|
|
271
|
+
self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
|
|
272
|
+
self._start_time = datetime.now()
|
|
273
|
+
logger.info(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
|
|
274
|
+
|
|
275
|
+
if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
|
|
276
|
+
self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
|
|
277
|
+
logger.warning(f"数据包丢失: {self._last_pkg_id} -> {packet.pkg_id}, 丢包数: {packet.pkg_id - self._last_pkg_id - 1}")
|
|
278
|
+
|
|
279
|
+
self._last_pkg_id = packet.pkg_id
|
|
280
|
+
self._total_packets += 1
|
|
281
|
+
|
|
282
|
+
if self._edf_writer_thread is None:
|
|
283
|
+
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
|
|
284
|
+
self._edf_writer_thread.set_start_time(self._start_time)
|
|
285
|
+
self._edf_writer_thread.start()
|
|
286
|
+
logger.info(f"开始写入数据: {self.file_name}")
|
|
287
|
+
|
|
288
|
+
self._edf_writer_thread.append(packet.eeg)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# trigger标记
|
|
292
|
+
# desc: 标记内容
|
|
293
|
+
# cur_time: 设备时间时间戳,非设备发出的trigger不要设置
|
|
294
|
+
def trigger(self, desc, cur_time=None):
|
|
295
|
+
if cur_time is None:
|
|
296
|
+
onset = datetime.now().timestamp() - self._start_time.timestamp()
|
|
297
|
+
else:
|
|
298
|
+
onset = cur_time - self._first_timestamp
|
|
299
|
+
self._edf_writer_thread.trigger(onset, desc)
|
|
300
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .network.discover import *
|
|
2
|
+
|
|
3
|
+
from .entity import DeviceParser, QLDevice
|
|
4
|
+
from .command import *
|
|
5
|
+
# from .device_manager import DeviceContainer
|
|
6
|
+
from .proxy import DeviceProxy
|
|
7
|
+
from .paradigm import *
|
|
8
|
+
from .manager import DeviceContainer
|
|
9
|
+
from .network import *
|