qlsdk2 0.3.0a3__tar.gz → 0.4.0__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.
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/PKG-INFO +2 -2
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/setup.py +1 -1
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/__init__.py +1 -0
- qlsdk2-0.4.0/src/qlsdk/core/__init__.py +5 -0
- qlsdk2-0.4.0/src/qlsdk/core/crc/__init__.py +5 -0
- qlsdk2-0.4.0/src/qlsdk/core/crc/crctools.py +95 -0
- qlsdk2-0.4.0/src/qlsdk/core/device.py +25 -0
- qlsdk2-0.4.0/src/qlsdk/core/entity/__init__.py +92 -0
- qlsdk2-0.4.0/src/qlsdk/core/exception.py +0 -0
- qlsdk2-0.4.0/src/qlsdk/core/filter/__init__.py +1 -0
- qlsdk2-0.4.0/src/qlsdk/core/filter/norch.py +59 -0
- qlsdk2-0.4.0/src/qlsdk/core/local.py +34 -0
- qlsdk2-0.4.0/src/qlsdk/core/message/__init__.py +2 -0
- qlsdk2-0.4.0/src/qlsdk/core/message/command.py +324 -0
- qlsdk2-0.4.0/src/qlsdk/core/message/tcp.py +0 -0
- qlsdk2-0.4.0/src/qlsdk/core/message/udp.py +96 -0
- qlsdk2-0.4.0/src/qlsdk/core/network/__init__.py +34 -0
- qlsdk2-0.4.0/src/qlsdk/core/network/monitor.py +55 -0
- qlsdk2-0.4.0/src/qlsdk/core/utils.py +70 -0
- qlsdk2-0.4.0/src/qlsdk/persist/__init__.py +2 -0
- qlsdk2-0.4.0/src/qlsdk/persist/rsc_edf.py +299 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/__init__.py +7 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/command/__init__.py +214 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/command/message.py +239 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/device_manager.py +119 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/discover.py +87 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/eegion.py +360 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/entity.py +447 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/paradigm.py +313 -0
- qlsdk2-0.4.0/src/qlsdk/rsc/proxy.py +76 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk2.egg-info/PKG-INFO +2 -2
- qlsdk2-0.4.0/src/qlsdk2.egg-info/SOURCES.txt +48 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk2.egg-info/requires.txt +1 -1
- qlsdk2-0.4.0/test/test.py +168 -0
- qlsdk2-0.3.0a3/src/qlsdk/persist/__init__.py +0 -1
- qlsdk2-0.3.0a3/src/qlsdk2.egg-info/SOURCES.txt +0 -21
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/README.md +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/setup.cfg +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/ar4/__init__.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/ar4m/__init__.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/persist/edf.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/sdk/__init__.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/sdk/ar4sdk.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/sdk/hub.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/x8/__init__.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk/x8m/__init__.py +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/src/qlsdk2.egg-info/top_level.txt +0 -0
- {qlsdk2-0.3.0a3 → qlsdk2-0.4.0}/test/test_ar4m.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: qlsdk2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: SDK for quanlan device
|
|
5
5
|
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
6
|
Author: hehuajun
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.9
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: loguru>=0.6.0
|
|
14
14
|
Requires-Dist: numpy>=1.23.5
|
|
15
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: bitarray>=1.5.3
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
18
18
|
Requires-Dist: twine>=3.0; extra == "dev"
|
|
@@ -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,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
|