qlsdk2 0.4.2__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 +42 -13
- qlsdk/rsc/command/__init__.py +32 -24
- qlsdk/rsc/device/__init__.py +6 -1
- qlsdk/rsc/device/arskindling.py +4 -2
- qlsdk/rsc/device/base.py +92 -39
- qlsdk/rsc/device/c16_rs.py +185 -0
- qlsdk/rsc/device/c256_rs.py +1 -0
- qlsdk/rsc/device/c64_rs.py +3 -2
- qlsdk/rsc/device/c64s1.py +3 -2
- qlsdk/rsc/device/device_factory.py +2 -1
- qlsdk/rsc/interface/device.py +13 -0
- qlsdk/rsc/manager/container.py +27 -23
- qlsdk/rsc/network/discover.py +25 -36
- qlsdk/rsc/parser/base-new.py +135 -0
- qlsdk/rsc/parser/base.py +114 -26
- qlsdk2-0.5.1.dist-info/METADATA +57 -0
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.1.dist-info}/RECORD +20 -18
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.1.dist-info}/WHEEL +1 -1
- qlsdk2-0.4.2.dist-info/METADATA +0 -121
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
|
|
2
|
+
from multiprocessing import Queue
|
|
3
|
+
from threading import Thread
|
|
4
|
+
from time import sleep, time_ns
|
|
5
|
+
from typing import Any, Dict, Literal
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from qlsdk.persist import RscEDFHandler
|
|
9
|
+
from qlsdk.rsc.device.device_factory import DeviceFactory
|
|
10
|
+
from qlsdk.rsc.interface import IDevice
|
|
11
|
+
from qlsdk.rsc.command import *
|
|
12
|
+
from qlsdk.rsc.device.base import QLBaseDevice
|
|
13
|
+
|
|
14
|
+
'''
|
|
15
|
+
C16RS设备类,继承自QLBaseDevice
|
|
16
|
+
提供设备特定的属性和方法,包括设备类型、采集参数设置、电刺激参
|
|
17
|
+
'''
|
|
18
|
+
class C16RS(QLBaseDevice):
|
|
19
|
+
|
|
20
|
+
device_type = 0x339 # C16RS设备类型标识符
|
|
21
|
+
|
|
22
|
+
def __init__(self, socket):
|
|
23
|
+
|
|
24
|
+
super().__init__(socket)
|
|
25
|
+
# 存储通道反向映射位置值
|
|
26
|
+
self._reverse_ch_pos = None
|
|
27
|
+
|
|
28
|
+
self.channel_name_mapping = {
|
|
29
|
+
"FP1": 1,
|
|
30
|
+
"FP2": 2,
|
|
31
|
+
"C3": 3,
|
|
32
|
+
"C4": 4,
|
|
33
|
+
"O1": 5,
|
|
34
|
+
"O2": 6,
|
|
35
|
+
"CZ": 7,
|
|
36
|
+
"T7": 8,
|
|
37
|
+
"T8": 9,
|
|
38
|
+
"M1": 10,
|
|
39
|
+
"M2": 11,
|
|
40
|
+
"F3": 12,
|
|
41
|
+
"F4": 13,
|
|
42
|
+
"FZ": 14,
|
|
43
|
+
"F7": 15,
|
|
44
|
+
"F8": 16,
|
|
45
|
+
"PZ": 17,
|
|
46
|
+
"P3": 18,
|
|
47
|
+
"P7": 19,
|
|
48
|
+
"P4": 20,
|
|
49
|
+
"P8": 21,
|
|
50
|
+
"T3": 8,
|
|
51
|
+
"T4": 9,
|
|
52
|
+
"A1": 10,
|
|
53
|
+
"A2": 11,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
self.channel_mapping = {
|
|
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
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
self.channel_display_mapping = {
|
|
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
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_parent(cls, parent:IDevice) -> IDevice:
|
|
107
|
+
rlt = cls(parent.socket)
|
|
108
|
+
rlt.device_id = parent.device_id
|
|
109
|
+
rlt._device_no = parent.device_no
|
|
110
|
+
return rlt
|
|
111
|
+
|
|
112
|
+
def init_edf_handler(self):
|
|
113
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution, record_duration = self._record_duration)
|
|
114
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
115
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
116
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
117
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C16R')
|
|
118
|
+
|
|
119
|
+
# 设置刺激参数
|
|
120
|
+
def set_stim_param(self, param):
|
|
121
|
+
self.stim_paradigm = param
|
|
122
|
+
|
|
123
|
+
# 设置采集参数
|
|
124
|
+
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
125
|
+
# 保存原始通道参数
|
|
126
|
+
self._acq_param["original_channels"] = channels
|
|
127
|
+
|
|
128
|
+
# 名称转换为数字通道
|
|
129
|
+
channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
130
|
+
|
|
131
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
132
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
133
|
+
channels = [self.channel_mapping.get(str(i), -1) for i in channels]
|
|
134
|
+
channels = [i if i != -1 else channels[0] for i in channels]
|
|
135
|
+
|
|
136
|
+
# 更新采集参数
|
|
137
|
+
self._acq_param["channels"] = channels
|
|
138
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
139
|
+
self._acq_param["sample_range"] = sample_range
|
|
140
|
+
self._acq_channels = channels
|
|
141
|
+
self._sample_rate = sample_rate
|
|
142
|
+
self._sample_range = sample_range
|
|
143
|
+
|
|
144
|
+
logger.debug(f"C16RS: set_acq_param: {self._acq_param}")
|
|
145
|
+
|
|
146
|
+
# 参数改变后,重置通道位置映射
|
|
147
|
+
self._reverse_ch_pos = None
|
|
148
|
+
|
|
149
|
+
# 信号数据转换(默认不处理)
|
|
150
|
+
def _signal_wrapper(self, data: RscPacket):
|
|
151
|
+
if data is None:
|
|
152
|
+
return
|
|
153
|
+
# 根据映射关系做通道转换-(注意数据和通道的一致性)
|
|
154
|
+
# data.channels = [self.channel_display_mapping.get(i, i) for i in data.channels]
|
|
155
|
+
|
|
156
|
+
# 升级为类变量,减少计算
|
|
157
|
+
if self._reverse_ch_pos is None:
|
|
158
|
+
self._reverse_ch_pos = map_indices(self._acq_param["channels"], data.channels)
|
|
159
|
+
|
|
160
|
+
# 更新通道(数据)顺序和输入一致
|
|
161
|
+
data.channels = self._acq_param["original_channels"]
|
|
162
|
+
data.eeg = [data.eeg[i] for i in self._reverse_ch_pos]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def map_indices(A, B):
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
参数:
|
|
169
|
+
A: 源数组(无重复值)
|
|
170
|
+
B: 目标数组(无重复值)
|
|
171
|
+
|
|
172
|
+
返回:
|
|
173
|
+
C: 与A长度相同的数组,元素为A中对应值在B中的索引(不存在则为-1)
|
|
174
|
+
"""
|
|
175
|
+
# 创建B的值到索引的映射字典(O(n)操作)
|
|
176
|
+
b_map = {value: idx for idx, value in enumerate(B)}
|
|
177
|
+
|
|
178
|
+
# 遍历A,获取每个元素在B中的位置(O(m)操作)
|
|
179
|
+
return [b_map.get(a, -1) for a in A]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Register the C16RS device with the DeviceFactory
|
|
183
|
+
DeviceFactory.register(C16RS.device_type, C16RS)
|
|
184
|
+
|
|
185
|
+
|
qlsdk/rsc/device/c256_rs.py
CHANGED
qlsdk/rsc/device/c64_rs.py
CHANGED
|
@@ -16,6 +16,7 @@ class C64RS(QLBaseDevice):
|
|
|
16
16
|
device_type = 0x39 # C64RS设备类型标识符
|
|
17
17
|
|
|
18
18
|
def __init__(self, socket):
|
|
19
|
+
super().__init__(socket)
|
|
19
20
|
self.socket = socket
|
|
20
21
|
|
|
21
22
|
self._id = None
|
|
@@ -145,9 +146,9 @@ class C64RS(QLBaseDevice):
|
|
|
145
146
|
def init_edf_handler(self):
|
|
146
147
|
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
147
148
|
self._edf_handler.set_device_type(self.device_type)
|
|
148
|
-
self._edf_handler.set_device_no(self.
|
|
149
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
149
150
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
150
|
-
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
151
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C64RS')
|
|
151
152
|
|
|
152
153
|
@property
|
|
153
154
|
def edf_handler(self):
|
qlsdk/rsc/device/c64s1.py
CHANGED
|
@@ -16,6 +16,7 @@ class C64S1(QLBaseDevice):
|
|
|
16
16
|
device_type = 0x40 # C64RS设备类型标识符
|
|
17
17
|
|
|
18
18
|
def __init__(self, socket):
|
|
19
|
+
super().__init__(socket)
|
|
19
20
|
self.socket = socket
|
|
20
21
|
|
|
21
22
|
self._id = None
|
|
@@ -151,9 +152,9 @@ class C64S1(QLBaseDevice):
|
|
|
151
152
|
def init_edf_handler(self):
|
|
152
153
|
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
153
154
|
self._edf_handler.set_device_type(self.device_type)
|
|
154
|
-
self._edf_handler.set_device_no(self.
|
|
155
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
155
156
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
156
|
-
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
157
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C64S1')
|
|
157
158
|
|
|
158
159
|
@property
|
|
159
160
|
def edf_handler(self):
|
|
@@ -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)}")
|
|
@@ -26,6 +26,7 @@ class DeviceFactory(object):
|
|
|
26
26
|
|
|
27
27
|
# Register the C64RS device with the DeviceFactory
|
|
28
28
|
DeviceFactory.register(C64RS.device_type, C64RS)
|
|
29
|
+
# Register the ARSKindling device with the DeviceFactory
|
|
29
30
|
DeviceFactory.register(ARSKindling.device_type, ARSKindling)
|
|
30
31
|
|
|
31
32
|
|
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,10 +12,22 @@ 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
|
|
|
28
|
+
def produce(self, data, type:Literal['signal', 'impedance']="signal") -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
18
31
|
def start_listening(self):
|
|
19
32
|
pass
|
|
20
33
|
|
qlsdk/rsc/manager/container.py
CHANGED
|
@@ -33,15 +33,15 @@ class DeviceContainer(object):
|
|
|
33
33
|
等待设备连接
|
|
34
34
|
'''
|
|
35
35
|
def connect(self, device_id: str, timeout:int=30) -> Optional[IDevice]:
|
|
36
|
-
logger.info(f"
|
|
36
|
+
logger.info(f"开始搜索设备: {device_id}")
|
|
37
37
|
self.add_search(device_id)
|
|
38
38
|
for s in range(timeout):
|
|
39
39
|
device = self.get_device(device_id)
|
|
40
40
|
if device:
|
|
41
|
-
logger.success(f"
|
|
41
|
+
logger.success(f"设备[{device_id}]已连接成功。")
|
|
42
42
|
return device
|
|
43
43
|
time.sleep(1)
|
|
44
|
-
logger.
|
|
44
|
+
logger.warning(f"在{timeout}内未搜索到设备:{device_id}")
|
|
45
45
|
return None
|
|
46
46
|
|
|
47
47
|
def _listening(self):
|
|
@@ -50,24 +50,30 @@ 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:
|
|
57
57
|
client_socket, addr = tcp_socket.accept()
|
|
58
|
-
logger.info(f"接收到来自
|
|
58
|
+
logger.info(f"接收到来自[{addr[0]}:{addr[1]}]的连接,待确认设备类型...")
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
# 为每个新连接创建线程处理
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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}")
|
|
68
72
|
|
|
69
73
|
except KeyboardInterrupt:
|
|
70
74
|
logger.error(f"端口[{self._tcp_port}]监听服务异常关闭")
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"端口[{self._tcp_port}]监听服务异常: {e}")
|
|
71
77
|
finally:
|
|
72
78
|
logger.error(f"端口[{self._tcp_port}]监听服务关闭")
|
|
73
79
|
tcp_socket.close()
|
|
@@ -84,18 +90,16 @@ class DeviceContainer(object):
|
|
|
84
90
|
device.start_listening()
|
|
85
91
|
# GET_DEVICE_INFO
|
|
86
92
|
msg = GetDeviceInfoCommand.build(device).pack()
|
|
87
|
-
logger.
|
|
93
|
+
logger.trace(f"发送获取设备信息命令: {msg.hex()}")
|
|
88
94
|
device.send(msg)
|
|
89
|
-
logger.info(f"base device {dir(device)}")
|
|
90
95
|
# 添加设备
|
|
91
96
|
while True:
|
|
92
97
|
if device.device_name:
|
|
93
|
-
logger.info(f"设备 {device.device_name} 已连接")
|
|
94
98
|
real_device = DeviceFactory.create_device(device)
|
|
95
99
|
device.stop_listening()
|
|
96
100
|
real_device.start_listening()
|
|
97
101
|
self.add_device(real_device)
|
|
98
|
-
logger.info(f"
|
|
102
|
+
logger.info(f"设备[{device.device_name}]已连接,设备类型为:{hex(real_device.device_type)}")
|
|
99
103
|
break
|
|
100
104
|
|
|
101
105
|
|
|
@@ -107,20 +111,20 @@ class DeviceContainer(object):
|
|
|
107
111
|
if device is None or device.device_no is None:
|
|
108
112
|
logger.warning("无效的设备")
|
|
109
113
|
|
|
110
|
-
self._devices
|
|
111
|
-
logger.
|
|
114
|
+
self._devices[device.device_no] = device
|
|
115
|
+
logger.info(f"添加设备[{device.device_no}]到设备列表,已连接设备数量:{len(self._devices)}")
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
# 标记设备为已连接
|
|
118
|
+
self._broadcaster.mark_device_as_connected(device.device_no)
|
|
115
119
|
|
|
116
|
-
def get_device(self,
|
|
117
|
-
logger.
|
|
120
|
+
def get_device(self, device_no:str=None)->IDevice:
|
|
121
|
+
logger.info(f"已连接设备数量:{len(self._devices)}")
|
|
118
122
|
if len(self._devices) == 0:
|
|
119
123
|
return None
|
|
120
124
|
|
|
121
125
|
# 未指定device_id,返回第一个设备
|
|
122
|
-
if
|
|
126
|
+
if device_no is None:
|
|
123
127
|
return list(self._devices.values())[0]
|
|
124
128
|
|
|
125
|
-
return self._devices.get(
|
|
129
|
+
return self._devices.get(device_no, None)
|
|
126
130
|
|
qlsdk/rsc/network/discover.py
CHANGED
|
@@ -5,6 +5,11 @@ from loguru import logger
|
|
|
5
5
|
|
|
6
6
|
from qlsdk.core.message import UDPMessage
|
|
7
7
|
|
|
8
|
+
'''
|
|
9
|
+
广播器类,用于发送和接收设备广播消息
|
|
10
|
+
主要功能:发送设备搜索消息,接收设备连接消息
|
|
11
|
+
注意:广播端口需要和ar4sdk做区分,使用54366时不能和x8同时使用
|
|
12
|
+
'''
|
|
8
13
|
class UdpBroadcaster:
|
|
9
14
|
# 广播端口需要和ar4sdk做区分, 使用54366时不能和x8同时使用
|
|
10
15
|
def __init__(self, port=54366):
|
|
@@ -18,70 +23,54 @@ class UdpBroadcaster:
|
|
|
18
23
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
19
24
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
20
25
|
|
|
26
|
+
# 添加设备序列号到待广播列表
|
|
21
27
|
def add_device(self, device_id):
|
|
22
|
-
|
|
28
|
+
|
|
23
29
|
with self.lock:
|
|
24
30
|
if device_id not in self.devices_to_broadcast:
|
|
25
31
|
self.devices_to_broadcast.append(device_id)
|
|
26
|
-
logger.info(f"
|
|
32
|
+
logger.info(f"添加设备[{device_id}]到搜索列表。")
|
|
27
33
|
|
|
34
|
+
# 从待广播列表移除设备序列号
|
|
28
35
|
def remove_device(self, device_id):
|
|
29
|
-
|
|
36
|
+
|
|
30
37
|
with self.lock:
|
|
31
38
|
if device_id in self.devices_to_broadcast:
|
|
32
39
|
self.devices_to_broadcast.remove(device_id)
|
|
33
|
-
logger.info(f"
|
|
40
|
+
logger.info(f"把设备[{device_id}]从搜索列表中移除。")
|
|
34
41
|
|
|
42
|
+
# 把设备标记为已连接
|
|
35
43
|
def mark_device_as_connected(self, device_id):
|
|
36
|
-
"""将设备标记为已连接,并从未广播列表中移除"""
|
|
37
44
|
with self.lock:
|
|
45
|
+
# 如果设备已连接,则从搜索列表中移除
|
|
38
46
|
if device_id in self.devices_to_broadcast:
|
|
39
47
|
self.devices_to_broadcast.remove(device_id)
|
|
48
|
+
|
|
49
|
+
# 添加到已连接设备集合
|
|
40
50
|
self.connected_devices.add(device_id)
|
|
41
|
-
|
|
51
|
+
|
|
52
|
+
logger.info(f"设备[{device_id}]已连接,从搜索列表中移除。")
|
|
42
53
|
|
|
54
|
+
# 广播设备信息,寻求配对
|
|
43
55
|
def broadcast_devices(self):
|
|
44
|
-
|
|
56
|
+
|
|
45
57
|
while self.running:
|
|
46
58
|
with self.lock:
|
|
47
59
|
for device_id in self.devices_to_broadcast:
|
|
48
60
|
message = UDPMessage.search(device_id)
|
|
49
61
|
self.sock.sendto(message, ('<broadcast>', self.broadcast_port))
|
|
50
|
-
logger.
|
|
51
|
-
|
|
62
|
+
logger.debug(f"设备[{device_id}]广播消息已发送。")
|
|
63
|
+
|
|
64
|
+
# 每隔1秒发送一次广播
|
|
65
|
+
time.sleep(1)
|
|
52
66
|
|
|
53
67
|
def start(self):
|
|
54
68
|
"""启动广播线程"""
|
|
55
|
-
self.broadcast_thread = Thread(target=self.broadcast_devices)
|
|
56
|
-
self.broadcast_thread.setDaemon(True)
|
|
69
|
+
self.broadcast_thread = Thread(target=self.broadcast_devices, daemon=True)
|
|
57
70
|
self.broadcast_thread.start()
|
|
58
71
|
|
|
59
72
|
def stop(self):
|
|
60
73
|
"""停止广播"""
|
|
61
74
|
self.running = False
|
|
62
75
|
self.broadcast_thread.join()
|
|
63
|
-
self.sock.close()
|
|
64
|
-
|
|
65
|
-
# 示例使用
|
|
66
|
-
if __name__ == "__main__":
|
|
67
|
-
broadcaster = UdpBroadcaster()
|
|
68
|
-
|
|
69
|
-
# 添加设备序列号到待广播列表
|
|
70
|
-
broadcaster.add_device_to_broadcast("390024130032")
|
|
71
|
-
|
|
72
|
-
# 启动广播
|
|
73
|
-
broadcaster.start()
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
# 模拟运行一段时间
|
|
77
|
-
time.sleep(10)
|
|
78
|
-
|
|
79
|
-
# 标记设备为已连接
|
|
80
|
-
broadcaster.mark_device_as_connected("390024130032")
|
|
81
|
-
|
|
82
|
-
# 继续运行一段时间
|
|
83
|
-
time.sleep(10)
|
|
84
|
-
|
|
85
|
-
finally:
|
|
86
|
-
# 停止广播
|
|
87
|
-
broadcaster.stop()
|
|
76
|
+
self.sock.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
|