qlsdk2 0.3.0a3__py3-none-any.whl → 0.4.0a2__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/__init__.py +1 -0
- qlsdk/core/__init__.py +4 -0
- qlsdk/core/crc/__init__.py +5 -0
- qlsdk/core/crc/crctools.py +95 -0
- qlsdk/core/device.py +25 -0
- qlsdk/core/entity/__init__.py +92 -0
- qlsdk/core/exception.py +0 -0
- qlsdk/core/filter/__init__.py +1 -0
- qlsdk/core/filter/norch.py +59 -0
- qlsdk/core/local.py +34 -0
- qlsdk/core/message/__init__.py +2 -0
- qlsdk/core/message/command.py +293 -0
- qlsdk/core/message/tcp.py +0 -0
- qlsdk/core/message/udp.py +96 -0
- qlsdk/core/utils.py +68 -0
- qlsdk/persist/__init__.py +2 -1
- qlsdk/persist/rsc_edf.py +236 -0
- qlsdk/rsc/__init__.py +7 -0
- qlsdk/rsc/command/__init__.py +214 -0
- qlsdk/rsc/command/message.py +239 -0
- qlsdk/rsc/device_manager.py +117 -0
- qlsdk/rsc/discover.py +87 -0
- qlsdk/rsc/eegion.py +360 -0
- qlsdk/rsc/entity.py +551 -0
- qlsdk/rsc/paradigm.py +312 -0
- qlsdk/rsc/proxy.py +76 -0
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0a2.dist-info}/METADATA +1 -1
- qlsdk2-0.4.0a2.dist-info/RECORD +40 -0
- qlsdk2-0.3.0a3.dist-info/RECORD +0 -16
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0a2.dist-info}/WHEEL +0 -0
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from time import timezone
|
|
4
|
+
from qlsdk.core.crc import check_crc, crc16
|
|
5
|
+
from qlsdk.core.local import get_ip
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UDPCommand(Enum):
|
|
11
|
+
CONNECT = 0x10
|
|
12
|
+
NOTIFY = 0x09
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UDPMessage(object):
|
|
16
|
+
START = "SHQuanLan"
|
|
17
|
+
MAC = 'mac'
|
|
18
|
+
DEV_TYPE = "dev_type"
|
|
19
|
+
VERSION_CODE = "version_code"
|
|
20
|
+
VERSION_NAME = "version_name"
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._base = None
|
|
24
|
+
self._message = None
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def parse(packet, address = None):
|
|
28
|
+
plen = len(packet)
|
|
29
|
+
if plen < 10:
|
|
30
|
+
logger.trace("message length too short.")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
start = packet[:9].decode("utf-8")
|
|
34
|
+
# quanlan udp message
|
|
35
|
+
if start != UDPMessage.START:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if not check_crc(packet):
|
|
39
|
+
logger.warn(f"数据CRC校验失败,丢弃!")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# message command
|
|
43
|
+
cmd = int.from_bytes(packet[10:12], 'little')
|
|
44
|
+
|
|
45
|
+
return UDPMessage._parse(cmd, packet[12:])
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _parse(cmd, data):
|
|
49
|
+
# 只解析0x09
|
|
50
|
+
if cmd == UDPCommand.NOTIFY.value:
|
|
51
|
+
return UDPMessage.parseDeviceInfo(data)
|
|
52
|
+
else:
|
|
53
|
+
logger.trace(f'不支持的消息. cmd: {hex(cmd)} dlen: {len(data)} data: {data}')
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def parseDeviceInfo(data):
|
|
58
|
+
device_info = {}
|
|
59
|
+
try:
|
|
60
|
+
device_info[UDPMessage.DEV_TYPE] = hex(int.from_bytes(data[:2], 'little'))
|
|
61
|
+
device_info[UDPMessage.MAC] = hex(int.from_bytes(data[2:10], 'little'))
|
|
62
|
+
device_info[UDPMessage.VERSION_CODE] = int.from_bytes(data[42:46], 'little')
|
|
63
|
+
device_info[UDPMessage.VERSION_NAME] = str(data[10:42],'utf-8').split('\x00')[0]
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"parseDeviceInfo异常:{e}")
|
|
66
|
+
|
|
67
|
+
return device_info
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def search(device_id : str, server_ip : str=None, server_port : int=19216):
|
|
71
|
+
# 服务端Ip
|
|
72
|
+
if server_ip is None:
|
|
73
|
+
server_ip = get_ip()
|
|
74
|
+
|
|
75
|
+
logger.debug(f"search device {device_id} on {server_ip}:{server_port}")
|
|
76
|
+
|
|
77
|
+
message = bytearray(28)
|
|
78
|
+
# 消息头
|
|
79
|
+
message[:10] = UDPMessage.START.encode('utf-8')
|
|
80
|
+
# 消息类型
|
|
81
|
+
message[10:12] = UDPCommand.CONNECT.value.to_bytes(2, 'little')
|
|
82
|
+
# 设备序列号格式沿用现有协议
|
|
83
|
+
serial_no = f'F0{device_id[:2]}FFFFFFFF{device_id[4:]}'
|
|
84
|
+
message[12:22] = bytes.fromhex(serial_no)
|
|
85
|
+
# 本机ip
|
|
86
|
+
ip = server_ip.split(".")
|
|
87
|
+
message[22] = (int)(ip[0])
|
|
88
|
+
message[23] = (int)(ip[1])
|
|
89
|
+
message[24] = (int)(ip[2])
|
|
90
|
+
message[25] = (int)(ip[3])
|
|
91
|
+
# 服务端端口(按大端输出)
|
|
92
|
+
message[26:28] = server_port.to_bytes(2, 'big')
|
|
93
|
+
# 校验和(按小端输出)
|
|
94
|
+
checksum = crc16(message).to_bytes(2, 'little')
|
|
95
|
+
|
|
96
|
+
return message + checksum
|
qlsdk/core/utils.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from bitarray import bitarray
|
|
2
|
+
from loguru import logger
|
|
3
|
+
# 通道数组[1,2,3,...]转换为bytes
|
|
4
|
+
def to_bytes(channels: list[int], upper=256) -> bytes:
|
|
5
|
+
byte_len = int(upper / 8)
|
|
6
|
+
channel = [0] * byte_len
|
|
7
|
+
result = bitarray(upper)
|
|
8
|
+
result.setall(0)
|
|
9
|
+
for i in range(len(channels)):
|
|
10
|
+
if channels[i] > 0 and channels[i] <= upper:
|
|
11
|
+
# 每个字节从低位开始计数
|
|
12
|
+
m = (channels[i] - 1) % 8
|
|
13
|
+
result[channels[i] + 6 - 2 * m] = 1
|
|
14
|
+
return result.tobytes()
|
|
15
|
+
|
|
16
|
+
# 通道bytes转换为数组[1,2,3,...]
|
|
17
|
+
def to_channels(data: bytes) -> list[int]:
|
|
18
|
+
ba = bitarray()
|
|
19
|
+
ba.frombytes(data)
|
|
20
|
+
channels = []
|
|
21
|
+
for i in range(len(ba)):
|
|
22
|
+
if ba[i] == 1:
|
|
23
|
+
m = i % 8
|
|
24
|
+
channels.append(i + 8 - 2 * m)
|
|
25
|
+
return channels
|
|
26
|
+
|
|
27
|
+
def bytes_to_ints(b):
|
|
28
|
+
"""将小端字节序的3字节bytes数组转换为int数组"""
|
|
29
|
+
if len(b) % 3 != 0:
|
|
30
|
+
raise ValueError("输入的bytes长度必须是3的倍数")
|
|
31
|
+
return [
|
|
32
|
+
b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
|
|
33
|
+
for i in range(0, len(b), 3)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def bytes_to_int(b: bytes) -> int:
|
|
37
|
+
"""将小端字节序的3字节bytes数组转换为int数组"""
|
|
38
|
+
if len(b) != 3:
|
|
39
|
+
raise ValueError("输入的bytes长度必须是3的倍数")
|
|
40
|
+
return b[0] | (b[1] << 8) | (b[2] << 16)
|
|
41
|
+
def bytes_to_ints2(b):
|
|
42
|
+
"""将小端字节序的3字节bytes数组转换为int数组"""
|
|
43
|
+
if len(b) % 3 != 0:
|
|
44
|
+
raise ValueError("输入的bytes长度必须是3的倍数")
|
|
45
|
+
return [
|
|
46
|
+
b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
|
|
47
|
+
for i in range(0, len(b), 3)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
|
|
53
|
+
channels = [1]
|
|
54
|
+
channels1 = [1, 2, 3, 4]
|
|
55
|
+
channels2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]
|
|
56
|
+
channels3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145,146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207,208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256]
|
|
57
|
+
channels4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
|
|
58
|
+
logger.info(to_bytes(channels).hex())
|
|
59
|
+
logger.info(to_bytes(channels1).hex())
|
|
60
|
+
logger.info(to_bytes(channels2).hex())
|
|
61
|
+
logger.info(to_bytes(channels3).hex())
|
|
62
|
+
logger.info(to_bytes(channels4).hex())
|
|
63
|
+
|
|
64
|
+
bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
|
|
65
|
+
bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
|
|
66
|
+
bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
|
|
67
|
+
logger.info(to_channels(bytes.fromhex(bs1)))
|
|
68
|
+
logger.info(to_channels(bytes.fromhex(bs2)))
|
qlsdk/persist/__init__.py
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
from .edf import EdfHandler
|
|
1
|
+
from .edf import EdfHandler
|
|
2
|
+
from .rsc_edf import RscEDFHandler
|
qlsdk/persist/rsc_edf.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from multiprocessing import Lock, Queue
|
|
3
|
+
from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from loguru import logger
|
|
6
|
+
import numpy as np
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
class EDFWriterThread(Thread):
|
|
10
|
+
def __init__(self, edf_writer : EdfWriter):
|
|
11
|
+
super().__init__(self)
|
|
12
|
+
self._edf_writer : EdfWriter = edf_writer
|
|
13
|
+
self.data_queue : Queue = Queue()
|
|
14
|
+
self._stop_event : bool = False
|
|
15
|
+
self._recording = False
|
|
16
|
+
self._chunk = np.array([])
|
|
17
|
+
self._points = 0
|
|
18
|
+
self._duration = 0
|
|
19
|
+
self._sample_frequency = 0
|
|
20
|
+
self._total_packets = 0
|
|
21
|
+
self._channels = []
|
|
22
|
+
self._sample_rate = 0
|
|
23
|
+
|
|
24
|
+
def start(self):
|
|
25
|
+
self._stop_event = False
|
|
26
|
+
super().start()
|
|
27
|
+
|
|
28
|
+
def stop(self):
|
|
29
|
+
self._stop_event = True
|
|
30
|
+
|
|
31
|
+
def append(self, data):
|
|
32
|
+
# 数据
|
|
33
|
+
self.data_queue.put(data)
|
|
34
|
+
|
|
35
|
+
def _consumer(self):
|
|
36
|
+
logger.debug(f"开始消费数据 _consumer: {self.data_queue.qsize()}")
|
|
37
|
+
while True:
|
|
38
|
+
if self._recording or (not self.data_queue.empty()):
|
|
39
|
+
try:
|
|
40
|
+
data = self.data_queue.get(timeout=10)
|
|
41
|
+
if data is None:
|
|
42
|
+
break
|
|
43
|
+
# 处理数据
|
|
44
|
+
self._points += len(data)
|
|
45
|
+
self._write_file(data)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error("数据队列为空,超时(10s)结束")
|
|
48
|
+
break
|
|
49
|
+
else:
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
self.close()
|
|
53
|
+
|
|
54
|
+
def _write_file(self, eeg_data):
|
|
55
|
+
try:
|
|
56
|
+
if (self._chunk.size == 0):
|
|
57
|
+
self._chunk = np.asarray(eeg_data)
|
|
58
|
+
else:
|
|
59
|
+
self._chunk = np.hstack((self._chunk, eeg_data))
|
|
60
|
+
|
|
61
|
+
if self._chunk.size >= self._sample_rate * self._channels:
|
|
62
|
+
self._write_chunk(self._chunk[:self._sample_rate])
|
|
63
|
+
self._chunk = self._chunk[self._sample_rate:]
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"写入数据异常: {str(e)}")
|
|
67
|
+
|
|
68
|
+
def close(self):
|
|
69
|
+
self._recording = False
|
|
70
|
+
if self._edf_writer:
|
|
71
|
+
self._end_time = datetime.now().timestamp()
|
|
72
|
+
self._edf_writer.writeAnnotation(0, 1, "start recording ")
|
|
73
|
+
self._edf_writer.writeAnnotation(self._duration, 1, "recording end")
|
|
74
|
+
self._edf_writer.close()
|
|
75
|
+
|
|
76
|
+
logger.info(f"文件: {self.file_name}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒 丢包数: {self._lost_packets}/{self._total_packets + self._lost_packets}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _write_chunk(self, chunk):
|
|
81
|
+
logger.debug(f"写入数据: {chunk}")
|
|
82
|
+
# 转换数据类型为float64(pyedflib要求)
|
|
83
|
+
data_float64 = chunk.astype(np.float64)
|
|
84
|
+
# 写入时转置为(样本数, 通道数)格式
|
|
85
|
+
self._edf_writer.writeSamples(data_float64)
|
|
86
|
+
self._duration += 1
|
|
87
|
+
|
|
88
|
+
class RscEDFHandler(object):
|
|
89
|
+
'''
|
|
90
|
+
Rsc EDF Handler
|
|
91
|
+
处理EDF文件的读写
|
|
92
|
+
RSC设备通道数根据选择变化,不同通道采样频率相同
|
|
93
|
+
sample_frequency: 采样频率
|
|
94
|
+
physical_max: 物理最大值
|
|
95
|
+
physical_min: 物理最小值
|
|
96
|
+
digital_max: 数字最大值
|
|
97
|
+
digital_min: 数字最小值
|
|
98
|
+
resolution: 分辨率
|
|
99
|
+
storage_path: 存储路径
|
|
100
|
+
|
|
101
|
+
@author: qlsdk
|
|
102
|
+
@since: 0.4.0
|
|
103
|
+
'''
|
|
104
|
+
def __init__(self, sample_frequency, physical_max, physical_min, digital_max, digital_min, resolution=32, storage_path = None):
|
|
105
|
+
# edf文件参数
|
|
106
|
+
self.physical_max = physical_max
|
|
107
|
+
self.physical_min = physical_min
|
|
108
|
+
self.digital_max = digital_max
|
|
109
|
+
self.digital_min = digital_min
|
|
110
|
+
# 点分辨率
|
|
111
|
+
self.resolution = resolution
|
|
112
|
+
# eeg通道数
|
|
113
|
+
self.eeg_channels = None
|
|
114
|
+
# eeg采样率
|
|
115
|
+
self.eeg_sample_rate = 500
|
|
116
|
+
self.acc_channels = None
|
|
117
|
+
self.acc_sample_rate = 50
|
|
118
|
+
# 缓存
|
|
119
|
+
self._cache = Queue()
|
|
120
|
+
# 采样频率
|
|
121
|
+
self.sample_frequency = sample_frequency
|
|
122
|
+
# bytes per second
|
|
123
|
+
self.bytes_per_second = 0
|
|
124
|
+
self._edf_writer = None
|
|
125
|
+
self._cache2 = tuple()
|
|
126
|
+
self._recording = False
|
|
127
|
+
self._edf_writer = None
|
|
128
|
+
self.annotations = None
|
|
129
|
+
# 每个数据块大小
|
|
130
|
+
self._chunk = np.array([])
|
|
131
|
+
self._Lock = Lock()
|
|
132
|
+
self._duration = 0
|
|
133
|
+
self._points = 0
|
|
134
|
+
self._first_pkg_id = None
|
|
135
|
+
self._last_pkg_id = None
|
|
136
|
+
self._first_timestamp = None
|
|
137
|
+
self._end_time = None
|
|
138
|
+
self._patient_code = "patient_code"
|
|
139
|
+
self._patient_name = "patient_name"
|
|
140
|
+
self._device_type = None
|
|
141
|
+
self._total_packets = 0
|
|
142
|
+
self._lost_packets = 0
|
|
143
|
+
self._storage_path = storage_path
|
|
144
|
+
self._edf_writer_thread = None
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def file_name(self):
|
|
148
|
+
if self._storage_path:
|
|
149
|
+
try:
|
|
150
|
+
os.makedirs(self._storage_path, exist_ok=True) # 自动创建目录,存在则忽略
|
|
151
|
+
return f"{self._storage_path}/{self._device_type}_{self._first_timestamp}.edf"
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"创建目录[{self._storage_path}]失败: {e}")
|
|
154
|
+
|
|
155
|
+
return f"{self._device_type}_{self._first_timestamp}.edf"
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def file_type(self):
|
|
159
|
+
return FILETYPE_BDFPLUS if self.resolution == 24 else FILETYPE_EDFPLUS
|
|
160
|
+
|
|
161
|
+
def set_device_type(self, device_type):
|
|
162
|
+
self._device_type = device_type
|
|
163
|
+
|
|
164
|
+
def set_storage_path(self, storage_path):
|
|
165
|
+
self._storage_path = storage_path
|
|
166
|
+
|
|
167
|
+
def set_patient_code(self, patient_code):
|
|
168
|
+
self._patient_code = patient_code
|
|
169
|
+
|
|
170
|
+
def set_patient_name(self, patient_name):
|
|
171
|
+
self._patient_name = patient_name
|
|
172
|
+
|
|
173
|
+
def append(self, data, channels=None):
|
|
174
|
+
if self._edf_writer_thread is None:
|
|
175
|
+
self._edf_writer_thread = EDFWriterThread(self.init_edf_writer())
|
|
176
|
+
self._edf_writer_thread.start()
|
|
177
|
+
self._recording = True
|
|
178
|
+
self._edf_writer_thread._recording = True
|
|
179
|
+
logger.info(f"开始写入数据: {self.file_name}")
|
|
180
|
+
|
|
181
|
+
if data:
|
|
182
|
+
# # 通道数
|
|
183
|
+
# if self._first_pkg_id is None:
|
|
184
|
+
# self.eeg_channels = data.eeg_ch_count
|
|
185
|
+
# self.acc_channels = data.acc_ch_count
|
|
186
|
+
# self._first_pkg_id = data.pkg_id
|
|
187
|
+
# self._first_timestamp = data.time_stamp
|
|
188
|
+
|
|
189
|
+
# if self._last_pkg_id and self._last_pkg_id != data.pkg_id - 1:
|
|
190
|
+
# self._lost_packets += data.pkg_id - self._last_pkg_id - 1
|
|
191
|
+
# logger.warning(f"数据包丢失: {self._last_pkg_id} -> {data.pkg_id}, 丢包数: {data.pkg_id - self._last_pkg_id - 1}")
|
|
192
|
+
|
|
193
|
+
self._last_pkg_id = data.pkg_id
|
|
194
|
+
self._total_packets += 1
|
|
195
|
+
|
|
196
|
+
# 数据
|
|
197
|
+
self._cache.put(data)
|
|
198
|
+
self._edf_writer_thread.append(data)
|
|
199
|
+
# if not self._recording:
|
|
200
|
+
# self.start()
|
|
201
|
+
|
|
202
|
+
def trigger(self, data):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
def init_edf_writer(self):
|
|
206
|
+
# 创建EDF+写入器
|
|
207
|
+
edf_writer = EdfWriter(
|
|
208
|
+
self.file_name,
|
|
209
|
+
self.eeg_channels,
|
|
210
|
+
file_type=self.file_type
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# 设置头信息
|
|
214
|
+
edf_writer.setPatientCode(self._patient_code)
|
|
215
|
+
edf_writer.setPatientName(self._patient_name)
|
|
216
|
+
edf_writer.setEquipment(self._device_type)
|
|
217
|
+
edf_writer.setStartdatetime(datetime.now())
|
|
218
|
+
|
|
219
|
+
# 配置通道参数
|
|
220
|
+
signal_headers = []
|
|
221
|
+
for ch in range(self.eeg_channels):
|
|
222
|
+
signal_headers.append({
|
|
223
|
+
"label": f'channels {ch + 1}',
|
|
224
|
+
"dimension": 'uV',
|
|
225
|
+
"sample_frequency": self.sample_frequency,
|
|
226
|
+
"physical_min": self.physical_min,
|
|
227
|
+
"physical_max": self.physical_max,
|
|
228
|
+
"digital_min": self.digital_min,
|
|
229
|
+
"digital_max": self.digital_max
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
edf_writer.setSignalHeaders(signal_headers)
|
|
233
|
+
|
|
234
|
+
return edf_writer
|
|
235
|
+
|
|
236
|
+
|
qlsdk/rsc/__init__.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Dict, Type
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from qlsdk.core import crc16
|
|
6
|
+
|
|
7
|
+
class UDPCommand(Enum):
|
|
8
|
+
CONNECT = 0x10
|
|
9
|
+
NOTIFY = 0x09
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DeviceCommand(abc.ABC):
|
|
13
|
+
# 消息头
|
|
14
|
+
HEADER_PREFIX = b'\x5A\xA5'
|
|
15
|
+
# 消息头总长度 2(prefix) +1(pkgType) +1(deviceType) +4(deviceId) +4(len) +2(cmd)
|
|
16
|
+
HEADER_LEN = 14
|
|
17
|
+
# 消息指令码位置
|
|
18
|
+
CMD_POS = 12
|
|
19
|
+
|
|
20
|
+
def __init__(self, device):
|
|
21
|
+
self.device = device
|
|
22
|
+
@property
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def cmd_code(self) -> int:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def pack(cls, body=b'') -> bytes:
|
|
29
|
+
# header+body+checksum
|
|
30
|
+
header = DeviceCommand.build_header(cls.cmd_code, len(body))
|
|
31
|
+
payload = header + body
|
|
32
|
+
return payload + DeviceCommand.calculate_checksum(payload)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def build_header(cmd_code, body_len: int = 0, pkg_type=2, device_type=0, device_id=0) -> bytes:
|
|
36
|
+
"""Construct protocol header"""
|
|
37
|
+
return (
|
|
38
|
+
DeviceCommand.HEADER_PREFIX
|
|
39
|
+
+ pkg_type.to_bytes(1, 'little')
|
|
40
|
+
+ device_type.to_bytes(1, 'little')
|
|
41
|
+
+ device_id.to_bytes(4, 'little')
|
|
42
|
+
+ (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
|
|
43
|
+
+ cmd_code.to_bytes(2, 'little')
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def calculate_checksum(data: bytes) -> bytes:
|
|
48
|
+
return crc16(data).to_bytes(2, 'little')
|
|
49
|
+
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
def parse_body(self, body: bytes):
|
|
52
|
+
"""Implement in subclasses for command-specific parsing"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
class CommandFactory:
|
|
56
|
+
"""Registry for command implementations"""
|
|
57
|
+
_commands: Dict[int, Type[DeviceCommand]] = {}
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def register_command(cls, code: int, command: Type[DeviceCommand]):
|
|
61
|
+
cls._commands[code] = command
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def create_command(cls, code: int) -> Type[DeviceCommand]:
|
|
65
|
+
logger.debug(f"Creating command for code: {hex(code)}")
|
|
66
|
+
if code not in cls._commands:
|
|
67
|
+
logger.warning(f"Unsupported command code: {hex(code)}")
|
|
68
|
+
return cls._commands[DefaultCommand.cmd_code]
|
|
69
|
+
return cls._commands[code]
|
|
70
|
+
|
|
71
|
+
# =============================================================================
|
|
72
|
+
class DefaultCommand(DeviceCommand):
|
|
73
|
+
cmd_code = 0x00
|
|
74
|
+
|
|
75
|
+
def parse_body(self, body: bytes):
|
|
76
|
+
# Response parsing example: 2 bytes version + 4 bytes serial
|
|
77
|
+
logger.info(f"Received body len: {len(body)}")
|
|
78
|
+
|
|
79
|
+
class GetDeviceInfoCommand(DeviceCommand):
|
|
80
|
+
cmd_code = 0x17
|
|
81
|
+
|
|
82
|
+
def parse_body(self, body: bytes):
|
|
83
|
+
logger.info(f"Received GetDeviceInfoCommand body len: {len(body)}")
|
|
84
|
+
# time - 8b
|
|
85
|
+
self.device.connect_time = int.from_bytes(body[0:8], 'little')
|
|
86
|
+
self.device.current_time = self.device.connect_time
|
|
87
|
+
# result - 1b
|
|
88
|
+
result = body[8]
|
|
89
|
+
# deviceId - 4b
|
|
90
|
+
self.device.device_id = body[9:13].hex()
|
|
91
|
+
# deviceType - 4b
|
|
92
|
+
self.device.device_type = body[13:17].hex()
|
|
93
|
+
# softVersion - 4b
|
|
94
|
+
self.device.software_version = body[17:21].hex()
|
|
95
|
+
# hardVersion - 4b
|
|
96
|
+
self.device.hardware_version = body[21:25].hex()
|
|
97
|
+
# deviceName - 16b
|
|
98
|
+
self.device.device_name = body[25:41].decode('utf-8').rstrip('\x00')
|
|
99
|
+
# flag - 4b
|
|
100
|
+
flag = int.from_bytes(body[41:45], 'little')
|
|
101
|
+
logger.debug(f"Received device info: {result}, {flag}, {self.device}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# 握手
|
|
105
|
+
class HandshakeCommand(DeviceCommand):
|
|
106
|
+
cmd_code = 0x01
|
|
107
|
+
|
|
108
|
+
def parse_body(self, body: bytes):
|
|
109
|
+
logger.info(f"Received handshake response: {body.hex()}")
|
|
110
|
+
|
|
111
|
+
# 查询电量
|
|
112
|
+
class QueryBatteryCommand(DeviceCommand):
|
|
113
|
+
cmd_code = 0x16
|
|
114
|
+
def parse_body(self, body: bytes):
|
|
115
|
+
logger.info(f"Received QueryBatteryCommand body len: {len(body)}")
|
|
116
|
+
# time - 8b
|
|
117
|
+
self.device.current_time = int.from_bytes(body[0:8], 'little')
|
|
118
|
+
# result - 1b
|
|
119
|
+
result = body[8]
|
|
120
|
+
# 更新设备信息
|
|
121
|
+
if result == 0:
|
|
122
|
+
# voltage - 2b mV
|
|
123
|
+
self.device.voltage = int.from_bytes(body[9:11], 'little')
|
|
124
|
+
# soc - 1b
|
|
125
|
+
self.device.battery_remain = body[11]
|
|
126
|
+
# soh - 1b
|
|
127
|
+
self.device.battery_total = body[12]
|
|
128
|
+
# state - 1b
|
|
129
|
+
# state = body[13]
|
|
130
|
+
else:
|
|
131
|
+
logger.warning(f"QueryBatteryCommand message received but result is failed.")
|
|
132
|
+
|
|
133
|
+
# 设置采集参数
|
|
134
|
+
class SetAcquisitionParamCommand(DeviceCommand):
|
|
135
|
+
cmd_code = 0x451
|
|
136
|
+
def parse_body(self, body: bytes):
|
|
137
|
+
logger.info(f"Received SetAcquisitionParam response: {body.hex()}")
|
|
138
|
+
|
|
139
|
+
# 启动采集
|
|
140
|
+
class StartAcquisitionCommand(DeviceCommand):
|
|
141
|
+
cmd_code = 0x452
|
|
142
|
+
def parse_body(self, body: bytes):
|
|
143
|
+
logger.info(f"Received acquisition start response: {body.hex()}")
|
|
144
|
+
|
|
145
|
+
# 停止采集
|
|
146
|
+
class StopAcquisitionCommand(DeviceCommand):
|
|
147
|
+
cmd_code = 0x453
|
|
148
|
+
|
|
149
|
+
def parse_body(self, body: bytes):
|
|
150
|
+
logger.info(f"Received acquisition stop response: {body.hex()}")
|
|
151
|
+
# 设置阻抗采集参数
|
|
152
|
+
class SetImpedanceParamCommand(DeviceCommand):
|
|
153
|
+
cmd_code = 0x411
|
|
154
|
+
def parse_body(self, body: bytes):
|
|
155
|
+
logger.info(f"Received SetImpedanceParamCommand response: {body.hex()}")
|
|
156
|
+
# 启动采集
|
|
157
|
+
class StartImpedanceCommand(DeviceCommand):
|
|
158
|
+
cmd_code = 0x412
|
|
159
|
+
def parse_body(self, body: bytes):
|
|
160
|
+
logger.info(f"Received StartImpedanceCommand response: {body.hex()}")
|
|
161
|
+
|
|
162
|
+
# 停止采集
|
|
163
|
+
class StopImpedanceCommand(DeviceCommand):
|
|
164
|
+
cmd_code = 0x413
|
|
165
|
+
|
|
166
|
+
def parse_body(self, body: bytes):
|
|
167
|
+
logger.info(f"Received StopImpedanceCommand response: {body.hex()}")
|
|
168
|
+
|
|
169
|
+
# 启动刺激
|
|
170
|
+
class StartStimulationCommand(DeviceCommand):
|
|
171
|
+
cmd_code = 0x48C
|
|
172
|
+
def parse_body(self, body: bytes):
|
|
173
|
+
logger.info(f"Received acquisition start response: {body.hex()}")
|
|
174
|
+
|
|
175
|
+
# 停止刺激
|
|
176
|
+
class StopStimulationCommand(DeviceCommand):
|
|
177
|
+
cmd_code = 0x488
|
|
178
|
+
|
|
179
|
+
def parse_body(self, body: bytes):
|
|
180
|
+
logger.info(f"Received acquisition stop response: {body.hex()}")
|
|
181
|
+
|
|
182
|
+
# 阻抗数据
|
|
183
|
+
class ImpedanceDataCommand(DeviceCommand):
|
|
184
|
+
cmd_code = 0x415
|
|
185
|
+
|
|
186
|
+
def parse_body(self, body: bytes):
|
|
187
|
+
logger.info(f"Received impedance data: {body.hex()}")
|
|
188
|
+
|
|
189
|
+
# 信号数据
|
|
190
|
+
class SignalDataCommand(DeviceCommand):
|
|
191
|
+
cmd_code = 0x455
|
|
192
|
+
|
|
193
|
+
def parse_body(self, body: bytes):
|
|
194
|
+
logger.info(f"Received signal data: {len(body)}字节, the subscribe is {self.device.signal_consumers}")
|
|
195
|
+
for q in list(self.device.signal_consumers.values()):
|
|
196
|
+
q.put(body)
|
|
197
|
+
|
|
198
|
+
# =============================================================================
|
|
199
|
+
# Command Registration
|
|
200
|
+
# =============================================================================
|
|
201
|
+
|
|
202
|
+
CommandFactory.register_command(DefaultCommand.cmd_code, DefaultCommand)
|
|
203
|
+
CommandFactory.register_command(GetDeviceInfoCommand.cmd_code, GetDeviceInfoCommand)
|
|
204
|
+
CommandFactory.register_command(HandshakeCommand.cmd_code, HandshakeCommand)
|
|
205
|
+
CommandFactory.register_command(QueryBatteryCommand.cmd_code, QueryBatteryCommand)
|
|
206
|
+
CommandFactory.register_command(SetAcquisitionParamCommand.cmd_code, SetAcquisitionParamCommand)
|
|
207
|
+
CommandFactory.register_command(StartAcquisitionCommand.cmd_code, StartAcquisitionCommand)
|
|
208
|
+
CommandFactory.register_command(StopAcquisitionCommand.cmd_code, StopAcquisitionCommand)
|
|
209
|
+
CommandFactory.register_command(SetImpedanceParamCommand.cmd_code, SetImpedanceParamCommand)
|
|
210
|
+
CommandFactory.register_command(StartImpedanceCommand.cmd_code, StartImpedanceCommand)
|
|
211
|
+
CommandFactory.register_command(StopImpedanceCommand.cmd_code, StopImpedanceCommand)
|
|
212
|
+
CommandFactory.register_command(StartStimulationCommand.cmd_code, StartStimulationCommand)
|
|
213
|
+
CommandFactory.register_command(ImpedanceDataCommand.cmd_code, ImpedanceDataCommand)
|
|
214
|
+
CommandFactory.register_command(SignalDataCommand.cmd_code, SignalDataCommand)
|