qlsdk2 0.4.0a2__py3-none-any.whl → 0.4.1__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.
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/__init__.py CHANGED
@@ -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 *
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
@@ -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,7 +1,10 @@
1
1
  import abc
2
+ import numpy as np
3
+ from time import time_ns
2
4
  from typing import Dict, Type
3
- from enum import Enum
4
5
  from loguru import logger
6
+
7
+
5
8
  from qlsdk.core.crc import crc16
6
9
  from qlsdk.core.device import BaseDevice
7
10
  from qlsdk.core.entity import RscPacket, ImpedancePacket
@@ -26,6 +29,10 @@ class DeviceCommand(abc.ABC):
26
29
  @abc.abstractmethod
27
30
  def cmd_code(self) -> int:
28
31
  pass
32
+ @property
33
+ @abc.abstractmethod
34
+ def cmd_desc(self) -> str:
35
+ pass
29
36
 
30
37
  @staticmethod
31
38
  def checksum(data: bytes) -> bytes:
@@ -59,6 +66,13 @@ class DeviceCommand(abc.ABC):
59
66
  # 解析消息体
60
67
  body = payload[self.HEADER_LEN:-2]
61
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
+
62
76
 
63
77
  class CommandFactory:
64
78
  """Registry for command implementations"""
@@ -70,15 +84,16 @@ class CommandFactory:
70
84
 
71
85
  @classmethod
72
86
  def create_command(cls, code: int) -> Type[DeviceCommand]:
73
- logger.debug(f"Creating command for code: {hex(code)}")
87
+ logger.trace(f"Creating command for code: {hex(code)}")
74
88
  if code not in cls._commands:
75
- logger.warning(f"Unsupported command code: {hex(code)}")
89
+ logger.warning(f"不支持的设备指令: {hex(code)}")
76
90
  return cls._commands[DefaultCommand.cmd_code]
77
91
  return cls._commands[code]
78
92
 
79
93
  # =============================================================================
80
94
  class DefaultCommand(DeviceCommand):
81
95
  cmd_code = 0x00
96
+ cmd_desc = "未定义"
82
97
 
83
98
  def parse_body(self, body: bytes):
84
99
  # Response parsing example: 2 bytes version + 4 bytes serial
@@ -86,9 +101,9 @@ class DefaultCommand(DeviceCommand):
86
101
 
87
102
  class GetDeviceInfoCommand(DeviceCommand):
88
103
  cmd_code = 0x17
104
+ cmd_desc = "设备信息"
89
105
 
90
106
  def parse_body(self, body: bytes):
91
- logger.info(f"Received GetDeviceInfoCommand body len: {len(body)}")
92
107
  # time - 8B
93
108
  self.device.connect_time = int.from_bytes(body[0:8], 'little')
94
109
  self.device.current_time = self.device.connect_time
@@ -108,10 +123,13 @@ class GetDeviceInfoCommand(DeviceCommand):
108
123
  flag = int.from_bytes(body[41:45], 'little')
109
124
  logger.debug(f"Received device info: {result}, {flag}, {self.device}")
110
125
 
126
+ # 创建设备对象
127
+
111
128
 
112
129
  # 握手
113
130
  class HandshakeCommand(DeviceCommand):
114
131
  cmd_code = 0x01
132
+ cmd_desc = "握手"
115
133
 
116
134
  def parse_body(self, body: bytes):
117
135
  logger.info(f"Received handshake response: {body.hex()}")
@@ -119,8 +137,8 @@ class HandshakeCommand(DeviceCommand):
119
137
  # 查询电量
120
138
  class QueryBatteryCommand(DeviceCommand):
121
139
  cmd_code = 0x16
140
+ cmd_desc = "电量信息"
122
141
  def parse_body(self, body: bytes):
123
- logger.info(f"Received QueryBatteryCommand body len: {len(body)}")
124
142
  # time - 8b
125
143
  self.device.current_time = int.from_bytes(body[0:8], 'little')
126
144
  # result - 1b
@@ -135,12 +153,15 @@ class QueryBatteryCommand(DeviceCommand):
135
153
  self.device.battery_total = body[12]
136
154
  # state - 1b
137
155
  # state = body[13]
156
+ logger.debug(f"电量更新: {self.device}")
138
157
  else:
139
158
  logger.warning(f"QueryBatteryCommand message received but result is failed.")
140
159
 
141
160
  # 设置采集参数
142
161
  class SetAcquisitionParamCommand(DeviceCommand):
143
162
  cmd_code = 0x451
163
+ cmd_desc = "设置信号采集参数"
164
+
144
165
  def pack_body(self):
145
166
  body = to_bytes(self.device.acq_channels)
146
167
  body += self.device.sample_range.to_bytes(4, byteorder='little')
@@ -150,76 +171,56 @@ class SetAcquisitionParamCommand(DeviceCommand):
150
171
  body += bytes.fromhex('00')
151
172
 
152
173
  return body
153
- def parse_body(self, body: bytes):
154
- logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
155
174
 
156
175
  # 启动采集
157
176
  class StartAcquisitionCommand(DeviceCommand):
158
177
  cmd_code = 0x452
178
+ cmd_desc = "启动信号采集"
159
179
 
160
180
  def pack_body(self):
161
181
  return bytes.fromhex('0000')
162
- def parse_body(self, body: bytes):
163
- logger.info(f"Received acquisition start response: {body.hex()}")
164
182
 
165
183
  # 停止采集
166
184
  class StopAcquisitionCommand(DeviceCommand):
167
185
  cmd_code = 0x453
186
+ cmd_desc = "停止信号采集"
168
187
 
169
188
  def pack_body(self):
170
189
  return b''
171
- def parse_body(self, body: bytes):
172
- logger.info(f"Received acquisition stop response: {body.hex()}")
190
+
191
+
173
192
  # 设置阻抗采集参数
174
193
  class SetImpedanceParamCommand(DeviceCommand):
175
194
  cmd_code = 0x411
176
- def parse_body(self, body: bytes):
177
- logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
178
- # 启动采集
195
+ cmd_desc = "设置阻抗测量参数"
196
+
197
+ # 启动阻抗测量
179
198
  class StartImpedanceCommand(DeviceCommand):
180
199
  cmd_code = 0x412
200
+ cmd_desc = "启动阻抗测量"
181
201
  def pack_body(self):
182
202
  body = bytes.fromhex('0000')
183
203
  body += to_bytes(self.device.acq_channels)
184
204
  body += bytes.fromhex('0000000000000000') # 8字节占位符
185
205
  return body
186
206
 
187
- def parse_body(self, body: bytes):
188
- logger.info(f"Received StartImpedanceCommand response: {body.hex()}")
189
207
 
190
- # 停止采集
208
+ # 停止阻抗测量
191
209
  class StopImpedanceCommand(DeviceCommand):
192
210
  cmd_code = 0x413
211
+ cmd_desc = "停止阻抗测量"
193
212
 
194
213
  def pack_body(self):
195
214
  return b''
196
-
197
- def parse_body(self, body: bytes):
198
- 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
215
 
215
- # 启动采集
216
+ # 启动刺激
216
217
  class StartStimulationCommand(DeviceCommand):
217
218
  cmd_code = 0x48C
219
+ cmd_desc = "启动刺激"
218
220
  def pack_body(self):
219
221
  return self.device.stim_paradigm.to_bytes()
220
222
  # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
221
223
  def parse_body(self, body: bytes):
222
- logger.info(f"Received stimulation start response: {body.hex()}")
223
224
  # time - 8B
224
225
  time = int.from_bytes(body[0:8], 'little')
225
226
  # result - 1B
@@ -227,28 +228,58 @@ class StartStimulationCommand(DeviceCommand):
227
228
  # error_channel - 8B
228
229
  # error_channel= int.from_bytes(body[9:17], 'big')
229
230
  channels = to_channels(body[9:17])
230
- logger.warning(f"通道 {channels} 刺激驱动不足")
231
+ logger.success(f"通道 {channels} 刺激开始")
232
+ self.device.trigger(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
237
-
239
+ cmd_desc = "停止刺激"
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')
238
248
  def parse_body(self, body: bytes):
239
- logger.info(f"Received stimulation stop response: {body.hex()}")
240
-
241
- # 停止采集
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
+ # 刺激信息
242
261
  class StimulationInfoCommand(DeviceCommand):
243
262
  cmd_code = 0x48e
263
+ cmd_desc = "刺激告警信息"
244
264
 
245
265
  def parse_body(self, body: bytes):
246
- logger.info(f"Received stimulation info response: {body.hex()}")
266
+ time = int.from_bytes(body[0:8], 'little')
267
+ # result - 1B
268
+ result = body[8]
269
+ # error_channel - 8B
270
+ channels = to_channels(body[9:17])
271
+ # 保留位-8B
272
+ # error_type - 1B
273
+ err_type = body[17]
274
+ # 特征位-4B
275
+ # errType = int.from_bytes(body[25:29], 'little')
276
+ logger.warning(f"刺激告警信息[{err_type}],通道 {channels} 刺激驱动不足")
247
277
 
248
278
 
249
279
  # 阻抗数据
250
280
  class ImpedanceDataCommand(DeviceCommand):
251
281
  cmd_code = 0x415
282
+ cmd_desc = "阻抗数据"
252
283
 
253
284
  def parse_body(self, body: bytes):
254
285
  logger.info(f"Received impedance data: {body.hex()}")
@@ -257,24 +288,33 @@ class ImpedanceDataCommand(DeviceCommand):
257
288
  # 信号数据
258
289
  class SignalDataCommand(DeviceCommand):
259
290
  cmd_code = 0x455
291
+ cmd_desc = "信号数据"
260
292
 
261
293
  def unpack(self, payload):
262
294
  return super().unpack(payload)
263
295
 
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:
267
- # 解析数据包
268
- rsc = RscPacket()
269
- 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))
308
+
270
309
  # 发送数据包到订阅者
271
310
  for q in list(self.device.signal_consumers.values()):
272
- q.put(rsc)
311
+ q.put(packet)
312
+
313
+
273
314
 
274
315
  # =============================================================================
275
- # Command Registration
316
+ # 指令实现类注册到指令工厂
276
317
  # =============================================================================
277
-
278
318
  CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
279
319
  CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
280
320
  CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
@@ -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())
qlsdk/core/utils.py CHANGED
@@ -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):
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