qlsdk2 0.4.0a3__py3-none-any.whl → 0.4.2__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.
Files changed (41) hide show
  1. qlsdk/ar4/__init__.py +9 -5
  2. qlsdk/core/device.py +10 -1
  3. qlsdk/core/entity/__init__.py +1 -0
  4. qlsdk/core/message/command.py +56 -35
  5. qlsdk/core/utils.py +20 -15
  6. qlsdk/persist/__init__.py +2 -1
  7. qlsdk/persist/ars_edf.py +278 -0
  8. qlsdk/persist/edf.py +21 -2
  9. qlsdk/persist/rsc_edf.py +187 -124
  10. qlsdk/persist/stream.py +161 -0
  11. qlsdk/rsc/__init__.py +4 -2
  12. qlsdk/rsc/command/__init__.py +177 -61
  13. qlsdk/rsc/command/message.py +171 -74
  14. qlsdk/rsc/device/__init__.py +2 -0
  15. qlsdk/rsc/device/arskindling.py +384 -0
  16. qlsdk/rsc/device/base.py +413 -0
  17. qlsdk/rsc/device/c256_rs.py +364 -0
  18. qlsdk/rsc/device/c64_rs.py +358 -0
  19. qlsdk/rsc/device/c64s1.py +364 -0
  20. qlsdk/rsc/device/device_factory.py +33 -0
  21. qlsdk/rsc/entity.py +63 -24
  22. qlsdk/rsc/interface/__init__.py +3 -0
  23. qlsdk/rsc/interface/command.py +10 -0
  24. qlsdk/rsc/interface/device.py +114 -0
  25. qlsdk/rsc/interface/handler.py +9 -0
  26. qlsdk/rsc/interface/parser.py +12 -0
  27. qlsdk/rsc/manager/__init__.py +2 -0
  28. qlsdk/rsc/manager/container.py +126 -0
  29. qlsdk/rsc/manager/search.py +0 -0
  30. qlsdk/rsc/network/__init__.py +1 -0
  31. qlsdk/rsc/network/discover.py +87 -0
  32. qlsdk/rsc/paradigm.py +1 -1
  33. qlsdk/rsc/parser/__init__.py +1 -0
  34. qlsdk/rsc/parser/base.py +69 -0
  35. qlsdk/sdk/ar4sdk.py +13 -4
  36. qlsdk/x8/__init__.py +4 -0
  37. {qlsdk2-0.4.0a3.dist-info → qlsdk2-0.4.2.dist-info}/METADATA +1 -1
  38. qlsdk2-0.4.2.dist-info/RECORD +63 -0
  39. qlsdk2-0.4.0a3.dist-info/RECORD +0 -42
  40. {qlsdk2-0.4.0a3.dist-info → qlsdk2-0.4.2.dist-info}/WHEEL +0 -0
  41. {qlsdk2-0.4.0a3.dist-info → qlsdk2-0.4.2.dist-info}/top_level.txt +0 -0
qlsdk/ar4/__init__.py CHANGED
@@ -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:
qlsdk/core/device.py CHANGED
@@ -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
@@ -26,6 +26,7 @@ class RscPacket(object):
26
26
  self.time_stamp = int.from_bytes(body[0:8], 'little')
27
27
  self.result = body[8]
28
28
  self.pkg_id = int.from_bytes(body[9: 13], 'little')
29
+ logger.trace(f"pkg_id: {self.pkg_id}")
29
30
  self.channels = to_channels(body[13: 45])
30
31
  self.origin_sample_rate = int.from_bytes(body[45: 49], 'little')
31
32
  self.sample_rate = int.from_bytes(body[49: 53], 'little')
@@ -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)
qlsdk/core/utils.py CHANGED
@@ -52,19 +52,24 @@ def bytes_to_ints2(b):
52
52
  import numpy as np
53
53
  if __name__ == "__main__":
54
54
 
55
- channels = [1]
56
- channels1 = [1, 2, 3, 4]
57
- channels2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]
58
- channels3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145,146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207,208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256]
59
- channels4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
60
- logger.info(to_bytes(channels).hex())
61
- logger.info(to_bytes(channels1).hex())
62
- logger.info(to_bytes(channels2).hex())
63
- logger.info(to_bytes(channels3).hex())
64
- logger.info(to_bytes(channels4).hex())
55
+ # channels = [1]
56
+ # channels1 = [1, 2, 3, 4]
57
+ # channels2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]
58
+ # channels3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145,146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207,208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256]
59
+ # channels4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
60
+ # logger.info(to_bytes(channels).hex())
61
+ # logger.info(to_bytes(channels1).hex())
62
+ # logger.info(to_bytes(channels2).hex())
63
+ # logger.info(to_bytes(channels3).hex())
64
+ # logger.info(to_bytes(channels4).hex())
65
65
 
66
- bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
67
- bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
68
- bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
69
- logger.info(to_channels(bytes.fromhex(bs1)))
70
- logger.info(to_channels(bytes.fromhex(bs2)))
66
+ # bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
67
+ # bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
68
+ # bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
69
+
70
+ # logger.info(to_channels(bytes.fromhex(bs1)))
71
+ # logger.info(to_channels(bytes.fromhex(bs2)))
72
+
73
+ aa = 'ff3fff3fff3fff3f000000000000000000000000000000000000000000000000'
74
+
75
+ logger.info(to_channels(bytes.fromhex(aa)))
qlsdk/persist/__init__.py CHANGED
@@ -1,2 +1,3 @@
1
1
  from .edf import EdfHandler
2
- from .rsc_edf import RscEDFHandler
2
+ from .rsc_edf import RscEDFHandler
3
+ from .ars_edf import ARSKindlingEDFHandler
@@ -0,0 +1,278 @@
1
+ from datetime import datetime
2
+ import os
3
+ from threading import Lock
4
+ from loguru import logger
5
+ import numpy as np
6
+
7
+ from qlsdk.core.entity import RscPacket
8
+ from qlsdk.persist.rsc_edf import RscEDFHandler
9
+ from qlsdk.persist.stream import EDF_FILE_TYPE, EDFStreamWriter
10
+
11
+
12
+ def intersection_positions(A, B):
13
+ setB = set(B)
14
+ seen = set()
15
+ return [idx for idx, elem in enumerate(A)
16
+ if elem in setB and elem not in seen and not seen.add(elem)]
17
+
18
+ # 用作数据结构一致化处理,通过调用公共类写入edf文件
19
+ # 入参包含写入edf的全部前置参数
20
+ # 实时数据包为个性化数据包,含有eeg数据部分
21
+ class ARSKindlingEDFHandler(object):
22
+ '''
23
+ Rsc EDF Handler
24
+ 处理EDF文件的读写
25
+ RSC设备通道数根据选择变化,不同通道采样频率相同
26
+ eeg_sample_rate: 采样频率
27
+ physical_max: 物理最大值 (uV)
28
+ physical_min: 物理最小值 (uV)
29
+ resolution: 分辨率
30
+ storage_path: 存储路径
31
+
32
+ @author: qlsdk
33
+ @since: 0.4.0
34
+ '''
35
+ def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
36
+ # edf文件参数
37
+ self.physical_max = physical_max
38
+ self.physical_min = physical_min
39
+ self.digital_max = 8388607 if resolution == 24 else 32767
40
+ self.digital_min = -8388607 if resolution == 24 else - 32768
41
+ self.file_type = EDF_FILE_TYPE["bdf"] if resolution == 24 else EDF_FILE_TYPE["edf"]
42
+ # 点分辨率
43
+ self.resolution = resolution
44
+ # eeg通道数
45
+ self.channels = None
46
+ # eeg采样率
47
+ self.sample_rate = eeg_sample_rate
48
+ # bytes per second
49
+ self.bytes_per_second = 0
50
+ self._edf_writer = None
51
+ self._cache2 = tuple()
52
+ self._recording = False
53
+ self._edf_writer = None
54
+ self.annotations = None
55
+ # 每个数据块大小
56
+ self._chunk = np.array([])
57
+ self._duration = 0
58
+ self._points = 0
59
+ self._first_pkg_id = None
60
+ self._last_pkg_id = None
61
+ self._first_timestamp = None
62
+ self._start_time = None
63
+ self._end_time = None
64
+ self._patient_code = "patient_code"
65
+ self._patient_name = "patient_name"
66
+ self._device_type = "24130032"
67
+ self._device_no = "24130032"
68
+ self._total_packets = 0
69
+ self._lost_packets = 0
70
+ self._storage_path = storage_path
71
+ self._edf_writer_thread = None
72
+ self._file_prefix = None
73
+
74
+ # 癫痫造模仪1对4, 这个是固定模式,写死,规则随设备
75
+ self._edf_handler = {}
76
+ self._edf_Handler_A = None
77
+ self._edf_Handler_B = None
78
+ self._edf_Handler_C = None
79
+ self._edf_Handler_D = None
80
+
81
+ self._channel_spilt = {
82
+ "A" : [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62],
83
+ "B" : [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46],
84
+ "C" : [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
85
+ "D" : [7, 8, 5, 6, 3, 4, 1, 2, 9, 10, 11, 12, 13, 14],
86
+ }
87
+
88
+ self._channel_mapping = {
89
+ 1: "D7",
90
+ 2: "D8",
91
+ 3: "D5",
92
+ 4: "D6",
93
+ 5: "D3",
94
+ 6: "D4",
95
+ 7: "D1",
96
+ 8: "D2",
97
+ 9: "D9",
98
+ 10: "D10",
99
+ 11: "D11",
100
+ 12: "D12",
101
+ 13: "D13",
102
+ 14: "D14",
103
+
104
+ 17: "C7",
105
+ 18: "C8",
106
+ 19: "C5",
107
+ 20: "C6",
108
+ 21: "C3",
109
+ 22: "C4",
110
+ 23: "C1",
111
+ 24: "C2",
112
+ 25: "C9",
113
+ 26: "C10",
114
+ 27: "C11",
115
+ 28: "C12",
116
+ 29: "C13",
117
+ 30: "C14",
118
+
119
+ 33: "B7",
120
+ 34: "B8",
121
+ 35: "B5",
122
+ 36: "B6",
123
+ 37: "B3",
124
+ 38: "B4",
125
+ 39: "B1",
126
+ 40: "B2",
127
+ 41: "B9",
128
+ 42: "B10",
129
+ 43: "B11",
130
+ 44: "B12",
131
+ 45: "B13",
132
+ 46: "B14",
133
+
134
+ 49: "A7",
135
+ 50: "A8",
136
+ 51: "A5",
137
+ 52: "A6",
138
+ 53: "A3",
139
+ 54: "A4",
140
+ 55: "A1",
141
+ 56: "A2",
142
+ 57: "A9",
143
+ 58: "A10",
144
+ 59: "A11",
145
+ 60: "A12",
146
+ 61: "A13",
147
+ 62: "A14"
148
+ }
149
+
150
+ self._lock = Lock()
151
+
152
+ @property
153
+ def file_name(self):
154
+ suffix = "bdf" if self.resolution == 24 else "edf"
155
+
156
+ # 文件名称
157
+ 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}"
158
+
159
+ if self._storage_path:
160
+ try:
161
+ # 自动创建目录,存在则忽略
162
+ os.makedirs(self._storage_path, exist_ok=True)
163
+
164
+ return f"{self._storage_path}/{file_name}"
165
+ except Exception as e:
166
+ logger.error(f"创建目录[{self._storage_path}]失败: {e}")
167
+
168
+ return file_name
169
+
170
+ def set_device_type(self, device_type):
171
+ if device_type == 0x39:
172
+ self._device_type = "C64RS"
173
+ elif device_type == 0x40:
174
+ self._device_type = "LJ64S1"
175
+ else:
176
+ self._device_type = hex(device_type)
177
+
178
+ def set_device_no(self, device_no):
179
+ self._device_no = device_no
180
+
181
+ def set_storage_path(self, storage_path):
182
+ self._storage_path = storage_path
183
+
184
+ def set_file_prefix(self, file_prefix):
185
+ self._file_prefix = file_prefix
186
+
187
+ def set_patient_code(self, patient_code):
188
+ self._patient_code = patient_code
189
+
190
+ def set_patient_name(self, patient_name):
191
+ self._patient_name = patient_name
192
+
193
+ def write(self, packet: RscPacket):
194
+ # logger.trace(f"packet: {packet}")
195
+ if packet is None:
196
+ # self._edf_writer_thread.stop_recording()
197
+ for k in self._edf_handler.keys():
198
+ self._edf_handler[k].append(None)
199
+ return
200
+
201
+ #按分区写入数据
202
+
203
+ for k in self._channel_spilt.keys():
204
+ logger.info(f'分区{k}, {self._channel_spilt[k]}')
205
+ p = packet
206
+ self.writeA(p, self._channel_spilt[k], k)
207
+
208
+ # with self._lock:
209
+ # if self.channels is None:
210
+ # logger.info(f"开始记录数据到文件...")
211
+ # self.channels = packet.channels
212
+ # self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
213
+ # self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
214
+ # self._start_time = datetime.now()
215
+ # logger.info(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
216
+
217
+ # if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
218
+ # self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
219
+ # logger.warning(f"数据包丢失: {self._last_pkg_id} -> {packet.pkg_id}, 丢包数: {packet.pkg_id - self._last_pkg_id - 1}")
220
+
221
+ # self._last_pkg_id = packet.pkg_id
222
+ # self._total_packets += 1
223
+
224
+ # if self._edf_writer_thread is None:
225
+ # self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
226
+ # self._edf_writer_thread.set_start_time(self._start_time)
227
+ # self._edf_writer_thread.start()
228
+ # logger.info(f"开始写入数据: {self.file_name}")
229
+
230
+ # self._edf_writer_thread.append(packet.eeg)
231
+
232
+ def writeA(self, packet: RscPacket, channel_filter, name='A'):
233
+ # 参数检查
234
+ if packet is None or channel_filter is None:
235
+ logger.warning("空数据,忽略")
236
+ return
237
+
238
+ channel_pos = intersection_positions(packet.channels, channel_filter)
239
+
240
+ if channel_pos is None or len(channel_pos) == 0 :
241
+ logger.debug(f"没有指定分区{name}的通道,跳过")
242
+ pass
243
+
244
+ # 分区数据包写入
245
+ if name not in self._edf_handler.keys():
246
+ edf_handler = RscEDFHandler(self.sample_rate, self.digital_max , self.digital_min, self.resolution)
247
+ edf_handler.set_device_type(self._device_type)
248
+ edf_handler.set_device_no(self._device_no)
249
+ edf_handler.set_storage_path(self._storage_path)
250
+ edf_handler.set_file_prefix(f'{self._file_prefix}_{name}' if self._file_prefix else name)
251
+ logger.info(f"开始写入分区{name}的数据到文件")
252
+ self._edf_handler[name] = edf_handler
253
+
254
+ # 保留本分区的通道和数据
255
+ channels = [packet.channels[p] for p in channel_pos]
256
+ eeg = [packet.eeg[p] for p in channel_pos]
257
+
258
+ # 新建数据包实例
259
+ data = RscPacket()
260
+ data.time_stamp = packet.time_stamp
261
+ data.pkg_id = packet.pkg_id
262
+ data.channels = channels
263
+ data.origin_sample_rate = packet.origin_sample_rate
264
+ data.sample_rate = packet.sample_rate
265
+ data.sample_num = packet.sample_num
266
+ data.resolution = packet.resolution
267
+ data.trigger = packet.trigger
268
+ data.eeg = eeg
269
+
270
+ self._edf_handler[name].write(data)
271
+
272
+ # trigger标记
273
+ # desc: 标记内容
274
+ # cur_time: 设备时间时间戳,非设备发出的trigger不要设置
275
+ def trigger(self, desc, cur_time=None):
276
+ # trigger现在(20250702)多个分区共享, 分发到所有分区文件中标记
277
+ for k in self._edf_handler.keys():
278
+ self._edf_handler[k].trigger(desc, cur_time)
qlsdk/persist/edf.py CHANGED
@@ -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