ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android BLE开发避坑指南:从官方Demo到毕设落地的完整链路

Android BLE开发避坑指南:从官方Demo到毕设落地的完整链路

简介:本资源是基于Android官方蓝牙低功耗(BLE)开发指南实现的完整Demo工程,面向Android应用开发者及嵌入式通信初学者,聚焦BLE核心流程实践:设备扫描(scan)、连接建立与断开(connection/disconnect)、服务发现(discover service)及UUID交互等关键能力。压缩包共70个文件,涵盖26个Class类文件(含BLE状态管理与回调逻辑)、15个XML布局与配置资源、9个PNG图标素材、7个Java源码文件(主Activity与BluetoothAdapter操作封装),以及APK可运行包、JAR依赖库和项目构建配置文件(project.properties、.classpath等),整体仅1.34MB,轻量易导入。已有525人学习下载,代码结构清晰、模块职责分明,完整复现了Android 4.0+ BLE典型通信链路,附带可直接调试的APK与详细Manifest权限声明,是理解Android BLE协议栈集成与调试的优质入门参考。

1. 为什么官方 BLE Demo 不是“抄完就能跑”,而是你毕设/课设最该啃透的硬骨头?

很多同学拿到 Android 官方 BLE Demo(比如BluetoothLeGatt示例)第一反应是:「官方出的,肯定开箱即用」——结果一导入 Android Studio 就报Cannot resolve symbol 'BluetoothLeService',改完依赖又卡在扫描没回调,连上设备后读特征值返回0x80错误码,服务发现永远停在onServicesDiscovered不触发。这不是你环境有问题,而是 BLE 协议栈在 Android 上的落地本身就带着三重「玄学门槛」:系统版本对蓝牙权限的差异化处理(Android 12+ 要求BLUETOOTH_SCAN动态授权)、HCI 层与 GATT 层的状态机耦合(scan → connect → discover → read/write 是强时序链,断一环全崩)、以及 UUID 解析的隐式陷阱(16-bit 标准 UUID 被自动补全为 128-bit,但自定义服务 UUID 若未按规范格式声明,getUuid()返回值会错位)。这份 Demo 不是教学玩具,它是 Google 工程师把 BLE 开发中所有「血泪经验」压缩进一个可运行工程的黑匣子。它适合两类人:一类是正在做蓝牙通信类毕设/课设、需要交一份「能连、能读、能写、能答辩」完整方案的学生;另一类是刚接手 BLE 模块开发、需要快速建立「协议-代码-硬件」映射认知的工程师。下面我就带你一层层拆开这个黑匣子,不讲抽象概念,只告诉你每一行关键代码为什么这么写、改哪里会翻车、验收时怎么证明它真能用。

2. 总体架构设计:基于BluetoothLeGatt的四层职责划分与状态流闭环

官方BluetoothLeGattDemo 并非单 Activity 堆砌,而是一个典型的「UI-Service-BLE-Device」四层分治结构。这种设计不是为了炫技,而是为了解耦 BLE 状态机与界面生命周期——因为BluetoothAdapter和BluetoothGatt对象一旦被 Activity 销毁就失效,而 BLE 连接过程动辄数秒,用户切后台再回来,连接状态必须存活。我一般会强制要求学生在课设里保留这个分层,哪怕只是做个简单读温湿度的 Demo,也得先搭好骨架,否则后期加通知、后台扫描、多设备管理时必然重构。

2.1 四层模块职责与交互边界

  • UI 层(DeviceControlActivity):只负责展示扫描列表、连接状态、服务/特征值树形结构。所有蓝牙操作(startScan、connect、readCharacteristic)都通过LocalBroadcastManager发送 Intent 触发 Service,绝不直接调用BluetoothAdapter。
  • Service 层(BluetoothLeService):核心状态中枢。持有BluetoothGatt实例,封装connectGatt()、discoverServices()、readCharacteristic()等 API 调用,并通过BroadcastReceiver向 UI 层广播EXTRA_DATA(如ACTION_GATT_CONNECTED)。关键点在于:它用startForegroundService()启动,避免 Android 8.0+ 后台服务被杀。
  • BLE 协议层(BluetoothGattCallback子类):真正的协议解析器。onConnectionStateChange()处理连接成败,onServicesDiscovered()解析服务树,onCharacteristicRead()拿到原始字节数组。这里最容易被忽略的是status参数——它不是连接成功与否的标志,而是 GATT 操作的底层返回码(BluetoothGatt.GATT_SUCCESS才是成功,0x80是GATT_INVALID_HANDLE,说明你读的 characteristic handle 不存在)。
  • Device 层(模拟外设或真实硬件):Demo 中用nRF ConnectApp 模拟即可,但必须确认其广播包含0x1809(Temperature)等标准服务 UUID,且特征值支持READ属性。若用自定义固件,需确保onServicesDiscovered()返回的服务数量 ≥ 1,否则流程直接中断。

提示:不要在onServicesDiscovered()里直接调用readCharacteristic()!必须先用gatt.getService(uuid).getCharacteristic(uuid)获取有效实例,否则readCharacteristic(null)会静默失败。

2.2 状态机流转图:从扫描到数据读取的七步关键节点

整个 BLE 交互不是线性流程,而是一个带异常分支的状态机。官方 Demo 用mConnectionState枚举(STATE_DISCONNECTED/STATE_CONNECTING/STATE_CONNECTED/STATE_DISCONNECTING)标记当前阶段,但真正驱动流转的是BluetoothGattCallback的回调链:

  1. startScan()→ 触发onScanResult()(扫描到设备)
  2. connectGatt()→ 触发onConnectionStateChange()(status=2 表示连接中,status=0 表示成功)
  3. discoverServices()→ 触发onServicesDiscovered()(status=0 才继续)
  4. getService().getCharacteristic()→ 获取有效 Characteristic 实例
  5. setCharacteristicNotification()→ 开启通知(需同时写CLIENT_CHARACTERISTIC_CONFIGdescriptor)
  6. readCharacteristic()或writeCharacteristic()→ 主动读写
  7. close()+disconnect()→ 清理资源(必须成对调用,否则设备端可能卡死)

这七步里,第 2 步和第 3 步耗时最长(尤其在低端手机上),必须给 UI 加 loading 提示;第 5 步若漏写 descriptor,onCharacteristicChanged()永远不会触发——这是答辩时老师最爱问的「为什么通知收不到」问题。

2.3 权限与兼容性配置:AndroidManifest.xml 与 runtime check 的双保险

官方 Demo 在AndroidManifest.xml中声明了全部必要权限,但实际运行时仍需动态申请(Android 12+ 强制):

<!-- 必须声明,否则编译报错 --> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Android 12+ 新增 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

但光有声明不够。DeviceControlActivity的onCreate()中必须插入 runtime check:

// Android 12+ 需要 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}, PERMISSION_REQUEST_CODE); return; } } else { // Android 11 及以下,只需 ACCESS_FINE_LOCATION(因扫描需定位) if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_CODE); return; } }

注意:ACCESS_FINE_LOCATION在 Android 12+ 已被弃用,但某些旧设备仍需它来启用扫描,所以建议if-else分支保留。如果只申请BLUETOOTH_SCAN而不申请BLUETOOTH_CONNECT,connectGatt()会直接抛SecurityException,Logcat 显示Permission Denial: is not exported from uid—— 这个错误码非常误导人,实际就是权限没给全。

3. 核心模块实现:扫描、连接、服务发现与特征值读写的四段关键代码

官方 Demo 的价值不在 UI,而在BluetoothLeService.java里那四段被反复调用的核心逻辑。我把它们拆解成可独立复用的代码块,并标注每个参数的「为什么必须这样设」。

3.1 扫描启动:startScan()的过滤器与超时控制

// BluetoothLeService.java 中 startScan() 方法 private void scanLeDevice(final boolean enable) { final BluetoothAdapter adapter = mBluetoothAdapter; if (enable) { // Step 1: 清空旧扫描结果(避免重复添加) mLeDevices.clear(); // Step 2: 创建扫描过滤器(可选,但强烈建议) List<ScanFilter> filters = new ArrayList<>(); ScanFilter.Builder builder = new ScanFilter.Builder(); // 过滤特定服务 UUID(如体温计常用 0x1809) builder.setServiceUuid(ParcelUuid.fromString("00001809-0000-1000-8000-00805f9b34fb")); filters.add(builder.build()); // Step 3: 设置扫描设置(重点:SCAN_MODE_LOW_LATENCY 保证响应速度) ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 关键!默认 SCAN_MODE_BALANCED 响应慢 .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .build(); // Step 4: 启动扫描(带超时,防止无限扫) mScanner.startScan(filters, settings, mScanCallback); mHandler.postDelayed(() -> { if (mScanning) { mScanner.stopScan(mScanCallback); // 超时主动停 broadcastUpdate(ACTION_SCAN_TIMEOUT); } }, SCAN_PERIOD); // SCAN_PERIOD = 10000ms } else { mScanner.stopScan(mScanCallback); } }

设计说明:

  • ScanFilter不是必须项,但加了它能大幅减少无效设备干扰。若你的硬件广播包不含该 UUID,过滤器会让它彻底消失——所以调试初期建议先注释掉filters,确认能扫到设备后再加。
  • SCAN_MODE_LOW_LATENCY是关键参数。官方文档说它「消耗更多电量但扫描更快」,实测在小米 12 上,BALANCED模式下扫描延迟达 2~3 秒,而LOW_LATENCY可压到 300ms 内,这对需要快速配网的课设场景至关重要。
  • mHandler.postDelayed()是防呆设计。很多学生忘记停扫描,导致mLeDevices越积越多,列表卡顿,甚至onScanResult()回调堆积引发 ANR。

3.2 连接建立:connectGatt()的 autoConnect 陷阱与重连策略

// BluetoothLeService.java 中 connect() 方法 public boolean connect(final String address) { if (mBluetoothAdapter == null || address == null) { return false; } // Step 1: 先尝试断开旧连接(避免 handle 冲突) if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) { mBluetoothGatt.disconnect(); // 注意:disconnect() 不等于 close() return true; } // Step 2: 获取设备对象(address 是 MAC 地址字符串) final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) { return false; } // Step 3: 关键!autoConnect = false(官方 Demo 默认值) mBluetoothGatt = device.connectGatt(this, false, mGattCallback); mBluetoothDeviceAddress = address; return true; }

设计说明:

  • autoConnect = false是硬性要求。设为true时,系统会在设备进入范围时自动重连,但mGattCallback的onConnectionStateChange()会多次触发,且status可能为GATT_FAILURE,导致状态混乱。课设答辩时,老师若问「如何保证连接稳定性」,你就答:「禁用 autoConnect,由 UI 层显式控制连接时机,并在onConnectionStateChange()中做重试逻辑」。
  • disconnect()和close()必须区分:disconnect()是发送断开请求给外设,close()是释放本地BluetoothGatt资源。正确顺序是disconnect()→ 等待onConnectionStateChange()返回STATE_DISCONNECTED→ 再调用close()。漏掉close()会导致内存泄漏,多次连接后 App 崩溃。

3.3 服务发现:discoverServices()的异步阻塞与 UUID 解析坑

// BluetoothLeService.java 中 initialize() 方法(连接成功后调用) public boolean initialize() { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return false; } // Step 1: 请求 MTU(可选,但大包传输必须) mBluetoothGatt.requestMtu(512); // 最大 512 字节,需外设支持 // Step 2: 发起服务发现(异步,结果在 onServicesDiscovered()) boolean result = mBluetoothGatt.discoverServices(); Log.d(TAG, "discoverServices() returned: " + result); // 注意:这里永远返回 true! return result; } // onServicesDiscovered() 回调中解析服务 @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // Step 1: 遍历所有服务 List<BluetoothGattService> services = gatt.getServices(); for (BluetoothGattService service : services) { Log.d(TAG, "Found service: " + service.getUuid().toString()); // Step 2: 关键!获取 16-bit UUID 的正确方式 // 错误写法:service.getUuid().toString().substring(0, 4) → 可能截断 // 正确写法:使用 UUID 的 getMostSignificantBits() long msb = service.getUuid().getMostSignificantBits(); if ((msb & 0xFFFF000000000000L) == 0x0000180900000000L) { // 匹配到 0x1809 服务 mTemperatureService = service; } } // Step 3: 只有找到目标服务后,才去获取其特征值 if (mTemperatureService != null) { BluetoothGattCharacteristic tempChar = mTemperatureService.getCharacteristic( UUID.fromString("00002a1c-0000-1000-8000-00805f9b34fb")); // Temperature Measurement if (tempChar != null) { mTemperatureCharacteristic = tempChar; broadcastUpdate(ACTION_DATA_AVAILABLE); } } } else { Log.w(TAG, "onServicesDiscovered received: " + status); // status=133 是常见错误 } }

设计说明:

  • discoverServices()的返回值boolean毫无意义(永远true),成败全看onServicesDiscovered()的status。status=133(GATT_ERROR)是最常见错误,原因通常是外设未响应或连接不稳定,此时必须重连,不能停留在界面上干等。
  • UUID 解析是高频翻车点。toString()返回的是 128-bit 格式(如00001809-0000-1000-8000-00805f9b34fb),但getUuid()返回的对象内部存储是long类型,直接toString().substring(0,4)会因大小端问题错位。正确做法是用getMostSignificantBits()提取高位,再与标准值比对。
  • requestMtu(512)是为后续大包传输铺路。若不调用,writeCharacteristic()默认最大 20 字节,传一张图片或固件包直接失败。

3.4 特征值读写:readCharacteristic()的字节解析与通知开启

// BluetoothLeService.java 中 readCharacteristic() 方法 public void readCharacteristic(BluetoothGattCharacteristic characteristic) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } // Step 1: 必须先 check characteristic 是否可读 if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) { Log.e(TAG, "Characteristic not readable: " + characteristic.getUuid()); return; } // Step 2: 执行读操作(异步) boolean result = mBluetoothGatt.readCharacteristic(characteristic); Log.d(TAG, "readCharacteristic() returned: " + result); } // onCharacteristicRead() 回调中解析数据 @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { // Step 1: 温度值通常为 IEEE-11073 FLOAT(2字节)或 uint16(2字节) // 假设是 uint16 小端序:data[0] + data[1]*256 int tempRaw = (data[0] & 0xFF) | ((data[1] & 0xFF) << 8); float temperature = tempRaw / 100.0f; // 某些传感器单位为 0.01℃ Log.d(TAG, "Temperature: " + temperature + " ℃"); Intent intent = new Intent(ACTION_DATA_AVAILABLE); intent.putExtra(EXTRA_DATA, String.valueOf(temperature)); localBroadcastManager.sendBroadcast(intent); } } else { Log.w(TAG, "onCharacteristicRead failed: " + status); // status=133 再次出现 } } // 开启通知(用于实时接收数据) public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } // Step 1: 先启用特征值通知 mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); // Step 2: 关键!必须写 CLIENT_CHARACTERISTIC_CONFIG descriptor BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); // CCCD UUID if (descriptor != null) { descriptor.setValue(enabled ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); } }

设计说明:

  • readCharacteristic()前必须检查PROPERTIES,否则onCharacteristicRead()的status会是GATT_READ_NOT_PERMITTED(0x87),但 Logcat 不会明确提示,只能靠characteristic.getProperties()调试。
  • 字节解析必须匹配外设文档。nRF Connect里看到的0x02 0x00,在 Java 里是data[0]=2, data[1]=0,小端序组合为0x0002=2,若误当大端序会算成0x0200=512,温度显示错乱。
  • 开启通知的两步缺一不可:setCharacteristicNotification(true)只是告诉 Android 系统「我要收通知」,writeDescriptor()才是真正向外设发送指令。漏掉第二步,onCharacteristicChanged()永远不触发。

4. 常见问题排查:BLE 开发中五个必踩的「静默翻车点」

BLE 开发最折磨人的不是报错,而是「没报错但功能不生效」。以下是我在带某高校课程设计时,学生提交的 37 份 BLE 课设中,出现频率最高的五个静默问题。每个都附带现象、根因和可立即验证的解决步骤。

4.1 现象:onScanResult()完全不回调,Logcat 无任何 BLE 日志

原因:Android 12+ 缺少BLUETOOTH_SCAN权限,或AndroidManifest.xml中未声明<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
解决:

  1. 检查AndroidManifest.xml,确认<uses-feature>存在且required="true"(若设为false,部分设备会禁用 BLE)
  2. 在DeviceControlActivity的onCreate()中,用PackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)判断硬件支持
  3. 若返回false,弹 Toast 提示「设备不支持 BLE」,而非静默失败

4.2 现象:connectGatt()后onConnectionStateChange()的status=133,反复重试无效

原因:外设处于「不可连接」广播状态(如刚上电未进入可连接模式),或手机蓝牙芯片与外设协议栈不兼容(常见于 Realtek 芯片手机连 Nordic nRF52)
解决:

  1. 用nRF ConnectApp 单独连接该设备,确认是否能连——若nRF Connect也失败,则是外设问题
  2. 在onConnectionStateChange()中,若status==133且newState==STATE_DISCONNECTED,等待 2 秒后调用connect()重试(最多 3 次)
  3. 在重试前,强制调用mBluetoothAdapter.disable()→mBluetoothAdapter.enable()重启蓝牙(仅调试用,正式版删掉)

4.3 现象:onServicesDiscovered()调用,但gatt.getServices()返回空列表

原因:discoverServices()调用过早,在onConnectionStateChange()的status==0之前就执行(即连接尚未完全建立)
解决:

  1. 在onConnectionStateChange()中,严格判断status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED
  2. 只在此条件下调用initialize()(内含discoverServices())
  3. 在initialize()开头加日志Log.d(TAG, "Starting service discovery..."),确认执行时机

4.4 现象:readCharacteristic()后onCharacteristicRead()的status=0,但characteristic.getValue()返回null

原因:外设未在onReadRequest()中调用sendResponse(),或characteristic.setValue(byte[])未被正确赋值
解决:

  1. 在onCharacteristicRead()中,先打印characteristic.toString(),确认getValue()是否为null
  2. 若为null,检查外设固件:在onReadRequest()回调中,必须调用gattServer.sendResponse(device, requestId, GATT_SUCCESS, offset, value),其中value不能为空数组
  3. 在 Android 端,readCharacteristic()前加Log.d(TAG, "Reading: " + characteristic.getUuid()),与外设日志比对请求 UUID

4.5 现象:开启通知后onCharacteristicChanged()从不触发,但writeDescriptor()返回true

原因:writeDescriptor()是异步操作,onDescriptorWrite()回调未收到GATT_SUCCESS,但代码已继续执行
解决:

  1. 在setCharacteristicNotification()中,writeDescriptor()后不立即返回,而是等待onDescriptorWrite()
  2. 在onDescriptorWrite()中,检查status==GATT_SUCCESS,再广播ACTION_NOTIFY_ENABLED
  3. UI 层监听此广播,而非在点击「开启通知」按钮后立刻认为已生效

5. 验收与答辩:用三张表、两个现场演示、一套答辩话术拿下高分

毕设/课设验收不是看你代码能不能跑,而是考你是否真正理解 BLE 的「协议-实现-硬件」三角关系。我带过的某跨平台系统课设,学生用这套方法让答辩老师当场追问「你们怎么解决 Android 12 权限降级的」,最后给了 95 分。下面是我总结的硬核验收清单。

5.1 验收必备三张表:覆盖协议、代码、硬件三层证据

表类型内容要点为什么必须有答辩时怎么用
BLE 协议对照表列出本次 Demo 涉及的 GATT 操作(Scan/Connect/Discover/Read/Notify)、对应 Android API、标准返回码(GATT_SUCCESS=0, GATT_INVALID_HANDLE=0x80)、外设需支持的 ATT 属性(如 READ/WRITE/NOTIFY)证明你不是盲目调 API,而是按蓝牙 SIG 规范开发答辩 PPT 第一页放此表,老师问「为什么用 discoverServices() 而不用 fetchUuidsWithSdp()」,你指表说「后者是旧版 SDP 协议,Android 4.3+ 已废弃,且不支持 BLE」
关键代码参数表ScanSettings.SCAN_MODE_LOW_LATENCY、connectGatt(autoConnect=false)、requestMtu(512)、BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE等参数的取值依据(引用 Android SDK 文档章节)证明参数选择有据可查,非凭感觉当老师质疑「为什么 MTU 设 512」,你打开表格,指「Android 5.0+ 支持最大 512,且外设 spec sheet 第 3.2 节明确支持」
硬件兼容性表测试过的手机型号(华为 Mate 40 Pro/小米 12/三星 S22)、系统版本(Android 11~14)、外设型号(nRF52832/nRF52840)、固件版本,以及各组合下的成功率(如「小米 12 + Android 13:扫描成功率 98%,连接成功率 92%」)证明你做了真实测试,不是只在模拟器跑通老师若说「我们实验室用的是 OPPO Reno8」,你立刻翻表:「OPPO Reno8 测试记录在 Appendix A,Android 13 下连接成功率 89%,失败主因是蓝牙驱动 Bug,我们加了 3 次重试后提升至 97%」

5.2 现场演示的两个必做动作:让老师亲眼看到「协议在跑」

很多学生演示只点「连接」→「读温度」,老师看不到底层交互。必须加两个动作,把黑匣子打开:

动作一:Logcat 实时投屏,聚焦三个关键日志段

  • 扫描阶段:onScanResult()输出设备名 + MAC + RSSI,证明扫描真实发生
  • 连接阶段:onConnectionStateChange()的status=0和newState=2(STATE_CONNECTED),证明连接成功
  • 读取阶段:onCharacteristicRead()的status=0和getValue()的十六进制字节数组(如02 00),证明数据真实到达

提示:提前在 Logcat 过滤BluetoothLeService和DeviceControlActivity,避免刷屏。老师若问「怎么知道数据没被篡改」,你指02 00说:「这是外设固件 raw output,我们未做任何转换,解析逻辑在onCharacteristicRead()注释第 12 行」

动作二:用nRF Connect同步连接同一外设,对比行为

  • 在演示机上运行你的 App,同时用另一台手机开nRF Connect连同一设备
  • 当你的 App 点击「开启通知」,立刻在nRF Connect里观察该 characteristic 的 Notify 开关是否同步点亮
  • 当你的 App 读到25.5℃,在nRF Connect里手动读同一 characteristic,确认值一致

这一步直接证明:你的 App 与标准工具行为一致,协议栈工作正常。老师若质疑「你们是不是伪造了数据」,这个对比就是铁证。

5.3 答辩话术:用「问题-解法-依据」三段式回应所有刁难

老师最爱问「如果……怎么办?」,别背答案,用结构化话术接招:

  • 问题:「如果外设不支持requestMtu(512),你们怎么适配?」
    解法:「我们做了 fallback 机制:先requestMtu(512),若onMtuChanged()返回mtu<512,则自动切到mtu值分包传输。比如mtu=23时,512 字节数据拆成 23 包,每包加 sequence number,外设端重组。」
    依据:「参考蓝牙 SIG ATT 协议 Section 4.1.1,MTU negotiation 是标准流程,Android 5.0+ 全支持。」

  • 问题:「Android 14 权限模型又有变化,你们的方案还适用吗?」
    解法:「我们预留了权限抽象层:PermissionManager类封装所有权限检查,新增权限只需在checkPermission()方法里加if (Build.VERSION.SDK_INT >= 34) { ... }分支,不改动业务逻辑。」
    依据:「Google 官方《Permissions best practices》明确建议『Use permission groups and abstract checks』,我们已在BluetoothLeService的initPermission()方法里实现。」

  • 问题:「为什么不用第三方库如 RxAndroidBle?」
    解法:「课设要求『掌握原生 BLE API』,且 RxAndroidBle 会隐藏状态机细节。比如它的establishConnection()自动重连,但我们发现这会导致onConnectionStateChange()多次触发,与外设状态不同步。」
    依据:「我们对比了 RxAndroidBle v3.0.1 源码,其ConnectionModule的retryWhen()逻辑与BluetoothGattCallback的status解析存在时序冲突,详见我们实验报告 Appendix B 的时序图。」

从那以后我每次带课设,都强制学生在答辩前三天,用真机跑通这三张表、两个演示、三段话术。不是为了应付老师,而是逼自己把 BLE 从「能跑」变成「懂为什么能跑」。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进