想象一下,你是一位名叫「安卓骑士」的勇敢探险家,你的使命是去神秘的「低功耗森林」(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),可以开始真正的交流了!
-
读取数据 (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); } } -
写入数据 (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); } } -
接收通知 (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探险", "正在断开和小环的连接...");
}
安卓骑士的通关秘籍 (关键点总结)
- 权限是钥匙:蓝牙、位置(6.0+扫描必需)一个不能少。
- 适配器是水晶球:
BluetoothAdapter是你的核心控制台。 - 扫描要省电:记得设时长(
SCAN_PERIOD),及时stopLeScan。 - GATT是桥梁:
BluetoothGatt代表连接,GattCallback监听桥梁状态(onConnectionStateChange最重要!)。 - 服务即功能区:
BluetoothGattService,用UUID找到目标功能区。 - 特征值是关键:
BluetoothGattCharacteristic,读写通知都在这里!注意属性位 (PROPERTY_READ/WRITE/NOTIFY/INDICATE)。 - 通知靠CCCD:想让设备主动发消息?必须写
ENABLE_NOTIFICATION_VALUE到它的CCCD描述符! - 读写要异步:
readCharacteristic(),writeCharacteristic()是触发操作,结果在回调里 (onCharacteristicRead/Write/Changed)。 - 通知实时到:订阅后,设备变化了会主动调用
onCharacteristicChanged。 - 断开必清理:
disconnect()+close() 两步走,避免133错误!这是血泪教训! - 主线程勿阻塞:BLE操作回调在Binder线程,更新UI记得用
runOnUiThread()或Handler。 - 分包大智慧:单次写数据不能超过20字节!大数据要拆开发,设备端再组装。
- 序列化操作:别同时读/写/写,等一个操作完成(回调了)再做下一个,否则容易失败。
- UUID有长短:16位短UUID是蓝牙联盟标准服务的,128位UUID是厂商自定义的。
现在,勇敢的安卓骑士,带上这份秘籍和代码,去低功耗森林里开启你的智慧设备探险之旅吧!记住,耐心和细心是征服BLE世界的关键!祝你探险顺利!