ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Flutter BLE开发实战:flutter_blue_plus核心机制与避坑指南

Flutter BLE开发实战:flutter_blue_plus核心机制与避坑指南 蓝牙低功耗开发在移动端一直是个让人又爱又恨的领域。爱的是它确实能打通手机和各类硬件的连接做出很多有意思的东西恨的是从扫描、连接、服务发现到特征值读写每一步都有坑在等着你。Flutter 生态里做 BLE 开发flutter_blue_plus 基本是绕不开的选择它把 Android 和 iOS 两套原生蓝牙 API 封装成了一套统一的 Dart 接口省去了大量平台适配的重复劳动。但封装归封装底层协议栈的行为逻辑不会因为多了一层 Dart 就变简单。这篇文章不打算只给你一份 API 清单而是从实际项目出发把 flutter_blue_plus 的核心功能拆开来看讲清楚每个操作背后到底发生了什么、为什么这么设计、以及我在真实设备上踩过的那些坑。适合已经上手 Flutter、准备或正在做蓝牙硬件对接的开发者也适合想搞清楚 BLE 协议栈行为逻辑的同行参考。1. 从扫描到连接BLE 设备发现阶段的真实行为1.1 扫描参数不是随便填的flutter_blue_plus 的startScan方法签名看起来很简单但里面几个参数直接决定了你能不能扫到目标设备。先看一段典型的扫描代码FlutterBluePlus.startScan( withServices: [Guid(0000FFE0-0000-1000-8000-00805F9B34FB)], withNames: [MyDevice], timeout: Duration(seconds: 15), androidUsesFineLocation: false, );withServices这个参数值得单独说。很多人以为它是过滤条件填了之后系统只返回包含这个服务的设备。这个理解只对了一半。在 Android 平台上如果填了withServices扫描时会走SCAN_MODE_LOW_LATENCY的硬件过滤通道扫描结果的回调频率会明显降低但功耗也更低。问题在于有些设备的广播包里根本不包含完整的 128 位服务 UUID只放了 16 位的短 UUID这时候你填完整的 128 位 GUID 反而扫不到。我的做法是如果目标设备是自己能控制的固件尽量在广播包里放完整的服务 UUID如果是第三方设备先不加withServices扫一遍拿到advertisementData.serviceUuids看看实际广播了什么再决定过滤条件。withNames的匹配逻辑也需要注意。它匹配的是advertisementData.advName也就是广播包里的设备名不是连接后的device.platformName。有些设备在广播阶段不放名字或者放的名字和连接后读到的名字不一样这时候用withNames过滤就会漏掉设备。实测下来最稳妥的扫描策略是第一轮不加任何过滤条件扫描 10 到 15 秒把所有设备的remoteId、advName、serviceUuids、rssi都打印出来确认目标设备的广播特征之后再在后续扫描中加上精确的过滤条件。1.2 扫描结果里的 RSSI 和广播数据怎么读ScanResult对象里包含的信息比大多数人用到的要多。除了device和rssiadvertisementData里的manufacturerData经常被忽略但它在实际项目中非常有用。很多厂商会把设备状态、电量、传感器数据直接编码在厂商自定义数据里这样不需要建立连接就能读取功耗极低。FlutterBluePlus.scanResults.listen((results) { for (ScanResult r in results) { print(${r.device.remoteId} ${r.advertisementData.advName} RSSI:${r.rssi}); if (r.advertisementData.manufacturerData.isNotEmpty) { r.advertisementData.manufacturerData.forEach((key, value) { print(厂商ID: $key 数据: ${value}); }); } } });RSSI 的读取有个细节它不是稳定值同一位置连续扫描会看到它在正负 5 到 10 dBm 之间跳动。如果你打算用 RSSI 做粗略的距离估算至少要做滑动平均滤波取最近 5 到 10 次的均值。但要说清楚RSSI 测距的精度非常有限2.4GHz 频段的信号受人体遮挡、墙面反射影响极大同一个位置转身都能让 RSSI 跳 15 dBm 以上。真要做距离判断建议只用 RSSI 做近/远的二值判断不要试图算出精确米数。1.3 连接建立过程中的超时与重试connect方法有一个timeout参数默认是 35 秒。这个默认值在大多数场景下够用但在设备广播间隔较长比如 1 秒以上或者信号较弱时可能会超时。我的经验是把超时设成 15 到 20 秒比较合理太短容易误判失败太长会让用户等得不耐烦。try { await device.connect(timeout: Duration(seconds: 15)); } catch (e) { print(连接失败: $e); // 重试逻辑 await Future.delayed(Duration(seconds: 2)); await device.connect(timeout: Duration(seconds: 15)); }连接失败的原因很多最常见的是设备已经被其他手机连上了。BLE 从设备通常只允许一个中心设备连接如果设备还在上一个连接里没释放新的连接请求就会被拒绝。这时候需要先让设备端断开或者等它自己超时释放。另一个常见原因是 Android 的蓝牙缓存问题设备换了 MAC 地址或者服务变了但系统缓存还是旧的导致连接后服务发现异常。解决办法是在 Android 设置里清除蓝牙缓存或者在应用层调用device.disconnect()后等几秒再重连。2. 服务发现与 GATT 通信数据读写的核心机制2.1 服务发现为什么有时候会失败连接成功之后下一步是discoverServices()。这个操作在 Android 上对应的是discoverServices回调在 iOS 上对应的是didDiscoverServices。看起来只是走个流程但实际项目中服务发现失败的概率不低。一个典型场景是连接成功后立刻调用discoverServices()结果返回空列表或者抛异常。原因在于 BLE 协议栈在连接建立后需要一定时间完成底层初始化尤其是 Android 平台上onConnectionStateChange回调收到STATE_CONNECTED之后GATT 层的准备可能还没完成。我的做法是在连接成功和调用discoverServices()之间加一个 200 到 500 毫秒的延迟实测能显著降低服务发现失败率。await device.connect(timeout: Duration(seconds: 15)); await Future.delayed(Duration(milliseconds: 300)); ListBluetoothService services await device.discoverServices();另一个坑是 Android 的 GATT 缓存。如果设备固件更新后服务 UUID 变了但手机系统还缓存着旧的服务列表discoverServices()返回的就是旧数据。这个问题在开发阶段特别烦人因为每次改固件都要手动清缓存。一个绕过方法是在连接时使用device.connect(mtu: null)并配合device.requestMtu()触发一次完整的服务刷新但这不是官方推荐做法。更可靠的方式是在 Android 的BluetoothGatt层面调用refresh()flutter_blue_plus 没有直接暴露这个接口需要自己写平台通道调用。2.2 特征值读写write 和 writeWithoutResponse 的区别flutter_blue_plus 提供了两种写特征值的方式write和writeWithoutResponse。这两个方法的区别不只是有没有响应这么简单底层走的协议流程完全不同。write走的是 ATT Write Request需要从设备返回 Write Response有确认机制可靠性高但每次写入都要等一个往返吞吐量低。writeWithoutResponse走的是 ATT Write Command不需要响应吞吐量高但丢包了应用层不知道。选择哪个取决于你的数据特性如果是配置参数、控制指令这类不能丢的数据用write如果是传感器数据流、音频数据这类可以容忍少量丢包的场景用writeWithoutResponse。// 可靠写入适合控制指令 await characteristic.write([0x01, 0x02], withoutResponse: false); // 高速写入适合数据流 await characteristic.write(data, withoutResponse: true);还有一个容易忽略的点writeWithoutResponse在 Android 上如果发送频率太高底层缓冲区满了会直接丢弃数据而且不会报错。我在做一个固件升级功能时用writeWithoutResponse连续发送数据包结果设备端收到的数据缺了一段排查了很久才发现是发送速率超过了 BLE 连接间隔允许的吞吐量。后来改成每发送一包等 10 到 15 毫秒问题就消失了。这个等待时间不是随便定的它和连接间隔有关后面会详细说。2.3 通知订阅与数据流处理BLE 的通知机制是设备主动向手机推送数据的方式对应的是 CCCDClient Characteristic Configuration Descriptor的配置。flutter_blue_plus 里用setNotifyValue(true)来开启通知然后监听characteristic.onValueReceived或者characteristic.lastValueStream。await characteristic.setNotifyValue(true); characteristic.onValueReceived.listen((value) { print(收到通知数据: $value); });这里有个关键细节setNotifyValue是异步操作它需要先写 CCCD 描述符等设备确认之后通知才会真正生效。如果在setNotifyValue返回之前就期待收到数据可能会漏掉前几包。正确的做法是await setNotifyValue(true)之后再开始监听或者更保险一点等 100 毫秒再开始处理数据。通知数据的解析也有讲究。BLE 特征值的数据格式完全由设备端定义没有统一标准。常见的有定长格式、TLV 格式、JSON 字符串等。我在项目里一般会先和固件同事确认数据协议然后在 Dart 侧写一个专门的解析类把Listint转成业务对象。不要直接在 UI 层解析原始字节那样代码会很难维护。3. 连接参数与 MTU影响吞吐量和稳定性的隐藏变量3.1 连接间隔、从设备延迟和超时时间BLE 连接建立后中心设备和从设备之间会协商一组连接参数包括连接间隔Connection Interval、从设备延迟Slave Latency和连接超时Connection Timeout。这三个参数直接决定了通信的实时性、功耗和稳定性但 flutter_blue_plus 没有直接暴露修改接口需要调用平台原生方法。连接间隔是两次连接事件之间的时间范围是 7.5 毫秒到 4 秒。间隔越短数据传输越及时但功耗越高。iOS 对连接间隔有比较严格的要求通常会在 15 到 30 毫秒之间Android 则相对灵活。从设备延迟允许从设备跳过若干个连接事件不响应用来省电但会增加数据延迟。连接超时是两次成功连接事件之间的最大允许时间超过这个时间就认为连接断了。在实际项目中如果你做的是实时性要求高的应用比如游戏手柄、实时控制需要把连接间隔设小通常 15 到 20 毫秒。如果是周期性上报数据的传感器连接间隔可以设大一些比如 100 到 500 毫秒省电。修改连接参数需要通过平台通道调用 Android 的requestConnectionPriority或 iOS 的setDesiredConnectionLatency。3.2 MTU 协商一次能传多少字节MTUMaximum Transmission Unit决定了单个 ATT 数据包能携带的最大字节数。BLE 默认的 ATT MTU 是 23 字节减去 3 字节的 ATT 头实际有效载荷是 20 字节。这意味着如果你要传 100 字节的数据至少需要分 5 包发送。flutter_blue_plus 提供了requestMtu方法来协商更大的 MTUint mtu await device.requestMtu(512); print(协商后的 MTU: $mtu);但要注意requestMtu返回的是协商结果不一定等于你请求的值。Android 上最大通常能到 517 字节iOS 上根据设备不同在 135 到 527 字节之间。而且 MTU 协商是双方的事情设备端也要支持才行。我在项目里遇到过请求 512 结果只协商到 23 的情况原因是固件端的 BLE 协议栈配置没改默认就是 23。所以 MTU 优化需要软硬件配合不能只改手机端。MTU 协商成功后write和writeWithoutResponse的单包数据长度就可以超过 20 字节了。但实际测试发现即使 MTU 协商到 512单次写入超过 200 字节时Android 底层有时还是会分片而且分片之间的间隔不稳定。所以我的建议是单包数据控制在 MTU 减去 3 字节以内并且留一些余量不要贴着上限发。3.3 吞吐量实测与优化理论上的 BLE 吞吐量计算是这样的假设连接间隔是 15 毫秒每个连接事件可以发送多个数据包MTU 是 247 字节那么理论吞吐量大约是 247 乘以每事件包数再除以 0.015 秒。但实际吞吐量远低于理论值因为协议栈处理、射频切换、确认机制都会消耗时间。我在实际项目中做过一组对比测试用的是同一款 Android 手机和同一个 BLE 从设备配置连接间隔MTU写入方式实测吞吐量A15ms23write约 2 KB/sB15ms247write约 12 KB/sC15ms247writeWithoutResponse约 45 KB/sD30ms247writeWithoutResponse约 28 KB/s从这组数据能看出几个结论MTU 从 23 提升到 247吞吐量提升非常明显writeWithoutResponse比write快好几倍连接间隔从 15 毫秒放宽到 30 毫秒吞吐量下降接近一半。所以如果你的应用需要高速传输优先优化 MTU 和写入方式其次才是连接间隔。但高速传输有个代价稳定性下降。writeWithoutResponse在高吞吐下丢包率会上升尤其是在 2.4GHz 频段拥挤的环境里。我的做法是在应用层加一个简单的序号和重传机制发送端给每个包编号接收端发现序号不连续就请求重传。这样虽然增加了一点复杂度但能把丢包率控制在可接受范围内。4. 平台差异与兼容性Android 和 iOS 的那些不一样4.1 Android 的权限与定位要求Android 平台上做 BLE 扫描权限是个绕不开的话题。Android 6.0 到 11 需要ACCESS_FINE_LOCATION权限才能扫描到 BLE 设备因为 BLE 扫描结果可以用来推断位置。Android 12 开始引入了BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE三个新权限不再强制要求定位权限。在AndroidManifest.xml里需要声明这些权限uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 /neverForLocation这个标志告诉系统你的应用不会用扫描结果推断位置这样在 Android 12 及以上就不需要定位权限了。但要注意如果你的应用确实需要定位功能或者目标设备在广播包里带了位置相关信息就不能加这个标志。还有一个坑Android 的权限请求是异步的而且用户可能拒绝。在调用startScan之前一定要检查权限状态没有权限就请求请求被拒就引导用户去设置页。不要假设权限一定会有否则在部分机型上会直接崩溃。4.2 iOS 的后台模式与状态保存iOS 对 BLE 的后台运行限制比较严格。默认情况下应用进入后台后 BLE 扫描会停止连接也可能被断开。如果需要在后台继续接收 BLE 数据需要在 Xcode 的 Capabilities 里开启Background Modes中的Uses Bluetooth LE accessories。但即使开启了后台模式扫描行为也会变化。后台扫描时withServices参数变成必填而且系统会降低扫描频率扫描结果的回调间隔会变长。另外iOS 后台扫描不会返回advertisementData里的厂商自定义数据只能拿到设备 UUID 和 RSSI。这个限制在做后台设备发现时影响很大需要提前设计好应对方案。iOS 还有一个State Preservation and Restoration机制允许应用被系统终止后在 BLE 事件发生时重新启动。这个机制需要在初始化CBCentralManager时传入一个恢复标识符。flutter_blue_plus 对这个机制的支持有限如果需要用到可能要在原生侧做额外处理。4.3 设备兼容性问题的排查思路BLE 设备兼容性问题是最让人头疼的因为同样的代码在不同手机上表现可能完全不同。我总结了一套排查思路第一步确认问题出在哪个阶段。是扫描不到、连接不上、服务发现失败、还是读写数据异常每个阶段的问题原因和排查方法都不一样。第二步用通用的 BLE 调试工具交叉验证。Android 上可以用 nRF ConnectiOS 上可以用 LightBlue。如果通用工具能正常操作设备说明设备端没问题问题在应用代码如果通用工具也不行那就是设备端或者手机蓝牙模块的问题。第三步抓包分析。如果有条件用 nRF52840 这类支持 BLE 抓包的开发板配合 Wireshark 抓空口数据能看到完整的协议交互过程。这是最彻底的排查方式但门槛也最高。第四步对比测试。用同一款应用在不同手机上测试如果只有某一款手机有问题大概率是那款手机的蓝牙协议栈实现有差异。这种情况只能针对性地做兼容处理比如增加重试次数、调整连接参数、延迟操作等。我在项目里遇到过一个典型兼容性问题某款国产手机在连接 BLE 设备后discoverServices()返回的服务列表里缺少一个自定义服务。用 nRF Connect 看是正常的说明设备端没问题。后来发现是那款手机的蓝牙协议栈对服务数量有限制超过一定数量后后面的服务就不返回了。解决办法是把自定义服务放在服务列表的前面或者减少不必要的服务。这种问题没有文档可查只能靠实测和积累。5. 连接稳定性与异常恢复的实战策略5.1 断开重连的正确姿势BLE 连接断开是常态不是异常。信号干扰、距离过远、设备休眠、系统资源回收都可能导致断开。关键是要有一套可靠的重连机制。flutter_blue_plus 提供了connectionState流来监听连接状态变化device.connectionState.listen((state) { if (state BluetoothConnectionState.disconnected) { // 触发重连 _scheduleReconnect(device); } });重连不能太频繁否则会消耗大量电量而且可能因为设备还没准备好而反复失败。我的做法是采用指数退避策略第一次断开后等 1 秒重连失败等 2 秒再失败等 4 秒最多等 30 秒。同时设置一个最大重试次数比如 10 次超过之后提示用户手动重连。还有一个细节重连之前要确保旧的连接已经完全释放。调用device.disconnect()之后底层可能需要几百毫秒才能完成清理。如果立刻发起新连接可能会因为资源冲突而失败。我的做法是在重连之前先等 500 毫秒并且检查device.connectionState确认已经是断开状态。5.2 数据收发的队列管理在实际项目中多个页面或模块可能同时需要读写同一个 BLE 特征值。如果不做队列管理并发读写会导致数据错乱或者操作失败。我的做法是给每个特征值维护一个操作队列所有读写请求都排队执行前一个完成后再执行下一个。class BleOperationQueue { final _queue Future Function()[]; bool _isProcessing false; FutureT addT(FutureT Function() operation) async { final completer CompleterT(); _queue.add(() async { try { final result await operation(); completer.complete(result); } catch (e) { completer.completeError(e); } }); _processQueue(); return completer.future; } void _processQueue() async { if (_isProcessing) return; _isProcessing true; while (_queue.isNotEmpty) { final op _queue.removeAt(0); await op(); } _isProcessing false; } }这个队列看起来简单但能解决很多并发问题。尤其是通知订阅和主动读取同时进行的时候没有队列管理很容易出现数据竞争。5.3 低功耗与性能的平衡BLE 应用最终都要面对功耗问题。手机端的功耗主要来自扫描、连接维持和数据传输。扫描是最耗电的所以扫描要有超时不能一直扫。连接维持的功耗和连接间隔有关间隔越大越省电。数据传输的功耗和吞吐量有关传得越快越省电因为可以更快进入空闲状态。我的经验是扫描阶段设置 15 秒超时超时后停止扫描让用户手动触发重新扫描。连接阶段根据业务需求选择合适的连接间隔不需要实时通信时用大间隔。数据传输阶段尽量批量发送减少连接事件的次数。另外不需要通知的时候及时关闭通知不需要连接的时候及时断开这些细节累积起来对功耗影响很大。还有一个容易被忽略的点Android 上如果应用进入后台但没有断开 BLE 连接系统可能会在一段时间后强制断开以节省电量。如果应用需要在后台保持连接需要申请前台服务或者使用 WorkManager 定期唤醒。这部分涉及 Android 后台机制和 BLE 本身关系不大但做产品时必须考虑。6. 从协议栈视角理解 flutter_blue_plus 的边界6.1 Dart 层能做什么、不能做什么flutter_blue_plus 把 BLE 操作封装成了 Dart 接口但它的能力边界是由底层平台 API 决定的。Dart 层能做的是发起扫描、建立连接、发现服务、读写特征值、订阅通知、协商 MTU。Dart 层不能做的是修改连接参数需要平台通道、访问原始 HCI 层数据、控制射频参数、实现自定义的 GATT 客户端行为。理解这个边界很重要因为它决定了遇到问题时你能在哪个层面解决。比如连接间隔优化flutter_blue_plus 没有提供接口你就需要写平台通道调用原生方法。再比如某些设备需要特殊的配对流程flutter_blue_plus 的createBond方法在 Android 上可用但 iOS 上没有对应的公开接口因为 iOS 的配对流程是系统自动处理的。6.2 什么时候需要写平台通道以下几种情况需要考虑写平台通道第一种是修改连接参数。前面提到过flutter_blue_plus 没有暴露连接间隔、从设备延迟、超时时间的设置接口。如果应用对实时性有要求需要在 Android 侧调用BluetoothGatt.requestConnectionPriority()在 iOS 侧调用setDesiredConnectionLatency()。第二种是清除 GATT 缓存。Android 的 GATT 缓存问题在开发阶段很常见flutter_blue_plus 没有提供清除缓存的接口需要在原生侧调用BluetoothGatt.refresh()。但要注意这个方法是隐藏 API不同 Android 版本行为可能不一致使用时要做好兼容处理。第三种是处理特殊的配对和加密流程。有些 BLE 设备在连接后需要进行配对才能访问某些特征值flutter_blue_plus 的createBond在 Android 上可以用但配对过程中的 PIN 码输入、配对结果回调等细节需要原生侧处理。写平台通道的原则是能用 Dart 层解决的就在 Dart 层解决实在解决不了的才写平台通道。因为平台通道会增加代码复杂度而且 Android 和 iOS 要分别实现维护成本高。6.3 常见异常码的含义与处理flutter_blue_plus 抛出的异常通常包含平台原生的错误码理解这些错误码能帮你快速定位问题。以下是我在实际项目中遇到过的几个典型错误错误码/信息平台含义处理方式GATT_INSUFFICIENT_AUTHENTICATIONAndroid特征值需要加密连接先配对再访问GATT_INSUFFICIENT_ENCRYPTIONAndroid加密强度不够检查配对状态GATT_WRITE_NOT_PERMITTEDAndroid特征值不可写检查特征值属性GATT_CONNECTION_CONGESTEDAndroid连接拥塞降低发送速率CBErrorCodePeerRemovedPairingiOS配对信息被移除重新配对CBErrorCodeConnectionTimeoutiOS连接超时重试或检查信号GATT_CONNECTION_CONGESTED这个错误值得单独说。它表示底层缓冲区满了通常是因为发送速率太快。遇到这个错误不要立刻重试而是应该等一段时间再发或者降低发送频率。我在做固件升级功能时遇到过这个错误一开始以为是设备问题后来发现是发送间隔太短改成每包间隔 20 毫秒后就再没出现过。7. 一个完整的 BLE 通信模块该怎么组织7.1 分层设计从协议到业务一个可维护的 BLE 模块不应该把所有逻辑堆在一个类里。我的做法是分成三层协议层、服务层、业务层。协议层负责和 flutter_blue_plus 直接交互封装扫描、连接、服务发现、读写、通知等基础操作。这一层不包含任何业务逻辑只提供通用的 BLE 操作接口。服务层针对具体的设备协议把原始字节数据解析成业务对象把业务指令编码成字节数据。业务层面向 UI处理用户交互和状态管理。这样分层的好处是换一个 BLE 设备只需要改服务层协议层和业务层基本不用动。协议层可以抽出来做成一个独立的包在多个项目中复用。7.2 状态管理的选择Flutter 里做 BLE 状态管理可选方案很多Provider、Riverpod、Bloc、GetX 等。我的建议是不要用太重的方案BLE 的状态变化比较频繁用轻量的状态管理就够了。我一般用ChangeNotifier或者ValueNotifier来管理连接状态和数据配合StreamBuilder或者ValueListenableBuilder更新 UI。关键是要把 BLE 的状态和 UI 的状态分开。BLE 状态包括适配器是否开启、是否在扫描、是否已连接、服务是否发现、通知是否订阅。UI 状态包括加载中、错误提示、数据展示。两者不要混在一起否则代码会很难维护。7.3 测试与调试的实用技巧BLE 开发离不开真机调试模拟器不支持蓝牙。我的调试流程是这样的开发阶段先用 nRF Connect 或者 LightBlue 确认设备的基本功能正常拿到设备的服务 UUID、特征值 UUID、读写属性、数据格式。然后在 Flutter 应用里实现基本流程用日志打印每一步的操作和结果。日志要详细包括时间戳、操作类型、数据内容、返回结果。测试阶段准备多台不同品牌和系统的手机覆盖 Android 和 iOS 的主流版本。重点测试扫描成功率、连接成功率、服务发现成功率、读写稳定性、通知实时性、断开重连可靠性。每个测试项都要有明确的通过标准比如连接成功率要在 95% 以上。遇到问题时先看日志定位到具体阶段再用通用工具交叉验证最后用抓包工具分析协议交互。这个过程看起来繁琐但能帮你快速缩小问题范围避免盲目猜测。7.4 代码组织的一个实际例子最后给一个我实际项目里的代码组织示例展示协议层和服务层怎么配合// 协议层通用 BLE 操作 class BleManager { Futurevoid connect(BluetoothDevice device) async { await device.connect(timeout: Duration(seconds: 15)); await Future.delayed(Duration(milliseconds: 300)); await device.discoverServices(); } FutureListint readCharacteristic( BluetoothCharacteristic characteristic, ) async { return await characteristic.read(); } Futurevoid writeCharacteristic( BluetoothCharacteristic characteristic, Listint data, { bool reliable true, }) async { await characteristic.write(data, withoutResponse: !reliable); } } // 服务层针对具体设备的协议解析 class MyDeviceService { final BleManager _ble; BluetoothCharacteristic? _dataCharacteristic; MyDeviceService(this._ble); Futurevoid init(BluetoothDevice device) async { await _ble.connect(device); final services device.servicesList; for (final service in services) { for (final characteristic in service.characteristics) { if (characteristic.uuid Guid(0000FFE1-0000-1000-8000-00805F9B34FB)) { _dataCharacteristic characteristic; } } } if (_dataCharacteristic null) { throw Exception(未找到数据特征值); } await _dataCharacteristic!.setNotifyValue(true); } Futurevoid sendCommand(int command, Listint payload) async { final data [command, ...payload]; await _ble.writeCharacteristic(_dataCharacteristic!, data); } StreamDeviceData get dataStream { return _dataCharacteristic!.onValueReceived.map((bytes) { return DeviceData.parse(bytes); }); } }这个结构看起来简单但实际用起来很顺手。协议层可以复用到其他 BLE 项目服务层只关注当前设备的协议细节业务层通过服务层暴露的接口操作设备不需要关心底层是 BLE 还是其他通信方式。我在实际使用中发现flutter_blue_plus 的稳定性在大多数场景下是够用的但它的抽象层次决定了它无法覆盖所有 BLE 的细节。遇到平台特有的问题时不要死磕 Dart 层该写平台通道就写平台通道。另外BLE 开发中很多问题的根源不在代码而在设备固件和射频环境所以软硬件联调的能力很重要。最后分享一个小技巧在开发阶段把 BLE 操作的日志写到文件里方便事后分析尤其是那些偶发的连接断开和数据异常没有日志根本无从查起。
返回列表