qlsdk2 0.3.0a2__py3-none-any.whl → 0.4.0a1__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.
@@ -0,0 +1,96 @@
1
+
2
+ from enum import Enum
3
+ from time import timezone
4
+ from qlsdk.core.crc import check_crc, crc16
5
+ from qlsdk.core.local import get_ip
6
+
7
+ from loguru import logger
8
+
9
+
10
+ class UDPCommand(Enum):
11
+ CONNECT = 0x10
12
+ NOTIFY = 0x09
13
+
14
+
15
+ class UDPMessage(object):
16
+ START = "SHQuanLan"
17
+ MAC = 'mac'
18
+ DEV_TYPE = "dev_type"
19
+ VERSION_CODE = "version_code"
20
+ VERSION_NAME = "version_name"
21
+
22
+ def __init__(self) -> None:
23
+ self._base = None
24
+ self._message = None
25
+
26
+ @staticmethod
27
+ def parse(packet, address = None):
28
+ plen = len(packet)
29
+ if plen < 10:
30
+ logger.trace("message length too short.")
31
+ return
32
+
33
+ start = packet[:9].decode("utf-8")
34
+ # quanlan udp message
35
+ if start != UDPMessage.START:
36
+ return
37
+
38
+ if not check_crc(packet):
39
+ logger.warn(f"数据CRC校验失败,丢弃!")
40
+ return
41
+
42
+ # message command
43
+ cmd = int.from_bytes(packet[10:12], 'little')
44
+
45
+ return UDPMessage._parse(cmd, packet[12:])
46
+
47
+ @staticmethod
48
+ def _parse(cmd, data):
49
+ # 只解析0x09
50
+ if cmd == UDPCommand.NOTIFY.value:
51
+ return UDPMessage.parseDeviceInfo(data)
52
+ else:
53
+ logger.trace(f'不支持的消息. cmd: {hex(cmd)} dlen: {len(data)} data: {data}')
54
+ return None
55
+
56
+ @staticmethod
57
+ def parseDeviceInfo(data):
58
+ device_info = {}
59
+ try:
60
+ device_info[UDPMessage.DEV_TYPE] = hex(int.from_bytes(data[:2], 'little'))
61
+ device_info[UDPMessage.MAC] = hex(int.from_bytes(data[2:10], 'little'))
62
+ device_info[UDPMessage.VERSION_CODE] = int.from_bytes(data[42:46], 'little')
63
+ device_info[UDPMessage.VERSION_NAME] = str(data[10:42],'utf-8').split('\x00')[0]
64
+ except Exception as e:
65
+ logger.error(f"parseDeviceInfo异常:{e}")
66
+
67
+ return device_info
68
+
69
+ @staticmethod
70
+ def search(device_id : str, server_ip : str=None, server_port : int=19216):
71
+ # 服务端Ip
72
+ if server_ip is None:
73
+ server_ip = get_ip()
74
+
75
+ logger.debug(f"search device {device_id} on {server_ip}:{server_port}")
76
+
77
+ message = bytearray(28)
78
+ # 消息头
79
+ message[:10] = UDPMessage.START.encode('utf-8')
80
+ # 消息类型
81
+ message[10:12] = UDPCommand.CONNECT.value.to_bytes(2, 'little')
82
+ # 设备序列号格式沿用现有协议
83
+ serial_no = f'F0{device_id[:2]}FFFFFFFF{device_id[4:]}'
84
+ message[12:22] = bytes.fromhex(serial_no)
85
+ # 本机ip
86
+ ip = server_ip.split(".")
87
+ message[22] = (int)(ip[0])
88
+ message[23] = (int)(ip[1])
89
+ message[24] = (int)(ip[2])
90
+ message[25] = (int)(ip[3])
91
+ # 服务端端口(按大端输出)
92
+ message[26:28] = server_port.to_bytes(2, 'big')
93
+ # 校验和(按小端输出)
94
+ checksum = crc16(message).to_bytes(2, 'little')
95
+
96
+ return message + checksum
qlsdk/core/utils.py ADDED
@@ -0,0 +1,68 @@
1
+ from bitarray import bitarray
2
+ from loguru import logger
3
+ # 通道数组[1,2,3,...]转换为bytes
4
+ def to_bytes(channels: list[int], upper=256) -> bytes:
5
+ byte_len = int(upper / 8)
6
+ channel = [0] * byte_len
7
+ result = bitarray(upper)
8
+ result.setall(0)
9
+ for i in range(len(channels)):
10
+ if channels[i] > 0 and channels[i] <= upper:
11
+ # 每个字节从低位开始计数
12
+ m = (channels[i] - 1) % 8
13
+ result[channels[i] + 6 - 2 * m] = 1
14
+ return result.tobytes()
15
+
16
+ # 通道bytes转换为数组[1,2,3,...]
17
+ def to_channels(data: bytes) -> list[int]:
18
+ ba = bitarray()
19
+ ba.frombytes(data)
20
+ channels = []
21
+ for i in range(len(ba)):
22
+ if ba[i] == 1:
23
+ m = i % 8
24
+ channels.append(i + 8 - 2 * m)
25
+ return channels
26
+
27
+ def bytes_to_ints(b):
28
+ """将小端字节序的3字节bytes数组转换为int数组"""
29
+ if len(b) % 3 != 0:
30
+ raise ValueError("输入的bytes长度必须是3的倍数")
31
+ return [
32
+ b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
33
+ for i in range(0, len(b), 3)
34
+ ]
35
+
36
+ def bytes_to_int(b: bytes) -> int:
37
+ """将小端字节序的3字节bytes数组转换为int数组"""
38
+ if len(b) != 3:
39
+ raise ValueError("输入的bytes长度必须是3的倍数")
40
+ return b[0] | (b[1] << 8) | (b[2] << 16)
41
+ def bytes_to_ints2(b):
42
+ """将小端字节序的3字节bytes数组转换为int数组"""
43
+ if len(b) % 3 != 0:
44
+ raise ValueError("输入的bytes长度必须是3的倍数")
45
+ return [
46
+ b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
47
+ for i in range(0, len(b), 3)
48
+ ]
49
+
50
+ import numpy as np
51
+ if __name__ == "__main__":
52
+
53
+ channels = [1]
54
+ channels1 = [1, 2, 3, 4]
55
+ channels2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]
56
+ channels3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145,146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207,208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256]
57
+ channels4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
58
+ logger.info(to_bytes(channels).hex())
59
+ logger.info(to_bytes(channels1).hex())
60
+ logger.info(to_bytes(channels2).hex())
61
+ logger.info(to_bytes(channels3).hex())
62
+ logger.info(to_bytes(channels4).hex())
63
+
64
+ bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
65
+ bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
66
+ bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
67
+ logger.info(to_channels(bytes.fromhex(bs1)))
68
+ logger.info(to_channels(bytes.fromhex(bs2)))
qlsdk/persist/__init__.py CHANGED
@@ -1 +1,2 @@
1
- from .edf import EdfHandler
1
+ from .edf import EdfHandler
2
+ from .rsc_edf import RscEDFHandler
@@ -6,15 +6,118 @@ from loguru import logger
6
6
  import numpy as np
7
7
  import os
8
8
 
9
- class EdfHandler(object):
10
- def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=16, storage_path = None):
9
+ class EDFWriterThread(Thread):
10
+ def __init__(self, edf_writer : EdfWriter):
11
+ super().__init__(self)
12
+ self._edf_writer : EdfWriter = edf_writer
13
+ self.data_queue : Queue = Queue()
14
+ self._stop_event : bool = False
15
+ self._recording = False
16
+ self._chunk = np.array([])
17
+ self._points = 0
18
+ self._duration = 0
19
+ self._sample_frequency = 0
20
+ self._total_packets = 0
21
+ self._channels = []
22
+ self._sample_rate = 0
23
+
24
+ def start(self):
25
+ self._stop_event = False
26
+ super().start()
27
+
28
+ def stop(self):
29
+ self._stop_event = True
30
+
31
+ def append(self, data):
32
+ # 数据
33
+ self.data_queue.put(data)
34
+
35
+ def _consumer(self):
36
+ logger.debug(f"开始消费数据 _consumer: {self.data_queue.qsize()}")
37
+ while True:
38
+ if self._recording or (not self.data_queue.empty()):
39
+ try:
40
+ data = self.data_queue.get(timeout=10)
41
+ if data is None:
42
+ break
43
+ # 处理数据
44
+ self._points += len(data)
45
+ self._write_file(data)
46
+ except Exception as e:
47
+ logger.error("数据队列为空,超时(10s)结束")
48
+ break
49
+ else:
50
+ break
51
+
52
+ self.close()
53
+
54
+ def _write_file(self, eeg_data):
55
+ try:
56
+ if (self._chunk.size == 0):
57
+ self._chunk = np.asarray(eeg_data)
58
+ else:
59
+ self._chunk = np.hstack((self._chunk, eeg_data))
60
+
61
+ if self._chunk.size >= self._sample_rate * self._channels:
62
+ self._write_chunk(self._chunk[:self._sample_rate])
63
+ self._chunk = self._chunk[self._sample_rate:]
64
+
65
+ except Exception as e:
66
+ logger.error(f"写入数据异常: {str(e)}")
67
+
68
+ def close(self):
69
+ self._recording = False
70
+ if self._edf_writer:
71
+ self._end_time = datetime.now().timestamp()
72
+ self._edf_writer.writeAnnotation(0, 1, "start recording ")
73
+ self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
74
+ self._edf_writer.close()
75
+
76
+ logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
77
+
78
+
79
+
80
+ def _write_chunk(self, chunk):
81
+ logger.debug(f"写入数据: {chunk}")
82
+ # 转换数据类型为float64(pyedflib要求)
83
+ data_float64 = chunk.astype(np.float64)
84
+ # 写入时转置为(样本数, 通道数)格式
85
+ self._edf_writer.writeSamples(data_float64)
86
+ self._duration += 1
87
+
88
+ class RscEDFHandler(object):
89
+ '''
90
+ Rsc EDF Handler
91
+ 处理EDF文件的读写
92
+ RSC设备通道数根据选择变化,不同通道采样频率相同
93
+ sample_frequency: 采样频率
94
+ physical_max: 物理最大值
95
+ physical_min: 物理最小值
96
+ digital_max: 数字最大值
97
+ digital_min: 数字最小值
98
+ resolution: 分辨率
99
+ storage_path: 存储路径
100
+
101
+ @author: qlsdk
102
+ @since: 0.4.0
103
+ '''
104
+ def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=32, storage_path = None):
105
+ # edf文件参数
11
106
  self.physical_max = physical_max
12
107
  self.physical_min = physical_min
13
108
  self.digital_max = digital_max
14
109
  self.digital_min = digital_min
15
- self.channels = None
16
- self._cache = Queue()
110
+ # 点分辨率
17
111
  self.resolution = resolution
112
+ # eeg通道数
113
+ self.eeg_channels = None
114
+ # eeg采样率
115
+ self.eeg_sample_rate = 500
116
+ self.acc_channels = None
117
+ self.acc_sample_rate = 50
118
+ # 缓存
119
+ self._cache = Queue()
120
+ # 采样频率
18
121
  self.sample_frequency = sample_frequency
19
122
  # bytes per second
20
123
  self.bytes_per_second = 0
@@ -38,6 +141,7 @@ class EdfHandler(object):
38
141
  self._total_packets = 0
39
142
  self._lost_packets = 0
40
143
  self._storage_path = storage_path
144
+ self._edf_writer_thread = None
41
145
 
42
146
  @property
43
147
  def file_name(self):
@@ -66,95 +170,55 @@ class EdfHandler(object):
66
170
  def set_patient_name(self, patient_name):
67
171
  self._patient_name = patient_name
68
172
 
69
- def append(self, data):
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}")
180
+
70
181
  if data:
71
- # 通道数
72
- if self._first_pkg_id is None:
73
- self.channels = data.eeg_ch_count
74
- self._first_pkg_id = data.pkg_id
75
- self._first_timestamp = data.time_stamp
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
76
188
 
77
- if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
78
- self._lost_packets += data.pkg_id - self._last_pkg_id - 1
79
- logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
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}")
80
192
 
81
193
  self._last_pkg_id = data.pkg_id
82
194
  self._total_packets += 1
83
195
 
84
196
  # 数据
85
197
  self._cache.put(data)
86
- if not self._recording:
87
- self.start()
198
+ self._edf_writer_thread.append(data)
199
+ # if not self._recording:
200
+ # self.start()
88
201
 
89
202
  def trigger(self, data):
90
203
  pass
91
-
92
- def start(self):
93
- self._recording = True
94
- record_thread = Thread(target=self._consumer)
95
- record_thread.start()
96
204
 
97
- def _consumer(self):
98
- logger.debug(f"开始消费数据 _consumer: {self._cache.qsize()}")
99
- while True:
100
- if self._recording or (not self._cache.empty()):
101
- data = self._cache.get()
102
- if data is None:
103
- break
104
- # 处理数据
105
- self._points += len(data.eeg[0])
106
- self._write_file(data.eeg)
107
- else:
108
- break
109
-
110
- self.close()
111
-
112
- def _write_file(self, data):
113
- try:
114
- if self._edf_writer is None:
115
- self.initialize_edf()
116
-
117
- if (self._chunk.size == 0):
118
- self._chunk = np.asarray(data)
119
- else:
120
- self._chunk = np.hstack((self._chunk, data))
121
-
122
- if self._chunk.size >= self.sample_frequency * self.channels:
123
- self._write_chunk(self._chunk[:self.sample_frequency])
124
- self._chunk = self._chunk[self.sample_frequency:]
125
-
126
- except Exception as e:
127
- logger.error(f"写入数据异常: {str(e)}")
128
-
129
- def close(self):
130
- self._recording = False
131
- if self._edf_writer:
132
- self._end_time = datetime.now().timestamp()
133
- self._edf_writer.writeAnnotation(0, 1, "start recording ")
134
- self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
135
- self._edf_writer.close()
136
-
137
- logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
138
-
139
-
140
-
141
- def initialize_edf(self):
205
+ def init_edf_writer(self):
142
206
  # 创建EDF+写入器
143
- self._edf_writer = EdfWriter(
207
+ edf_writer = EdfWriter(
144
208
  self.file_name,
145
- self.channels,
209
+ self.eeg_channels,
146
210
  file_type=self.file_type
147
211
  )
148
212
 
149
213
  # 设置头信息
150
- self._edf_writer.setPatientCode(self._patient_code)
151
- self._edf_writer.setPatientName(self._patient_name)
152
- self._edf_writer.setEquipment(self._device_type)
153
- self._edf_writer.setStartdatetime(datetime.now())
214
+ edf_writer.setPatientCode(self._patient_code)
215
+ edf_writer.setPatientName(self._patient_name)
216
+ edf_writer.setEquipment(self._device_type)
217
+ edf_writer.setStartdatetime(datetime.now())
154
218
 
155
219
  # 配置通道参数
156
220
  signal_headers = []
157
- for ch in range(self.channels):
221
+ for ch in range(self.eeg_channels):
158
222
  signal_headers.append({
159
223
  "label": f'channels {ch + 1}',
160
224
  "dimension": 'uV',
@@ -165,14 +229,8 @@ class EdfHandler(object):
165
229
  "digital_max": self.digital_max
166
230
  })
167
231
 
168
- self._edf_writer.setSignalHeaders(signal_headers)
169
-
170
- def _write_chunk(self, chunk):
171
- logger.debug(f"写入数据: {chunk}")
172
- # 转换数据类型为float64(pyedflib要求)
173
- data_float64 = chunk.astype(np.float64)
174
- # 写入时转置为(样本数, 通道数)格式
175
- self._edf_writer.writeSamples(data_float64)
176
- self._duration += 1
232
+ edf_writer.setSignalHeaders(signal_headers)
233
+
234
+ return edf_writer
177
235
 
178
236
 
qlsdk/rsc/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from .discover import *
2
+
3
+ from .entity import DeviceParser, QLDevice
4
+ from .command import *
5
+ from .device_manager import DeviceContainer
6
+ from .proxy import DeviceProxy
7
+ from .paradigm import *
@@ -0,0 +1,214 @@
1
+ import abc
2
+ from typing import Dict, Type
3
+ from enum import Enum
4
+ from loguru import logger
5
+ from qlsdk.core import crc16
6
+
7
+ class UDPCommand(Enum):
8
+ CONNECT = 0x10
9
+ NOTIFY = 0x09
10
+
11
+
12
+ class DeviceCommand(abc.ABC):
13
+ # 消息头
14
+ HEADER_PREFIX = b'\x5A\xA5'
15
+ # 消息头总长度 2(prefix) +1(pkgType) +1(deviceType) +4(deviceId) +4(len) +2(cmd)
16
+ HEADER_LEN = 14
17
+ # 消息指令码位置
18
+ CMD_POS = 12
19
+
20
+ def __init__(self, device):
21
+ self.device = device
22
+ @property
23
+ @abc.abstractmethod
24
+ def cmd_code(self) -> int:
25
+ pass
26
+
27
+ @classmethod
28
+ def pack(cls, body=b'') -> bytes:
29
+ # header+body+checksum
30
+ header = DeviceCommand.build_header(cls.cmd_code, len(body))
31
+ 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"""
37
+ return (
38
+ DeviceCommand.HEADER_PREFIX
39
+ + pkg_type.to_bytes(1, 'little')
40
+ + device_type.to_bytes(1, 'little')
41
+ + device_id.to_bytes(4, 'little')
42
+ + (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
43
+ + cmd_code.to_bytes(2, 'little')
44
+ )
45
+
46
+ @staticmethod
47
+ def calculate_checksum(data: bytes) -> bytes:
48
+ return crc16(data).to_bytes(2, 'little')
49
+
50
+ @abc.abstractmethod
51
+ def parse_body(self, body: bytes):
52
+ """Implement in subclasses for command-specific parsing"""
53
+ pass
54
+
55
+ class CommandFactory:
56
+ """Registry for command implementations"""
57
+ _commands: Dict[int, Type[DeviceCommand]] = {}
58
+
59
+ @classmethod
60
+ def register_command(cls, code: int, command: Type[DeviceCommand]):
61
+ cls._commands[code] = command
62
+
63
+ @classmethod
64
+ def create_command(cls, code: int) -> Type[DeviceCommand]:
65
+ logger.debug(f"Creating command for code: {hex(code)}")
66
+ if code not in cls._commands:
67
+ logger.warning(f"Unsupported command code: {hex(code)}")
68
+ return cls._commands[DefaultCommand.cmd_code]
69
+ return cls._commands[code]
70
+
71
+ # =============================================================================
72
+ class DefaultCommand(DeviceCommand):
73
+ cmd_code = 0x00
74
+
75
+ def parse_body(self, body: bytes):
76
+ # Response parsing example: 2 bytes version + 4 bytes serial
77
+ logger.info(f"Received body len: {len(body)}")
78
+
79
+ class GetDeviceInfoCommand(DeviceCommand):
80
+ cmd_code = 0x17
81
+
82
+ def parse_body(self, body: bytes):
83
+ logger.info(f"Received GetDeviceInfoCommand body len: {len(body)}")
84
+ # time - 8b
85
+ self.device.connect_time = int.from_bytes(body[0:8], 'little')
86
+ self.device.current_time = self.device.connect_time
87
+ # result - 1b
88
+ result = body[8]
89
+ # deviceId - 4b
90
+ self.device.device_id = body[9:13].hex()
91
+ # deviceType - 4b
92
+ self.device.device_type = body[13:17].hex()
93
+ # softVersion - 4b
94
+ self.device.software_version = body[17:21].hex()
95
+ # hardVersion - 4b
96
+ self.device.hardware_version = body[21:25].hex()
97
+ # deviceName - 16b
98
+ self.device.device_name = body[25:41].decode('utf-8').rstrip('\x00')
99
+ # flag - 4b
100
+ flag = int.from_bytes(body[41:45], 'little')
101
+ logger.debug(f"Received device info: {result}, {flag}, {self.device}")
102
+
103
+
104
+ # 握手
105
+ class HandshakeCommand(DeviceCommand):
106
+ cmd_code = 0x01
107
+
108
+ def parse_body(self, body: bytes):
109
+ logger.info(f"Received handshake response: {body.hex()}")
110
+
111
+ # 查询电量
112
+ class QueryBatteryCommand(DeviceCommand):
113
+ cmd_code = 0x16
114
+ def parse_body(self, body: bytes):
115
+ logger.info(f"Received QueryBatteryCommand body len: {len(body)}")
116
+ # time - 8b
117
+ self.device.current_time = int.from_bytes(body[0:8], 'little')
118
+ # result - 1b
119
+ result = body[8]
120
+ # 更新设备信息
121
+ if result == 0:
122
+ # voltage - 2b mV
123
+ self.device.voltage = int.from_bytes(body[9:11], 'little')
124
+ # soc - 1b
125
+ self.device.battery_remain = body[11]
126
+ # soh - 1b
127
+ self.device.battery_total = body[12]
128
+ # state - 1b
129
+ # state = body[13]
130
+ else:
131
+ logger.warning(f"QueryBatteryCommand message received but result is failed.")
132
+
133
+ # 设置采集参数
134
+ class SetAcquisitionParamCommand(DeviceCommand):
135
+ cmd_code = 0x451
136
+ def parse_body(self, body: bytes):
137
+ logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
138
+
139
+ # 启动采集
140
+ class StartAcquisitionCommand(DeviceCommand):
141
+ cmd_code = 0x452
142
+ def parse_body(self, body: bytes):
143
+ logger.info(f"Received acquisition start response: {body.hex()}")
144
+
145
+ # 停止采集
146
+ class StopAcquisitionCommand(DeviceCommand):
147
+ cmd_code = 0x453
148
+
149
+ def parse_body(self, body: bytes):
150
+ logger.info(f"Received acquisition stop response: {body.hex()}")
151
+ # 设置阻抗采集参数
152
+ class SetImpedanceParamCommand(DeviceCommand):
153
+ cmd_code = 0x411
154
+ def parse_body(self, body: bytes):
155
+ logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
156
+ # 启动采集
157
+ class StartImpedanceCommand(DeviceCommand):
158
+ cmd_code = 0x412
159
+ def parse_body(self, body: bytes):
160
+ logger.info(f"Received StartImpedanceCommand response: {body.hex()}")
161
+
162
+ # 停止采集
163
+ class StopImpedanceCommand(DeviceCommand):
164
+ cmd_code = 0x413
165
+
166
+ def parse_body(self, body: bytes):
167
+ logger.info(f"Received StopImpedanceCommand response: {body.hex()}")
168
+
169
+ # 启动刺激
170
+ class StartStimulationCommand(DeviceCommand):
171
+ cmd_code = 0x48C
172
+ def parse_body(self, body: bytes):
173
+ logger.info(f"Received acquisition start response: {body.hex()}")
174
+
175
+ # 停止刺激
176
+ class StopStimulationCommand(DeviceCommand):
177
+ cmd_code = 0x488
178
+
179
+ def parse_body(self, body: bytes):
180
+ logger.info(f"Received acquisition stop response: {body.hex()}")
181
+
182
+ # 阻抗数据
183
+ class ImpedanceDataCommand(DeviceCommand):
184
+ cmd_code = 0x415
185
+
186
+ def parse_body(self, body: bytes):
187
+ logger.info(f"Received impedance data: {body.hex()}")
188
+
189
+ # 信号数据
190
+ class SignalDataCommand(DeviceCommand):
191
+ cmd_code = 0x455
192
+
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)
197
+
198
+ # =============================================================================
199
+ # Command Registration
200
+ # =============================================================================
201
+
202
+ CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
203
+ CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
204
+ CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
205
+ CommandFactory.register_command(QueryBatteryCommand.cmd_code, QueryBatteryCommand)
206
+ CommandFactory.register_command(SetAcquisitionParamCommand.cmd_code, SetAcquisitionParamCommand)
207
+ CommandFactory.register_command(StartAcquisitionCommand.cmd_code, StartAcquisitionCommand)
208
+ CommandFactory.register_command(StopAcquisitionCommand.cmd_code, StopAcquisitionCommand)
209
+ CommandFactory.register_command(SetImpedanceParamCommand.cmd_code, SetImpedanceParamCommand)
210
+ CommandFactory.register_command(StartImpedanceCommand.cmd_code, StartImpedanceCommand)
211
+ CommandFactory.register_command(StopImpedanceCommand.cmd_code, StopImpedanceCommand)
212
+ CommandFactory.register_command(StartStimulationCommand.cmd_code, StartStimulationCommand)
213
+ CommandFactory.register_command(ImpedanceDataCommand.cmd_code, ImpedanceDataCommand)
214
+ CommandFactory.register_command(SignalDataCommand.cmd_code, SignalDataCommand)