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.
@@ -1,18 +1,17 @@
1
1
  from qlsdk.core.utils import to_channels
2
2
  from loguru import logger
3
3
 
4
- class DataPacket(object):
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(object):
67
+ class ImpedancePacket(Packet):
69
68
  def __init__(self):
70
- self.time_stamp = None
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
- self._writer.writeAnnotation(onset, 1, desc)
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
- onset = datetime.now().timestamp() - self._start_time.timestamp()
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)
@@ -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.debug(f"电量更新: {self.device}")
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:
@@ -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": 2,
77
- "2": 3,
78
- "3": 27,
79
- "4": 31,
80
- "5": 60,
81
- "6": 61,
82
- "7": 25,
83
- "8": 29,
84
- "9": 33,
85
- "10": 62,
86
- "11": 63,
87
- "12": 10,
88
- "13": 14,
89
- "14": 8,
90
- "15": 12,
91
- "16": 16,
92
- "17": 43,
93
- "18": 45,
94
- "19": 47,
95
- "20": 49,
96
- "21": 51
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
- 2: 1,
101
- 3: 2,
102
- 27: 3,
103
- 31: 4,
104
- 60: 5,
105
- 61: 6,
106
- 25: 7,
107
- 29: 8,
108
- 33: 9,
109
- 62: 10,
110
- 63: 11,
111
- 10: 12,
112
- 14: 13,
113
- 8: 14,
114
- 12: 15,
115
- 16: 16,
116
- 43: 17,
117
- 45: 18,
118
- 47: 19,
119
- 49: 20,
120
- 51: 21
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.debug(f"Creating device for device type: {hex(device.device_type)}, support types: {cls._devices.keys()}")
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)}")
@@ -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):
@@ -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(5)
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
- client_handler = Thread(
63
- target=self.client_handler,
64
- args=(client_socket,),
65
- daemon=True
66
- )
67
-
68
- client_handler.start()
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.trace("数据解析开始")
46
+ logger.info("数据解析开始")
47
47
 
48
48
  # 告警阈值(10M)
49
49
  warn_len = 10 * 1024 * 1024
50
50
 
51
- while self.running:
52
- buf_len = get_len(self.buffer)
53
-
54
- # logger.info(f"当前操作区缓存长度: {buf_len}, 缓存内容: {self.buffer.getvalue().hex()}")
55
- if buf_len < self.header_len:
56
- # logger.trace(f"操作区缓存数据不足: {len}, 等待数据...")
57
- if not self.__fill_from_cache():
58
- time.sleep(0.05)
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
- if buf_len > warn_len:
62
- logger.warning(f"操作区缓存数据过大: {buf_len} bytes, 可能存在数据丢失风险")
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
- start_pos = self.buffer.tell()
65
- # logger.info(f"当前缓存位置: {start_pos}")
66
- head = self.buffer.read(2)
67
- # logger.info(f'当前缓存头部: {head.hex()}')
68
- if head != self.header:
69
- logger.debug(f"数据包头部不匹配: {head.hex()}, 期望: {self.header.hex()},继续查找...")
70
- self.buffer.seek(start_pos + 1) # 移动到下一个字节
71
- continue
72
-
73
- # 移动下标(指向包长度的位置)
74
- self.buffer.seek(start_pos + 8)
75
- # 包总长度
76
- pkg_len = int.from_bytes(self.buffer.read(4), 'little')
77
- # logger.trace(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
78
-
79
- buf_len = get_len(self.buffer)
80
- # 直接等待长度足够(如果从头开始判断,因为逻辑相同,所以会执行一样的操作)
81
- while buf_len < pkg_len:
82
- if self.__fill_from_cache():
83
- continue
84
- else:
85
- time.sleep(0.05)
86
-
87
- # 读取剩余数据
88
- self.buffer.seek(pkg_len)
89
- tmp = self.buffer.read()
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.seek(0, 2)
115
+ with self._lock:
116
+ cache_len = get_len(self.cache)
113
117
 
114
118
  # 临时缓冲区只要有数据,就写入操作缓冲区(避免分片传输导致数据不完整)
115
- if self.cache.tell() > 0:
116
- self.buffer.write(self.cache.getvalue())
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=OHedug2XusjSuxQ479bcX5IVq35FTFWEfYriFT3rEzY,3339
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=wV3akdwzEihDAyR9DtmO0jNdLH1jEvx34ghJQtrfk2k,12578
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=FpumdCRV3aSnCvLT7MceMb7lF8WgGbdi_w0L8wX7ryg,12137
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=6cMaACVVgZ7oKdqbyVRl3B32M9Em6_jjr-FUuxRW6Ys,17404
37
- qlsdk/rsc/device/c16_rs.py,sha256=IpJn4hBFHg67lvWkl8kAAztdZYsgi2-njFuxMi2SsHQ,6390
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=P8nNDB2qk0kbu4OMYtEZMKSdXWp-7fLDzuNyR1Thf8Q,1315
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=apBQAeu1g0Qmw73qQqr6uG1re9qCep6oKfjWKlGJdp4,3092
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=N9QB85FOA_7Oa_8M1y1M2UODA_tpS9JQONhGj8ObBG8,4811
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=s6tkWQXoMq8ZA4Nns_aG1JS5PV3UXLMFarTZiCoPagM,5504
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.0.dist-info/METADATA,sha256=Lp7nByzF1dCM1LGXQxyKv8sc-vkTDuLjZtw9nu_JFBk,1134
62
- qlsdk2-0.5.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
63
- qlsdk2-0.5.0.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
64
- qlsdk2-0.5.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (70.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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、日志级别及文案优化