s6.9.2.py 47.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
# -*- coding: utf-8 -*-
"""
高性能BLE客户端 - 优化版本
修复了以下问题:
1. 内存泄漏 - 每次连接创建新实例导致资源无法释放
2. 日志阻塞 - 实现异步日志队列
3. 通知失效 - 完善连接状态检查和清理
4. WebSocket保活 - 添加ping/pong机制
5. 错误处理 - 全面增强异常捕获和处理
"""
import sys
import asyncio
import websockets
from bleak import BleakScanner, BleakClient
import json
import base64
import threading
from collections import defaultdict
import socket
import time
import traceback

import platform

# 方法1:通过sys模块快速判断(推荐)
if sys.platform.startswith('win32'):
    import winrt.windows.foundation.collections  # noqa
    import winrt.windows.devices.bluetooth  # noqa
    import winrt.windows.devices.bluetooth.advertisement  # noq
    print("当前运行在Windows系统")
elif sys.platform.startswith('linux'):
    print("当前运行在Linux系统")
elif sys.platform.startswith('darwin'):
    print("当前运行在macOS系统")
# 高性能日志系统
write_lock = threading.Lock()
_log_queue = None
_log_task = None
_log_dropped_count = 0

def log_message_sync(direction, message):
    """同步日志记录函数"""
    try:
        log_entry = f"{direction}: {message}\n"
        print(log_entry, end='')  # 控制台仍然输出
        
        # 截断过长消息,避免写入过大的日志
        if len(log_entry) > 4000:
            log_entry = log_entry[:4000] + "... [truncated]\n"
        
        with write_lock:
            with open('b.log', 'a', encoding='utf-8') as f:
                f.write(log_entry)
    except Exception:
        pass  # 日志失败不应影响主流程

async def _log_writer_loop():
    """后台日志写入循环"""
    global _log_queue
    while True:
        try:
            item = await _log_queue.get()
            if item is None:
                break
            direction, message = item
            log_message_sync(direction, message)
        except asyncio.CancelledError:
            break
        except Exception as e:
            await log_exception_async(e, "日志写入循环")
            await asyncio.sleep(0.05)

async def log_message(direction, message):
    """异步日志记录,非阻塞"""
    global _log_queue, _log_task, _log_dropped_count
    
    # 首次调用时初始化
    if _log_queue is None:
        _log_queue = asyncio.Queue(maxsize=1000)
        _log_task = asyncio.create_task(_log_writer_loop())
    
    try:
        _log_queue.put_nowait((direction, message))
    except asyncio.QueueFull:
        _log_dropped_count += 1
        if _log_dropped_count % 100 == 1:
            print(f"警告: 日志队列已满,已丢弃 {_log_dropped_count} 条日志")

async def log_exception_async(exc, context=""):
    """异步记录异常到日志"""
    try:
        exc_type = type(exc).__name__
        exc_msg = str(exc)
        exc_traceback = traceback.format_exc()
        error_msg = f"异常[{context}]: {exc_type}: {exc_msg}\n堆栈:\n{exc_traceback}"
        await log_message("异常", error_msg)
    except Exception:
        pass  # 记录异常失败时pass,避免循环异常

def log_exception_sync_to_async(exc, context=""):
    """同步函数中将异常放入异步日志队列"""
    try:
        exc_type = type(exc).__name__
        exc_msg = str(exc)
        exc_traceback = traceback.format_exc()
        error_msg = f"异常[{context}]: {exc_type}: {exc_msg}\n堆栈:\n{exc_traceback}"
        
        global _log_queue, _log_task
        if _log_queue is not None:
            try:
                _log_queue.put_nowait(("异常", error_msg))
            except asyncio.QueueFull:
                pass  # 队列满时pass
    except Exception:
        pass  # 记录异常失败时pass

def is_port_in_use(port, host='localhost'):
    """检查端口是否被占用"""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.bind((host, port))
            return False
        except socket.error:
            return True

async def self_test(websocket):
    """发送自检测试消息"""
    test_message = json.dumps({
        "jsonrpc": "2.0",
        "method": "ping",
        "params": {"timestamp": int(time.time())},
        "id": "test"
    })
    await log_message("自测", test_message)
    await websocket.send(test_message)
    
    try:
        # 等待响应,设置超时
        response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
        await log_message("自测响应", response)
        return True
    except asyncio.TimeoutError as e:
        await log_exception_async(e, "自测超时")
        pass
        return False
    except Exception as e:
        await log_exception_async(e, "自测")
        pass
        return False

class BLEClient:
    def __init__(self):
        self.target_device = None
        self.client = None
        self.services = []
        self.optional_services = []
        self.websocket = None
        self.notification_records = defaultdict(lambda: (None, 0.0))  # 特征ID: (最后消息, 时间戳)
        self._notification_callbacks = {}  # 存储通知回调,用于清理
        self._shutdown = False
        self._scanner = None
        self._connecting = False  # 连接状态标志
        self._connect_task = None  # 连接任务
        
        # 设备缓存机制 - 提升扫描速度和稳定性
        self._device_cache = {}  # {address: (device, adv_data, timestamp, rssi)}
        self._cache_timeout = 30.0  # 缓存30秒
        self._last_scan_time = 0
        self._cache_scan_interval = 5.0  # 5秒内复用缓存
        
        # 连接状态管理
        self._connection_history = {}  # {address: (success_time, last_attempt_time, success_count)}
        self._last_connected_address = None
        
        # 扫描优化
        self._best_rssi_device = None  # 信号最强的设备
        self._best_rssi_value = -999

    def on_disconnect(self, client):
        print("BLE连接断开,关闭WebSocket")
        # 设置关闭标志
        self._shutdown = True
        # 在事件循环中关闭WebSocket
        if self.websocket and not self.websocket.closed:
            try:
                # 尝试获取当前事件循环
                loop = asyncio.get_event_loop()
                if loop.is_running():
                    loop.create_task(self.close_websocket())
            except Exception as e:
                # 如果没有事件循环,直接设置为None(延迟清理)
                log_exception_sync_to_async(e, "on_disconnect获取事件循环")
                pass

    async def close_websocket(self):
        """主动关闭WebSocket连接"""
        if self.websocket:
            try:
                if not self.websocket.closed:
                    # 使用1000状态码正常关闭
                    await self.websocket.close(code=1000, reason="BLE connection closed")
                    print("WebSocket连接已关闭")
            except Exception as e:
                await log_exception_async(e, "关闭WebSocket")
                pass
            finally:
                self.websocket = None
                self._shutdown = True

    def detection_callback(self, device, advertisement_data):
        """设备发现回调函数,从广播包中匹配服务UUID,支持信号强度优化"""
        try:
            # 检查服务UUID匹配
            if not self.services or not any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services):
                return None
            
            # 严格从广播包中获取设备名称,优先使用local_name
            device_name = advertisement_data.local_name if advertisement_data.local_name else ""
            
            # 如果广播包中没有local_name,跳过该设备
            if not device_name or device_name.strip() == "":
                return None
            
            # 验证设备地址格式
            if not device.address or len(device.address) < 12:
                return None
            
            # 更新设备缓存(包含时间戳和RSSI)
            current_time = time.time()
            rssi = device.rssi if hasattr(device, 'rssi') else -100
            self._device_cache[device.address] = (device, advertisement_data, current_time, rssi)
            
            # 信号强度优化:选择信号最强的设备
            if rssi > self._best_rssi_value:
                self._best_rssi_value = rssi
                self._best_rssi_device = (device, advertisement_data)
            
            # 保存匹配的设备(优先使用信号最强的)
            self.target_device = (device, advertisement_data)
            
            if self.target_device:
                device, adv_data = self.target_device
                print("\n找到目标设备:")
                print(f"设备名称(从广播包local_name): {device_name}")
                print(f"设备地址: {device.address}")
                print(f"信号强度: {device.rssi} dBm")
                print("\n广播信息:")
                print(f"服务UUID列表: {adv_data.service_uuids}")
                print(f"制造商数据: {adv_data.manufacturer_data}")
                print(f"服务数据: {adv_data.service_data}")
                print(f"本地名称(local_name): {adv_data.local_name}")
                print(f"设备名称(device.name): {device.name if device.name else 'N/A'} (仅作参考,实际使用广播包名称)")
                # 发现设备后,尝试立即停止扫描
                try:
                    if self._scanner is not None:
                        # 在事件循环中停止扫描,避免阻塞回调
                        asyncio.create_task(self._stop_scan_early())
                except Exception as e:
                    log_exception_sync_to_async(e, "detection_callback停止扫描")
                    pass
            
            return self.target_device
        except Exception as e:
            log_exception_sync_to_async(e, "detection_callback")
            return None

    async def _stop_scan_early(self):
        try:
            if self._scanner is not None:
                await self._scanner.stop()
        except Exception as e:
            await log_exception_async(e, "_stop_scan_early")
            pass

    async def _background_connect(self, peripheral_id, request_id):
        """
        后台执行BLE连接任务
        策略:
        1. 验证设备地址格式
        2. 异步断开旧连接(不阻塞新连接)
        3. 执行新连接(带超时)
        4. 连接完成后发送状态通知
        """
        try:
            self._connecting = True
            
            # 步骤0: 验证设备地址格式
            if not peripheral_id or len(peripheral_id) < 12:
                error_msg = f"无效的设备地址: {peripheral_id}"
                await log_message("连接", error_msg)
                error_response = json.dumps({
                    "jsonrpc": "2.0",
                    "method": "connectionStatus",
                    "params": {
                        "connected": False,
                        "error": error_msg,
                        "peripheralId": peripheral_id
                    },
                    "id": request_id
                })
                await log_message("下发", error_response)
                if self.websocket and not self.websocket.closed:
                    await self.websocket.send(error_response)
                self._connecting = False
                return
            
            # 步骤1: 异步断开旧连接(最多等待1秒,超时后强制清理)
            old_client = self.client
            if old_client:
                try:
                    if old_client.is_connected:
                        # 设置较短的超时,快速失败
                        await asyncio.wait_for(old_client.disconnect(), timeout=1.0)
                        await log_message("连接", f"旧连接已断开: {peripheral_id}")
                    # 清理旧客户端引用
                    if old_client == self.client:
                        self.client = None
                except asyncio.TimeoutError:
                    await log_exception_async(
                        asyncio.TimeoutError("断开旧连接超时"),
                        "后台连接-断开旧连接超时"
                    )
                    # 强制清理,不等待
                    if old_client == self.client:
                        self.client = None
                    pass
                except Exception as e:
                    await log_exception_async(e, "后台连接-断开旧连接")
                    if old_client == self.client:
                        self.client = None
                    pass
            
            # 等待一小段时间,确保旧连接资源完全释放
            await asyncio.sleep(0.2)
            
            # 步骤2: 执行新连接
            # 注意:BLE连接包含多个阶段:
            # 1. 建立物理连接(通常1-2秒)
            # 2. 发现服务(get_services)(通常3-8秒,是主要瓶颈)
            # 因此总超时设置为8秒,确保大多数设备能完成连接
            client = None
            try:
                # BleakClient的超时只影响物理连接阶段
                client = BleakClient(peripheral_id, timeout=10.0)
                client.set_disconnected_callback(self.on_disconnect)
                
                # 连接操作,总超时8秒(覆盖物理连接+服务发现)
                await asyncio.wait_for(client.connect(), timeout=8.0)
                
                # 验证连接状态
                if not client.is_connected:
                    raise Exception("连接后状态检查失败:is_connected为False")
                
                # 再次验证连接确实可用(等待一小段时间后再次检查)
                await asyncio.sleep(0.1)
                if not client.is_connected:
                    raise Exception("连接后状态验证失败:连接已断开")
                
                # 连接成功,保存客户端引用
                self.client = client
                self._connecting = False
                client = None  # 避免finally中重复清理
                
                await log_message("连接", f"成功连接到设备: {peripheral_id}")
                
                # 发送连接成功通知
                success_response = json.dumps({
                    "jsonrpc": "2.0",
                    "method": "connectionStatus",
                    "params": {
                        "connected": True,
                        "peripheralId": peripheral_id
                    },
                    "id": request_id
                })
                await log_message("下发", success_response)
                if self.websocket and not self.websocket.closed:
                    await self.websocket.send(success_response)
                    
            except asyncio.TimeoutError:
                self._connecting = False
                error_msg = "连接BLE设备超时(8秒),可能是服务发现阶段耗时过长"
                await log_exception_async(
                    asyncio.TimeoutError(error_msg),
                    "后台连接-连接超时"
                )
                
                # 清理超时的client资源
                if client is not None:
                    try:
                        # 尝试断开连接,但不等待(快速清理)
                        asyncio.create_task(client.disconnect())
                    except Exception:
                        pass
                    client = None
                
                # 发送连接失败通知
                error_response = json.dumps({
                    "jsonrpc": "2.0",
                    "method": "connectionStatus",
                    "params": {
                        "connected": False,
                        "error": error_msg,
                        "peripheralId": peripheral_id
                    },
                    "id": request_id
                })
                await log_message("下发", error_response)
                if self.websocket and not self.websocket.closed:
                    await self.websocket.send(error_response)
                pass
                    
            except asyncio.CancelledError:
                # 任务被取消,清理资源
                self._connecting = False
                if client is not None:
                    try:
                        asyncio.create_task(client.disconnect())
                    except Exception:
                        pass
                    client = None
                # 被取消时不需要发送通知(可能是新的连接请求)
                pass
                    
            except Exception as e:
                self._connecting = False
                await log_exception_async(e, "后台连接-连接失败")
                
                # 清理异常的client资源
                if client is not None:
                    try:
                        asyncio.create_task(client.disconnect())
                    except Exception:
                        pass
                    client = None
                
                # 发送连接失败通知
                error_response = json.dumps({
                    "jsonrpc": "2.0",
                    "method": "connectionStatus",
                    "params": {
                        "connected": False,
                        "error": str(e),
                        "peripheralId": peripheral_id
                    },
                    "id": request_id
                })
                await log_message("下发", error_response)
                if self.websocket and not self.websocket.closed:
                    await self.websocket.send(error_response)
                pass
            finally:
                # 确保清理未使用的client资源
                if client is not None and client != self.client:
                    try:
                        # 异步断开,不阻塞
                        asyncio.create_task(client.disconnect())
                    except Exception:
                        pass
                
        except Exception as e:
            self._connecting = False
            await log_exception_async(e, "后台连接-未知错误")
            pass

    async def handle_client(self, websocket, path):
        self.websocket = websocket
        self._shutdown = False  # 重置关闭标志
        if path != "/scratch/ble":
            await websocket.close(code=1003, reason="Path not allowed")
            return

        try:
            async for message in websocket:
                # 检查是否应该关闭连接
                if self._shutdown:
                    print("检测到关闭标志,终止消息接收")
                    break
                    
                try:
                    await log_message("接收", message)
                    request = json.loads(message)
                    
                    if request["jsonrpc"] != "2.0":
                        continue

                    method = request.get("method")
                    params = request.get("params", {})
                    request_id = request.get("id")

                    if method == "discover":
                        self.services = []
                        for filt in params.get("filters", [{}]):
                            self.services.extend(filt.get("services", []))
                        self.optional_services = params.get("optionalServices", [])
                        
                        # 验证服务列表不为空
                        if not self.services:
                            error_response = json.dumps({
                                "jsonrpc": "2.0",
                                "error": {"code": -32602, "message": "Invalid params: services filter is required"},
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", error_response)
                            if not websocket.closed:
                                await websocket.send(error_response)
                            continue

                        # 优化策略1: 检查缓存,快速返回
                        current_time = time.time()
                        self._best_rssi_device = None
                        self._best_rssi_value = -999
                        
                        # 清理过期缓存
                        expired_addresses = [
                            addr for addr, (_, _, ts, _) in self._device_cache.items()
                            if current_time - ts > self._cache_timeout
                        ]
                        for addr in expired_addresses:
                            del self._device_cache[addr]
                        
                        # 如果缓存有效且时间间隔短,直接使用缓存
                        if (current_time - self._last_scan_time < self._cache_scan_interval and 
                            self._device_cache):
                            # 从缓存中选择信号最强的设备
                            best_cache = None
                            best_rssi = -999
                            for addr, (dev, adv, ts, rssi) in self._device_cache.items():
                                # 验证服务匹配
                                if any(service_uuid in adv.service_uuids for service_uuid in self.services):
                                    if adv.local_name and rssi > best_rssi:
                                        best_rssi = rssi
                                        best_cache = (dev, adv)
                            
                            if best_cache:
                                device, adv_data = best_cache
                                device_name = adv_data.local_name if adv_data.local_name else ""
                                if device_name:
                                    print(f"使用缓存设备: {device_name} ({device.address}), RSSI: {best_rssi} dBm")
                                    self.target_device = best_cache
                                    found = True
                        
                        # 如果缓存未命中,进行扫描
                        if not found:
                            # 双重扫描:快速扫描后若未发现,再进行扩展扫描;发现即停
                            phases = [("active", 3.0), ("passive", 6.0)]
                            scan_error = None
                        
                        for phase_index, (scan_mode, duration) in enumerate(phases, start=1):
                            self.target_device = None
                            self._scanner = None
                            
                            try:
                                # 创建扫描器
                                self._scanner = BleakScanner(scanning_mode=scan_mode)
                                self._scanner.register_detection_callback(self.detection_callback)
                                print(f"开始第{phase_index}阶段扫描(模式: {scan_mode}, 时长: {duration}s)...")
                                
                                # 启动扫描
                                try:
                                    await self._scanner.start()
                                except Exception as e:
                                    await log_exception_async(e, f"扫描启动失败-阶段{phase_index}")
                                    scan_error = str(e)
                                    continue
                                
                                # 轮询检查是否已找到,找到则提前停止
                                start_ts = time.time()
                                while time.time() - start_ts < duration and not self.target_device:
                                    await asyncio.sleep(0.1)
                                    
                                    # 检查是否应该关闭
                                    if self._shutdown:
                                        break
                                
                            except Exception as e:
                                await log_exception_async(e, f"扫描过程异常-阶段{phase_index}")
                                scan_error = str(e)
                            finally:
                                # 确保扫描器被停止和清理
                                if self._scanner is not None:
                                    try:
                                        await self._scanner.stop()
                                    except Exception as e:
                                        await log_exception_async(e, f"扫描停止失败-阶段{phase_index}")
                                        pass
                                    self._scanner = None
                                
                                # 等待一小段时间确保扫描完全停止
                                await asyncio.sleep(0.1)
                            
                            # 检查是否找到设备
                            if self.target_device:
                                found = True
                                break
                            else:
                                print(f"第{phase_index}阶段未找到设备")
                        
                        # 处理扫描结果
                        if found:
                            device, adv_data = self.target_device
                            # 严格从广播包中获取设备名称,使用local_name
                            device_name = adv_data.local_name if adv_data.local_name else ""
                            
                            # 确保从广播包获取到名字才发送discover通知
                            if device_name and device_name.strip():
                                discover_response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "method": "didDiscoverPeripheral",
                                    "params": {
                                        "name": device_name,  # 从广播包获取的名称
                                        "peripheralId": device.address,
                                        "rssi": device.rssi
                                    }
                                })
                                await log_message("下发", discover_response)
                                if not websocket.closed:
                                    await websocket.send(discover_response)
                            else:
                                print(f"警告: 设备 {device.address} 广播包中没有local_name,无法发送discover通知")

                            # 无论是否有名字,都返回result(表示扫描完成)
                            result_response = json.dumps({
                                "jsonrpc": "2.0",
                                "result": None,
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", result_response)
                            if not websocket.closed:
                                await websocket.send(result_response)
                        else:
                            # 未找到设备,返回错误
                            error_msg = "未找到匹配的蓝牙设备"
                            if scan_error:
                                error_msg += f" (扫描错误: {scan_error})"
                            
                            error_response = json.dumps({
                                "jsonrpc": "2.0",
                                "error": {"code": -1, "message": error_msg},
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", error_response)
                            if not websocket.closed:
                                await websocket.send(error_response)

                    elif method == "connect":
                        """
                        优化策略:
                        1. 立即返回"连接中"状态,不阻塞WebSocket请求处理
                        2. 在后台任务中执行连接操作
                        3. 连接完成后通过connectionStatus通知客户端
                        这样可以显著减少WebSocket响应时间,从10秒降低到<100ms
                        """
                        peripheral_id = params.get("peripheralId")
                        if peripheral_id:
                            # 检查是否已有连接任务在进行
                            if self._connecting:
                                # 已有连接任务,返回提示
                                response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id
                                })
                                await log_message("下发", response)
                                if not websocket.closed:
                                    await websocket.send(response)
                            else:
                                # 取消之前的连接任务(如果存在)
                                if self._connect_task and not self._connect_task.done():
                                    self._connect_task.cancel()
                                
                                # 立即返回"连接中"状态(不等待实际连接完成)
                                response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": {"connecting": True},
                                    "id": request_id
                                })
                                await log_message("下发", response)
                                if not websocket.closed:
                                    await websocket.send(response)
                                
                                # 在后台启动连接任务(不阻塞)
                                self._connect_task = asyncio.create_task(
                                    self._background_connect(peripheral_id, request_id)
                                )
                        else:
                            # 参数错误,立即返回
                            error_response = json.dumps({
                                "jsonrpc": "2.0",
                                "result": None,
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", error_response)
                            if not websocket.closed:
                                await websocket.send(error_response)

                    elif method == "write":
                        service_id = params.get("serviceId")
                        characteristic_id = params.get("characteristicId")
                        message = params.get("message")
                        encoding = params.get("encoding", "utf-8")

                        if all([service_id, characteristic_id, message]) and self.client and self.client.is_connected:
                            try:
                                if encoding == "base64":
                                    message_bytes = base64.b64decode(message)
                                    print("write message_bytes",message_bytes)
                                else:
                                    print("write message",message)
                                    message_bytes = message.encode(encoding)

                                await self.client.write_gatt_char(characteristic_id, message_bytes)
                                response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id
                                })
                                await log_message("下发", response)
                                if not websocket.closed:
                                    await websocket.send(response)
                            except Exception as e:
                                await log_exception_async(e, "write写入特征值")
                                error_response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id if request_id else 0
                                })
                                await log_message("下发", error_response)
                                if not websocket.closed:
                                    await websocket.send(error_response)
                                pass
                        else:
                            error_response = json.dumps({
                                "jsonrpc": "2.0",
                                "result": None,
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", error_response)
                            if not websocket.closed:
                                await websocket.send(error_response)

                    elif method == "read":
                        service_id = params.get("serviceId")
                        characteristic_id = params.get("characteristicId")

                        if all([service_id, characteristic_id]) and self.client and self.client.is_connected:
                            try:
                                # 添加读取超时,避免长时间阻塞
                                data = await asyncio.wait_for(
                                    self.client.read_gatt_char(characteristic_id),
                                    timeout=10.0
                                )
                                print('read-data',data)
                                response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": {
                                        "serviceId": service_id,
                                        "characteristicId": characteristic_id,
                                        "message": base64.b64encode(data).decode("utf-8")
                                    },
                                    "id": request_id
                                })
                                await log_message("下发", response)
                                # 检查WebSocket状态后再发送
                                if not websocket.closed:
                                    await websocket.send(response)
                            except asyncio.TimeoutError as e:
                                await log_exception_async(e, "read读取超时")
                                error_response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id if request_id else 0
                                })
                                await log_message("下发", error_response)
                                if not websocket.closed:
                                    await websocket.send(error_response)
                                pass
                            except Exception as e:
                                await log_exception_async(e, "read读取特征值")
                                error_response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id if request_id else 0
                                })
                                await log_message("下发", error_response)
                                # 检查WebSocket状态后再发送
                                if not websocket.closed:
                                    await websocket.send(error_response)
                                pass
                        else:
                            error_response = json.dumps({
                                "jsonrpc": "2.0",
                                "result": None,
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", error_response)
                            if not websocket.closed:
                                await websocket.send(error_response)

                    elif method == "startNotifications":
                        service_id = params.get("serviceId")
                        characteristic_id = params.get("characteristicId")

                        if all([service_id, characteristic_id]) and self.client and self.client.is_connected:
                            try:
                                # 如果已经监听,先停止旧的
                                if characteristic_id in self._notification_callbacks:
                                    try:
                                        await self.client.stop_notify(characteristic_id)
                                    except Exception as e:
                                        await log_exception_async(e, "startNotifications停止旧通知")
                                        pass
                                
                                # 创建新的通知处理器
                                callback = self.notification_handler(websocket, service_id, characteristic_id)
                                await self.client.start_notify(characteristic_id, callback)
                                self._notification_callbacks[characteristic_id] = callback
                                
                                response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id
                                })
                                await log_message("下发", response)
                                if not websocket.closed:
                                    await websocket.send(response)
                            except Exception as e:
                                await log_exception_async(e, "startNotifications启动通知")
                                error_response = json.dumps({
                                    "jsonrpc": "2.0",
                                    "result": None,
                                    "id": request_id if request_id else 0
                                })
                                await log_message("下发", error_response)
                                if not websocket.closed:
                                    await websocket.send(error_response)
                                pass
                        else:
                            error_response = json.dumps({
                                "jsonrpc": "2.0",
                                "result": None,
                                "id": request_id if request_id else 0
                            })
                            await log_message("下发", error_response)
                            if not websocket.closed:
                                await websocket.send(error_response)
                            
                    elif method == "ping":
                        # 处理ping请求,返回pong响应
                        response = json.dumps({
                            "jsonrpc": "2.0",
                            "result": {"pong": True, "timestamp": int(time.time())},
                            "id": request_id if request_id else 0
                        })
                        await log_message("下发", response)
                        if not websocket.closed:
                            await websocket.send(response)

                except json.JSONDecodeError as e:
                    await log_exception_async(e, "JSON解析")
                    error_msg = json.dumps({
                        "jsonrpc": "2.0",
                        "result": None,
                        "id": request.get("id") if request else 0
                    })
                    await log_message("下发", error_msg)
                    try:
                        if not websocket.closed:
                            await websocket.send(error_msg)
                    except Exception as e2:
                        await log_exception_async(e2, "JSON解析后发送响应")
                        pass
                        pass
                except Exception as e:
                    await log_exception_async(e, "处理请求")
                    error_msg = json.dumps({
                        "jsonrpc": "2.0",
                        "result": None,
                        "id": request.get("id") if request else 0
                    })
                    await log_message("下发", error_msg)
                    try:
                        if not websocket.closed:
                            await websocket.send(error_msg)
                    except Exception as e2:
                        await log_exception_async(e2, "处理请求后发送响应")
                        pass
                        pass

        except websockets.exceptions.ConnectionClosed as e:
            await log_exception_async(e, "WebSocket连接关闭")
            pass
        finally:
            # 清理BLE连接和通知
            self._shutdown = True
            self._connecting = False
            
            # 取消后台连接任务
            if self._connect_task and not self._connect_task.done():
                try:
                    self._connect_task.cancel()
                except Exception as e:
                    await log_exception_async(e, "finally取消连接任务")
                    pass
            
            if self.client and self.client.is_connected:
                try:
                    # 停止所有通知
                    for characteristic_id in list(self._notification_callbacks.keys()):
                        try:
                            await self.client.stop_notify(characteristic_id)
                        except Exception as e:
                            await log_exception_async(e, f"停止通知{characteristic_id}")
                            pass
                    
                    # 断开连接
                    await self.client.disconnect()
                except Exception as e:
                    await log_exception_async(e, "断开BLE连接")
                    pass
                finally:
                    self.client = None
                    self.target_device = None
                    self._notification_callbacks.clear()
                    self.notification_records.clear()

    def notification_handler(self, websocket, service_id, characteristic_id):
        async def callback(sender, data):
            try:
                # 检查websocket是否已关闭
                if self.websocket is None or self.websocket.closed or self._shutdown:
                    return
                
                current_time = time.time()  # 使用time.time()而不是event_loop.time()
                last_message, last_time = self.notification_records[characteristic_id]
                
                # 解码当前数据用于比较
                current_message = base64.b64encode(data).decode('utf-8')
                print('notification_handler current_message',data)
                
                # 过滤逻辑 - 只在0.5秒内且消息完全相同时跳过
                if current_message == last_message and (current_time - last_time) < 0.5:
                    return
                
                # 更新记录
                self.notification_records[characteristic_id] = (current_message, current_time)
                
                response = json.dumps({
                    "jsonrpc": "2.0",
                    "method": "characteristicDidChange",
                    "params": {
                        "serviceId": service_id,
                        "characteristicId": characteristic_id,
                        "message": current_message
                    }
                })
                
                # 使用非阻塞日志
                try:
                    await log_message("下发", response)
                except Exception as e:
                    await log_exception_async(e, "notification_handler日志写入")
                    pass
                
                # 发送响应
                try:
                    await websocket.send(response)
                except websockets.exceptions.ConnectionClosed as e:
                    await log_exception_async(e, "notification_handler发送通知连接关闭")
                    self._shutdown = True
                    pass
                except Exception as e:
                    await log_exception_async(e, "notification_handler发送通知")
                    pass
            except Exception as e:
                await log_exception_async(e, "notification_handler")
                pass
        return callback

async def check_port_and_start_server(port=20111, host='localhost'):
    """检查端口并启动服务器"""
    if is_port_in_use(port, host):
        print(f"错误: 端口 {port} 已被占用,无法启动服务")
        return False
    
    print(f"端口 {port} 可用,正在启动服务...")
    server = await websockets.serve(
        lambda websocket, path: BLEClient().handle_client(websocket, path),
        host, port,
        ping_interval=20,  # 每20秒发送ping
        ping_timeout=10,   # 等待pong超时10秒
        max_size=2 * 1024 * 1024,  # 最大消息大小2MB
        close_timeout=5,   # 关闭超时5秒
    )
    
    print(f"WebSocket服务已启动: ws://{host}:{port}/scratch/ble")
    print("日志文件路径: ./b.log")
    
    # 执行自检测试
    try:
        async with websockets.connect(f"ws://{host}:{port}/scratch/ble") as websocket:
            print("正在执行自检测试...")
            test_result = await self_test(websocket)
            if test_result:
                print("自检测试成功: 服务正常运行")
            else:
                print("自检测试失败: 服务可能存在问题")
    except Exception as e:
        await log_exception_async(e, "自检测试")
        pass
    
    return server

async def main():
    server = await check_port_and_start_server()
    if server:
        try:
            await asyncio.Future()  # 保持服务器运行
        finally:
            # 清理日志系统
            global _log_queue, _log_task
            if _log_queue is not None:
                try:
                    await _log_queue.put(None)
                except Exception as e:
                    await log_exception_async(e, "main清理日志队列")
                    pass
            if _log_task is not None:
                try:
                    await _log_task
                except Exception as e:
                    await log_exception_async(e, "main等待日志任务")
                    pass

if __name__ == "__main__":
    asyncio.run(main())