qlsdk2 0.4.1__py3-none-any.whl → 0.5.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.
- qlsdk/core/entity/__init__.py +1 -0
- qlsdk/core/utils.py +20 -15
- qlsdk/persist/__init__.py +2 -1
- qlsdk/persist/ars_edf.py +278 -0
- qlsdk/persist/rsc_edf.py +47 -28
- qlsdk/persist/stream.py +161 -0
- qlsdk/rsc/command/__init__.py +28 -30
- qlsdk/rsc/device/__init__.py +6 -1
- qlsdk/rsc/device/arskindling.py +386 -0
- qlsdk/rsc/device/base.py +137 -70
- qlsdk/rsc/device/c16_rs.py +205 -0
- qlsdk/rsc/device/c256_rs.py +365 -0
- qlsdk/rsc/device/c64_rs.py +9 -14
- qlsdk/rsc/device/c64s1.py +365 -0
- qlsdk/rsc/device/device_factory.py +6 -1
- qlsdk/rsc/interface/device.py +12 -2
- qlsdk/rsc/interface/parser.py +4 -1
- qlsdk/rsc/manager/container.py +21 -17
- qlsdk/rsc/network/discover.py +25 -36
- qlsdk/rsc/parser/base.py +106 -22
- qlsdk2-0.5.0.dist-info/METADATA +40 -0
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.5.0.dist-info}/RECORD +24 -18
- qlsdk2-0.4.1.dist-info/METADATA +0 -121
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.5.0.dist-info}/WHEEL +0 -0
- {qlsdk2-0.4.1.dist-info → qlsdk2-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
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.interface import IDevice, IParser
|
|
10
|
+
from qlsdk.rsc.command import *
|
|
11
|
+
from qlsdk.rsc.parser.base import TcpMessageParser
|
|
12
|
+
from qlsdk.rsc.device.base import QLBaseDevice
|
|
13
|
+
|
|
14
|
+
class C64S1(QLBaseDevice):
|
|
15
|
+
|
|
16
|
+
device_type = 0x40 # C64RS设备类型标识符
|
|
17
|
+
|
|
18
|
+
def __init__(self, socket):
|
|
19
|
+
super().__init__(socket)
|
|
20
|
+
self.socket = socket
|
|
21
|
+
|
|
22
|
+
self._id = None
|
|
23
|
+
|
|
24
|
+
# 设备信息
|
|
25
|
+
self.device_id = None
|
|
26
|
+
self.device_name = None
|
|
27
|
+
self._device_no = None
|
|
28
|
+
self.software_version = None
|
|
29
|
+
self.hardware_version = None
|
|
30
|
+
self.connect_time = None
|
|
31
|
+
self.current_time = None
|
|
32
|
+
# mV
|
|
33
|
+
self.voltage = None
|
|
34
|
+
# %
|
|
35
|
+
self.battery_remain = None
|
|
36
|
+
# %
|
|
37
|
+
self.battery_total = None
|
|
38
|
+
# persist
|
|
39
|
+
self._recording = False
|
|
40
|
+
self._storage_path = None
|
|
41
|
+
self._file_prefix = None
|
|
42
|
+
|
|
43
|
+
# 可设置参数
|
|
44
|
+
# 采集:采样量程、采样率、采样通道
|
|
45
|
+
# 刺激:刺激电流、刺激频率、刺激时间、刺激通道
|
|
46
|
+
# 采样量程(mV):188、375、563、750、1125、2250、4500
|
|
47
|
+
self._sample_range:Literal[188, 375, 563, 750, 1125, 2250, 4500] = 188
|
|
48
|
+
# 采样率(Hz):250、500、1000、2000、4000、8000、16000、32000
|
|
49
|
+
self._sample_rate:Literal[250, 500, 1000, 2000, 4000, 8000, 16000, 32000] = 500
|
|
50
|
+
self._physical_max = self._sample_range * 1000 # 物理最大值(uV)
|
|
51
|
+
self._physical_min = -self._sample_range * 1000 # 物理最小值(uV)
|
|
52
|
+
self._digital_max = 8388607
|
|
53
|
+
self._digital_min = -8388608
|
|
54
|
+
self._physical_range = self._physical_max - self._physical_min
|
|
55
|
+
self._digital_range = 16777215
|
|
56
|
+
self._acq_channels = None
|
|
57
|
+
self._acq_param = {
|
|
58
|
+
"sample_range": 188,
|
|
59
|
+
"sample_rate": 500,
|
|
60
|
+
"channels": [],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
self._stim_param = {
|
|
64
|
+
"stim_type": 0, # 刺激类型:0-所有通道参数相同, 1: 通道参数不同
|
|
65
|
+
"channels": [],
|
|
66
|
+
"param": [{
|
|
67
|
+
"channel_id": 0, #通道号 从0开始 -- 必填
|
|
68
|
+
"waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
|
|
69
|
+
"current": 1, #电流强度(mA) -- 必填
|
|
70
|
+
"duration": 30, #平稳阶段持续时间(s) -- 必填
|
|
71
|
+
"ramp_up": 5, #上升时间(s) 默认0
|
|
72
|
+
"ramp_down": 5, #下降时间(s) 默认0
|
|
73
|
+
"frequency": 500, #频率(Hz) -- 非直流必填
|
|
74
|
+
"phase_position": 0, #相位 -- 默认0
|
|
75
|
+
"duration_delay": "0", #延迟启动时间(s) -- 默认0
|
|
76
|
+
"pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"channel_id": 1, #通道号 从0开始 -- 必填
|
|
80
|
+
"waveform": 3, #波形类型:0-直流,1-交流 2-方波 3-脉冲 -- 必填
|
|
81
|
+
"current": 1, #电流强度(mA) -- 必填
|
|
82
|
+
"duration": 30, #平稳阶段持续时间(s) -- 必填
|
|
83
|
+
"ramp_up": 5, #上升时间(s) 默认0
|
|
84
|
+
"ramp_down": 5, #下降时间(s) 默认0
|
|
85
|
+
"frequency": 500, #频率(Hz) -- 非直流必填
|
|
86
|
+
"phase_position": 0, #相位 -- 默认0
|
|
87
|
+
"duration_delay": "0", #延迟启动时间(s) -- 默认0
|
|
88
|
+
"pulse_width": 0, #脉冲宽度(us) -- 仅脉冲类型电流有效, 默认100us
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self.stim_paradigm = None
|
|
94
|
+
|
|
95
|
+
signal_info = {
|
|
96
|
+
"param" : None,
|
|
97
|
+
"start_time" : None,
|
|
98
|
+
"finished_time" : None,
|
|
99
|
+
"packet_total" : None,
|
|
100
|
+
"last_packet_time" : None,
|
|
101
|
+
"state" : 0
|
|
102
|
+
}
|
|
103
|
+
stim_info = {
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
Impedance_info = {
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
# 信号采集状态
|
|
110
|
+
# 信号数据包总数(一个信号采集周期内)
|
|
111
|
+
# 信号采集参数
|
|
112
|
+
# 电刺激状态
|
|
113
|
+
# 电刺激开始时间(最近一次)
|
|
114
|
+
# 电刺激结束时间(最近一次)
|
|
115
|
+
# 电刺激参数
|
|
116
|
+
# 启动数据解析线程
|
|
117
|
+
# 数据存储状态
|
|
118
|
+
# 存储目录
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
self.__signal_consumer: Dict[str, Queue[Any]]={}
|
|
122
|
+
self.__impedance_consumer: Dict[str, Queue[Any]]={}
|
|
123
|
+
|
|
124
|
+
# EDF文件处理器
|
|
125
|
+
self._edf_handler = None
|
|
126
|
+
self.storage_enable = True
|
|
127
|
+
|
|
128
|
+
self._parser: IParser = TcpMessageParser(self)
|
|
129
|
+
self._parser.start()
|
|
130
|
+
|
|
131
|
+
# 启动数据接收线程
|
|
132
|
+
self._accept = Thread(target=self.accept)
|
|
133
|
+
self._accept.daemon = True
|
|
134
|
+
self._accept.start()
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def device_no(self):
|
|
138
|
+
return self._device_no
|
|
139
|
+
|
|
140
|
+
@device_no.setter
|
|
141
|
+
def device_no(self, value: str):
|
|
142
|
+
self._device_no = value
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_parent(cls, parent:IDevice) -> IDevice:
|
|
146
|
+
rlt = cls(parent.socket)
|
|
147
|
+
rlt.device_id = parent.device_id
|
|
148
|
+
rlt._device_no = parent.device_no
|
|
149
|
+
return rlt
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def init_edf_handler(self):
|
|
153
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
154
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
155
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
156
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
157
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C64S1')
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def edf_handler(self):
|
|
161
|
+
if not self.storage_enable:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
if self._edf_handler is None:
|
|
165
|
+
self.init_edf_handler()
|
|
166
|
+
|
|
167
|
+
return self._edf_handler
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def acq_channels(self):
|
|
171
|
+
if self._acq_channels is None:
|
|
172
|
+
self._acq_channels = [i for i in range(1, 63)]
|
|
173
|
+
return self._acq_channels
|
|
174
|
+
@property
|
|
175
|
+
def sample_range(self):
|
|
176
|
+
return self._sample_range if self._sample_range else 188
|
|
177
|
+
@property
|
|
178
|
+
def sample_rate(self):
|
|
179
|
+
return self._sample_rate if self._sample_rate else 500
|
|
180
|
+
@property
|
|
181
|
+
def resolution(self):
|
|
182
|
+
return 24
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def signal_consumers(self):
|
|
186
|
+
return self.__signal_consumer
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def impedance_consumers(self):
|
|
190
|
+
return self.__impedance_consumer
|
|
191
|
+
|
|
192
|
+
# 设置记录文件路径
|
|
193
|
+
def set_storage_path(self, path):
|
|
194
|
+
self._storage_path = path
|
|
195
|
+
|
|
196
|
+
# 设置记录文件名称前缀
|
|
197
|
+
def set_file_prefix(self, prefix):
|
|
198
|
+
self._file_prefix = prefix
|
|
199
|
+
|
|
200
|
+
# 接收socket消息
|
|
201
|
+
def accept(self):
|
|
202
|
+
while True:
|
|
203
|
+
# 缓冲去4M
|
|
204
|
+
data = self.socket.recv(4096*1024)
|
|
205
|
+
if not data:
|
|
206
|
+
logger.warning(f"设备{self.device_name}连接结束")
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
self._parser.append(data)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# socket发送数据
|
|
213
|
+
def send(self, data):
|
|
214
|
+
self.socket.sendall(data)
|
|
215
|
+
|
|
216
|
+
# 设置刺激参数
|
|
217
|
+
def set_stim_param(self, param):
|
|
218
|
+
self.stim_paradigm = param
|
|
219
|
+
|
|
220
|
+
# 设置采集参数
|
|
221
|
+
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
222
|
+
self._acq_param["channels"] = channels
|
|
223
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
224
|
+
self._acq_param["sample_range"] = sample_range
|
|
225
|
+
self._acq_channels = channels
|
|
226
|
+
self._sample_rate = sample_rate
|
|
227
|
+
self._sample_range = sample_range
|
|
228
|
+
|
|
229
|
+
# 通用配置-TODO
|
|
230
|
+
def set_config(self, key:str, val: str):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
def start_impedance(self):
|
|
234
|
+
logger.info("启动阻抗测量")
|
|
235
|
+
msg = StartImpedanceCommand.build(self).pack()
|
|
236
|
+
logger.debug(f"start_impedance message is {msg.hex()}")
|
|
237
|
+
self.socket.sendall(msg)
|
|
238
|
+
|
|
239
|
+
def stop_impedance(self):
|
|
240
|
+
logger.info("停止阻抗测量")
|
|
241
|
+
msg = StopImpedanceCommand.build(self).pack()
|
|
242
|
+
logger.debug(f"stop_impedance message is {msg.hex()}")
|
|
243
|
+
self.socket.sendall(msg)
|
|
244
|
+
|
|
245
|
+
def start_stimulation(self):
|
|
246
|
+
if self.stim_paradigm is None:
|
|
247
|
+
logger.warning("刺激参数未设置,请先设置刺激参数")
|
|
248
|
+
return
|
|
249
|
+
logger.info("启动电刺激")
|
|
250
|
+
msg = StartStimulationCommand.build(self).pack()
|
|
251
|
+
logger.debug(f"start_stimulation message is {msg.hex()}")
|
|
252
|
+
self.socket.sendall(msg)
|
|
253
|
+
t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
|
|
254
|
+
t.start()
|
|
255
|
+
|
|
256
|
+
def _stop_stimulation_trigger(self, duration):
|
|
257
|
+
delay = duration
|
|
258
|
+
while delay > 0:
|
|
259
|
+
sleep(1)
|
|
260
|
+
delay -= 1
|
|
261
|
+
logger.info(f"_stop_stimulation_trigger duration: {duration}")
|
|
262
|
+
if self._edf_handler:
|
|
263
|
+
self._edf_handler.trigger("stimulation should be stopped")
|
|
264
|
+
else:
|
|
265
|
+
logger.warning("stop stim trigger fail. no edf writer alive")
|
|
266
|
+
|
|
267
|
+
def stop_stimulation(self):
|
|
268
|
+
logger.info("停止电刺激")
|
|
269
|
+
msg = StopStimulationCommand.pack()
|
|
270
|
+
logger.debug(f"stop_stimulation message is {msg.hex()}")
|
|
271
|
+
self.socket.sendall(msg)
|
|
272
|
+
|
|
273
|
+
# 启动采集
|
|
274
|
+
def start_acquisition(self, recording = True):
|
|
275
|
+
logger.info("启动信号采集")
|
|
276
|
+
self._recording = recording
|
|
277
|
+
# 设置数据采集参数
|
|
278
|
+
param_bytes = SetAcquisitionParamCommand.build(self).pack()
|
|
279
|
+
# 启动数据采集
|
|
280
|
+
start_bytes = StartAcquisitionCommand.build(self).pack()
|
|
281
|
+
msg = param_bytes + start_bytes
|
|
282
|
+
logger.debug(f"start_acquisition message is {msg.hex()}")
|
|
283
|
+
self.socket.sendall(msg)
|
|
284
|
+
|
|
285
|
+
# 停止采集
|
|
286
|
+
def stop_acquisition(self):
|
|
287
|
+
logger.info("停止信号采集")
|
|
288
|
+
msg = StopAcquisitionCommand.build(self).pack()
|
|
289
|
+
logger.debug(f"stop_acquisition message is {msg.hex()}")
|
|
290
|
+
self.socket.sendall(msg)
|
|
291
|
+
if self._edf_handler:
|
|
292
|
+
# 发送结束标识
|
|
293
|
+
self.edf_handler.write(None)
|
|
294
|
+
|
|
295
|
+
# 订阅实时数据
|
|
296
|
+
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
297
|
+
|
|
298
|
+
# 数据队列
|
|
299
|
+
if q is None:
|
|
300
|
+
q = Queue(maxsize=1000)
|
|
301
|
+
|
|
302
|
+
# 队列名称
|
|
303
|
+
if topic is None:
|
|
304
|
+
topic = f"{type}_{time_ns()}"
|
|
305
|
+
|
|
306
|
+
# 订阅生理电信号数据
|
|
307
|
+
if type == "signal":
|
|
308
|
+
# topic唯一,用来区分不同的订阅队列(下同)
|
|
309
|
+
if topic in list(self.__signal_consumer.keys()):
|
|
310
|
+
logger.warning(f"exists {type} subscribe of {topic}")
|
|
311
|
+
else:
|
|
312
|
+
self.__signal_consumer[topic] = q
|
|
313
|
+
|
|
314
|
+
# 订阅阻抗数据
|
|
315
|
+
if type == "impedance":
|
|
316
|
+
if topic in list(self.__signal_consumer.keys()):
|
|
317
|
+
logger.warning(f"exists {type} subscribe of {topic}")
|
|
318
|
+
else:
|
|
319
|
+
self.__impedance_consumer[topic] = q
|
|
320
|
+
|
|
321
|
+
return topic, q
|
|
322
|
+
|
|
323
|
+
def trigger(self, desc):
|
|
324
|
+
if self._edf_handler:
|
|
325
|
+
self.edf_handler.trigger(desc)
|
|
326
|
+
else:
|
|
327
|
+
logger.warning("no edf handler, no place to recording trigger")
|
|
328
|
+
|
|
329
|
+
def __str__(self):
|
|
330
|
+
return f'''
|
|
331
|
+
Device:
|
|
332
|
+
Name: {self.device_name},
|
|
333
|
+
Type: {hex(self.device_type) if self.device_type else None},
|
|
334
|
+
ID: {self.device_id if self.device_id else None},
|
|
335
|
+
Software: {self.software_version},
|
|
336
|
+
Hardware: {self.hardware_version},
|
|
337
|
+
Connect Time: {self.connect_time},
|
|
338
|
+
Current Time: {self.current_time},
|
|
339
|
+
Voltage: {str(self.voltage) + "mV" if self.voltage else None},
|
|
340
|
+
Battery Remain: {str(self.battery_remain)+ "%" if self.battery_remain else None},
|
|
341
|
+
Battery Total: {str(self.battery_total) + "%" if self.battery_total else None}
|
|
342
|
+
'''
|
|
343
|
+
|
|
344
|
+
def __repr__(self):
|
|
345
|
+
return f'''
|
|
346
|
+
Device:
|
|
347
|
+
Name: {self.device_name},
|
|
348
|
+
Type: {hex(self.device_type)},
|
|
349
|
+
ID: {self.device_id},
|
|
350
|
+
Software: {self.software_version},
|
|
351
|
+
Hardware: {self.hardware_version},
|
|
352
|
+
Connect Time: {self.connect_time},
|
|
353
|
+
Current Time: {self.current_time},
|
|
354
|
+
Voltage: {self.voltage}mV,
|
|
355
|
+
Battery Remain: {self.battery_remain}%,
|
|
356
|
+
Battery Total: {self.battery_total}%
|
|
357
|
+
'''
|
|
358
|
+
|
|
359
|
+
def __eq__(self, other):
|
|
360
|
+
return self.device_name == other.device_name and self.device_type == other.device_type and self.device_id == other.device_id
|
|
361
|
+
|
|
362
|
+
def __hash__(self):
|
|
363
|
+
return hash((self.device_name, self.device_type, self.device_id))
|
|
364
|
+
|
|
365
|
+
|
|
@@ -2,6 +2,7 @@ from typing import Dict, Type
|
|
|
2
2
|
from qlsdk.rsc.interface import IDevice
|
|
3
3
|
|
|
4
4
|
from qlsdk.rsc.device.c64_rs import C64RS
|
|
5
|
+
from qlsdk.rsc.device.arskindling import ARSKindling
|
|
5
6
|
|
|
6
7
|
from loguru import logger
|
|
7
8
|
|
|
@@ -24,6 +25,10 @@ class DeviceFactory(object):
|
|
|
24
25
|
return instance.from_parent(device) if hasattr(instance, 'from_parent') else instance(device.socket)
|
|
25
26
|
|
|
26
27
|
# Register the C64RS device with the DeviceFactory
|
|
27
|
-
DeviceFactory.register(C64RS.device_type, C64RS)
|
|
28
|
+
DeviceFactory.register(C64RS.device_type, C64RS)
|
|
29
|
+
# Register the ARSKindling device with the DeviceFactory
|
|
30
|
+
DeviceFactory.register(ARSKindling.device_type, ARSKindling)
|
|
31
|
+
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
|
qlsdk/rsc/interface/device.py
CHANGED
|
@@ -15,6 +15,15 @@ class IDevice(ABC):
|
|
|
15
15
|
def device_no(self) -> str:
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
|
+
def produce(self, data) -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def start_listening(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def stop_listening(self):
|
|
25
|
+
pass
|
|
26
|
+
|
|
18
27
|
def set_device_no(self, value: str):
|
|
19
28
|
pass
|
|
20
29
|
|
|
@@ -44,9 +53,10 @@ class IDevice(ABC):
|
|
|
44
53
|
|
|
45
54
|
def stop_stimulation(self) -> None:
|
|
46
55
|
pass
|
|
56
|
+
|
|
57
|
+
def gen_set_acquirement_param(self) -> bytes:
|
|
58
|
+
pass
|
|
47
59
|
|
|
48
|
-
def __eq__(self, other):
|
|
49
|
-
return self.device_id == other.device_id
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
class RscDevice(IDevice):
|
qlsdk/rsc/interface/parser.py
CHANGED
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
|
-
for
|
|
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):
|
|
@@ -55,15 +55,16 @@ class DeviceContainer(object):
|
|
|
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
62
|
client_handler = Thread(
|
|
63
63
|
target=self.client_handler,
|
|
64
|
-
args=(client_socket,)
|
|
64
|
+
args=(client_socket,),
|
|
65
|
+
daemon=True
|
|
65
66
|
)
|
|
66
|
-
|
|
67
|
+
|
|
67
68
|
client_handler.start()
|
|
68
69
|
|
|
69
70
|
except KeyboardInterrupt:
|
|
@@ -81,16 +82,19 @@ class DeviceContainer(object):
|
|
|
81
82
|
else:
|
|
82
83
|
# 数据监听
|
|
83
84
|
device = QLBaseDevice(client_socket)
|
|
85
|
+
device.start_listening()
|
|
84
86
|
# GET_DEVICE_INFO
|
|
85
87
|
msg = GetDeviceInfoCommand.build(device).pack()
|
|
86
|
-
logger.
|
|
88
|
+
logger.trace(f"发送获取设备信息命令: {msg.hex()}")
|
|
87
89
|
device.send(msg)
|
|
88
90
|
# 添加设备
|
|
89
91
|
while True:
|
|
90
92
|
if device.device_name:
|
|
91
|
-
logger.info(f"设备 {device.device_name} 已连接")
|
|
92
93
|
real_device = DeviceFactory.create_device(device)
|
|
94
|
+
device.stop_listening()
|
|
95
|
+
real_device.start_listening()
|
|
93
96
|
self.add_device(real_device)
|
|
97
|
+
logger.info(f"设备[{device.device_name}]已连接,设备类型为:{hex(real_device.device_type)}")
|
|
94
98
|
break
|
|
95
99
|
|
|
96
100
|
|
|
@@ -102,20 +106,20 @@ class DeviceContainer(object):
|
|
|
102
106
|
if device is None or device.device_no is None:
|
|
103
107
|
logger.warning("无效的设备")
|
|
104
108
|
|
|
105
|
-
self._devices
|
|
106
|
-
logger.
|
|
109
|
+
self._devices[device.device_no] = device
|
|
110
|
+
logger.info(f"添加设备[{device.device_no}]到设备列表,已连接设备数量:{len(self._devices)}")
|
|
107
111
|
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
# 标记设备为已连接
|
|
113
|
+
self._broadcaster.mark_device_as_connected(device.device_no)
|
|
110
114
|
|
|
111
|
-
def get_device(self,
|
|
112
|
-
logger.
|
|
115
|
+
def get_device(self, device_no:str=None)->IDevice:
|
|
116
|
+
logger.info(f"已连接设备数量:{len(self._devices)}")
|
|
113
117
|
if len(self._devices) == 0:
|
|
114
118
|
return None
|
|
115
119
|
|
|
116
120
|
# 未指定device_id,返回第一个设备
|
|
117
|
-
if
|
|
121
|
+
if device_no is None:
|
|
118
122
|
return list(self._devices.values())[0]
|
|
119
123
|
|
|
120
|
-
return self._devices.get(
|
|
124
|
+
return self._devices.get(device_no, None)
|
|
121
125
|
|
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()
|