qlsdk2 0.4.1__py3-none-any.whl → 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qlsdk/core/entity/__init__.py +1 -0
- qlsdk/core/utils.py +20 -15
- qlsdk/persist/__init__.py +2 -1
- qlsdk/persist/ars_edf.py +278 -0
- qlsdk/persist/rsc_edf.py +28 -24
- qlsdk/persist/stream.py +161 -0
- qlsdk/rsc/command/__init__.py +2 -8
- qlsdk/rsc/device/arskindling.py +384 -0
- qlsdk/rsc/device/base.py +64 -39
- qlsdk/rsc/device/c256_rs.py +364 -0
- qlsdk/rsc/device/c64_rs.py +6 -12
- qlsdk/rsc/device/c64s1.py +364 -0
- qlsdk/rsc/device/device_factory.py +5 -1
- qlsdk/rsc/interface/device.py +9 -2
- qlsdk/rsc/interface/parser.py +4 -1
- qlsdk/rsc/manager/container.py +6 -1
- qlsdk/rsc/parser/base.py +7 -4
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.4.2.dist-info}/METADATA +1 -1
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.4.2.dist-info}/RECORD +21 -16
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.4.2.dist-info}/WHEEL +0 -0
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.4.2.dist-info}/top_level.txt +0 -0
qlsdk/core/entity/__init__.py
CHANGED
|
@@ -26,6 +26,7 @@ class RscPacket(object):
|
|
|
26
26
|
self.time_stamp = int.from_bytes(body[0:8], 'little')
|
|
27
27
|
self.result = body[8]
|
|
28
28
|
self.pkg_id = int.from_bytes(body[9: 13], 'little')
|
|
29
|
+
logger.trace(f"pkg_id: {self.pkg_id}")
|
|
29
30
|
self.channels = to_channels(body[13: 45])
|
|
30
31
|
self.origin_sample_rate = int.from_bytes(body[45: 49], 'little')
|
|
31
32
|
self.sample_rate = int.from_bytes(body[49: 53], 'little')
|
qlsdk/core/utils.py
CHANGED
|
@@ -52,19 +52,24 @@ def bytes_to_ints2(b):
|
|
|
52
52
|
import numpy as np
|
|
53
53
|
if __name__ == "__main__":
|
|
54
54
|
|
|
55
|
-
channels = [1]
|
|
56
|
-
channels1 = [1, 2, 3, 4]
|
|
57
|
-
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]
|
|
58
|
-
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]
|
|
59
|
-
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]
|
|
60
|
-
logger.info(to_bytes(channels).hex())
|
|
61
|
-
logger.info(to_bytes(channels1).hex())
|
|
62
|
-
logger.info(to_bytes(channels2).hex())
|
|
63
|
-
logger.info(to_bytes(channels3).hex())
|
|
64
|
-
logger.info(to_bytes(channels4).hex())
|
|
55
|
+
# channels = [1]
|
|
56
|
+
# channels1 = [1, 2, 3, 4]
|
|
57
|
+
# 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]
|
|
58
|
+
# 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]
|
|
59
|
+
# 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]
|
|
60
|
+
# logger.info(to_bytes(channels).hex())
|
|
61
|
+
# logger.info(to_bytes(channels1).hex())
|
|
62
|
+
# logger.info(to_bytes(channels2).hex())
|
|
63
|
+
# logger.info(to_bytes(channels3).hex())
|
|
64
|
+
# logger.info(to_bytes(channels4).hex())
|
|
65
65
|
|
|
66
|
-
bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
|
|
67
|
-
bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
|
|
68
|
-
bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
|
|
69
|
-
|
|
70
|
-
logger.info(to_channels(bytes.fromhex(
|
|
66
|
+
# bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
|
|
67
|
+
# bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
|
|
68
|
+
# bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
|
|
69
|
+
|
|
70
|
+
# logger.info(to_channels(bytes.fromhex(bs1)))
|
|
71
|
+
# logger.info(to_channels(bytes.fromhex(bs2)))
|
|
72
|
+
|
|
73
|
+
aa = 'ff3fff3fff3fff3f000000000000000000000000000000000000000000000000'
|
|
74
|
+
|
|
75
|
+
logger.info(to_channels(bytes.fromhex(aa)))
|
qlsdk/persist/__init__.py
CHANGED
qlsdk/persist/ars_edf.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import os
|
|
3
|
+
from threading import Lock
|
|
4
|
+
from loguru import logger
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from qlsdk.core.entity import RscPacket
|
|
8
|
+
from qlsdk.persist.rsc_edf import RscEDFHandler
|
|
9
|
+
from qlsdk.persist.stream import EDF_FILE_TYPE, EDFStreamWriter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def intersection_positions(A, B):
|
|
13
|
+
setB = set(B)
|
|
14
|
+
seen = set()
|
|
15
|
+
return [idx for idx, elem in enumerate(A)
|
|
16
|
+
if elem in setB and elem not in seen and not seen.add(elem)]
|
|
17
|
+
|
|
18
|
+
# 用作数据结构一致化处理,通过调用公共类写入edf文件
|
|
19
|
+
# 入参包含写入edf的全部前置参数
|
|
20
|
+
# 实时数据包为个性化数据包,含有eeg数据部分
|
|
21
|
+
class ARSKindlingEDFHandler(object):
|
|
22
|
+
'''
|
|
23
|
+
Rsc EDF Handler
|
|
24
|
+
处理EDF文件的读写
|
|
25
|
+
RSC设备通道数根据选择变化,不同通道采样频率相同
|
|
26
|
+
eeg_sample_rate: 采样频率
|
|
27
|
+
physical_max: 物理最大值 (uV)
|
|
28
|
+
physical_min: 物理最小值 (uV)
|
|
29
|
+
resolution: 分辨率
|
|
30
|
+
storage_path: 存储路径
|
|
31
|
+
|
|
32
|
+
@author: qlsdk
|
|
33
|
+
@since: 0.4.0
|
|
34
|
+
'''
|
|
35
|
+
def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
|
|
36
|
+
# edf文件参数
|
|
37
|
+
self.physical_max = physical_max
|
|
38
|
+
self.physical_min = physical_min
|
|
39
|
+
self.digital_max = 8388607 if resolution == 24 else 32767
|
|
40
|
+
self.digital_min = -8388607 if resolution == 24 else - 32768
|
|
41
|
+
self.file_type = EDF_FILE_TYPE["bdf"] if resolution == 24 else EDF_FILE_TYPE["edf"]
|
|
42
|
+
# 点分辨率
|
|
43
|
+
self.resolution = resolution
|
|
44
|
+
# eeg通道数
|
|
45
|
+
self.channels = None
|
|
46
|
+
# eeg采样率
|
|
47
|
+
self.sample_rate = eeg_sample_rate
|
|
48
|
+
# bytes per second
|
|
49
|
+
self.bytes_per_second = 0
|
|
50
|
+
self._edf_writer = None
|
|
51
|
+
self._cache2 = tuple()
|
|
52
|
+
self._recording = False
|
|
53
|
+
self._edf_writer = None
|
|
54
|
+
self.annotations = None
|
|
55
|
+
# 每个数据块大小
|
|
56
|
+
self._chunk = np.array([])
|
|
57
|
+
self._duration = 0
|
|
58
|
+
self._points = 0
|
|
59
|
+
self._first_pkg_id = None
|
|
60
|
+
self._last_pkg_id = None
|
|
61
|
+
self._first_timestamp = None
|
|
62
|
+
self._start_time = None
|
|
63
|
+
self._end_time = None
|
|
64
|
+
self._patient_code = "patient_code"
|
|
65
|
+
self._patient_name = "patient_name"
|
|
66
|
+
self._device_type = "24130032"
|
|
67
|
+
self._device_no = "24130032"
|
|
68
|
+
self._total_packets = 0
|
|
69
|
+
self._lost_packets = 0
|
|
70
|
+
self._storage_path = storage_path
|
|
71
|
+
self._edf_writer_thread = None
|
|
72
|
+
self._file_prefix = None
|
|
73
|
+
|
|
74
|
+
# 癫痫造模仪1对4, 这个是固定模式,写死,规则随设备
|
|
75
|
+
self._edf_handler = {}
|
|
76
|
+
self._edf_Handler_A = None
|
|
77
|
+
self._edf_Handler_B = None
|
|
78
|
+
self._edf_Handler_C = None
|
|
79
|
+
self._edf_Handler_D = None
|
|
80
|
+
|
|
81
|
+
self._channel_spilt = {
|
|
82
|
+
"A" : [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62],
|
|
83
|
+
"B" : [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46],
|
|
84
|
+
"C" : [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
|
|
85
|
+
"D" : [7, 8, 5, 6, 3, 4, 1, 2, 9, 10, 11, 12, 13, 14],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
self._channel_mapping = {
|
|
89
|
+
1: "D7",
|
|
90
|
+
2: "D8",
|
|
91
|
+
3: "D5",
|
|
92
|
+
4: "D6",
|
|
93
|
+
5: "D3",
|
|
94
|
+
6: "D4",
|
|
95
|
+
7: "D1",
|
|
96
|
+
8: "D2",
|
|
97
|
+
9: "D9",
|
|
98
|
+
10: "D10",
|
|
99
|
+
11: "D11",
|
|
100
|
+
12: "D12",
|
|
101
|
+
13: "D13",
|
|
102
|
+
14: "D14",
|
|
103
|
+
|
|
104
|
+
17: "C7",
|
|
105
|
+
18: "C8",
|
|
106
|
+
19: "C5",
|
|
107
|
+
20: "C6",
|
|
108
|
+
21: "C3",
|
|
109
|
+
22: "C4",
|
|
110
|
+
23: "C1",
|
|
111
|
+
24: "C2",
|
|
112
|
+
25: "C9",
|
|
113
|
+
26: "C10",
|
|
114
|
+
27: "C11",
|
|
115
|
+
28: "C12",
|
|
116
|
+
29: "C13",
|
|
117
|
+
30: "C14",
|
|
118
|
+
|
|
119
|
+
33: "B7",
|
|
120
|
+
34: "B8",
|
|
121
|
+
35: "B5",
|
|
122
|
+
36: "B6",
|
|
123
|
+
37: "B3",
|
|
124
|
+
38: "B4",
|
|
125
|
+
39: "B1",
|
|
126
|
+
40: "B2",
|
|
127
|
+
41: "B9",
|
|
128
|
+
42: "B10",
|
|
129
|
+
43: "B11",
|
|
130
|
+
44: "B12",
|
|
131
|
+
45: "B13",
|
|
132
|
+
46: "B14",
|
|
133
|
+
|
|
134
|
+
49: "A7",
|
|
135
|
+
50: "A8",
|
|
136
|
+
51: "A5",
|
|
137
|
+
52: "A6",
|
|
138
|
+
53: "A3",
|
|
139
|
+
54: "A4",
|
|
140
|
+
55: "A1",
|
|
141
|
+
56: "A2",
|
|
142
|
+
57: "A9",
|
|
143
|
+
58: "A10",
|
|
144
|
+
59: "A11",
|
|
145
|
+
60: "A12",
|
|
146
|
+
61: "A13",
|
|
147
|
+
62: "A14"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
self._lock = Lock()
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def file_name(self):
|
|
154
|
+
suffix = "bdf" if self.resolution == 24 else "edf"
|
|
155
|
+
|
|
156
|
+
# 文件名称
|
|
157
|
+
file_name = f"{self._file_prefix}_{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}" if self._file_prefix else f"{self._device_no}_{self._start_time.strftime('%y%m%d%H%I%M')}.{suffix}"
|
|
158
|
+
|
|
159
|
+
if self._storage_path:
|
|
160
|
+
try:
|
|
161
|
+
# 自动创建目录,存在则忽略
|
|
162
|
+
os.makedirs(self._storage_path, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
return f"{self._storage_path}/{file_name}"
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"创建目录[{self._storage_path}]失败: {e}")
|
|
167
|
+
|
|
168
|
+
return file_name
|
|
169
|
+
|
|
170
|
+
def set_device_type(self, device_type):
|
|
171
|
+
if device_type == 0x39:
|
|
172
|
+
self._device_type = "C64RS"
|
|
173
|
+
elif device_type == 0x40:
|
|
174
|
+
self._device_type = "LJ64S1"
|
|
175
|
+
else:
|
|
176
|
+
self._device_type = hex(device_type)
|
|
177
|
+
|
|
178
|
+
def set_device_no(self, device_no):
|
|
179
|
+
self._device_no = device_no
|
|
180
|
+
|
|
181
|
+
def set_storage_path(self, storage_path):
|
|
182
|
+
self._storage_path = storage_path
|
|
183
|
+
|
|
184
|
+
def set_file_prefix(self, file_prefix):
|
|
185
|
+
self._file_prefix = file_prefix
|
|
186
|
+
|
|
187
|
+
def set_patient_code(self, patient_code):
|
|
188
|
+
self._patient_code = patient_code
|
|
189
|
+
|
|
190
|
+
def set_patient_name(self, patient_name):
|
|
191
|
+
self._patient_name = patient_name
|
|
192
|
+
|
|
193
|
+
def write(self, packet: RscPacket):
|
|
194
|
+
# logger.trace(f"packet: {packet}")
|
|
195
|
+
if packet is None:
|
|
196
|
+
# self._edf_writer_thread.stop_recording()
|
|
197
|
+
for k in self._edf_handler.keys():
|
|
198
|
+
self._edf_handler[k].append(None)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
#按分区写入数据
|
|
202
|
+
|
|
203
|
+
for k in self._channel_spilt.keys():
|
|
204
|
+
logger.info(f'分区{k}, {self._channel_spilt[k]}')
|
|
205
|
+
p = packet
|
|
206
|
+
self.writeA(p, self._channel_spilt[k], k)
|
|
207
|
+
|
|
208
|
+
# with self._lock:
|
|
209
|
+
# if self.channels is None:
|
|
210
|
+
# logger.info(f"开始记录数据到文件...")
|
|
211
|
+
# self.channels = packet.channels
|
|
212
|
+
# self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
|
|
213
|
+
# self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
|
|
214
|
+
# self._start_time = datetime.now()
|
|
215
|
+
# logger.info(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
|
|
216
|
+
|
|
217
|
+
# if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
|
|
218
|
+
# self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
|
|
219
|
+
# logger.warning(f"数据包丢失: {self._last_pkg_id} -> {packet.pkg_id}, 丢包数: {packet.pkg_id - self._last_pkg_id - 1}")
|
|
220
|
+
|
|
221
|
+
# self._last_pkg_id = packet.pkg_id
|
|
222
|
+
# self._total_packets += 1
|
|
223
|
+
|
|
224
|
+
# if self._edf_writer_thread is None:
|
|
225
|
+
# self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
|
|
226
|
+
# self._edf_writer_thread.set_start_time(self._start_time)
|
|
227
|
+
# self._edf_writer_thread.start()
|
|
228
|
+
# logger.info(f"开始写入数据: {self.file_name}")
|
|
229
|
+
|
|
230
|
+
# self._edf_writer_thread.append(packet.eeg)
|
|
231
|
+
|
|
232
|
+
def writeA(self, packet: RscPacket, channel_filter, name='A'):
|
|
233
|
+
# 参数检查
|
|
234
|
+
if packet is None or channel_filter is None:
|
|
235
|
+
logger.warning("空数据,忽略")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
channel_pos = intersection_positions(packet.channels, channel_filter)
|
|
239
|
+
|
|
240
|
+
if channel_pos is None or len(channel_pos) == 0 :
|
|
241
|
+
logger.debug(f"没有指定分区{name}的通道,跳过")
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
# 分区数据包写入
|
|
245
|
+
if name not in self._edf_handler.keys():
|
|
246
|
+
edf_handler = RscEDFHandler(self.sample_rate, self.digital_max , self.digital_min, self.resolution)
|
|
247
|
+
edf_handler.set_device_type(self._device_type)
|
|
248
|
+
edf_handler.set_device_no(self._device_no)
|
|
249
|
+
edf_handler.set_storage_path(self._storage_path)
|
|
250
|
+
edf_handler.set_file_prefix(f'{self._file_prefix}_{name}' if self._file_prefix else name)
|
|
251
|
+
logger.info(f"开始写入分区{name}的数据到文件")
|
|
252
|
+
self._edf_handler[name] = edf_handler
|
|
253
|
+
|
|
254
|
+
# 保留本分区的通道和数据
|
|
255
|
+
channels = [packet.channels[p] for p in channel_pos]
|
|
256
|
+
eeg = [packet.eeg[p] for p in channel_pos]
|
|
257
|
+
|
|
258
|
+
# 新建数据包实例
|
|
259
|
+
data = RscPacket()
|
|
260
|
+
data.time_stamp = packet.time_stamp
|
|
261
|
+
data.pkg_id = packet.pkg_id
|
|
262
|
+
data.channels = channels
|
|
263
|
+
data.origin_sample_rate = packet.origin_sample_rate
|
|
264
|
+
data.sample_rate = packet.sample_rate
|
|
265
|
+
data.sample_num = packet.sample_num
|
|
266
|
+
data.resolution = packet.resolution
|
|
267
|
+
data.trigger = packet.trigger
|
|
268
|
+
data.eeg = eeg
|
|
269
|
+
|
|
270
|
+
self._edf_handler[name].write(data)
|
|
271
|
+
|
|
272
|
+
# trigger标记
|
|
273
|
+
# desc: 标记内容
|
|
274
|
+
# cur_time: 设备时间时间戳,非设备发出的trigger不要设置
|
|
275
|
+
def trigger(self, desc, cur_time=None):
|
|
276
|
+
# trigger现在(20250702)多个分区共享, 分发到所有分区文件中标记
|
|
277
|
+
for k in self._edf_handler.keys():
|
|
278
|
+
self._edf_handler[k].trigger(desc, cur_time)
|
qlsdk/persist/rsc_edf.py
CHANGED
|
@@ -199,7 +199,6 @@ class RscEDFHandler(object):
|
|
|
199
199
|
self.annotations = None
|
|
200
200
|
# 每个数据块大小
|
|
201
201
|
self._chunk = np.array([])
|
|
202
|
-
self._Lock = Lock()
|
|
203
202
|
self._duration = 0
|
|
204
203
|
self._points = 0
|
|
205
204
|
self._first_pkg_id = None
|
|
@@ -217,6 +216,8 @@ class RscEDFHandler(object):
|
|
|
217
216
|
self._edf_writer_thread = None
|
|
218
217
|
self._file_prefix = None
|
|
219
218
|
|
|
219
|
+
self._lock = Lock()
|
|
220
|
+
|
|
220
221
|
@property
|
|
221
222
|
def file_name(self):
|
|
222
223
|
suffix = "bdf" if self.resolution == 24 else "edf"
|
|
@@ -240,8 +241,10 @@ class RscEDFHandler(object):
|
|
|
240
241
|
self._device_type = "C64RS"
|
|
241
242
|
elif device_type == 0x40:
|
|
242
243
|
self._device_type = "LJ64S1"
|
|
244
|
+
elif device_type == 0x60:
|
|
245
|
+
self._device_type = "ARSKindling"
|
|
243
246
|
else:
|
|
244
|
-
self._device_type =
|
|
247
|
+
self._device_type = device_type
|
|
245
248
|
|
|
246
249
|
def set_device_no(self, device_no):
|
|
247
250
|
self._device_no = device_no
|
|
@@ -259,33 +262,34 @@ class RscEDFHandler(object):
|
|
|
259
262
|
self._patient_name = patient_name
|
|
260
263
|
|
|
261
264
|
def write(self, packet: RscPacket):
|
|
262
|
-
logger.
|
|
265
|
+
# logger.trace(f"packet: {packet}")
|
|
263
266
|
if packet is None:
|
|
264
267
|
self._edf_writer_thread.stop_recording()
|
|
265
268
|
return
|
|
266
269
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
self.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if self._edf_writer_thread is None:
|
|
283
|
-
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
|
|
284
|
-
self._edf_writer_thread.set_start_time(self._start_time)
|
|
285
|
-
self._edf_writer_thread.start()
|
|
286
|
-
logger.info(f"开始写入数据: {self.file_name}")
|
|
270
|
+
with self._lock:
|
|
271
|
+
if self.channels is None:
|
|
272
|
+
logger.info(f"开始记录数据到文件...")
|
|
273
|
+
self.channels = packet.channels
|
|
274
|
+
self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
|
|
275
|
+
self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
|
|
276
|
+
self._start_time = datetime.now()
|
|
277
|
+
logger.info(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
|
|
278
|
+
|
|
279
|
+
if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
|
|
280
|
+
self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
|
|
281
|
+
logger.warning(f"数据包丢失: {self._last_pkg_id} -> {packet.pkg_id}, 丢包数: {packet.pkg_id - self._last_pkg_id - 1}")
|
|
282
|
+
|
|
283
|
+
self._last_pkg_id = packet.pkg_id
|
|
284
|
+
self._total_packets += 1
|
|
287
285
|
|
|
288
|
-
|
|
286
|
+
if self._edf_writer_thread is None:
|
|
287
|
+
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
|
|
288
|
+
self._edf_writer_thread.set_start_time(self._start_time)
|
|
289
|
+
self._edf_writer_thread.start()
|
|
290
|
+
logger.info(f"开始写入数据: {self.file_name}")
|
|
291
|
+
|
|
292
|
+
self._edf_writer_thread.append(packet.eeg)
|
|
289
293
|
|
|
290
294
|
|
|
291
295
|
# trigger标记
|
qlsdk/persist/stream.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from multiprocessing import Lock, Queue
|
|
5
|
+
from time import time_ns
|
|
6
|
+
from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
|
|
7
|
+
from threading import Thread
|
|
8
|
+
from loguru import logger
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
EDF_FILE_TYPE = {
|
|
12
|
+
"bdf": FILETYPE_BDFPLUS,
|
|
13
|
+
"edf": FILETYPE_EDFPLUS
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class EDFStreamWriter(Thread):
|
|
17
|
+
def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._writer : EdfWriter = None
|
|
20
|
+
self.data_queue : Queue = Queue()
|
|
21
|
+
self._recording = False
|
|
22
|
+
self._points = 0
|
|
23
|
+
self._duration = 0
|
|
24
|
+
self._buffer = None
|
|
25
|
+
|
|
26
|
+
# signal info
|
|
27
|
+
self._channels = channels
|
|
28
|
+
self._n_channels = len(channels)
|
|
29
|
+
self.sample_frequency = sample_frequency
|
|
30
|
+
self.physical_max = physical_max
|
|
31
|
+
self.physical_min = digital_min
|
|
32
|
+
# 和位数相关,edf 16 bits/bdf 24 bits
|
|
33
|
+
self.digital_max = 8388607 if file_type == EDF_FILE_TYPE['bdf'] else 32767
|
|
34
|
+
self.digital_min = -8388608 if file_type == EDF_FILE_TYPE['bdf'] else -32768
|
|
35
|
+
self.file_type = file_type
|
|
36
|
+
self.file_path = file_path
|
|
37
|
+
|
|
38
|
+
# 记录开始时间
|
|
39
|
+
self.start_time = None
|
|
40
|
+
|
|
41
|
+
# header info
|
|
42
|
+
self.equipment = "equipment"
|
|
43
|
+
self.patient_code = "patient_code"
|
|
44
|
+
self.patient_name = "patient_name"
|
|
45
|
+
|
|
46
|
+
def set_channels(self, channels):
|
|
47
|
+
self._channels = channels
|
|
48
|
+
self._n_channels = len(channels)
|
|
49
|
+
|
|
50
|
+
def set_sample_rate(self, sample_rate):
|
|
51
|
+
self._sample_rate = sample_rate
|
|
52
|
+
|
|
53
|
+
def set_start_time(self, time):
|
|
54
|
+
self.start_time = time
|
|
55
|
+
|
|
56
|
+
def stop_recording(self):
|
|
57
|
+
self._recording = False
|
|
58
|
+
|
|
59
|
+
def append(self, data):
|
|
60
|
+
if data:
|
|
61
|
+
# 数据
|
|
62
|
+
self.data_queue.put(data)
|
|
63
|
+
|
|
64
|
+
def trigger(self, onset, desc):
|
|
65
|
+
if self._writer:
|
|
66
|
+
self._writer.writeAnnotation(onset, 1, desc)
|
|
67
|
+
else:
|
|
68
|
+
logger.warning("未创建文件,无法写入Trigger标记")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(self):
|
|
72
|
+
logger.debug(f"启动bdf文件写入线程,写入数据到文件 {self.file_path}")
|
|
73
|
+
# 记录状态
|
|
74
|
+
self._recording = True
|
|
75
|
+
|
|
76
|
+
# 初始化
|
|
77
|
+
if self._writer is None:
|
|
78
|
+
self._init_writer()
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
if self._recording or (not self.data_queue.empty()):
|
|
82
|
+
try:
|
|
83
|
+
data = self.data_queue.get(timeout=30)
|
|
84
|
+
if data is None:
|
|
85
|
+
logger.debug("收到结束信号,停止写入数据")
|
|
86
|
+
break
|
|
87
|
+
# 处理数据
|
|
88
|
+
self._points += len(data[1])
|
|
89
|
+
logger.trace(f"已处理数据点数:{self._points}")
|
|
90
|
+
self._write_file(data)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"异常或超时(30s)结束: {str(e)}")
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
95
|
+
logger.debug("数据记录完成")
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
self.close()
|
|
99
|
+
|
|
100
|
+
def _init_writer(self):
|
|
101
|
+
|
|
102
|
+
# 创建EDF+写入器
|
|
103
|
+
self._writer = EdfWriter(
|
|
104
|
+
self.file_path,
|
|
105
|
+
self._n_channels,
|
|
106
|
+
file_type=self.file_type
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 设置头信息
|
|
110
|
+
self._writer.setPatientCode(self.patient_code)
|
|
111
|
+
self._writer.setPatientName(self.patient_name)
|
|
112
|
+
self._writer.setEquipment(self.equipment)
|
|
113
|
+
self._writer.setStartdatetime(self.start_time if self.start_time else datetime.now())
|
|
114
|
+
|
|
115
|
+
# 配置通道参数
|
|
116
|
+
signal_headers = []
|
|
117
|
+
for ch in range(self._n_channels):
|
|
118
|
+
signal_headers.append({
|
|
119
|
+
"label": f'channels {self._channels[ch]}',
|
|
120
|
+
"dimension": 'uV',
|
|
121
|
+
"sample_frequency": self.sample_frequency,
|
|
122
|
+
"physical_min": self.physical_min,
|
|
123
|
+
"physical_max": self.physical_max,
|
|
124
|
+
"digital_min": self.digital_min,
|
|
125
|
+
"digital_max": self.digital_max
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
self._writer.setSignalHeaders(signal_headers)
|
|
129
|
+
|
|
130
|
+
def _write_file(self, eeg_data):
|
|
131
|
+
try:
|
|
132
|
+
if self._buffer is None or self._buffer.size == 0:
|
|
133
|
+
self._buffer = np.asarray(eeg_data)
|
|
134
|
+
else:
|
|
135
|
+
self._buffer = np.hstack((self._buffer, eeg_data))
|
|
136
|
+
|
|
137
|
+
if self._buffer.shape[1] >= self.sample_frequency:
|
|
138
|
+
block = self._buffer[:, :self.sample_frequency]
|
|
139
|
+
self._write_block(block)
|
|
140
|
+
self._buffer = self._buffer[:, self.sample_frequency:]
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"写入数据异常: {str(e)}")
|
|
144
|
+
|
|
145
|
+
def close(self):
|
|
146
|
+
self._recording = False
|
|
147
|
+
if self._writer:
|
|
148
|
+
self._writer.writeAnnotation(0, 1, "recording start")
|
|
149
|
+
self._writer.writeAnnotation(self._duration, 1, "recording end")
|
|
150
|
+
self._writer.close()
|
|
151
|
+
|
|
152
|
+
logger.info(f"文件: {self.file_path}完成记录, 总点数: {self._points}, 总时长: {self._duration}秒")
|
|
153
|
+
|
|
154
|
+
# 写入1秒的数据
|
|
155
|
+
def _write_block(self, block):
|
|
156
|
+
logger.trace(f"写入数据: {block}")
|
|
157
|
+
# 转换数据类型为float64
|
|
158
|
+
data_float64 = block.astype(np.float64)
|
|
159
|
+
# 写入时转置为(样本数, 通道数)格式
|
|
160
|
+
self._writer.writeSamples(data_float64)
|
|
161
|
+
self._duration += 1
|
qlsdk/rsc/command/__init__.py
CHANGED
|
@@ -165,15 +165,9 @@ class SetAcquisitionParamCommand(DeviceCommand):
|
|
|
165
165
|
cmd_code = 0x451
|
|
166
166
|
cmd_desc = "设置信号采集参数"
|
|
167
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')
|
|
168
|
+
def pack_body(self):
|
|
175
169
|
|
|
176
|
-
return
|
|
170
|
+
return self.device.gen_set_acquirement_param()
|
|
177
171
|
|
|
178
172
|
# 启动采集
|
|
179
173
|
class StartAcquisitionCommand(DeviceCommand):
|