qlsdk2 0.5.0__py3-none-any.whl → 0.5.1__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 +8 -12
- qlsdk/persist/rsc_edf.py +21 -7
- qlsdk/rsc/command/__init__.py +6 -2
- qlsdk/rsc/device/base.py +13 -2
- qlsdk/rsc/device/c16_rs.py +51 -71
- qlsdk/rsc/device/device_factory.py +1 -1
- qlsdk/rsc/interface/device.py +11 -1
- qlsdk/rsc/manager/container.py +13 -8
- qlsdk/rsc/parser/base-new.py +135 -0
- qlsdk/rsc/parser/base.py +65 -58
- qlsdk2-0.5.1.dist-info/METADATA +57 -0
- {qlsdk2-0.5.0.dist-info → qlsdk2-0.5.1.dist-info}/RECORD +14 -13
- {qlsdk2-0.5.0.dist-info → qlsdk2-0.5.1.dist-info}/WHEEL +1 -1
- qlsdk2-0.5.0.dist-info/METADATA +0 -40
- {qlsdk2-0.5.0.dist-info → qlsdk2-0.5.1.dist-info}/top_level.txt +0 -0
qlsdk/core/entity/__init__.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
from qlsdk.core.utils import to_channels
|
|
2
2
|
from loguru import logger
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
def __init__(self, device_type, device_id, channels, data):
|
|
6
|
-
self.data = data
|
|
7
|
-
self.channels = None
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class RscPacket(object):
|
|
4
|
+
class Packet(object):
|
|
11
5
|
def __init__(self):
|
|
12
6
|
self.time_stamp = None
|
|
13
7
|
self.pkg_id = None
|
|
14
8
|
self.result = None
|
|
15
9
|
self.channels = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RscPacket(Packet):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
16
15
|
self.origin_sample_rate = None
|
|
17
16
|
self.sample_rate = None
|
|
18
17
|
self.sample_num = None
|
|
@@ -65,12 +64,9 @@ class RscPacket(object):
|
|
|
65
64
|
eeg: {self.eeg}
|
|
66
65
|
"""
|
|
67
66
|
|
|
68
|
-
class ImpedancePacket(
|
|
67
|
+
class ImpedancePacket(Packet):
|
|
69
68
|
def __init__(self):
|
|
70
|
-
|
|
71
|
-
self.pkg_id = None
|
|
72
|
-
self.result = None
|
|
73
|
-
self.channels = None
|
|
69
|
+
super().__init__()
|
|
74
70
|
self.data_len = None
|
|
75
71
|
self.impedance = None
|
|
76
72
|
|
qlsdk/persist/rsc_edf.py
CHANGED
|
@@ -15,7 +15,8 @@ EDF_FILE_TYPE = {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
class EDFStreamWriter(Thread):
|
|
18
|
-
def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path):
|
|
18
|
+
def __init__(self, channels, sample_frequency, physical_max, digital_min, file_type, file_path, record_duration=None):
|
|
19
|
+
|
|
19
20
|
super().__init__()
|
|
20
21
|
self._writer : EdfWriter = None
|
|
21
22
|
self.data_queue : Queue = Queue()
|
|
@@ -23,6 +24,8 @@ class EDFStreamWriter(Thread):
|
|
|
23
24
|
self._points = 0
|
|
24
25
|
self._duration = 0
|
|
25
26
|
self._buffer = None
|
|
27
|
+
# 设置edf/bdf文件参数,设置[0.001, 1)可以在1秒内记录多个事件(不建议开启)
|
|
28
|
+
self.record_duration = record_duration
|
|
26
29
|
|
|
27
30
|
# signal info
|
|
28
31
|
self._channels = channels
|
|
@@ -62,9 +65,10 @@ class EDFStreamWriter(Thread):
|
|
|
62
65
|
# 数据
|
|
63
66
|
self.data_queue.put(data)
|
|
64
67
|
|
|
65
|
-
def trigger(self, onset, desc):
|
|
68
|
+
def trigger(self, onset, desc: str):
|
|
66
69
|
if self._writer:
|
|
67
|
-
|
|
70
|
+
logger.trace(f"[{onset} : {desc}]")
|
|
71
|
+
self._writer.writeAnnotation(onset, -1, desc)
|
|
68
72
|
else:
|
|
69
73
|
logger.warning("未创建文件,无法写入Trigger标记")
|
|
70
74
|
|
|
@@ -135,6 +139,8 @@ class EDFStreamWriter(Thread):
|
|
|
135
139
|
})
|
|
136
140
|
|
|
137
141
|
self._writer.setSignalHeaders(signal_headers)
|
|
142
|
+
if self.record_duration:
|
|
143
|
+
self._writer.setDatarecordDuration(self.record_duration) # 每个数据块1秒
|
|
138
144
|
|
|
139
145
|
def _write_file(self, eeg_data):
|
|
140
146
|
try:
|
|
@@ -189,7 +195,7 @@ class RscEDFHandler(object):
|
|
|
189
195
|
@author: qlsdk
|
|
190
196
|
@since: 0.4.0
|
|
191
197
|
'''
|
|
192
|
-
def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None):
|
|
198
|
+
def __init__(self, eeg_sample_rate, physical_max, physical_min, resolution=24, storage_path = None, record_duration=None):
|
|
193
199
|
# edf文件参数
|
|
194
200
|
self.physical_max = physical_max
|
|
195
201
|
self.physical_min = physical_min
|
|
@@ -225,6 +231,7 @@ class RscEDFHandler(object):
|
|
|
225
231
|
self._total_packets = 0
|
|
226
232
|
self._lost_packets = 0
|
|
227
233
|
self._storage_path = storage_path
|
|
234
|
+
self._record_duration = record_duration
|
|
228
235
|
self._edf_writer_thread = None
|
|
229
236
|
self._file_prefix = None
|
|
230
237
|
|
|
@@ -299,7 +306,7 @@ class RscEDFHandler(object):
|
|
|
299
306
|
self._total_packets += 1
|
|
300
307
|
|
|
301
308
|
if self._edf_writer_thread is None:
|
|
302
|
-
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name)
|
|
309
|
+
self._edf_writer_thread = EDFStreamWriter(self.channels, self.sample_rate, self.physical_max, self.physical_min, self.file_type, self.file_name, self._record_duration)
|
|
303
310
|
self._edf_writer_thread.set_start_time(self._start_time)
|
|
304
311
|
self._edf_writer_thread.start()
|
|
305
312
|
logger.info(f"开始写入数据: {self.file_name}")
|
|
@@ -310,9 +317,16 @@ class RscEDFHandler(object):
|
|
|
310
317
|
# trigger标记
|
|
311
318
|
# desc: 标记内容
|
|
312
319
|
# cur_time: 设备时间时间戳,非设备发出的trigger不要设置
|
|
313
|
-
def trigger(self, desc, cur_time=None):
|
|
320
|
+
def trigger(self, desc: str, cur_time=None):
|
|
321
|
+
if self._edf_writer_thread is None:
|
|
322
|
+
logger.warning(f"File writing has not started, discarding trigger {desc}")
|
|
323
|
+
return
|
|
324
|
+
|
|
314
325
|
if cur_time is None:
|
|
315
|
-
|
|
326
|
+
# 计算trigger位置
|
|
327
|
+
if self._start_time:
|
|
328
|
+
onset = datetime.now().timestamp() - self._start_time.timestamp()
|
|
329
|
+
else: onset = 0
|
|
316
330
|
else:
|
|
317
331
|
onset = cur_time - self._first_timestamp
|
|
318
332
|
self._edf_writer_thread.trigger(onset, desc)
|
qlsdk/rsc/command/__init__.py
CHANGED
|
@@ -170,7 +170,7 @@ class QueryBatteryCommand(DeviceCommand):
|
|
|
170
170
|
self.device.battery_total = body[12]
|
|
171
171
|
# state - 1b
|
|
172
172
|
# state = body[13]
|
|
173
|
-
logger.
|
|
173
|
+
logger.trace(f"电量更新: {self.device}")
|
|
174
174
|
else:
|
|
175
175
|
logger.warning(f"QueryBatteryCommand message received but result is failed.")
|
|
176
176
|
|
|
@@ -214,6 +214,10 @@ class StartImpedanceCommand(DeviceCommand):
|
|
|
214
214
|
body += to_bytes(self.device.acq_channels)
|
|
215
215
|
body += bytes.fromhex('0000000000000000') # 8字节占位符
|
|
216
216
|
return body
|
|
217
|
+
|
|
218
|
+
def parse_body(self, body):
|
|
219
|
+
logger.info(f"Received StartImpedance response: {body.hex()}")
|
|
220
|
+
return super().parse_body(body)
|
|
217
221
|
|
|
218
222
|
|
|
219
223
|
# 停止阻抗测量
|
|
@@ -293,7 +297,7 @@ class ImpedanceDataCommand(DeviceCommand):
|
|
|
293
297
|
cmd_desc = "阻抗数据"
|
|
294
298
|
|
|
295
299
|
def parse_body(self, body: bytes):
|
|
296
|
-
logger.info(f"Received impedance data: {body.hex()}")
|
|
300
|
+
# logger.info(f"Received impedance data: {body.hex()}")
|
|
297
301
|
packet = ImpedancePacket().transfer(body)
|
|
298
302
|
|
|
299
303
|
# 信号数据
|
qlsdk/rsc/device/base.py
CHANGED
|
@@ -20,6 +20,9 @@ class QLBaseDevice(IDevice):
|
|
|
20
20
|
# 启动数据解析线程
|
|
21
21
|
self._parser: IParser = None
|
|
22
22
|
|
|
23
|
+
# 用在edf/bdf文件写入中,不建议使用
|
|
24
|
+
self._record_duration = None
|
|
25
|
+
|
|
23
26
|
# 设备信息
|
|
24
27
|
self.device_id = None
|
|
25
28
|
self.device_name = None
|
|
@@ -130,8 +133,11 @@ class QLBaseDevice(IDevice):
|
|
|
130
133
|
def parser(self) -> IParser:
|
|
131
134
|
return self._parser
|
|
132
135
|
|
|
136
|
+
def set_record_duration(self, record_duration: float):
|
|
137
|
+
self._record_duration = record_duration
|
|
138
|
+
|
|
133
139
|
# 数据包处理
|
|
134
|
-
def produce(self, data: RscPacket):
|
|
140
|
+
def produce(self, data: RscPacket, type:Literal['signal', 'impedance']="signal"):
|
|
135
141
|
if data is None: return
|
|
136
142
|
|
|
137
143
|
# 处理信号数据
|
|
@@ -147,6 +153,10 @@ class QLBaseDevice(IDevice):
|
|
|
147
153
|
|
|
148
154
|
# 发送数据包到订阅者
|
|
149
155
|
for q in list(self.signal_consumers.values()):
|
|
156
|
+
# 队列满了就丢弃最早的数据
|
|
157
|
+
if q.full():
|
|
158
|
+
q.get()
|
|
159
|
+
|
|
150
160
|
q.put(data)
|
|
151
161
|
|
|
152
162
|
# 信号数据转换(默认不处理)
|
|
@@ -176,7 +186,8 @@ class QLBaseDevice(IDevice):
|
|
|
176
186
|
|
|
177
187
|
def stop_listening(self):
|
|
178
188
|
logger.trace(f"设备{self.device_no}停止socket监听")
|
|
179
|
-
self._listening = False
|
|
189
|
+
self._listening = False
|
|
190
|
+
self._parser.stop()
|
|
180
191
|
|
|
181
192
|
@property
|
|
182
193
|
def device_type(self) -> int:
|
qlsdk/rsc/device/c16_rs.py
CHANGED
|
@@ -23,7 +23,8 @@ class C16RS(QLBaseDevice):
|
|
|
23
23
|
|
|
24
24
|
super().__init__(socket)
|
|
25
25
|
# 存储通道反向映射位置值
|
|
26
|
-
self._reverse_ch_pos = None
|
|
26
|
+
self._reverse_ch_pos = None
|
|
27
|
+
|
|
27
28
|
self.channel_name_mapping = {
|
|
28
29
|
"FP1": 1,
|
|
29
30
|
"FP2": 2,
|
|
@@ -45,79 +46,59 @@ class C16RS(QLBaseDevice):
|
|
|
45
46
|
"P3": 18,
|
|
46
47
|
"P7": 19,
|
|
47
48
|
"P4": 20,
|
|
48
|
-
"P8": 21
|
|
49
|
+
"P8": 21,
|
|
50
|
+
"T3": 8,
|
|
51
|
+
"T4": 9,
|
|
52
|
+
"A1": 10,
|
|
53
|
+
"A2": 11,
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
# self.channel_mapping = {
|
|
52
|
-
# "1": 61,
|
|
53
|
-
# "2": 62,
|
|
54
|
-
# "3": 63,
|
|
55
|
-
# "4": 64,
|
|
56
|
-
# "5": 65,
|
|
57
|
-
# "6": 66,
|
|
58
|
-
# "7": 68,
|
|
59
|
-
# "8": 67,
|
|
60
|
-
# "9": 53,
|
|
61
|
-
# "10": 54,
|
|
62
|
-
# "11": 55,
|
|
63
|
-
# "12": 57,
|
|
64
|
-
# "13": 59,
|
|
65
|
-
# "14": 58,
|
|
66
|
-
# "15": 60,
|
|
67
|
-
# "16": 56,
|
|
68
|
-
# "17": 26,
|
|
69
|
-
# "18": 25,
|
|
70
|
-
# "19": 24,
|
|
71
|
-
# "20": 23,
|
|
72
|
-
# "21": 22
|
|
73
|
-
# }
|
|
74
|
-
|
|
75
56
|
self.channel_mapping = {
|
|
76
|
-
"1":
|
|
77
|
-
"2":
|
|
78
|
-
"3":
|
|
79
|
-
"4":
|
|
80
|
-
"5":
|
|
81
|
-
"6":
|
|
82
|
-
"7":
|
|
83
|
-
"8":
|
|
84
|
-
"9":
|
|
85
|
-
"10":
|
|
86
|
-
"11":
|
|
87
|
-
"12":
|
|
88
|
-
"13":
|
|
89
|
-
"14":
|
|
90
|
-
"15":
|
|
91
|
-
"16":
|
|
92
|
-
"17":
|
|
93
|
-
"18":
|
|
94
|
-
"19":
|
|
95
|
-
"20":
|
|
96
|
-
"21":
|
|
57
|
+
"1": 59,
|
|
58
|
+
"2": 60,
|
|
59
|
+
"3": 53,
|
|
60
|
+
"4": 50,
|
|
61
|
+
"5": 51,
|
|
62
|
+
"6": 64,
|
|
63
|
+
"7": 49,
|
|
64
|
+
"8": 63,
|
|
65
|
+
"9": 24,
|
|
66
|
+
"10": 14,
|
|
67
|
+
"11": 10,
|
|
68
|
+
"12": 11,
|
|
69
|
+
"13": 57,
|
|
70
|
+
"14": 16,
|
|
71
|
+
"15": 55,
|
|
72
|
+
"16": 15,
|
|
73
|
+
"17": 40,
|
|
74
|
+
"18": 37,
|
|
75
|
+
"19": 34,
|
|
76
|
+
"20": 18,
|
|
77
|
+
"21": 47
|
|
97
78
|
}
|
|
98
79
|
|
|
99
80
|
self.channel_display_mapping = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
81
|
+
59: 1,
|
|
82
|
+
60: 2,
|
|
83
|
+
53: 3,
|
|
84
|
+
50: 4,
|
|
85
|
+
51: 5,
|
|
86
|
+
64: 6,
|
|
87
|
+
49: 7,
|
|
88
|
+
63: 8,
|
|
89
|
+
24: 9,
|
|
90
|
+
14: 10,
|
|
91
|
+
10: 11,
|
|
92
|
+
11: 12,
|
|
93
|
+
57: 13,
|
|
94
|
+
16: 14,
|
|
95
|
+
55: 15,
|
|
96
|
+
15: 16,
|
|
97
|
+
40: 17,
|
|
98
|
+
37: 18,
|
|
99
|
+
34: 19,
|
|
100
|
+
18: 20,
|
|
101
|
+
47: 21
|
|
121
102
|
}
|
|
122
103
|
|
|
123
104
|
|
|
@@ -126,11 +107,10 @@ class C16RS(QLBaseDevice):
|
|
|
126
107
|
rlt = cls(parent.socket)
|
|
127
108
|
rlt.device_id = parent.device_id
|
|
128
109
|
rlt._device_no = parent.device_no
|
|
129
|
-
return rlt
|
|
130
|
-
|
|
110
|
+
return rlt
|
|
131
111
|
|
|
132
112
|
def init_edf_handler(self):
|
|
133
|
-
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
113
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution, record_duration = self._record_duration)
|
|
134
114
|
self._edf_handler.set_device_type(self.device_type)
|
|
135
115
|
self._edf_handler.set_device_no(self.device_no)
|
|
136
116
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
@@ -16,7 +16,7 @@ class DeviceFactory(object):
|
|
|
16
16
|
|
|
17
17
|
@classmethod
|
|
18
18
|
def create_device(cls, device: IDevice) -> Type[IDevice]:
|
|
19
|
-
logger.
|
|
19
|
+
logger.trace(f"Creating device for device type: {hex(device.device_type)}, support types: {cls._devices.keys()}")
|
|
20
20
|
if device.device_type not in cls._devices.keys():
|
|
21
21
|
logger.warning(f"不支持的设备类型: {hex(device.device_type)}")
|
|
22
22
|
raise ValueError(f"Unsupported device type: {hex(device.device_type)}")
|
qlsdk/rsc/interface/device.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
import abc
|
|
3
|
+
from typing import Literal
|
|
3
4
|
|
|
4
5
|
class IDevice(ABC):
|
|
5
6
|
|
|
@@ -11,11 +12,20 @@ class IDevice(ABC):
|
|
|
11
12
|
def set_device_type(self, value: int):
|
|
12
13
|
pass
|
|
13
14
|
|
|
15
|
+
def set_storage_path(self, path: str):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def set_file_prefix(self, pre: str):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
22
|
+
pass
|
|
23
|
+
|
|
14
24
|
@property
|
|
15
25
|
def device_no(self) -> str:
|
|
16
26
|
pass
|
|
17
27
|
|
|
18
|
-
def produce(self, data) -> None:
|
|
28
|
+
def produce(self, data, type:Literal['signal', 'impedance']="signal") -> None:
|
|
19
29
|
pass
|
|
20
30
|
|
|
21
31
|
def start_listening(self):
|
qlsdk/rsc/manager/container.py
CHANGED
|
@@ -50,7 +50,7 @@ class DeviceContainer(object):
|
|
|
50
50
|
try:
|
|
51
51
|
# 绑定到所有接口的19216端口
|
|
52
52
|
tcp_socket.bind(('0.0.0.0', self._tcp_port))
|
|
53
|
-
tcp_socket.listen(
|
|
53
|
+
tcp_socket.listen(100)
|
|
54
54
|
logger.info(f"端口[{self._tcp_port}]监听服务已启动")
|
|
55
55
|
|
|
56
56
|
while True:
|
|
@@ -59,16 +59,21 @@ class DeviceContainer(object):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
# 为每个新连接创建线程处理
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
try:
|
|
63
|
+
client_handler = Thread(
|
|
64
|
+
target=self.client_handler,
|
|
65
|
+
args=(client_socket,),
|
|
66
|
+
daemon=False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
client_handler.start()
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Handler Error: {e}")
|
|
69
72
|
|
|
70
73
|
except KeyboardInterrupt:
|
|
71
74
|
logger.error(f"端口[{self._tcp_port}]监听服务异常关闭")
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"端口[{self._tcp_port}]监听服务异常: {e}")
|
|
72
77
|
finally:
|
|
73
78
|
logger.error(f"端口[{self._tcp_port}]监听服务关闭")
|
|
74
79
|
tcp_socket.close()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import threading
|
|
3
|
+
import collections
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from qlsdk.rsc.interface import IDevice, IParser
|
|
8
|
+
from qlsdk.rsc.command import CommandFactory
|
|
9
|
+
|
|
10
|
+
# 解析状态机
|
|
11
|
+
class ParserState(Enum):
|
|
12
|
+
FIND_HEADER = 1
|
|
13
|
+
READ_LENGTH = 2
|
|
14
|
+
READ_BODY = 3
|
|
15
|
+
|
|
16
|
+
class TcpMessageParser(IParser):
|
|
17
|
+
HEADER = b'\x5a\xa5'
|
|
18
|
+
HEADER_LEN = 14
|
|
19
|
+
MAX_PKG_LEN = 1 * 1024 * 1024 # 1 MB
|
|
20
|
+
MAX_BUF_LEN = 100 * 1024 * 1024 # 10 MB
|
|
21
|
+
READ_CHUNK = 4096
|
|
22
|
+
|
|
23
|
+
def __init__(self, device: IDevice):
|
|
24
|
+
self.device = device
|
|
25
|
+
|
|
26
|
+
# 生产者-消费者缓冲区:线程安全 deque + bytearray
|
|
27
|
+
self._buf = bytearray()
|
|
28
|
+
self._lock = threading.Lock()
|
|
29
|
+
self._not_empty = threading.Condition(self._lock)
|
|
30
|
+
|
|
31
|
+
self._running = threading.Event()
|
|
32
|
+
|
|
33
|
+
# ---------- 生产者 ----------
|
|
34
|
+
def append(self, data: bytes) -> None:
|
|
35
|
+
logger.info(f"接收数据: {data.hex()}")
|
|
36
|
+
with self._not_empty:
|
|
37
|
+
if len(self._buf) + len(data) > self.MAX_BUF_LEN:
|
|
38
|
+
logger.warning("缓冲区超限,丢弃旧数据")
|
|
39
|
+
self._buf.clear()
|
|
40
|
+
self._buf.extend(data)
|
|
41
|
+
self._not_empty.notify()
|
|
42
|
+
|
|
43
|
+
# ---------- 消费者 ----------
|
|
44
|
+
def start(self) -> None:
|
|
45
|
+
self._running.set()
|
|
46
|
+
self._thread = threading.Thread(target=self._parser_loop, daemon=True)
|
|
47
|
+
self._thread.start()
|
|
48
|
+
|
|
49
|
+
def stop(self) -> None:
|
|
50
|
+
self._running.clear()
|
|
51
|
+
with self._not_empty:
|
|
52
|
+
self._not_empty.notify_all()
|
|
53
|
+
self._thread.join(timeout=1)
|
|
54
|
+
|
|
55
|
+
# ---------- 解析主循环 ----------
|
|
56
|
+
def _parser_loop(self) -> None:
|
|
57
|
+
"""状态机:找头 -> 读长度 -> 读体 -> 校验 -> 投递"""
|
|
58
|
+
state = ParserState.FIND_HEADER
|
|
59
|
+
need = len(self.HEADER)
|
|
60
|
+
header_pos = 0
|
|
61
|
+
pkg_len = 0
|
|
62
|
+
logger.info("数据解析开始")
|
|
63
|
+
|
|
64
|
+
while self._running.is_set():
|
|
65
|
+
with self._not_empty:
|
|
66
|
+
while len(self._buf) < need:
|
|
67
|
+
if not self._running.is_set():
|
|
68
|
+
return
|
|
69
|
+
self._not_empty.wait(timeout=0.1)
|
|
70
|
+
|
|
71
|
+
view = memoryview(self._buf) # 零拷贝视图
|
|
72
|
+
|
|
73
|
+
if state == ParserState.FIND_HEADER:
|
|
74
|
+
# idx = view.find(self.HEADER)
|
|
75
|
+
# if idx == -1:
|
|
76
|
+
# # 整段数据都没有头,全部丢弃
|
|
77
|
+
# del self._buf[:len(self._buf) - 1]
|
|
78
|
+
# continue
|
|
79
|
+
# # 去掉头部之前可能残留的脏数据
|
|
80
|
+
# del self._buf[:idx]
|
|
81
|
+
# state = 'READ_LENGTH'
|
|
82
|
+
# need = self.HEADER_LEN
|
|
83
|
+
# continue
|
|
84
|
+
with self._not_empty:
|
|
85
|
+
while len(view) < need:
|
|
86
|
+
if not self._running.is_set():
|
|
87
|
+
return
|
|
88
|
+
self._not_empty.wait(timeout=0.1)
|
|
89
|
+
|
|
90
|
+
# 1. 用 bytearray.find 找包头
|
|
91
|
+
idx = view.find(self.HEADER)
|
|
92
|
+
if idx == -1:
|
|
93
|
+
# 没有包头,保留最后 len(self.HEADER)-1 个字节即可
|
|
94
|
+
del self._buf[:-len(self.HEADER) + 1 or None]
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# 2. 去掉头部的脏数据
|
|
98
|
+
del self._buf[:idx]
|
|
99
|
+
state = 'READ_LENGTH'
|
|
100
|
+
need = self.HEADER_LEN
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if state == 'READ_LENGTH':
|
|
104
|
+
pkg_len = int.from_bytes(view[8:12], 'little')
|
|
105
|
+
if pkg_len < self.HEADER_LEN or pkg_len > self.MAX_PKG_LEN:
|
|
106
|
+
logger.warning(f"非法包长度 {pkg_len},丢弃")
|
|
107
|
+
del self._buf[:1] # 跳过当前头继续找下一个
|
|
108
|
+
state = 'FIND_HEADER'
|
|
109
|
+
need = len(self.HEADER)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
state = 'READ_BODY'
|
|
113
|
+
need = pkg_len
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if state == 'READ_BODY':
|
|
117
|
+
packet = bytes(view[:pkg_len]) # 拷贝一份完整包
|
|
118
|
+
del self._buf[:pkg_len] # 从缓冲区删除
|
|
119
|
+
self._dispatch(packet)
|
|
120
|
+
state = 'FIND_HEADER'
|
|
121
|
+
need = len(self.HEADER)
|
|
122
|
+
|
|
123
|
+
# ---------- 业务分发 ----------
|
|
124
|
+
def _dispatch(self, packet: bytes) -> None:
|
|
125
|
+
try:
|
|
126
|
+
cmd = int.from_bytes(packet[12:14], 'little')
|
|
127
|
+
cls = CommandFactory.create_command(cmd)
|
|
128
|
+
inst = cls(self.device)
|
|
129
|
+
inst.parse_body(packet[self.HEADER_LEN:-2]) # 去掉头尾
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
logger.exception(f"解析失败: {exc}")
|
|
132
|
+
|
|
133
|
+
# ---------- 工具 ----------
|
|
134
|
+
def set_device(self, device: IDevice) -> None:
|
|
135
|
+
self.device = device
|
qlsdk/rsc/parser/base.py
CHANGED
|
@@ -41,64 +41,67 @@ class TcpMessageParser(IParser):
|
|
|
41
41
|
# self.cache.write(buffer)
|
|
42
42
|
with self._lock:
|
|
43
43
|
self.cache.write(value)
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
def __parser__(self):
|
|
46
|
-
logger.
|
|
46
|
+
logger.info("数据解析开始")
|
|
47
47
|
|
|
48
48
|
# 告警阈值(10M)
|
|
49
49
|
warn_len = 10 * 1024 * 1024
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
try:
|
|
52
|
+
while self.running:
|
|
53
|
+
buf_len = get_len(self.buffer)
|
|
54
|
+
|
|
55
|
+
if buf_len < self.header_len:
|
|
56
|
+
logger.trace(f"操作区缓存数据不足: expect: {self.header_len}, actual: {buf_len}, 等待数据...")
|
|
57
|
+
if not self.__fill_from_cache():
|
|
58
|
+
time.sleep(0.1)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if buf_len > warn_len:
|
|
62
|
+
logger.warning(f"操作区缓存数据过大: {buf_len} bytes, 可能存在数据丢失风险")
|
|
63
|
+
|
|
64
|
+
start_pos = self.buffer.tell()
|
|
65
|
+
head = self.buffer.read(2)
|
|
66
|
+
if head != self.header:
|
|
67
|
+
logger.debug(f"数据包头部不匹配: {head.hex()}, 期望: {self.header.hex()},继续查找...")
|
|
68
|
+
self.buffer.seek(start_pos + 1) # 移动到下一个字节
|
|
59
69
|
continue
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
# 移动下标(指向包长度的位置)
|
|
72
|
+
self.buffer.seek(start_pos + 8)
|
|
73
|
+
# 包总长度
|
|
74
|
+
pkg_len = int.from_bytes(self.buffer.read(4), 'little')
|
|
75
|
+
buf_len = get_len(self.buffer)
|
|
63
76
|
|
|
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
|
-
self.buffer.seek(start_pos)
|
|
93
|
-
pkg = self.buffer.read(pkg_len)
|
|
94
|
-
|
|
95
|
-
# 清空操作区缓存(truncate会保留内存,重新初始化)
|
|
96
|
-
self.buffer = BytesIO()
|
|
97
|
-
if len(tmp) > 0:
|
|
98
|
-
self.buffer.write(tmp)
|
|
99
|
-
self.buffer.seek(0)
|
|
100
|
-
|
|
101
|
-
self.unpack(pkg)
|
|
77
|
+
# 直接等待长度足够(如果从头开始判断,因为逻辑相同,所以会执行一样的操作)
|
|
78
|
+
while buf_len < pkg_len:
|
|
79
|
+
logger.trace(f"操作区缓存数据不足: expect: {pkg_len}, actual: {buf_len}, 等待数据...")
|
|
80
|
+
if self.__fill_from_cache():
|
|
81
|
+
buf_len = get_len(self.buffer)
|
|
82
|
+
continue
|
|
83
|
+
else:
|
|
84
|
+
time.sleep(0.05)
|
|
85
|
+
|
|
86
|
+
# 读取剩余数据
|
|
87
|
+
self.buffer.seek(pkg_len)
|
|
88
|
+
tmp = self.buffer.read()
|
|
89
|
+
|
|
90
|
+
# 读取当前数据包
|
|
91
|
+
self.buffer.seek(start_pos)
|
|
92
|
+
pkg = self.buffer.read(pkg_len)
|
|
93
|
+
|
|
94
|
+
# 清空操作区缓存(truncate会保留内存,重新初始化)
|
|
95
|
+
self.buffer = BytesIO()
|
|
96
|
+
if len(tmp) > 0:
|
|
97
|
+
self.buffer.write(tmp)
|
|
98
|
+
self.buffer.seek(0)
|
|
99
|
+
|
|
100
|
+
self.unpack(pkg)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"数据解析异常: {e}")
|
|
103
|
+
|
|
104
|
+
logger.info(f"数据解析结束:{self.running}")
|
|
102
105
|
|
|
103
106
|
# 填充操作区缓存
|
|
104
107
|
def __fill_from_cache(self) -> bool:
|
|
@@ -107,13 +110,18 @@ class TcpMessageParser(IParser):
|
|
|
107
110
|
cur_pos = self.buffer.tell()
|
|
108
111
|
# 移动到操作区缓存末尾,内容追加到缓冲区尾部
|
|
109
112
|
self.buffer.seek(0,2)
|
|
113
|
+
len = self.buffer.tell()
|
|
110
114
|
# 操作缓冲区
|
|
111
|
-
with self._lock:
|
|
112
|
-
self.cache
|
|
115
|
+
with self._lock:
|
|
116
|
+
cache_len = get_len(self.cache)
|
|
113
117
|
|
|
114
118
|
# 临时缓冲区只要有数据,就写入操作缓冲区(避免分片传输导致数据不完整)
|
|
115
|
-
if
|
|
116
|
-
|
|
119
|
+
if cache_len > 0:
|
|
120
|
+
if len == 0:
|
|
121
|
+
self.buffer = self.cache
|
|
122
|
+
cur_pos = 0
|
|
123
|
+
else:
|
|
124
|
+
self.buffer.write(self.cache.getvalue())
|
|
117
125
|
self.cache = BytesIO() # 清空缓冲区
|
|
118
126
|
result = True
|
|
119
127
|
|
|
@@ -125,12 +133,8 @@ class TcpMessageParser(IParser):
|
|
|
125
133
|
# 提取指令码
|
|
126
134
|
cmd_code = int.from_bytes(packet[self.cmd_pos : self.cmd_pos + 2], 'little')
|
|
127
135
|
cmd_class = CommandFactory.create_command(cmd_code)
|
|
128
|
-
# logger.trace(f"收到指令:{cmd_class.cmd_desc}[{hex(cmd_code)}]")
|
|
129
136
|
instance = cmd_class(self.device)
|
|
130
|
-
start = time_ns()
|
|
131
|
-
# logger.trace(f"开始解析: {start}")
|
|
132
137
|
instance.parse_body(packet[self.header_len:-2])
|
|
133
|
-
# logger.trace(f"解析完成:{time_ns()}, 解析耗时:{time_ns() - start}ns")
|
|
134
138
|
return instance
|
|
135
139
|
|
|
136
140
|
def start(self):
|
|
@@ -138,6 +142,9 @@ class TcpMessageParser(IParser):
|
|
|
138
142
|
parser = Thread(target=self.__parser__, daemon=True)
|
|
139
143
|
parser.start()
|
|
140
144
|
|
|
145
|
+
def stop(self):
|
|
146
|
+
self.running = False
|
|
147
|
+
|
|
141
148
|
# 工具方法
|
|
142
149
|
def get_len(buf: BytesIO) -> int:
|
|
143
150
|
if buf is None:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: qlsdk2
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: SDK for quanlan device
|
|
5
|
+
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
|
+
Author: hehuajun
|
|
7
|
+
Author-email: hehuajun@eegion.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: loguru >=0.6.0
|
|
14
|
+
Requires-Dist: numpy >=1.23.5
|
|
15
|
+
Requires-Dist: bitarray >=1.5.3
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest >=6.0 ; extra == 'dev'
|
|
18
|
+
Requires-Dist: twine >=3.0 ; extra == 'dev'
|
|
19
|
+
|
|
20
|
+
# 版本 v0.5.1 (2025-08-04)
|
|
21
|
+
|
|
22
|
+
## ⚙️ 优化
|
|
23
|
+
#### 修复C16R通道映射的问题
|
|
24
|
+
|
|
25
|
+
#### 支持设置记录参数,在需要保存大量事件时使用
|
|
26
|
+
|
|
27
|
+
#### 订阅队列满的时候,丢弃最早的数据
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# 版本 v0.5.0 (2025-07-29)
|
|
31
|
+
|
|
32
|
+
## 🚀 新特性
|
|
33
|
+
|
|
34
|
+
1. **C16R设备连接**
|
|
35
|
+
支持C16R类型设备的搜索与连接
|
|
36
|
+
|
|
37
|
+
2. **C16R信号采集/停止控制**
|
|
38
|
+
支持信号采集的参数配置
|
|
39
|
+
支持信号采集的启动与停止控制
|
|
40
|
+
|
|
41
|
+
3. **C16R数据自动记录**
|
|
42
|
+
采集到的信号数据自动保存为bdf文件
|
|
43
|
+
|
|
44
|
+
4. **C16R采集通道设置**
|
|
45
|
+
- 支持数字模式通道配置
|
|
46
|
+
- 支持名称模式通道配置
|
|
47
|
+
- 支持两种模式混合使用
|
|
48
|
+
|
|
49
|
+
## ⚙️ 优化
|
|
50
|
+
|
|
51
|
+
1. **性能提升**
|
|
52
|
+
- 信号接收效率优化
|
|
53
|
+
- 指令拆包方式优化
|
|
54
|
+
|
|
55
|
+
2. **日志系统改进**
|
|
56
|
+
- 日志级别精细化调整,减少不必要的日志信息
|
|
57
|
+
- 日志文案清晰化
|
|
@@ -8,7 +8,7 @@ qlsdk/core/local.py,sha256=vbison4XZtS4SNYLJ9CqBhetEcukdviTWmvtdA1efkQ,811
|
|
|
8
8
|
qlsdk/core/utils.py,sha256=yfCiLpufO96I68MLs6Drc6IECRjcQ-If8sXn7RaRHrk,4241
|
|
9
9
|
qlsdk/core/crc/__init__.py,sha256=kaYSr6KN5g4U49xlxAvT2lnEeGtwX4Dz1ArwKDvUIIY,143
|
|
10
10
|
qlsdk/core/crc/crctools.py,sha256=sDeE6CMccQX2cRAyMQK0SZUk1fa50XMuwqXau5UX5C8,4242
|
|
11
|
-
qlsdk/core/entity/__init__.py,sha256=
|
|
11
|
+
qlsdk/core/entity/__init__.py,sha256=1BBbL41zqb0_UsxgoiuyG5zM0CKVzT5VUA_BXSKZmAs,3177
|
|
12
12
|
qlsdk/core/filter/__init__.py,sha256=YIWIzDUKN30mq2JTr53ZGblggZfC_rLUp2FSRrsQFgU,36
|
|
13
13
|
qlsdk/core/filter/norch.py,sha256=5RdIBX5eqs5w5nmVAnCB3ESSuAT_vVBZ2g-dg6HMZdY,1858
|
|
14
14
|
qlsdk/core/message/__init__.py,sha256=sHuavOyHf4bhH6VdDpTA1EsCh7Q-XsPHcFiItpVz3Rs,51
|
|
@@ -20,7 +20,7 @@ qlsdk/core/network/monitor.py,sha256=QqjjPwSr1kgqDTTySp5bpalZmsBQTaAWSxrfPLdROZo
|
|
|
20
20
|
qlsdk/persist/__init__.py,sha256=b8qk1aOU6snEMCQNYDl1ijV3-2gwBmMt76fiAzNk1E8,107
|
|
21
21
|
qlsdk/persist/ars_edf.py,sha256=_pYtHqucB-utMw-xUXZc9IB8_8ThbLFpTl_-WBQR-Sc,10555
|
|
22
22
|
qlsdk/persist/edf.py,sha256=ETngb86CfkIUJYWmw86QR445MvTFC7Edk_CH9nyNgtY,7857
|
|
23
|
-
qlsdk/persist/rsc_edf.py,sha256=
|
|
23
|
+
qlsdk/persist/rsc_edf.py,sha256=89Wv7r6u_J4fHlGKsIBAUYD7SOdfqFRxbwV2asvutH8,13315
|
|
24
24
|
qlsdk/persist/stream.py,sha256=TCVF1sqDrHiYBsJC27At66AaCs-_blXeXA_WXdJiIVA,5828
|
|
25
25
|
qlsdk/rsc/__init__.py,sha256=hOMiN0eYn4jYo7O4_0IPlQT0hD15SqqCQUihOVlTZvs,269
|
|
26
26
|
qlsdk/rsc/device_manager.py,sha256=1ucd-lzHkNeQPKPzXV6OBkAMqPp_vOcsLyS-9TJ7wRc,4448
|
|
@@ -29,28 +29,29 @@ qlsdk/rsc/eegion.py,sha256=lxrktO-3Z_MYdFIwc4NxvgLM5AL5kU3UItjH6tsKmHY,11670
|
|
|
29
29
|
qlsdk/rsc/entity.py,sha256=-fRWFkVWp9d8Y1uh6GiacXC5scdeEKNiNFf3aziGdCE,17751
|
|
30
30
|
qlsdk/rsc/paradigm.py,sha256=DGfwY36sMdPIMRjbGo661GvUTEwsRRi3jrmG405mSTk,12840
|
|
31
31
|
qlsdk/rsc/proxy.py,sha256=9CPdGNGWremwBUh4GvlXAykYB-x_BEPPLqsNvwuwIDE,2736
|
|
32
|
-
qlsdk/rsc/command/__init__.py,sha256=
|
|
32
|
+
qlsdk/rsc/command/__init__.py,sha256=2rqWM23fFqvFfgN7K3PLT7rkTyW9mawK4OGCo63hnJg,12292
|
|
33
33
|
qlsdk/rsc/command/message.py,sha256=nTdG-Vp4MBnltyrgedAWiKD6kzOaPrg58Z_hq6yjhys,12220
|
|
34
34
|
qlsdk/rsc/device/__init__.py,sha256=xtTXLT9QFKtb-qS-A8-ewSxJ3zXgImFCX0OoAPw6hHE,185
|
|
35
35
|
qlsdk/rsc/device/arskindling.py,sha256=owci6MEGjyWqohEXzPdKj_ESeVIZKgO53StVj6Tmi18,15002
|
|
36
|
-
qlsdk/rsc/device/base.py,sha256=
|
|
37
|
-
qlsdk/rsc/device/c16_rs.py,sha256=
|
|
36
|
+
qlsdk/rsc/device/base.py,sha256=osLToxNrb745OzCpgXZU5AH72emMh4OxATeqy4fCxsg,17856
|
|
37
|
+
qlsdk/rsc/device/c16_rs.py,sha256=BHQRHOnsTMAKgqSXaAS2RjPIklZQAl2CVfe6i_iX-i4,5928
|
|
38
38
|
qlsdk/rsc/device/c256_rs.py,sha256=K1XmLqZpvHTAfCm_dr2VsGxHc67aJQVDV1cI41a1WTI,13955
|
|
39
39
|
qlsdk/rsc/device/c64_rs.py,sha256=cZIioIRGgd4Ub0ieho4_XujBNo8AQgJEjXcqgcEkyFQ,13644
|
|
40
40
|
qlsdk/rsc/device/c64s1.py,sha256=L7nKmsoMCGj6GMjHYfYkKgkBtrGfP516kQHQ5I1FAUE,13986
|
|
41
|
-
qlsdk/rsc/device/device_factory.py,sha256=
|
|
41
|
+
qlsdk/rsc/device/device_factory.py,sha256=AL_dtjx6ThcyWTHxGSrLKjEDaCt1Y9gClK4HQ5FGjFI,1315
|
|
42
42
|
qlsdk/rsc/interface/__init__.py,sha256=xeRzIlQSB7ZSf4r5kLfH5cDQLzCyWeJAReG8Xq5nOE0,70
|
|
43
43
|
qlsdk/rsc/interface/command.py,sha256=1s5Lxb_ejsd-JNvKMqU2aFSnOoW-_cx01VSD3czxmQI,199
|
|
44
|
-
qlsdk/rsc/interface/device.py,sha256=
|
|
44
|
+
qlsdk/rsc/interface/device.py,sha256=VEV-Ige8tjvASdddP6SPRolTqPuIuHrIZP8wiX-Fhu8,3391
|
|
45
45
|
qlsdk/rsc/interface/handler.py,sha256=ADDe_a2RAxGMuooLyivH0JBPTGBcFP2JaTVX41R1A4w,198
|
|
46
46
|
qlsdk/rsc/interface/parser.py,sha256=DxuFZiprJJbG4pfFbbZPaG8MlBiBRe0S0lJrvc2Iees,251
|
|
47
47
|
qlsdk/rsc/manager/__init__.py,sha256=4ljT3mR8YPBDQur46B5xPqK5tjLKlsWfgCJVuA0gs-8,40
|
|
48
|
-
qlsdk/rsc/manager/container.py,sha256=
|
|
48
|
+
qlsdk/rsc/manager/container.py,sha256=mowoFJNVDSEhqsz-EDzPVDcMRiuu_oakdGLZbJrPvlM,5071
|
|
49
49
|
qlsdk/rsc/manager/search.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
50
|
qlsdk/rsc/network/__init__.py,sha256=PfYiqXS2pZV__uegQ1TjaeYhY1pefZ_shwE_X5HNVbs,23
|
|
51
51
|
qlsdk/rsc/network/discover.py,sha256=4aojzRFInTC3d8K2TYGbnP1Ji5fOFEi31ekghj7ce5k,2977
|
|
52
52
|
qlsdk/rsc/parser/__init__.py,sha256=8RgwbKCINu3eTsxVLF9cMoBXJnVrDocOEFP6NGP_atk,34
|
|
53
|
-
qlsdk/rsc/parser/base.py,sha256=
|
|
53
|
+
qlsdk/rsc/parser/base-new.py,sha256=cAOy1V_1fAJyGq7bm7uLxpW41DbkllWOprnfWKpjtsQ,5116
|
|
54
|
+
qlsdk/rsc/parser/base.py,sha256=Cqel02BA_AH-deSmzTyUvqecIGoYVBre5UuFlG1eNGA,5728
|
|
54
55
|
qlsdk/sdk/__init__.py,sha256=v9LKP-5qXCqnAsCkiRE9LDb5Tagvl_Qd_fqrw7y9yd4,68
|
|
55
56
|
qlsdk/sdk/ar4sdk.py,sha256=tugH3UUeNebdka78AzLyrtAXbYQQE3iFJ227zUit6tY,27261
|
|
56
57
|
qlsdk/sdk/hub.py,sha256=uEOGZBZtMDCWlV8G2TZe6FAo6eTPcwHAW8zdqr1eq_0,1571
|
|
@@ -58,7 +59,7 @@ qlsdk/sdk/libs/libAr4SDK.dll,sha256=kZp9_DRwPdAJ5OgTFQSqS8tEETxUs7YmmETuBP2g60U,
|
|
|
58
59
|
qlsdk/sdk/libs/libwinpthread-1.dll,sha256=W77ySaDQDi0yxpnQu-ifcU6-uHKzmQpcvsyx2J9j5eg,52224
|
|
59
60
|
qlsdk/x8/__init__.py,sha256=FDpDK7GAYL-g3vzfU9U_V03QzoYoxH9YLm93PjMlANg,4870
|
|
60
61
|
qlsdk/x8m/__init__.py,sha256=cLeUqEEj65qXw4Qa4REyxoLh6T24anSqPaKe9_lR340,634
|
|
61
|
-
qlsdk2-0.5.
|
|
62
|
-
qlsdk2-0.5.
|
|
63
|
-
qlsdk2-0.5.
|
|
64
|
-
qlsdk2-0.5.
|
|
62
|
+
qlsdk2-0.5.1.dist-info/METADATA,sha256=vJ3DPDoa8rM-m9YdziuNsMNlnx5o21C570W8uZ7FtZU,1572
|
|
63
|
+
qlsdk2-0.5.1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
|
64
|
+
qlsdk2-0.5.1.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
|
|
65
|
+
qlsdk2-0.5.1.dist-info/RECORD,,
|
qlsdk2-0.5.0.dist-info/METADATA
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: qlsdk2
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: SDK for quanlan device
|
|
5
|
-
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
|
-
Author: hehuajun
|
|
7
|
-
Author-email: hehuajun@eegion.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: loguru>=0.6.0
|
|
14
|
-
Requires-Dist: numpy>=1.23.5
|
|
15
|
-
Requires-Dist: bitarray>=1.5.3
|
|
16
|
-
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
18
|
-
Requires-Dist: twine>=3.0; extra == "dev"
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: author-email
|
|
21
|
-
Dynamic: classifier
|
|
22
|
-
Dynamic: description
|
|
23
|
-
Dynamic: description-content-type
|
|
24
|
-
Dynamic: home-page
|
|
25
|
-
Dynamic: requires-dist
|
|
26
|
-
Dynamic: requires-python
|
|
27
|
-
Dynamic: summary
|
|
28
|
-
|
|
29
|
-
版本:v0.5.0
|
|
30
|
-
时间:2025-07-29
|
|
31
|
-
[新特性]
|
|
32
|
-
1. C16R设备搜索
|
|
33
|
-
2、C16R设备连接
|
|
34
|
-
3、C16R信号采集/停止
|
|
35
|
-
4、C16R数据自动记录到文件
|
|
36
|
-
5、C16R采集通道设置支持数字和名称两种模式(可混用)
|
|
37
|
-
|
|
38
|
-
[优化]
|
|
39
|
-
1、提升信号接收及指令解析性能
|
|
40
|
-
2、日志级别及文案优化
|
|
File without changes
|