一、什么是蓝牙 BLE?
蓝牙 BLE(Bluetooth Low Energy)就是我们手机连接蓝牙耳机、智能手环、智能家居时用的技术。
为什么叫"低功耗"? 因为它比传统蓝牙省电很多,一块纽扣电池就能用好几年。
二、第一步:导入需要的模块
在开始写代码之前,先要导入蓝牙相关的工具包,就像做饭前要先准备食材一样:
typescript
// ====== common/BLEManager.ets ======
// 这个文件放在 entry/src/main/ets/common/ 目录下
// 蓝牙功能模块 - 用来扫描、连接、收发数据
import { ble, access } from '@kit.ConnectivityKit';
// 权限管理模块 - 用来向用户申请权限
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';
// 错误处理模块 - 用来捕获错误信息
import { BusinessError } from '@kit.BasicServicesKit';
// 日志模块 - 用来打印调试信息(可选)
import { hilog } from '@kit.PerformanceAnalysisKit';
三、第二步:配置权限
蓝牙功能涉及用户隐私,所以必须先申请权限。这分两步:
- 静态声明:告诉系统你的应用需要什么权限
- 动态申请:弹窗让用户点击"允许"
3.1 静态权限声明(在配置文件里写)
json
// ====== entry/src/main/module.json5 ======
// 打开这个文件,找到 "module" 部分,添加 "requestPermissions"
{
"module": {
// ... 其他配置 ...
"requestPermissions": [
{
"name": "ohos.permission.ACCESS_BLUETOOTH", // 权限1:访问蓝牙
"reason": "$string:bluetooth_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.DISCOVER_BLUETOOTH", // 权限2:发现蓝牙设备
"reason": "$string:bluetooth_discover_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.USE_BLUETOOTH", // 权限3:使用蓝牙
"reason": "$string:bluetooth_use_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
💡 为什么要3个权限? 因为蓝牙功能分成了扫描、连接、传输三部分,每部分需要单独授权。
3.2 权限说明字符串(告诉用户为什么需要权限)
json
// ====== entry/src/main/resources/base/element/string.json ======
// 这些文字会显示在权限申请弹窗里,让用户知道为什么需要这个权限
{
"string": [
{
"name": "bluetooth_permission_reason",
"value": "需要蓝牙权限以连接和控制蓝牙设备"
},
{
"name": "bluetooth_discover_permission_reason",
"value": "需要发现蓝牙权限以扫描附近的蓝牙设备"
},
{
"name": "bluetooth_use_permission_reason",
"value": "需要使用蓝牙权限以传输数据"
}
]
}
3.3 动态权限申请(弹窗让用户确认)
光在配置文件里写还不够,还需要在代码里弹窗询问用户。下面这个工具类可以直接复制使用:
typescript
// ====== common/PermissionManager.ets ======
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';
export class PermissionManager {
private static instance: PermissionManager;
private context?: common.UIAbilityContext;
public static getInstance(): PermissionManager {
if (!PermissionManager.instance) {
PermissionManager.instance = new PermissionManager();
}
return PermissionManager.instance;
}
public setContext(context: common.UIAbilityContext): void {
this.context = context;
}
/**
* 检查并申请蓝牙权限
* @returns 是否已授权
*/
public async checkAndRequestBlePermissions(): Promise<boolean> {
if (!this.context) {
console.error('Context not set');
return false;
}
const permissions: Array<Permissions> = [
'ohos.permission.USE_BLUETOOTH',
'ohos.permission.ACCESS_BLUETOOTH',
'ohos.permission.DISCOVER_BLUETOOTH'
];
try {
const atManager = abilityAccessCtrl.createAtManager();
// 检查每个权限
for (const permission of permissions) {
const grantStatus = await atManager.checkAccessToken(
this.context.applicationInfo.accessTokenId,
permission
);
if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
// 权限未授予,请求用户授权
const requestResult = await atManager.requestPermissionsFromUser(
this.context,
[permission]
);
if (requestResult.authResults[0] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
console.error(`Permission ${permission} denied by user`);
return false;
}
}
}
console.info('All BLE permissions granted');
return true;
} catch (error) {
console.error(`Permission request failed: ${error}`);
return false;
}
}
/**
* 检查单个权限状态
*/
public async checkPermission(permission: Permissions): Promise<boolean> {
if (!this.context) return false;
try {
const atManager = abilityAccessCtrl.createAtManager();
const grantStatus = await atManager.checkAccessToken(
this.context.applicationInfo.accessTokenId,
permission
);
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
return false;
}
}
}
3.4 在 EntryAbility 中初始化(应用启动时设置)
应用启动时,需要把上下文(context)传给权限管理器和蓝牙管理器:
typescript
// ====== EntryAbility.ets ======
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PermissionManager } from '../common/PermissionManager';
import { BLEManager } from '../common/BLEManager';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('EntryAbility onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 设置上下文
PermissionManager.getInstance().setContext(this.context);
BLEManager.getInstance().setContext(this.context);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load content');
return;
}
});
}
}
四、第三步:蓝牙管理器(核心代码)
这是整个蓝牙功能的核心,包含扫描、连接、收发数据等所有功能。
注意:代码看着很长,但其实是一个完整的工具类,可以直接复制到项目里使用(我很喜欢把管理器放src/main/ets/common里面。
typescript
// ====== common/BLEManager.ets ======
// 这个文件放在 entry/src/main/ets/common/ 目录下(
import { ble, access } from '@kit.ConnectivityKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// ========== 第一部分:定义数据类型 ==========
// 设备信息(扫描到的设备长什么样)
export interface DeviceInfo {
deviceId: string; // 设备唯一标识
name: string; // 设备名称,比如"小米手环7"
address: string; // MAC地址
rssi: number; // 信号强度,负数,越大越强(比如-50比-80信号强)
}
// 连接状态(设备当前是什么状态)
export enum ConnectionState {
DISCONNECTED = 0, // 已断开
CONNECTING = 1, // 连接中...
CONNECTED = 2, // 已连接
DISCONNECTING = 3 // 断开中...
}
// GATT服务(一个设备可能有多个服务)
export interface BLEService {
serviceUuid: string; // 服务UUID
characteristics: BLECharacteristic[]; // 这个服务下的所有特征值
}
// GATT特征值(数据就存在这里)
export interface BLECharacteristic {
characteristicUuid: string; // 特征值UUID
properties: number; // 属性(可读/可写/可通知)
descriptors: BLEDescriptor[]; // 描述符列表
}
// GATT描述符
export interface BLEDescriptor {
descriptorUuid: string;
}
// ========== 第二部分:蓝牙管理器类 ==========
export class BLEManager {
// 单例模式:确保全局只有一个蓝牙管理器实例
private static instance: BLEManager;
// 重要变量
private context?: common.UIAbilityContext; // 应用上下文
private gattClient: ble.GattClientDevice | null = null; // GATT客户端(用来连接设备)
private isScanning: boolean = false; // 是否正在扫描
private connectedDevice: DeviceInfo | null = null; // 当前连接的设备
private discoveredServices: BLEService[] = []; // 发现的服务列表
// 回调函数(用来通知页面状态变化)
private onDeviceFoundCallback?: (device: DeviceInfo) => void; // 发现设备时调用
private onConnectionStateChangedCallback?: (device: DeviceInfo, state: ConnectionState) => void; // 连接状态变化时调用
private onDataReceivedCallback?: (data: ArrayBuffer) => void; // 收到数据时调用
private constructor() {} // 私有构造函数,防止外部直接 new
// 获取单例(整个应用只用这一个实例)
public static getInstance(): BLEManager {
if (!BLEManager.instance) {
BLEManager.instance = new BLEManager();
}
return BLEManager.instance;
}
// 设置上下文(在 EntryAbility 里调用)
public setContext(context: common.UIAbilityContext): void {
this.context = context;
}
// ========== 第三部分:检查蓝牙状态 ==========
// 检查蓝牙是否开启
public isBluetoothEnabled(): boolean {
try {
const state: access.BluetoothState = access.getState();
return state === access.BluetoothState.STATE_ON; // STATE_ON 表示已开启
} catch (error) {
console.error(`检查蓝牙状态失败: ${error}`);
return false;
}
}
// ========== 第四部分:设置回调 ==========
// 这些回调函数会在特定事件发生时被调用,比如发现了新设备、连接成功等
public setOnDeviceFoundCallback(callback: (device: DeviceInfo) => void): void {
this.onDeviceFoundCallback = callback;
}
public setOnConnectionStateChangedCallback(callback: (device: DeviceInfo, state: ConnectionState) => void): void {
this.onConnectionStateChangedCallback = callback;
}
public setOnDataReceivedCallback(callback: (data: ArrayBuffer) => void): void {
this.onDataReceivedCallback = callback;
}
// ========== 第五部分:扫描设备 ==========
// 开始扫描(扫描附近所有蓝牙设备)
public startScan(): void {
// 防止重复扫描
if (this.isScanning) {
console.warn('已经在扫描中,不要重复启动');
return;
}
// 检查蓝牙是否开启
if (!this.isBluetoothEnabled()) {
console.error('蓝牙未开启,请先开启蓝牙');
return;
}
try {
// 配置扫描参数
const scanOptions: ble.ScanOptions = {
interval: 50, // 扫描间隔,单位毫秒
dutyMode: ble.ScanDuty.SCAN_MODE_BALANCED, // 平衡模式(速度和省电平衡)
matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE // 积极匹配模式(优先发现设备)
};
// 注册扫描结果回调(关键!)
ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>) => {
// 每次发现设备,这个函数都会被调用
data.forEach((result: ble.ScanResult) => {
// 把扫描结果转换成我们定义的格式
const device: DeviceInfo = {
deviceId: result.deviceId,
name: result.deviceName || '未知设备', // 有些设备没有名字
address: result.deviceId,
rssi: result.rssi // 信号强度
};
// 通知页面"发现了新设备"
if (this.onDeviceFoundCallback) {
this.onDeviceFoundCallback(device);
}
});
});
// 开始扫描(空过滤器表示扫描所有设备)
const scanFilters: ble.ScanFilter[] = [{}];
ble.startBLEScan(scanFilters, scanOptions);
this.isScanning = true;
console.info('✅ 开始扫描蓝牙设备');
} catch (error) {
console.error(`启动扫描失败: ${error}`);
}
}
// 带设备名过滤的扫描
public startScanWithFilter(deviceName: string): void {
if (this.isScanning) return;
if (!this.isBluetoothEnabled()) return;
try {
const scanOptions: ble.ScanOptions = {
interval: 50,
dutyMode: ble.ScanDuty.SCAN_MODE_BALANCED,
matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
};
ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>) => {
data.forEach((result: ble.ScanResult) => {
// 过滤设备名
if (result.deviceName && result.deviceName.includes(deviceName)) {
const device: DeviceInfo = {
deviceId: result.deviceId,
name: result.deviceName,
address: result.deviceId,
rssi: result.rssi
};
if (this.onDeviceFoundCallback) {
this.onDeviceFoundCallback(device);
}
}
});
});
const scanFilters: ble.ScanFilter[] = [{}];
ble.startBLEScan(scanFilters, scanOptions);
this.isScanning = true;
} catch (error) {
console.error(`Start scan with filter failed: ${error}`);
}
}
// 停止扫描
public stopScan(): void {
if (!this.isScanning) return;
try {
ble.stopBLEScan();
ble.off('BLEDeviceFind');
this.isScanning = false;
console.info('BLE scan stopped');
} catch (error) {
console.error(`Stop scan failed: ${error}`);
}
}
// 连接设备
public async connectDevice(device: DeviceInfo): Promise<boolean> {
try {
if (this.gattClient) {
this.gattClient.close();
}
this.gattClient = ble.createGattClientDevice(device.deviceId);
// 监听连接状态
this.gattClient.on('BLEConnectionStateChange', (state: ble.BLEConnectionChangeState) => {
let connectionState: ConnectionState;
switch (state.state) {
case 0:
connectionState = ConnectionState.DISCONNECTED;
this.connectedDevice = null;
break;
case 1:
connectionState = ConnectionState.CONNECTING;
break;
case 2:
connectionState = ConnectionState.CONNECTED;
this.connectedDevice = device;
this.discoverServices();
break;
case 3:
connectionState = ConnectionState.DISCONNECTING;
break;
default:
connectionState = ConnectionState.DISCONNECTED;
}
if (this.onConnectionStateChangedCallback) {
this.onConnectionStateChangedCallback(device, connectionState);
}
});
await this.gattClient.connect();
return true;
} catch (error) {
console.error(`Connect failed: ${error}`);
return false;
}
}
// 服务发现
private async discoverServices(): Promise<void> {
if (!this.gattClient) return;
try {
const services = await this.gattClient.getServices();
this.discoveredServices = services.map((service: ble.GattService): BLEService => ({
serviceUuid: service.serviceUuid,
characteristics: service.characteristics.map((char: ble.BLECharacteristic): BLECharacteristic => ({
characteristicUuid: char.characteristicUuid,
properties: 0,
descriptors: char.descriptors.map((desc: ble.BLEDescriptor): BLEDescriptor => ({
descriptorUuid: desc.descriptorUuid
}))
}))
}));
console.info(`Discovered ${services.length} services`);
// 设置特征值通知
await this.setupNotification();
} catch (error) {
console.error(`Discover services failed: ${error}`);
}
}
// 设置特征值通知
private async setupNotification(): Promise<void> {
if (!this.gattClient) return;
try {
// 监听特征值变化
this.gattClient.on('BLECharacteristicChange', (char: ble.BLECharacteristic) => {
if (this.onDataReceivedCallback && char.characteristicValue) {
this.onDataReceivedCallback(char.characteristicValue);
}
});
// 查找目标服务并启用通知
for (const service of this.discoveredServices) {
for (const char of service.characteristics) {
const characteristic: ble.BLECharacteristic = {
serviceUuid: service.serviceUuid,
characteristicUuid: char.characteristicUuid,
characteristicValue: new ArrayBuffer(0),
descriptors: []
};
await this.gattClient.setCharacteristicChangeNotification(characteristic, true);
}
}
} catch (error) {
console.error(`Setup notification failed: ${error}`);
}
}
// 读取特征值
public async readCharacteristic(serviceUuid: string, charUuid: string): Promise<ArrayBuffer | null> {
if (!this.gattClient) return null;
try {
const characteristic: ble.BLECharacteristic = {
serviceUuid: serviceUuid,
characteristicUuid: charUuid,
characteristicValue: new ArrayBuffer(0),
descriptors: []
};
const result = await this.gattClient.readCharacteristicValue(characteristic);
return result.characteristicValue;
} catch (error) {
console.error(`Read characteristic failed: ${error}`);
return null;
}
}
// 写入特征值
public async writeCharacteristic(serviceUuid: string, charUuid: string, data: ArrayBuffer): Promise<boolean> {
if (!this.gattClient) return false;
try {
const characteristic: ble.BLECharacteristic = {
serviceUuid: serviceUuid,
characteristicUuid: charUuid,
characteristicValue: data,
descriptors: []
};
// 双重写入策略
try {
await this.gattClient.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE);
return true;
} catch (writeError) {
await this.gattClient.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE_NO_RESPONSE);
return true;
}
} catch (error) {
console.error(`Write characteristic failed: ${error}`);
return false;
}
}
// 断开连接
public async disconnect(): Promise<void> {
if (this.gattClient) {
try {
await this.gattClient.disconnect();
this.gattClient.close();
this.gattClient = null;
this.connectedDevice = null;
this.discoveredServices = [];
} catch (error) {
console.error(`Disconnect failed: ${error}`);
}
}
}
// Getter 方法
public getIsScanning(): boolean {
return this.isScanning;
}
public getConnectedDevice(): DeviceInfo | null {
return this.connectedDevice;
}
public getDiscoveredServices(): BLEService[] {
return this.discoveredServices;
}
}
五、第四步:UI 界面开发(复制即用)
下面是完整的设备卡片和设备列表页面,可以直接复制到你的项目中。
5.1 设备卡片组件(显示每个扫描到的设备)
typescript
// ====== pages/ConnectionPage.ets ======
import { DeviceInfo, ConnectionState } from '../common/BLEManager';
@Component
struct DeviceCard {
@Prop device: DeviceInfo;
@Prop isConnected: boolean = false;
onConnect?: () => void;
// 根据信号强度获取颜色
private getSignalColor(rssi: number): string {
if (rssi >= -50) return '#4CAF50'; // 优秀 - 绿色
if (rssi >= -70) return '#FF9800'; // 良好 - 橙色
return '#FF5722'; // 较弱 - 红色
}
build() {
Row() {
// 左侧:设备信息
Column() {
Text(this.device.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.isConnected ? '#2196F3' : '#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(this.device.address)
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text(this.isConnected ? '已连接' : '未连接')
.fontSize(12)
.fontColor(this.isConnected ? '#4CAF50' : '#999999')
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 右侧:信号强度
Column() {
Text(`${this.device.rssi}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.getSignalColor(this.device.rssi))
Text('dBm')
.fontSize(12)
.fontColor('#999999')
}
.width(60)
.alignItems(HorizontalAlign.Center)
// 连接按钮
Button(this.isConnected ? '断开' : '连接')
.fontSize(14)
.backgroundColor(this.isConnected ? '#FF5722' : '#2196F3')
.fontColor(Color.White)
.height(36)
.width(70)
.margin({ left: 12 })
.onClick(() => {
if (this.onConnect) {
this.onConnect();
}
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.1)', offsetY: 2 })
}
}
5.2 设备列表页面(主页面,显示所有扫描到的设备)
typescript
// ====== pages/ConnectionPage.ets ======
// 这是蓝牙设备列表页面,显示扫描到的所有设备
import { BLEManager, DeviceInfo, ConnectionState } from '../common/BLEManager';
import { PermissionManager } from '../common/PermissionManager';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct ConnectionPage {
@State discoveredDevices: DeviceInfo[] = []; // 发现的设备列表
@State isScanning: boolean = false; // 是否正在扫描
@State connectedDevice: DeviceInfo | null = null; // 当前连接的设备
private bleManager: BLEManager = BLEManager.getInstance();
// 页面加载时调用
aboutToAppear(): void {
// 设置设备发现回调(发现新设备时添加到列表)
this.bleManager.setOnDeviceFoundCallback((device: DeviceInfo) => {
// 去重:如果已经存在就不加了
const exists = this.discoveredDevices.find(d => d.deviceId === device.deviceId);
if (!exists) {
this.discoveredDevices = [...this.discoveredDevices, device];
}
});
// 设置连接状态回调(连接成功/断开时提示用户)
this.bleManager.setOnConnectionStateChangedCallback((device: DeviceInfo, state: ConnectionState) => {
if (state === ConnectionState.CONNECTED) {
this.connectedDevice = device;
promptAction.showToast({ message: `✅ 已连接: ${device.name}` });
} else if (state === ConnectionState.DISCONNECTED) {
this.connectedDevice = null;
promptAction.showToast({ message: '设备已断开' });
}
});
}
// 页面销毁时调用(清理资源)
aboutToDisappear(): void {
this.bleManager.stopScan();
}
// 开始扫描(带权限检查)
private async startScan(): Promise<void> {
// 第一步:检查并申请权限
const hasPermission = await PermissionManager.getInstance().checkAndRequestBlePermissions();
if (!hasPermission) {
promptAction.showToast({ message: '请授予蓝牙权限后重试' });
return;
}
// 第二步:检查蓝牙是否开启
if (!this.bleManager.isBluetoothEnabled()) {
promptAction.showToast({ message: '请先开启蓝牙' });
return;
}
// 第三步:清空之前的设备列表,开始扫描
this.discoveredDevices = [];
this.bleManager.startScan();
this.isScanning = true;
// 10秒后自动停止扫描(防止一直扫描耗电)
setTimeout(() => {
this.stopScan();
}, 10000);
}
private stopScan(): void {
this.bleManager.stopScan();
this.isScanning = false;
}
private async onDeviceClick(device: DeviceInfo): Promise<void> {
if (this.connectedDevice?.deviceId === device.deviceId) {
// 已连接,断开
await this.bleManager.disconnect();
} else {
// 未连接,连接
this.stopScan();
await this.bleManager.connectDevice(device);
}
}
build() {
Column() {
// 标题栏
Row() {
Text('蓝牙设备')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Blank()
Button(this.isScanning ? '停止扫描' : '开始扫描')
.fontSize(14)
.backgroundColor(this.isScanning ? '#FF5722' : '#2196F3')
.onClick(() => {
if (this.isScanning) {
this.stopScan();
} else {
this.startScan();
}
})
}
.width('100%')
.padding(16)
// 扫描状态
if (this.isScanning) {
Row() {
LoadingProgress()
.width(20)
.height(20)
.color('#2196F3')
Text('正在扫描设备...')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(16)
}
// 设备列表
if (this.discoveredDevices.length === 0) {
Column() {
Text('📡')
.fontSize(48)
.margin({ bottom: 16 })
Text(this.isScanning ? '正在搜索设备...' : '未发现设备')
.fontSize(16)
.fontColor('#999999')
if (!this.isScanning) {
Text('点击"开始扫描"按钮搜索')
.fontSize(14)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
} else {
List({ space: 12 }) {
ForEach(this.discoveredDevices, (device: DeviceInfo) => {
ListItem() {
DeviceCard({
device: device,
isConnected: this.connectedDevice?.deviceId === device.deviceId,
onConnect: () => this.onDeviceClick(device)
})
}
}, (device: DeviceInfo) => device.deviceId)
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
5.3 服务/UUID 卡片组件(显示设备提供的服务和特征值)
typescript
// ====== pages/DeviceDetailPage.ets ======
import { BLEManager, BLEService, BLECharacteristic } from '../common/BLEManager';
import { router } from '@kit.ArkUI';
@Entry
@Component
struct DeviceDetailPage {
@State services: BLEService[] = [];
@State selectedServiceUuid: string = '';
@State selectedCharUuid: string = '';
private bleManager: BLEManager = BLEManager.getInstance();
aboutToAppear(): void {
this.services = this.bleManager.getDiscoveredServices();
}
// 导入UUID到设置页面
private importUUID(serviceUuid: string, charUuid: string): void {
router.pushUrl({
url: 'pages/SettingsPage',
params: {
importServiceUUID: serviceUuid,
importCharacteristicUUID: charUuid
}
});
}
@Builder
ServiceCard(service: BLEService, index: number) {
Column() {
// 服务标题
Row() {
Text(`服务 ${index + 1}`)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Blank()
Text('GATT Service')
.fontSize(12)
.fontColor('#2196F3')
.backgroundColor('rgba(33, 150, 243, 0.1)')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(4)
}
.width('100%')
.margin({ bottom: 8 })
// 服务 UUID
Text(service.serviceUuid)
.fontSize(12)
.fontColor('#666666')
.width('100%')
.margin({ bottom: 12 })
// 特征值列表
if (service.characteristics.length > 0) {
Text('特征值列表')
.fontSize(13)
.fontColor('#999999')
.width('100%')
.margin({ bottom: 8 })
ForEach(service.characteristics, (char: BLECharacteristic, charIndex: number) => {
Row() {
Column() {
Text(`特征 ${charIndex + 1}`)
.fontSize(12)
.fontColor('#333333')
Text(char.characteristicUuid)
.fontSize(11)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('导入')
.fontSize(12)
.height(28)
.backgroundColor('#2196F3')
.fontColor(Color.White)
.onClick(() => {
this.importUUID(service.serviceUuid, char.characteristicUuid);
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(4)
.margin({ bottom: 4 })
}, (char: BLECharacteristic) => char.characteristicUuid)
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 2, color: 'rgba(0,0,0,0.1)', offsetY: 1 })
}
build() {
Column() {
// 标题栏
Row() {
Button('返回')
.backgroundColor(Color.Transparent)
.fontColor('#2196F3')
.onClick(() => router.back())
Text('设备详情')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('')
.width(60)
}
.width('100%')
.padding(16)
// 服务列表
if (this.services.length === 0) {
Column() {
Text('未发现服务')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
} else {
Scroll() {
Column({ space: 12 }) {
ForEach(this.services, (service: BLEService, index: number) => {
this.ServiceCard(service, index)
}, (service: BLEService) => service.serviceUuid)
}
.width('100%')
.padding(16)
}
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
5.4 UUID 配置页面
typescript
// ====== pages/SettingsPage.ets ======
import { router } from '@kit.ArkUI';
@Entry
@Component
struct SettingsPage {
@State serviceUUID: string = '';
@State characteristicUUID: string = '';
@State filterEnabled: boolean = false;
@State filterDeviceName: string = '';
aboutToAppear(): void {
// 接收导入的 UUID
const params = router.getParams() as Record<string, string>;
if (params) {
if (params.importServiceUUID) {
this.serviceUUID = params.importServiceUUID;
}
if (params.importCharacteristicUUID) {
this.characteristicUUID = params.importCharacteristicUUID;
}
}
}
@Builder
UUIDConfigSection() {
Column() {
Text('UUID 配置')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.alignSelf(ItemAlign.Start)
.margin({ bottom: 16 })
// 服务 UUID
Row() {
Text('服务UUID')
.fontSize(14)
.fontColor('#666666')
.width(80)
TextInput({ placeholder: '请输入服务UUID', text: this.serviceUUID })
.layoutWeight(1)
.margin({ left: 12, right: 12 })
.onChange((value: string) => {
this.serviceUUID = value;
})
Button('重置')
.fontSize(12)
.backgroundColor('#F5F5F5')
.fontColor('#2196F3')
.width(60)
.height(32)
.onClick(() => {
this.serviceUUID = '';
})
}
.width('100%')
.margin({ bottom: 12 })
// 特征值 UUID
Row() {
Text('特征值UUID')
.fontSize(14)
.fontColor('#666666')
.width(80)
TextInput({ placeholder: '请输入特征值UUID', text: this.characteristicUUID })
.layoutWeight(1)
.margin({ left: 12, right: 12 })
.onChange((value: string) => {
this.characteristicUUID = value;
})
Button('重置')
.fontSize(12)
.backgroundColor('#F5F5F5')
.fontColor('#2196F3')
.width(60)
.height(32)
.onClick(() => {
this.characteristicUUID = '';
})
}
.width('100%')
.margin({ bottom: 8 })
Text('UUID格式:0000ffe1-0000-1000-8000-00805f9b34fb')
.fontSize(12)
.fontColor('#999999')
.alignSelf(ItemAlign.Start)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
@Builder
DeviceFilterSection() {
Column() {
Text('设备过滤器')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.alignSelf(ItemAlign.Start)
.margin({ bottom: 16 })
// 过滤器开关
Row() {
Text('启用设备名过滤')
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.filterEnabled })
.onChange((isOn: boolean) => {
this.filterEnabled = isOn;
})
}
.width('100%')
.margin({ bottom: 12 })
// 设备名输入
if (this.filterEnabled) {
TextInput({ placeholder: '请输入设备名关键词', text: this.filterDeviceName })
.width('100%')
.onChange((value: string) => {
this.filterDeviceName = value;
})
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
build() {
Column() {
// 标题栏
Row() {
Button('返回')
.backgroundColor(Color.Transparent)
.fontColor('#2196F3')
.onClick(() => router.back())
Text('设置')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Button('保存')
.backgroundColor(Color.Transparent)
.fontColor('#2196F3')
.onClick(() => {
// 保存配置逻辑
router.back();
})
}
.width('100%')
.padding(16)
Scroll() {
Column({ space: 16 }) {
this.UUIDConfigSection()
this.DeviceFilterSection()
}
.width('100%')
.padding(16)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
六、常见 UUID 参考(查文档时常用)
什么是 UUID? UUID 是服务和特征值的唯一标识,就像身份证号码一样。每个蓝牙设备都有自己的 UUID。
| 服务名称 | UUID | 用途说明 |
|---|---|---|
| 电池服务 | 0000180F-0000-1000-8000-00805F9B34FB |
读取电池电量 |
| 设备信息 | 0000180A-0000-1000-8000-00805F9B34FB |
读取设备名称、厂商等 |
| 心率服务 | 0000180D-0000-1000-8000-00805F9B34FB |
读取心率数据 |
| 通用访问 | 00001800-0000-1000-8000-00805F9B34FB |
设备名称、外观等 |
| 客户端配置描述符 | 00002902-0000-1000-8000-00805F9B34FB |
启用通知时需要写入 |
💡 小贴士 :不知道设备的 UUID?可以先连接设备,然后调用 getServices() 查看所有服务的 UUID。
七、最佳实践(踩坑总结)
7.1 必须遵守的规则
- 权限先行:扫描前必须检查并申请权限,否则扫描会静默失败
- 检查蓝牙状态:扫描前要检查蓝牙是否开启
- 资源释放:页面销毁时停止扫描、断开连接,否则会内存泄漏
- 状态同步:使用回调机制同步 UI 状态
7.2 常见问题排查
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 扫描不到设备 | 权限未授予 | 检查应用设置里的权限 |
| 扫描不到设备 | 蓝牙未开启 | 下拉状态栏检查蓝牙开关 |
| 扫描不到设备 | 设备没在广播 | 确认设备处于可发现状态 |
| 连接失败 | 设备已被其他手机连接 | 断开其他连接后重试 |
| 读写失败 | UUID 不对 | 检查服务UUID和特征值UUID |
| 读写失败 | 没有读/写权限 | 检查特征值的 properties |
7.3 写入数据的技巧
typescript
// 双重写入策略:先尝试 WRITE,失败后尝试 WRITE_NO_RESPONSE
public async sendData(serviceId: string, charId: string, data: ArrayBuffer): Promise<boolean> {
try {
// 第一次尝试:带响应的写入(更可靠)
await this.gattClient?.writeCharacteristicValue(char, ble.GattWriteType.WRITE);
return true;
} catch (error) {
try {
// 第二次尝试:不带响应的写入(更快但不确认)
await this.gattClient?.writeCharacteristicValue(char, ble.GattWriteType.WRITE_NO_RESPONSE);
return true;
} catch (e) {
return false;
}
}
}
为什么要双重写入? 因为不同设备支持的写入方式不同,有的只支持带响应,有的只支持不带响应。
八、学习路径建议
初学者建议按这个顺序学习:
- 先跑通权限 - 能成功弹出权限申请弹窗
- 再跑通扫描 - 能扫描到蓝牙设备
- 然后跑通连接 - 能连接到设备
- 最后跑通读写 - 能收发数据
学习资源
九、常见问题 FAQ
Q1: 为什么扫描不到任何设备?
按这个顺序检查:
- 应用设置 → 权限 → 蓝牙权限是否开启
- 下拉状态栏 → 蓝牙是否开启
- 设备是否在广播模式(可被发现)
- 设备是否已被其他手机连接
Q2: 连接成功但读写失败?
可能原因:
- UUID 不对 - 检查服务UUID和特征值UUID
- 没有具备读写权限 - 检查特征值的 properties
- 需要先启用通知 - 调用
setCharacteristicChangeNotification
Q3: 如何知道设备的 UUID?
两种方法:
- 查看设备说明书或压商文档
- 先连接设备,调用
getServices()查看所有服务
- 作者声明:个人拙作,仅总结了本人开发经验,专业性指导请参考官方文档,欢迎各位大佬斧正。本文档不是最简蓝牙调试工具开发文档,代码截取自成熟应用,欢迎下载易管闪联(星闪端,蓝牙端正在突破3.5)体验。