qlsdk2 0.4.0a2__tar.gz → 0.4.0a3__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 (49) hide show
  1. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/PKG-INFO +2 -2
  2. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/setup.py +1 -1
  3. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/__init__.py +2 -1
  4. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/entity/__init__.py +1 -1
  5. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/command.py +45 -26
  6. qlsdk2-0.4.0a3/src/qlsdk/core/network/__init__.py +34 -0
  7. qlsdk2-0.4.0a3/src/qlsdk/core/network/monitor.py +55 -0
  8. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/utils.py +2 -0
  9. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/persist/rsc_edf.py +34 -29
  10. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/device_manager.py +2 -0
  11. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/entity.py +36 -159
  12. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/paradigm.py +4 -3
  13. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/PKG-INFO +2 -2
  14. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/SOURCES.txt +2 -0
  15. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/requires.txt +1 -1
  16. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/README.md +0 -0
  17. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/setup.cfg +0 -0
  18. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/__init__.py +0 -0
  19. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/ar4/__init__.py +0 -0
  20. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/ar4m/__init__.py +0 -0
  21. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/crc/__init__.py +0 -0
  22. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/crc/crctools.py +0 -0
  23. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/device.py +0 -0
  24. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/exception.py +0 -0
  25. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/filter/__init__.py +0 -0
  26. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/filter/norch.py +0 -0
  27. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/local.py +0 -0
  28. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/__init__.py +0 -0
  29. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/tcp.py +0 -0
  30. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/udp.py +0 -0
  31. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/persist/__init__.py +0 -0
  32. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/persist/edf.py +0 -0
  33. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/__init__.py +0 -0
  34. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/command/__init__.py +0 -0
  35. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/command/message.py +0 -0
  36. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/discover.py +0 -0
  37. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/eegion.py +0 -0
  38. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/proxy.py +0 -0
  39. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/__init__.py +0 -0
  40. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/ar4sdk.py +0 -0
  41. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/hub.py +0 -0
  42. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
  43. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
  44. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/x8/__init__.py +0 -0
  45. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk/x8m/__init__.py +0 -0
  46. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
  47. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/top_level.txt +0 -0
  48. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/test/test.py +0 -0
  49. {qlsdk2-0.4.0a2 → qlsdk2-0.4.0a3}/test/test_ar4m.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qlsdk2
3
- Version: 0.4.0a2
3
+ Version: 0.4.0a3
4
4
  Summary: SDK for quanlan device
5
5
  Home-page: https://github.com/hehuajun/qlsdk
6
6
  Author: hehuajun
@@ -12,7 +12,7 @@ Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: loguru>=0.6.0
14
14
  Requires-Dist: numpy>=1.23.5
15
- Requires-Dist: pyedflib>=0.1.40
15
+ Requires-Dist: bitarray>=1.5.3
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=6.0; extra == "dev"
18
18
  Requires-Dist: twine>=3.0; extra == "dev"
@@ -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.0a2",
9
+ version="0.4.0a3",
10
10
  author="hehuajun",
11
11
  author_email="hehuajun@eegion.com",
12
12
  description="SDK for quanlan device",
@@ -1,4 +1,5 @@
1
1
  from qlsdk.core.crc import *
2
2
  from qlsdk.core.message import *
3
3
  from qlsdk.core.local import *
4
- from qlsdk.core.utils import *
4
+ from qlsdk.core.utils import *
5
+ from qlsdk.core.entity import *
@@ -46,7 +46,7 @@ class RscPacket(object):
46
46
  for i in range(ch_num)
47
47
  ]
48
48
 
49
- logger.trace(self)
49
+ # logger.trace(self)
50
50
  return self
51
51
 
52
52
  def __str__(self):
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ from time import time_ns
2
3
  from typing import Dict, Type
3
4
  from enum import Enum
4
5
  from loguru import logger
@@ -26,6 +27,10 @@ class DeviceCommand(abc.ABC):
26
27
  @abc.abstractmethod
27
28
  def cmd_code(self) -> int:
28
29
  pass
30
+ @property
31
+ @abc.abstractmethod
32
+ def cmd_desc(self) -> str:
33
+ pass
29
34
 
30
35
  @staticmethod
31
36
  def checksum(data: bytes) -> bytes:
@@ -70,15 +75,16 @@ class CommandFactory:
70
75
 
71
76
  @classmethod
72
77
  def create_command(cls, code: int) -> Type[DeviceCommand]:
73
- logger.debug(f"Creating command for code: {hex(code)}")
78
+ logger.trace(f"Creating command for code: {hex(code)}")
74
79
  if code not in cls._commands:
75
- logger.warning(f"Unsupported command code: {hex(code)}")
80
+ logger.warning(f"不支持的设备指令: {hex(code)}")
76
81
  return cls._commands[DefaultCommand.cmd_code]
77
82
  return cls._commands[code]
78
83
 
79
84
  # =============================================================================
80
85
  class DefaultCommand(DeviceCommand):
81
86
  cmd_code = 0x00
87
+ cmd_desc = "未定义"
82
88
 
83
89
  def parse_body(self, body: bytes):
84
90
  # Response parsing example: 2 bytes version + 4 bytes serial
@@ -86,9 +92,9 @@ class DefaultCommand(DeviceCommand):
86
92
 
87
93
  class GetDeviceInfoCommand(DeviceCommand):
88
94
  cmd_code = 0x17
95
+ cmd_desc = "设备信息"
89
96
 
90
97
  def parse_body(self, body: bytes):
91
- logger.info(f"Received GetDeviceInfoCommand body len: {len(body)}")
92
98
  # time - 8B
93
99
  self.device.connect_time = int.from_bytes(body[0:8], 'little')
94
100
  self.device.current_time = self.device.connect_time
@@ -112,6 +118,7 @@ class GetDeviceInfoCommand(DeviceCommand):
112
118
  # 握手
113
119
  class HandshakeCommand(DeviceCommand):
114
120
  cmd_code = 0x01
121
+ cmd_desc = "握手"
115
122
 
116
123
  def parse_body(self, body: bytes):
117
124
  logger.info(f"Received handshake response: {body.hex()}")
@@ -119,8 +126,8 @@ class HandshakeCommand(DeviceCommand):
119
126
  # 查询电量
120
127
  class QueryBatteryCommand(DeviceCommand):
121
128
  cmd_code = 0x16
129
+ cmd_desc = "电量信息"
122
130
  def parse_body(self, body: bytes):
123
- logger.info(f"Received QueryBatteryCommand body len: {len(body)}")
124
131
  # time - 8b
125
132
  self.device.current_time = int.from_bytes(body[0:8], 'little')
126
133
  # result - 1b
@@ -135,12 +142,15 @@ class QueryBatteryCommand(DeviceCommand):
135
142
  self.device.battery_total = body[12]
136
143
  # state - 1b
137
144
  # state = body[13]
145
+ logger.debug(f"电量更新: {self.device}")
138
146
  else:
139
147
  logger.warning(f"QueryBatteryCommand message received but result is failed.")
140
148
 
141
149
  # 设置采集参数
142
150
  class SetAcquisitionParamCommand(DeviceCommand):
143
151
  cmd_code = 0x451
152
+ cmd_desc = "设置信号采集参数"
153
+
144
154
  def pack_body(self):
145
155
  body = to_bytes(self.device.acq_channels)
146
156
  body += self.device.sample_range.to_bytes(4, byteorder='little')
@@ -156,6 +166,7 @@ class SetAcquisitionParamCommand(DeviceCommand):
156
166
  # 启动采集
157
167
  class StartAcquisitionCommand(DeviceCommand):
158
168
  cmd_code = 0x452
169
+ cmd_desc = "启动信号采集"
159
170
 
160
171
  def pack_body(self):
161
172
  return bytes.fromhex('0000')
@@ -165,19 +176,24 @@ class StartAcquisitionCommand(DeviceCommand):
165
176
  # 停止采集
166
177
  class StopAcquisitionCommand(DeviceCommand):
167
178
  cmd_code = 0x453
179
+ cmd_desc = "停止信号采集"
168
180
 
169
181
  def pack_body(self):
170
182
  return b''
171
183
  def parse_body(self, body: bytes):
172
184
  logger.info(f"Received acquisition stop response: {body.hex()}")
185
+
173
186
  # 设置阻抗采集参数
174
187
  class SetImpedanceParamCommand(DeviceCommand):
175
188
  cmd_code = 0x411
189
+ cmd_desc = "设置阻抗测量参数"
176
190
  def parse_body(self, body: bytes):
177
191
  logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
192
+
178
193
  # 启动采集
179
194
  class StartImpedanceCommand(DeviceCommand):
180
195
  cmd_code = 0x412
196
+ cmd_desc = "启动阻抗测量"
181
197
  def pack_body(self):
182
198
  body = bytes.fromhex('0000')
183
199
  body += to_bytes(self.device.acq_channels)
@@ -190,36 +206,22 @@ class StartImpedanceCommand(DeviceCommand):
190
206
  # 停止采集
191
207
  class StopImpedanceCommand(DeviceCommand):
192
208
  cmd_code = 0x413
209
+ cmd_desc = "停止阻抗测量"
193
210
 
194
211
  def pack_body(self):
195
212
  return b''
196
213
 
197
214
  def parse_body(self, body: bytes):
198
215
  logger.info(f"Received StopImpedanceCommand response: {body.hex()}")
199
-
200
- # 设置采集参数
201
- class SetStimulationParamCommand(DeviceCommand):
202
- cmd_code = 0x451
203
- def pack_body(self):
204
- body = to_bytes(self.device.acq_channels)
205
- body += self.device.sample_range.to_bytes(4, byteorder='little')
206
- body += self.device.sample_rate.to_bytes(4, byteorder='little')
207
- body += self.device.sample_num.to_bytes(4, byteorder='little')
208
- body += self.device.resolution.to_bytes(1, byteorder='little')
209
- body += bytes.fromhex('00')
210
-
211
- return body
212
- def parse_body(self, body: bytes):
213
- logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
214
216
 
215
- # 启动采集
217
+ # 启动刺激
216
218
  class StartStimulationCommand(DeviceCommand):
217
219
  cmd_code = 0x48C
220
+ cmd_desc = "启动刺激"
218
221
  def pack_body(self):
219
222
  return self.device.stim_paradigm.to_bytes()
220
223
  # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
221
224
  def parse_body(self, body: bytes):
222
- logger.info(f"Received stimulation start response: {body.hex()}")
223
225
  # time - 8B
224
226
  time = int.from_bytes(body[0:8], 'little')
225
227
  # result - 1B
@@ -227,13 +229,14 @@ class StartStimulationCommand(DeviceCommand):
227
229
  # error_channel - 8B
228
230
  # error_channel= int.from_bytes(body[9:17], 'big')
229
231
  channels = to_channels(body[9:17])
230
- logger.warning(f"通道 {channels} 刺激驱动不足")
232
+ logger.warning(f"通道 {channels} 刺激开始")
231
233
  # error_type - 1B
232
234
  error_type = body[17]
233
235
 
234
236
  # 停止采集
235
237
  class StopStimulationCommand(DeviceCommand):
236
238
  cmd_code = 0x488
239
+ cmd_desc = "停止刺激"
237
240
 
238
241
  def parse_body(self, body: bytes):
239
242
  logger.info(f"Received stimulation stop response: {body.hex()}")
@@ -241,14 +244,25 @@ class StopStimulationCommand(DeviceCommand):
241
244
  # 停止采集
242
245
  class StimulationInfoCommand(DeviceCommand):
243
246
  cmd_code = 0x48e
247
+ cmd_desc = "刺激告警信息"
244
248
 
245
249
  def parse_body(self, body: bytes):
246
- logger.info(f"Received stimulation info response: {body.hex()}")
250
+ time = int.from_bytes(body[0:8], 'little')
251
+ # result - 1B
252
+ result = body[8]
253
+ # error_channel - 8B
254
+ channels = to_channels(body[9:17])
255
+ # 保留位-8B
256
+ # error_type - 1B
257
+ # 特征位-4B
258
+ characteristic = int.from_bytes(body[25:29], 'little')
259
+ logger.warning(f"[{characteristic}]刺激告警,通道 {channels} 刺激驱动不足")
247
260
 
248
261
 
249
262
  # 阻抗数据
250
263
  class ImpedanceDataCommand(DeviceCommand):
251
264
  cmd_code = 0x415
265
+ cmd_desc = "阻抗数据"
252
266
 
253
267
  def parse_body(self, body: bytes):
254
268
  logger.info(f"Received impedance data: {body.hex()}")
@@ -257,19 +271,24 @@ class ImpedanceDataCommand(DeviceCommand):
257
271
  # 信号数据
258
272
  class SignalDataCommand(DeviceCommand):
259
273
  cmd_code = 0x455
274
+ cmd_desc = "信号数据"
260
275
 
261
276
  def unpack(self, payload):
262
277
  return super().unpack(payload)
263
278
 
264
- def parse_body(self, body: bytes):
265
- # logger.info(f"Received signal data: {len(body)}字节, the subscribe is {self.device.signal_consumers}")
266
- if len(self.device.signal_consumers) > 0:
279
+ def parse_body(self, body: bytes):
280
+ if len(self.device.signal_consumers) > 0 or self.device.edf_handler:
267
281
  # 解析数据包
268
282
  rsc = RscPacket()
269
283
  rsc.transfer(body)
284
+
270
285
  # 发送数据包到订阅者
271
286
  for q in list(self.device.signal_consumers.values()):
272
287
  q.put(rsc)
288
+
289
+ # 文件写入到edf
290
+ if self.device.edf_handler:
291
+ self.device.edf_handler.append(rsc)
273
292
 
274
293
  # =============================================================================
275
294
  # Command Registration
@@ -0,0 +1,34 @@
1
+ import psutil
2
+ import socket
3
+
4
+ def get_active_interfaces():
5
+ interfaces = {}
6
+ # 获取所有接口地址信息
7
+ all_addrs = psutil.net_if_addrs()
8
+ # 获取接口状态(是否处于UP状态)
9
+ stats = psutil.net_if_stats()
10
+
11
+ for name, addrs in all_addrs.items():
12
+ # 检查接口是否启用
13
+ if stats[name].isup:
14
+ ips = []
15
+ for addr in addrs:
16
+ # 提取IPv4和IPv6地址
17
+ if addr.family == socket.AF_INET:
18
+ ips.append(f"IPv4: {addr.address}")
19
+ elif addr.family == socket.AF_INET6:
20
+ ips.append(f"IPv6: {addr.address}")
21
+ # 过滤无IP的接口(可选)
22
+ if ips:
23
+ interfaces[name] = {
24
+ "status": "UP",
25
+ "IPs": ips
26
+ }
27
+ return interfaces
28
+
29
+ # 调用并打印结果
30
+ active_ifs = get_active_interfaces()
31
+ for iface, info in active_ifs.items():
32
+ print(f"接口: {iface}")
33
+ print(f"状态: {info['status']}")
34
+ print(f"IP地址: {info['IPs']}\n")
@@ -0,0 +1,55 @@
1
+ from threading import Thread
2
+ import psutil
3
+ import time
4
+
5
+ def get_active_ipv4():
6
+ ips = []
7
+ # 获取所有接口地址信息
8
+ all_addrs = psutil.net_if_addrs()
9
+ # 获取接口状态(是否处于UP状态)
10
+ stats = psutil.net_if_stats()
11
+
12
+ for name, addrs in all_addrs.items():
13
+ # 检查接口是否启用
14
+ if stats[name].isup:
15
+ for addr in addrs:
16
+ # 提取IPv4地址
17
+ if addr.family == socket.AF_INET:
18
+ ips.append(addr.address)
19
+ return ips
20
+ def monitor_up_interfaces(interval=2, callback=None):
21
+ prev_status = {iface: psutil.net_if_stats()[iface].isup
22
+ for iface in psutil.net_if_stats()}
23
+
24
+ while True:
25
+ current_stats = psutil.net_if_stats()
26
+ for iface, stats in current_stats.items():
27
+ current_up = stats.isup
28
+ # 检测状态变化
29
+ if current_up != prev_status.get(iface, None):
30
+ if current_up:
31
+ print(f"[UP] 接口 {iface} 激活")
32
+ if callback: callback(iface, "UP")
33
+ else:
34
+ print(f"[DOWN] 接口 {iface} 断开")
35
+ if callback: callback(iface, "DOWN")
36
+ prev_status[iface] = current_up
37
+ time.sleep(interval)
38
+
39
+ # 自定义回调函数示例
40
+ def notify(iface, status):
41
+ if status == "UP":
42
+ print(f"接口 {iface} 已激活")
43
+
44
+ # 启动监听
45
+ # monitor_up_interfaces(callback=notify)
46
+
47
+ monitor = Thread(target=monitor_up_interfaces, name="Network Status Monitor", args=(2, notify))
48
+ monitor.start()
49
+
50
+ import socket
51
+ def is_port_in_use(ip, port):
52
+ with socket.socket() as s:
53
+ return s.connect_ex((ip, port)) == 0
54
+
55
+ print(get_active_ipv4())
@@ -22,6 +22,8 @@ def to_channels(data: bytes) -> list[int]:
22
22
  if ba[i] == 1:
23
23
  m = i % 8
24
24
  channels.append(i + 8 - 2 * m)
25
+
26
+ channels.sort()
25
27
  return channels
26
28
 
27
29
  def bytes_to_ints(b):
@@ -5,10 +5,11 @@ from threading import Thread
5
5
  from loguru import logger
6
6
  import numpy as np
7
7
  import os
8
+ from qlsdk.core import RscPacket
8
9
 
9
10
  class EDFWriterThread(Thread):
10
11
  def __init__(self, edf_writer : EdfWriter):
11
- super().__init__(self)
12
+ super().__init__()
12
13
  self._edf_writer : EdfWriter = edf_writer
13
14
  self.data_queue : Queue = Queue()
14
15
  self._stop_event : bool = False
@@ -21,10 +22,6 @@ class EDFWriterThread(Thread):
21
22
  self._channels = []
22
23
  self._sample_rate = 0
23
24
 
24
- def start(self):
25
- self._stop_event = False
26
- super().start()
27
-
28
25
  def stop(self):
29
26
  self._stop_event = True
30
27
 
@@ -32,7 +29,7 @@ class EDFWriterThread(Thread):
32
29
  # 数据
33
30
  self.data_queue.put(data)
34
31
 
35
- def _consumer(self):
32
+ def run(self):
36
33
  logger.debug(f"开始消费数据 _consumer: {self.data_queue.qsize()}")
37
34
  while True:
38
35
  if self._recording or (not self.data_queue.empty()):
@@ -73,7 +70,8 @@ class EDFWriterThread(Thread):
73
70
  self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
74
71
  self._edf_writer.close()
75
72
 
76
- logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
73
+ # logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
74
+ # logger.info(f"文件: 完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
77
75
 
78
76
 
79
77
 
@@ -101,7 +99,7 @@ class RscEDFHandler(object):
101
99
  @author: qlsdk
102
100
  @since: 0.4.0
103
101
  '''
104
- def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=32, storage_path = None):
102
+ def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=24, storage_path = None):
105
103
  # edf文件参数
106
104
  self.physical_max = physical_max
107
105
  self.physical_min = physical_min
@@ -137,7 +135,7 @@ class RscEDFHandler(object):
137
135
  self._end_time = None
138
136
  self._patient_code = "patient_code"
139
137
  self._patient_name = "patient_name"
140
- self._device_type = None
138
+ self._device_type = "24130032"
141
139
  self._total_packets = 0
142
140
  self._lost_packets = 0
143
141
  self._storage_path = storage_path
@@ -170,32 +168,39 @@ class RscEDFHandler(object):
170
168
  def set_patient_name(self, patient_name):
171
169
  self._patient_name = patient_name
172
170
 
173
- def append(self, data, channels=None):
174
- if self._edf_writer_thread is None:
175
- self._edf_writer_thread = EDFWriterThread(self.init_edf_writer())
176
- self._edf_writer_thread.start()
177
- self._recording = True
178
- self._edf_writer_thread._recording = True
179
- logger.info(f"开始写入数据: {self.file_name}")
171
+ def append(self, data: RscPacket):
180
172
 
181
173
  if data:
182
- # # 通道数
183
- # if self._first_pkg_id is None:
184
- # self.eeg_channels = data.eeg_ch_count
185
- # self.acc_channels = data.acc_ch_count
186
- # self._first_pkg_id = data.pkg_id
187
- # self._first_timestamp = data.time_stamp
174
+ if self.eeg_channels is None:
175
+ logger.info(f"开始记录数据到文件...")
176
+ self.eeg_channels = data.channels
177
+ self._first_pkg_id = data.pkg_id if self._first_pkg_id is None else self._first_pkg_id
178
+ self._first_timestamp = data.time_stamp if self._first_timestamp is None else self._first_timestamp
188
179
 
189
- # if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
190
- # self._lost_packets += data.pkg_id - self._last_pkg_id - 1
191
- # logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
180
+ if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
181
+ self._lost_packets += data.pkg_id - self._last_pkg_id - 1
182
+ logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
192
183
 
193
184
  self._last_pkg_id = data.pkg_id
194
185
  self._total_packets += 1
195
186
 
196
- # 数据
197
- self._cache.put(data)
187
+
188
+
189
+ # 通道数变化、采样频率、信号放大幅度变化时应生成新的edf文件,handler内部不关注,外部调用方自行控制
190
+ # elif len(self.eeg_channels) != len(data.channels):
191
+
192
+ if self._edf_writer_thread is None:
193
+ self._edf_writer_thread = EDFWriterThread(self.init_edf_writer())
194
+ self._edf_writer_thread.start()
195
+ self._recording = True
196
+ self._edf_writer_thread._recording = True
197
+ logger.info(f"开始写入数据: {self.file_name}")
198
+
198
199
  self._edf_writer_thread.append(data)
200
+
201
+ # 数据
202
+ # self._cache.put(data)
203
+ # self._edf_writer_thread.append(data)
199
204
  # if not self._recording:
200
205
  # self.start()
201
206
 
@@ -206,7 +211,7 @@ class RscEDFHandler(object):
206
211
  # 创建EDF+写入器
207
212
  edf_writer = EdfWriter(
208
213
  self.file_name,
209
- self.eeg_channels,
214
+ len(self.eeg_channels),
210
215
  file_type=self.file_type
211
216
  )
212
217
 
@@ -218,7 +223,7 @@ class RscEDFHandler(object):
218
223
 
219
224
  # 配置通道参数
220
225
  signal_headers = []
221
- for ch in range(self.eeg_channels):
226
+ for ch in range(len(self.eeg_channels)):
222
227
  signal_headers.append({
223
228
  "label": f'channels {ch + 1}',
224
229
  "dimension": 'uV',
@@ -74,6 +74,7 @@ class DeviceContainer(object):
74
74
  def client_handler(self, client_socket):
75
75
 
76
76
  if self._proxy_enabled:
77
+ # 启动代理 TODO: 代理的同时支持接口控制和数据转发
77
78
  proxy = DeviceProxy(client_socket)
78
79
  proxy.start()
79
80
  else:
@@ -86,6 +87,7 @@ class DeviceContainer(object):
86
87
  # 添加设备
87
88
  while True:
88
89
  if device.device_name:
90
+ logger.info(f"设备 {device.device_name} 已连接")
89
91
  self.add_device(device)
90
92
  break
91
93
 
@@ -1,12 +1,13 @@
1
1
  from multiprocessing import Queue, Process
2
2
  from typing import Any, Dict, Literal
3
- # from rsc.command import *
4
- from qlsdk.core import *
5
- from qlsdk.core.device import BaseDevice
6
3
  from threading import Thread
7
4
  from loguru import logger
8
5
  from time import time_ns
9
6
 
7
+ from qlsdk.core import *
8
+ from qlsdk.core.device import BaseDevice
9
+ from qlsdk.persist import RscEDFHandler
10
+
10
11
  class QLDevice(BaseDevice):
11
12
  def __init__(self, socket):
12
13
  self.socket = socket
@@ -101,6 +102,9 @@ class QLDevice(BaseDevice):
101
102
  self.__signal_consumer: Dict[str, Queue[Any]]={}
102
103
  self.__impedance_consumer: Dict[str, Queue[Any]]={}
103
104
 
105
+ # EDF文件处理器
106
+ self._edf_handler = None
107
+ self.storage_enable = True
104
108
 
105
109
  self._parser = DeviceParser(self)
106
110
  self._parser.start()
@@ -109,6 +113,19 @@ class QLDevice(BaseDevice):
109
113
  self._accept = Thread(target=self.accept)
110
114
  self._accept.daemon = True
111
115
  self._accept.start()
116
+
117
+ def init_edf_handler(self):
118
+ self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range, - self.sample_range, - 2 >> 24, 2 >> 24 - 1, self.resolution)
119
+
120
+ @property
121
+ def edf_handler(self):
122
+ if not self.storage_enable:
123
+ return None
124
+
125
+ if self._edf_handler is None:
126
+ self.init_edf_handler()
127
+
128
+ return self._edf_handler
112
129
 
113
130
  @property
114
131
  def acq_channels(self):
@@ -132,6 +149,9 @@ class QLDevice(BaseDevice):
132
149
  @property
133
150
  def impedance_consumers(self):
134
151
  return self.__impedance_consumer
152
+
153
+ def set_storage_path(self, path):
154
+ pass
135
155
 
136
156
  def accept(self):
137
157
  while True:
@@ -220,13 +240,16 @@ class QLDevice(BaseDevice):
220
240
  msg = StopAcquisitionCommand.build(self).pack()
221
241
  logger.debug(f"stop_acquisition message is {msg}")
222
242
  self.socket.sendall(msg)
243
+ if self._edf_handler:
244
+ # 发送结束标识
245
+ self.edf_handler.append(None)
223
246
 
224
247
  # 订阅实时数据
225
248
  def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
226
249
 
227
250
  # 数据队列
228
251
  if q is None:
229
- q = Queue()
252
+ q = Queue(maxsize=1000)
230
253
 
231
254
  # 队列名称
232
255
  if topic is None:
@@ -318,7 +341,7 @@ class DeviceParser(object):
318
341
 
319
342
  def append(self, buffer):
320
343
  self.cache += buffer
321
- logger.debug(f"append cache len: {len(self.cache)}")
344
+ logger.debug(f"已缓存的数据长度: {len(self.cache)}")
322
345
 
323
346
  # if not self.running:
324
347
  # self.start()
@@ -326,14 +349,13 @@ class DeviceParser(object):
326
349
  def __parser__(self):
327
350
  logger.info("数据解析开始")
328
351
  while self.running:
329
- # logger.debug(f" cache len: {len(self.cache)}")
330
352
  if len(self.cache) < 14:
331
353
  continue
332
354
  if self.cache[0] != 0x5A or self.cache[1] != 0xA5:
333
355
  self.cache = self.cache[1:]
334
356
  continue
335
357
  pkg_len = int.from_bytes(self.cache[8:12], 'little')
336
- logger.debug(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
358
+ logger.trace(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
337
359
  # 一次取整包数据
338
360
  if len(self.cache) < pkg_len:
339
361
  continue
@@ -368,11 +390,13 @@ class TCPMessage(object):
368
390
  TCPMessage._validate_packet(data)
369
391
  # 提取指令码
370
392
  cmd_code = int.from_bytes(data[TCPMessage.CMD_POS:TCPMessage.CMD_POS+2], 'little')
371
- logger.debug(f"收到指令:{hex(cmd_code)}")
372
393
  cmd_class = CommandFactory.create_command(cmd_code)
373
- logger.debug(f"Command class: {cmd_class}")
394
+ logger.debug(f"收到指令:{cmd_class.cmd_desc}[{hex(cmd_code)}]")
374
395
  instance = cmd_class(device)
396
+ start = time_ns()
397
+ logger.debug(f"开始解析: {start}")
375
398
  instance.parse_body(data[TCPMessage.HEADER_LEN:-2])
399
+ logger.debug(f"解析完成:{time_ns()}, 解析耗时:{time_ns() - start}ns")
376
400
  return instance
377
401
 
378
402
  @staticmethod
@@ -388,9 +412,9 @@ class TCPMessage(object):
388
412
  if len(data) != expected_len:
389
413
  raise ValueError(f"Length mismatch: {len(data)} vs {expected_len}")
390
414
 
391
- logger.debug(f"checksum: {int.from_bytes(data[-2:], 'little')}")
392
- checksum = crc16(data[:-2])
393
- logger.debug(f"checksum recv: {checksum}")
415
+ # logger.trace(f"checksum: {int.from_bytes(data[-2:], 'little')}")
416
+ # checksum = crc16(data[:-2])
417
+ # logger.trace(f"checksum recv: {checksum}")
394
418
 
395
419
 
396
420
 
@@ -402,150 +426,3 @@ class DataPacket(object):
402
426
 
403
427
  def parse_body(self, body: bytes):
404
428
  raise NotImplementedError("Subclasses should implement this method")
405
-
406
-
407
-
408
- class C64Channel(Enum):
409
- CH0 = 0
410
- CH1 = 1
411
- CH2 = 2
412
- CH3 = 3
413
- CH4 = 4
414
- CH5 = 5
415
- CH6 = 6
416
- CH7 = 7
417
- CH8 = 8
418
- CH9 = 9
419
- CH10 = 10
420
- CH11 = 11
421
- CH12 = 12
422
- CH13 = 13
423
- CH14 = 14
424
- CH15 = 15
425
-
426
- class WaveForm(Enum):
427
- DC = 0
428
- SQUARE = 1
429
- AC = 2
430
- CUSTOM = 3
431
- PULSE = 4
432
-
433
- # # 刺激通道
434
- # class StimulationChannel(object):
435
- # def __init__(self, channel_id: int, waveform: int, current: float, duration: float, ramp_up: float = None, ramp_down: float = None,
436
- # frequency: float = None, phase_position: int = None, duration_delay: float = None, pulse_width: int = None, pulse_width_rate: int = 1):
437
- # self.channel_id = channel_id
438
- # self.waveform = waveform
439
- # self.current_max = current
440
- # self.current_min = current
441
- # self.duration = duration
442
- # self.ramp_up = ramp_up
443
- # self.ramp_down = ramp_down
444
- # self.frequency = frequency
445
- # self.phase_position = phase_position
446
- # self.duration_delay = duration_delay
447
- # self.pulse_width = pulse_width
448
- # self.delay_time = 0
449
- # self.pulse_interval = 0
450
- # self.with_group_repeats = 1
451
- # self.pulse_width_rate = 1065353216
452
- # self.pulse_time_f = 0
453
- # self.pulse_time_out = 0
454
- # self.pulse_time_idle = 0
455
-
456
- # def to_bytes(self):
457
- # # Convert the object to bytes for transmission
458
- # result = self.channel_id.to_bytes(1, 'little')
459
- # wave_form = WaveForm.SQUARE.value if self.waveform == WaveForm.PULSE.value else self.waveform
460
- # result += wave_form.to_bytes(1, 'little')
461
- # result += int(self.current_max * 1000 * 1000).to_bytes(4, 'little')
462
- # # result += int(self.current_min * 1000).to_bytes(2, 'little')
463
- # result += int(self.frequency).to_bytes(2, 'little')
464
- # result += int(self.pulse_width).to_bytes(2, 'little')
465
- # result += int(self.pulse_width_rate).to_bytes(4, 'little')
466
-
467
- # result += int(self.pulse_interval).to_bytes(2, 'little')
468
- # result += int(self.with_group_repeats).to_bytes(2, 'little')
469
- # result += int(self.pulse_time_f).to_bytes(4, 'little')
470
- # result += int(self.pulse_time_out).to_bytes(4, 'little')
471
- # result += int(self.pulse_time_idle).to_bytes(4, 'little')
472
-
473
- # result += int(self.delay_time).to_bytes(4, 'little')
474
- # result += int(self.ramp_up * 1000).to_bytes(4, 'little')
475
- # result += int((self.duration + self.ramp_up) * 1000).to_bytes(4, 'little')
476
- # result += int(self.ramp_down * 1000).to_bytes(4, 'little')
477
-
478
- # return result
479
-
480
- # def to_json(self):
481
- # return {
482
- # "channel_id": self.channel_id,
483
- # "waveform": self.waveform,
484
- # "current_max": self.current_max,
485
- # "current_min": self.current_min,
486
- # "duration": self.duration,
487
- # "ramp_up": self.ramp_up,
488
- # "ramp_down": self.ramp_down,
489
- # "frequency": self.frequency,
490
- # "phase_position": self.phase_position,
491
- # "duration_delay": self.duration_delay,
492
- # "pulse_width": self.pulse_width,
493
- # "delay_time": self.delay_time,
494
- # "pulse_interval": self.pulse_interval,
495
- # "with_group_repeats": self.with_group_repeats
496
- # }
497
-
498
- # # 刺激范式
499
- # class StimulationParadigm(object):
500
- # def __init__(self):
501
- # self.channels = None
502
- # self.duration = None
503
- # self.interval_time = 0
504
- # self.characteristic = 0
505
- # self.mode = 0
506
- # self.repeats = 0
507
-
508
- # def add_channel(self, channel: StimulationChannel, update=False):
509
- # if self.channels is None:
510
- # self.channels = {}
511
- # channel_id = channel.channel_id + 1
512
- # if channel_id in self.channels.keys():
513
- # logger.warning(f"Channel {channel_id} already exists")
514
- # if update:
515
- # self.channels[channel_id] = channel
516
- # else:
517
- # self.channels[channel_id] = channel
518
-
519
- # # 计算刺激时间
520
- # duration = channel.duration + channel.ramp_up + channel.ramp_down
521
- # if self.duration is None or duration > self.duration:
522
- # self.duration = duration
523
-
524
-
525
- # def to_bytes(self):
526
- # result = to_bytes(list(self.channels.keys()), 64)
527
- # result += int(self.duration * 1000).to_bytes(4, 'little')
528
- # result += int(self.interval_time).to_bytes(4, 'little')
529
- # result += int(self.characteristic).to_bytes(4, 'little')
530
- # result += int(self.mode).to_bytes(1, 'little')
531
- # result += int(self.repeats).to_bytes(4, 'little')
532
- # for channel in self.channels.values():
533
- # result += channel.to_bytes()
534
- # return result
535
-
536
- # def to_json(self):
537
- # # Convert the object to JSON for transmission
538
- # return {
539
- # "channels": list(self.channels.keys()),
540
- # "duration": self.duration,
541
- # "interval_time": self.interval_time,
542
- # "characteristic": self.characteristic,
543
- # "mode": self.mode,
544
- # "repeats": self.repeats,
545
- # "stim": [channel.to_json() for channel in self.channels.values()]
546
- # }
547
-
548
- # @staticmethod
549
- # def from_json(param: Dict[str, Any]):
550
- # pass
551
-
@@ -187,20 +187,21 @@ class PulseStimulation(StimulationChannel):
187
187
  current: float, 电流值,单位为mA
188
188
  duration: float, 刺激时间,单位为秒
189
189
  frequency: float, 频率,单位为Hz
190
- pulse_width: int, 脉冲宽度,单位为ms
190
+ pulse_width: int, 脉冲宽度,单位为uS
191
191
  pulse_width_ratio: float, 脉冲宽度比,范围(0, 1)
192
+ pulse_interval: int 脉冲间隔,单位为uS
192
193
  ramp_up: float, 上升时间,单位为秒
193
194
  ramp_down: float, 下降时间,单位为秒
194
195
  delay_time: float, 延迟启动时间,单位为秒(暂未启用)
195
196
  '''
196
197
  def __init__(self, channel_id: int, current: float, duration: float, frequency: float, pulse_width: int,
197
- pulse_width_ratio: float = 1, ramp_up: float = 0, ramp_down: float = 0, delay_time = 0):
198
+ pulse_width_ratio: float = 1, pulse_interval = 0, ramp_up: float = 0, ramp_down: float = 0, delay_time = 0):
198
199
  super().__init__(channel_id, WaveForm.PULSE.value, current, duration, ramp_up, ramp_down)
199
200
  self.frequency = frequency
200
201
  self.delay_time = delay_time
201
202
  self.pulse_width = pulse_width
202
203
  self.pulse_width_ratio = pulse_width_ratio
203
- self.pulse_interval = 0
204
+ self.pulse_interval = pulse_interval
204
205
  self.with_group_repeats = 1
205
206
  self.pulse_time_f = 0
206
207
  self.pulse_time_out = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qlsdk2
3
- Version: 0.4.0a2
3
+ Version: 0.4.0a3
4
4
  Summary: SDK for quanlan device
5
5
  Home-page: https://github.com/hehuajun/qlsdk
6
6
  Author: hehuajun
@@ -12,7 +12,7 @@ Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: loguru>=0.6.0
14
14
  Requires-Dist: numpy>=1.23.5
15
- Requires-Dist: pyedflib>=0.1.40
15
+ Requires-Dist: bitarray>=1.5.3
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=6.0; extra == "dev"
18
18
  Requires-Dist: twine>=3.0; extra == "dev"
@@ -23,6 +23,8 @@ src/qlsdk/core/message/__init__.py
23
23
  src/qlsdk/core/message/command.py
24
24
  src/qlsdk/core/message/tcp.py
25
25
  src/qlsdk/core/message/udp.py
26
+ src/qlsdk/core/network/__init__.py
27
+ src/qlsdk/core/network/monitor.py
26
28
  src/qlsdk/persist/__init__.py
27
29
  src/qlsdk/persist/edf.py
28
30
  src/qlsdk/persist/rsc_edf.py
@@ -1,6 +1,6 @@
1
1
  loguru>=0.6.0
2
2
  numpy>=1.23.5
3
- pyedflib>=0.1.40
3
+ bitarray>=1.5.3
4
4
 
5
5
  [dev]
6
6
  pytest>=6.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes