# --------------------------------------
# Created by Milo on 2025/3/18
# Project: app_maintain
# Desc: 纯蓝牙连接
# --------------------------------------
import traceback
from asyncio import Event
from threading import Thread
from typing import Union
from PyQt6.QtCore import QThread
from bleak import BleakClient, BleakScanner, BLEDevice
from bleak.exc import BleakError
import asyncio
import logging
from customer_blue.include.basealgorithm import decimal_to_hex, get_high_low
from customer_blue.include.protobuf_msg import MessagePB
from customer_blue.include.seting import logger
from customer_blue.include.signalmanage import MainSignal
BLUE_SERVICE_UUID = "0000ffff-0000-1000-8000-00805f9b34fb"
BLUE_WRITE_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"
BLUE_NOTIF_CHAR_UUID = "0000ff02-0000-1000-8000-00805f9b34fb"
BLUE_NOTIF_DESC_UUID = "00002902-0000-1000-8000-00805f9b34fb"
class StableBleakClient:
def __init__(self, device_name=None, device_address=None, timeout=20.0, retries=3, heartbeat_interval=10.0):
"""
稳定的BLE客户端封装类
参数:
device_name: 目标设备名称 (优先使用)
device_address: 目标设备MAC地址
timeout: 连接超时时间(秒)
retries: 最大重连次数
heartbeat_interval: 心跳检测间隔(秒)
"""
self.device_name = device_name
self.device_address = device_address
self.timeout = timeout
self.max_retries = retries
self.heartbeat_interval = heartbeat_interval
self.client = None
self.heartbeat_task = None
self.connected = False
self.alive_callbacks = None
self.main_disconnected_callbacks = None
self.notification_callbacks = {} # 存储通知回调
async def connect(self):
"""建立稳定连接,包含重试机制和信号强度筛选"""
if not self.device_name and not self.device_address:
raise ValueError("必须提供device_name或device_address")
device = None
attempts = 0
while attempts < self.max_retries and not self.connected:
attempts += 1
try:
# 通过扫描查找设备
if self.device_name:
device = await self._find_device_by_name()
elif self.device_address:
device = await self._find_device_by_address()
if not device:
print(f"未找到设备,重试 {attempts}/{self.max_retries}")
await asyncio.sleep(2)
continue
# 创建客户端并连接
self.client = BleakClient(device.address, timeout=self.timeout, disconnected_callback=self._handle_disconnect)
await self.client.connect()
# 设置断开回调
# 启动心跳检测
self.heartbeat_task = asyncio.create_task(self._heartbeat_check())
self.connected = True
print(f"成功连接到设备: {device.name} ({device.address})")
return True
except (BleakError, asyncio.TimeoutError) as e:
print(f"连接失败 (尝试 {attempts}/{self.max_retries}): {str(e)}")
await self._cleanup_resources()
await asyncio.sleep(1 + attempts) # 指数退避重试
return False
async def _find_device_by_name(self):
"""通过设备名称查找信号最强的设备"""
devices = await BleakScanner.discover(timeout=5.0)
target_devices = [d for d in devices if d.name and d.name == self.device_name]
if not target_devices:
return None
# 选择RSSI信号最强的设备
strongest_device = max(target_devices, key=lambda d: d.rssi)
print(f"找到设备: {strongest_device.name}, RSSI: {strongest_device.rssi}")
return strongest_device
async def _find_device_by_address(self):
"""通过MAC地址查找设备"""
device = await BleakScanner.find_device_by_address(
self.device_address,
timeout=5.0
)
if device:
print(f"找到设备: {device.address}")
return device
def regst_disconnect_callback(self, callback):
self.main_disconnected_callbacks = callback
async def _heartbeat_check(self):
"""心跳检测维持连接"""
while self.connected:
try:
# 读取设备信息作为心跳检测
if self.client and self.client.is_connected:
await self.client.read_gatt_char(BLUE_NOTIF_CHAR_UUID) # 设备名称特征
print("心跳检测成功")
else:
print("心跳检测失败,连接已断开")
self.connected = False
except BleakError as e:
print(f"心跳检测错误: {str(e)}")
self.connected = False
await self.alive_callbacks(
self.connected # 内部状态标志
and self.client is not None # 客户端对象存在
and self.client.is_connected
)
await asyncio.sleep(self.heartbeat_interval)
async def enable_notifications(self, char_uuid, callback):
"""启用通知并注册回调"""
if not self.connected or not self.client:
raise ConnectionError("未连接")
await self.client.start_notify(char_uuid, callback)
self.notification_callbacks[char_uuid] = callback
async def disable_notifications(self, char_uuid):
"""禁用通知并移除回调"""
if not self.connected or not self.client:
return
if char_uuid in self.notification_callbacks:
await self.client.stop_notify(char_uuid)
del self.notification_callbacks[char_uuid]
def _handle_disconnect(self, client):
"""处理断开连接事件"""
print("设备断开连接")
self.connected = False
self.main_disconnected_callbacks("disconnected")
asyncio.create_task(self._cleanup_resources())
async def disconnect(self):
"""安全断开连接并清理资源"""
if not self.connected or not self.client:
return
print("正在断开连接...")
self.connected = False
await self.alive_callbacks(False)
# 取消心跳任务
if self.heartbeat_task:
self.heartbeat_task.cancel()
try:
await self.heartbeat_task
except asyncio.CancelledError:
pass
await self._cleanup_resources()
async def _cleanup_resources(self):
"""清理所有资源"""
try:
if self.client:
# 停止所有通知
for char_uuid in list(self.notification_callbacks.keys()):
try:
await self.client.stop_notify(char_uuid)
except BleakError:
pass
self.notification_callbacks.clear()
# 断开连接
if self.client.is_connected:
await self.client.disconnect()
# 强制清理(平台特定)
if hasattr(self.client, '_backend') and hasattr(self.client._backend, 'cleanup'):
self.client._backend.cleanup()
# print("资源清理完成")
except Exception as e:
print(f"清理资源时出错: {str(e)}")
finally:
self.client = None
async def read_characteristic(self, char_uuid):
"""读取特征值"""
if not self.connected or not self.client:
raise ConnectionError("未连接")
return await self.client.read_gatt_char(char_uuid)
async def write_characteristic(self, char_uuid, data, response=True):
"""写入特征值"""
if not self.connected or not self.client:
raise ConnectionError("未连接")
await self.client.write_gatt_char(char_uuid, data)
async def __aenter__(self):
await self.connect()
return self
async def __aexit__(self, exc_type, exc_value, traceback):
await self.disconnect()
def is_alive_callbacks(self, callback):
"""是否连接"""
self.alive_callbacks = callback
class BleakClientThread(QThread):
FLAG = 'BleakClientThread'
def __init__(self, address_or_ble_device: Union[BLEDevice, str], parent, disconnect_callback=None):
super().__init__()
self.send_sequence = 0
self.manage = parent
self.ble_device = address_or_ble_device
self.loop = asyncio.new_event_loop()
self.stop_event = asyncio.Event()
self.write_queue = asyncio.Queue()
# 当前已经存的长度
self.already_len = None
# 当前的片计数
self.cont = None
# 需要拼包的总长的,第一次进入时赋值
self.total_length = None
# 拼包数据
self.pack_data = []
self.pack_flag = False
self.last_seq = None
self.notice_callback = None
self.recv_callback = None
self.is_connected = False
self.disconnect_callback = disconnect_callback
def register_connect(self, parm):
"""
注册连接成功、失败、断开的回调通知函数
:param parm: 函数指针名
:return:
"""
self.notice_callback = parm
def unregister_connect(self):
"""
注销回调函数
:return:
"""
self.notice_callback = None
def register_recv(self, parm):
"""
注册收数
:param parm:
:return:
"""
self.recv_callback = parm
def unregister_recv(self):
"""
注销收数
:return:
"""
self.recv_callback = None
def run(self):
try:
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.main())
except Exception as e:
logger.error(f"{self.FLAG}, run error: {str(e)}")
finally:
# Clean up loop resources
self.loop.run_until_complete(self.loop.shutdown_asyncgens())
# self.loop.close()
async def main(self):
# 使用设备名称连接
async with StableBleakClient(device_address=self.ble_device) as client:
# 启用通知
await client.enable_notifications(
BLUE_NOTIF_CHAR_UUID,
self.notification_handler
)
client.is_alive_callbacks(self.is_connect)
client.regst_disconnect_callback(self.disconnect_callback)
# 读取设备信息
# device_name = await client.read_characteristic(BLUE_NOTIF_DESC_UUID)
# print(f"设备名称: {device_name.decode('utf-8')}")
while True:
try:
data = await asyncio.wait_for(self.write_queue.get(), timeout=1.0)
if data is None:
break
print("send to ",data)
# 写入数据
await client.write_characteristic(
BLUE_WRITE_CHAR_UUID,
data,
response=True
)
except asyncio.TimeoutError:
continue # Timeout is expected, continue the loop
# async def main(self):
# try:
# await self.client.connect()
# for service in self.client.services:
# logger.error(
# '-----------------service uuid: {} [{}]---------------------'.format(service.uuid,
# service.description))
# for c in service.characteristics:
# logger.error('characteristic uuid: {} [{}] [{}]'.format(c.uuid, c.description, c.properties))
# await self.client.start_notify(BLUE_NOTIF_CHAR_UUID, self.notification_handler)
# MainSignal.get_instance().manage_client_connected.emit(True)
# if self.notice_callback:
# self.notice_callback(True)
# while not self.stop_event.is_set():
# try:
# data = await asyncio.wait_for(self.write_queue.get(), timeout=1.0)
# if data is None:
# break
# await self.client.write_gatt_char(BLUE_WRITE_CHAR_UUID, data)
# except asyncio.TimeoutError:
# continue # Timeout is expected, continue the loop
# except Exception as e:
# logger.error(f"{self.FLAG}, connection or notification error: {str(e)}")
# finally:
# try:
# await self.client.disconnect()
# logger.error(f"{self.FLAG}, Bluetooth disconnected")
# except Exception as e:
# logger.error(f"{self.FLAG}, disconnect error: {str(e)}")
#
# MainSignal.get_instance().manage_client_connected.emit(False)
# if self.notice_callback:
# self.notice_callback(True)
# 第一次收到14帧序号,进行初始拼包操作
def inti_merge_data(self, data):
self.pack_data.clear()
high = data[0:2]
low = data[2:4]
self.total_length = int(low + high, 16) * 2
self.pack_data.append(data[4:])
self.already_len = len(data[4:])
self.pack_flag = True
# 拼包0x14
def dispose_0x14(self, msg_type, fc, data):
if not self.pack_flag and fc == '14':
self.inti_merge_data(data)
elif fc == '14': # 第二次来的还是14 就进行检查是否为上一帧的后续,如果不是,就当第一次14帧处理
temp_height = data[0:2]
temp_low = data[2:4]
temp_len = int(temp_low + temp_height, 16) * 2
if self.total_length != temp_len + self.already_len:
self.inti_merge_data(data)
else:
self.pack_data.append(data[4:])
self.already_len += len(data[4:])
else:
temp_len = len(data)
if self.total_length == self.already_len + temp_len:
real_data = ''
for str_data in self.pack_data:
real_data += str_data
real_data += data
self.pack_flag = False
MainSignal.get_instance().manage_data_received.emit(msg_type, real_data)
if self.recv_callback:
self.recv_callback(msg_type, data)
self.pack_data.clear()
else:
self.dispose_0x04(msg_type, data)
# 直接丢出0x04
def dispose_0x04(self, msg_type, data):
MainSignal.get_instance().manage_data_received.emit(msg_type, data)
if self.recv_callback:
self.recv_callback(msg_type, data)
def notification_handler(self, sender, data):
try:
print("", data)
data_t = bytes(data).hex()
lo = ' '.join([data_t[i:i + 2] for i in range(0, len(data_t), 2)])
# print(lo)
msg_type = data_t[0:2]
fc = data_t[2:4]
seq = data_t[4:6]
data_len = data_t[6:8]
data_protobuf = data_t[8:]
if msg_type == '3d':
return
# if msg_type != '4d':
# print(lo)
if fc == '14' or self.pack_flag:
self.dispose_0x14(msg_type, fc, data_protobuf)
else:
self.dispose_0x04(msg_type, data_protobuf)
except Exception:
traceback.print_exc()
# 获取计数
def get_send_num(self):
self.send_sequence += 1
self.send_sequence = self.send_sequence & 0xFF
return self.send_sequence
def send_data(self, obj: object):
seqs = self.get_send_num()
data = None
# TODO
if isinstance(obj, MessagePB):
obj.setseqs(seqs)
data = obj.create_msg()
else:
# data = obj
return
while True:
data_len = len(data)
if data_len == 0:
break
if data_len > 100:
head = [77, 20, seqs, 102]
hex_st = decimal_to_hex(data_len)
high_byte, low_byte = get_high_low(hex_st)
low_byte = int(low_byte, 16)
head.append(low_byte)
high_byte = int(high_byte, 16)
head.append(high_byte)
head.extend(data[0:100])
data = data[100:]
send_data = bytearray(head)
seqs = self.get_send_num()
else:
len_data = len(data)
head = [77, 4, seqs, len_data]
head.extend(data)
send_data = bytearray(head)
data = ''
hx = send_data.hex()
# logger.debug(f" send data: {' '.join([hx[i:i + 2] for i in range(0, len(hx), 2)])}")
if not self.loop.is_closed():
self.loop.call_soon_threadsafe(self.write_queue.put_nowait, send_data)
def send_wifi_cntrl(self, msg_type, msg_data):
seqs = self.get_send_num()
if msg_data is None:
head = [msg_type, 0, seqs, 0]
else:
head = [msg_type, 0, seqs, len(msg_data)]
head.extend(msg_data)
send_data = bytearray(head)
hx = send_data.hex()
# logger.debug(f"send data: {' '.join([hx[i:i + 2] for i in range(0, len(hx), 2)])}")
if not self.loop.is_closed():
self.loop.call_soon_threadsafe(self.write_queue.put_nowait, send_data)
async def is_connect(self, parm):
self.is_connected = parm
def is_alive(self):
return self.is_connected
def stop(self):
try:
self.loop.call_soon_threadsafe(self.write_queue.put_nowait, None)
# self.loop.call_soon_threadsafe(self.stop_event.set)
except Exception as e:
logger.error(f'{self.FLAG} stop error: {str(e)}')
整理成一个可复用的
最新发布