ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android蓝牙开发步骤详解:权限、扫描、连接与数据传输

Android蓝牙开发步骤详解:权限、扫描、连接与数据传输 简介这是一份面向Android系统开发者的蓝牙开发步骤详解文档核心解决从蓝牙芯片硬件到上层应用全链路如何打通的问题。内容按硬件、Linux内核BlueZ协议栈、libbluedroid.so库、框架层蓝牙设备服务、HSP/HFP/SDP应用等分层展开梳理了蓝牙电源开启、hciattach通信、hciconfig配置、hcid守护进程、DBus接口及设备扫描的完整操作步骤并配有源码级分析和配置修改示例适合需要深入理解Android蓝牙启动与扫描机制的工程师。资源为单个PDF文件压缩包大小约230KB目前已有306人学习。文档不仅介绍了蓝牙芯片组、2.4GHz跳频等硬件基础还重点剖析了BlueZ协议栈中的UART驱动、HCI、L2CAP、RFCOMM等层次关系并对init.rc配置、rfkill电源管理和dbus调用等关键节点给出实际处理方法能帮助读者快速建立Android蓝牙开发整体框架减少底层排错时间。1. Android 蓝牙开发步骤到底卡在哪一步把「android蓝牙开发步骤.pdf」这个标题搜出来的人十有八九不是被连接代码拦住而是被「扫描不到设备」「权限弹窗没有」「连上就断」这类现象逼到找教程的。这个标题背后真正的问题是 Android 蓝牙开发里有两条完全不同的技术路线经典蓝牙SPP、A2DP、HSP和低功耗蓝牙 BLE它们在权限、扫描、连接、收发数据上用的是一套 API 但完全不同的逻辑。而 Android 12 又把权限体系拆成三类老教程里那一套直接失效。这个开发任务本质上就是五步准备权限、扫描设备、建立连接、收发数据、调试验证。新手按顺序走完这五步就能对接 HC-05 这类经典模块、BLE 温湿度传感器、心率计以及 ESP32 这类双模设备。下面每个环节都会给出可以直接抄的代码和参数并指出那些在真机上才会遇到的坑。2. Android 蓝牙权限与协议栈动手前先把适配版本理清蓝牙开发的第一步不是startDiscovery而是把 Android 版本差异处理干净。Android 12API 31之前蓝牙权限只有BLUETOOTH和BLUETOOTH_ADMIN两个一个管连接、一个管扫描开关Android 12 之后Google 出于隐私考虑把权限按场景拆分老权限在新系统上几乎失效。如果你的targetSdk已经升到 31 以上代码里还只声明旧权限运行时一调用扫描就会抛SecurityException而且没有任何弹窗提示。2.1 targetSdk 31 或更高时的权限列表与兼容写法先把权限表列清楚开发时对照着声明targetSdk 版本权限常量作用30 及以下BLUETOOTH、BLUETOOTH_ADMIN连接、扫描、开关蓝牙31BLUETOOTH_SCAN扫描蓝牙设备经典蓝牙与 BLE 通用31BLUETOOTH_CONNECT连接设备、读取 RSSI、发起配对31BLUETOOTH_ADVERTISE作为外设广播数据中心设备不需要需要留意的是BLUETOOTH_SCAN和BLUETOOTH_CONNECT是「附近设备」权限的两个独立项系统会分别弹窗。如果同时申请系统弹窗上会显示「允许附近设备连接和扫描」之类的合并文案但权限在系统设置里是分开的用户关闭其中一项对应的功能就会失效。在 Android 11 及以下机型上跑targetSdk31 的安装包时系统会直接忽略BLUETOOTH_SCAN和BLUETOOTH_CONNECT并不报错但你的动态权限检查也不能只盯着新权限判断。兼容写法是按Build.VERSION.SDK_INT分两条路径申请。下面这段代码可以放进 Application 或首屏 Activity 里。// Android 12 用新权限Android 11 及以下用旧权限和定位权限 private val btPermissionLauncher registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result - // result 是 MapString, Boolean逐个检查 if (result.values.all { it }) { onPermissionReady() // 权限齐全开始扫描或连接 } else { // 跳转应用设置页让用户手动打开或者直接提示蓝牙功能不可用 } } fun requestBluetoothPermission() { val permissions if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) } else { arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, // BLE 扫描强制要求 Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN ) } btPermissionLauncher.launch(permissions) }这段代码的核心逻辑是同一套申请流程按系统版本决定申请哪些权限。RequestMultiplePermissions会一次性弹完所有请求不用自己写requestPermissions回调。注意 Android 11 及以下机型扫 BLE 时必须要有ACCESS_FINE_LOCATION因为蓝牙扫描会被系统视为获取用户位置的一种方式只申请蓝牙权限不会弹定位授权扫描也返回空列表。2.2 获取 BluetoothAdapter 的正确姿势权限解决后第二步是拿BluetoothAdapter。很多老教程写BluetoothAdapter.getDefaultAdapter()这个 API 在 Android 11 上还能用但不推荐它拿的是全局单例测试时不好 mock而且getSystemService才是官方推荐的路径。Android 的蓝牙协议栈从应用层看就是一个系统服务BluetoothManager是入口适配器只是它暴露出来的门面。// 正确做法通过 BluetoothManager 拿适配器 val manager getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val adapter: BluetoothAdapter? manager.adapter if (adapter null) { // 设备没有蓝牙硬件直接终断流程 Log.e(TAG, device not support bluetooth) return }adapter为 null 的情况不多但国内一些低端平板上确实存在没装蓝牙驱动的系统。isEnabled()用来判断蓝牙开关状态如果返回false不能用adapter.enable()强行打开——这个方法在 API 33 上已经被标记为不允许应用调用强行走会静默失败。正确方式是发ACTION_REQUEST_ENABLE拉起系统弹窗if (!adapter.isEnabled) { val intent Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) activityResultLauncher.launch(intent) }这里有个顺序问题在 targetSdk 31 上ACTION_REQUEST_ENABLE本身需要BLUETOOTH_CONNECT权限。所以代码里必须先跑完 2.1 的权限申请再执行开启蓝牙的弹窗否则startActivity会直接抛SecurityException。把这两个动作串成有状态机的流程比简单写在onCreate里可靠得多。2.3 判断本机蓝牙是否可用和是否已开启完整的启动流程应该是检查硬件是否存在 → 检查权限是否授予 → 检查蓝牙是否开启。这一步最好做成回调式的异步流程因为权限申请和系统弹窗都会中断 Activity 生命周期。实际项目中我一般会封装一个BluetoothController对外暴露ensureReady(callback)内部处理版本分支、权限请求和开关状态这样对接 UI 时只需要关心「可以了吗」和「失败了吗」两个结果。fun ensureReady(onReady: () - Unit, onError: (String) - Unit) { when { adapter null - onError(no bluetooth hardware) !hasPermission() - requestBluetoothPermission() !adapter.isEnabled - enableBluetooth() else - onReady() } }这个流程写完后所有的业务代码——扫描、连接、数据传输——统一在onReady回调里开始。不要在每个页面自己判断权限蓝牙状态变化用户在系统设置里关掉蓝牙会触发ACTION_STATE_CHANGED广播BluetoothController里注册一个全局监听把状态同步给 UI比在各页面单独处理省很多事。3. 设备扫描经典蓝牙和 BLE 的扫描写法与参数陷阱权限就绪后进入设备扫描环节。这里第一个认知要纠正经典蓝牙和 BLE 的扫描不是同一个 API。经典蓝牙用的是startDiscovery()通过系统全局广播上报发现结果BLE 用的是startScan()通过回调拿到ScanResult。两个 API 还会互相干扰——startDiscovery扫描时系统的射频资源被占用BLE 扫描结果明显变慢甚至超时所以代码里要先cancelDiscovery()再startScan()。3.1 经典蓝牙扫描与 BLE 扫描的完整代码经典蓝牙扫描用BroadcastReceiver接收系统广播这是 Android 早期就有的设计到现在没有变过。核心代码如下// 注册广播接收器 val filter IntentFilter().apply { addAction(BluetoothDevice.ACTION_FOUND) addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) } registerReceiver(discoveryReceiver, filter) // 开始扫描前必须先取消已有扫描否则返回 false if (adapter.isDiscovering) adapter.cancelDiscovery() val started adapter.startDiscovery() // 异步执行startDiscovery()是异步的返回true只代表扫描已启动设备回调陆续通过广播到达。ACTION_FOUND的广播里带EXTRA_DEVICE和EXTRA_RSSI两个关键参数。这里有个老坑直接intent.getParcelableExtra(EXTRA_DEVICE)在 Android 13API 33上会导致 lint 报错因为系统对Parcelable的读取要求指定类型。兼容写法是private val discoveryReceiver object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { BluetoothDevice.ACTION_FOUND - { val device if (Build.VERSION.SDK_INT 33) { intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) } else { Suppress(DEPRECATION) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) } val rssi intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, -127).toInt() // 注意device.name 在这里可能为 null配对前拿不到广播名 } BluetoothAdapter.ACTION_DISCOVERY_FINISHED - { // 一轮扫描结束需要再次调用 startDiscovery 才能继续 // 通常这里做一个 3 秒延时循环直到找到目标设备 } } } }广播接收器记得在onDestroy里unregisterReceiver不然后台泄漏和一秒一次的ACTION_FOUND刷屏会拖垮 UI 线程。经典蓝牙扫描是全局操作系统扫描一轮大约 12 秒扫完自动停不会一直跑。想持续扫描就得在ACTION_DISCOVERY_FINISHED里再调一次startDiscovery。BLE 扫描走的是另一套回调风格没有广播所有数据通过ScanCallback回调private val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult?) { result ?: return val device result.device val rssi result.rssi val advData result.scanRecord?.bytes // 广播包里经常没有设备名优先用 serviceUuid 判断是不是目标设备 } override fun onScanFailed(errorCode: Int) { // 常见 errorCode // SCAN_FAILED_ALREADY_STARTED 1 重复 startScan // SCAN_FAILED_APPLICATION_REGISTRATION_FAILED 2 应用注册失败等 5 秒重试 } } // 启动 BLE 扫描 val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() // filters 为 null 表示扫描所有 BLE 设备 bluetoothAdapter.bluetoothLeScanner?.startScan(null, settings, scanCallback)bluetoothLeScanner可能是 null比如设备不支持 BLE或者蓝牙开关未开启。扫描回调里result.device.name十有八九是 null——很多 BLE 外设只有在连接后才会把设备名通过 GATT 服务暴露出来广播里只放服务 UUID。所以判断目标设备时优先匹配scanRecord.serviceUuids不要用getName()过滤。SCAN_MODE_LOW_LATENCY模式回调密集、耗电高适合需要快速发现的场景平时用SCAN_MODE_BALANCED就够省电模式在设备密集环境里经常漏包。3.2 扫描过滤参数避免返回一堆无关设备BLE 扫描如果不带过滤条件会把周围所有广播设备都回上来。办公室里经常同时出现几十个蓝牙耳机、体脂秤和手机这会带来两个问题回调频繁导致 UI 刷不过来以及目标设备名匹配容易误判。ScanFilter是官方提供的过滤手段可以在系统层做初步筛选减少上行数据量。// 过滤方式一按 MAC 直连 val macFilter ScanFilter.Builder() .setDeviceAddress(AA:BB:CC:DD:EE:FF) .build() // 过滤方式二按服务 UUID最常用 val uuidFilter ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(0000ffe0-0000-1000-8000-00805f9b34fb)) .build() // 过滤方式三按设备名不推荐因为广播名经常为空 val nameFilter ScanFilter.Builder() .setDeviceName(ESP32) .build() val filters listOf(uuidFilter, macFilter) // 多个 filter 之间是 OR 关系ScanFilter的多个条件在同一个 Builder 里是 AND 关系多个ScanFilter在 list 里是 OR 关系。常见的误用是把「服务 UUID 设备名」写在一个 Builder 里结果目标设备广播里没有名字过滤条件永远不满足扫描结果为空。建议只保留服务 UUID 或 MAC 地址过滤设备名留到连接后再校验。ScanSettings还有一个重要参数setReportDelay(0)0 表示实时回调大于 0 表示按毫秒批量上报结果。做设备列表展示时用批量上报可以减少 UI 刷新频率但首次发现设备会变慢一般保持默认 0。3.3 HC-05 / HC-06 蓝牙模块扫描和连接不上的排查清单HC-05 和 HC-06 是典型的经典蓝牙 SPP 模块在嵌入式开发和 Arduino 项目里极常见但它们带来的排查问题占据了蓝牙开发社区很大一部分提问量。最常见的现象是「手机能扫到模块连接就失败」或「连接成功但 AT 指令无响应」。现象常见原因处理方式手机扫不到模块HC-05 处于 AT 模式模块不进入可被发现状态重新上电拔出 EN/KEY 引脚再试扫得到但连接失败配对码错误HC-05 默认 1234确认模块指示灯是否进入配对状态快闪AT 无响应波特率不匹配HC-06 默认 9600HC-05 AT 模式常用 38400连接后立刻断开SPP UUID 不对使用标准串口 UUID00001101-0000-1000-8000-00805f9b34fbHC-05 和 HC-06 都只支持经典蓝牙用 BLE 的startScan是永远扫不到它们的这个方向性错误最容易在混合开发时出现。HC-05 进入 AT 模式的方法是把 EN 引脚拉高或用按键上电这时模块波特率固定为 38400不能通信数据退出 AT 模式后波特率才回到通信波特率。遇到 AT 无响应第一步不是改代码而是确认模块当前工作在什么模式——方案验证阶段建议用 USB 转 TTL 小板在串口助手上先确认模块本身是否正常再回来看手机端的问题。4. 连接与数据传输RFCOMM Socket 和 BLE GATT 两条路的完整代码扫描到设备只是第一步真正的开发重点在连接和收发数据。经典蓝牙和 BLE 的连接模型完全不同经典蓝牙走 RFCOMM 套接字API 长得像 TCP SocketBLE 走 GATT 协议基于服务Service和特征值Characteristic操作。很多从 Arduino 转过来的开发者会把两条路混着用——拿 BLE 的方式连 HC-05或者拿经典蓝牙的方式连 BLE 传感器结果自然是各种失败。4.1 经典蓝牙 RFCOMM先配对再开 Socket经典蓝牙的连接模型是「配对 Socket」。配对可以由系统弹窗完成也可以代码主动createBond()发起但真正的数据传输不依赖配对本身而是依赖BluetoothSocket上的输入输出流。HC-05 这类模块出厂时已经配置好固定配对码手机端通常直接就能连。private fun connectSPP(device: BluetoothDevice) { // 标准 SPP UUIDHC-05/06 和绝大多数串口模块默认使用这个 val sppUuid UUID.fromString(00001101-0000-1000-8000-00805f9b34fb) Thread { try { // 连接前停掉扫描射频资源是共享的 adapter.cancelDiscovery() // 注意createRfcommSocketToServiceRecord 必须放在 try 里 // socket 不能复用每连一次都新建 val socket: BluetoothSocket device.createRfcommSocketToServiceRecord(sppUuid) // connect() 是阻塞调用必须放子线程 socket.connect() Log.d(TAG, connect success) // 收发数据走 IO 流 val output socket.outputStream val input socket.inputStream // 向模块发送 AT 指令注意换行符 output.write(AT\r\n.toByteArray(Charsets.US_ASCII)) output.flush() // 读取模块回应读取会阻塞也要在子线程 val buffer ByteArray(256) val len input.read(buffer) Log.d(TAG, received: String(buffer, 0, len)) socket.close() // 用完必须关否则下次连接抛 socket closed } catch (e: IOException) { // 连接失败常见原因配对未完成、模块处于 AT 模式、socket 未关闭 Log.e(TAG, SPP connect failed, e) } }.start() }这个流程里有几个关键点。第一connect()是阻塞的短则几百毫秒、长则十几秒不能在主线程调用。第二createRfcommSocketToServiceRecord每次连接前都会新建一个 socket旧 socket 没有 close下次连接大概率失败报错往往是java.io.IOException: socket closed。第三cancelDiscovery()必须在connect()之前调用否则系统射频资源被扫描占用连接会一直卡住直到超时。如果设备之前没有配对过先调用device.createBond()等待ACTION_BOND_STATE_CHANGED变成BOND_BONDED再走 socket 连接成功率更高。经典蓝牙还有一种「隐藏的宝藏」连法用device.createInsecureRfcommSocketToServiceRecord()字面意思是「不安全的套接字」好处是跳过配对弹窗适合无交互的 IoT 设备坏处是部分定制 ROM 会拦截这个方法而且它绕过了系统配对流程某些模块会拒绝连接。生产环境建议走标准配对流程只把 insecure 方式留给自己测试用。4.2 BLE GATT 连接、服务发现与特征值读写BLE 连接模型和经典蓝牙完全不同。手机作为中心设备Central连接外设Peripheral连接后不是直接读写流而是先发现服务和特征值然后对特征值做读、写、通知三种操作。整个流程严格按状态机推进连接成功 → 发现服务 → 找到特征值 → 配置通知 → 读写数据少一步都不行。private var gatt: BluetoothGatt? null private fun connectGatt(device: BluetoothDevice) { // 清掉上一次的连接防止回调错乱 gatt?.close() gatt device.connectGatt( this, false, // 不要 autoReconnect断线重连逻辑自己写更可控 object : BluetoothGattCallback() { // 状态机第一站连接状态变化 override fun onConnectionStateChange( gatt: BluetoothGatt, status: Int, newState: Int ) { when (newState) { BluetoothProfile.STATE_CONNECTED - { // 连接成功必须调用 discoverServices否则读不到服务 gatt.discoverServices() } BluetoothProfile.STATE_DISCONNECTED - { if (status ! BluetoothGatt.GATT_SUCCESS) { // 133 和 19 是两个最常见的异常状态 gatt.close() } // 这里按需做重连 } } } // 状态机第二站服务发现完成 override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status ! BluetoothGatt.GATT_SUCCESS) { Log.e(TAG, service discover failed, status$status) return } val service gatt.getService(SERVICE_UUID) ?: return val writeChar service.getCharacteristic(WRITE_CHAR_UUID) // 打开通知setCharacteristicNotification 只是本地使能 // 真正的开启要写 0x2902 客户端描述符CCCD val notifyChar service.getCharacteristic(NOTIFY_CHAR_UUID) gatt.setCharacteristicNotification(notifyChar, true) val desc notifyChar.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb) ) desc?.let { it.value BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(it) } // 写数据0x01 是任意业务指令实际项目按协议封装 writeChar.value byteArrayOf(0x01) val success gatt.writeCharacteristic(writeChar) Log.d(TAG, write result: $success) } // 外设主动上报数据会走到这里 override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { val data characteristic.value // 通知数据是按 MTU 分包上来的业务层需要做拼接 } } ) }整段代码的核心是回调顺序STATE_CONNECTED后不能立刻读写——Android 蓝牙协议栈里连接建立和服务发现是两个独立步骤必须在onServicesDiscovered之后才能拿到特征值对象。很多新手在这里踩坑连接成功日志都打了getCharacteristic却返回 null。另外autoConnect参数在connectGatt里传false更稳妥Google 官方也建议不要依赖系统的自动重连原因是autoConnecttrue时连接失败不会走回调应用层无法感知状态。在 Android 13API 33上writeCharacteristic(BluetoothGattCharacteristic)这个旧 API 被标记为废弃官方要求改用// API 33 的写法value 和 writeType 都从方法参数传入 if (Build.VERSION.SDK_INT 33) { gatt.writeCharacteristic( characteristic, byteArrayOf(0x01), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT ) } else { characteristic.value byteArrayOf(0x01) gatt.writeCharacteristic(characteristic) }新旧 API 的差异不只是在参数位置上旧 API 里characteristic.value是全局共享的如果你把同一个特征值对象发给两个线程写数据会互相覆盖新 API 每个写请求独立携带数据线程安全更清晰。兼容写法务必带上版本判断别指望伸手党式的「新方法能跑在旧系统上」。4.3 MTU 协商与数据分包大文件传输的分包设计BLE 单次写入的数据长度上限由 MTUMaximum Transmission Unit决定。默认 MTU 为 23 字节减去 3 字节协议头实际单次可写数据只有 20 字节。这对于发送一条指令够用但要传日志文件或 OTA 固件就完全不行。连接建立后可以发起 MTU 协商// 建议请求 247这是绝大多数外设支持的上限 gatt.requestMtu(247)协商结果通过onMtuChanged(gatt, mtu, status)回调告知mtu是包括协议头在内的完整值实际可用 payload 要减 3。协商失败时不要硬写大于 20 字节的数据否则部分模块会静默丢包。MTU 协商里最常见的坑是外设端 MTU 配置为 23出厂默认手机端请求 247 被拒绝此时中国厂商的很多模块根本不会回错误码而是直接忽略请求onMtuChanged不触发。稳妥做法是请求后加 5 秒超时超时后按 payload 20 字节分包传输。// 按 MTU 分包发送示例把大文件拆成多包包头带序号 private var sequence 0 fun sendFileChunk(stream: InputStream, mtu: Int) { val payloadSize mtu - 3 val buffer ByteArray(payloadSize) while (true) { val len stream.read(buffer) if (len 0) break val chunk ByteArray(len 4) chunk[0] 0xAA // 帧头 chunk[1] sequence.toByte() chunk[2] (len shr 8).toByte() chunk[3] len.toByte() System.arraycopy(buffer, 0, chunk, 4, len) // writeCharacteristic 每次调完要等 onCharacteristicWrite 回调再发下一包 writeChunk(chunk) } }注意每发一包都要等onCharacteristicWrite回调确认后再发下一包因为 BLE 写请求是带顺序确认的连续调用writeCharacteristic而不等回调系统会丢掉中间若干包。这里如果外设是 ESP32还要考虑它同时开启蓝牙和 Wi-Fi 时 2.4GHz 射频切换带来的吞吐下降实测并发场景下 BLE 吞吐可能掉一半以上分包间隔要适当放宽或引入 ACK 重传机制。5. 调试验证logcat 过滤、RSSI 测距边界与掉线自动重连代码写完调试阶段才是真正分高下的地方。蓝牙外设有大量异步状态机光看应用日志找问题很痛苦系统层协议栈的日志更加有用。Android 系统自带蓝牙 HCI 日志抓取开关位于开发者选项的「蓝牙调试」打开后系统会把蓝牙控制器通信的 HCI 分组写入/sdcard/btsnoop_hci.log用 Wireshark 打开btsnoop文件能看到扫描请求、连接参数、配对过程这些系统层细节判断问题出在手机端还是外设端比看应用日志高效得多。日常开发中先用 logcat 把系统蓝牙日志过滤出来# 只看 GATT 连接状态和错误 adb logcat -s BluetoothGatt:V BluetoothDevice:V BluetoothAdapter:V BluetoothManager:V # 或按关键字过滤用 -e 匹配正则 adb logcat -v time | grep -E bt_gatt|bta_gatt|GATT|connection stateBluetoothGatt:V会打出服务发现、特征值读写和 MTU 协商的完整过程外设返回的错误码也在这里。最常见的错误码是 133连接关闭或参数异常和 19远端设备主动断开前者多半是连接参数协商失败后者是外设超时踢掉了连接。对应排查方向133 检查外设的连接间隔和超时参数19 检查外设端的空闲超时设置。另一个值得做的验证是 RSSI 测距。扫描回调里的result.rssi和外设连接后的readRemoteRssi()都提供信号强度理论上可以用路径损耗公式估算距离// RSSI 转距离的简化公式 fun estimateDistance(rssi: Int, txPowerAtOneMeter: Int -59, pathLoss: Double 2.0): Double { return 10.0.pow((txPowerAtOneMeter - rssi) / (10.0 * pathLoss)) }但这个值的参考意义远小于直觉。室内环境下信号反射和吸收让 RSSI 抖动经常超过 ±8dBm换算成距离就是 2 米变 5 米。单独测一次距离没有意义正确用法是把 RSSI 分成「近 / 中 / 远」三档比如 -55 近-55 到 -70 中 -70 远用于门禁判断和存在性检测足够做精确测距工程量就大了。另一个容易被忽略的现象是手机天线方向对 RSSI 的影响握持姿势变化就能造成 10dBm 的波动所以任何测距逻辑上都要加滤波至少取 5 次扫描的平均值。最后是掉线自动重连这也是蓝牙开发实战中最常被要求的稳定性能力。在onConnectionStateChange里收到STATE_DISCONNECTED且 status 133 时按指数退避重连是比固定间隔更稳的策略override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState BluetoothProfile.STATE_DISCONNECTED) { if (status ! BluetoothGatt.GATT_SUCCESS) { gatt.close() } // 指数退避3 秒 → 6 秒 → 12 秒最高 30 秒 if (retryCount 5) { val delay minOf(3_000L * (1 shl retryCount), 30_000L) handler.postDelayed({ retryCount connectGatt(device) }, delay) } } } // 连接成功时把重连次数归零 override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status BluetoothGatt.GATT_SUCCESS) { retryCount 0 } }重连还有一个细节连接成功和失败的状态要能区分「手机蓝牙关闭」和「设备真掉线」蓝牙关闭时重连只会浪费电量。在BluetoothController里监听ACTION_STATE_CHANGED广播蓝牙关闭时暂停重连计时器开启时恢复连接。针对 BLE 设备的重连connectGatt调用前先close()掉旧的BluetoothGatt对象否则旧回调会干扰新连接。对于经典蓝牙重连前先确认 socket 已 close再走一遍 4.1 的流程不能复用旧 socket 对象。本文还有配套的精品资源点击获取
返回列表