业务功能·

基于uni-app的蓝牙设备连接开发实践

本文结合uni-app跨平台框架,通过封装可复用的蓝牙连接模块,实现从权限管理到设备通信的全流程控制,并提供完整的实战案例,帮助开发者快速落地蓝牙设备交互功能。

背景

在智能硬件与移动应用深度融合的场景中,蓝牙连接是实现设备交互的核心桥梁。本文结合uni-app跨平台框架,通过封装可复用的蓝牙连接模块,实现从权限管理到设备通信的全流程控制,并提供完整的实战案例,帮助开发者快速落地蓝牙设备交互功能。

实现

一、蓝牙连接核心模块设计

我们通过两个核心类实现逻辑分层:BluetoothConnector 处理业务逻辑与状态管理,InnerBluetoothConnector 封装uni-app原生蓝牙API,确保代码的可维护性与跨平台兼容性。

// 蓝牙连接核心类
class BluetoothConnector {
    constructor() {
        // 内部蓝牙连接实例
        this.connector = new InnerBluetoothConnector();
        // 连接状态:0 表示未连接,1 表示扫描中,2 表示已连接
        this.currentState = 0;
        // 存储扫描到的设备列表
        this.deviceList = [];
        // 存储当前选中要连接的设备
        this.selectedDevice = null;
        // 常见蓝牙错误码对应的错误信息
        this.errorMap = {
            10000: '蓝牙未初始化',
            10001: '蓝牙不可用',
            10002: '未找到指定设备',
            10003: '连接失败',
            10004: '未找到指定服务',
            10005: '未找到指定特征值',
            10006: '连接已断开',
            10007: '当前操作不支持',
            10008: '系统蓝牙异常',
            10009: '系统版本过低,不支持蓝牙功能',
            10012: '连接超时',
            10013: '设备 ID 无效'
        };
    }

    // 初始化蓝牙模块,包括权限检查、适配器初始化和监听设置
    async init() {
        try {
            await this.checkPermissions();
            await this.initAdapter();
            this.setupAdapterListeners();
        } catch (error) {
            console.error('蓝牙初始化失败:', error);
            throw error;
        }
    }

    // 检查蓝牙权限,若未授权则请求授权
    checkPermissions() {
        return new Promise((resolve, reject) => {
            uni.getSetting({
                success: ({ authSetting }) => {
                    if (authSetting['scope.bluetooth']) {
                        resolve();
                    } else {
                        uni.authorize({
                            scope: 'scope.bluetooth',
                            success: resolve,
                            fail: () => {
                                uni.showModal({
                                    title: '权限申请',
                                    content: '需要蓝牙权限以连接设备',
                                    success: ({ confirm }) => {
                                        if (confirm) {
                                            uni.openSetting({
                                                success: () => {
                                                    uni.getSetting({
                                                        success: ({ authSetting }) => {
                                                            if (authSetting['scope.bluetooth']) {
                                                                resolve();
                                                            } else {
                                                                reject(new Error('用户未授予蓝牙权限'));
                                                            }
                                                        },
                                                        fail: reject
                                                    });
                                                },
                                                fail: reject
                                            });
                                        } else {
                                            reject(new Error('用户拒绝授予蓝牙权限'));
                                        }
                                    },
                                    fail: reject
                                });
                            }
                        });
                    }
                },
                fail: reject
            });
        });
    }

    // 初始化蓝牙适配器
    initAdapter() {
        return new Promise((resolve, reject) => {
            uni.openBluetoothAdapter({
                success: () => {
                    resolve();
                    this.setupAdapterListeners();
                },
                fail: (err) => {
                    if (err.errMsg.includes('already opened')) {
                        resolve();
                    } else {
                        reject(new Error('蓝牙初始化失败: ' + this.getErrorMessage(err.errCode)));
                    }
                }
            });
        });
    }

    // 开始扫描设备
    startScan() {
        return new Promise((resolve, reject) => {
            if (this.currentState === 1) {
                reject(new Error('正在扫描中,请勿重复操作'));
                return;
            }
            this.currentState = 1;
            this.connector.scanEcgCards({
                duration: 10000,
                discovery: (devices) => {
                    // 去重处理
                    const newDevices = devices.filter(
                        d =>!this.deviceList.some(exist => exist.device.deviceId === d.deviceId)
                    );
                    this.deviceList = [...this.deviceList, ...newDevices];
                },
                success: () => {
                    this.currentState = 0;
                    resolve(this.deviceList);
                },
                fail: (err) => {
                    this.currentState = 0;
                    reject(new Error('设备扫描失败: ' + this.getErrorMessage(err.errCode)));
                }
            });
        });
    }

    // 连接指定设备
    connect(device) {
        return new Promise((resolve, reject) => {
            if (this.currentState === 2) {
                reject(new Error('已连接设备,请勿重复连接'));
                return;
            }
            this.connector.connect({
                device,
                success: () => {
                    this.currentState = 2;
                    this.selectedDevice = device;
                    resolve();
                },
                fail: (err) => {
                    this.currentState = 0;
                    reject(new Error('设备连接失败: ' + this.getErrorMessage(err.errCode)));
                }
            });
        });
    }

    // 断开当前连接的设备
    disconnect() {
        return new Promise((resolve, reject) => {
            if (this.currentState!== 2) {
                reject(new Error('未连接设备,无需断开'));
                return;
            }
            this.connector.disconnect({
                success: () => {
                    this.currentState = 0;
                    this.selectedDevice = null;
                    resolve();
                },
                fail: (err) => {
                    this.currentState = 2;
                    reject(new Error('设备断开失败: ' + this.getErrorMessage(err.errCode)));
                }
            });
        });
    }

    // 设置连接状态变化的回调函数
    setConnectCallback(callback) {
        this.connector.setConnectCallback({
            disconnectionCallback: () => {
                this.currentState = 0;
                this.selectedDevice = null;
                if (typeof callback === 'function') {
                    callback();
                }
            }
        });
    }

    // 根据错误码获取对应的错误信息
    getErrorMessage(errCode) {
        return this.errorMap[errCode] || `未知错误: ${errCode}`;
    }

    // 设置蓝牙适配器状态变化的监听
    setupAdapterListeners() {
        uni.onBluetoothAdapterStateChange((res) => {
            if (!res.available) {
                if (this.currentState === 2) {
                    this.disconnect().catch((err) => {
                        console.error('蓝牙适配器不可用,断开连接失败:', err);
                    });
                }
                this.currentState = 0;
                this.deviceList = [];
                this.selectedDevice = null;
            }
        });
    }
}

// 内部蓝牙连接类,封装具体的蓝牙操作
class InnerBluetoothConnector {
    // 扫描设备
    scanEcgCards(options) {
        const { duration, discovery, success, fail } = options;
        uni.startBluetoothDevicesDiscovery({
            services: [],
            allowDuplicatesKey: false,
            success: () => {
                uni.onBluetoothDeviceFound((res) => {
                    if (discovery) {
                        discovery(res.devices);
                    }
                });
                setTimeout(() => {
                    uni.stopBluetoothDevicesDiscovery({
                        success: () => {
                            if (success) {
                                success();
                            }
                        },
                        fail: (err) => {
                            if (fail) {
                                fail(err);
                            }
                        }
                    });
                }, duration);
            },
            fail: (err) => {
                if (fail) {
                    fail(err);
                }
            }
        });
    }

    // 连接设备
    connect(options) {
        const { device, success, fail } = options;
        uni.createBLEConnection({
            deviceId: device.deviceId,
            success: () => {
                if (success) {
                    success();
                }
            },
            fail: (err) => {
                if (fail) {
                    fail(err);
                }
            }
        });
    }

    // 断开连接
    disconnect(options) {
        const { success, fail } = options;
        if (this.connectedDevice) {
            uni.closeBLEConnection({
                deviceId: this.connectedDevice.deviceId,
                success: () => {
                    if (success) {
                        success();
                    }
                },
                fail: (err) => {
                    if (fail) {
                        fail(err);
                    }
                }
            });
        } else {
            if (fail) {
                fail(new Error('未连接设备'));
            }
        }
    }

    // 设置连接状态变化的回调
    setConnectCallback(options) {
        const { disconnectionCallback } = options;
        uni.onBLEConnectionStateChange((res) => {
            if (!res.connected && this.connectedDevice && res.deviceId === this.connectedDevice.deviceId) {
                if (disconnectionCallback) {
                    disconnectionCallback();
                }
            }
        });
    }
}

// 导出单例对象
const connector = new BluetoothConnector();
export default connector;

二、关键功能实现细节

  1. 状态机驱动的流程控制
    通过currentState严格控制操作流程,避免异步竞态问题:
    • 扫描中(1)时禁止重复扫描
    • 已连接(2)时禁止重复连接
    • 适配器关闭时自动重置所有状态
  2. 权限处理最佳实践
    // 渐进式权限申请(检查 → 申请 → 设置页引导)
    checkPermissions() {
        return new Promise((resolve, reject) => {
            uni.getSetting({
                success: ({ authSetting }) => {
                    authSetting['scope.bluetooth'] ? resolve() : this.requestPermission(resolve, reject);
                }
            });
        });
    }
    
    requestPermission(resolve, reject) {
        uni.authorize({
            scope: 'scope.bluetooth',
            success: resolve,
            fail: () => this.showPermissionModal(reject);
        });
    }
    
    showPermissionModal(reject) {
        uni.showModal({
            title: '权限申请',
            content: '需要蓝牙权限以连接设备',
            success: ({ confirm }) => 
                confirm ? uni.openSetting() : reject(new Error('权限拒绝')),
        });
    }
    
  3. 设备去重与过滤
    • 基于deviceId进行唯一性校验
    • 过滤信号强度弱的设备(RSSI > -80dBm)
    • 按信号强度降序排序
  4. 健壮的错误处理
    通过errorMap映射uni-app蓝牙错误码,提供清晰的错误提示:
    getErrorMessage(errCode) {
        return this.errorMap[errCode] || `未知错误(代码:${errCode}`;
    }
    

案例

以下是一个完整的Vue组件,演示如何使用封装好的BluetoothConnector实现设备连接功能:

<template>
  <view class="bluetooth-container">
    <!-- 连接状态按钮 -->
    <button 
      :disabled="isScanning || isConnecting" 
      @click="handleConnect"
    >
      {{ currentStateText }}
    </button>

    <!-- 设备列表(弹窗显示) -->
    <uni-popup v-model="showDeviceList">
      <view class="device-item" v-for="device in filteredDevices" :key="device.deviceId">
        <text>{{ device.name || '未知设备' }}</text>
        <text class="rssi">信号:{{ device.RSSI }}dBm</text>
      </view>
    </uni-popup>
  </view>
</template>

<script setup>
import { ref, computed, onUnmounted } from 'vue';
import connector from './bluetooth_connector.js';

// 状态映射
const currentState = ref(connector.currentState);
const isScanning = computed(() => currentState.value === 1);
const isConnecting = computed(() => currentState.value === 2);
const currentStateText = computed(() => 
  isScanning.value ? '扫描中...' : 
  isConnecting.value ? '已连接' : '搜索设备'
);

// 过滤后的设备列表(信号过滤+排序)
const filteredDevices = computed(() => 
  connector.deviceList
    .filter(d => d.RSSI > -80)
    .sort((a, b) => b.RSSI - a.RSSI)
);

// 连接处理
const handleConnect = async () => {
  if (isConnecting.value) {
    // 已连接时断开连接
    await connector.disconnect();
    return;
  }
  try {
    await connector.init(); // 初始化权限和适配器
    await connector.startScan(); // 启动设备扫描
    showDeviceList.value = true; // 显示设备列表
  } catch (error) {
    uni.showToast({ title: error.message, icon: 'none' });
  }
};

// 设备选择
const selectDevice = (device) => {
  connector.connect(device)
    .then(() => {
      showDeviceList.value = false;
      uni.showToast({ title: '连接成功', icon: 'success' });
    })
    .catch((err) => {
      uni.showToast({ title: err.message, icon: 'none' });
    });
};

// 页面卸载时清理资源
onUnmounted(() => {
  connector.resetConnection(); // 断开连接并清理监听
});
</script>

<style scoped>
.bluetooth-container {
  padding: 20px;
  text-align: center;
}

button {
  padding: 10px 20px;
  background-color: #1677FF;
  color: white;
  border-radius: 4px;
  border: none;
  margin-bottom: 10px;
}

.device-item {
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.rssi {
  color: #666;
  font-size: 0.9em;
}
</style>

最佳实践与注意事项

  1. 资源释放
    • 在页面卸载时调用connector.resetConnection(),确保断开连接并移除所有监听
    • 使用uni.removeAllBluetoothListeners()避免内存泄漏
  2. 性能优化
    • 限制单次扫描时长(建议10-15秒),降低功耗
    • 实时增量更新设备列表,避免全量刷新
  3. 跨平台适配
    • 处理iOS和Android在蓝牙权限提示、状态回调上的差异
    • 使用uni.getSystemInfo()判断平台,添加特殊逻辑(如iOS后台扫描)
  4. 用户体验
    • 连接过程中显示加载状态,避免重复点击
    • 对弱信号设备进行视觉弱化,优先展示强信号设备

总结

通过将蓝牙连接逻辑封装为独立的BluetoothConnector模块,开发者可专注于业务功能实现,大幅降低跨设备开发成本。核心优势包括:

  • 状态驱动:通过状态机确保操作流程的原子性
  • 错误闭环:完善的权限处理与错误提示机制
  • 资源安全:严格的状态校验与资源释放策略

此方案适用于心率监测、智能穿戴、医疗设备等多种蓝牙交互场景。实际开发中,只需根据设备特定的GATT服务/特征值扩展数据收发逻辑,即可快速落地完整的蓝牙通信功能。