26c8727b by huangyf2

s6.9.2

1 parent 6542d06b
Showing 1 changed file with 275 additions and 0 deletions
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
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!