qlsdk2 0.3.0a3__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
@@ -0,0 +1,34 @@
1
+ import psutil
2
+ import socket
3
+
4
+ def get_active_interfaces():
5
+ interfaces = {}
6
+ # 获取所有接口地址信息
7
+ all_addrs = psutil.net_if_addrs()
8
+ # 获取接口状态(是否处于UP状态)
9
+ stats = psutil.net_if_stats()
10
+
11
+ for name, addrs in all_addrs.items():
12
+ # 检查接口是否启用
13
+ if stats[name].isup:
14
+ ips = []
15
+ for addr in addrs:
16
+ # 提取IPv4和IPv6地址
17
+ if addr.family == socket.AF_INET:
18
+ ips.append(f"IPv4: {addr.address}")
19
+ elif addr.family == socket.AF_INET6:
20
+ ips.append(f"IPv6: {addr.address}")
21
+ # 过滤无IP的接口(可选)
22
+ if ips:
23
+ interfaces[name] = {
24
+ "status": "UP",
25
+ "IPs": ips
26
+ }
27
+ return interfaces
28
+
29
+ # 调用并打印结果
30
+ active_ifs = get_active_interfaces()
31
+ for iface, info in active_ifs.items():
32
+ print(f"接口: {iface}")
33
+ print(f"状态: {info['status']}")
34
+ print(f"IP地址: {info['IPs']}\n")
@@ -0,0 +1,55 @@
1
+ from threading import Thread
2
+ import psutil
3
+ import time
4
+
5
+ def get_active_ipv4():
6
+ ips = []
7
+ # 获取所有接口地址信息
8
+ all_addrs = psutil.net_if_addrs()
9
+ # 获取接口状态(是否处于UP状态)
10
+ stats = psutil.net_if_stats()
11
+
12
+ for name, addrs in all_addrs.items():
13
+ # 检查接口是否启用
14
+ if stats[name].isup:
15
+ for addr in addrs:
16
+ # 提取IPv4地址
17
+ if addr.family == socket.AF_INET:
18
+ ips.append(addr.address)
19
+ return ips
20
+ def monitor_up_interfaces(interval=2, callback=None):
21
+ prev_status = {iface: psutil.net_if_stats()[iface].isup
22
+ for iface in psutil.net_if_stats()}
23
+
24
+ while True:
25
+ current_stats = psutil.net_if_stats()
26
+ for iface, stats in current_stats.items():
27
+ current_up = stats.isup
28
+ # 检测状态变化
29
+ if current_up != prev_status.get(iface, None):
30
+ if current_up:
31
+ print(f"[UP] 接口 {iface} 激活")
32
+ if callback: callback(iface, "UP")
33
+ else:
34
+ print(f"[DOWN] 接口 {iface} 断开")
35
+ if callback: callback(iface, "DOWN")
36
+ prev_status[iface] = current_up
37
+ time.sleep(interval)
38
+
39
+ # 自定义回调函数示例
40
+ def notify(iface, status):
41
+ if status == "UP":
42
+ print(f"接口 {iface} 已激活")
43
+
44
+ # 启动监听
45
+ # monitor_up_interfaces(callback=notify)
46
+
47
+ monitor = Thread(target=monitor_up_interfaces, name="Network Status Monitor", args=(2, notify))
48
+ monitor.start()
49
+
50
+ import socket
51
+ def is_port_in_use(ip, port):
52
+ with socket.socket() as s:
53
+ return s.connect_ex((ip, port)) == 0
54
+
55
+ print(get_active_ipv4())
qlsdk/core/utils.py ADDED
@@ -0,0 +1,70 @@
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
+
26
+ channels.sort()
27
+ return channels
28
+
29
+ def bytes_to_ints(b):
30
+ """将小端字节序的3字节bytes数组转换为int数组"""
31
+ if len(b) % 3 != 0:
32
+ raise ValueError("输入的bytes长度必须是3的倍数")
33
+ return [
34
+ b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
35
+ for i in range(0, len(b), 3)
36
+ ]
37
+
38
+ def bytes_to_int(b: bytes) -> int:
39
+ """将小端字节序的3字节bytes数组转换为int数组"""
40
+ if len(b) != 3:
41
+ raise ValueError("输入的bytes长度必须是3的倍数")
42
+ return b[0] | (b[1] << 8) | (b[2] << 16)
43
+ def bytes_to_ints2(b):
44
+ """将小端字节序的3字节bytes数组转换为int数组"""
45
+ if len(b) % 3 != 0:
46
+ raise ValueError("输入的bytes长度必须是3的倍数")
47
+ return [
48
+ b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
49
+ for i in range(0, len(b), 3)
50
+ ]
51
+
52
+ import numpy as np
53
+ if __name__ == "__main__":
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())
65
+
66
+ bs = 'ffffffffffffff7f000000000000000000000000000000000000000000000000'
67
+ bs1 = '8000000000000000000000000000000000000000000000000000000000000000'
68
+ bs2 = '0100000000000000000000000000000000000000000000000000000000000000'
69
+ logger.info(to_channels(bytes.fromhex(bs1)))
70
+ 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
@@ -0,0 +1,299 @@
1
+ from datetime import datetime
2
+ from multiprocessing import Lock, Queue
3
+ from time import time_ns
4
+ from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
5
+ from threading import Thread
6
+ from loguru import logger
7
+ import numpy as np
8
+ import os
9
+ from qlsdk.core import RscPacket
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
162
+
163
+ # 用作数据结构一致化处理,通过调用公共类写入edf文件
164
+ # 入参包含写入edf的全部前置参数
165
+ # 实时数据包为个性化数据包,含有eeg数据部分
166
+ class RscEDFHandler(object):
167
+ '''
168
+ Rsc EDF Handler
169
+ 处理EDF文件的读写
170
+ RSC设备通道数根据选择变化,不同通道采样频率相同
171
+ eeg_sample_rate: 采样频率
172
+ physical_max: 物理最大值
173
+ physical_min: 物理最小值
174
+ resolution: 分辨率
175
+ storage_path: 存储路径
176
+
177
+ @author: qlsdk
178
+ @since: 0.4.0
179
+ '''
180
+ def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
181
+ # edf文件参数
182
+ self.physical_max = physical_max
183
+ self.physical_min = physical_min
184
+ self.digital_max = 8388607
185
+ self.digital_min = -8388607
186
+ self.file_type = EDF_FILE_TYPE["bdf"] if resolution == 24 else EDF_FILE_TYPE["edf"]
187
+ # 点分辨率
188
+ self.resolution = resolution
189
+ # eeg通道数
190
+ self.channels = None
191
+ # eeg采样率
192
+ self.sample_rate = eeg_sample_rate
193
+ # bytes per second
194
+ self.bytes_per_second = 0
195
+ self._edf_writer = None
196
+ self._cache2 = tuple()
197
+ self._recording = False
198
+ self._edf_writer = None
199
+ self.annotations = None
200
+ # 每个数据块大小
201
+ self._chunk = np.array([])
202
+ self._Lock = Lock()
203
+ self._duration = 0
204
+ self._points = 0
205
+ self._first_pkg_id = None
206
+ self._last_pkg_id = None
207
+ self._first_timestamp = None
208
+ self._start_time = None
209
+ self._end_time = None
210
+ self._patient_code = "patient_code"
211
+ self._patient_name = "patient_name"
212
+ self._device_type = "24130032"
213
+ self._device_no = "24130032"
214
+ self._total_packets = 0
215
+ self._lost_packets = 0
216
+ self._storage_path = storage_path
217
+ self._edf_writer_thread = None
218
+ self._file_prefix = None
219
+
220
+ @property
221
+ def file_name(self):
222
+ suffix = "bdf" if self.resolution == 24 else "edf"
223
+
224
+ # 文件名称
225
+ 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}"
226
+
227
+ if self._storage_path:
228
+ try:
229
+ os.makedirs(self._storage_path, exist_ok=True) # 自动创建目录,存在则忽略
230
+ return f"{self._storage_path}/{file_name}"
231
+ except Exception as e:
232
+ logger.error(f"创建目录[{self._storage_path}]失败: {e}")
233
+
234
+
235
+ return file_name
236
+
237
+ def set_device_type(self, device_type):
238
+ if device_type == 0x39:
239
+ self._device_type = "C64RS"
240
+ elif device_type == 0x40:
241
+ self._device_type = "LJ64S1"
242
+ else:
243
+ self._device_type = hex(device_type)
244
+
245
+ def set_device_no(self, device_no):
246
+ self._device_no = device_no
247
+
248
+ def set_storage_path(self, storage_path):
249
+ self._storage_path = storage_path
250
+
251
+ def set_file_prefix(self, file_prefix):
252
+ self._file_prefix = file_prefix
253
+
254
+ def set_patient_code(self, patient_code):
255
+ self._patient_code = patient_code
256
+
257
+ def set_patient_name(self, patient_name):
258
+ self._patient_name = patient_name
259
+
260
+ def write(self, packet: RscPacket):
261
+ logger.debug(f"packet: {packet}")
262
+ if packet is None:
263
+ self._edf_writer_thread.stop_recording()
264
+ return
265
+
266
+ if self.channels is None:
267
+ logger.info(f"开始记录数据到文件...")
268
+ self.channels = packet.channels
269
+ self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
270
+ self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
271
+ self._start_time = datetime.now()
272
+ logger.info(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
273
+
274
+ if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
275
+ self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
276
+ logger.warning(f"数据包丢失: {self._last_pkg_id} -> {packet.pkg_id}, 丢包数: {packet.pkg_id - self._last_pkg_id - 1}")
277
+
278
+ self._last_pkg_id = packet.pkg_id
279
+ self._total_packets += 1
280
+
281
+ if self._edf_writer_thread is None:
282
+ self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
283
+ self._edf_writer_thread.set_start_time(self._start_time)
284
+ self._edf_writer_thread.start()
285
+ logger.info(f"开始写入数据: {self.file_name}")
286
+
287
+ self._edf_writer_thread.append(packet.eeg)
288
+
289
+
290
+ # trigger标记
291
+ # desc: 标记内容
292
+ # cur_time: 设备时间时间戳,非设备发出的trigger不要设置
293
+ def trigger(self, desc, cur_time=None):
294
+ if cur_time is None:
295
+ onset = datetime.now().timestamp() - self._start_time.timestamp()
296
+ else:
297
+ onset = cur_time - self._first_timestamp
298
+ self._edf_writer_thread.trigger(onset, desc)
299
+
qlsdk/rsc/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from .discover import *
2
+
3
+ from .entity import DeviceParser, QLDevice
4
+ from .command import *
5
+ from .device_manager import DeviceContainer
6
+ from .proxy import DeviceProxy
7
+ from .paradigm import *