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.
@@ -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)
@@ -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")
@@ -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.6.0a13
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 (2025-xx-xx)
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=P-qKn3yLpP68GIik-1qRozUjYfhsYOOnW1HGfLt3hLI,8427
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=djR_qXScb_XOOXaoMsvnK_qO2NCPzqEeKxKJCm4xnpk,13989
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=lewJj_Ys_oIhAGinLvAp7ZFc_BrCbWjfaNMnOw9sfXw,12791
43
- qlsdk/rsc/command/message.py,sha256=nTdG-Vp4MBnltyrgedAWiKD6kzOaPrg58Z_hq6yjhys,12220
44
- qlsdk/rsc/device/__init__.py,sha256=BzY9lRfssGPUlJ1ys-v3CWNgGihg7mPa2T4X0tl0Vg4,214
45
- qlsdk/rsc/device/arskindling.py,sha256=qFniIc1L3sNWCXmw4sQaRets7yrODYgFpq6QKXRtXoQ,12025
46
- qlsdk/rsc/device/base.py,sha256=PlLdkpR_wZpOA-6nLenTzD6ZLyvpGIre5l8f5CxH_cQ,22662
47
- qlsdk/rsc/device/c16_rs.py,sha256=qXt8m5vwcKQsN8JBllWnAsda5_Y6qkEhfHQQX101TMQ,5826
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=8Aqx-jS6v9pKd9qvTGytPspzGtDUlIxHc0g9R-wkV_s,2623
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=S6E0_ZXejC_YM_kzMQQHr7daPJPbVwZINBbYhreFW5w,5389
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.6.0a13.dist-info/METADATA,sha256=D0Xoxrh2Kn5atzYucsNw0P7c0Q6hUVfPM8ahzOQgK6c,1883
74
- qlsdk2-0.6.0a13.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
75
- qlsdk2-0.6.0a13.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
76
- qlsdk2-0.6.0a13.dist-info/RECORD,,
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,,
@@ -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
-