qlsdk2 0.6.0a13__py3-none-any.whl → 0.7.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.
- qlsdk/core/entity/__init__.py +16 -0
- qlsdk/persist/rsc_edf.py +3 -1
- qlsdk/rsc/command/__init__.py +5 -4
- qlsdk/rsc/device/__init__.py +5 -1
- qlsdk/rsc/device/arskindling.py +2 -1
- qlsdk/rsc/device/base.py +25 -18
- qlsdk/rsc/device/c16_rs.py +2 -1
- qlsdk/rsc/device/c16r.py +203 -0
- qlsdk/rsc/device/c21r.py +239 -0
- qlsdk/rsc/device/c8r.py +203 -0
- qlsdk/rsc/interface/device.py +1 -1
- qlsdk/rsc/manager/container.py +1 -0
- {qlsdk2-0.6.0a13.dist-info → qlsdk2-0.7.0a1.dist-info}/METADATA +5 -3
- {qlsdk2-0.6.0a13.dist-info → qlsdk2-0.7.0a1.dist-info}/RECORD +16 -16
- qlsdk/rsc/command/message.py +0 -336
- qlsdk/rsc/device_manager.py +0 -119
- qlsdk/rsc/discover.py +0 -87
- {qlsdk2-0.6.0a13.dist-info → qlsdk2-0.7.0a1.dist-info}/WHEEL +0 -0
- {qlsdk2-0.6.0a13.dist-info → qlsdk2-0.7.0a1.dist-info}/top_level.txt +0 -0
qlsdk/rsc/device/c8r.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
from qlsdk.core.entity import ImpedancePacket, RscPacket
|
|
3
|
+
from qlsdk.persist import RscEDFHandler
|
|
4
|
+
from qlsdk.rsc.device.base import QLBaseDevice, intersection_positions
|
|
5
|
+
from qlsdk.rsc.device.device_factory import DeviceFactory
|
|
6
|
+
from qlsdk.rsc.interface import IDevice
|
|
7
|
+
from qlsdk.rsc.device.base import QLBaseDevice
|
|
8
|
+
|
|
9
|
+
class C8R(QLBaseDevice):
|
|
10
|
+
|
|
11
|
+
device_type = 0x739 # C8R设备类型标识符 0x3907
|
|
12
|
+
|
|
13
|
+
def __init__(self, socket):
|
|
14
|
+
super().__init__(socket)
|
|
15
|
+
|
|
16
|
+
# 存储通道反向映射位置值
|
|
17
|
+
self._reverse_ch_pos = None
|
|
18
|
+
|
|
19
|
+
# 存储通道反向映射位置值
|
|
20
|
+
self._impedance_ch_pos = None
|
|
21
|
+
self._impedance_channels_origin = None
|
|
22
|
+
|
|
23
|
+
self.channel_name_mapping = {
|
|
24
|
+
"FP1": 1,
|
|
25
|
+
"FP2": 2,
|
|
26
|
+
"F7": 3,
|
|
27
|
+
"F8": 4,
|
|
28
|
+
"P3": 5,
|
|
29
|
+
"P4": 6,
|
|
30
|
+
"O1": 7,
|
|
31
|
+
"O2": 8,
|
|
32
|
+
"CZ": 9,
|
|
33
|
+
"A1": 10,
|
|
34
|
+
"A2": 11,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
self.channel_mapping = {
|
|
38
|
+
"1": 59,
|
|
39
|
+
"2": 60,
|
|
40
|
+
"3": 53,
|
|
41
|
+
"4": 50,
|
|
42
|
+
"5": 63,
|
|
43
|
+
"6": 24,
|
|
44
|
+
"7": 51,
|
|
45
|
+
"8": 64,
|
|
46
|
+
"9": 49,
|
|
47
|
+
"10": 14,
|
|
48
|
+
"11": 10,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
self.channel_display_mapping = {
|
|
52
|
+
59: 1,
|
|
53
|
+
60: 2,
|
|
54
|
+
53: 3,
|
|
55
|
+
50: 4,
|
|
56
|
+
63: 5,
|
|
57
|
+
24: 6,
|
|
58
|
+
51: 7,
|
|
59
|
+
64: 8,
|
|
60
|
+
49: 9,
|
|
61
|
+
14: 10,
|
|
62
|
+
10: 11,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_parent(cls, parent:IDevice) -> IDevice:
|
|
67
|
+
rlt = cls(parent.socket)
|
|
68
|
+
rlt.device_id = parent.device_id
|
|
69
|
+
rlt._device_no = parent.device_no
|
|
70
|
+
return rlt
|
|
71
|
+
|
|
72
|
+
def init_edf_handler(self):
|
|
73
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
74
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
75
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
76
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
77
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C8R')
|
|
78
|
+
logger.debug(f"EDF Handler initialized")
|
|
79
|
+
# 设置采集参数
|
|
80
|
+
def set_acq_param(self, channels:list, sample_rate: int = 500, sample_range:int = 188):
|
|
81
|
+
# 通道合法性校验
|
|
82
|
+
self._channel_validate(channels)
|
|
83
|
+
# 保存原始通道参数
|
|
84
|
+
self._acq_param["original_channels"] = channels
|
|
85
|
+
|
|
86
|
+
# 名称转换为数字通道
|
|
87
|
+
channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
88
|
+
|
|
89
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
90
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
91
|
+
channels = [self.channel_mapping.get(str(i), -1) for i in channels]
|
|
92
|
+
channels = [i if i != -1 else channels[0] for i in channels]
|
|
93
|
+
|
|
94
|
+
# 更新采集参数
|
|
95
|
+
self._acq_param["channels"] = channels
|
|
96
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
97
|
+
self._acq_param["sample_range"] = sample_range
|
|
98
|
+
self._acq_channels = channels
|
|
99
|
+
self._sample_rate = sample_rate
|
|
100
|
+
self._sample_range = sample_range
|
|
101
|
+
|
|
102
|
+
logger.debug(f"C16RS: set_acq_param: {self._acq_param}")
|
|
103
|
+
|
|
104
|
+
# 参数改变后,重置通道位置映射
|
|
105
|
+
self._reverse_ch_pos = None
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def acq_channels(self):
|
|
109
|
+
if self._acq_channels is None:
|
|
110
|
+
# 初始化通道参数为1-11
|
|
111
|
+
self.set_acq_param([i for i in range(1, 12)])
|
|
112
|
+
return self._acq_channels
|
|
113
|
+
|
|
114
|
+
def set_impedance_channels(self, channels:list):
|
|
115
|
+
# 通道合法性校验
|
|
116
|
+
self._channel_validate(channels)
|
|
117
|
+
# 保存原始通道参数
|
|
118
|
+
self._impedance_channels_origin = channels
|
|
119
|
+
|
|
120
|
+
# 名称转换为数字通道
|
|
121
|
+
channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
122
|
+
|
|
123
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
124
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
125
|
+
channels = [self.channel_mapping.get(str(i), -1) for i in channels]
|
|
126
|
+
channels = [i if i != -1 else channels[0] for i in channels]
|
|
127
|
+
self._impedance_channels = channels
|
|
128
|
+
|
|
129
|
+
# 初始化位置映射
|
|
130
|
+
self._impedance_ch_pos = None
|
|
131
|
+
|
|
132
|
+
def get_impedance_channels(self):
|
|
133
|
+
if self._impedance_channels is None or len(self._impedance_channels) == 0:
|
|
134
|
+
self.set_impedance_channels([i for i in range(1, 12)])
|
|
135
|
+
return self._impedance_channels
|
|
136
|
+
|
|
137
|
+
def _channel_validate(self, channels: list):
|
|
138
|
+
# 名称转换为数字通道
|
|
139
|
+
temp_channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
140
|
+
|
|
141
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
142
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
143
|
+
temp_channels = [self.channel_mapping.get(str(i), -1) for i in temp_channels]
|
|
144
|
+
for i in range(len(temp_channels)):
|
|
145
|
+
if temp_channels[i] == -1:
|
|
146
|
+
raise ValueError(f"通道参数有误,通道应为1-11的数字或对应名称,如:1、2或FP1、FP2等")
|
|
147
|
+
|
|
148
|
+
def _impedance_wrapper(self, body: bytes):
|
|
149
|
+
packet = ImpedancePacket().transfer(body)
|
|
150
|
+
impedance_channels = self.get_impedance_channels()
|
|
151
|
+
if impedance_channels is not None and len(impedance_channels) > 0:
|
|
152
|
+
# 只保留设置的阻抗通道
|
|
153
|
+
channel_pos = intersection_positions(packet.channels, impedance_channels)
|
|
154
|
+
packet.impedance = [packet.impedance[i] for i in channel_pos]
|
|
155
|
+
packet.channels = [packet.channels[i] for i in channel_pos]
|
|
156
|
+
|
|
157
|
+
# 升级为类变量,减少计算
|
|
158
|
+
if self._impedance_ch_pos is None:
|
|
159
|
+
self._impedance_ch_pos = map_indices(impedance_channels, packet.channels)
|
|
160
|
+
|
|
161
|
+
packet.channels = self._impedance_channels_origin
|
|
162
|
+
packet.impedance = [packet.impedance[i] for i in self._impedance_ch_pos]
|
|
163
|
+
|
|
164
|
+
return packet
|
|
165
|
+
|
|
166
|
+
# 信号数据转换(默认不处理)
|
|
167
|
+
def _signal_wrapper(self, body: bytes):
|
|
168
|
+
if body is None:
|
|
169
|
+
return
|
|
170
|
+
data = RscPacket().transfer(body)
|
|
171
|
+
# 根据映射关系做通道转换-(注意数据和通道的一致性)
|
|
172
|
+
# data.channels = [self.channel_display_mapping.get(i, i) for i in data.channels]
|
|
173
|
+
|
|
174
|
+
# 升级为类变量,减少计算
|
|
175
|
+
if self._reverse_ch_pos is None:
|
|
176
|
+
self._reverse_ch_pos = map_indices(self._acq_param["channels"], data.channels)
|
|
177
|
+
|
|
178
|
+
# 更新通道(数据)顺序和输入一致
|
|
179
|
+
data.channels = self._acq_param["original_channels"]
|
|
180
|
+
data.eeg = [data.eeg[i] for i in self._reverse_ch_pos]
|
|
181
|
+
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
def start_stimulation(self):
|
|
185
|
+
raise NotImplementedError("Stimulation function is not supported")
|
|
186
|
+
|
|
187
|
+
def map_indices(A, B):
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
参数:
|
|
191
|
+
A: 源数组(无重复值)
|
|
192
|
+
B: 目标数组(无重复值)
|
|
193
|
+
|
|
194
|
+
返回:
|
|
195
|
+
C: 与A长度相同的数组,元素为A中对应值在B中的索引(不存在则为-1)
|
|
196
|
+
"""
|
|
197
|
+
# 创建B的值到索引的映射字典(O(n)操作)
|
|
198
|
+
b_map = {value: idx for idx, value in enumerate(B)}
|
|
199
|
+
|
|
200
|
+
# 遍历A,获取每个元素在B中的位置(O(m)操作)
|
|
201
|
+
return [b_map.get(a, -1) for a in A]
|
|
202
|
+
|
|
203
|
+
DeviceFactory.register(C8R.device_type, C8R)
|
qlsdk/rsc/interface/device.py
CHANGED
|
@@ -53,7 +53,7 @@ class IDevice(ABC):
|
|
|
53
53
|
def stop_acquisition(self) -> None:
|
|
54
54
|
raise NotImplementedError("Not Supported")
|
|
55
55
|
|
|
56
|
-
def subscribe(self, type="signal") -> None:
|
|
56
|
+
def subscribe(self, type: Literal["signal", "impedance"] = "signal") -> None:
|
|
57
57
|
raise NotImplementedError("Not Supported")
|
|
58
58
|
def unsubscribe(self, topic) -> None:
|
|
59
59
|
raise NotImplementedError("Not Supported")
|
qlsdk/rsc/manager/container.py
CHANGED
|
@@ -97,6 +97,7 @@ class DeviceContainer(object):
|
|
|
97
97
|
if device.device_no:
|
|
98
98
|
real_device = DeviceFactory.create_device(device)
|
|
99
99
|
device.stop_listening()
|
|
100
|
+
time.sleep(0.5)
|
|
100
101
|
real_device.start_listening()
|
|
101
102
|
self.add_device(real_device)
|
|
102
103
|
logger.info(f"设备[{device.device_name}]已连接,设备类型为:{hex(real_device.device_type)}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: qlsdk2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0a1
|
|
4
4
|
Summary: SDK for quanlan device
|
|
5
5
|
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
6
|
Author: hehuajun
|
|
@@ -17,9 +17,11 @@ Provides-Extra: dev
|
|
|
17
17
|
Requires-Dist: pytest >=6.0 ; extra == 'dev'
|
|
18
18
|
Requires-Dist: twine >=3.0 ; extra == 'dev'
|
|
19
19
|
|
|
20
|
-
## **v0.6.0 (
|
|
20
|
+
## **v0.6.0 (2026-01-16)
|
|
21
21
|
#### 新设备
|
|
22
|
-
- C256RS
|
|
22
|
+
- C256RS设备支持设备连接、信号采集、阻抗测量、文件记录、刺激等功能
|
|
23
|
+
- ARS信号采集、阻抗测量、文件记录等功能
|
|
24
|
+
- 修复其他问题
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
## **v0.5.1.1** (2025-08-24)
|
|
@@ -8,7 +8,7 @@ qlsdk/core/local.py,sha256=vbison4XZtS4SNYLJ9CqBhetEcukdviTWmvtdA1efkQ,811
|
|
|
8
8
|
qlsdk/core/utils.py,sha256=yfCiLpufO96I68MLs6Drc6IECRjcQ-If8sXn7RaRHrk,4241
|
|
9
9
|
qlsdk/core/crc/__init__.py,sha256=kaYSr6KN5g4U49xlxAvT2lnEeGtwX4Dz1ArwKDvUIIY,143
|
|
10
10
|
qlsdk/core/crc/crctools.py,sha256=sDeE6CMccQX2cRAyMQK0SZUk1fa50XMuwqXau5UX5C8,4242
|
|
11
|
-
qlsdk/core/entity/__init__.py,sha256=
|
|
11
|
+
qlsdk/core/entity/__init__.py,sha256=5Uoywn4WkCj9SJc4rm1AypC-92zc34ztGMyam8FQ6VY,9026
|
|
12
12
|
qlsdk/core/filter/__init__.py,sha256=YIWIzDUKN30mq2JTr53ZGblggZfC_rLUp2FSRrsQFgU,36
|
|
13
13
|
qlsdk/core/filter/norch.py,sha256=5RdIBX5eqs5w5nmVAnCB3ESSuAT_vVBZ2g-dg6HMZdY,1858
|
|
14
14
|
qlsdk/core/message/__init__.py,sha256=sHuavOyHf4bhH6VdDpTA1EsCh7Q-XsPHcFiItpVz3Rs,51
|
|
@@ -30,32 +30,32 @@ qlsdk/interface/store.py,sha256=694WQnnrtXThP44JkQlBXzghvqA0PwHKK3uA640aiUo,23
|
|
|
30
30
|
qlsdk/persist/__init__.py,sha256=b8qk1aOU6snEMCQNYDl1ijV3-2gwBmMt76fiAzNk1E8,107
|
|
31
31
|
qlsdk/persist/ars_edf.py,sha256=aY_bGM2QsqpKAAJqs6kR7hBVpq8PaayNtcqeLJlt1Fc,9138
|
|
32
32
|
qlsdk/persist/edf.py,sha256=ETngb86CfkIUJYWmw86QR445MvTFC7Edk_CH9nyNgtY,7857
|
|
33
|
-
qlsdk/persist/rsc_edf.py,sha256=
|
|
33
|
+
qlsdk/persist/rsc_edf.py,sha256=Opn-nMxVyT2DUv2y3voxGfs-izJqD-a-Yg2PpRDLYME,14041
|
|
34
34
|
qlsdk/persist/stream.py,sha256=TCVF1sqDrHiYBsJC27At66AaCs-_blXeXA_WXdJiIVA,5828
|
|
35
35
|
qlsdk/rsc/__init__.py,sha256=hOMiN0eYn4jYo7O4_0IPlQT0hD15SqqCQUihOVlTZvs,269
|
|
36
|
-
qlsdk/rsc/device_manager.py,sha256=1ucd-lzHkNeQPKPzXV6OBkAMqPp_vOcsLyS-9TJ7wRc,4448
|
|
37
|
-
qlsdk/rsc/discover.py,sha256=ONXN6YWY-OMU0sBoLqqKUyb-8drtAp1g_MvnpzaFvHQ,3124
|
|
38
36
|
qlsdk/rsc/eegion.py,sha256=lxrktO-3Z_MYdFIwc4NxvgLM5AL5kU3UItjH6tsKmHY,11670
|
|
39
37
|
qlsdk/rsc/entity.py,sha256=-fRWFkVWp9d8Y1uh6GiacXC5scdeEKNiNFf3aziGdCE,17751
|
|
40
38
|
qlsdk/rsc/paradigm.py,sha256=WH9kAez0wbci-B1z5BC-nw3nqqOtE_txFILue4TM-cE,17644
|
|
41
39
|
qlsdk/rsc/proxy.py,sha256=9CPdGNGWremwBUh4GvlXAykYB-x_BEPPLqsNvwuwIDE,2736
|
|
42
|
-
qlsdk/rsc/command/__init__.py,sha256=
|
|
43
|
-
qlsdk/rsc/
|
|
44
|
-
qlsdk/rsc/device/
|
|
45
|
-
qlsdk/rsc/device/
|
|
46
|
-
qlsdk/rsc/device/
|
|
47
|
-
qlsdk/rsc/device/
|
|
40
|
+
qlsdk/rsc/command/__init__.py,sha256=Mqj6Oms7bGguLL7CSkyMiG-6qiHm_mXrPk3Vkn57--M,12795
|
|
41
|
+
qlsdk/rsc/device/__init__.py,sha256=s5AmzJFa2ZBZIEvmOB1ZTsqzYg2qMVOBySMzWJI9Nas,310
|
|
42
|
+
qlsdk/rsc/device/arskindling.py,sha256=BMbNBhVVAK87Sph9AoDjQyzfaqR9U_KVy4NJ8bVHMoM,12090
|
|
43
|
+
qlsdk/rsc/device/base.py,sha256=SRLHDoHOGsrhSc63Z0kK6_tpB5cmopXqNYTMUNxhVqM,22114
|
|
44
|
+
qlsdk/rsc/device/c16_rs.py,sha256=GDMdV-D6Yxdvj6BNTskxiXPOOtJt283EMU3TBpMFY7o,5875
|
|
45
|
+
qlsdk/rsc/device/c16r.py,sha256=vsLcO-eUC_CnEjO85OCEYZ3Y3WXmyhJholbbMYQoEec,8131
|
|
46
|
+
qlsdk/rsc/device/c21r.py,sha256=nV89sedNMjyiU7XNOHxztwEzqBpwlhQ7V25LIb06HII,8973
|
|
48
47
|
qlsdk/rsc/device/c256_rs.py,sha256=7vAEzf_ggNcwrXKmcZMylnKzLFD5ZqtAIfkkI3lQ1iI,1682
|
|
49
48
|
qlsdk/rsc/device/c64_rs.py,sha256=x8wHdwATKDU34j9vXNEXsNSJg23RAWmAKL8pgIGamG8,1091
|
|
50
49
|
qlsdk/rsc/device/c64s1.py,sha256=WwiKSjxYpUJVkHDMDzPgp7-klbaiZ2f8EOe3wV6d2WU,1098
|
|
50
|
+
qlsdk/rsc/device/c8r.py,sha256=QcIr_CiKyKuYIvddWuNsiSTUBf62iJlwuWuJ5VY5SKg,8123
|
|
51
51
|
qlsdk/rsc/device/device_factory.py,sha256=6cPhm3pPGrVXA1s1HePFLjZqmhNI1vOAucFI0VRD_Y0,1317
|
|
52
52
|
qlsdk/rsc/interface/__init__.py,sha256=xeRzIlQSB7ZSf4r5kLfH5cDQLzCyWeJAReG8Xq5nOE0,70
|
|
53
53
|
qlsdk/rsc/interface/command.py,sha256=1s5Lxb_ejsd-JNvKMqU2aFSnOoW-_cx01VSD3czxmQI,199
|
|
54
|
-
qlsdk/rsc/interface/device.py,sha256=
|
|
54
|
+
qlsdk/rsc/interface/device.py,sha256=9tyEcP0N-ZVOi_KGIlRcdmUIpt4ff9BCQIa-lf1CG7I,2657
|
|
55
55
|
qlsdk/rsc/interface/handler.py,sha256=ADDe_a2RAxGMuooLyivH0JBPTGBcFP2JaTVX41R1A4w,198
|
|
56
56
|
qlsdk/rsc/interface/parser.py,sha256=Z4PND5LXcJ_8CQ-OIq3KlOEVOceU1hKUuZkoFSIGGLM,334
|
|
57
57
|
qlsdk/rsc/manager/__init__.py,sha256=4ljT3mR8YPBDQur46B5xPqK5tjLKlsWfgCJVuA0gs-8,40
|
|
58
|
-
qlsdk/rsc/manager/container.py,sha256=
|
|
58
|
+
qlsdk/rsc/manager/container.py,sha256=lAHOzN2CAyNxZbpAy3t1BnDJJIO_JMYvtPYyh51kAi4,5426
|
|
59
59
|
qlsdk/rsc/manager/search.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
60
|
qlsdk/rsc/network/__init__.py,sha256=PfYiqXS2pZV__uegQ1TjaeYhY1pefZ_shwE_X5HNVbs,23
|
|
61
61
|
qlsdk/rsc/network/discover.py,sha256=GRXP0WxxIorDZWXq1X5CPAV60raSRvNiVwMQE8647XA,3044
|
|
@@ -70,7 +70,7 @@ qlsdk/sdk/libs/libAr4SDK.dll,sha256=kZp9_DRwPdAJ5OgTFQSqS8tEETxUs7YmmETuBP2g60U,
|
|
|
70
70
|
qlsdk/sdk/libs/libwinpthread-1.dll,sha256=W77ySaDQDi0yxpnQu-ifcU6-uHKzmQpcvsyx2J9j5eg,52224
|
|
71
71
|
qlsdk/x8/__init__.py,sha256=FDpDK7GAYL-g3vzfU9U_V03QzoYoxH9YLm93PjMlANg,4870
|
|
72
72
|
qlsdk/x8m/__init__.py,sha256=cLeUqEEj65qXw4Qa4REyxoLh6T24anSqPaKe9_lR340,634
|
|
73
|
-
qlsdk2-0.
|
|
74
|
-
qlsdk2-0.
|
|
75
|
-
qlsdk2-0.
|
|
76
|
-
qlsdk2-0.
|
|
73
|
+
qlsdk2-0.7.0a1.dist-info/METADATA,sha256=Vj7f2irnY_kSVzWN-heLELKHMRhyKbNsJzis1uJtblk,2043
|
|
74
|
+
qlsdk2-0.7.0a1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
|
75
|
+
qlsdk2-0.7.0a1.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
|
|
76
|
+
qlsdk2-0.7.0a1.dist-info/RECORD,,
|
qlsdk/rsc/command/message.py
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import numpy as np
|
|
3
|
-
from time import time_ns
|
|
4
|
-
from typing import Dict, Type
|
|
5
|
-
from loguru import logger
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from qlsdk.core.crc import crc16
|
|
9
|
-
from qlsdk.core.device import BaseDevice,
|
|
10
|
-
from qlsdk.core.entity import RscPacket, ImpedancePacket
|
|
11
|
-
from qlsdk.core.utils import to_channels, to_bytes
|
|
12
|
-
from qlsdk.rsc.interface import IDevice, IParser
|
|
13
|
-
from qlsdk.rsc.device import DeviceFactory
|
|
14
|
-
|
|
15
|
-
class DeviceCommand(abc.ABC):
|
|
16
|
-
# 消息头
|
|
17
|
-
HEADER_PREFIX = b'\x5A\xA5'
|
|
18
|
-
# 消息头总长度 2(prefix) +1(pkgType) +1(deviceType) +4(deviceId) +4(len) +2(cmd)
|
|
19
|
-
HEADER_LEN = 14
|
|
20
|
-
# 消息指令码位置
|
|
21
|
-
CMD_POS = 12
|
|
22
|
-
|
|
23
|
-
def __init__(self, device: BaseDevice):
|
|
24
|
-
self.device = device
|
|
25
|
-
|
|
26
|
-
@classmethod
|
|
27
|
-
def build(cls, device) :
|
|
28
|
-
return cls(device)
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
@abc.abstractmethod
|
|
32
|
-
def cmd_code(self) -> int:
|
|
33
|
-
pass
|
|
34
|
-
@property
|
|
35
|
-
@abc.abstractmethod
|
|
36
|
-
def cmd_desc(self) -> str:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
@staticmethod
|
|
40
|
-
def checksum(data: bytes) -> bytes:
|
|
41
|
-
return crc16(data).to_bytes(2, 'little')
|
|
42
|
-
|
|
43
|
-
def pack(self, body=b'') -> bytes:
|
|
44
|
-
# header+body+checksum
|
|
45
|
-
body = self.pack_body()
|
|
46
|
-
header = self.pack_header(len(body))
|
|
47
|
-
payload = header + body
|
|
48
|
-
return payload + DeviceCommand.checksum(payload)
|
|
49
|
-
def pack_body(self) -> bytes:
|
|
50
|
-
"""构建消息体"""
|
|
51
|
-
return b''
|
|
52
|
-
def pack_header(self, body_len: int) -> bytes:
|
|
53
|
-
device_id = int(self.device.device_id) if self.device and self.device.device_id else 0
|
|
54
|
-
device_type = int(self.device.device_type) if self.device and self.device.device_type else 0
|
|
55
|
-
|
|
56
|
-
"""构建消息头"""
|
|
57
|
-
return (
|
|
58
|
-
DeviceCommand.HEADER_PREFIX
|
|
59
|
-
+ int(2).to_bytes(1, 'little') # pkgType
|
|
60
|
-
+ device_type.to_bytes(1, 'little')
|
|
61
|
-
+ device_id.to_bytes(4, 'little')
|
|
62
|
-
+ (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
|
|
63
|
-
+ self.cmd_code.to_bytes(2, 'little')
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
def unpack(self, payload: bytes) -> bytes:
|
|
67
|
-
"""解析消息体"""
|
|
68
|
-
# 解析消息体
|
|
69
|
-
body = payload[self.HEADER_LEN:-2]
|
|
70
|
-
|
|
71
|
-
def parse_body(self, body: bytes):
|
|
72
|
-
time = int.from_bytes(body[0:8], 'little')
|
|
73
|
-
# result - 1B
|
|
74
|
-
result = body[8]
|
|
75
|
-
logger.info(f"[{time}]{self.cmd_desc}{'成功' if result == 0 else '失败'}")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class CommandFactory:
|
|
80
|
-
"""Registry for command implementations"""
|
|
81
|
-
_commands: Dict[int, Type[DeviceCommand]] = {}
|
|
82
|
-
|
|
83
|
-
@classmethod
|
|
84
|
-
def register_command(cls, code: int, command: Type[DeviceCommand]):
|
|
85
|
-
cls._commands[code] = command
|
|
86
|
-
|
|
87
|
-
@classmethod
|
|
88
|
-
def create_command(cls, code: int) -> Type[DeviceCommand]:
|
|
89
|
-
logger.trace(f"Creating command for code: {hex(code)}")
|
|
90
|
-
if code not in cls._commands:
|
|
91
|
-
logger.warning(f"不支持的设备指令: {hex(code)}")
|
|
92
|
-
return cls._commands[DefaultCommand.cmd_code]
|
|
93
|
-
return cls._commands[code]
|
|
94
|
-
|
|
95
|
-
# =============================================================================
|
|
96
|
-
class DefaultCommand(DeviceCommand):
|
|
97
|
-
cmd_code = 0x00
|
|
98
|
-
cmd_desc = "未定义"
|
|
99
|
-
|
|
100
|
-
def parse_body(self, body: bytes):
|
|
101
|
-
# Response parsing example: 2 bytes version + 4 bytes serial
|
|
102
|
-
logger.info(f"Received body len: {len(body)}")
|
|
103
|
-
|
|
104
|
-
class GetDeviceInfoCommand(DeviceCommand):
|
|
105
|
-
cmd_code = 0x17
|
|
106
|
-
cmd_desc = "设备信息"
|
|
107
|
-
|
|
108
|
-
def parse_body(self, body: bytes):
|
|
109
|
-
# time - 8B
|
|
110
|
-
self.device.connect_time = int.from_bytes(body[0:8], 'little')
|
|
111
|
-
self.device.current_time = self.device.connect_time
|
|
112
|
-
# result - 1B
|
|
113
|
-
result = body[8]
|
|
114
|
-
# deviceId - 4B
|
|
115
|
-
self.device.device_id = int.from_bytes(body[9:13], 'big')
|
|
116
|
-
# deviceType - 4B
|
|
117
|
-
self.device.device_type = int.from_bytes(body[13:17], 'little')
|
|
118
|
-
# softVersion - 4B
|
|
119
|
-
self.device.software_version = body[17:21].hex()
|
|
120
|
-
# hardVersion - 4B
|
|
121
|
-
self.device.hardware_version = body[21:25].hex()
|
|
122
|
-
# deviceName - 16B
|
|
123
|
-
self.device.device_name = body[25:41].decode('utf-8').rstrip('\x00')
|
|
124
|
-
# flag - 4B
|
|
125
|
-
flag = int.from_bytes(body[41:45], 'little')
|
|
126
|
-
logger.debug(f"Received device info: {result}, {flag}, {self.device}")
|
|
127
|
-
|
|
128
|
-
# 创建设备对象
|
|
129
|
-
device = Dev
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# 握手
|
|
133
|
-
class HandshakeCommand(DeviceCommand):
|
|
134
|
-
cmd_code = 0x01
|
|
135
|
-
cmd_desc = "握手"
|
|
136
|
-
|
|
137
|
-
def parse_body(self, body: bytes):
|
|
138
|
-
logger.info(f"Received handshake response: {body.hex()}")
|
|
139
|
-
|
|
140
|
-
# 查询电量
|
|
141
|
-
class QueryBatteryCommand(DeviceCommand):
|
|
142
|
-
cmd_code = 0x16
|
|
143
|
-
cmd_desc = "电量信息"
|
|
144
|
-
def parse_body(self, body: bytes):
|
|
145
|
-
# time - 8b
|
|
146
|
-
self.device.current_time = int.from_bytes(body[0:8], 'little')
|
|
147
|
-
# result - 1b
|
|
148
|
-
result = body[8]
|
|
149
|
-
# 更新设备信息
|
|
150
|
-
if result == 0:
|
|
151
|
-
# voltage - 2b mV
|
|
152
|
-
self.device.voltage = int.from_bytes(body[9:11], 'little')
|
|
153
|
-
# soc - 1b
|
|
154
|
-
self.device.battery_remain = body[11]
|
|
155
|
-
# soh - 1b
|
|
156
|
-
self.device.battery_total = body[12]
|
|
157
|
-
# state - 1b
|
|
158
|
-
# state = body[13]
|
|
159
|
-
logger.debug(f"电量更新: {self.device}")
|
|
160
|
-
else:
|
|
161
|
-
logger.warning(f"QueryBatteryCommand message received but result is failed.")
|
|
162
|
-
|
|
163
|
-
# 设置采集参数
|
|
164
|
-
class SetAcquisitionParamCommand(DeviceCommand):
|
|
165
|
-
cmd_code = 0x451
|
|
166
|
-
cmd_desc = "设置信号采集参数"
|
|
167
|
-
|
|
168
|
-
def pack_body(self):
|
|
169
|
-
body = to_bytes(self.device.acq_channels)
|
|
170
|
-
body += self.device.sample_range.to_bytes(4, byteorder='little')
|
|
171
|
-
body += self.device.sample_rate.to_bytes(4, byteorder='little')
|
|
172
|
-
body += self.device.sample_num.to_bytes(4, byteorder='little')
|
|
173
|
-
body += self.device.resolution.to_bytes(1, byteorder='little')
|
|
174
|
-
body += bytes.fromhex('00')
|
|
175
|
-
|
|
176
|
-
return body
|
|
177
|
-
|
|
178
|
-
# 启动采集
|
|
179
|
-
class StartAcquisitionCommand(DeviceCommand):
|
|
180
|
-
cmd_code = 0x452
|
|
181
|
-
cmd_desc = "启动信号采集"
|
|
182
|
-
|
|
183
|
-
def pack_body(self):
|
|
184
|
-
return bytes.fromhex('0000')
|
|
185
|
-
|
|
186
|
-
# 停止采集
|
|
187
|
-
class StopAcquisitionCommand(DeviceCommand):
|
|
188
|
-
cmd_code = 0x453
|
|
189
|
-
cmd_desc = "停止信号采集"
|
|
190
|
-
|
|
191
|
-
def pack_body(self):
|
|
192
|
-
return b''
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# 设置阻抗采集参数
|
|
196
|
-
class SetImpedanceParamCommand(DeviceCommand):
|
|
197
|
-
cmd_code = 0x411
|
|
198
|
-
cmd_desc = "设置阻抗测量参数"
|
|
199
|
-
|
|
200
|
-
# 启动阻抗测量
|
|
201
|
-
class StartImpedanceCommand(DeviceCommand):
|
|
202
|
-
cmd_code = 0x412
|
|
203
|
-
cmd_desc = "启动阻抗测量"
|
|
204
|
-
def pack_body(self):
|
|
205
|
-
body = bytes.fromhex('0000')
|
|
206
|
-
body += to_bytes(self.device.acq_channels)
|
|
207
|
-
body += bytes.fromhex('0000000000000000') # 8字节占位符
|
|
208
|
-
return body
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# 停止阻抗测量
|
|
212
|
-
class StopImpedanceCommand(DeviceCommand):
|
|
213
|
-
cmd_code = 0x413
|
|
214
|
-
cmd_desc = "停止阻抗测量"
|
|
215
|
-
|
|
216
|
-
def pack_body(self):
|
|
217
|
-
return b''
|
|
218
|
-
|
|
219
|
-
# 启动刺激
|
|
220
|
-
class StartStimulationCommand(DeviceCommand):
|
|
221
|
-
cmd_code = 0x48C
|
|
222
|
-
cmd_desc = "启动刺激"
|
|
223
|
-
def pack_body(self):
|
|
224
|
-
return self.device.stim_paradigm.to_bytes()
|
|
225
|
-
# return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
|
|
226
|
-
def parse_body(self, body: bytes):
|
|
227
|
-
# time - 8B
|
|
228
|
-
time = int.from_bytes(body[0:8], 'little')
|
|
229
|
-
# result - 1B
|
|
230
|
-
result = body[8]
|
|
231
|
-
# error_channel - 8B
|
|
232
|
-
# error_channel= int.from_bytes(body[9:17], 'big')
|
|
233
|
-
channels = to_channels(body[9:17])
|
|
234
|
-
logger.success(f"通道 {channels} 刺激开始")
|
|
235
|
-
self.device.trigger(f"通道 {channels} 刺激开始")
|
|
236
|
-
# error_type - 1B
|
|
237
|
-
error_type = body[17]
|
|
238
|
-
|
|
239
|
-
# 停止刺激
|
|
240
|
-
class StopStimulationCommand(DeviceCommand):
|
|
241
|
-
cmd_code = 0x488
|
|
242
|
-
cmd_desc = "停止刺激"
|
|
243
|
-
|
|
244
|
-
# 启动刺激
|
|
245
|
-
class StopStimulationNotifyCommand(DeviceCommand):
|
|
246
|
-
cmd_code = 0x48D
|
|
247
|
-
cmd_desc = "停止刺激通知"
|
|
248
|
-
def pack_body(self):
|
|
249
|
-
return self.device.stim_paradigm.to_bytes()
|
|
250
|
-
# return bytes.fromhex('01000000000000008813000000000000010000000000000000000140420f00640064000000803f0000010000000000000000000000000000000000000000008813000000000000')
|
|
251
|
-
def parse_body(self, body: bytes):
|
|
252
|
-
# time - 8B
|
|
253
|
-
time = int.from_bytes(body[0:8], 'little')
|
|
254
|
-
# result - 1B
|
|
255
|
-
result = body[8]
|
|
256
|
-
# error_channel - 8B
|
|
257
|
-
# error_channel= int.from_bytes(body[9:17], 'big')
|
|
258
|
-
channels = to_channels(body[9:17])
|
|
259
|
-
logger.success(f"通道 {channels} 刺激结束")
|
|
260
|
-
self.device.trigger(f"通道 {channels} 刺激结束", time)
|
|
261
|
-
# error_type - 1B
|
|
262
|
-
error_type = body[17]
|
|
263
|
-
# 刺激信息
|
|
264
|
-
class StimulationInfoCommand(DeviceCommand):
|
|
265
|
-
cmd_code = 0x48e
|
|
266
|
-
cmd_desc = "刺激告警信息"
|
|
267
|
-
|
|
268
|
-
def parse_body(self, body: bytes):
|
|
269
|
-
time = int.from_bytes(body[0:8], 'little')
|
|
270
|
-
# result - 1B
|
|
271
|
-
result = body[8]
|
|
272
|
-
# error_channel - 8B
|
|
273
|
-
channels = to_channels(body[9:17])
|
|
274
|
-
# 保留位-8B
|
|
275
|
-
# error_type - 1B
|
|
276
|
-
err_type = body[17]
|
|
277
|
-
# 特征位-4B
|
|
278
|
-
# errType = int.from_bytes(body[25:29], 'little')
|
|
279
|
-
logger.warning(f"刺激告警信息[{err_type}],通道 {channels} 刺激驱动不足")
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# 阻抗数据
|
|
283
|
-
class ImpedanceDataCommand(DeviceCommand):
|
|
284
|
-
cmd_code = 0x415
|
|
285
|
-
cmd_desc = "阻抗数据"
|
|
286
|
-
|
|
287
|
-
def parse_body(self, body: bytes):
|
|
288
|
-
logger.info(f"Received impedance data: {body.hex()}")
|
|
289
|
-
packet = ImpedancePacket().transfer(body)
|
|
290
|
-
|
|
291
|
-
# 信号数据
|
|
292
|
-
class SignalDataCommand(DeviceCommand):
|
|
293
|
-
cmd_code = 0x455
|
|
294
|
-
cmd_desc = "信号数据"
|
|
295
|
-
|
|
296
|
-
def unpack(self, payload):
|
|
297
|
-
return super().unpack(payload)
|
|
298
|
-
|
|
299
|
-
def parse_body(self, body: bytes):
|
|
300
|
-
# 解析数据包
|
|
301
|
-
packet = RscPacket()
|
|
302
|
-
packet.transfer(body)
|
|
303
|
-
|
|
304
|
-
# 文件写入到edf
|
|
305
|
-
if self.device.edf_handler:
|
|
306
|
-
self.device.edf_handler.write(packet)
|
|
307
|
-
|
|
308
|
-
if len(self.device.signal_consumers) > 0 :
|
|
309
|
-
# 信号数字值转物理值
|
|
310
|
-
packet.eeg = self.device.eeg2phy(np.array(packet.eeg))
|
|
311
|
-
|
|
312
|
-
# 发送数据包到订阅者
|
|
313
|
-
for q in list(self.device.signal_consumers.values()):
|
|
314
|
-
q.put(packet)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
# =============================================================================
|
|
319
|
-
# 指令实现类注册到指令工厂
|
|
320
|
-
# =============================================================================
|
|
321
|
-
CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
|
|
322
|
-
CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
|
|
323
|
-
CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
|
|
324
|
-
CommandFactory.register_command(QueryBatteryCommand.cmd_code, QueryBatteryCommand)
|
|
325
|
-
CommandFactory.register_command(SetAcquisitionParamCommand.cmd_code, SetAcquisitionParamCommand)
|
|
326
|
-
CommandFactory.register_command(StartAcquisitionCommand.cmd_code, StartAcquisitionCommand)
|
|
327
|
-
CommandFactory.register_command(StopAcquisitionCommand.cmd_code, StopAcquisitionCommand)
|
|
328
|
-
CommandFactory.register_command(SetImpedanceParamCommand.cmd_code, SetImpedanceParamCommand)
|
|
329
|
-
CommandFactory.register_command(StartImpedanceCommand.cmd_code, StartImpedanceCommand)
|
|
330
|
-
CommandFactory.register_command(StopImpedanceCommand.cmd_code, StopImpedanceCommand)
|
|
331
|
-
CommandFactory.register_command(StartStimulationCommand.cmd_code, StartStimulationCommand)
|
|
332
|
-
CommandFactory.register_command(StimulationInfoCommand.cmd_code, StimulationInfoCommand)
|
|
333
|
-
CommandFactory.register_command(ImpedanceDataCommand.cmd_code, ImpedanceDataCommand)
|
|
334
|
-
CommandFactory.register_command(SignalDataCommand.cmd_code, SignalDataCommand)
|
|
335
|
-
|
|
336
|
-
|