qlsdk2 0.3.0a2__tar.gz → 0.4.0a1__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.3.0a2 → qlsdk2-0.4.0a1}/PKG-INFO +1 -1
  2. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/setup.py +4 -10
  3. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/__init__.py +1 -0
  4. qlsdk2-0.4.0a1/src/qlsdk/core/__init__.py +4 -0
  5. qlsdk2-0.4.0a1/src/qlsdk/core/crc/__init__.py +5 -0
  6. qlsdk2-0.4.0a1/src/qlsdk/core/crc/crctools.py +95 -0
  7. qlsdk2-0.4.0a1/src/qlsdk/core/device.py +25 -0
  8. qlsdk2-0.4.0a1/src/qlsdk/core/entity/__init__.py +92 -0
  9. qlsdk2-0.4.0a1/src/qlsdk/core/exception.py +0 -0
  10. qlsdk2-0.4.0a1/src/qlsdk/core/filter/__init__.py +1 -0
  11. qlsdk2-0.4.0a1/src/qlsdk/core/filter/norch.py +59 -0
  12. qlsdk2-0.4.0a1/src/qlsdk/core/local.py +34 -0
  13. qlsdk2-0.4.0a1/src/qlsdk/core/message/__init__.py +2 -0
  14. qlsdk2-0.4.0a1/src/qlsdk/core/message/command.py +293 -0
  15. qlsdk2-0.4.0a1/src/qlsdk/core/message/tcp.py +0 -0
  16. qlsdk2-0.4.0a1/src/qlsdk/core/message/udp.py +96 -0
  17. qlsdk2-0.4.0a1/src/qlsdk/core/utils.py +68 -0
  18. qlsdk2-0.4.0a1/src/qlsdk/persist/__init__.py +2 -0
  19. qlsdk2-0.4.0a1/src/qlsdk/persist/rsc_edf.py +236 -0
  20. qlsdk2-0.4.0a1/src/qlsdk/rsc/__init__.py +7 -0
  21. qlsdk2-0.4.0a1/src/qlsdk/rsc/command/__init__.py +214 -0
  22. qlsdk2-0.4.0a1/src/qlsdk/rsc/command/message.py +239 -0
  23. qlsdk2-0.4.0a1/src/qlsdk/rsc/device_manager.py +96 -0
  24. qlsdk2-0.4.0a1/src/qlsdk/rsc/discover.py +86 -0
  25. qlsdk2-0.4.0a1/src/qlsdk/rsc/eegion.py +360 -0
  26. qlsdk2-0.4.0a1/src/qlsdk/rsc/entity.py +552 -0
  27. qlsdk2-0.4.0a1/src/qlsdk/rsc/paradigm.py +310 -0
  28. qlsdk2-0.4.0a1/src/qlsdk/rsc/proxy.py +76 -0
  29. qlsdk2-0.4.0a1/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
  30. qlsdk2-0.4.0a1/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
  31. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk2.egg-info/PKG-INFO +1 -1
  32. qlsdk2-0.4.0a1/src/qlsdk2.egg-info/SOURCES.txt +46 -0
  33. qlsdk2-0.4.0a1/test/test.py +168 -0
  34. qlsdk2-0.3.0a2/src/qlsdk/persist/__init__.py +0 -1
  35. qlsdk2-0.3.0a2/src/qlsdk2.egg-info/SOURCES.txt +0 -19
  36. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/README.md +0 -0
  37. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/setup.cfg +0 -0
  38. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/ar4/__init__.py +0 -0
  39. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/ar4m/__init__.py +0 -0
  40. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/persist/edf.py +0 -0
  41. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/sdk/__init__.py +0 -0
  42. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/sdk/ar4sdk.py +0 -0
  43. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/sdk/hub.py +0 -0
  44. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/x8/__init__.py +0 -0
  45. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk/x8m/__init__.py +0 -0
  46. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
  47. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk2.egg-info/requires.txt +0 -0
  48. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/src/qlsdk2.egg-info/top_level.txt +0 -0
  49. {qlsdk2-0.3.0a2 → qlsdk2-0.4.0a1}/test/test_ar4m.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qlsdk2
3
- Version: 0.3.0a2
3
+ Version: 0.4.0a1
4
4
  Summary: SDK for quanlan device
5
5
  Home-page: https://github.com/hehuajun/qlsdk
6
6
  Author: hehuajun
@@ -6,7 +6,7 @@ with open("README.md", "r") as fh:
6
6
 
7
7
  setuptools.setup(
8
8
  name="qlsdk2",
9
- version="0.3.0a2",
9
+ version="0.4.0a1",
10
10
  author="hehuajun",
11
11
  author_email="hehuajun@eegion.com",
12
12
  description="SDK for quanlan device",
@@ -24,13 +24,7 @@ setuptools.setup(
24
24
  install_requires=open("requirements.txt").read().splitlines(),
25
25
  include_package_data=True,
26
26
  package_data={
27
- # "src/qlsdk/ar4m/": ["libs/*.dll"],
28
- "qlsdk/sdk": ["libs/*.dll"],
29
- "":["*.txt", "*.md"]
30
- },
31
- # entry_points={
32
- # 'console_scripts': [
33
- # 'qlsdk-cli=qlsdk.cli:main', # 如果有命令行工具
34
- # ],
35
- # },
27
+ # "qlsdk2": ["/**/*.dll"],
28
+ "qlsdk": ["./**/*.dll"]
29
+ }
36
30
  )
@@ -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
 
@@ -0,0 +1,4 @@
1
+ from qlsdk.core.crc import *
2
+ from qlsdk.core.message import *
3
+ from qlsdk.core.local import *
4
+ from qlsdk.core.utils 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
+
@@ -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
@@ -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,293 @@
1
+ import abc
2
+ from typing import Dict, Type
3
+ from enum import Enum
4
+ from loguru import logger
5
+ from qlsdk.core.crc import crc16
6
+ from qlsdk.core.device import BaseDevice
7
+ from qlsdk.core.entity import RscPacket, ImpedancePacket
8
+ from qlsdk.core.utils import to_channels, to_bytes
9
+
10
+ class DeviceCommand(abc.ABC):
11
+ # 消息头
12
+ HEADER_PREFIX = b'\x5A\xA5'
13
+ # 消息头总长度 2(prefix) +1(pkgType) +1(deviceType) +4(deviceId) +4(len) +2(cmd)
14
+ HEADER_LEN = 14
15
+ # 消息指令码位置
16
+ CMD_POS = 12
17
+
18
+ def __init__(self, device: BaseDevice):
19
+ self.device = device
20
+
21
+ @classmethod
22
+ def build(cls, device) :
23
+ return cls(device)
24
+
25
+ @property
26
+ @abc.abstractmethod
27
+ def cmd_code(self) -> int:
28
+ pass
29
+
30
+ @staticmethod
31
+ def checksum(data: bytes) -> bytes:
32
+ return crc16(data).to_bytes(2, 'little')
33
+
34
+ def pack(self, body=b'') -> bytes:
35
+ # header+body+checksum
36
+ body = self.pack_body()
37
+ header = self.pack_header(len(body))
38
+ payload = header + body
39
+ return payload + DeviceCommand.checksum(payload)
40
+ def pack_body(self) -> bytes:
41
+ """构建消息体"""
42
+ return b''
43
+ def pack_header(self, body_len: int) -> bytes:
44
+ device_id = int(self.device.device_id) if self.device and self.device.device_id else 0
45
+ device_type = int(self.device.device_type) if self.device and self.device.device_type else 0
46
+
47
+ """构建消息头"""
48
+ return (
49
+ DeviceCommand.HEADER_PREFIX
50
+ + int(2).to_bytes(1, 'little') # pkgType
51
+ + device_type.to_bytes(1, 'little')
52
+ + device_id.to_bytes(4, 'little')
53
+ + (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
54
+ + self.cmd_code.to_bytes(2, 'little')
55
+ )
56
+
57
+ def unpack(self, payload: bytes) -> bytes:
58
+ """解析消息体"""
59
+ # 解析消息体
60
+ body = payload[self.HEADER_LEN:-2]
61
+
62
+
63
+ class CommandFactory:
64
+ """Registry for command implementations"""
65
+ _commands: Dict[int, Type[DeviceCommand]] = {}
66
+
67
+ @classmethod
68
+ def register_command(cls, code: int, command: Type[DeviceCommand]):
69
+ cls._commands[code] = command
70
+
71
+ @classmethod
72
+ def create_command(cls, code: int) -> Type[DeviceCommand]:
73
+ logger.debug(f"Creating command for code: {hex(code)}")
74
+ if code not in cls._commands:
75
+ logger.warning(f"Unsupported command code: {hex(code)}")
76
+ return cls._commands[DefaultCommand.cmd_code]
77
+ return cls._commands[code]
78
+
79
+ # =============================================================================
80
+ class DefaultCommand(DeviceCommand):
81
+ cmd_code = 0x00
82
+
83
+ def parse_body(self, body: bytes):
84
+ # Response parsing example: 2 bytes version + 4 bytes serial
85
+ logger.info(f"Received body len: {len(body)}")
86
+
87
+ class GetDeviceInfoCommand(DeviceCommand):
88
+ cmd_code = 0x17
89
+
90
+ def parse_body(self, body: bytes):
91
+ logger.info(f"Received GetDeviceInfoCommand body len: {len(body)}")
92
+ # time - 8B
93
+ self.device.connect_time = int.from_bytes(body[0:8], 'little')
94
+ self.device.current_time = self.device.connect_time
95
+ # result - 1B
96
+ result = body[8]
97
+ # deviceId - 4B
98
+ self.device.device_id = int.from_bytes(body[9:13], 'big')
99
+ # deviceType - 4B
100
+ self.device.device_type = int.from_bytes(body[13:17], 'little')
101
+ # softVersion - 4B
102
+ self.device.software_version = body[17:21].hex()
103
+ # hardVersion - 4B
104
+ self.device.hardware_version = body[21:25].hex()
105
+ # deviceName - 16B
106
+ self.device.device_name = body[25:41].decode('utf-8').rstrip('\x00')
107
+ # flag - 4B
108
+ flag = int.from_bytes(body[41:45], 'little')
109
+ logger.debug(f"Received device info: {result}, {flag}, {self.device}")
110
+
111
+
112
+ # 握手
113
+ class HandshakeCommand(DeviceCommand):
114
+ cmd_code = 0x01
115
+
116
+ def parse_body(self, body: bytes):
117
+ logger.info(f"Received handshake response: {body.hex()}")
118
+
119
+ # 查询电量
120
+ class QueryBatteryCommand(DeviceCommand):
121
+ cmd_code = 0x16
122
+ def parse_body(self, body: bytes):
123
+ logger.info(f"Received QueryBatteryCommand body len: {len(body)}")
124
+ # time - 8b
125
+ self.device.current_time = int.from_bytes(body[0:8], 'little')
126
+ # result - 1b
127
+ result = body[8]
128
+ # 更新设备信息
129
+ if result == 0:
130
+ # voltage - 2b mV
131
+ self.device.voltage = int.from_bytes(body[9:11], 'little')
132
+ # soc - 1b
133
+ self.device.battery_remain = body[11]
134
+ # soh - 1b
135
+ self.device.battery_total = body[12]
136
+ # state - 1b
137
+ # state = body[13]
138
+ else:
139
+ logger.warning(f"QueryBatteryCommand message received but result is failed.")
140
+
141
+ # 设置采集参数
142
+ class SetAcquisitionParamCommand(DeviceCommand):
143
+ cmd_code = 0x451
144
+ def pack_body(self):
145
+ body = to_bytes(self.device.acq_channels)
146
+ body += self.device.sample_range.to_bytes(4, byteorder='little')
147
+ body += self.device.sample_rate.to_bytes(4, byteorder='little')
148
+ body += self.device.sample_num.to_bytes(4, byteorder='little')
149
+ body += self.device.resolution.to_bytes(1, byteorder='little')
150
+ body += bytes.fromhex('00')
151
+
152
+ return body
153
+ def parse_body(self, body: bytes):
154
+ logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
155
+
156
+ # 启动采集
157
+ class StartAcquisitionCommand(DeviceCommand):
158
+ cmd_code = 0x452
159
+
160
+ def pack_body(self):
161
+ return bytes.fromhex('0000')
162
+ def parse_body(self, body: bytes):
163
+ logger.info(f"Received acquisition start response: {body.hex()}")
164
+
165
+ # 停止采集
166
+ class StopAcquisitionCommand(DeviceCommand):
167
+ cmd_code = 0x453
168
+
169
+ def pack_body(self):
170
+ return b''
171
+ def parse_body(self, body: bytes):
172
+ logger.info(f"Received acquisition stop response: {body.hex()}")
173
+ # 设置阻抗采集参数
174
+ class SetImpedanceParamCommand(DeviceCommand):
175
+ cmd_code = 0x411
176
+ def parse_body(self, body: bytes):
177
+ logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
178
+ # 启动采集
179
+ class StartImpedanceCommand(DeviceCommand):
180
+ cmd_code = 0x412
181
+ def pack_body(self):
182
+ body = bytes.fromhex('0000')
183
+ body += to_bytes(self.device.acq_channels)
184
+ body += bytes.fromhex('0000000000000000') # 8字节占位符
185
+ return body
186
+
187
+ def parse_body(self, body: bytes):
188
+ logger.info(f"Received StartImpedanceCommand response: {body.hex()}")
189
+
190
+ # 停止采集
191
+ class StopImpedanceCommand(DeviceCommand):
192
+ cmd_code = 0x413
193
+
194
+ def pack_body(self):
195
+ 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
+ # 启动采集
216
+ class StartStimulationCommand(DeviceCommand):
217
+ cmd_code = 0x48C
218
+ def pack_body(self):
219
+ return self.device.stim_paradigm.to_bytes()
220
+ # return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
221
+ def parse_body(self, body: bytes):
222
+ logger.info(f"Received stimulation start response: {body.hex()}")
223
+ # time - 8B
224
+ time = int.from_bytes(body[0:8], 'little')
225
+ # result - 1B
226
+ result = body[8]
227
+ # error_channel - 8B
228
+ # error_channel= int.from_bytes(body[9:17], 'big')
229
+ channels = to_channels(body[9:17])
230
+ logger.warning(f"通道 {channels} 刺激驱动不足")
231
+ # error_type - 1B
232
+ error_type = body[17]
233
+
234
+ # 停止采集
235
+ class StopStimulationCommand(DeviceCommand):
236
+ cmd_code = 0x488
237
+
238
+ def parse_body(self, body: bytes):
239
+ logger.info(f"Received stimulation stop response: {body.hex()}")
240
+
241
+ # 停止采集
242
+ class StimulationInfoCommand(DeviceCommand):
243
+ cmd_code = 0x48e
244
+
245
+ def parse_body(self, body: bytes):
246
+ logger.info(f"Received stimulation info response: {body.hex()}")
247
+
248
+
249
+ # 阻抗数据
250
+ class ImpedanceDataCommand(DeviceCommand):
251
+ cmd_code = 0x415
252
+
253
+ def parse_body(self, body: bytes):
254
+ logger.info(f"Received impedance data: {body.hex()}")
255
+ packet = ImpedancePacket().transfer(body)
256
+
257
+ # 信号数据
258
+ class SignalDataCommand(DeviceCommand):
259
+ cmd_code = 0x455
260
+
261
+ def unpack(self, payload):
262
+ return super().unpack(payload)
263
+
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)
270
+ # 发送数据包到订阅者
271
+ for q in list(self.device.signal_consumers.values()):
272
+ q.put(rsc)
273
+
274
+ # =============================================================================
275
+ # Command Registration
276
+ # =============================================================================
277
+
278
+ CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
279
+ CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
280
+ CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
281
+ CommandFactory.register_command(QueryBatteryCommand.cmd_code, QueryBatteryCommand)
282
+ CommandFactory.register_command(SetAcquisitionParamCommand.cmd_code, SetAcquisitionParamCommand)
283
+ CommandFactory.register_command(StartAcquisitionCommand.cmd_code, StartAcquisitionCommand)
284
+ CommandFactory.register_command(StopAcquisitionCommand.cmd_code, StopAcquisitionCommand)
285
+ CommandFactory.register_command(SetImpedanceParamCommand.cmd_code, SetImpedanceParamCommand)
286
+ CommandFactory.register_command(StartImpedanceCommand.cmd_code, StartImpedanceCommand)
287
+ CommandFactory.register_command(StopImpedanceCommand.cmd_code, StopImpedanceCommand)
288
+ CommandFactory.register_command(StartStimulationCommand.cmd_code, StartStimulationCommand)
289
+ CommandFactory.register_command(StimulationInfoCommand.cmd_code, StimulationInfoCommand)
290
+ CommandFactory.register_command(ImpedanceDataCommand.cmd_code, ImpedanceDataCommand)
291
+ CommandFactory.register_command(SignalDataCommand.cmd_code, SignalDataCommand)
292
+
293
+
File without changes