1fb31449 by huangyf2

日志异步处理,增加

1 parent aa901ba8
Showing 1 changed file with 292 additions and 88 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 """同步日志记录函数"""
30 log_entry = f"{direction}: {message}\n" 43 try:
31 print(log_entry, end='') # 控制台仍然输出 44 log_entry = f"{direction}: {message}\n"
32 with write_lock: 45 print(log_entry, end='') # 控制台仍然输出
33 with open('b.log', 'a', encoding='utf-8') as f: 46
34 f.write(log_entry) 47 # 截断过长消息,避免写入过大的日志
48 if len(log_entry) > 4000:
49 log_entry = log_entry[:4000] + "... [truncated]\n"
50
51 with write_lock:
52 with open('b.log', 'a', encoding='utf-8') as f:
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,8 +134,13 @@ class BLEClient: ...@@ -85,8 +134,13 @@ 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):
88 await self.websocket.close() 137 if self.websocket:
89 self.websocket = None 138 try:
139 await self.websocket.close()
140 except Exception as e:
141 print(f"关闭WebSocket时出错: {e}")
142 finally:
143 self.websocket = None
90 144
91 def detection_callback(self, device, advertisement_data): 145 def detection_callback(self, device, advertisement_data):
92 if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services): 146 if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services):
...@@ -176,18 +230,44 @@ class BLEClient: ...@@ -176,18 +230,44 @@ 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 # 如果已有连接,先断开
180 self.client.set_disconnected_callback(self.on_disconnect) 234 if self.client and self.client.is_connected:
181 await self.client.connect() 235 try:
236 await self.client.disconnect()
237 except Exception:
238 pass
182 239
183 if self.client.is_connected: 240 try:
184 response = json.dumps({ 241 self.client = BleakClient(peripheral_id, timeout=10.0) # 添加超时
242 self.client.set_disconnected_callback(self.on_disconnect)
243 await self.client.connect()
244
245 if self.client.is_connected:
246 response = json.dumps({
247 "jsonrpc": "2.0",
248 "result": None,
249 "id": request_id
250 })
251 await log_message("下发", 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({
185 "jsonrpc": "2.0", 263 "jsonrpc": "2.0",
186 "result": None, 264 "error": {"code": -1, "message": f"连接异常: {str(e)}"},
187 "id": request_id 265 "id": request_id
188 }) 266 })
189 # await log_message("下发", response) 267 await log_message("下发", error_response)
190 await websocket.send(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,59 +275,117 @@ class BLEClient: ...@@ -195,59 +275,117 @@ 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:
199 if encoding == "base64": 279 try:
200 message_bytes = base64.b64decode(message) 280 if encoding == "base64":
201 print("write message_bytes",message_bytes) 281 message_bytes = base64.b64decode(message)
202 else: 282 print("write message_bytes",message_bytes)
203 print("write message",message) 283 else:
204 message_bytes = message.encode(encoding) 284 print("write message",message)
205 285 message_bytes = message.encode(encoding)
206 286
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({
289 "jsonrpc": "2.0",
290 "result": None,
291 "id": request_id
292 })
293 await log_message("下发", 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({
209 "jsonrpc": "2.0", 305 "jsonrpc": "2.0",
210 "result": None, 306 "error": {"code": -1, "message": "未连接或参数不全"},
211 "id": request_id 307 "id": request_id
212 }) 308 })
213 await log_message("下发", response) 309 await log_message("下发", error_response)
214 await websocket.send(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:
221 data = await self.client.read_gatt_char(characteristic_id) 317 try:
222 print('read-data',base64.decode(data)) 318 data = await self.client.read_gatt_char(characteristic_id)
223 response = json.dumps({ 319 print('read-data',data)
320 response = json.dumps({
321 "jsonrpc": "2.0",
322 "result": {
323 "serviceId": service_id,
324 "characteristicId": characteristic_id,
325 "message": base64.b64encode(data).decode("utf-8")
326 },
327 "id": request_id
328 })
329 await log_message("下发", 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({
224 "jsonrpc": "2.0", 341 "jsonrpc": "2.0",
225 "result": { 342 "error": {"code": -1, "message": "未连接或参数不全"},
226 "serviceId": service_id,
227 "characteristicId": characteristic_id,
228 "message": base64.b64encode(data).decode("utf-8")
229 },
230 "id": request_id 343 "id": request_id
231 }) 344 })
232 await log_message("下发", response) 345 await log_message("下发", error_response)
233 await websocket.send(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:
244 response = json.dumps({ 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
366 response = json.dumps({
367 "jsonrpc": "2.0",
368 "result": None,
369 "id": request_id
370 })
371 await log_message("下发", 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({
245 "jsonrpc": "2.0", 383 "jsonrpc": "2.0",
246 "result": None, 384 "error": {"code": -1, "message": "未连接或参数不全"},
247 "id": request_id 385 "id": request_id
248 }) 386 })
249 await log_message("下发", response) 387 await log_message("下发", error_response)
250 await websocket.send(response) 388 await websocket.send(error_response)
251 389
252 elif method == "ping": 390 elif method == "ping":
253 # 处理ping请求,返回pong响应 391 # 处理ping请求,返回pong响应
...@@ -259,56 +397,104 @@ class BLEClient: ...@@ -259,56 +397,104 @@ 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:
281 await self.client.disconnect() 433 try:
282 self.client = None 434 # 停止所有通知
283 self.target_device = None 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 # 断开连接
442 await self.client.disconnect()
443 except Exception as e:
444 print(f"断开BLE连接失败: {e}")
445 finally:
446 self.client = 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:
288 last_message, last_time = self.notification_records[characteristic_id] 454 # 检查websocket是否已关闭
289 455 if self.websocket is None or self.websocket.closed or self._shutdown:
290 # 解码当前数据用于比较 456 return
291 current_message = base64.b64encode(data).decode('utf-8') 457
292 print('notification_handler current_message',base64.b64decode(current_message)) 458 current_time = time.time() # 使用time.time()而不是event_loop.time()
293 459 last_message, last_time = self.notification_records[characteristic_id]
294 # 过滤逻辑 460
295 if current_message == last_message and (current_time - last_time) < 0.5: 461 # 解码当前数据用于比较
296 return 462 current_message = base64.b64encode(data).decode('utf-8')
297 463 print('notification_handler current_message',data)
298 # 更新记录 464
299 self.notification_records[characteristic_id] = (current_message, current_time) 465 # 过滤逻辑 - 只在0.5秒内且消息完全相同时跳过
300 466 if current_message == last_message and (current_time - last_time) < 0.5:
301 response = json.dumps({ 467 return
302 "jsonrpc": "2.0", 468
303 "method": "characteristicDidChange", 469 # 更新记录
304 "params": { 470 self.notification_records[characteristic_id] = (current_message, current_time)
305 "serviceId": service_id, 471
306 "characteristicId": characteristic_id, 472 response = json.dumps({
307 "message": current_message 473 "jsonrpc": "2.0",
308 } 474 "method": "characteristicDidChange",
309 }) 475 "params": {
310 await log_message("下发", response) 476 "serviceId": service_id,
311 await websocket.send(response) 477 "characteristicId": characteristic_id,
478 "message": current_message
479 }
480 })
481
482 # 使用非阻塞日志
483 try:
484 await log_message("下发", response)
485 except Exception as e:
486 print(f"日志写入失败: {e}")
487
488 # 发送响应
489 try:
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:
346 await asyncio.Future() # 保持服务器运行 536 try:
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!