qlsdk2 0.4.0a1__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.0a1 → qlsdk2-0.4.0a3}/PKG-INFO +2 -2
  2. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/setup.py +1 -1
  3. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/__init__.py +2 -1
  4. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/entity/__init__.py +1 -1
  5. {qlsdk2-0.4.0a1 → 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.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/utils.py +2 -0
  9. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/persist/rsc_edf.py +34 -29
  10. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/device_manager.py +26 -3
  11. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/discover.py +1 -0
  12. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/entity.py +36 -160
  13. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/paradigm.py +8 -5
  14. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/PKG-INFO +2 -2
  15. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/SOURCES.txt +2 -0
  16. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/requires.txt +1 -1
  17. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/README.md +0 -0
  18. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/setup.cfg +0 -0
  19. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/__init__.py +0 -0
  20. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/ar4/__init__.py +0 -0
  21. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/ar4m/__init__.py +0 -0
  22. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/crc/__init__.py +0 -0
  23. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/crc/crctools.py +0 -0
  24. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/device.py +0 -0
  25. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/exception.py +0 -0
  26. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/filter/__init__.py +0 -0
  27. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/filter/norch.py +0 -0
  28. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/local.py +0 -0
  29. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/__init__.py +0 -0
  30. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/tcp.py +0 -0
  31. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/core/message/udp.py +0 -0
  32. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/persist/__init__.py +0 -0
  33. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/persist/edf.py +0 -0
  34. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/__init__.py +0 -0
  35. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/command/__init__.py +0 -0
  36. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/command/message.py +0 -0
  37. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/eegion.py +0 -0
  38. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/rsc/proxy.py +0 -0
  39. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/__init__.py +0 -0
  40. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/ar4sdk.py +0 -0
  41. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/hub.py +0 -0
  42. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
  43. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
  44. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/x8/__init__.py +0 -0
  45. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk/x8m/__init__.py +0 -0
  46. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
  47. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/src/qlsdk2.egg-info/top_level.txt +0 -0
  48. {qlsdk2-0.4.0a1 → qlsdk2-0.4.0a3}/test/test.py +0 -0
  49. {qlsdk2-0.4.0a1 → 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.0a1
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.0a1",
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',
@@ -1,16 +1,18 @@
1
1
  import socket
2
+ from typing import Optional
2
3
  from loguru import logger
3
4
  from qlsdk.rsc.discover import UdpBroadcaster
4
5
  from threading import Thread
5
6
  from qlsdk.core import *
6
7
  from qlsdk.rsc.entity import QLDevice
7
8
  from qlsdk.rsc.proxy import DeviceProxy
9
+ import time
8
10
 
9
11
 
10
12
  class DeviceContainer(object):
11
- def __init__(self, proxy_enabled=False):
13
+ def __init__(self, proxy_enabled=False, tcp_port = 19216):
12
14
  self._devices = {}
13
- self._tcp_port = 19216
15
+ self._tcp_port = tcp_port
14
16
  self._proxy_enabled = proxy_enabled
15
17
 
16
18
  # 设备搜索广播器
@@ -21,6 +23,25 @@ class DeviceContainer(object):
21
23
  self._listening_thread = Thread(target=self._listening)
22
24
  self._listening_thread.daemon = True
23
25
  self._listening_thread.start()
26
+
27
+ @property
28
+ def devices(self)-> QLDevice:
29
+ return self._devices
30
+
31
+ '''
32
+ 等待设备连接
33
+ '''
34
+ def connect(self, device_id: str, timeout:int=30) -> Optional[QLDevice]:
35
+ logger.info(f"Searching for device: {device_id}")
36
+ self.add_search(device_id)
37
+ for _ in range(timeout):
38
+ device = self.get_device(device_id)
39
+ if device:
40
+ logger.success(f"Device {device_id} connected")
41
+ return device
42
+ time.sleep(1)
43
+ logger.error(f"Device {device_id} not found")
44
+ return None
24
45
 
25
46
  def _listening(self):
26
47
  tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -53,6 +74,7 @@ class DeviceContainer(object):
53
74
  def client_handler(self, client_socket):
54
75
 
55
76
  if self._proxy_enabled:
77
+ # 启动代理 TODO: 代理的同时支持接口控制和数据转发
56
78
  proxy = DeviceProxy(client_socket)
57
79
  proxy.start()
58
80
  else:
@@ -65,6 +87,7 @@ class DeviceContainer(object):
65
87
  # 添加设备
66
88
  while True:
67
89
  if device.device_name:
90
+ logger.info(f"设备 {device.device_name} 已连接")
68
91
  self.add_device(device)
69
92
  break
70
93
 
@@ -84,7 +107,7 @@ class DeviceContainer(object):
84
107
  logger.debug(f"add_device {device.device_name} then has broadcaster {self._broadcaster}")
85
108
 
86
109
  def get_device(self, device_id=None)->QLDevice:
87
- logger.debug(f"已连接设备数量:{len(self._devices)}")
110
+ logger.trace(f"已连接设备数量:{len(self._devices)}")
88
111
  if len(self._devices) == 0:
89
112
  return None
90
113
 
@@ -6,6 +6,7 @@ from loguru import logger
6
6
  from qlsdk.core.message import UDPMessage
7
7
 
8
8
  class UdpBroadcaster:
9
+ # 广播端口需要和ar4sdk做区分, 使用54366时不能和x8同时使用
9
10
  def __init__(self, port=54366):
10
11
  self.broadcast_port = port
11
12
  self.devices_to_broadcast = [] # 待广播的设备序列号列表
@@ -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):
@@ -128,11 +145,13 @@ class QLDevice(BaseDevice):
128
145
  @property
129
146
  def signal_consumers(self):
130
147
  return self.__signal_consumer
131
- return self.__signal_consumer
132
148
 
133
149
  @property
134
150
  def impedance_consumers(self):
135
151
  return self.__impedance_consumer
152
+
153
+ def set_storage_path(self, path):
154
+ pass
136
155
 
137
156
  def accept(self):
138
157
  while True:
@@ -221,13 +240,16 @@ class QLDevice(BaseDevice):
221
240
  msg = StopAcquisitionCommand.build(self).pack()
222
241
  logger.debug(f"stop_acquisition message is {msg}")
223
242
  self.socket.sendall(msg)
243
+ if self._edf_handler:
244
+ # 发送结束标识
245
+ self.edf_handler.append(None)
224
246
 
225
247
  # 订阅实时数据
226
248
  def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
227
249
 
228
250
  # 数据队列
229
251
  if q is None:
230
- q = Queue()
252
+ q = Queue(maxsize=1000)
231
253
 
232
254
  # 队列名称
233
255
  if topic is None:
@@ -319,7 +341,7 @@ class DeviceParser(object):
319
341
 
320
342
  def append(self, buffer):
321
343
  self.cache += buffer
322
- logger.debug(f"append cache len: {len(self.cache)}")
344
+ logger.debug(f"已缓存的数据长度: {len(self.cache)}")
323
345
 
324
346
  # if not self.running:
325
347
  # self.start()
@@ -327,14 +349,13 @@ class DeviceParser(object):
327
349
  def __parser__(self):
328
350
  logger.info("数据解析开始")
329
351
  while self.running:
330
- # logger.debug(f" cache len: {len(self.cache)}")
331
352
  if len(self.cache) < 14:
332
353
  continue
333
354
  if self.cache[0] != 0x5A or self.cache[1] != 0xA5:
334
355
  self.cache = self.cache[1:]
335
356
  continue
336
357
  pkg_len = int.from_bytes(self.cache[8:12], 'little')
337
- 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)}")
338
359
  # 一次取整包数据
339
360
  if len(self.cache) < pkg_len:
340
361
  continue
@@ -369,11 +390,13 @@ class TCPMessage(object):
369
390
  TCPMessage._validate_packet(data)
370
391
  # 提取指令码
371
392
  cmd_code = int.from_bytes(data[TCPMessage.CMD_POS:TCPMessage.CMD_POS+2], 'little')
372
- logger.debug(f"收到指令:{hex(cmd_code)}")
373
393
  cmd_class = CommandFactory.create_command(cmd_code)
374
- logger.debug(f"Command class: {cmd_class}")
394
+ logger.debug(f"收到指令:{cmd_class.cmd_desc}[{hex(cmd_code)}]")
375
395
  instance = cmd_class(device)
396
+ start = time_ns()
397
+ logger.debug(f"开始解析: {start}")
376
398
  instance.parse_body(data[TCPMessage.HEADER_LEN:-2])
399
+ logger.debug(f"解析完成:{time_ns()}, 解析耗时:{time_ns() - start}ns")
377
400
  return instance
378
401
 
379
402
  @staticmethod
@@ -389,9 +412,9 @@ class TCPMessage(object):
389
412
  if len(data) != expected_len:
390
413
  raise ValueError(f"Length mismatch: {len(data)} vs {expected_len}")
391
414
 
392
- logger.debug(f"checksum: {int.from_bytes(data[-2:], 'little')}")
393
- checksum = crc16(data[:-2])
394
- 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}")
395
418
 
396
419
 
397
420
 
@@ -403,150 +426,3 @@ class DataPacket(object):
403
426
 
404
427
  def parse_body(self, body: bytes):
405
428
  raise NotImplementedError("Subclasses should implement this method")
406
-
407
-
408
-
409
- class C64Channel(Enum):
410
- CH0 = 0
411
- CH1 = 1
412
- CH2 = 2
413
- CH3 = 3
414
- CH4 = 4
415
- CH5 = 5
416
- CH6 = 6
417
- CH7 = 7
418
- CH8 = 8
419
- CH9 = 9
420
- CH10 = 10
421
- CH11 = 11
422
- CH12 = 12
423
- CH13 = 13
424
- CH14 = 14
425
- CH15 = 15
426
-
427
- class WaveForm(Enum):
428
- DC = 0
429
- SQUARE = 1
430
- AC = 2
431
- CUSTOM = 3
432
- PULSE = 4
433
-
434
- # # 刺激通道
435
- # class StimulationChannel(object):
436
- # def __init__(self, channel_id: int, waveform: int, current: float, duration: float, ramp_up: float = None, ramp_down: float = None,
437
- # frequency: float = None, phase_position: int = None, duration_delay: float = None, pulse_width: int = None, pulse_width_rate: int = 1):
438
- # self.channel_id = channel_id
439
- # self.waveform = waveform
440
- # self.current_max = current
441
- # self.current_min = current
442
- # self.duration = duration
443
- # self.ramp_up = ramp_up
444
- # self.ramp_down = ramp_down
445
- # self.frequency = frequency
446
- # self.phase_position = phase_position
447
- # self.duration_delay = duration_delay
448
- # self.pulse_width = pulse_width
449
- # self.delay_time = 0
450
- # self.pulse_interval = 0
451
- # self.with_group_repeats = 1
452
- # self.pulse_width_rate = 1065353216
453
- # self.pulse_time_f = 0
454
- # self.pulse_time_out = 0
455
- # self.pulse_time_idle = 0
456
-
457
- # def to_bytes(self):
458
- # # Convert the object to bytes for transmission
459
- # result = self.channel_id.to_bytes(1, 'little')
460
- # wave_form = WaveForm.SQUARE.value if self.waveform == WaveForm.PULSE.value else self.waveform
461
- # result += wave_form.to_bytes(1, 'little')
462
- # result += int(self.current_max * 1000 * 1000).to_bytes(4, 'little')
463
- # # result += int(self.current_min * 1000).to_bytes(2, 'little')
464
- # result += int(self.frequency).to_bytes(2, 'little')
465
- # result += int(self.pulse_width).to_bytes(2, 'little')
466
- # result += int(self.pulse_width_rate).to_bytes(4, 'little')
467
-
468
- # result += int(self.pulse_interval).to_bytes(2, 'little')
469
- # result += int(self.with_group_repeats).to_bytes(2, 'little')
470
- # result += int(self.pulse_time_f).to_bytes(4, 'little')
471
- # result += int(self.pulse_time_out).to_bytes(4, 'little')
472
- # result += int(self.pulse_time_idle).to_bytes(4, 'little')
473
-
474
- # result += int(self.delay_time).to_bytes(4, 'little')
475
- # result += int(self.ramp_up * 1000).to_bytes(4, 'little')
476
- # result += int((self.duration + self.ramp_up) * 1000).to_bytes(4, 'little')
477
- # result += int(self.ramp_down * 1000).to_bytes(4, 'little')
478
-
479
- # return result
480
-
481
- # def to_json(self):
482
- # return {
483
- # "channel_id": self.channel_id,
484
- # "waveform": self.waveform,
485
- # "current_max": self.current_max,
486
- # "current_min": self.current_min,
487
- # "duration": self.duration,
488
- # "ramp_up": self.ramp_up,
489
- # "ramp_down": self.ramp_down,
490
- # "frequency": self.frequency,
491
- # "phase_position": self.phase_position,
492
- # "duration_delay": self.duration_delay,
493
- # "pulse_width": self.pulse_width,
494
- # "delay_time": self.delay_time,
495
- # "pulse_interval": self.pulse_interval,
496
- # "with_group_repeats": self.with_group_repeats
497
- # }
498
-
499
- # # 刺激范式
500
- # class StimulationParadigm(object):
501
- # def __init__(self):
502
- # self.channels = None
503
- # self.duration = None
504
- # self.interval_time = 0
505
- # self.characteristic = 0
506
- # self.mode = 0
507
- # self.repeats = 0
508
-
509
- # def add_channel(self, channel: StimulationChannel, update=False):
510
- # if self.channels is None:
511
- # self.channels = {}
512
- # channel_id = channel.channel_id + 1
513
- # if channel_id in self.channels.keys():
514
- # logger.warning(f"Channel {channel_id} already exists")
515
- # if update:
516
- # self.channels[channel_id] = channel
517
- # else:
518
- # self.channels[channel_id] = channel
519
-
520
- # # 计算刺激时间
521
- # duration = channel.duration + channel.ramp_up + channel.ramp_down
522
- # if self.duration is None or duration > self.duration:
523
- # self.duration = duration
524
-
525
-
526
- # def to_bytes(self):
527
- # result = to_bytes(list(self.channels.keys()), 64)
528
- # result += int(self.duration * 1000).to_bytes(4, 'little')
529
- # result += int(self.interval_time).to_bytes(4, 'little')
530
- # result += int(self.characteristic).to_bytes(4, 'little')
531
- # result += int(self.mode).to_bytes(1, 'little')
532
- # result += int(self.repeats).to_bytes(4, 'little')
533
- # for channel in self.channels.values():
534
- # result += channel.to_bytes()
535
- # return result
536
-
537
- # def to_json(self):
538
- # # Convert the object to JSON for transmission
539
- # return {
540
- # "channels": list(self.channels.keys()),
541
- # "duration": self.duration,
542
- # "interval_time": self.interval_time,
543
- # "characteristic": self.characteristic,
544
- # "mode": self.mode,
545
- # "repeats": self.repeats,
546
- # "stim": [channel.to_json() for channel in self.channels.values()]
547
- # }
548
-
549
- # @staticmethod
550
- # def from_json(param: Dict[str, Any]):
551
- # pass
552
-
@@ -186,19 +186,22 @@ class PulseStimulation(StimulationChannel):
186
186
  channel_id: int, 通道编号,从0开始
187
187
  current: float, 电流值,单位为mA
188
188
  duration: float, 刺激时间,单位为秒
189
+ frequency: float, 频率,单位为Hz
190
+ pulse_width: int, 脉冲宽度,单位为uS
191
+ pulse_width_ratio: float, 脉冲宽度比,范围(0, 1)
192
+ pulse_interval: int 脉冲间隔,单位为uS
189
193
  ramp_up: float, 上升时间,单位为秒
190
194
  ramp_down: float, 下降时间,单位为秒
191
- frequency: float, 频率,单位为Hz
192
- phase_position: int, 相位位置,单位为度
195
+ delay_time: float, 延迟启动时间,单位为秒(暂未启用)
193
196
  '''
194
197
  def __init__(self, channel_id: int, current: float, duration: float, frequency: float, pulse_width: int,
195
- 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):
196
199
  super().__init__(channel_id, WaveForm.PULSE.value, current, duration, ramp_up, ramp_down)
197
200
  self.frequency = frequency
198
- self.duration_delay = delay_time
201
+ self.delay_time = delay_time
199
202
  self.pulse_width = pulse_width
200
203
  self.pulse_width_ratio = pulse_width_ratio
201
- self.pulse_interval = 0
204
+ self.pulse_interval = pulse_interval
202
205
  self.with_group_repeats = 1
203
206
  self.pulse_time_f = 0
204
207
  self.pulse_time_out = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qlsdk2
3
- Version: 0.4.0a1
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