s6.9.2
Showing
1 changed file
with
275 additions
and
0 deletions
s6.9.2.py
0 → 100644
| 1 | # -*- coding: utf-8 -*- | ||
| 2 | import sys | ||
| 3 | import asyncio | ||
| 4 | import websockets | ||
| 5 | from bleak import BleakScanner, BleakClient | ||
| 6 | import json | ||
| 7 | import base64 | ||
| 8 | import threading | ||
| 9 | from collections import defaultdict | ||
| 10 | |||
| 11 | import platform | ||
| 12 | |||
| 13 | # 方法1:通过sys模块快速判断(推荐) | ||
| 14 | if sys.platform.startswith('win32'): | ||
| 15 | import winrt.windows.foundation.collections # noqa | ||
| 16 | import winrt.windows.devices.bluetooth # noqa | ||
| 17 | import winrt.windows.devices.bluetooth.advertisement # noq | ||
| 18 | print("当前运行在Windows系统") | ||
| 19 | elif sys.platform.startswith('linux'): | ||
| 20 | print("当前运行在Linux系统") | ||
| 21 | elif sys.platform.startswith('darwin'): | ||
| 22 | print("当前运行在macOS系统") | ||
| 23 | # 添加线程锁以确保日志写入的原子性 | ||
| 24 | write_lock = threading.Lock() | ||
| 25 | |||
| 26 | def log_message_sync(direction, message): | ||
| 27 | """同步日志记录函数""" | ||
| 28 | log_entry = f"{direction}: {message}\n" | ||
| 29 | print(log_entry, end='') # 控制台仍然输出 | ||
| 30 | with write_lock: | ||
| 31 | with open('b.log', 'a', encoding='utf-8') as f: | ||
| 32 | f.write(log_entry) | ||
| 33 | |||
| 34 | async def log_message(direction, message): | ||
| 35 | """异步封装日志记录""" | ||
| 36 | loop = asyncio.get_event_loop() | ||
| 37 | await loop.run_in_executor(None, log_message_sync, direction, message) | ||
| 38 | |||
| 39 | class BLEClient: | ||
| 40 | def __init__(self): | ||
| 41 | self.target_device = None | ||
| 42 | self.client = None | ||
| 43 | self.services = [] | ||
| 44 | self.optional_services = [] | ||
| 45 | self.websocket = None | ||
| 46 | self.notification_records = defaultdict(lambda: (None, 0)) # 特征ID: (最后消息, 时间戳) | ||
| 47 | |||
| 48 | def on_disconnect(self, client): | ||
| 49 | print("BLE连接断开,关闭WebSocket") | ||
| 50 | if self.websocket and not self.websocket.closed: | ||
| 51 | asyncio.create_task(self.close_websocket()) | ||
| 52 | |||
| 53 | async def close_websocket(self): | ||
| 54 | await self.websocket.close() | ||
| 55 | self.websocket = None | ||
| 56 | |||
| 57 | def detection_callback(self, device, advertisement_data): | ||
| 58 | if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services): | ||
| 59 | self.target_device = (device, advertisement_data) | ||
| 60 | if not self.target_device: | ||
| 61 | print("未找到匹配设备") | ||
| 62 | return | ||
| 63 | else: | ||
| 64 | device, adv_data = self.target_device | ||
| 65 | print("\n找到目标设备:") | ||
| 66 | print(f"设备名称: {device.name}") | ||
| 67 | print(f"设备地址: {device.address}") | ||
| 68 | print(f"信号强度: {device.rssi} dBm") | ||
| 69 | print("\n广播信息:") | ||
| 70 | print(f"服务UUID列表: {adv_data.service_uuids}") | ||
| 71 | print(f"制造商数据: {adv_data.manufacturer_data}") | ||
| 72 | print(f"服务数据: {adv_data.service_data}") | ||
| 73 | print(f"本地名称: {adv_data.local_name}") | ||
| 74 | return self.target_device | ||
| 75 | |||
| 76 | async def handle_client(self, websocket, path): | ||
| 77 | self.websocket = websocket | ||
| 78 | if path != "/scratch/ble": | ||
| 79 | await websocket.close(code=1003, reason="Path not allowed") | ||
| 80 | return | ||
| 81 | |||
| 82 | try: | ||
| 83 | async for message in websocket: | ||
| 84 | try: | ||
| 85 | await log_message("接收", message) | ||
| 86 | request = json.loads(message) | ||
| 87 | |||
| 88 | if request["jsonrpc"] != "2.0": | ||
| 89 | continue | ||
| 90 | |||
| 91 | method = request.get("method") | ||
| 92 | params = request.get("params", {}) | ||
| 93 | request_id = request.get("id") | ||
| 94 | |||
| 95 | if method == "discover": | ||
| 96 | self.services = [] | ||
| 97 | for filt in params.get("filters", [{}]): | ||
| 98 | self.services.extend(filt.get("services", [])) | ||
| 99 | self.optional_services = params.get("optionalServices", []) | ||
| 100 | |||
| 101 | scanner = BleakScanner(scanning_mode="active") | ||
| 102 | scanner.register_detection_callback(self.detection_callback) | ||
| 103 | |||
| 104 | max_retries = 3 | ||
| 105 | found = False | ||
| 106 | for attempt in range(max_retries): | ||
| 107 | self.target_device = None | ||
| 108 | await scanner.start() | ||
| 109 | await asyncio.sleep(5) | ||
| 110 | await scanner.stop() | ||
| 111 | |||
| 112 | if self.target_device: | ||
| 113 | found = True | ||
| 114 | break | ||
| 115 | |||
| 116 | if attempt < max_retries - 1: | ||
| 117 | print(f"未找到设备,第{attempt+1}次重试...") | ||
| 118 | await asyncio.sleep(3) | ||
| 119 | |||
| 120 | if found: | ||
| 121 | device, adv_data = self.target_device | ||
| 122 | discover_response = json.dumps({ | ||
| 123 | "jsonrpc": "2.0", | ||
| 124 | "method": "didDiscoverPeripheral", | ||
| 125 | "params": { | ||
| 126 | "name": device.name, | ||
| 127 | "peripheralId": device.address, | ||
| 128 | "rssi": device.rssi | ||
| 129 | } | ||
| 130 | }) | ||
| 131 | await log_message("下发", discover_response) | ||
| 132 | await websocket.send(discover_response) | ||
| 133 | |||
| 134 | result_response = json.dumps({ | ||
| 135 | "jsonrpc": "2.0", | ||
| 136 | "result": None, | ||
| 137 | "id": request_id | ||
| 138 | }) | ||
| 139 | await log_message("下发", result_response) | ||
| 140 | await websocket.send(result_response) | ||
| 141 | |||
| 142 | elif method == "connect": | ||
| 143 | peripheral_id = params.get("peripheralId") | ||
| 144 | if peripheral_id: | ||
| 145 | self.client = BleakClient(peripheral_id) | ||
| 146 | self.client.set_disconnected_callback(self.on_disconnect) | ||
| 147 | await self.client.connect() | ||
| 148 | |||
| 149 | if self.client.is_connected: | ||
| 150 | response = json.dumps({ | ||
| 151 | "jsonrpc": "2.0", | ||
| 152 | "result": None, | ||
| 153 | "id": request_id | ||
| 154 | }) | ||
| 155 | await log_message("下发", response) | ||
| 156 | await websocket.send(response) | ||
| 157 | |||
| 158 | elif method == "write": | ||
| 159 | service_id = params.get("serviceId") | ||
| 160 | characteristic_id = params.get("characteristicId") | ||
| 161 | message = params.get("message") | ||
| 162 | encoding = params.get("encoding", "utf-8") | ||
| 163 | |||
| 164 | if all([service_id, characteristic_id, message]): | ||
| 165 | if encoding == "base64": | ||
| 166 | message_bytes = base64.b64decode(message) | ||
| 167 | else: | ||
| 168 | message_bytes = message.encode(encoding) | ||
| 169 | |||
| 170 | await self.client.write_gatt_char(characteristic_id, message_bytes) | ||
| 171 | response = json.dumps({ | ||
| 172 | "jsonrpc": "2.0", | ||
| 173 | "result": None, | ||
| 174 | "id": request_id | ||
| 175 | }) | ||
| 176 | await log_message("下发", response) | ||
| 177 | await websocket.send(response) | ||
| 178 | |||
| 179 | elif method == "read": | ||
| 180 | service_id = params.get("serviceId") | ||
| 181 | characteristic_id = params.get("characteristicId") | ||
| 182 | |||
| 183 | if all([service_id, characteristic_id]): | ||
| 184 | data = await self.client.read_gatt_char(characteristic_id) | ||
| 185 | response = json.dumps({ | ||
| 186 | "jsonrpc": "2.0", | ||
| 187 | "result": { | ||
| 188 | "serviceId": service_id, | ||
| 189 | "characteristicId": characteristic_id, | ||
| 190 | "message": base64.b64encode(data).decode("utf-8") | ||
| 191 | }, | ||
| 192 | "id": request_id | ||
| 193 | }) | ||
| 194 | await log_message("下发", response) | ||
| 195 | await websocket.send(response) | ||
| 196 | |||
| 197 | elif method == "startNotifications": | ||
| 198 | service_id = params.get("serviceId") | ||
| 199 | characteristic_id = params.get("characteristicId") | ||
| 200 | |||
| 201 | if all([service_id, characteristic_id]): | ||
| 202 | await self.client.start_notify( | ||
| 203 | characteristic_id, | ||
| 204 | self.notification_handler(websocket, service_id, characteristic_id) | ||
| 205 | ) | ||
| 206 | response = json.dumps({ | ||
| 207 | "jsonrpc": "2.0", | ||
| 208 | "result": None, | ||
| 209 | "id": request_id | ||
| 210 | }) | ||
| 211 | await log_message("下发", response) | ||
| 212 | await websocket.send(response) | ||
| 213 | |||
| 214 | except json.JSONDecodeError: | ||
| 215 | error_msg = json.dumps({ | ||
| 216 | "jsonrpc": "2.0", | ||
| 217 | "result": {"message": "Parse error"}, | ||
| 218 | "id": None | ||
| 219 | }) | ||
| 220 | await log_message("下发", error_msg) | ||
| 221 | except Exception as e: | ||
| 222 | error_msg = json.dumps({ | ||
| 223 | "jsonrpc": "2.0", | ||
| 224 | "result": {"message": str(e)}, | ||
| 225 | "id": request.get("id") if request else None | ||
| 226 | }) | ||
| 227 | await log_message("下发", error_msg) | ||
| 228 | |||
| 229 | except websockets.exceptions.ConnectionClosed: | ||
| 230 | print("WebSocket连接关闭") | ||
| 231 | finally: | ||
| 232 | if self.client and self.client.is_connected: | ||
| 233 | await self.client.disconnect() | ||
| 234 | self.client = None | ||
| 235 | self.target_device = None | ||
| 236 | |||
| 237 | def notification_handler(self, websocket, service_id, characteristic_id): | ||
| 238 | async def callback(sender, data): | ||
| 239 | current_time = asyncio.get_event_loop().time() | ||
| 240 | last_message, last_time = self.notification_records[characteristic_id] | ||
| 241 | |||
| 242 | # 解码当前数据用于比较 | ||
| 243 | current_message = base64.b64encode(data).decode('utf-8') | ||
| 244 | |||
| 245 | # 过滤逻辑 | ||
| 246 | if current_message == last_message and (current_time - last_time) < 0.5: | ||
| 247 | return | ||
| 248 | |||
| 249 | # 更新记录 | ||
| 250 | self.notification_records[characteristic_id] = (current_message, current_time) | ||
| 251 | |||
| 252 | response = json.dumps({ | ||
| 253 | "jsonrpc": "2.0", | ||
| 254 | "method": "characteristicDidChange", | ||
| 255 | "params": { | ||
| 256 | "serviceId": service_id, | ||
| 257 | "characteristicId": characteristic_id, | ||
| 258 | "message": current_message | ||
| 259 | } | ||
| 260 | }) | ||
| 261 | await log_message("下发", response) | ||
| 262 | await websocket.send(response) | ||
| 263 | return callback | ||
| 264 | |||
| 265 | async def main(): | ||
| 266 | async with websockets.serve( | ||
| 267 | lambda websocket, path: BLEClient().handle_client(websocket, path), | ||
| 268 | "localhost", 20111 | ||
| 269 | ): | ||
| 270 | print("WebSocket服务已启动: ws://localhost:20111/scratch/ble") | ||
| 271 | print("日志文件路径: ./b.log") | ||
| 272 | await asyncio.Future() | ||
| 273 | |||
| 274 | if __name__ == "__main__": | ||
| 275 | asyncio.run(main()) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment