1fb31449 by huangyf2

日志异步处理,增加

1 parent aa901ba8
Showing 1 changed file with 228 additions and 24 deletions
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 """
3 高性能BLE客户端 - 优化版本
4 修复了以下问题:
5 1. 内存泄漏 - 每次连接创建新实例导致资源无法释放
6 2. 日志阻塞 - 实现异步日志队列
7 3. 通知失效 - 完善连接状态检查和清理
8 4. WebSocket保活 - 添加ping/pong机制
9 5. 错误处理 - 全面增强异常捕获和处理
10 """
2 import sys 11 import sys
3 import asyncio 12 import asyncio
4 import websockets 13 import websockets
...@@ -9,6 +18,7 @@ import threading ...@@ -9,6 +18,7 @@ import threading
9 from collections import defaultdict 18 from collections import defaultdict
10 import socket 19 import socket
11 import time 20 import time
21 import traceback
12 22
13 import platform 23 import platform
14 24
...@@ -22,21 +32,58 @@ elif sys.platform.startswith('linux'): ...@@ -22,21 +32,58 @@ elif sys.platform.startswith('linux'):
22 print("当前运行在Linux系统") 32 print("当前运行在Linux系统")
23 elif sys.platform.startswith('darwin'): 33 elif sys.platform.startswith('darwin'):
24 print("当前运行在macOS系统") 34 print("当前运行在macOS系统")
25 # 添加线程锁以确保日志写入的原子性 35 # 高性能日志系统
26 write_lock = threading.Lock() 36 write_lock = threading.Lock()
37 _log_queue = None
38 _log_task = None
39 _log_dropped_count = 0
27 40
28 def log_message_sync(direction, message): 41 def log_message_sync(direction, message):
29 """同步日志记录函数""" 42 """同步日志记录函数"""
43 try:
30 log_entry = f"{direction}: {message}\n" 44 log_entry = f"{direction}: {message}\n"
31 print(log_entry, end='') # 控制台仍然输出 45 print(log_entry, end='') # 控制台仍然输出
46
47 # 截断过长消息,避免写入过大的日志
48 if len(log_entry) > 4000:
49 log_entry = log_entry[:4000] + "... [truncated]\n"
50
32 with write_lock: 51 with write_lock:
33 with open('b.log', 'a', encoding='utf-8') as f: 52 with open('b.log', 'a', encoding='utf-8') as f:
34 f.write(log_entry) 53 f.write(log_entry)
54 except Exception:
55 pass # 日志失败不应影响主流程
56
57 async def _log_writer_loop():
58 """后台日志写入循环"""
59 global _log_queue
60 while True:
61 try:
62 item = await _log_queue.get()
63 if item is None:
64 break
65 direction, message = item
66 log_message_sync(direction, message)
67 except asyncio.CancelledError:
68 break
69 except Exception:
70 await asyncio.sleep(0.05)
35 71
36 async def log_message(direction, message): 72 async def log_message(direction, message):
37 """异步封装日志记录""" 73 """异步日志记录,非阻塞"""
38 loop = asyncio.get_event_loop() 74 global _log_queue, _log_task, _log_dropped_count
39 await loop.run_in_executor(None, log_message_sync, direction, message) 75
76 # 首次调用时初始化
77 if _log_queue is None:
78 _log_queue = asyncio.Queue(maxsize=1000)
79 _log_task = asyncio.create_task(_log_writer_loop())
80
81 try:
82 _log_queue.put_nowait((direction, message))
83 except asyncio.QueueFull:
84 _log_dropped_count += 1
85 if _log_dropped_count % 100 == 1:
86 print(f"警告: 日志队列已满,已丢弃 {_log_dropped_count} 条日志")
40 87
41 def is_port_in_use(port, host='localhost'): 88 def is_port_in_use(port, host='localhost'):
42 """检查端口是否被占用""" 89 """检查端口是否被占用"""
...@@ -77,7 +124,9 @@ class BLEClient: ...@@ -77,7 +124,9 @@ class BLEClient:
77 self.services = [] 124 self.services = []
78 self.optional_services = [] 125 self.optional_services = []
79 self.websocket = None 126 self.websocket = None
80 self.notification_records = defaultdict(lambda: (None, 0)) # 特征ID: (最后消息, 时间戳) 127 self.notification_records = defaultdict(lambda: (None, 0.0)) # 特征ID: (最后消息, 时间戳)
128 self._notification_callbacks = {} # 存储通知回调,用于清理
129 self._shutdown = False # 添加关闭标志
81 130
82 def on_disconnect(self, client): 131 def on_disconnect(self, client):
83 print("BLE连接断开,关闭WebSocket") 132 print("BLE连接断开,关闭WebSocket")
...@@ -85,7 +134,12 @@ class BLEClient: ...@@ -85,7 +134,12 @@ class BLEClient:
85 asyncio.create_task(self.close_websocket()) 134 asyncio.create_task(self.close_websocket())
86 135
87 async def close_websocket(self): 136 async def close_websocket(self):
137 if self.websocket:
138 try:
88 await self.websocket.close() 139 await self.websocket.close()
140 except Exception as e:
141 print(f"关闭WebSocket时出错: {e}")
142 finally:
89 self.websocket = None 143 self.websocket = None
90 144
91 def detection_callback(self, device, advertisement_data): 145 def detection_callback(self, device, advertisement_data):
...@@ -176,7 +230,15 @@ class BLEClient: ...@@ -176,7 +230,15 @@ class BLEClient:
176 elif method == "connect": 230 elif method == "connect":
177 peripheral_id = params.get("peripheralId") 231 peripheral_id = params.get("peripheralId")
178 if peripheral_id: 232 if peripheral_id:
179 self.client = BleakClient(peripheral_id) 233 # 如果已有连接,先断开
234 if self.client and self.client.is_connected:
235 try:
236 await self.client.disconnect()
237 except Exception:
238 pass
239
240 try:
241 self.client = BleakClient(peripheral_id, timeout=10.0) # 添加超时
180 self.client.set_disconnected_callback(self.on_disconnect) 242 self.client.set_disconnected_callback(self.on_disconnect)
181 await self.client.connect() 243 await self.client.connect()
182 244
...@@ -186,8 +248,26 @@ class BLEClient: ...@@ -186,8 +248,26 @@ class BLEClient:
186 "result": None, 248 "result": None,
187 "id": request_id 249 "id": request_id
188 }) 250 })
189 # await log_message("下发", response) 251 await log_message("下发", response)
190 await websocket.send(response) 252 await websocket.send(response)
253 else:
254 error_response = json.dumps({
255 "jsonrpc": "2.0",
256 "error": {"code": -1, "message": "连接失败"},
257 "id": request_id
258 })
259 await log_message("下发", error_response)
260 await websocket.send(error_response)
261 except Exception as e:
262 error_response = json.dumps({
263 "jsonrpc": "2.0",
264 "error": {"code": -1, "message": f"连接异常: {str(e)}"},
265 "id": request_id
266 })
267 await log_message("下发", error_response)
268 await websocket.send(error_response)
269 if self.client:
270 self.client = None
191 271
192 elif method == "write": 272 elif method == "write":
193 service_id = params.get("serviceId") 273 service_id = params.get("serviceId")
...@@ -195,7 +275,8 @@ class BLEClient: ...@@ -195,7 +275,8 @@ class BLEClient:
195 message = params.get("message") 275 message = params.get("message")
196 encoding = params.get("encoding", "utf-8") 276 encoding = params.get("encoding", "utf-8")
197 277
198 if all([service_id, characteristic_id, message]): 278 if all([service_id, characteristic_id, message]) and self.client and self.client.is_connected:
279 try:
199 if encoding == "base64": 280 if encoding == "base64":
200 message_bytes = base64.b64decode(message) 281 message_bytes = base64.b64decode(message)
201 print("write message_bytes",message_bytes) 282 print("write message_bytes",message_bytes)
...@@ -203,7 +284,6 @@ class BLEClient: ...@@ -203,7 +284,6 @@ class BLEClient:
203 print("write message",message) 284 print("write message",message)
204 message_bytes = message.encode(encoding) 285 message_bytes = message.encode(encoding)
205 286
206
207 await self.client.write_gatt_char(characteristic_id, message_bytes) 287 await self.client.write_gatt_char(characteristic_id, message_bytes)
208 response = json.dumps({ 288 response = json.dumps({
209 "jsonrpc": "2.0", 289 "jsonrpc": "2.0",
...@@ -212,14 +292,31 @@ class BLEClient: ...@@ -212,14 +292,31 @@ class BLEClient:
212 }) 292 })
213 await log_message("下发", response) 293 await log_message("下发", response)
214 await websocket.send(response) 294 await websocket.send(response)
295 except Exception as e:
296 error_response = json.dumps({
297 "jsonrpc": "2.0",
298 "error": {"code": -1, "message": f"写入失败: {str(e)}"},
299 "id": request_id
300 })
301 await log_message("下发", error_response)
302 await websocket.send(error_response)
303 else:
304 error_response = json.dumps({
305 "jsonrpc": "2.0",
306 "error": {"code": -1, "message": "未连接或参数不全"},
307 "id": request_id
308 })
309 await log_message("下发", error_response)
310 await websocket.send(error_response)
215 311
216 elif method == "read": 312 elif method == "read":
217 service_id = params.get("serviceId") 313 service_id = params.get("serviceId")
218 characteristic_id = params.get("characteristicId") 314 characteristic_id = params.get("characteristicId")
219 315
220 if all([service_id, characteristic_id]): 316 if all([service_id, characteristic_id]) and self.client and self.client.is_connected:
317 try:
221 data = await self.client.read_gatt_char(characteristic_id) 318 data = await self.client.read_gatt_char(characteristic_id)
222 print('read-data',base64.decode(data)) 319 print('read-data',data)
223 response = json.dumps({ 320 response = json.dumps({
224 "jsonrpc": "2.0", 321 "jsonrpc": "2.0",
225 "result": { 322 "result": {
...@@ -231,16 +328,41 @@ class BLEClient: ...@@ -231,16 +328,41 @@ class BLEClient:
231 }) 328 })
232 await log_message("下发", response) 329 await log_message("下发", response)
233 await websocket.send(response) 330 await websocket.send(response)
331 except Exception as e:
332 error_response = json.dumps({
333 "jsonrpc": "2.0",
334 "error": {"code": -1, "message": f"读取失败: {str(e)}"},
335 "id": request_id
336 })
337 await log_message("下发", error_response)
338 await websocket.send(error_response)
339 else:
340 error_response = json.dumps({
341 "jsonrpc": "2.0",
342 "error": {"code": -1, "message": "未连接或参数不全"},
343 "id": request_id
344 })
345 await log_message("下发", error_response)
346 await websocket.send(error_response)
234 347
235 elif method == "startNotifications": 348 elif method == "startNotifications":
236 service_id = params.get("serviceId") 349 service_id = params.get("serviceId")
237 characteristic_id = params.get("characteristicId") 350 characteristic_id = params.get("characteristicId")
238 351
239 if all([service_id, characteristic_id]): 352 if all([service_id, characteristic_id]) and self.client and self.client.is_connected:
240 await self.client.start_notify( 353 try:
241 characteristic_id, 354 # 如果已经监听,先停止旧的
242 self.notification_handler(websocket, service_id, characteristic_id) 355 if characteristic_id in self._notification_callbacks:
243 ) 356 try:
357 await self.client.stop_notify(characteristic_id)
358 except Exception:
359 pass
360
361 # 创建新的通知处理器
362 callback = self.notification_handler(websocket, service_id, characteristic_id)
363 await self.client.start_notify(characteristic_id, callback)
364 self._notification_callbacks[characteristic_id] = callback
365
244 response = json.dumps({ 366 response = json.dumps({
245 "jsonrpc": "2.0", 367 "jsonrpc": "2.0",
246 "result": None, 368 "result": None,
...@@ -248,6 +370,22 @@ class BLEClient: ...@@ -248,6 +370,22 @@ class BLEClient:
248 }) 370 })
249 await log_message("下发", response) 371 await log_message("下发", response)
250 await websocket.send(response) 372 await websocket.send(response)
373 except Exception as e:
374 error_response = json.dumps({
375 "jsonrpc": "2.0",
376 "error": {"code": -1, "message": f"启动通知失败: {str(e)}"},
377 "id": request_id
378 })
379 await log_message("下发", error_response)
380 await websocket.send(error_response)
381 else:
382 error_response = json.dumps({
383 "jsonrpc": "2.0",
384 "error": {"code": -1, "message": "未连接或参数不全"},
385 "id": request_id
386 })
387 await log_message("下发", error_response)
388 await websocket.send(error_response)
251 389
252 elif method == "ping": 390 elif method == "ping":
253 # 处理ping请求,返回pong响应 391 # 处理ping请求,返回pong响应
...@@ -259,39 +397,72 @@ class BLEClient: ...@@ -259,39 +397,72 @@ class BLEClient:
259 await log_message("下发", response) 397 await log_message("下发", response)
260 await websocket.send(response) 398 await websocket.send(response)
261 399
262 except json.JSONDecodeError: 400 except json.JSONDecodeError as e:
263 error_msg = json.dumps({ 401 error_msg = json.dumps({
264 "jsonrpc": "2.0", 402 "jsonrpc": "2.0",
265 "result": {"message": "Parse error"}, 403 "error": {"code": -32700, "message": "Parse error", "data": str(e)},
266 "id": None 404 "id": request.get("id") if request else None
267 }) 405 })
268 await log_message("下发", error_msg) 406 await log_message("下发", error_msg)
407 try:
408 await websocket.send(error_msg)
409 except Exception:
410 pass
269 except Exception as e: 411 except Exception as e:
412 # 记录完整错误堆栈
413 error_trace = traceback.format_exc()
414 print(f"处理请求时出错: {error_trace}")
415
270 error_msg = json.dumps({ 416 error_msg = json.dumps({
271 "jsonrpc": "2.0", 417 "jsonrpc": "2.0",
272 "result": {"message": str(e)}, 418 "error": {"code": -32603, "message": "Internal error", "data": str(e)},
273 "id": request.get("id") if request else None 419 "id": request.get("id") if request else None
274 }) 420 })
275 await log_message("下发", error_msg) 421 await log_message("下发", error_msg)
422 try:
423 await websocket.send(error_msg)
424 except Exception:
425 pass
276 426
277 except websockets.exceptions.ConnectionClosed: 427 except websockets.exceptions.ConnectionClosed:
278 print("WebSocket连接关闭") 428 print("WebSocket连接关闭")
279 finally: 429 finally:
430 # 清理BLE连接和通知
431 self._shutdown = True
280 if self.client and self.client.is_connected: 432 if self.client and self.client.is_connected:
433 try:
434 # 停止所有通知
435 for characteristic_id in list(self._notification_callbacks.keys()):
436 try:
437 await self.client.stop_notify(characteristic_id)
438 except Exception as e:
439 print(f"停止通知失败 {characteristic_id}: {e}")
440
441 # 断开连接
281 await self.client.disconnect() 442 await self.client.disconnect()
443 except Exception as e:
444 print(f"断开BLE连接失败: {e}")
445 finally:
282 self.client = None 446 self.client = None
283 self.target_device = None 447 self.target_device = None
448 self._notification_callbacks.clear()
449 self.notification_records.clear()
284 450
285 def notification_handler(self, websocket, service_id, characteristic_id): 451 def notification_handler(self, websocket, service_id, characteristic_id):
286 async def callback(sender, data): 452 async def callback(sender, data):
287 current_time = asyncio.get_event_loop().time() 453 try:
454 # 检查websocket是否已关闭
455 if self.websocket is None or self.websocket.closed or self._shutdown:
456 return
457
458 current_time = time.time() # 使用time.time()而不是event_loop.time()
288 last_message, last_time = self.notification_records[characteristic_id] 459 last_message, last_time = self.notification_records[characteristic_id]
289 460
290 # 解码当前数据用于比较 461 # 解码当前数据用于比较
291 current_message = base64.b64encode(data).decode('utf-8') 462 current_message = base64.b64encode(data).decode('utf-8')
292 print('notification_handler current_message',base64.b64decode(current_message)) 463 print('notification_handler current_message',data)
293 464
294 # 过滤逻辑 465 # 过滤逻辑 - 只在0.5秒内且消息完全相同时跳过
295 if current_message == last_message and (current_time - last_time) < 0.5: 466 if current_message == last_message and (current_time - last_time) < 0.5:
296 return 467 return
297 468
...@@ -307,8 +478,23 @@ class BLEClient: ...@@ -307,8 +478,23 @@ class BLEClient:
307 "message": current_message 478 "message": current_message
308 } 479 }
309 }) 480 })
481
482 # 使用非阻塞日志
483 try:
310 await log_message("下发", response) 484 await log_message("下发", response)
485 except Exception as e:
486 print(f"日志写入失败: {e}")
487
488 # 发送响应
489 try:
311 await websocket.send(response) 490 await websocket.send(response)
491 except websockets.exceptions.ConnectionClosed:
492 print("WebSocket连接已关闭,停止发送通知")
493 self._shutdown = True
494 except Exception as e:
495 print(f"发送通知失败: {e}")
496 except Exception as e:
497 print(f"通知处理器出错: {e}")
312 return callback 498 return callback
313 499
314 async def check_port_and_start_server(port=20111, host='localhost'): 500 async def check_port_and_start_server(port=20111, host='localhost'):
...@@ -320,7 +506,11 @@ async def check_port_and_start_server(port=20111, host='localhost'): ...@@ -320,7 +506,11 @@ async def check_port_and_start_server(port=20111, host='localhost'):
320 print(f"端口 {port} 可用,正在启动服务...") 506 print(f"端口 {port} 可用,正在启动服务...")
321 server = await websockets.serve( 507 server = await websockets.serve(
322 lambda websocket, path: BLEClient().handle_client(websocket, path), 508 lambda websocket, path: BLEClient().handle_client(websocket, path),
323 host, port 509 host, port,
510 ping_interval=20, # 每20秒发送ping
511 ping_timeout=10, # 等待pong超时10秒
512 max_size=2 * 1024 * 1024, # 最大消息大小2MB
513 close_timeout=5, # 关闭超时5秒
324 ) 514 )
325 515
326 print(f"WebSocket服务已启动: ws://{host}:{port}/scratch/ble") 516 print(f"WebSocket服务已启动: ws://{host}:{port}/scratch/ble")
...@@ -343,7 +533,21 @@ async def check_port_and_start_server(port=20111, host='localhost'): ...@@ -343,7 +533,21 @@ async def check_port_and_start_server(port=20111, host='localhost'):
343 async def main(): 533 async def main():
344 server = await check_port_and_start_server() 534 server = await check_port_and_start_server()
345 if server: 535 if server:
536 try:
346 await asyncio.Future() # 保持服务器运行 537 await asyncio.Future() # 保持服务器运行
538 finally:
539 # 清理日志系统
540 global _log_queue, _log_task
541 if _log_queue is not None:
542 try:
543 await _log_queue.put(None)
544 except Exception:
545 pass
546 if _log_task is not None:
547 try:
548 await _log_task
549 except Exception:
550 pass
347 551
348 if __name__ == "__main__": 552 if __name__ == "__main__":
349 asyncio.run(main()) 553 asyncio.run(main())
...\ No newline at end of file ...\ No newline at end of file
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!