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.
Files changed (67) hide show
  1. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/PKG-INFO +1 -1
  2. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/setup.py +1 -1
  3. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/ar4/__init__.py +9 -5
  4. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/device.py +10 -1
  5. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/command.py +56 -35
  6. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/persist/edf.py +21 -2
  7. qlsdk2-0.4.1/src/qlsdk/persist/rsc_edf.py +300 -0
  8. qlsdk2-0.4.1/src/qlsdk/rsc/__init__.py +9 -0
  9. qlsdk2-0.4.1/src/qlsdk/rsc/command/__init__.py +336 -0
  10. qlsdk2-0.4.1/src/qlsdk/rsc/command/message.py +336 -0
  11. qlsdk2-0.4.1/src/qlsdk/rsc/device/__init__.py +2 -0
  12. qlsdk2-0.4.1/src/qlsdk/rsc/device/base.py +388 -0
  13. qlsdk2-0.4.1/src/qlsdk/rsc/device/c64_rs.py +364 -0
  14. qlsdk2-0.4.1/src/qlsdk/rsc/device/device_factory.py +29 -0
  15. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/entity.py +63 -24
  16. qlsdk2-0.4.1/src/qlsdk/rsc/interface/__init__.py +3 -0
  17. qlsdk2-0.4.1/src/qlsdk/rsc/interface/command.py +10 -0
  18. qlsdk2-0.4.1/src/qlsdk/rsc/interface/device.py +107 -0
  19. qlsdk2-0.4.1/src/qlsdk/rsc/interface/handler.py +9 -0
  20. qlsdk2-0.4.1/src/qlsdk/rsc/interface/parser.py +9 -0
  21. qlsdk2-0.4.1/src/qlsdk/rsc/manager/__init__.py +2 -0
  22. qlsdk2-0.4.0a3/src/qlsdk/rsc/device_manager.py → qlsdk2-0.4.1/src/qlsdk/rsc/manager/container.py +15 -13
  23. qlsdk2-0.4.1/src/qlsdk/rsc/manager/search.py +0 -0
  24. qlsdk2-0.4.1/src/qlsdk/rsc/network/__init__.py +1 -0
  25. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/paradigm.py +1 -1
  26. qlsdk2-0.4.1/src/qlsdk/rsc/parser/__init__.py +1 -0
  27. qlsdk2-0.4.1/src/qlsdk/rsc/parser/base.py +66 -0
  28. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/ar4sdk.py +13 -4
  29. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/x8/__init__.py +4 -0
  30. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/PKG-INFO +1 -1
  31. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/SOURCES.txt +16 -2
  32. qlsdk2-0.4.0a3/src/qlsdk/persist/rsc_edf.py +0 -241
  33. qlsdk2-0.4.0a3/src/qlsdk/rsc/__init__.py +0 -7
  34. qlsdk2-0.4.0a3/src/qlsdk/rsc/command/__init__.py +0 -214
  35. qlsdk2-0.4.0a3/src/qlsdk/rsc/command/message.py +0 -239
  36. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/README.md +0 -0
  37. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/setup.cfg +0 -0
  38. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/__init__.py +0 -0
  39. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/ar4m/__init__.py +0 -0
  40. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/__init__.py +0 -0
  41. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/crc/__init__.py +0 -0
  42. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/crc/crctools.py +0 -0
  43. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/entity/__init__.py +0 -0
  44. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/exception.py +0 -0
  45. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/filter/__init__.py +0 -0
  46. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/filter/norch.py +0 -0
  47. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/local.py +0 -0
  48. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/__init__.py +0 -0
  49. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/tcp.py +0 -0
  50. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/message/udp.py +0 -0
  51. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/network/__init__.py +0 -0
  52. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/network/monitor.py +0 -0
  53. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/core/utils.py +0 -0
  54. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/persist/__init__.py +0 -0
  55. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/eegion.py +0 -0
  56. {qlsdk2-0.4.0a3/src/qlsdk/rsc → qlsdk2-0.4.1/src/qlsdk/rsc/network}/discover.py +0 -0
  57. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/rsc/proxy.py +0 -0
  58. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/__init__.py +0 -0
  59. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/hub.py +0 -0
  60. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
  61. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
  62. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk/x8m/__init__.py +0 -0
  63. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
  64. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/requires.txt +0 -0
  65. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/src/qlsdk2.egg-info/top_level.txt +0 -0
  66. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/test/test.py +0 -0
  67. {qlsdk2-0.4.0a3 → qlsdk2-0.4.1}/test/test_ar4m.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qlsdk2
3
- Version: 0.4.0a3
3
+ Version: 0.4.1
4
4
  Summary: SDK for quanlan device
5
5
  Home-page: https://github.com/hehuajun/qlsdk
6
6
  Author: hehuajun
@@ -6,7 +6,7 @@ with open("README.md", "r") as fh:
6
6
 
7
7
  setuptools.setup(
8
8
  name="qlsdk2",
9
- version="0.4.0a3",
9
+ version="0.4.1",
10
10
  author="hehuajun",
11
11
  author_email="hehuajun@eegion.com",
12
12
  description="SDK for quanlan device",
@@ -39,12 +39,12 @@ class AR4(LMDevice):
39
39
  consumer.put(packet)
40
40
 
41
41
  if len(self._phy_subscriber) > 0:
42
- logger.info(f"dig data eeg: {packet.eeg}")
43
- logger.info(f"dig data acc: {packet.acc}")
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.info(f"phy data eeg: {packet.eeg}")
47
- logger.info(f"phy data acc: {packet.acc}")
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.device_type = None
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
- def parse_body(self, body: bytes):
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.warning(f"通道 {channels} 刺激开始")
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
- logger.info(f"Received stimulation stop response: {body.hex()}")
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
- characteristic = int.from_bytes(body[25:29], 'little')
259
- logger.warning(f"[{characteristic}]刺激告警,通道 {channels} 刺激驱动不足")
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
- if len(self.device.signal_consumers) > 0 or self.device.edf_handler:
281
- # 解析数据包
282
- rsc = RscPacket()
283
- rsc.transfer(body)
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(rsc)
311
+ q.put(packet)
288
312
 
289
- # 文件写入到edf
290
- if self.device.edf_handler:
291
- self.device.edf_handler.append(rsc)
313
+
292
314
 
293
315
  # =============================================================================
294
- # Command Registration
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
- def trigger(self, data):
94
- pass
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 *