BLE之「安卓骑士」的勇敢探险

67 阅读10分钟

想象一下,你是一位名叫「安卓骑士」的勇敢探险家,你的使命是去神秘的「低功耗森林」(BLE世界)里,寻找并沟通那些神奇的「智慧村民」(BLE设备,比如手环、智能灯泡、传感器)。这些村民能量消耗极低,可以靠一颗小电池活很久,但他们只说一种特殊的语言——「GATT语」。我们的冒险就此开始!

​第一章:森林入口与通行证 (权限与初始化)​

在你进入森林前,森林守卫(Android系统)需要检查你的通行证:

java
Copy
// 在 AndroidManifest.xml 里申请通行证
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 对于 Android 6.0+,进入森林还需要位置许可(因为扫描设备需要) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 更精确 -->
<!-- 告诉守卫你的手机支持低功耗森林探险(可选,但推荐) -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

进入森林村庄(你的App),你需要找到「蓝牙魔法水晶球」(BluetoothAdapter),它是你和森林沟通的核心工具:

java
Copy
public class BleAdventureActivity extends AppCompatActivity {

    private BluetoothAdapter bluetoothAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 获取蓝牙魔法管理器
        BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        // 2. 获取水晶球核心 - BluetoothAdapter
        bluetoothAdapter = bluetoothManager.getAdapter();

        // 3. 检查水晶球是否发光(蓝牙是否可用)
        if (bluetoothAdapter == null) {
            // 哎呀!手机没有蓝牙功能,探险中止!
            Toast.makeText(this, "手机不支持蓝牙,无法探险!", Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        // 4. 检查水晶球是否激活(蓝牙是否开启)
        if (!bluetoothAdapter.isEnabled()) {
            // 弹出系统对话框请求用户激活水晶球
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        } else {
            // 水晶球已激活,可以开始扫描村民了!
            startScanningForVillagers();
        }
    }

    // ... (处理用户是否同意开启蓝牙的回调 onActivityResult)
}

​第二章:寻找智慧村民 (扫描设备)​

水晶球激活了!现在,你需要用它发射「侦测波束」(startLeScan),在森林里寻找发出特定信号的智慧村民。村民会不断广播喊:“我在这里!我是XX村的!我的地址是XX!”

java
Copy
private boolean isScanning = false;
private Handler scanHandler = new Handler(); // 一个计时小助手
private static final long SCAN_PERIOD = 10000; // 扫描10秒,省电!

private void startScanningForVillagers() {
    // 防止重复扫描
    if (isScanning) {
        return;
    }

    // 村民名单(用于存放找到的BluetoothDevice)
    List<BluetoothDevice> discoveredVillagers = new ArrayList<>();

    // 定义侦听器:当波束碰到村民时触发
    BluetoothAdapter.LeScanCallback scanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            // device: 遇到的村民对象(包含名字、地址等)
            // rssi: 信号强度(距离越近,值越大,负数的绝对值越小)
            // scanRecord: 村民广播的原始信息(可能包含服务UUID、自定义数据等)
            runOnUiThread(() -> { // 回到主线程更新UI
                Log.d("BLE探险", "发现村民: " + device.getName() + ", 地址: " + device.getAddress() + ", 信号强度: " + rssi);
                // 检查是不是你要找的村民(比如名字包含"智慧手环")
                if (device.getName() != null && device.getName().contains("智慧手环")) {
                    discoveredVillagers.add(device);
                    // 可以显示在列表里供用户选择连接
                }
            });
        }
    };

    // 开始扫描!
    isScanning = true;
    bluetoothAdapter.startLeScan(scanCallback); // 旧API,可用但建议用新的BluetoothLeScanner
    // 新API (API 21+) 更强大,可以过滤、设置扫描模式等,原理类似。

    // 让小助手10秒后停止扫描(省电!)
    scanHandler.postDelayed(() -> {
        if (isScanning) {
            bluetoothAdapter.stopLeScan(scanCallback);
            isScanning = false;
            Log.d("BLE探险", "扫描结束");
            // 现在你可以让用户从 discoveredVillagers 中选择一个村民连接
        }
    }, SCAN_PERIOD);
}

​第三章:建立友谊桥梁 (连接设备)​

你选择了一个叫「小环」的手环村民 (BluetoothDevice)。现在,你需要和它建立一条「友谊之桥」(GATT连接)来深入交流。

java
Copy
private BluetoothGatt bluetoothGatt; // 代表这座桥梁
private int connectionState = STATE_DISCONNECTED; // 记录桥梁状态

// 状态常量
private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;

public void connectToVillager(BluetoothDevice device) {
    if (bluetoothGatt != null) {
        // 如果之前有桥,先礼貌地拆掉旧的
        bluetoothGatt.close();
        bluetoothGatt = null;
    }

    // 建立新桥!需要指定连接回调监听器 (GattCallback)
    bluetoothGatt = device.connectGatt(this, false, gattCallback); // false: 非自动重连
    connectionState = STATE_CONNECTING;
    Log.d("BLE探险", "正在和小环建立桥梁...");
}

// **核心!** 桥梁状态监听器 - GattCallback
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {

    // 桥梁状态变化的回调(最重要的回调!)
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        String intentAction;
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            // 哇!桥建好了!
            connectionState = STATE_CONNECTED;
            Log.d("BLE探险", "成功连接小环!");
            // 连接成功后的第一件事:探索村庄结构(发现服务)
            gatt.discoverServices(); // 触发 onServicesDiscovered 回调
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            // 哎呀,桥断了!
            connectionState = STATE_DISCONNECTED;
            Log.w("BLE探险", "和小环的桥梁断开了!原因: " + status);
            // **非常重要!** 拆掉断桥释放资源(防止133错误!)
            gatt.close();
            bluetoothGatt = null;
        }
    }

    // 发现村庄结构(服务)完成的回调
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            Log.d("BLE探险", "成功发现小环的村庄结构(服务)!");
            // **关键步骤!** 现在可以获取服务并找到特征值了!
            findImportantCharacteristics(gatt);
        } else {
            Log.w("BLE探险", "探索村庄结构失败: " + status);
        }
    }

    // ... (其他重要回调:读取数据、写入结果、特征值通知等,后面章节讲解)
};

​第四章:探索村庄结构 (服务、特征值、描述符)​

恭喜你!桥建好了,小环也向你敞开了大门(onServicesDiscovered 成功)。现在,你需要探索小环村庄的内部结构。小环的村庄就像一个「服务树」(BluetoothGattService)。

  • ​服务 (Service)​​: 相当于村庄里的不同功能区。比如:

    • 心率服务 (UUID: 0000180d-0000-1000-8000-00805f9b34fb) - 专门管心率数据
    • 电池服务 (UUID: 0000180f-0000-1000-8000-00805f9b34fb) - 专门管电量
  • ​特征值 (Characteristic)​​: 每个功能区里的具体「信息点」或「操作点」。它是你和小环真正交互的地方!每个特征值有一个唯一的UUID。比如:

    • 心率服务 下面可能有:

      • 心率测量 (UUID: 00002a37-0000-1000-8000-00805f9b34fb) - ​​只能读​​ (PROPERTY_READ),这里存放着当前心率值。
      • 心率控制点 (UUID: xxxxx...) - ​​只能写​​ (PROPERTY_WRITE),你写指令到这里可以控制心率监测开关。
    • 电池服务 下面可能有:

      • 电量等级 (UUID: 00002a19-0000-1000-8000-00805f9b34fb) - ​​只能读​​,这里放着剩余电量百分比。
  • ​描述符 (Descriptor)​​: 对特征值的额外说明或设置项。最常用的是 ​​客户端特征配置描述符 (CCCD)​​。如果你想小环在心率变化时​​主动通知​​你,你需要在这个描述符里写入 ENABLE_NOTIFICATION_VALUE 或 ENABLE_INDICATION_VALUE 来订阅通知!

java
Copy
// 在 onServicesDiscovered 成功后被调用
private void findImportantCharacteristics(BluetoothGatt gatt) {
    // 1. 获取小环提供的所有服务列表
    List<BluetoothGattService> services = gatt.getServices();

    // 2. 遍历所有服务,寻找我们关心的服务
    for (BluetoothGattService service : services) {
        String serviceUuid = service.getUuid().toString();

        // 例1:寻找心率服务
        if (serviceUuid.equalsIgnoreCase("0000180d-0000-1000-8000-00805f9b34fb")) {
            Log.d("BLE探险", "找到心率功能区!");
            // 3. 在服务里寻找特征值
            List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
            for (BluetoothGattCharacteristic characteristic : characteristics) {
                String charUuid = characteristic.getUuid().toString();
                int properties = characteristic.getProperties(); // 属性位(读/写/通知等)

                // 例1.1:寻找心率测量特征值 (只读)
                if (charUuid.equalsIgnoreCase("00002a37-0000-1000-8000-00805f9b34fb")) {
                    Log.d("BLE探险", "  找到心率测量点(只读)");
                    heartRateMeasurementChar = characteristic; // 保存起来后面用
                }
                // 例1.2:寻找心率控制点特征值 (只写)
                else if (charUuid.equalsIgnoreCase("XXXXX...")) { // 替换成实际UUID
                    Log.d("BLE探险", "  找到心率控制点(只写)");
                    heartRateControlPointChar = characteristic;
                }
            }
        }

        // 例2:寻找电池服务 (类似过程)
        if (serviceUuid.equalsIgnoreCase("0000180f-0000-1000-8000-00805f9b34fb")) {
            // ... 寻找电量等级特征值 ...
        }
    }

    // **关键操作:开启心率变化通知!**
    if (heartRateMeasurementChar != null) {
        // a. 告诉桥梁对象:我要监听这个特征值的通知
        gatt.setCharacteristicNotification(heartRateMeasurementChar, true);

        // b. 找到这个特征值的CCCD描述符(固定UUID)
        BluetoothGattDescriptor descriptor = heartRateMeasurementChar.getDescriptor(
                UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); // CCCD的标准UUID

        if (descriptor != null) {
            // c. 向CCCD写入魔法指令:开启通知!
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            // d. 执行写入操作(触发 onDescriptorWrite 回调)
            gatt.writeDescriptor(descriptor);
        } else {
            Log.w("BLE探险", "找不到心率测量的CCCD!");
        }
    }
}

​第五章:村民交流之道 (读写数据 & 通知)​

现在你找到了小环村庄里的「信息点」(Characteristic),可以开始真正的交流了!

  1. ​读取数据 (Read)​​: 你想主动问问小环当前的心率是多少?

    java
    Copy
    // 假设 heartRateMeasurementChar 是我们之前保存的心率测量特征值
    if (bluetoothGatt != null && heartRateMeasurementChar != null) {
        boolean readStarted = bluetoothGatt.readCharacteristic(heartRateMeasurementChar);
        if (readStarted) {
            Log.d("BLE探险", "已发送读取心率指令...");
        } else {
            Log.w("BLE探险", "发送读取心率指令失败!");
        }
    }
    
    // **监听读取结果** - 在 GattCallback 中
    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicRead(gatt, characteristic, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            // 检查是不是我们关心的特征值
            if (characteristic.getUuid().equals(heartRateMeasurementChar.getUuid())) {
                // 解析心率数据!(具体格式看设备文档)
                byte[] data = characteristic.getValue();
                int heartRate = data[1]; // 假设第2个字节是心率值(简化,实际格式更复杂)
                Log.d("BLE探险", "读取到当前心率: " + heartRate + " bpm");
                runOnUiThread(() -> updateHeartRateUI(heartRate)); // 更新UI
            }
        } else {
            Log.w("BLE探险", "读取心率失败!状态码: " + status);
        }
    }
    
  2. ​写入数据 (Write)​​: 你想让小环开始测量心率(假设需要发送指令)。

    java
    Copy
    // 假设 heartRateControlPointChar 是我们之前保存的心率控制点特征值(可写)
    if (bluetoothGatt != null && heartRateControlPointChar != null) {
        // 构造指令(例如:0x01 代表开始测量)
        byte[] command = new byte[]{0x01};
        heartRateControlPointChar.setValue(command);
        // 执行写入操作 (注意:属性必须是 WRITE 或 WRITE_NO_RESPONSE)
        boolean writeStarted = bluetoothGatt.writeCharacteristic(heartRateControlPointChar);
        if (writeStarted) {
            Log.d("BLE探险", "已发送开始测量指令...");
        } else {
            Log.w("BLE探险", "发送开始测量指令失败!");
        }
    }
    
    // **监听写入结果** - 在 GattCallback 中 (对于 WRITE 属性会触发,WRITE_NO_RESPONSE 不会)
    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicWrite(gatt, characteristic, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            if (characteristic.getUuid().equals(heartRateControlPointChar.getUuid())) {
                Log.d("BLE探险", "开始测量指令写入成功!");
            }
        } else {
            Log.w("BLE探险", "开始测量指令写入失败!状态码: " + status);
        }
    }
    
  3. ​接收通知 (Notify)​​: 小环的心率变化了,它主动告诉你!(这是我们之前订阅的)

    java
    Copy
    // **监听通知** - 在 GattCallback 中
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        super.onCharacteristicChanged(gatt, characteristic);
        // 检查是不是心率测量特征值发来的通知
        if (characteristic.getUuid().equals(heartRateMeasurementChar.getUuid())) {
            // 解析通知里的心率数据!(格式和读取时一样)
            byte[] data = characteristic.getValue();
            int heartRate = data[1]; // 假设第2个字节是心率值
            Log.d("BLE探险", "收到心率变化通知: " + heartRate + " bpm");
            runOnUiThread(() -> updateHeartRateUI(heartRate)); // 更新UI - 实时刷新!
        }
    }
    

​第六章:告别与清理 (断开连接)​

探险结束,是时候和小环说再见了。一定要​​礼貌地拆掉友谊之桥并清理场地​​!否则森林守卫会不高兴(出现133错误,连接资源耗尽),下次你可能连不上其他村民,甚至小环也连不上了!

java
Copy
public void disconnectFromVillager() {
    if (bluetoothGatt == null) {
        return;
    }
    // 1. 先请求断开连接 (会触发 onConnectionStateChange -> DISCONNECTED)
    bluetoothGatt.disconnect();
    // 2. 在 onConnectionStateChange 的 DISCONNECTED 分支里,我们做了:
    //    gatt.close();
    //    bluetoothGatt = null;
    Log.d("BLE探险", "正在断开和小环的连接...");
}

​安卓骑士的通关秘籍 (关键点总结)​

  1. ​权限是钥匙​​:蓝牙、位置(6.0+扫描必需)一个不能少。
  2. ​适配器是水晶球​​:BluetoothAdapter 是你的核心控制台。
  3. ​扫描要省电​​:记得设时长(SCAN_PERIOD),及时stopLeScan
  4. ​GATT是桥梁​​:BluetoothGatt 代表连接,GattCallback 监听桥梁状态(​onConnectionStateChange 最重要!​​)。
  5. ​服务即功能区​​:BluetoothGattService,用UUID找到目标功能区。
  6. ​特征值是关键​​:BluetoothGattCharacteristic,读写通知都在这里!​​注意属性位​​ (PROPERTY_READ/WRITE/NOTIFY/INDICATE)。
  7. ​通知靠CCCD​​:想让设备主动发消息?必须写 ENABLE_NOTIFICATION_VALUE 到它的CCCD描述符!
  8. ​读写要异步​​:readCharacteristic()writeCharacteristic() 是触发操作,结果在回调里 (onCharacteristicRead/Write/Changed)。
  9. ​通知实时到​​:订阅后,设备变化了会主动调用 onCharacteristicChanged
  10. ​断开必清理​​:​disconnect() + close()​ 两步走,​​避免133错误​​!这是血泪教训!
  11. ​主线程勿阻塞​​:BLE操作回调在Binder线程,更新UI记得用 runOnUiThread() 或 Handler
  12. ​分包大智慧​​:单次写数据不能超过​​20字节​​!大数据要拆开发,设备端再组装。
  13. ​序列化操作​​:别同时读/写/写,等一个操作完成(回调了)再做下一个,否则容易失败。
  14. ​UUID有长短​​:16位短UUID是蓝牙联盟标准服务的,128位UUID是厂商自定义的。

现在,勇敢的安卓骑士,带上这份秘籍和代码,去低功耗森林里开启你的智慧设备探险之旅吧!记住,耐心和细心是征服BLE世界的关键!祝你探险顺利!