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.
@@ -1,13 +1,14 @@
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
5
- from qlsdk.core import crc16
6
6
 
7
- class UDPCommand(Enum):
8
- CONNECT = 0x10
9
- NOTIFY = 0x09
10
-
7
+
8
+ from qlsdk.core.crc import crc16
9
+ from qlsdk.core.entity import RscPacket, ImpedancePacket
10
+ from qlsdk.core.utils import to_channels, to_bytes
11
+ from qlsdk.rsc.interface import IDevice
11
12
 
12
13
  class DeviceCommand(abc.ABC):
13
14
  # 消息头
@@ -17,40 +18,61 @@ class DeviceCommand(abc.ABC):
17
18
  # 消息指令码位置
18
19
  CMD_POS = 12
19
20
 
20
- def __init__(self, device):
21
+ def __init__(self, device: IDevice):
21
22
  self.device = device
23
+
24
+ @classmethod
25
+ def build(cls, device) :
26
+ return cls(device)
27
+
22
28
  @property
23
29
  @abc.abstractmethod
24
30
  def cmd_code(self) -> int:
25
31
  pass
32
+ @property
33
+ @abc.abstractmethod
34
+ def cmd_desc(self) -> str:
35
+ pass
36
+
37
+ @staticmethod
38
+ def checksum(data: bytes) -> bytes:
39
+ return crc16(data).to_bytes(2, 'little')
26
40
 
27
- @classmethod
28
- def pack(cls, body=b'') -> bytes:
41
+ def pack(self, body=b'') -> bytes:
29
42
  # header+body+checksum
30
- header = DeviceCommand.build_header(cls.cmd_code, len(body))
43
+ body = self.pack_body()
44
+ header = self.pack_header(len(body))
31
45
  payload = header + body
32
- return payload + DeviceCommand.calculate_checksum(payload)
33
-
34
- @staticmethod
35
- def build_header(cmd_code, body_len: int = 0, pkg_type=2, device_type=0, device_id=0) -> bytes:
36
- """Construct protocol header"""
46
+ return payload + DeviceCommand.checksum(payload)
47
+ def pack_body(self) -> bytes:
48
+ """构建消息体"""
49
+ return b''
50
+ def pack_header(self, body_len: int) -> bytes:
51
+ device_id = int(self.device.device_id) if self.device and self.device.device_id else 0
52
+ device_type = int(self.device.device_type) if self.device and self.device.device_type else 0
53
+
54
+ """构建消息头"""
37
55
  return (
38
56
  DeviceCommand.HEADER_PREFIX
39
- + pkg_type.to_bytes(1, 'little')
57
+ + int(2).to_bytes(1, 'little') # pkgType
40
58
  + device_type.to_bytes(1, 'little')
41
59
  + device_id.to_bytes(4, 'little')
42
60
  + (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
43
- + cmd_code.to_bytes(2, 'little')
61
+ + self.cmd_code.to_bytes(2, 'little')
44
62
  )
45
-
46
- @staticmethod
47
- def calculate_checksum(data: bytes) -> bytes:
48
- return crc16(data).to_bytes(2, 'little')
49
-
50
- @abc.abstractmethod
63
+
64
+ def unpack(self, payload: bytes) -> bytes:
65
+ """解析消息体"""
66
+ # 解析消息体
67
+ body = payload[self.HEADER_LEN:-2]
68
+
51
69
  def parse_body(self, body: bytes):
52
- """Implement in subclasses for command-specific parsing"""
53
- pass
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
+
54
76
 
55
77
  class CommandFactory:
56
78
  """Registry for command implementations"""
@@ -62,15 +84,16 @@ class CommandFactory:
62
84
 
63
85
  @classmethod
64
86
  def create_command(cls, code: int) -> Type[DeviceCommand]:
65
- logger.debug(f"Creating command for code: {hex(code)}")
87
+ logger.trace(f"Creating command for code: {hex(code)}")
66
88
  if code not in cls._commands:
67
- logger.warning(f"Unsupported command code: {hex(code)}")
89
+ logger.warning(f"不支持的设备指令: {hex(code)}")
68
90
  return cls._commands[DefaultCommand.cmd_code]
69
91
  return cls._commands[code]
70
92
 
71
93
  # =============================================================================
72
94
  class DefaultCommand(DeviceCommand):
73
95
  cmd_code = 0x00
96
+ cmd_desc = "未定义"
74
97
 
75
98
  def parse_body(self, body: bytes):
76
99
  # Response parsing example: 2 bytes version + 4 bytes serial
@@ -78,32 +101,38 @@ class DefaultCommand(DeviceCommand):
78
101
 
79
102
  class GetDeviceInfoCommand(DeviceCommand):
80
103
  cmd_code = 0x17
104
+ cmd_desc = "设备信息"
81
105
 
82
106
  def parse_body(self, body: bytes):
83
- logger.info(f"Received GetDeviceInfoCommand body len: {len(body)}")
84
- # time - 8b
107
+ # time - 8B
85
108
  self.device.connect_time = int.from_bytes(body[0:8], 'little')
86
109
  self.device.current_time = self.device.connect_time
87
- # result - 1b
110
+ # result - 1B
88
111
  result = body[8]
89
- # deviceId - 4b
112
+ # deviceId - 4B
113
+ # self.device.device_id = int.from_bytes(body[9:13], 'big')
90
114
  self.device.device_id = body[9:13].hex()
91
- # deviceType - 4b
92
- self.device.device_type = body[13:17].hex()
93
- # softVersion - 4b
115
+ # deviceType - 4B
116
+ device_type = int.from_bytes(body[13:17], 'little')
117
+ self.device.set_device_type(device_type)
118
+ # softVersion - 4B
94
119
  self.device.software_version = body[17:21].hex()
95
- # hardVersion - 4b
120
+ # hardVersion - 4B
96
121
  self.device.hardware_version = body[21:25].hex()
97
- # deviceName - 16b
122
+ # deviceName - 16B
98
123
  self.device.device_name = body[25:41].decode('utf-8').rstrip('\x00')
99
- # flag - 4b
124
+ self.device.set_device_no(self.device.device_name)
125
+ # flag - 4B
100
126
  flag = int.from_bytes(body[41:45], 'little')
101
127
  logger.debug(f"Received device info: {result}, {flag}, {self.device}")
102
128
 
129
+ # 创建设备对象
130
+
103
131
 
104
132
  # 握手
105
133
  class HandshakeCommand(DeviceCommand):
106
134
  cmd_code = 0x01
135
+ cmd_desc = "握手"
107
136
 
108
137
  def parse_body(self, body: bytes):
109
138
  logger.info(f"Received handshake response: {body.hex()}")
@@ -111,8 +140,8 @@ class HandshakeCommand(DeviceCommand):
111
140
  # 查询电量
112
141
  class QueryBatteryCommand(DeviceCommand):
113
142
  cmd_code = 0x16
143
+ cmd_desc = "电量信息"
114
144
  def parse_body(self, body: bytes):
115
- logger.info(f"Received QueryBatteryCommand body len: {len(body)}")
116
145
  # time - 8b
117
146
  self.device.current_time = int.from_bytes(body[0:8], 'little')
118
147
  # result - 1b
@@ -127,78 +156,168 @@ class QueryBatteryCommand(DeviceCommand):
127
156
  self.device.battery_total = body[12]
128
157
  # state - 1b
129
158
  # state = body[13]
159
+ logger.debug(f"电量更新: {self.device}")
130
160
  else:
131
161
  logger.warning(f"QueryBatteryCommand message received but result is failed.")
132
162
 
133
163
  # 设置采集参数
134
164
  class SetAcquisitionParamCommand(DeviceCommand):
135
165
  cmd_code = 0x451
136
- def parse_body(self, body: bytes):
137
- logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
166
+ cmd_desc = "设置信号采集参数"
167
+
168
+ def pack_body(self):
169
+ body = to_bytes(self.device.acq_channels)
170
+ body += self.device.sample_range.to_bytes(4, byteorder='little')
171
+ body += self.device.sample_rate.to_bytes(4, byteorder='little')
172
+ body += self.device.sample_num.to_bytes(4, byteorder='little')
173
+ body += self.device.resolution.to_bytes(1, byteorder='little')
174
+ body += bytes.fromhex('00')
175
+
176
+ return body
138
177
 
139
178
  # 启动采集
140
179
  class StartAcquisitionCommand(DeviceCommand):
141
180
  cmd_code = 0x452
142
- def parse_body(self, body: bytes):
143
- logger.info(f"Received acquisition start response: {body.hex()}")
181
+ cmd_desc = "启动信号采集"
182
+
183
+ def pack_body(self):
184
+ return bytes.fromhex('0000')
144
185
 
145
186
  # 停止采集
146
187
  class StopAcquisitionCommand(DeviceCommand):
147
188
  cmd_code = 0x453
189
+ cmd_desc = "停止信号采集"
148
190
 
149
- def parse_body(self, body: bytes):
150
- logger.info(f"Received acquisition stop response: {body.hex()}")
191
+ def pack_body(self):
192
+ return b''
193
+
194
+
151
195
  # 设置阻抗采集参数
152
196
  class SetImpedanceParamCommand(DeviceCommand):
153
197
  cmd_code = 0x411
154
- def parse_body(self, body: bytes):
155
- logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
156
- # 启动采集
198
+ cmd_desc = "设置阻抗测量参数"
199
+
200
+ # 启动阻抗测量
157
201
  class StartImpedanceCommand(DeviceCommand):
158
202
  cmd_code = 0x412
159
- def parse_body(self, body: bytes):
160
- logger.info(f"Received StartImpedanceCommand response: {body.hex()}")
203
+ cmd_desc = "启动阻抗测量"
204
+ def pack_body(self):
205
+ body = bytes.fromhex('0000')
206
+ body += to_bytes(self.device.acq_channels)
207
+ body += bytes.fromhex('0000000000000000') # 8字节占位符
208
+ return body
209
+
161
210
 
162
- # 停止采集
211
+ # 停止阻抗测量
163
212
  class StopImpedanceCommand(DeviceCommand):
164
213
  cmd_code = 0x413
214
+ cmd_desc = "停止阻抗测量"
165
215
 
166
- def parse_body(self, body: bytes):
167
- logger.info(f"Received StopImpedanceCommand response: {body.hex()}")
168
-
216
+ def pack_body(self):
217
+ return b''
218
+
169
219
  # 启动刺激
170
220
  class StartStimulationCommand(DeviceCommand):
171
221
  cmd_code = 0x48C
222
+ cmd_desc = "启动刺激"
223
+ def pack_body(self):
224
+ return self.device.stim_paradigm.to_bytes()
225
+ # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
172
226
  def parse_body(self, body: bytes):
173
- logger.info(f"Received acquisition start response: {body.hex()}")
227
+ # time - 8B
228
+ time = int.from_bytes(body[0:8], 'little')
229
+ # result - 1B
230
+ result = body[8]
231
+ # error_channel - 8B
232
+ # error_channel= int.from_bytes(body[9:17], 'big')
233
+ channels = to_channels(body[9:17])
234
+ logger.success(f"通道 {channels} 刺激开始")
235
+ self.device.trigger(f"通道 {channels} 刺激开始")
236
+ # error_type - 1B
237
+ error_type = body[17]
174
238
 
175
239
  # 停止刺激
176
240
  class StopStimulationCommand(DeviceCommand):
177
241
  cmd_code = 0x488
242
+ cmd_desc = "停止刺激"
243
+
244
+ # 启动刺激
245
+ class StopStimulationNotifyCommand(DeviceCommand):
246
+ cmd_code = 0x48D
247
+ cmd_desc = "停止刺激通知"
248
+ def pack_body(self):
249
+ return self.device.stim_paradigm.to_bytes()
250
+ # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
251
+ def parse_body(self, body: bytes):
252
+ # time - 8B
253
+ time = int.from_bytes(body[0:8], 'little')
254
+ # result - 1B
255
+ result = body[8]
256
+ # error_channel - 8B
257
+ # error_channel= int.from_bytes(body[9:17], 'big')
258
+ channels = to_channels(body[9:17])
259
+ logger.success(f"通道 {channels} 刺激结束")
260
+ self.device.trigger(f"通道 {channels} 刺激结束", time)
261
+ # error_type - 1B
262
+ error_type = body[17]
263
+ # 刺激信息
264
+ class StimulationInfoCommand(DeviceCommand):
265
+ cmd_code = 0x48e
266
+ cmd_desc = "刺激告警信息"
178
267
 
179
268
  def parse_body(self, body: bytes):
180
- logger.info(f"Received acquisition stop response: {body.hex()}")
269
+ time = int.from_bytes(body[0:8], 'little')
270
+ # result - 1B
271
+ result = body[8]
272
+ # error_channel - 8B
273
+ channels = to_channels(body[9:17])
274
+ # 保留位-8B
275
+ # error_type - 1B
276
+ err_type = body[17]
277
+ # 特征位-4B
278
+ # errType = int.from_bytes(body[25:29], 'little')
279
+ logger.warning(f"刺激告警信息[{err_type}],通道 {channels} 刺激驱动不足")
280
+
181
281
 
182
282
  # 阻抗数据
183
283
  class ImpedanceDataCommand(DeviceCommand):
184
284
  cmd_code = 0x415
285
+ cmd_desc = "阻抗数据"
185
286
 
186
287
  def parse_body(self, body: bytes):
187
288
  logger.info(f"Received impedance data: {body.hex()}")
289
+ packet = ImpedancePacket().transfer(body)
188
290
 
189
291
  # 信号数据
190
292
  class SignalDataCommand(DeviceCommand):
191
293
  cmd_code = 0x455
294
+ cmd_desc = "信号数据"
192
295
 
193
- def parse_body(self, body: bytes):
194
- logger.info(f"Received signal data: {len(body)}字节, the subscribe is {self.device.signal_consumers}")
195
- for q in list(self.device.signal_consumers.values()):
196
- q.put(body)
296
+ def unpack(self, payload):
297
+ return super().unpack(payload)
298
+
299
+ def parse_body(self, body: bytes):
300
+ # 解析数据包
301
+ packet = RscPacket()
302
+ packet.transfer(body)
303
+
304
+ # 文件写入到edf
305
+ if self.device.edf_handler:
306
+ self.device.edf_handler.write(packet)
307
+
308
+ if len(self.device.signal_consumers) > 0 :
309
+ # 信号数字值转物理值
310
+ packet.eeg = self.device.eeg2phy(np.array(packet.eeg))
311
+
312
+ # 发送数据包到订阅者
313
+ for q in list(self.device.signal_consumers.values()):
314
+ q.put(packet)
315
+
316
+
197
317
 
198
318
  # =============================================================================
199
- # Command Registration
319
+ # 指令实现类注册到指令工厂
200
320
  # =============================================================================
201
-
202
321
  CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
203
322
  CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
204
323
  CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
@@ -210,5 +329,8 @@ CommandFactory.register_command(SetImpedanceParamCommand.cmd_code, SetImpedanceP
210
329
  CommandFactory.register_command(StartImpedanceCommand.cmd_code, StartImpedanceCommand)
211
330
  CommandFactory.register_command(StopImpedanceCommand.cmd_code, StopImpedanceCommand)
212
331
  CommandFactory.register_command(StartStimulationCommand.cmd_code, StartStimulationCommand)
332
+ CommandFactory.register_command(StimulationInfoCommand.cmd_code, StimulationInfoCommand)
213
333
  CommandFactory.register_command(ImpedanceDataCommand.cmd_code, ImpedanceDataCommand)
214
334
  CommandFactory.register_command(SignalDataCommand.cmd_code, SignalDataCommand)
335
+
336
+