简介:本资源是基于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的回调链:
startScan()→ 触发onScanResult()(扫描到设备)connectGatt()→ 触发onConnectionStateChange()(status=2 表示连接中,status=0 表示成功)discoverServices()→ 触发onServicesDiscovered()(status=0 才继续)getService().getCharacteristic()→ 获取有效 Characteristic 实例setCharacteristicNotification()→ 开启通知(需同时写CLIENT_CHARACTERISTIC_CONFIGdescriptor)readCharacteristic()或writeCharacteristic()→ 主动读写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" />
解决:
- 检查
AndroidManifest.xml,确认<uses-feature>存在且required="true"(若设为false,部分设备会禁用 BLE) - 在
DeviceControlActivity的onCreate()中,用PackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)判断硬件支持 - 若返回
false,弹 Toast 提示「设备不支持 BLE」,而非静默失败
4.2 现象:connectGatt()后onConnectionStateChange()的status=133,反复重试无效
原因:外设处于「不可连接」广播状态(如刚上电未进入可连接模式),或手机蓝牙芯片与外设协议栈不兼容(常见于 Realtek 芯片手机连 Nordic nRF52)
解决:
- 用
nRF ConnectApp 单独连接该设备,确认是否能连——若nRF Connect也失败,则是外设问题 - 在
onConnectionStateChange()中,若status==133且newState==STATE_DISCONNECTED,等待 2 秒后调用connect()重试(最多 3 次) - 在重试前,强制调用
mBluetoothAdapter.disable()→mBluetoothAdapter.enable()重启蓝牙(仅调试用,正式版删掉)
4.3 现象:onServicesDiscovered()调用,但gatt.getServices()返回空列表
原因:discoverServices()调用过早,在onConnectionStateChange()的status==0之前就执行(即连接尚未完全建立)
解决:
- 在
onConnectionStateChange()中,严格判断status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED - 只在此条件下调用
initialize()(内含discoverServices()) - 在
initialize()开头加日志Log.d(TAG, "Starting service discovery..."),确认执行时机
4.4 现象:readCharacteristic()后onCharacteristicRead()的status=0,但characteristic.getValue()返回null
原因:外设未在onReadRequest()中调用sendResponse(),或characteristic.setValue(byte[])未被正确赋值
解决:
- 在
onCharacteristicRead()中,先打印characteristic.toString(),确认getValue()是否为null - 若为
null,检查外设固件:在onReadRequest()回调中,必须调用gattServer.sendResponse(device, requestId, GATT_SUCCESS, offset, value),其中value不能为空数组 - 在 Android 端,
readCharacteristic()前加Log.d(TAG, "Reading: " + characteristic.getUuid()),与外设日志比对请求 UUID
4.5 现象:开启通知后onCharacteristicChanged()从不触发,但writeDescriptor()返回true
原因:writeDescriptor()是异步操作,onDescriptorWrite()回调未收到GATT_SUCCESS,但代码已继续执行
解决:
- 在
setCharacteristicNotification()中,writeDescriptor()后不立即返回,而是等待onDescriptorWrite() - 在
onDescriptorWrite()中,检查status==GATT_SUCCESS,再广播ACTION_NOTIFY_ENABLED - 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 从「能跑」变成「懂为什么能跑」。希望帮到你。
本文还有配套的精品资源,点击获取