qlsdk2 0.6.0a13__tar.gz → 0.7.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.
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/PKG-INFO +5 -3
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/README.md +4 -2
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/setup.py +1 -1
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/entity/__init__.py +16 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/persist/rsc_edf.py +3 -1
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/command/__init__.py +5 -4
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/__init__.py +5 -1
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/arskindling.py +2 -1
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/base.py +25 -18
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/c16_rs.py +2 -1
- qlsdk2-0.7.0a1/src/qlsdk/rsc/device/c16r.py +203 -0
- qlsdk2-0.7.0a1/src/qlsdk/rsc/device/c21r.py +239 -0
- qlsdk2-0.7.0a1/src/qlsdk/rsc/device/c8r.py +203 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/interface/device.py +1 -1
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/manager/container.py +1 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk2.egg-info/PKG-INFO +5 -3
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk2.egg-info/SOURCES.txt +3 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/setup.cfg +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/ar4/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/ar4m/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/crc/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/crc/crctools.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/device.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/exception.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/filter/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/filter/norch.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/local.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/message/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/message/command.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/message/tcp.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/message/udp.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/network/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/network/monitor.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/core/utils.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/entity/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/entity/message.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/entity/signal.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/analyzer.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/collector.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/device.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/parser.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/stimulator.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/interface/store.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/persist/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/persist/ars_edf.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/persist/edf.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/persist/stream.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/c256_rs.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/c64_rs.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/c64s1.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/device/device_factory.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/eegion.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/entity.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/interface/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/interface/command.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/interface/handler.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/interface/parser.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/manager/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/manager/search.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/network/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/network/discover.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/paradigm.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/parser/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/parser/base-new.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/parser/base.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/parser/rsc.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/rsc/proxy.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/sdk/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/sdk/ar4sdk.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/sdk/hub.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/sdk/libs/libAr4SDK.dll +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/sdk/libs/libwinpthread-1.dll +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/x8/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk/x8m/__init__.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk2.egg-info/dependency_links.txt +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk2.egg-info/requires.txt +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/src/qlsdk2.egg-info/top_level.txt +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/test/test.222.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/test/test.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/test/test_ar4m.py +0 -0
- {qlsdk2-0.6.0a13 → qlsdk2-0.7.0a1}/test/test_bdf.py +0 -0
|
@@ -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)
|
|
@@ -54,6 +54,22 @@ class RscPacket(Packet):
|
|
|
54
54
|
logger.trace(packet)
|
|
55
55
|
|
|
56
56
|
return packet
|
|
57
|
+
|
|
58
|
+
def copy(self) -> 'RscPacket':
|
|
59
|
+
packet = RscPacket()
|
|
60
|
+
packet.time_stamp = self.time_stamp
|
|
61
|
+
packet.pkg_id = self.pkg_id
|
|
62
|
+
packet.result = self.result
|
|
63
|
+
packet.channels = self.channels
|
|
64
|
+
packet.origin_sample_rate = self.origin_sample_rate
|
|
65
|
+
packet.sample_rate = self.sample_rate
|
|
66
|
+
packet.sample_num = self.sample_num
|
|
67
|
+
packet.resolution = self.resolution
|
|
68
|
+
packet.filter = self.filter
|
|
69
|
+
packet.data_len = self.data_len
|
|
70
|
+
packet.trigger = self.trigger
|
|
71
|
+
packet.eeg = self.eeg
|
|
72
|
+
return packet
|
|
57
73
|
|
|
58
74
|
def __str__(self):
|
|
59
75
|
return f"""[
|
|
@@ -131,7 +131,7 @@ class EDFStreamWriter(Thread):
|
|
|
131
131
|
logger.trace(f"sf: {self.sample_frequency}, pm: {self.physical_max}, pn: {self.physical_min}, dm: {self.digital_max}, dn: {self.digital_min}")
|
|
132
132
|
for ch in range(self._n_channels):
|
|
133
133
|
signal_headers.append({
|
|
134
|
-
"label": f'
|
|
134
|
+
"label": f'channel {self._channels[ch]}',
|
|
135
135
|
"dimension": 'uV',
|
|
136
136
|
"sample_frequency": self.sample_frequency,
|
|
137
137
|
"physical_min": self.physical_min,
|
|
@@ -296,6 +296,8 @@ class RscEDFHandler(object):
|
|
|
296
296
|
logger.info(f"收到结束信号,即将停止写入数据:{self.file_name}")
|
|
297
297
|
self._edf_writer_thread.stop_recording()
|
|
298
298
|
return
|
|
299
|
+
|
|
300
|
+
logger.debug(f"packet: {packet}")
|
|
299
301
|
|
|
300
302
|
with self._lock:
|
|
301
303
|
if self.channels is None:
|
|
@@ -6,7 +6,6 @@ from loguru import logger
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
from qlsdk.core.crc import crc16
|
|
9
|
-
from qlsdk.core.entity import RscPacket, ImpedancePacket
|
|
10
9
|
from qlsdk.core.utils import to_channels, to_bytes
|
|
11
10
|
from qlsdk.rsc.interface import IDevice
|
|
12
11
|
|
|
@@ -57,6 +56,7 @@ class DeviceCommand(abc.ABC):
|
|
|
57
56
|
if device_type > 0xFF:
|
|
58
57
|
b_device_type = int.to_bytes(device_type, 2, 'little')
|
|
59
58
|
|
|
59
|
+
packet_len = DeviceCommand.HEADER_LEN + body_len + 2
|
|
60
60
|
if b_device_type is None:
|
|
61
61
|
return (
|
|
62
62
|
DeviceCommand.HEADER_PREFIX
|
|
@@ -69,10 +69,11 @@ class DeviceCommand(abc.ABC):
|
|
|
69
69
|
else:
|
|
70
70
|
return (
|
|
71
71
|
DeviceCommand.HEADER_PREFIX
|
|
72
|
-
+ b_device_type[0].to_bytes(1, 'little') # pkgType
|
|
73
72
|
+ b_device_type[1].to_bytes(1, 'little')
|
|
74
|
-
+
|
|
75
|
-
|
|
73
|
+
+ b_device_type[0].to_bytes(1, 'little') # pkgType
|
|
74
|
+
# + device_id.to_bytes(4, 'little')
|
|
75
|
+
+ device_id
|
|
76
|
+
+ packet_len.to_bytes(4, 'little') # +1 for checksum
|
|
76
77
|
+ self.cmd_code.to_bytes(2, 'little')
|
|
77
78
|
)
|
|
78
79
|
|
|
@@ -5,4 +5,8 @@ from .device_factory import DeviceFactory
|
|
|
5
5
|
from .c64_rs import C64RS
|
|
6
6
|
from .c16_rs import C16RS
|
|
7
7
|
from .arskindling import ARSKindling
|
|
8
|
-
from .c256_rs import C256RS
|
|
8
|
+
from .c256_rs import C256RS
|
|
9
|
+
from .c8r import C8R
|
|
10
|
+
from .c16r import C16R
|
|
11
|
+
from .c21r import C21R
|
|
12
|
+
from .c64s1 import C64S1
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from multiprocessing import Queue
|
|
2
2
|
from typing import Literal
|
|
3
3
|
from loguru import logger
|
|
4
|
+
from qlsdk.core.entity import ImpedancePacket, RscPacket
|
|
4
5
|
from qlsdk.persist import ARSKindlingEDFHandler
|
|
5
6
|
from qlsdk.rsc.interface import IDevice
|
|
6
7
|
from qlsdk.rsc.command import *
|
|
@@ -238,7 +239,7 @@ class ARSKindling(QLBaseDevice):
|
|
|
238
239
|
# 写入文件的缓存队列
|
|
239
240
|
if self._signal_cache is None:
|
|
240
241
|
self._signal_cache = Queue(1000000) # 缓冲队列
|
|
241
|
-
tmp = real_data
|
|
242
|
+
tmp = real_data.copy()
|
|
242
243
|
self._signal_cache.put(tmp)
|
|
243
244
|
|
|
244
245
|
if len(self.signal_consumers) > 0 :
|
|
@@ -139,6 +139,10 @@ class QLBaseDevice(IDevice):
|
|
|
139
139
|
|
|
140
140
|
# 处理信号数据
|
|
141
141
|
data = self._signal_wrapper(body)
|
|
142
|
+
|
|
143
|
+
if data is None:
|
|
144
|
+
logger.warning("signal data is None")
|
|
145
|
+
return
|
|
142
146
|
# logger.debug("pkg_id: {}, eeg len: {}".format(data.pkg_id, len(data.eeg)))
|
|
143
147
|
#
|
|
144
148
|
trigger_positions = [index for index, value in enumerate(data.trigger) if value != 0]
|
|
@@ -155,7 +159,7 @@ class QLBaseDevice(IDevice):
|
|
|
155
159
|
# 写入文件的缓存队列
|
|
156
160
|
if self._signal_cache is None:
|
|
157
161
|
self._signal_cache = Queue(256 * 1024 * 1024) # 256MB缓存
|
|
158
|
-
tmp = data
|
|
162
|
+
tmp = data.copy()
|
|
159
163
|
self._signal_cache.put(tmp)
|
|
160
164
|
|
|
161
165
|
if len(self.signal_consumers) > 0 :
|
|
@@ -173,10 +177,11 @@ class QLBaseDevice(IDevice):
|
|
|
173
177
|
q.put(data)
|
|
174
178
|
|
|
175
179
|
def _impedance_wrapper(self, body: bytes):
|
|
176
|
-
packet = ImpedancePacket().transfer(body)
|
|
177
|
-
|
|
180
|
+
packet = ImpedancePacket().transfer(body)
|
|
181
|
+
impedance_channels = self.get_impedance_channels()
|
|
182
|
+
if impedance_channels is not None and len(impedance_channels) > 0:
|
|
178
183
|
# 只保留设置的阻抗通道
|
|
179
|
-
channel_pos = intersection_positions(packet.channels,
|
|
184
|
+
channel_pos = intersection_positions(packet.channels, impedance_channels)
|
|
180
185
|
packet.impedance = [packet.impedance[i] for i in channel_pos]
|
|
181
186
|
packet.channels = [packet.channels[i] for i in channel_pos]
|
|
182
187
|
|
|
@@ -258,6 +263,8 @@ class QLBaseDevice(IDevice):
|
|
|
258
263
|
self._impedance_channels = channels
|
|
259
264
|
|
|
260
265
|
def get_impedance_channels(self):
|
|
266
|
+
if self._impedance_channels is None or len(self._impedance_channels) == 0:
|
|
267
|
+
self._impedance_channels = [i for i in range(1, 63)]
|
|
261
268
|
return self._impedance_channels
|
|
262
269
|
|
|
263
270
|
def start_message_parser(self) -> None:
|
|
@@ -396,15 +403,13 @@ class QLBaseDevice(IDevice):
|
|
|
396
403
|
def start_impedance(self):
|
|
397
404
|
logger.info(f"[设备-{self.device_no}]启动阻抗测量")
|
|
398
405
|
# 设置数据采集参数
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
set_param_msg = bytes.fromhex(f'5aa50239{device_id}3f0000001104ffffffffffffffff000000000000000000000000000000000000000000000000e8030000fa00000010000164000000745c5aa50239390045243a00000012040000000000000000000000000000000000000000000000000000000000000000000001000000000000004c2a')
|
|
402
|
-
logger.debug(f"set_param_msg message is {set_param_msg.hex()}")
|
|
406
|
+
set_param_msg = SetImpedanceParamCommand.build(self).pack()
|
|
407
|
+
# logger.debug(f"set_param_msg message is {set_param_msg.hex()}")
|
|
403
408
|
self.socket.sendall(set_param_msg)
|
|
404
409
|
sleep(0.5)
|
|
405
410
|
|
|
406
411
|
impedance_start_msg = StartImpedanceCommand.build(self).pack()
|
|
407
|
-
logger.debug(f"start_impedance message is {impedance_start_msg.hex()}")
|
|
412
|
+
# logger.debug(f"start_impedance message is {impedance_start_msg.hex()}")
|
|
408
413
|
self.socket.sendall(impedance_start_msg)
|
|
409
414
|
|
|
410
415
|
def stop_impedance(self):
|
|
@@ -524,15 +529,17 @@ class QLBaseDevice(IDevice):
|
|
|
524
529
|
def gen_set_impedance_param(self) -> bytes:
|
|
525
530
|
|
|
526
531
|
# 仅通道生效 32字节,其他不生效-272字节,实际73字节
|
|
527
|
-
body = to_bytes(self._impedance_channels)
|
|
528
|
-
#
|
|
529
|
-
|
|
530
|
-
#
|
|
531
|
-
|
|
532
|
-
#
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
532
|
+
# body = to_bytes(self._impedance_channels)
|
|
533
|
+
# 阻抗参数设置当前是固定长度3f000000, 通道全部指定ffffffffffffffff
|
|
534
|
+
body = bytes.fromhex('ffffffffffffffff')
|
|
535
|
+
# 测量类型-正弦波 不生效 放15字节的0
|
|
536
|
+
body += bytes.fromhex('000000000000000000000000000000')
|
|
537
|
+
# 测量频率-1000Hz 不生效
|
|
538
|
+
# freq = 1000
|
|
539
|
+
# body += freq.to_bytes(4, byteorder='little')# 24 bytes
|
|
540
|
+
body += bytes.fromhex('000000000000000000000000000000000000000000000000')
|
|
541
|
+
|
|
542
|
+
return body
|
|
536
543
|
def disconnect(self):
|
|
537
544
|
logger.info(f"[断开设备-{self.device_no}]的连接...")
|
|
538
545
|
self._listening = False
|
|
@@ -5,6 +5,7 @@ from time import sleep, time_ns
|
|
|
5
5
|
from typing import Any, Dict, Literal
|
|
6
6
|
|
|
7
7
|
from loguru import logger
|
|
8
|
+
from qlsdk.core.entity import RscPacket
|
|
8
9
|
from qlsdk.persist import RscEDFHandler
|
|
9
10
|
from qlsdk.rsc.device.device_factory import DeviceFactory
|
|
10
11
|
from qlsdk.rsc.interface import IDevice
|
|
@@ -17,7 +18,7 @@ from qlsdk.rsc.device.base import QLBaseDevice
|
|
|
17
18
|
'''
|
|
18
19
|
class C16RS(QLBaseDevice):
|
|
19
20
|
|
|
20
|
-
device_type =
|
|
21
|
+
device_type = 0x439 # C16RS设备类型标识符 0x3904
|
|
21
22
|
|
|
22
23
|
def __init__(self, socket):
|
|
23
24
|
|
|
@@ -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 C16R(QLBaseDevice):
|
|
10
|
+
|
|
11
|
+
device_type = 0x339 # C16R设备类型标识符 0x3903
|
|
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(C16R.device_type, C16R)
|
|
@@ -0,0 +1,239 @@
|
|
|
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 C21R(QLBaseDevice):
|
|
10
|
+
|
|
11
|
+
device_type = 0x839 # C21R设备类型标识符 0x3908
|
|
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
|
+
"FPz": 1,
|
|
25
|
+
"FP1": 2,
|
|
26
|
+
"FP2": 3,
|
|
27
|
+
"Fz": 4,
|
|
28
|
+
"F3": 5,
|
|
29
|
+
"F7": 6,
|
|
30
|
+
"F4": 7,
|
|
31
|
+
"F8": 8,
|
|
32
|
+
"Cz": 9,
|
|
33
|
+
"C3": 10,
|
|
34
|
+
"T3": 11,
|
|
35
|
+
"C4": 12,
|
|
36
|
+
"T4": 13,
|
|
37
|
+
"Pz": 14,
|
|
38
|
+
"P3": 15,
|
|
39
|
+
"T5": 16,
|
|
40
|
+
"P4": 17,
|
|
41
|
+
"T6": 18,
|
|
42
|
+
"Qz": 19,
|
|
43
|
+
"O1": 20,
|
|
44
|
+
"O2": 21,
|
|
45
|
+
"M1": 22,
|
|
46
|
+
"M2": 23,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.channel_mapping = {
|
|
50
|
+
"1": 48,
|
|
51
|
+
"2": 59,
|
|
52
|
+
"3": 60,
|
|
53
|
+
"4": 16,
|
|
54
|
+
"5": 11,
|
|
55
|
+
"6": 55,
|
|
56
|
+
"7": 57,
|
|
57
|
+
"8": 15,
|
|
58
|
+
"9": 49,
|
|
59
|
+
"10": 53,
|
|
60
|
+
"11": 63,
|
|
61
|
+
"12": 50,
|
|
62
|
+
"13": 24,
|
|
63
|
+
"14": 40,
|
|
64
|
+
"15": 37,
|
|
65
|
+
"16": 34,
|
|
66
|
+
"17": 18,
|
|
67
|
+
"18": 47,
|
|
68
|
+
"19": 38,
|
|
69
|
+
"20": 51,
|
|
70
|
+
"21": 62,
|
|
71
|
+
"22": 14,
|
|
72
|
+
"23": 10
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
self.channel_display_mapping = {
|
|
76
|
+
48: 1,
|
|
77
|
+
59: 2,
|
|
78
|
+
60: 3,
|
|
79
|
+
16: 4,
|
|
80
|
+
11: 5,
|
|
81
|
+
55: 6,
|
|
82
|
+
57: 7,
|
|
83
|
+
15: 8,
|
|
84
|
+
49: 9,
|
|
85
|
+
53: 10,
|
|
86
|
+
63: 11,
|
|
87
|
+
50: 12,
|
|
88
|
+
24: 13,
|
|
89
|
+
40: 14,
|
|
90
|
+
37: 15,
|
|
91
|
+
34: 16,
|
|
92
|
+
18: 17,
|
|
93
|
+
47: 18,
|
|
94
|
+
38: 19,
|
|
95
|
+
51: 20,
|
|
96
|
+
62: 21,
|
|
97
|
+
14: 22,
|
|
98
|
+
10: 23
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_parent(cls, parent:IDevice) -> IDevice:
|
|
103
|
+
rlt = cls(parent.socket)
|
|
104
|
+
rlt.device_id = parent.device_id
|
|
105
|
+
rlt._device_no = parent.device_no
|
|
106
|
+
return rlt
|
|
107
|
+
|
|
108
|
+
def init_edf_handler(self):
|
|
109
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
110
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
111
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
112
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
113
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C21R')
|
|
114
|
+
logger.debug(f"EDF Handler initialized")
|
|
115
|
+
# 设置采集参数
|
|
116
|
+
def set_acq_param(self, channels:list, sample_rate: int = 500, sample_range:int = 188):
|
|
117
|
+
# 通道合法性校验
|
|
118
|
+
self._channel_validate(channels)
|
|
119
|
+
# 保存原始通道参数
|
|
120
|
+
self._acq_param["original_channels"] = channels
|
|
121
|
+
|
|
122
|
+
# 名称转换为数字通道
|
|
123
|
+
channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
124
|
+
|
|
125
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
126
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
127
|
+
channels = [self.channel_mapping.get(str(i), -1) for i in channels]
|
|
128
|
+
channels = [i if i != -1 else channels[0] for i in channels]
|
|
129
|
+
|
|
130
|
+
# 更新采集参数
|
|
131
|
+
self._acq_param["channels"] = channels
|
|
132
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
133
|
+
self._acq_param["sample_range"] = sample_range
|
|
134
|
+
self._acq_channels = channels
|
|
135
|
+
self._sample_rate = sample_rate
|
|
136
|
+
self._sample_range = sample_range
|
|
137
|
+
|
|
138
|
+
logger.debug(f"C16RS: set_acq_param: {self._acq_param}")
|
|
139
|
+
|
|
140
|
+
# 参数改变后,重置通道位置映射
|
|
141
|
+
self._reverse_ch_pos = None
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def acq_channels(self):
|
|
145
|
+
if self._acq_channels is None:
|
|
146
|
+
# 初始化通道参数为1-11
|
|
147
|
+
self.set_acq_param([i for i in range(1, 12)])
|
|
148
|
+
return self._acq_channels
|
|
149
|
+
|
|
150
|
+
def set_impedance_channels(self, channels:list):
|
|
151
|
+
# 通道合法性校验
|
|
152
|
+
self._channel_validate(channels)
|
|
153
|
+
# 保存原始通道参数
|
|
154
|
+
self._impedance_channels_origin = channels
|
|
155
|
+
|
|
156
|
+
# 名称转换为数字通道
|
|
157
|
+
channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
158
|
+
|
|
159
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
160
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
161
|
+
channels = [self.channel_mapping.get(str(i), -1) for i in channels]
|
|
162
|
+
channels = [i if i != -1 else channels[0] for i in channels]
|
|
163
|
+
self._impedance_channels = channels
|
|
164
|
+
|
|
165
|
+
# 初始化位置映射
|
|
166
|
+
self._impedance_ch_pos = None
|
|
167
|
+
|
|
168
|
+
def get_impedance_channels(self):
|
|
169
|
+
if self._impedance_channels is None or len(self._impedance_channels) == 0:
|
|
170
|
+
self.set_impedance_channels([i for i in range(1, 12)])
|
|
171
|
+
return self._impedance_channels
|
|
172
|
+
|
|
173
|
+
def _channel_validate(self, channels: list):
|
|
174
|
+
# 名称转换为数字通道
|
|
175
|
+
temp_channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
176
|
+
|
|
177
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
178
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
179
|
+
temp_channels = [self.channel_mapping.get(str(i), -1) for i in temp_channels]
|
|
180
|
+
for i in range(len(temp_channels)):
|
|
181
|
+
if temp_channels[i] == -1:
|
|
182
|
+
raise ValueError(f"通道参数有误,通道应为1-23的数字或对应名称,如:1、2或FP1、FP2等")
|
|
183
|
+
|
|
184
|
+
def _impedance_wrapper(self, body: bytes):
|
|
185
|
+
packet = ImpedancePacket().transfer(body)
|
|
186
|
+
impedance_channels = self.get_impedance_channels()
|
|
187
|
+
if impedance_channels is not None and len(impedance_channels) > 0:
|
|
188
|
+
# 只保留设置的阻抗通道
|
|
189
|
+
channel_pos = intersection_positions(packet.channels, impedance_channels)
|
|
190
|
+
packet.impedance = [packet.impedance[i] for i in channel_pos]
|
|
191
|
+
packet.channels = [packet.channels[i] for i in channel_pos]
|
|
192
|
+
|
|
193
|
+
# 升级为类变量,减少计算
|
|
194
|
+
if self._impedance_ch_pos is None:
|
|
195
|
+
self._impedance_ch_pos = map_indices(impedance_channels, packet.channels)
|
|
196
|
+
|
|
197
|
+
packet.channels = self._impedance_channels_origin
|
|
198
|
+
packet.impedance = [packet.impedance[i] for i in self._impedance_ch_pos]
|
|
199
|
+
|
|
200
|
+
return packet
|
|
201
|
+
|
|
202
|
+
# 信号数据转换(默认不处理)
|
|
203
|
+
def _signal_wrapper(self, body: bytes):
|
|
204
|
+
if body is None:
|
|
205
|
+
return
|
|
206
|
+
data = RscPacket().transfer(body)
|
|
207
|
+
# 根据映射关系做通道转换-(注意数据和通道的一致性)
|
|
208
|
+
# data.channels = [self.channel_display_mapping.get(i, i) for i in data.channels]
|
|
209
|
+
|
|
210
|
+
# 升级为类变量,减少计算
|
|
211
|
+
if self._reverse_ch_pos is None:
|
|
212
|
+
self._reverse_ch_pos = map_indices(self._acq_param["channels"], data.channels)
|
|
213
|
+
|
|
214
|
+
# 更新通道(数据)顺序和输入一致
|
|
215
|
+
data.channels = self._acq_param["original_channels"]
|
|
216
|
+
data.eeg = [data.eeg[i] for i in self._reverse_ch_pos]
|
|
217
|
+
|
|
218
|
+
return data
|
|
219
|
+
|
|
220
|
+
def start_stimulation(self):
|
|
221
|
+
raise NotImplementedError("Stimulation function is not supported")
|
|
222
|
+
|
|
223
|
+
def map_indices(A, B):
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
参数:
|
|
227
|
+
A: 源数组(无重复值)
|
|
228
|
+
B: 目标数组(无重复值)
|
|
229
|
+
|
|
230
|
+
返回:
|
|
231
|
+
C: 与A长度相同的数组,元素为A中对应值在B中的索引(不存在则为-1)
|
|
232
|
+
"""
|
|
233
|
+
# 创建B的值到索引的映射字典(O(n)操作)
|
|
234
|
+
b_map = {value: idx for idx, value in enumerate(B)}
|
|
235
|
+
|
|
236
|
+
# 遍历A,获取每个元素在B中的位置(O(m)操作)
|
|
237
|
+
return [b_map.get(a, -1) for a in A]
|
|
238
|
+
|
|
239
|
+
DeviceFactory.register(C21R.device_type, C21R)
|
|
@@ -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.
|
|
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)
|
|
@@ -50,9 +50,12 @@ src/qlsdk/rsc/device/__init__.py
|
|
|
50
50
|
src/qlsdk/rsc/device/arskindling.py
|
|
51
51
|
src/qlsdk/rsc/device/base.py
|
|
52
52
|
src/qlsdk/rsc/device/c16_rs.py
|
|
53
|
+
src/qlsdk/rsc/device/c16r.py
|
|
54
|
+
src/qlsdk/rsc/device/c21r.py
|
|
53
55
|
src/qlsdk/rsc/device/c256_rs.py
|
|
54
56
|
src/qlsdk/rsc/device/c64_rs.py
|
|
55
57
|
src/qlsdk/rsc/device/c64s1.py
|
|
58
|
+
src/qlsdk/rsc/device/c8r.py
|
|
56
59
|
src/qlsdk/rsc/device/device_factory.py
|
|
57
60
|
src/qlsdk/rsc/interface/__init__.py
|
|
58
61
|
src/qlsdk/rsc/interface/command.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|