qlsdk2 0.3.0a2__py3-none-any.whl → 0.4.0__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/__init__.py CHANGED
@@ -5,6 +5,7 @@ from .ar4m import AR4M
5
5
  from .ar4 import AR4
6
6
  from .x8 import X8
7
7
  from .x8m import X8M
8
+ from .rsc import *
8
9
 
9
10
  __all__ = ['AR4M', 'AR4', 'AR4Packet', 'X8']
10
11
 
qlsdk/core/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from qlsdk.core.crc import *
2
+ from qlsdk.core.message import *
3
+ from qlsdk.core.local import *
4
+ from qlsdk.core.utils import *
5
+ from qlsdk.core.entity import *
@@ -0,0 +1,5 @@
1
+ from .crctools import crc16
2
+
3
+ # packet crc validate
4
+ def check_crc(data):
5
+ return int.from_bytes(data[-2:], 'little') == crc16(data[:-2])
@@ -0,0 +1,95 @@
1
+ # len=256
2
+ CRC_HI = [
3
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
4
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
5
+ 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
6
+ 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
7
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
8
+ 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
9
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
10
+ 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
11
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
12
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
13
+ 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
14
+ 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
15
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
16
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
17
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
18
+ 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
19
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
20
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
21
+ 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
22
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
23
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
24
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
25
+ 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
26
+ 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
27
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
28
+ 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
29
+ ]
30
+
31
+ # len=256
32
+ CRC_LO = [
33
+ 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
34
+ 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
35
+ 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
36
+ 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
37
+ 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
38
+ 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
39
+ 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
40
+ 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
41
+ 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
42
+ 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
43
+ 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
44
+ 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
45
+ 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
46
+ 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
47
+ 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
48
+ 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
49
+ 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
50
+ 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
51
+ 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
52
+ 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
53
+ 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
54
+ 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
55
+ 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
56
+ 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
57
+ 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
58
+ 0x43, 0x83, 0x41, 0x81, 0x80, 0x40
59
+ ]
60
+
61
+ def crc16(data):
62
+ crcHi = 0xFF
63
+ crcLo = 0xFF
64
+ for i in range(len(data)):
65
+ index = crcHi ^ data[i]
66
+ crcHi = crcLo ^ CRC_HI[index]
67
+ crcLo = CRC_LO[index]
68
+
69
+ return crcHi << 8 | crcLo
70
+
71
+ class CRCEnum(object):
72
+ CRC8 = 8
73
+ CRC16 = 16
74
+ CRC32 = 32
75
+
76
+ class CRC(object):
77
+ def __init__(self, width=CRCEnum.CRC16) :
78
+ self.width = width
79
+ self.crcHi = 0xFF
80
+ self.crcLo = 0xFF
81
+
82
+ def _init(self):
83
+ if self.width == CRCEnum.CRC16:
84
+ self.crcHi = 0xFF
85
+ self.crcLo = 0xFF
86
+
87
+ def calc(self, data):
88
+ for i in range(len(data)):
89
+ index = self.crcHi ^ data[i]
90
+ self.crcHi = self.crcLo ^ CRC_HI[index]
91
+ self.crcLo = CRC_LO[index]
92
+
93
+ def checksum(self):
94
+ return self.crcHi << 8 | self.crcLo
95
+
qlsdk/core/device.py ADDED
@@ -0,0 +1,25 @@
1
+ # from abc import ABC, abstractmethod
2
+
3
+ class BaseDevice(object):
4
+ def __init__(self, socket = None):
5
+ self.socket = socket
6
+ self.device_name = None
7
+ self.device_type = None
8
+ self.device_id = None
9
+
10
+ @property
11
+ def acq_channels(self) :
12
+ return None
13
+ @property
14
+ def sample_range(self) -> int:
15
+ return None
16
+ @property
17
+ def sample_rate(self) -> int:
18
+ return None
19
+ @property
20
+ def sample_num(self) -> int:
21
+ return 10
22
+ @property
23
+ def resolution(self):
24
+ return 24
25
+
@@ -0,0 +1,92 @@
1
+ from qlsdk.core.utils import to_channels
2
+ from loguru import logger
3
+
4
+ class DataPacket(object):
5
+ def __init__(self, device_type, device_id, channels, data):
6
+ self.data = data
7
+ self.channels = None
8
+
9
+
10
+ class RscPacket(object):
11
+ def __init__(self):
12
+ self.time_stamp = None
13
+ self.pkg_id = None
14
+ self.result = None
15
+ self.channels = None
16
+ self.origin_sample_rate = None
17
+ self.sample_rate = None
18
+ self.sample_num = None
19
+ self.resolution = None
20
+ self.filter = None
21
+ self.data_len = None
22
+ self.trigger = None
23
+ self.eeg = None
24
+
25
+ def transfer(self, body: bytes) -> 'RscPacket':
26
+ self.time_stamp = int.from_bytes(body[0:8], 'little')
27
+ self.result = body[8]
28
+ self.pkg_id = int.from_bytes(body[9: 13], 'little')
29
+ self.channels = to_channels(body[13: 45])
30
+ self.origin_sample_rate = int.from_bytes(body[45: 49], 'little')
31
+ self.sample_rate = int.from_bytes(body[49: 53], 'little')
32
+ self.sample_num = int.from_bytes(body[53: 57], 'little')
33
+ self.resolution = int(int(body[57]) / 8)
34
+ self.filter = body[58]
35
+ self.data_len = int.from_bytes(body[59: 63], 'little')
36
+ # 步径 相同通道的点间隔
37
+ step = int(len(self.channels) * self.resolution + 4)
38
+ self.trigger = [int.from_bytes(body[i:i+4], 'little') for i in range(63, len(body) - 3, step)]
39
+ b_eeg = body[63:]
40
+ ch_num = len(self.channels)
41
+ self.eeg = [
42
+ [
43
+ int.from_bytes(b_eeg[i * self.resolution + 4 + j * step:i * self.resolution + 4 + j * step + 3], 'big', signed=True)
44
+ for j in range(self.sample_num)
45
+ ]
46
+ for i in range(ch_num)
47
+ ]
48
+
49
+ # logger.trace(self)
50
+ return self
51
+
52
+ def __str__(self):
53
+ return f"""
54
+ time_stamp: {self.time_stamp}
55
+ pkg_id: {self.pkg_id}
56
+ origin_sample_rate: {self.origin_sample_rate}
57
+ sample_rate: {self.sample_rate}
58
+ sample_num: {self.sample_num}
59
+ resolution: {self.resolution}
60
+ filter: {self.filter}
61
+ channels: {self.channels}
62
+ data len: {self.data_len}
63
+ trigger: {self.trigger}
64
+ eeg: {self.eeg}
65
+ """
66
+
67
+ class ImpedancePacket(object):
68
+ def __init__(self):
69
+ self.time_stamp = None
70
+ self.pkg_id = None
71
+ self.result = None
72
+ self.channels = None
73
+ self.data_len = None
74
+ self.impedance = None
75
+
76
+ def transfer(self, body:bytes) -> 'ImpedancePacket':
77
+ self.time_stamp = int.from_bytes(body[0:8], 'little')
78
+ self.result = body[8]
79
+ self.pkg_id = int.from_bytes(body[9: 13], 'little')
80
+ self.channels = to_channels(body[13: 45])
81
+
82
+ logger.debug(f"impedance: {self}")
83
+
84
+ def __str__(self):
85
+ return f"""
86
+ time_stamp: {self.time_stamp}
87
+ pkg_id: {self.pkg_id}
88
+ result: {self.result}
89
+ channels: {self.channels}
90
+ data len: {self.data_len}
91
+ impedance: {self.impedance}
92
+ """
File without changes
@@ -0,0 +1 @@
1
+ from .norch import notch_filter_50hz
@@ -0,0 +1,59 @@
1
+ import numpy as np
2
+
3
+ def notch_filter_50hz(data: np.ndarray,
4
+ fs: float,
5
+ notch_width: float = 2.0,
6
+ max_harmonics: int = 5) -> np.ndarray:
7
+ """
8
+ 多通道50Hz谐波陷波滤波器
9
+
10
+ 参数:
11
+ data : 输入信号,形状为 [通道数, 采样点数] 的二维数组
12
+ fs : 采样频率 (Hz)
13
+ notch_width : 陷波带宽 (Hz),默认2Hz
14
+ max_harmonics : 最大谐波次数,默认处理前10次谐波
15
+
16
+ 返回:
17
+ 滤波后的信号,形状与输入相同
18
+ """
19
+ # 输入校验
20
+ if data.ndim != 2:
21
+ raise ValueError("输入必须为二维数组 [channels, samples]")
22
+ if fs <= 0:
23
+ raise ValueError("采样频率必须为正数")
24
+
25
+ n_channels, n_samples = data.shape
26
+ nyquist = fs / 2
27
+ processed = np.empty_like(data)
28
+
29
+ # 生成频率轴
30
+ freqs = np.fft.fftfreq(n_samples, 1/fs)
31
+
32
+ for ch in range(n_channels):
33
+ # FFT变换
34
+ fft_data = np.fft.fft(data[ch])
35
+
36
+ # 生成陷波掩模
37
+ mask = np.ones(n_samples, dtype=bool)
38
+
39
+ # 计算需要消除的谐波
40
+ for k in range(1, max_harmonics+1):
41
+ target_freq = 50 * k
42
+
43
+ # 超过奈奎斯特频率则停止
44
+ if target_freq > nyquist:
45
+ break
46
+
47
+ # 生成陷波范围
48
+ notch_range = (np.abs(freqs - target_freq) <= notch_width/2) | \
49
+ (np.abs(freqs + target_freq) <= notch_width/2)
50
+
51
+ mask &= ~notch_range
52
+
53
+ # 应用频域滤波
54
+ filtered_fft = fft_data * mask
55
+
56
+ # 逆变换并取实数部分
57
+ processed[ch] = np.real(np.fft.ifft(filtered_fft))
58
+
59
+ return processed
qlsdk/core/local.py ADDED
@@ -0,0 +1,34 @@
1
+ import os
2
+ import socket
3
+ from time import time
4
+
5
+ # 读取本机全部ip列表
6
+ # return list
7
+ def get_ips():
8
+ return socket.gethostbyname_ex(socket.gethostname())[-1]
9
+
10
+ # 读取本机的ip地址
11
+ # return str
12
+ def get_ip():
13
+ # 优先读取活跃ip地址
14
+ routes = os.popen('route print').readlines()
15
+ for idx, item in enumerate(routes):
16
+ if ' 0.0.0.0 ' in item and len(item.split()) > 2:
17
+ return item.split()[-2]
18
+
19
+ # 取第一个地址
20
+ ips = get_ips()
21
+ if len(ips) > 0 :
22
+ return ips[0]
23
+
24
+ raise ValueError("Ip address not exists.")
25
+
26
+ def get_cache(fname=None):
27
+ if fname is None:
28
+ fname = int(time())
29
+
30
+ cpath = os.path.abspath(os.path.abspath(__file__))
31
+ print(cpath)
32
+
33
+ if __name__ == '__main__':
34
+ get_cache()
@@ -0,0 +1,2 @@
1
+ from .command import *
2
+ from .udp import UDPMessage
@@ -0,0 +1,324 @@
1
+ import abc
2
+ from time import time_ns
3
+ from typing import Dict, Type
4
+ from enum import Enum
5
+ from loguru import logger
6
+ from qlsdk.core.crc import crc16
7
+ from qlsdk.core.device import BaseDevice
8
+ from qlsdk.core.entity import RscPacket, ImpedancePacket
9
+ from qlsdk.core.utils import to_channels, to_bytes
10
+
11
+ class DeviceCommand(abc.ABC):
12
+ # 消息头
13
+ HEADER_PREFIX = b'\x5A\xA5'
14
+ # 消息头总长度 2(prefix) +1(pkgType) +1(deviceType) +4(deviceId) +4(len) +2(cmd)
15
+ HEADER_LEN = 14
16
+ # 消息指令码位置
17
+ CMD_POS = 12
18
+
19
+ def __init__(self, device: BaseDevice):
20
+ self.device = device
21
+
22
+ @classmethod
23
+ def build(cls, device) :
24
+ return cls(device)
25
+
26
+ @property
27
+ @abc.abstractmethod
28
+ def cmd_code(self) -> int:
29
+ pass
30
+ @property
31
+ @abc.abstractmethod
32
+ def cmd_desc(self) -> str:
33
+ pass
34
+
35
+ @staticmethod
36
+ def checksum(data: bytes) -> bytes:
37
+ return crc16(data).to_bytes(2, 'little')
38
+
39
+ def pack(self, body=b'') -> bytes:
40
+ # header+body+checksum
41
+ body = self.pack_body()
42
+ header = self.pack_header(len(body))
43
+ payload = header + body
44
+ return payload + DeviceCommand.checksum(payload)
45
+ def pack_body(self) -> bytes:
46
+ """构建消息体"""
47
+ return b''
48
+ def pack_header(self, body_len: int) -> bytes:
49
+ device_id = int(self.device.device_id) if self.device and self.device.device_id else 0
50
+ device_type = int(self.device.device_type) if self.device and self.device.device_type else 0
51
+
52
+ """构建消息头"""
53
+ return (
54
+ DeviceCommand.HEADER_PREFIX
55
+ + int(2).to_bytes(1, 'little') # pkgType
56
+ + device_type.to_bytes(1, 'little')
57
+ + device_id.to_bytes(4, 'little')
58
+ + (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
59
+ + self.cmd_code.to_bytes(2, 'little')
60
+ )
61
+
62
+ def unpack(self, payload: bytes) -> bytes:
63
+ """解析消息体"""
64
+ # 解析消息体
65
+ body = payload[self.HEADER_LEN:-2]
66
+
67
+ def parse_body(self, body: bytes):
68
+ time = int.from_bytes(body[0:8], 'little')
69
+ # result - 1B
70
+ result = body[8]
71
+ logger.info(f"[{time}]{self.cmd_desc}{'成功' if result == 0 else '失败'}")
72
+
73
+
74
+
75
+ class CommandFactory:
76
+ """Registry for command implementations"""
77
+ _commands: Dict[int, Type[DeviceCommand]] = {}
78
+
79
+ @classmethod
80
+ def register_command(cls, code: int, command: Type[DeviceCommand]):
81
+ cls._commands[code] = command
82
+
83
+ @classmethod
84
+ def create_command(cls, code: int) -> Type[DeviceCommand]:
85
+ logger.trace(f"Creating command for code: {hex(code)}")
86
+ if code not in cls._commands:
87
+ logger.warning(f"不支持的设备指令: {hex(code)}")
88
+ return cls._commands[DefaultCommand.cmd_code]
89
+ return cls._commands[code]
90
+
91
+ # =============================================================================
92
+ class DefaultCommand(DeviceCommand):
93
+ cmd_code = 0x00
94
+ cmd_desc = "未定义"
95
+
96
+ def parse_body(self, body: bytes):
97
+ # Response parsing example: 2 bytes version + 4 bytes serial
98
+ logger.info(f"Received body len: {len(body)}")
99
+
100
+ class GetDeviceInfoCommand(DeviceCommand):
101
+ cmd_code = 0x17
102
+ cmd_desc = "设备信息"
103
+
104
+ def parse_body(self, body: bytes):
105
+ # time - 8B
106
+ self.device.connect_time = int.from_bytes(body[0:8], 'little')
107
+ self.device.current_time = self.device.connect_time
108
+ # result - 1B
109
+ result = body[8]
110
+ # deviceId - 4B
111
+ self.device.device_id = int.from_bytes(body[9:13], 'big')
112
+ # deviceType - 4B
113
+ self.device.device_type = int.from_bytes(body[13:17], 'little')
114
+ # softVersion - 4B
115
+ self.device.software_version = body[17:21].hex()
116
+ # hardVersion - 4B
117
+ self.device.hardware_version = body[21:25].hex()
118
+ # deviceName - 16B
119
+ self.device.device_name = body[25:41].decode('utf-8').rstrip('\x00')
120
+ # flag - 4B
121
+ flag = int.from_bytes(body[41:45], 'little')
122
+ logger.debug(f"Received device info: {result}, {flag}, {self.device}")
123
+
124
+
125
+ # 握手
126
+ class HandshakeCommand(DeviceCommand):
127
+ cmd_code = 0x01
128
+ cmd_desc = "握手"
129
+
130
+ def parse_body(self, body: bytes):
131
+ logger.info(f"Received handshake response: {body.hex()}")
132
+
133
+ # 查询电量
134
+ class QueryBatteryCommand(DeviceCommand):
135
+ cmd_code = 0x16
136
+ cmd_desc = "电量信息"
137
+ def parse_body(self, body: bytes):
138
+ # time - 8b
139
+ self.device.current_time = int.from_bytes(body[0:8], 'little')
140
+ # result - 1b
141
+ result = body[8]
142
+ # 更新设备信息
143
+ if result == 0:
144
+ # voltage - 2b mV
145
+ self.device.voltage = int.from_bytes(body[9:11], 'little')
146
+ # soc - 1b
147
+ self.device.battery_remain = body[11]
148
+ # soh - 1b
149
+ self.device.battery_total = body[12]
150
+ # state - 1b
151
+ # state = body[13]
152
+ logger.debug(f"电量更新: {self.device}")
153
+ else:
154
+ logger.warning(f"QueryBatteryCommand message received but result is failed.")
155
+
156
+ # 设置采集参数
157
+ class SetAcquisitionParamCommand(DeviceCommand):
158
+ cmd_code = 0x451
159
+ cmd_desc = "设置信号采集参数"
160
+
161
+ def pack_body(self):
162
+ body = to_bytes(self.device.acq_channels)
163
+ body += self.device.sample_range.to_bytes(4, byteorder='little')
164
+ body += self.device.sample_rate.to_bytes(4, byteorder='little')
165
+ body += self.device.sample_num.to_bytes(4, byteorder='little')
166
+ body += self.device.resolution.to_bytes(1, byteorder='little')
167
+ body += bytes.fromhex('00')
168
+
169
+ return body
170
+
171
+ # 启动采集
172
+ class StartAcquisitionCommand(DeviceCommand):
173
+ cmd_code = 0x452
174
+ cmd_desc = "启动信号采集"
175
+
176
+ def pack_body(self):
177
+ return bytes.fromhex('0000')
178
+
179
+ # 停止采集
180
+ class StopAcquisitionCommand(DeviceCommand):
181
+ cmd_code = 0x453
182
+ cmd_desc = "停止信号采集"
183
+
184
+ def pack_body(self):
185
+ return b''
186
+
187
+
188
+ # 设置阻抗采集参数
189
+ class SetImpedanceParamCommand(DeviceCommand):
190
+ cmd_code = 0x411
191
+ cmd_desc = "设置阻抗测量参数"
192
+
193
+ # 启动阻抗测量
194
+ class StartImpedanceCommand(DeviceCommand):
195
+ cmd_code = 0x412
196
+ cmd_desc = "启动阻抗测量"
197
+ def pack_body(self):
198
+ body = bytes.fromhex('0000')
199
+ body += to_bytes(self.device.acq_channels)
200
+ body += bytes.fromhex('0000000000000000') # 8字节占位符
201
+ return body
202
+
203
+
204
+ # 停止阻抗测量
205
+ class StopImpedanceCommand(DeviceCommand):
206
+ cmd_code = 0x413
207
+ cmd_desc = "停止阻抗测量"
208
+
209
+ def pack_body(self):
210
+ return b''
211
+
212
+ # 启动刺激
213
+ class StartStimulationCommand(DeviceCommand):
214
+ cmd_code = 0x48C
215
+ cmd_desc = "启动刺激"
216
+ def pack_body(self):
217
+ return self.device.stim_paradigm.to_bytes()
218
+ # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
219
+ def parse_body(self, body: bytes):
220
+ # time - 8B
221
+ time = int.from_bytes(body[0:8], 'little')
222
+ # result - 1B
223
+ result = body[8]
224
+ # error_channel - 8B
225
+ # error_channel= int.from_bytes(body[9:17], 'big')
226
+ channels = to_channels(body[9:17])
227
+ logger.success(f"通道 {channels} 刺激开始")
228
+ self.device.trigger(f"通道 {channels} 刺激开始")
229
+ # error_type - 1B
230
+ error_type = body[17]
231
+
232
+ # 停止刺激
233
+ class StopStimulationCommand(DeviceCommand):
234
+ cmd_code = 0x488
235
+ cmd_desc = "停止刺激"
236
+
237
+ # 启动刺激
238
+ class StopStimulationNotifyCommand(DeviceCommand):
239
+ cmd_code = 0x48D
240
+ cmd_desc = "停止刺激通知"
241
+ def pack_body(self):
242
+ return self.device.stim_paradigm.to_bytes()
243
+ # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
244
+ def parse_body(self, body: bytes):
245
+ # time - 8B
246
+ time = int.from_bytes(body[0:8], 'little')
247
+ # result - 1B
248
+ result = body[8]
249
+ # error_channel - 8B
250
+ # error_channel= int.from_bytes(body[9:17], 'big')
251
+ channels = to_channels(body[9:17])
252
+ logger.success(f"通道 {channels} 刺激结束")
253
+ self.device.trigger(f"通道 {channels} 刺激结束", time)
254
+ # error_type - 1B
255
+ error_type = body[17]
256
+ # 刺激信息
257
+ class StimulationInfoCommand(DeviceCommand):
258
+ cmd_code = 0x48e
259
+ cmd_desc = "刺激告警信息"
260
+
261
+ def parse_body(self, body: bytes):
262
+ time = int.from_bytes(body[0:8], 'little')
263
+ # result - 1B
264
+ result = body[8]
265
+ # error_channel - 8B
266
+ channels = to_channels(body[9:17])
267
+ # 保留位-8B
268
+ # error_type - 1B
269
+ err_type = body[17]
270
+ # 特征位-4B
271
+ # errType = int.from_bytes(body[25:29], 'little')
272
+ logger.warning(f"刺激告警信息[{err_type}],通道 {channels} 刺激驱动不足")
273
+
274
+
275
+ # 阻抗数据
276
+ class ImpedanceDataCommand(DeviceCommand):
277
+ cmd_code = 0x415
278
+ cmd_desc = "阻抗数据"
279
+
280
+ def parse_body(self, body: bytes):
281
+ logger.info(f"Received impedance data: {body.hex()}")
282
+ packet = ImpedancePacket().transfer(body)
283
+
284
+ # 信号数据
285
+ class SignalDataCommand(DeviceCommand):
286
+ cmd_code = 0x455
287
+ cmd_desc = "信号数据"
288
+
289
+ def unpack(self, payload):
290
+ return super().unpack(payload)
291
+
292
+ def parse_body(self, body: bytes):
293
+ if len(self.device.signal_consumers) > 0 or self.device.edf_handler:
294
+ # 解析数据包
295
+ packet = RscPacket()
296
+ packet.transfer(body)
297
+
298
+ # 发送数据包到订阅者
299
+ for q in list(self.device.signal_consumers.values()):
300
+ q.put(packet)
301
+
302
+ # 文件写入到edf
303
+ if self.device.edf_handler:
304
+ self.device.edf_handler.write(packet)
305
+
306
+ # =============================================================================
307
+ # 指令实现类注册到指令工厂
308
+ # =============================================================================
309
+ CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
310
+ CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
311
+ CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
312
+ CommandFactory.register_command(QueryBatteryCommand.cmd_code, QueryBatteryCommand)
313
+ CommandFactory.register_command(SetAcquisitionParamCommand.cmd_code, SetAcquisitionParamCommand)
314
+ CommandFactory.register_command(StartAcquisitionCommand.cmd_code, StartAcquisitionCommand)
315
+ CommandFactory.register_command(StopAcquisitionCommand.cmd_code, StopAcquisitionCommand)
316
+ CommandFactory.register_command(SetImpedanceParamCommand.cmd_code, SetImpedanceParamCommand)
317
+ CommandFactory.register_command(StartImpedanceCommand.cmd_code, StartImpedanceCommand)
318
+ CommandFactory.register_command(StopImpedanceCommand.cmd_code, StopImpedanceCommand)
319
+ CommandFactory.register_command(StartStimulationCommand.cmd_code, StartStimulationCommand)
320
+ CommandFactory.register_command(StimulationInfoCommand.cmd_code, StimulationInfoCommand)
321
+ CommandFactory.register_command(ImpedanceDataCommand.cmd_code, ImpedanceDataCommand)
322
+ CommandFactory.register_command(SignalDataCommand.cmd_code, SignalDataCommand)
323
+
324
+
File without changes