aa901ba8 by huangyf2

6.8.2.py

1 parent 54ac76a2
1 import asyncio
2 import websockets
3 from bleak import BleakScanner, BleakClient
4 import json
5 import base64
6 import threading
7 from collections import defaultdict
8
9 # 添加线程锁以确保日志写入的原子性
10 write_lock = threading.Lock()
11
12 def log_message_sync(direction, message):
13 """同步日志记录函数"""
14 log_entry = f"{direction}: {message}\n"
15 print(log_entry, end='') # 控制台仍然输出
16 with write_lock:
17 with open('b.log', 'a', encoding='utf-8') as f:
18 f.write(log_entry)
19
20 async def log_message(direction, message):
21 """异步封装日志记录"""
22 loop = asyncio.get_event_loop()
23 await loop.run_in_executor(None, log_message_sync, direction, message)
24
25 class BLEClient:
26 def __init__(self):
27 self.target_device = None
28 self.client = None
29 self.services = []
30 self.optional_services = []
31 self.websocket = None
32 self.notification_records = defaultdict(lambda: (None, 0)) # 特征ID: (最后消息, 时间戳)
33
34 def on_disconnect(self, client):
35 print("BLE连接断开,关闭WebSocket")
36 if self.websocket and not self.websocket.closed:
37 asyncio.create_task(self.close_websocket())
38
39 async def close_websocket(self):
40 await self.websocket.close()
41 self.websocket = None
42
43 def detection_callback(self, device, advertisement_data):
44 if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services):
45 self.target_device = (device, advertisement_data)
46 if not self.target_device:
47 print("未找到匹配设备")
48 return
49 else:
50 device, adv_data = self.target_device
51 print("\n找到目标设备:")
52 print(f"设备名称: {device.name}")
53 print(f"设备地址: {device.address}")
54 print(f"信号强度: {device.rssi} dBm")
55 print("\n广播信息:")
56 print(f"服务UUID列表: {adv_data.service_uuids}")
57 print(f"制造商数据: {adv_data.manufacturer_data}")
58 print(f"服务数据: {adv_data.service_data}")
59 print(f"本地名称: {adv_data.local_name}")
60 return self.target_device
61
62 async def handle_client(self, websocket, path):
63 self.websocket = websocket
64 if path != "/scratch/ble":
65 await websocket.close(code=1003, reason="Path not allowed")
66 return
67
68 try:
69 async for message in websocket:
70 try:
71 await log_message("接收", message)
72 request = json.loads(message)
73
74 if request["jsonrpc"] != "2.0":
75 continue
76
77 method = request.get("method")
78 params = request.get("params", {})
79 request_id = request.get("id")
80
81 if method == "discover":
82 self.services = []
83 for filt in params.get("filters", [{}]):
84 self.services.extend(filt.get("services", []))
85 self.optional_services = params.get("optionalServices", [])
86
87 scanner = BleakScanner()
88 scanner.register_detection_callback(self.detection_callback)
89
90 max_retries = 3
91 found = False
92 for attempt in range(max_retries):
93 self.target_device = None
94 await scanner.start()
95 await asyncio.sleep(5)
96 await scanner.stop()
97
98 if self.target_device:
99 found = True
100 break
101
102 if attempt < max_retries - 1:
103 print(f"未找到设备,第{attempt+1}次重试...")
104 await asyncio.sleep(3)
105
106 if found:
107 device, adv_data = self.target_device
108 discover_response = json.dumps({
109 "jsonrpc": "2.0",
110 "method": "didDiscoverPeripheral",
111 "params": {
112 "name": device.name,
113 "peripheralId": device.address,
114 "rssi": device.rssi
115 }
116 })
117 await log_message("下发", discover_response)
118 await websocket.send(discover_response)
119
120 result_response = json.dumps({
121 "jsonrpc": "2.0",
122 "result": None,
123 "id": request_id
124 })
125 await log_message("下发", result_response)
126 await websocket.send(result_response)
127
128 elif method == "connect":
129 peripheral_id = params.get("peripheralId")
130 if peripheral_id:
131 self.client = BleakClient(peripheral_id)
132 self.client.set_disconnected_callback(self.on_disconnect)
133 await self.client.connect()
134
135 if self.client.is_connected:
136 response = json.dumps({
137 "jsonrpc": "2.0",
138 "result": None,
139 "id": request_id
140 })
141 await log_message("下发", response)
142 await websocket.send(response)
143
144 elif method == "write":
145 service_id = params.get("serviceId")
146 characteristic_id = params.get("characteristicId")
147 message = params.get("message")
148 encoding = params.get("encoding", "utf-8")
149
150 if all([service_id, characteristic_id, message]):
151 if encoding == "base64":
152 message_bytes = base64.b64decode(message)
153 else:
154 message_bytes = message.encode(encoding)
155 if "withResponse" in params:
156 response=params.get("withResponse")
157 await self.client.write_gatt_char(characteristic_id, message_bytes,response=False)
158 else:
159 await self.client.write_gatt_char(characteristic_id, message_bytes)
160
161 # await self.client.write_gatt_char(characteristic_id, message_bytes)
162 response = json.dumps({
163 "jsonrpc": "2.0",
164 "result": None,
165 "id": request_id
166 })
167 await log_message("下发", response)
168 await websocket.send(response)
169
170 elif method == "read":
171 service_id = params.get("serviceId")
172 characteristic_id = params.get("characteristicId")
173
174 if all([service_id, characteristic_id]):
175 data = await self.client.read_gatt_char(characteristic_id)
176 response = json.dumps({
177 "jsonrpc": "2.0",
178 "result": {
179 "serviceId": service_id,
180 "characteristicId": characteristic_id,
181 "message": base64.b64encode(data).decode("utf-8")
182 },
183 "id": request_id
184 })
185 await log_message("下发", response)
186 await websocket.send(response)
187
188 elif method == "startNotifications":
189 service_id = params.get("serviceId")
190 characteristic_id = params.get("characteristicId")
191
192 if all([service_id, characteristic_id]):
193 await self.client.start_notify(
194 characteristic_id,
195 self.notification_handler(websocket, service_id, characteristic_id)
196 )
197 response = json.dumps({
198 "jsonrpc": "2.0",
199 "result": None,
200 "id": request_id
201 })
202 await log_message("下发", response)
203 await websocket.send(response)
204
205 except json.JSONDecodeError:
206 error_msg = json.dumps({
207 "jsonrpc": "2.0",
208 "result": {"message": "Parse error"},
209 "id": None
210 })
211 await log_message("下发", error_msg)
212 except Exception as e:
213 error_msg = json.dumps({
214 "jsonrpc": "2.0",
215 "result": {"message": str(e)},
216 "id": request.get("id") if request else None
217 })
218 await log_message("下发", error_msg)
219
220 except websockets.exceptions.ConnectionClosed:
221 print("WebSocket连接关闭")
222 finally:
223 if self.client and self.client.is_connected:
224 await self.client.disconnect()
225 self.client = None
226 self.target_device = None
227
228 def notification_handler(self, websocket, service_id, characteristic_id):
229 async def callback(sender, data):
230 current_time = asyncio.get_event_loop().time()
231 last_message, last_time = self.notification_records[characteristic_id]
232
233 # 解码当前数据用于比较
234 current_message = base64.b64encode(data).decode('utf-8')
235
236 # 过滤逻辑
237 if current_message == last_message and (current_time - last_time) < 0.5:
238 return
239
240 # 更新记录
241 self.notification_records[characteristic_id] = (current_message, current_time)
242
243 response = json.dumps({
244 "jsonrpc": "2.0",
245 "method": "characteristicDidChange",
246 "params": {
247 "serviceId": service_id,
248 "characteristicId": characteristic_id,
249 "message": current_message
250 }
251 })
252 await log_message("下发", response)
253 await websocket.send(response)
254 return callback
255
256 async def main():
257 async with websockets.serve(
258 lambda websocket, path: BLEClient().handle_client(websocket, path),
259 "localhost", 20111
260 ):
261 print("WebSocket服务已启动: ws://localhost:20111/scratch/ble")
262 print("日志文件路径: ./b.log")
263 await asyncio.Future()
264
265 if __name__ == "__main__":
266 asyncio.run(main())
...\ No newline at end of file ...\ No newline at end of file
1 # -*- coding: utf-8 -*-
2 import sys
3 import asyncio
4 import websockets
5 import json
6 import base64
7 import threading
8 from collections import defaultdict
9 import socket
10 import time
11 import platform
12 from typing import Optional, Dict, List, Callable, Any
13
14 # Windows 蓝牙 API 导入
15 if sys.platform.startswith('win32'):
16 import winrt.windows.foundation.collections as wfc
17 import winrt.windows.devices.bluetooth as bt
18 import winrt.windows.devices.bluetooth.advertisement as bt_adv
19 import winrt.windows.devices.bluetooth.genericattributeprofile as gatt
20 import winrt.windows.devices.enumeration as de
21 import winrt.windows.storage.streams as streams
22 import winrt.windows.foundation as wf
23 print("当前运行在Windows系统,使用 pywinrt 蓝牙 API")
24 USE_PYWINRT = True
25 else:
26 # 非 Windows 系统回退到 bleak
27 from bleak import BleakScanner, BleakClient
28 print("当前运行在非Windows系统,使用 bleak 蓝牙库")
29 USE_PYWINRT = False
30
31 # 添加线程锁以确保日志写入的原子性
32 write_lock = threading.Lock()
33
34 def log_message_sync(direction, message):
35 """同步日志记录函数"""
36 log_entry = f"{direction}: {message}\n"
37 print(log_entry, end='') # 控制台仍然输出
38 with write_lock:
39 with open('b.log', 'a', encoding='utf-8') as f:
40 f.write(log_entry)
41
42 async def log_message(direction, message):
43 """异步封装日志记录"""
44 loop = asyncio.get_event_loop()
45 await loop.run_in_executor(None, log_message_sync, direction, message)
46
47 def is_port_in_use(port, host='localhost'):
48 """检查端口是否被占用"""
49 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
50 try:
51 s.bind((host, port))
52 return False
53 except socket.error:
54 return True
55
56 async def self_test(websocket):
57 """发送自检测试消息"""
58 test_message = json.dumps({
59 "jsonrpc": "2.0",
60 "method": "ping",
61 "params": {"timestamp": int(time.time())},
62 "id": "test"
63 })
64 await log_message("自测", test_message)
65 await websocket.send(test_message)
66
67 try:
68 # 等待响应,设置超时
69 response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
70 await log_message("自测响应", response)
71 return True
72 except asyncio.TimeoutError:
73 await log_message("自测", "自测超时,未收到响应")
74 return False
75 except Exception as e:
76 await log_message("自测", f"自测异常: {str(e)}")
77 return False
78
79 class BLEClient:
80 def __init__(self):
81 self.target_device = None
82 self.client = None
83 self.services = []
84 self.optional_services = []
85 self.websocket = None
86 self.notification_records = defaultdict(lambda: (None, 0)) # 特征ID: (最后消息, 时间戳)
87
88 # pywinrt 相关属性
89 self.device_watcher = None
90 self.bluetooth_device = None
91 self.gatt_device_service = None
92 self.characteristics = {}
93 self.notification_handlers = {}
94
95 # 设备发现回调
96 self.device_found_callback = None
97 self.scan_timeout = 10.0
98 self.connection_timeout = 10.0
99 self.operation_timeout = 5.0
100
101 # 连接状态管理
102 self.connection_state = {
103 'status': 'disconnected', # disconnected, connecting, connected, disconnecting, error
104 'device_id': None,
105 'device_name': None,
106 'connection_time': None,
107 'last_activity': None,
108 'error_count': 0,
109 'retry_count': 0,
110 'max_retries': 3,
111 'connection_quality': 'unknown', # excellent, good, fair, poor, unknown
112 'signal_strength': None,
113 'services_discovered': False,
114 'characteristics_discovered': False,
115 'notifications_active': set(),
116 'connection_history': [],
117 'error_history': []
118 }
119
120 # 状态变化回调
121 self.state_change_callbacks = []
122 self.connection_monitor_task = None
123 self.auto_reconnect = False
124 self.auto_reconnect_interval = 5.0
125
126 def on_disconnect(self, client):
127 print("BLE连接断开,关闭WebSocket")
128 if self.websocket and not self.websocket.closed:
129 asyncio.create_task(self.close_websocket())
130
131 async def close_websocket(self):
132 if self.websocket:
133 await self.websocket.close()
134 self.websocket = None
135
136 async def start_pywinrt_scan(self, target_services: List[str] = None) -> Optional[Dict]:
137 """使用 pywinrt 进行蓝牙设备扫描"""
138 if not USE_PYWINRT:
139 raise RuntimeError("pywinrt 仅在 Windows 系统上可用")
140
141 try:
142 # 创建设备观察器
143 aqs_filter = "System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\""
144 self.device_watcher = de.DeviceWatcher.create_from_aqs_filter(aqs_filter)
145
146 discovered_devices = []
147
148 def on_device_added(sender, device_info):
149 try:
150 device_name = device_info.name or "未知设备"
151 device_id = device_info.id
152 device_kind = device_info.kind
153
154 print(f"发现设备: {device_name}, ID: {device_id}")
155
156 # 检查是否包含目标服务
157 if target_services:
158 # 这里可以添加更复杂的服务过滤逻辑
159 pass
160
161 discovered_devices.append({
162 'name': device_name,
163 'id': device_id,
164 'kind': device_kind,
165 'info': device_info
166 })
167
168 except Exception as e:
169 print(f"处理设备信息时出错: {e}")
170
171 def on_device_updated(sender, device_info):
172 print(f"设备更新: {device_info.name}")
173
174 def on_device_removed(sender, device_info):
175 print(f"设备移除: {device_info.name}")
176
177 def on_enumeration_completed(sender, args):
178 print("设备枚举完成")
179
180 def on_stopped(sender, args):
181 print("设备扫描停止")
182
183 # 注册事件处理器
184 self.device_watcher.added += on_device_added
185 self.device_watcher.updated += on_device_updated
186 self.device_watcher.removed += on_device_removed
187 self.device_watcher.enumeration_completed += on_enumeration_completed
188 self.device_watcher.stopped += on_stopped
189
190 # 开始扫描
191 print("开始扫描蓝牙设备...")
192 self.device_watcher.start()
193
194 # 等待扫描完成或超时
195 start_time = time.time()
196 while (self.device_watcher.status != de.DeviceWatcherStatus.STOPPED and
197 time.time() - start_time < self.scan_timeout):
198 await asyncio.sleep(0.1)
199
200 # 停止扫描
201 if self.device_watcher.status == de.DeviceWatcherStatus.STARTED:
202 self.device_watcher.stop()
203 # 等待停止完成
204 while self.device_watcher.status != de.DeviceWatcherStatus.STOPPED:
205 await asyncio.sleep(0.1)
206
207 print(f"扫描完成,发现 {len(discovered_devices)} 个设备")
208 return discovered_devices
209
210 except Exception as e:
211 print(f"pywinrt 扫描出错: {e}")
212 return None
213
214 async def connect_pywinrt_device(self, device_id: str) -> bool:
215 """使用 pywinrt 连接蓝牙设备"""
216 if not USE_PYWINRT:
217 raise RuntimeError("pywinrt 仅在 Windows 系统上可用")
218
219 try:
220 print(f"正在连接设备: {device_id}")
221 self.update_connection_state('connecting', device_id=device_id)
222
223 # 从设备ID获取蓝牙设备
224 self.bluetooth_device = await bt.BluetoothLEDevice.from_id_async(device_id)
225
226 if not self.bluetooth_device:
227 print("无法获取蓝牙设备对象")
228 self.record_error('device_not_found', f'无法获取设备对象: {device_id}')
229 self.update_connection_state('error')
230 return False
231
232 # 检查连接状态
233 if self.bluetooth_device.connection_status == bt.BluetoothConnectionStatus.CONNECTED:
234 print("设备已连接")
235 self.update_connection_state('connected',
236 device_id=device_id,
237 connection_time=time.time(),
238 services_discovered=False,
239 characteristics_discovered=False)
240 return True
241
242 # 等待连接建立
243 start_time = time.time()
244
245 while (self.bluetooth_device.connection_status != bt.BluetoothConnectionStatus.CONNECTED and
246 time.time() - start_time < self.connection_timeout):
247 await asyncio.sleep(0.1)
248
249 if self.bluetooth_device.connection_status == bt.BluetoothConnectionStatus.CONNECTED:
250 print("设备连接成功")
251 self.update_connection_state('connected',
252 device_id=device_id,
253 connection_time=time.time(),
254 services_discovered=False,
255 characteristics_discovered=False)
256
257 # 连接成功后自动发现服务
258 try:
259 await self.discover_services_pywinrt()
260 self.update_connection_state('connected', services_discovered=True)
261 except Exception as e:
262 print(f"服务发现失败: {e}")
263 self.record_error('service_discovery_failed', str(e))
264
265 return True
266 else:
267 print("设备连接失败")
268 self.record_error('connection_timeout', f'连接超时: {device_id}')
269 self.update_connection_state('error')
270 return False
271
272 except Exception as e:
273 print(f"连接设备时出错: {e}")
274 self.record_error('connection_exception', str(e))
275 self.update_connection_state('error')
276 return False
277
278 async def discover_services_pywinrt(self) -> List[Dict]:
279 """使用 pywinrt 发现设备服务"""
280 if not USE_PYWINRT or not self.bluetooth_device:
281 return []
282
283 try:
284 services = []
285 gatt_result = await self.bluetooth_device.get_gatt_services_async()
286
287 if gatt_result.status == gatt.GattCommunicationStatus.SUCCESS:
288 for service in gatt_result.services:
289 service_info = {
290 'uuid': str(service.uuid),
291 'service': service
292 }
293 services.append(service_info)
294 print(f"发现服务: {service.uuid}")
295
296 return services
297
298 except Exception as e:
299 print(f"发现服务时出错: {e}")
300 return []
301
302 async def discover_characteristics_pywinrt(self, service_uuid: str) -> List[Dict]:
303 """使用 pywinrt 发现特征"""
304 if not USE_PYWINRT or not self.bluetooth_device:
305 return []
306
307 try:
308 # 获取服务
309 gatt_result = await self.bluetooth_device.get_gatt_services_async()
310 if gatt_result.status != gatt.GattCommunicationStatus.SUCCESS:
311 return []
312
313 target_service = None
314 for service in gatt_result.services:
315 if str(service.uuid) == service_uuid:
316 target_service = service
317 break
318
319 if not target_service:
320 print(f"未找到服务: {service_uuid}")
321 return []
322
323 # 获取特征
324 characteristics = []
325 char_result = await target_service.get_characteristics_async()
326
327 if char_result.status == gatt.GattCommunicationStatus.SUCCESS:
328 for char in char_result.characteristics:
329 char_info = {
330 'uuid': str(char.uuid),
331 'characteristic': char,
332 'properties': {
333 'read': char.characteristic_properties & gatt.GattCharacteristicProperties.READ != 0,
334 'write': char.characteristic_properties & gatt.GattCharacteristicProperties.WRITE != 0,
335 'notify': char.characteristic_properties & gatt.GattCharacteristicProperties.NOTIFY != 0,
336 'indicate': char.characteristic_properties & gatt.GattCharacteristicProperties.INDICATE != 0
337 }
338 }
339 characteristics.append(char_info)
340 self.characteristics[str(char.uuid)] = char
341 print(f"发现特征: {char.uuid}, 属性: {char_info['properties']}")
342
343 return characteristics
344
345 except Exception as e:
346 print(f"发现特征时出错: {e}")
347 return []
348
349 async def write_characteristic_pywinrt(self, characteristic_uuid: str, data: bytes) -> bool:
350 """使用 pywinrt 写入特征值"""
351 if not USE_PYWINRT or not self.bluetooth_device:
352 return False
353
354 try:
355 if characteristic_uuid not in self.characteristics:
356 print(f"特征未找到: {characteristic_uuid}")
357 return False
358
359 characteristic = self.characteristics[characteristic_uuid]
360
361 # 创建数据写入器
362 writer = streams.DataWriter()
363 writer.write_bytes(data)
364 buffer = writer.detach_buffer()
365
366 # 写入数据
367 result = await characteristic.write_value_async(buffer)
368
369 if result == gatt.GattCommunicationStatus.SUCCESS:
370 print(f"成功写入特征 {characteristic_uuid}: {data}")
371 return True
372 else:
373 print(f"写入特征失败: {result}")
374 return False
375
376 except Exception as e:
377 print(f"写入特征时出错: {e}")
378 return False
379
380 async def read_characteristic_pywinrt(self, characteristic_uuid: str) -> Optional[bytes]:
381 """使用 pywinrt 读取特征值"""
382 if not USE_PYWINRT or not self.bluetooth_device:
383 return None
384
385 try:
386 if characteristic_uuid not in self.characteristics:
387 print(f"特征未找到: {characteristic_uuid}")
388 return None
389
390 characteristic = self.characteristics[characteristic_uuid]
391
392 # 读取数据
393 result = await characteristic.read_value_async()
394
395 if result.status == gatt.GattCommunicationStatus.SUCCESS:
396 # 读取数据
397 reader = streams.DataReader.from_buffer(result.value)
398 data = bytearray(reader.unconsumed_buffer_length)
399 reader.read_bytes(data)
400 print(f"成功读取特征 {characteristic_uuid}: {bytes(data)}")
401 return bytes(data)
402 else:
403 print(f"读取特征失败: {result.status}")
404 return None
405
406 except Exception as e:
407 print(f"读取特征时出错: {e}")
408 return None
409
410 async def start_notifications_pywinrt(self, characteristic_uuid: str, callback: Callable) -> bool:
411 """使用 pywinrt 启动通知"""
412 if not USE_PYWINRT or not self.bluetooth_device:
413 return False
414
415 try:
416 if characteristic_uuid not in self.characteristics:
417 print(f"特征未找到: {characteristic_uuid}")
418 return False
419
420 characteristic = self.characteristics[characteristic_uuid]
421
422 # 检查特征是否支持通知
423 if not (characteristic.characteristic_properties & gatt.GattCharacteristicProperties.NOTIFY):
424 print(f"特征 {characteristic_uuid} 不支持通知")
425 return False
426
427 # 设置通知回调
428 def notification_handler(sender, args):
429 try:
430 # 读取通知数据
431 reader = streams.DataReader.from_buffer(args.characteristic_value)
432 data = bytearray(reader.unconsumed_buffer_length)
433 reader.read_bytes(data)
434
435 # 调用用户回调
436 asyncio.create_task(callback(bytes(data)))
437
438 except Exception as e:
439 print(f"处理通知时出错: {e}")
440
441 # 订阅通知
442 characteristic.value_changed += notification_handler
443 result = await characteristic.write_client_characteristic_configuration_descriptor_async(
444 gatt.GattClientCharacteristicConfigurationDescriptorValue.NOTIFY
445 )
446
447 if result == gatt.GattCommunicationStatus.SUCCESS:
448 self.notification_handlers[characteristic_uuid] = notification_handler
449 print(f"成功启动特征 {characteristic_uuid} 的通知")
450 return True
451 else:
452 print(f"启动通知失败: {result}")
453 return False
454
455 except Exception as e:
456 print(f"启动通知时出错: {e}")
457 return False
458
459 async def stop_notifications_pywinrt(self, characteristic_uuid: str) -> bool:
460 """使用 pywinrt 停止通知"""
461 if not USE_PYWINRT or not self.bluetooth_device:
462 return False
463
464 try:
465 if characteristic_uuid not in self.characteristics:
466 print(f"特征未找到: {characteristic_uuid}")
467 return False
468
469 characteristic = self.characteristics[characteristic_uuid]
470
471 # 取消订阅通知
472 result = await characteristic.write_client_characteristic_configuration_descriptor_async(
473 gatt.GattClientCharacteristicConfigurationDescriptorValue.NONE
474 )
475
476 if result == gatt.GattCommunicationStatus.SUCCESS:
477 # 移除事件处理器
478 if characteristic_uuid in self.notification_handlers:
479 characteristic.value_changed -= self.notification_handlers[characteristic_uuid]
480 del self.notification_handlers[characteristic_uuid]
481
482 print(f"成功停止特征 {characteristic_uuid} 的通知")
483 return True
484 else:
485 print(f"停止通知失败: {result}")
486 return False
487
488 except Exception as e:
489 print(f"停止通知时出错: {e}")
490 return False
491
492 async def disconnect_pywinrt(self):
493 """使用 pywinrt 断开连接"""
494 if not USE_PYWINRT or not self.bluetooth_device:
495 return
496
497 try:
498 print("正在断开 pywinrt 蓝牙连接...")
499 self.update_connection_state('disconnecting')
500
501 # 停止所有通知
502 for char_uuid in list(self.notification_handlers.keys()):
503 try:
504 await self.stop_notifications_pywinrt(char_uuid)
505 except Exception as e:
506 print(f"停止通知 {char_uuid} 时出错: {e}")
507
508 # 清理资源
509 self.characteristics.clear()
510 self.notification_handlers.clear()
511 self.bluetooth_device = None
512
513 # 更新状态
514 self.update_connection_state('disconnected',
515 device_id=None,
516 device_name=None,
517 connection_time=None,
518 services_discovered=False,
519 characteristics_discovered=False,
520 notifications_active=set())
521
522 print("已断开 pywinrt 蓝牙连接")
523
524 except Exception as e:
525 print(f"断开连接时出错: {e}")
526 self.record_error('disconnect_exception', str(e))
527 self.update_connection_state('error')
528
529 def is_connected_pywinrt(self) -> bool:
530 """检查 pywinrt 连接状态"""
531 if not USE_PYWINRT or not self.bluetooth_device:
532 return False
533 return self.bluetooth_device.connection_status == bt.BluetoothConnectionStatus.CONNECTED
534
535 def set_timeouts(self, scan_timeout: float = None, connection_timeout: float = None, operation_timeout: float = None):
536 """设置超时时间"""
537 if scan_timeout is not None:
538 self.scan_timeout = scan_timeout
539 if connection_timeout is not None:
540 self.connection_timeout = connection_timeout
541 if operation_timeout is not None:
542 self.operation_timeout = operation_timeout
543
544 def update_connection_state(self, status: str, **kwargs):
545 """更新连接状态"""
546 old_status = self.connection_state['status']
547 self.connection_state['status'] = status
548 self.connection_state['last_activity'] = time.time()
549
550 # 更新其他状态信息
551 for key, value in kwargs.items():
552 if key in self.connection_state:
553 self.connection_state[key] = value
554
555 # 记录状态变化历史
556 state_change = {
557 'timestamp': time.time(),
558 'old_status': old_status,
559 'new_status': status,
560 'details': kwargs
561 }
562 self.connection_state['connection_history'].append(state_change)
563
564 # 保持历史记录在合理范围内
565 if len(self.connection_state['connection_history']) > 100:
566 self.connection_state['connection_history'] = self.connection_state['connection_history'][-50:]
567
568 # 触发状态变化回调
569 for callback in self.state_change_callbacks:
570 try:
571 callback(old_status, status, kwargs)
572 except Exception as e:
573 print(f"状态变化回调出错: {e}")
574
575 print(f"连接状态变化: {old_status} -> {status}")
576
577 def add_state_change_callback(self, callback: Callable):
578 """添加状态变化回调"""
579 self.state_change_callbacks.append(callback)
580
581 def remove_state_change_callback(self, callback: Callable):
582 """移除状态变化回调"""
583 if callback in self.state_change_callbacks:
584 self.state_change_callbacks.remove(callback)
585
586 def get_connection_state(self) -> Dict:
587 """获取当前连接状态"""
588 return self.connection_state.copy()
589
590 def get_connection_quality(self) -> str:
591 """评估连接质量"""
592 if not self.is_connected():
593 return 'disconnected'
594
595 # 基于错误计数和重试次数评估连接质量
596 error_count = self.connection_state['error_count']
597 retry_count = self.connection_state['retry_count']
598
599 if error_count == 0 and retry_count == 0:
600 return 'excellent'
601 elif error_count <= 2 and retry_count <= 1:
602 return 'good'
603 elif error_count <= 5 and retry_count <= 2:
604 return 'fair'
605 else:
606 return 'poor'
607
608 def is_connected(self) -> bool:
609 """检查是否已连接"""
610 if USE_PYWINRT:
611 return (self.connection_state['status'] == 'connected' and
612 self.bluetooth_device and
613 self.bluetooth_device.connection_status == bt.BluetoothConnectionStatus.CONNECTED)
614 else:
615 return (self.connection_state['status'] == 'connected' and
616 self.client and
617 self.client.is_connected)
618
619 def is_connecting(self) -> bool:
620 """检查是否正在连接"""
621 return self.connection_state['status'] == 'connecting'
622
623 def is_disconnecting(self) -> bool:
624 """检查是否正在断开连接"""
625 return self.connection_state['status'] == 'disconnecting'
626
627 def has_error(self) -> bool:
628 """检查是否有错误"""
629 return self.connection_state['status'] == 'error'
630
631 def get_connection_duration(self) -> float:
632 """获取连接持续时间(秒)"""
633 if self.connection_state['connection_time']:
634 return time.time() - self.connection_state['connection_time']
635 return 0.0
636
637 def get_last_activity_duration(self) -> float:
638 """获取最后活动时间(秒)"""
639 if self.connection_state['last_activity']:
640 return time.time() - self.connection_state['last_activity']
641 return 0.0
642
643 def record_error(self, error_type: str, error_message: str):
644 """记录错误"""
645 self.connection_state['error_count'] += 1
646 error_record = {
647 'timestamp': time.time(),
648 'type': error_type,
649 'message': error_message,
650 'status': self.connection_state['status']
651 }
652 self.connection_state['error_history'].append(error_record)
653
654 # 保持错误历史在合理范围内
655 if len(self.connection_state['error_history']) > 50:
656 self.connection_state['error_history'] = self.connection_state['error_history'][-25:]
657
658 # 更新连接质量
659 self.connection_state['connection_quality'] = self.get_connection_quality()
660
661 print(f"记录错误: {error_type} - {error_message}")
662
663 def reset_error_count(self):
664 """重置错误计数"""
665 self.connection_state['error_count'] = 0
666 self.connection_state['retry_count'] = 0
667 self.connection_state['connection_quality'] = self.get_connection_quality()
668
669 async def start_connection_monitor(self):
670 """启动连接监控"""
671 if self.connection_monitor_task:
672 return
673
674 self.connection_monitor_task = asyncio.create_task(self._connection_monitor_loop())
675
676 async def stop_connection_monitor(self):
677 """停止连接监控"""
678 if self.connection_monitor_task:
679 self.connection_monitor_task.cancel()
680 try:
681 await self.connection_monitor_task
682 except asyncio.CancelledError:
683 pass
684 self.connection_monitor_task = None
685
686 async def _connection_monitor_loop(self):
687 """连接监控循环"""
688 while True:
689 try:
690 await asyncio.sleep(1.0) # 每秒检查一次
691
692 if self.connection_state['status'] == 'connected':
693 # 检查连接是否仍然有效
694 if not self.is_connected():
695 print("检测到连接丢失")
696 self.update_connection_state('error', error_type='connection_lost')
697
698 # 自动重连
699 if self.auto_reconnect:
700 await self._attempt_auto_reconnect()
701
702 elif self.connection_state['status'] == 'error' and self.auto_reconnect:
703 # 尝试自动重连
704 await self._attempt_auto_reconnect()
705
706 except asyncio.CancelledError:
707 break
708 except Exception as e:
709 print(f"连接监控出错: {e}")
710 await asyncio.sleep(5.0) # 出错时等待更长时间
711
712 async def _attempt_auto_reconnect(self):
713 """尝试自动重连"""
714 if self.connection_state['retry_count'] >= self.connection_state['max_retries']:
715 print("达到最大重试次数,停止自动重连")
716 return
717
718 device_id = self.connection_state['device_id']
719 if not device_id:
720 print("没有设备ID,无法自动重连")
721 return
722
723 print(f"尝试自动重连 (第{self.connection_state['retry_count'] + 1}次)")
724 self.connection_state['retry_count'] += 1
725
726 await asyncio.sleep(self.auto_reconnect_interval)
727
728 if USE_PYWINRT:
729 success = await self.connect_pywinrt_device(device_id)
730 else:
731 # 非 Windows 系统的重连逻辑
732 if self.client:
733 await self.client.connect()
734 success = self.client.is_connected
735
736 if success:
737 print("自动重连成功")
738 self.reset_error_count()
739 else:
740 print("自动重连失败")
741 self.record_error('auto_reconnect_failed', f'重连尝试 {self.connection_state["retry_count"]} 失败')
742
743 def detection_callback(self, device, advertisement_data):
744 if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services):
745 self.target_device = (device, advertisement_data)
746 if not self.target_device:
747 print("未找到匹配设备")
748 return
749 else:
750 device, adv_data = self.target_device
751 print("\n找到目标设备:")
752 print(f"设备名称: {device.name}")
753 print(f"设备地址: {device.address}")
754 print(f"信号强度: {device.rssi} dBm")
755 print("\n广播信息:")
756 print(f"服务UUID列表: {adv_data.service_uuids}")
757 print(f"制造商数据: {adv_data.manufacturer_data}")
758 print(f"服务数据: {adv_data.service_data}")
759 print(f"本地名称: {adv_data.local_name}")
760 return self.target_device
761
762 async def handle_client(self, websocket, path):
763 self.websocket = websocket
764 if path != "/scratch/ble":
765 await websocket.close(code=1003, reason="Path not allowed")
766 return
767
768 try:
769 async for message in websocket:
770 try:
771 await log_message("接收", message)
772 request = json.loads(message)
773
774 if request["jsonrpc"] != "2.0":
775 continue
776
777 method = request.get("method")
778 params = request.get("params", {})
779 request_id = request.get("id")
780
781 if method == "discover":
782 self.services = []
783 for filt in params.get("filters", [{}]):
784 self.services.extend(filt.get("services", []))
785 self.optional_services = params.get("optionalServices", [])
786
787 if USE_PYWINRT:
788 # 使用 pywinrt 进行设备发现
789 discovered_devices = await self.start_pywinrt_scan(self.services)
790
791 if discovered_devices:
792 # 选择第一个设备(可以根据需要修改选择逻辑)
793 device_info = discovered_devices[0]
794
795 discover_response = json.dumps({
796 "jsonrpc": "2.0",
797 "method": "didDiscoverPeripheral",
798 "params": {
799 "name": device_info['name'],
800 "peripheralId": device_info['id'],
801 "rssi": -50 # pywinrt 不直接提供 RSSI,使用默认值
802 }
803 })
804 await log_message("下发", discover_response)
805 await websocket.send(discover_response)
806
807 result_response = json.dumps({
808 "jsonrpc": "2.0",
809 "result": None,
810 "id": request_id
811 })
812 await log_message("下发", result_response)
813 await websocket.send(result_response)
814 else:
815 # 未找到设备
816 error_response = json.dumps({
817 "jsonrpc": "2.0",
818 "error": {"code": -1, "message": "未找到匹配的蓝牙设备"},
819 "id": request_id
820 })
821 await log_message("下发", error_response)
822 await websocket.send(error_response)
823 else:
824 # 使用 bleak 进行设备发现(非 Windows 系统)
825 scanner = BleakScanner(scanning_mode="active")
826 scanner.register_detection_callback(self.detection_callback)
827
828 max_retries = 3
829 found = False
830 for attempt in range(max_retries):
831 self.target_device = None
832 await scanner.start()
833 await asyncio.sleep(5)
834 await scanner.stop()
835
836 if self.target_device:
837 found = True
838 break
839
840 if attempt < max_retries - 1:
841 print(f"未找到设备,第{attempt+1}次重试...")
842 await asyncio.sleep(3)
843
844 if found:
845 device, adv_data = self.target_device
846 discover_response = json.dumps({
847 "jsonrpc": "2.0",
848 "method": "didDiscoverPeripheral",
849 "params": {
850 "name": device.name,
851 "peripheralId": device.address,
852 "rssi": device.rssi
853 }
854 })
855 await log_message("下发", discover_response)
856 await websocket.send(discover_response)
857
858 result_response = json.dumps({
859 "jsonrpc": "2.0",
860 "result": None,
861 "id": request_id
862 })
863 await log_message("下发", result_response)
864 await websocket.send(result_response)
865 else:
866 error_response = json.dumps({
867 "jsonrpc": "2.0",
868 "error": {"code": -1, "message": "未找到匹配的蓝牙设备"},
869 "id": request_id
870 })
871 await log_message("下发", error_response)
872 await websocket.send(error_response)
873
874 elif method == "connect":
875 peripheral_id = params.get("peripheralId")
876 if peripheral_id:
877 if USE_PYWINRT:
878 # 使用 pywinrt 连接
879 success = await self.connect_pywinrt_device(peripheral_id)
880 if success:
881 response = json.dumps({
882 "jsonrpc": "2.0",
883 "result": None,
884 "id": request_id
885 })
886 await log_message("下发", response)
887 await websocket.send(response)
888 else:
889 error_response = json.dumps({
890 "jsonrpc": "2.0",
891 "error": {"code": -1, "message": "连接设备失败"},
892 "id": request_id
893 })
894 await log_message("下发", error_response)
895 await websocket.send(error_response)
896 else:
897 # 使用 bleak 连接
898 self.client = BleakClient(peripheral_id)
899 self.client.set_disconnected_callback(self.on_disconnect)
900 await self.client.connect()
901
902 if self.client.is_connected:
903 response = json.dumps({
904 "jsonrpc": "2.0",
905 "result": None,
906 "id": request_id
907 })
908 await log_message("下发", response)
909 await websocket.send(response)
910 else:
911 error_response = json.dumps({
912 "jsonrpc": "2.0",
913 "error": {"code": -1, "message": "连接设备失败"},
914 "id": request_id
915 })
916 await log_message("下发", error_response)
917 await websocket.send(error_response)
918
919 elif method == "write":
920 service_id = params.get("serviceId")
921 characteristic_id = params.get("characteristicId")
922 message = params.get("message")
923 encoding = params.get("encoding", "utf-8")
924
925 if all([service_id, characteristic_id, message]):
926 if encoding == "base64":
927 message_bytes = base64.b64decode(message)
928 print("write message_bytes", message_bytes)
929 else:
930 print("write message", message)
931 message_bytes = message.encode(encoding)
932
933 if USE_PYWINRT:
934 # 使用 pywinrt 写入
935 success = await self.write_characteristic_pywinrt(characteristic_id, message_bytes)
936 if success:
937 response = json.dumps({
938 "jsonrpc": "2.0",
939 "result": None,
940 "id": request_id
941 })
942 await log_message("下发", response)
943 await websocket.send(response)
944 else:
945 error_response = json.dumps({
946 "jsonrpc": "2.0",
947 "error": {"code": -1, "message": "写入特征失败"},
948 "id": request_id
949 })
950 await log_message("下发", error_response)
951 await websocket.send(error_response)
952 else:
953 # 使用 bleak 写入
954 await self.client.write_gatt_char(characteristic_id, message_bytes)
955 response = json.dumps({
956 "jsonrpc": "2.0",
957 "result": None,
958 "id": request_id
959 })
960 await log_message("下发", response)
961 await websocket.send(response)
962
963 elif method == "read":
964 service_id = params.get("serviceId")
965 characteristic_id = params.get("characteristicId")
966
967 if all([service_id, characteristic_id]):
968 if USE_PYWINRT:
969 # 使用 pywinrt 读取
970 data = await self.read_characteristic_pywinrt(characteristic_id)
971 if data is not None:
972 print('read-data', data)
973 response = json.dumps({
974 "jsonrpc": "2.0",
975 "result": {
976 "serviceId": service_id,
977 "characteristicId": characteristic_id,
978 "message": base64.b64encode(data).decode("utf-8")
979 },
980 "id": request_id
981 })
982 await log_message("下发", response)
983 await websocket.send(response)
984 else:
985 error_response = json.dumps({
986 "jsonrpc": "2.0",
987 "error": {"code": -1, "message": "读取特征失败"},
988 "id": request_id
989 })
990 await log_message("下发", error_response)
991 await websocket.send(error_response)
992 else:
993 # 使用 bleak 读取
994 data = await self.client.read_gatt_char(characteristic_id)
995 print('read-data', data)
996 response = json.dumps({
997 "jsonrpc": "2.0",
998 "result": {
999 "serviceId": service_id,
1000 "characteristicId": characteristic_id,
1001 "message": base64.b64encode(data).decode("utf-8")
1002 },
1003 "id": request_id
1004 })
1005 await log_message("下发", response)
1006 await websocket.send(response)
1007
1008 elif method == "startNotifications":
1009 service_id = params.get("serviceId")
1010 characteristic_id = params.get("characteristicId")
1011
1012 if all([service_id, characteristic_id]):
1013 if USE_PYWINRT:
1014 # 使用 pywinrt 启动通知
1015 async def pywinrt_notification_callback(data: bytes):
1016 await self.pywinrt_notification_handler(websocket, service_id, characteristic_id, data)
1017
1018 success = await self.start_notifications_pywinrt(characteristic_id, pywinrt_notification_callback)
1019 if success:
1020 response = json.dumps({
1021 "jsonrpc": "2.0",
1022 "result": None,
1023 "id": request_id
1024 })
1025 await log_message("下发", response)
1026 await websocket.send(response)
1027 else:
1028 error_response = json.dumps({
1029 "jsonrpc": "2.0",
1030 "error": {"code": -1, "message": "启动通知失败"},
1031 "id": request_id
1032 })
1033 await log_message("下发", error_response)
1034 await websocket.send(error_response)
1035 else:
1036 # 使用 bleak 启动通知
1037 await self.client.start_notify(
1038 characteristic_id,
1039 self.notification_handler(websocket, service_id, characteristic_id)
1040 )
1041 response = json.dumps({
1042 "jsonrpc": "2.0",
1043 "result": None,
1044 "id": request_id
1045 })
1046 await log_message("下发", response)
1047 await websocket.send(response)
1048
1049 elif method == "discoverServices":
1050 # 发现设备服务
1051 if USE_PYWINRT and self.bluetooth_device:
1052 services = await self.discover_services_pywinrt()
1053 response = json.dumps({
1054 "jsonrpc": "2.0",
1055 "result": {"services": [s['uuid'] for s in services]},
1056 "id": request_id
1057 })
1058 await log_message("下发", response)
1059 await websocket.send(response)
1060 else:
1061 error_response = json.dumps({
1062 "jsonrpc": "2.0",
1063 "error": {"code": -1, "message": "服务发现功能仅在 Windows 系统上可用"},
1064 "id": request_id
1065 })
1066 await log_message("下发", error_response)
1067 await websocket.send(error_response)
1068
1069 elif method == "discoverCharacteristics":
1070 # 发现服务特征
1071 service_id = params.get("serviceId")
1072 if service_id and USE_PYWINRT and self.bluetooth_device:
1073 characteristics = await self.discover_characteristics_pywinrt(service_id)
1074 response = json.dumps({
1075 "jsonrpc": "2.0",
1076 "result": {
1077 "characteristics": [
1078 {
1079 "uuid": char['uuid'],
1080 "properties": char['properties']
1081 } for char in characteristics
1082 ]
1083 },
1084 "id": request_id
1085 })
1086 await log_message("下发", response)
1087 await websocket.send(response)
1088 else:
1089 error_response = json.dumps({
1090 "jsonrpc": "2.0",
1091 "error": {"code": -1, "message": "特征发现功能仅在 Windows 系统上可用"},
1092 "id": request_id
1093 })
1094 await log_message("下发", error_response)
1095 await websocket.send(error_response)
1096
1097 elif method == "disconnect":
1098 # 断开连接
1099 if USE_PYWINRT:
1100 await self.disconnect_pywinrt()
1101 else:
1102 if self.client and self.client.is_connected:
1103 await self.client.disconnect()
1104 self.client = None
1105
1106 response = json.dumps({
1107 "jsonrpc": "2.0",
1108 "result": None,
1109 "id": request_id
1110 })
1111 await log_message("下发", response)
1112 await websocket.send(response)
1113
1114 elif method == "getConnectionStatus":
1115 # 获取连接状态
1116 if USE_PYWINRT:
1117 is_connected = self.is_connected_pywinrt()
1118 else:
1119 is_connected = self.client and self.client.is_connected
1120
1121 response = json.dumps({
1122 "jsonrpc": "2.0",
1123 "result": {"connected": is_connected},
1124 "id": request_id
1125 })
1126 await log_message("下发", response)
1127 await websocket.send(response)
1128
1129 elif method == "setTimeouts":
1130 # 设置超时时间
1131 scan_timeout = params.get("scanTimeout")
1132 connection_timeout = params.get("connectionTimeout")
1133 operation_timeout = params.get("operationTimeout")
1134
1135 self.set_timeouts(scan_timeout, connection_timeout, operation_timeout)
1136
1137 response = json.dumps({
1138 "jsonrpc": "2.0",
1139 "result": {
1140 "scanTimeout": self.scan_timeout,
1141 "connectionTimeout": self.connection_timeout,
1142 "operationTimeout": self.operation_timeout
1143 },
1144 "id": request_id
1145 })
1146 await log_message("下发", response)
1147 await websocket.send(response)
1148
1149 elif method == "ping":
1150 # 处理ping请求,返回pong响应
1151 response = json.dumps({
1152 "jsonrpc": "2.0",
1153 "result": {"pong": True, "timestamp": int(time.time())},
1154 "id": request_id
1155 })
1156 await log_message("下发", response)
1157 await websocket.send(response)
1158
1159 except json.JSONDecodeError:
1160 error_msg = json.dumps({
1161 "jsonrpc": "2.0",
1162 "result": {"message": "Parse error"},
1163 "id": None
1164 })
1165 await log_message("下发", error_msg)
1166 except Exception as e:
1167 error_msg = json.dumps({
1168 "jsonrpc": "2.0",
1169 "result": {"message": str(e)},
1170 "id": request.get("id") if request else None
1171 })
1172 await log_message("下发", error_msg)
1173
1174 except websockets.exceptions.ConnectionClosed:
1175 print("WebSocket连接关闭")
1176 finally:
1177 # 清理连接
1178 if USE_PYWINRT:
1179 await self.disconnect_pywinrt()
1180 else:
1181 if self.client and self.client.is_connected:
1182 await self.client.disconnect()
1183 self.client = None
1184 self.target_device = None
1185
1186 async def pywinrt_notification_handler(self, websocket, service_id, characteristic_id, data: bytes):
1187 """pywinrt 通知处理器"""
1188 try:
1189 current_time = asyncio.get_event_loop().time()
1190 last_message, last_time = self.notification_records[characteristic_id]
1191
1192 # 解码当前数据用于比较
1193 current_message = base64.b64encode(data).decode('utf-8')
1194 print('pywinrt_notification_handler current_message', data)
1195
1196 # 过滤逻辑
1197 if current_message == last_message and (current_time - last_time) < 0.5:
1198 return
1199
1200 # 更新记录
1201 self.notification_records[characteristic_id] = (current_message, current_time)
1202
1203 response = json.dumps({
1204 "jsonrpc": "2.0",
1205 "method": "characteristicDidChange",
1206 "params": {
1207 "serviceId": service_id,
1208 "characteristicId": characteristic_id,
1209 "message": current_message
1210 }
1211 })
1212 await log_message("下发", response)
1213 await websocket.send(response)
1214
1215 except Exception as e:
1216 print(f"pywinrt 通知处理出错: {e}")
1217
1218 def notification_handler(self, websocket, service_id, characteristic_id):
1219 async def callback(sender, data):
1220 current_time = asyncio.get_event_loop().time()
1221 last_message, last_time = self.notification_records[characteristic_id]
1222
1223 # 解码当前数据用于比较
1224 current_message = base64.b64encode(data).decode('utf-8')
1225 print('notification_handler current_message',base64.b64decode(current_message))
1226
1227 # 过滤逻辑
1228 if current_message == last_message and (current_time - last_time) < 0.5:
1229 return
1230
1231 # 更新记录
1232 self.notification_records[characteristic_id] = (current_message, current_time)
1233
1234 response = json.dumps({
1235 "jsonrpc": "2.0",
1236 "method": "characteristicDidChange",
1237 "params": {
1238 "serviceId": service_id,
1239 "characteristicId": characteristic_id,
1240 "message": current_message
1241 }
1242 })
1243 await log_message("下发", response)
1244 await websocket.send(response)
1245 return callback
1246
1247 async def check_port_and_start_server(port=20111, host='localhost'):
1248 """检查端口并启动服务器"""
1249 if is_port_in_use(port, host):
1250 print(f"错误: 端口 {port} 已被占用,无法启动服务")
1251 return False
1252
1253 print(f"端口 {port} 可用,正在启动服务...")
1254 server = await websockets.serve(
1255 lambda websocket, path: BLEClient().handle_client(websocket, path),
1256 host, port
1257 )
1258
1259 print(f"WebSocket服务已启动: ws://{host}:{port}/scratch/ble")
1260 print("日志文件路径: ./b.log")
1261
1262 # 执行自检测试
1263 try:
1264 async with websockets.connect(f"ws://{host}:{port}/scratch/ble") as websocket:
1265 print("正在执行自检测试...")
1266 test_result = await self_test(websocket)
1267 if test_result:
1268 print("自检测试成功: 服务正常运行")
1269 else:
1270 print("自检测试失败: 服务可能存在问题")
1271 except Exception as e:
1272 print(f"自检测试异常: {str(e)}")
1273
1274 return server
1275
1276 async def main():
1277 server = await check_port_and_start_server()
1278 if server:
1279 await asyncio.Future() # 保持服务器运行
1280
1281 if __name__ == "__main__":
1282 asyncio.run(main())
...\ No newline at end of file ...\ No newline at end of file
...@@ -186,7 +186,7 @@ class BLEClient: ...@@ -186,7 +186,7 @@ class BLEClient:
186 "result": None, 186 "result": None,
187 "id": request_id 187 "id": request_id
188 }) 188 })
189 await log_message("下发", response) 189 # await log_message("下发", response)
190 await websocket.send(response) 190 await websocket.send(response)
191 191
192 elif method == "write": 192 elif method == "write":
...@@ -198,9 +198,12 @@ class BLEClient: ...@@ -198,9 +198,12 @@ class BLEClient:
198 if all([service_id, characteristic_id, message]): 198 if all([service_id, characteristic_id, message]):
199 if encoding == "base64": 199 if encoding == "base64":
200 message_bytes = base64.b64decode(message) 200 message_bytes = base64.b64decode(message)
201 print("write message_bytes",message_bytes)
201 else: 202 else:
203 print("write message",message)
202 message_bytes = message.encode(encoding) 204 message_bytes = message.encode(encoding)
203 205
206
204 await self.client.write_gatt_char(characteristic_id, message_bytes) 207 await self.client.write_gatt_char(characteristic_id, message_bytes)
205 response = json.dumps({ 208 response = json.dumps({
206 "jsonrpc": "2.0", 209 "jsonrpc": "2.0",
...@@ -216,6 +219,7 @@ class BLEClient: ...@@ -216,6 +219,7 @@ class BLEClient:
216 219
217 if all([service_id, characteristic_id]): 220 if all([service_id, characteristic_id]):
218 data = await self.client.read_gatt_char(characteristic_id) 221 data = await self.client.read_gatt_char(characteristic_id)
222 print('read-data',base64.decode(data))
219 response = json.dumps({ 223 response = json.dumps({
220 "jsonrpc": "2.0", 224 "jsonrpc": "2.0",
221 "result": { 225 "result": {
...@@ -285,6 +289,7 @@ class BLEClient: ...@@ -285,6 +289,7 @@ class BLEClient:
285 289
286 # 解码当前数据用于比较 290 # 解码当前数据用于比较
287 current_message = base64.b64encode(data).decode('utf-8') 291 current_message = base64.b64encode(data).decode('utf-8')
292 print('notification_handler current_message',base64.b64decode(current_message))
288 293
289 # 过滤逻辑 294 # 过滤逻辑
290 if current_message == last_message and (current_time - last_time) < 0.5: 295 if current_message == last_message and (current_time - last_time) < 0.5:
......
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 import socket
11 import time
12
13 import platform
14
15 # 方法1:通过sys模块快速判断(推荐)
16 if sys.platform.startswith('win32'):
17 import winrt.windows.foundation.collections # noqa
18 import winrt.windows.devices.bluetooth # noqa
19 import winrt.windows.devices.bluetooth.advertisement # noq
20 print("当前运行在Windows系统")
21 elif sys.platform.startswith('linux'):
22 print("当前运行在Linux系统")
23 elif sys.platform.startswith('darwin'):
24 print("当前运行在macOS系统")
25 # 添加线程锁以确保日志写入的原子性
26 write_lock = threading.Lock()
27
28 def log_message_sync(direction, message):
29 """同步日志记录函数"""
30 log_entry = f"{direction}: {message}\n"
31 print(log_entry, end='') # 控制台仍然输出
32 with write_lock:
33 with open('b.log', 'a', encoding='utf-8') as f:
34 f.write(log_entry)
35
36 async def log_message(direction, message):
37 """异步封装日志记录"""
38 loop = asyncio.get_event_loop()
39 await loop.run_in_executor(None, log_message_sync, direction, message)
40
41 def is_port_in_use(port, host='localhost'):
42 """检查端口是否被占用"""
43 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
44 try:
45 s.bind((host, port))
46 return False
47 except socket.error:
48 return True
49
50 async def self_test(websocket):
51 """发送自检测试消息"""
52 test_message = json.dumps({
53 "jsonrpc": "2.0",
54 "method": "ping",
55 "params": {"timestamp": int(time.time())},
56 "id": "test"
57 })
58 await log_message("自测", test_message)
59 await websocket.send(test_message)
60
61 try:
62 # 等待响应,设置超时
63 response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
64 await log_message("自测响应", response)
65 return True
66 except asyncio.TimeoutError:
67 await log_message("自测", "自测超时,未收到响应")
68 return False
69 except Exception as e:
70 await log_message("自测", f"自测异常: {str(e)}")
71 return False
72
73 class BLEClient:
74 def __init__(self):
75 self.target_device = None
76 self.client = None
77 self.services = []
78 self.optional_services = []
79 self.websocket = None
80 self.notification_records = defaultdict(lambda: (None, 0)) # 特征ID: (最后消息, 时间戳)
81
82 def on_disconnect(self, client):
83 print("BLE连接断开,关闭WebSocket")
84 if self.websocket and not self.websocket.closed:
85 asyncio.create_task(self.close_websocket())
86
87 async def close_websocket(self):
88 await self.websocket.close()
89 self.websocket = None
90
91 def detection_callback(self, device, advertisement_data):
92 if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services):
93 self.target_device = (device, advertisement_data)
94 if not self.target_device:
95 print("未找到匹配设备")
96 return
97 else:
98 device, adv_data = self.target_device
99 print("\n找到目标设备:")
100 print(f"设备名称: {device.name}")
101 print(f"设备地址: {device.address}")
102 print(f"信号强度: {device.rssi} dBm")
103 print("\n广播信息:")
104 print(f"服务UUID列表: {adv_data.service_uuids}")
105 print(f"制造商数据: {adv_data.manufacturer_data}")
106 print(f"服务数据: {adv_data.service_data}")
107 print(f"本地名称: {adv_data.local_name}")
108 return self.target_device
109
110 async def handle_client(self, websocket, path):
111 self.websocket = websocket
112 if path != "/scratch/ble":
113 await websocket.close(code=1003, reason="Path not allowed")
114 return
115
116 try:
117 async for message in websocket:
118 try:
119 await log_message("接收", message)
120 request = json.loads(message)
121
122 if request["jsonrpc"] != "2.0":
123 continue
124
125 method = request.get("method")
126 params = request.get("params", {})
127 request_id = request.get("id")
128
129 if method == "discover":
130 self.services = []
131 for filt in params.get("filters", [{}]):
132 self.services.extend(filt.get("services", []))
133 self.optional_services = params.get("optionalServices", [])
134
135 scanner = BleakScanner(scanning_mode="active")
136 scanner.register_detection_callback(self.detection_callback)
137
138 max_retries = 3
139 found = False
140 for attempt in range(max_retries):
141 self.target_device = None
142 await scanner.start()
143 await asyncio.sleep(5)
144 await scanner.stop()
145
146 if self.target_device:
147 found = True
148 break
149
150 if attempt < max_retries - 1:
151 print(f"未找到设备,第{attempt+1}次重试...")
152 await asyncio.sleep(3)
153
154 if found:
155 device, adv_data = self.target_device
156 discover_response = json.dumps({
157 "jsonrpc": "2.0",
158 "method": "didDiscoverPeripheral",
159 "params": {
160 "name": device.name,
161 "peripheralId": device.address,
162 "rssi": device.rssi
163 }
164 })
165 await log_message("下发", discover_response)
166 await websocket.send(discover_response)
167
168 result_response = json.dumps({
169 "jsonrpc": "2.0",
170 "result": None,
171 "id": request_id
172 })
173 await log_message("下发", result_response)
174 await websocket.send(result_response)
175
176 elif method == "connect":
177 peripheral_id = params.get("peripheralId")
178 if peripheral_id:
179 self.client = BleakClient(peripheral_id)
180 self.client.set_disconnected_callback(self.on_disconnect)
181 await self.client.connect()
182
183 if self.client.is_connected:
184 response = json.dumps({
185 "jsonrpc": "2.0",
186 "result": None,
187 "id": request_id
188 })
189 # await log_message("下发", response)
190 await websocket.send(response)
191
192 elif method == "write":
193 service_id = params.get("serviceId")
194 characteristic_id = params.get("characteristicId")
195 message = params.get("message")
196 encoding = params.get("encoding", "utf-8")
197
198 if all([service_id, characteristic_id, message]):
199 if encoding == "base64":
200 message_bytes = base64.b64decode(message)
201 print("write message_bytes",message_bytes)
202 else:
203 print("write message",message)
204 message_bytes = message.encode(encoding)
205
206
207 await self.client.write_gatt_char(characteristic_id, message_bytes)
208 response = json.dumps({
209 "jsonrpc": "2.0",
210 "result": None,
211 "id": request_id
212 })
213 await log_message("下发", response)
214 await websocket.send(response)
215
216 elif method == "read":
217 service_id = params.get("serviceId")
218 characteristic_id = params.get("characteristicId")
219
220 if all([service_id, characteristic_id]):
221 data = await self.client.read_gatt_char(characteristic_id)
222 print('read-data',base64.decode(data))
223 response = json.dumps({
224 "jsonrpc": "2.0",
225 "result": {
226 "serviceId": service_id,
227 "characteristicId": characteristic_id,
228 "message": base64.b64encode(data).decode("utf-8")
229 },
230 "id": request_id
231 })
232 await log_message("下发", response)
233 await websocket.send(response)
234
235 elif method == "startNotifications":
236 service_id = params.get("serviceId")
237 characteristic_id = params.get("characteristicId")
238
239 if all([service_id, characteristic_id]):
240 await self.client.start_notify(
241 characteristic_id,
242 self.notification_handler(websocket, service_id, characteristic_id)
243 )
244 response = json.dumps({
245 "jsonrpc": "2.0",
246 "result": None,
247 "id": request_id
248 })
249 await log_message("下发", response)
250 await websocket.send(response)
251
252 elif method == "ping":
253 # 处理ping请求,返回pong响应
254 response = json.dumps({
255 "jsonrpc": "2.0",
256 "result": {"pong": True, "timestamp": int(time.time())},
257 "id": request_id
258 })
259 await log_message("下发", response)
260 await websocket.send(response)
261
262 except json.JSONDecodeError:
263 error_msg = json.dumps({
264 "jsonrpc": "2.0",
265 "result": {"message": "Parse error"},
266 "id": None
267 })
268 await log_message("下发", error_msg)
269 except Exception as e:
270 error_msg = json.dumps({
271 "jsonrpc": "2.0",
272 "result": {"message": str(e)},
273 "id": request.get("id") if request else None
274 })
275 await log_message("下发", error_msg)
276
277 except websockets.exceptions.ConnectionClosed:
278 print("WebSocket连接关闭")
279 finally:
280 if self.client and self.client.is_connected:
281 await self.client.disconnect()
282 self.client = None
283 self.target_device = None
284
285 def notification_handler(self, websocket, service_id, characteristic_id):
286 async def callback(sender, data):
287 current_time = asyncio.get_event_loop().time()
288 last_message, last_time = self.notification_records[characteristic_id]
289
290 # 解码当前数据用于比较
291 current_message = base64.b64encode(data).decode('utf-8')
292 print('notification_handler current_message',base64.b64decode(current_message))
293
294 # 过滤逻辑
295 if current_message == last_message and (current_time - last_time) < 0.5:
296 return
297
298 # 更新记录
299 self.notification_records[characteristic_id] = (current_message, current_time)
300
301 response = json.dumps({
302 "jsonrpc": "2.0",
303 "method": "characteristicDidChange",
304 "params": {
305 "serviceId": service_id,
306 "characteristicId": characteristic_id,
307 "message": current_message
308 }
309 })
310 await log_message("下发", response)
311 await websocket.send(response)
312 return callback
313
314 async def check_port_and_start_server(port=20111, host='localhost'):
315 """检查端口并启动服务器"""
316 if is_port_in_use(port, host):
317 print(f"错误: 端口 {port} 已被占用,无法启动服务")
318 return False
319
320 print(f"端口 {port} 可用,正在启动服务...")
321 server = await websockets.serve(
322 lambda websocket, path: BLEClient().handle_client(websocket, path),
323 host, port
324 )
325
326 print(f"WebSocket服务已启动: ws://{host}:{port}/scratch/ble")
327 print("日志文件路径: ./b.log")
328
329 # 执行自检测试
330 try:
331 async with websockets.connect(f"ws://{host}:{port}/scratch/ble") as websocket:
332 print("正在执行自检测试...")
333 test_result = await self_test(websocket)
334 if test_result:
335 print("自检测试成功: 服务正常运行")
336 else:
337 print("自检测试失败: 服务可能存在问题")
338 except Exception as e:
339 print(f"自检测试异常: {str(e)}")
340
341 return server
342
343 async def main():
344 server = await check_port_and_start_server()
345 if server:
346 await asyncio.Future() # 保持服务器运行
347
348 if __name__ == "__main__":
349 asyncio.run(main())
...\ No newline at end of file ...\ No newline at end of file
1 # -*- coding: utf-8 -*-
2 import sys
3 import asyncio
4 import websockets
5 import json
6 import base64
7 import threading
8 from collections import defaultdict
9 import socket
10 import time
11 import platform
12
13 # 在导入其他模块前处理Windows特有的COM初始化问题
14 if sys.platform.startswith('win32'):
15 print("Windows系统: 处理COM初始化")
16 try:
17 from bleak.backends.winrt.util import uninitialize_sta, allow_sta, assert_mta
18 # 取消其他库可能设置的STA初始化
19 uninitialize_sta()
20 # 允许STA模式运行(需要确保有Windows事件循环)
21 allow_sta()
22 except ImportError:
23 print("警告: 无法导入Windows特有的bleak工具")
24 except Exception as e:
25 print(f"Windows初始化错误: {str(e)}")
26
27 # 导入其他模块
28 from bleak import BleakScanner, BleakClient
29
30 # 添加线程锁以确保日志写入的原子性
31 write_lock = threading.Lock()
32
33 def log_message_sync(direction, message):
34 """同步日志记录函数"""
35 log_entry = f"{direction}: {message}\n"
36 print(log_entry, end='') # 控制台仍然输出
37 with write_lock:
38 with open('b.log', 'a', encoding='utf-8') as f:
39 f.write(log_entry)
40
41 async def log_message(direction, message):
42 """异步封装日志记录"""
43 loop = asyncio.get_event_loop()
44 await loop.run_in_executor(None, log_message_sync, direction, message)
45
46 def is_port_in_use(port, host='localhost'):
47 """检查端口是否被占用"""
48 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
49 try:
50 s.bind((host, port))
51 return False
52 except socket.error:
53 return True
54
55 async def self_test(websocket):
56 """发送自检测试消息"""
57 test_message = json.dumps({
58 "jsonrpc": "2.0",
59 "method": "ping",
60 "params": {"timestamp": int(time.time())},
61 "id": "test"
62 })
63 await log_message("自测", test_message)
64 await websocket.send(test_message)
65
66 try:
67 # 等待响应,设置超时
68 response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
69 await log_message("自测响应", response)
70 return True
71 except asyncio.TimeoutError:
72 await log_message("自测", "自测超时,未收到响应")
73 return False
74 except Exception as e:
75 await log_message("自测", f"自测异常: {str(e)}")
76 return False
77
78 class BLEClient:
79 def __init__(self):
80 self.target_device = None
81 self.client = None
82 self.services = []
83 self.optional_services = []
84 self.websocket = None
85 self.notification_records = defaultdict(lambda: (None, 0)) # 特征ID: (最后消息, 时间戳)
86 self.is_windows = sys.platform.startswith('win32')
87
88 def on_disconnect(self, client):
89 print("BLE连接断开,关闭WebSocket")
90 if self.websocket and not self.websocket.closed:
91 asyncio.create_task(self.close_websocket())
92
93 async def close_websocket(self):
94 await self.websocket.close()
95 self.websocket = None
96
97 def detection_callback(self, device, advertisement_data):
98 if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services):
99 self.target_device = (device, advertisement_data)
100 if not self.target_device:
101 print("未找到匹配设备")
102 return
103 else:
104 device, adv_data = self.target_device
105 print("\n找到目标设备:")
106 print(f"设备名称: {device.name}")
107 print(f"设备地址: {device.address}")
108 print(f"信号强度: {device.rssi} dBm")
109 print("\n广播信息:")
110 print(f"服务UUID列表: {adv_data.service_uuids}")
111 print(f"制造商数据: {adv_data.manufacturer_data}")
112 print(f"服务数据: {adv_data.service_data}")
113 print(f"本地名称: {adv_data.local_name}")
114 return self.target_device
115
116 async def create_bleak_client(self, address):
117 """创建BleakClient,考虑Windows平台的特性"""
118 if self.is_windows:
119 print("Windows系统: 使用带地址类型的客户端")
120 winrt_args = {"address_type": "public"} # 或 "random"
121 return BleakClient(address, winrt=winrt_args)
122 else:
123 return BleakClient(address)
124
125 async def handle_client(self, websocket, path):
126 self.websocket = websocket
127 if path != "/scratch/ble":
128 await websocket.close(code=1003, reason="Path not allowed")
129 return
130
131 try:
132 async for message in websocket:
133 try:
134 await log_message("接收", message)
135 request = json.loads(message)
136
137 if request["jsonrpc"] != "2.0":
138 continue
139
140 method = request.get("method")
141 params = request.get("params", {})
142 request_id = request.get("id")
143
144 if method == "discover":
145 self.services = []
146 for filt in params.get("filters", [{}]):
147 self.services.extend(filt.get("services", []))
148 self.optional_services = params.get("optionalServices", [])
149
150 # Windows使用特定扫描模式配置
151 scanning_args = {"scanning_mode": "active"}
152 scanner = BleakScanner(scanning_mode="active")
153 scanner.register_detection_callback(self.detection_callback)
154
155 max_retries = 3
156 found = False
157 for attempt in range(max_retries):
158 self.target_device = None
159 await scanner.start()
160 await asyncio.sleep(5)
161 await scanner.stop()
162
163 if self.target_device:
164 found = True
165 break
166
167 if attempt < max_retries - 1:
168 print(f"未找到设备,第{attempt+1}次重试...")
169 await asyncio.sleep(3)
170
171 if found:
172 device, adv_data = self.target_device
173 discover_response = json.dumps({
174 "jsonrpc": "2.0",
175 "method": "didDiscoverPeripheral",
176 "params": {
177 "name": device.name,
178 "peripheralId": device.address,
179 "rssi": device.rssi
180 }
181 })
182 await log_message("下发", discover_response)
183 await websocket.send(discover_response)
184
185 result_response = json.dumps({
186 "jsonrpc": "2.0",
187 "result": None,
188 "id": request_id
189 })
190 await log_message("下发", result_response)
191 await websocket.send(result_response)
192
193 elif method == "connect":
194 peripheral_id = params.get("peripheralId")
195 if peripheral_id:
196 self.client = await self.create_bleak_client(peripheral_id)
197 self.client.set_disconnected_callback(self.on_disconnect)
198 await self.client.connect()
199
200 if self.client.is_connected:
201 response = json.dumps({
202 "jsonrpc": "2.0",
203 "result": None,
204 "id": request_id
205 })
206 await websocket.send(response)
207
208 elif method == "write":
209 service_id = params.get("serviceId")
210 characteristic_id = params.get("characteristicId")
211 message = params.get("message")
212 encoding = params.get("encoding", "utf-8")
213
214 if all([service_id, characteristic_id, message]):
215 if encoding == "base64":
216 message_bytes = base64.b64decode(message)
217 else:
218 message_bytes = message.encode(encoding)
219
220 await self.client.write_gatt_char(characteristic_id, message_bytes)
221 response = json.dumps({
222 "jsonrpc": "2.0",
223 "result": None,
224 "id": request_id
225 })
226 await log_message("下发", response)
227 await websocket.send(response)
228
229 elif method == "read":
230 service_id = params.get("serviceId")
231 characteristic_id = params.get("characteristicId")
232
233 if all([service_id, characteristic_id]):
234 data = await self.client.read_gatt_char(characteristic_id)
235 response = json.dumps({
236 "jsonrpc": "2.0",
237 "result": {
238 "serviceId": service_id,
239 "characteristicId": characteristic_id,
240 "message": base64.b64encode(data).decode("utf-8")
241 },
242 "id": request_id
243 })
244 await log_message("下发", response)
245 await websocket.send(response)
246
247 elif method == "startNotifications":
248 service_id = params.get("serviceId")
249 characteristic_id = params.get("characteristicId")
250
251 if all([service_id, characteristic_id]):
252 await self.client.start_notify(
253 characteristic_id,
254 self.notification_handler(websocket, service_id, characteristic_id)
255 )
256 response = json.dumps({
257 "jsonrpc": "2.0",
258 "result": None,
259 "id": request_id
260 })
261 await log_message("下发", response)
262 await websocket.send(response)
263
264 elif method == "ping":
265 # 处理ping请求,返回pong响应
266 response = json.dumps({
267 "jsonrpc": "2.0",
268 "result": {"pong": True, "timestamp": int(time.time())},
269 "id": request_id
270 })
271 await log_message("下发", response)
272 await websocket.send(response)
273
274 except json.JSONDecodeError:
275 error_msg = json.dumps({
276 "jsonrpc": "2.0",
277 "error": {"message": "Parse error", "code": -32700},
278 "id": None
279 })
280 await log_message("下发", error_msg)
281 await websocket.send(error_msg)
282 except Exception as e:
283 error_msg = json.dumps({
284 "jsonrpc": "2.0",
285 "error": {"message": str(e), "code": -32000},
286 "id": request.get("id") if request else None
287 })
288 await log_message("下发", error_msg)
289 await websocket.send(error_msg)
290
291 except websockets.exceptions.ConnectionClosed:
292 print("WebSocket连接关闭")
293 finally:
294 if self.client and self.client.is_connected:
295 await self.client.disconnect()
296 self.client = None
297 self.target_device = None
298
299 def notification_handler(self, websocket, service_id, characteristic_id):
300 async def callback(sender, data):
301 current_time = asyncio.get_event_loop().time()
302 last_message, last_time = self.notification_records[characteristic_id]
303
304 # 解码当前数据用于比较
305 current_message = base64.b64encode(data).decode('utf-8')
306
307 # 过滤逻辑
308 if current_message == last_message and (current_time - last_time) < 0.5:
309 return
310
311 # 更新记录
312 self.notification_records[characteristic_id] = (current_message, current_time)
313
314 response = json.dumps({
315 "jsonrpc": "2.0",
316 "method": "characteristicDidChange",
317 "params": {
318 "serviceId": service_id,
319 "characteristicId": characteristic_id,
320 "message": current_message
321 }
322 })
323 await log_message("下发", response)
324 await websocket.send(response)
325 return callback
326
327 async def check_port_and_start_server(port=20111, host='localhost'):
328 """检查端口并启动服务器"""
329 if is_port_in_use(port, host):
330 print(f"错误: 端口 {port} 已被占用,无法启动服务")
331 return False
332
333 print(f"端口 {port} 可用,正在启动服务...")
334 server = await websockets.serve(
335 lambda websocket, path: BLEClient().handle_client(websocket, path),
336 host, port
337 )
338
339 print(f"WebSocket服务已启动: ws://{host}:{port}/scratch/ble")
340 print("日志文件路径: ./b.log")
341
342 # 执行自检测试
343 try:
344 async with websockets.connect(f"ws://{host}:{port}/scratch/ble") as websocket:
345 print("正在执行自检测试...")
346 test_result = await self_test(websocket)
347 if test_result:
348 print("自检测试成功: 服务正常运行")
349 else:
350 print("自检测试失败: 服务可能存在问题")
351 except Exception as e:
352 print(f"自检测试异常: {str(e)}")
353
354 return server
355
356 async def main():
357 # Windows上保证事件循环设置正确
358 if sys.platform.startswith('win32'):
359 import asyncio
360 asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
361
362 server = await check_port_and_start_server()
363 if server:
364 await asyncio.Future() # 保持服务器运行
365
366 if __name__ == "__main__":
367 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!