搞机器人开发,或者做智能硬件的朋友,大概率都有过这个体验:开发板买回来,烧录好了系统,通电之后一切正常,结果卡在最不起眼的一步——给设备配上网。
RDK X5 这块板子是地平线面向机器人场景的整套开发套件,处理器、接口、系统都齐了,但它没有标配一块触摸屏。你在调试时想让它连上家里的路由器,总不能拿根网线插上去,也不能像手机一样弹个 WiFi 设置界面出来。这时候,BLE 配网就是最顺手的方案:手机 App 打开蓝牙,靠近设备,把 WiFi 的 SSID 和密码通过低功耗蓝牙传过去,设备收到后自己去连路由器,整个过程不需要额外硬件,不需要输入命令行,也避免了反复切换热点。
这篇文章我打算按照自己实际调过 BLE 配网项目的经验,把原理和实操一起讲清楚。内容包括:BLE 配网为什么能解决机器人开发板的联网问题、从广播到回连 WiFi 的完整协议流程、RDK X5 上怎么用 BlueZ 和 Python 实现一个配网 GATT 服务、手机端(尤其是 Android/iOS 差异和 uni-app 这类跨平台框架)做配网要注意什么,以及我在实际调试中踩过的一堆坑。内容偏硬核,但我会尽量把每一步为什么这样做都说透,保证你拿回去能直接用。
1. BLE配网是什么,为什么RDK X5需要它
1.1 没有屏幕的联网设备,怎么把WiFi密码告诉它
先说一个最基本的场景:新买的智能音箱、扫地机器人、摄像头,它们都没有屏幕,也没有键盘,但都需要连上你家 WiFi 才能用。配网的本质,就是找到一种方式,让手机或电脑把你选择的 WiFi 名称和密码"安全地"告诉设备。
传统做法有不少:有些设备支持 Web 配网,也就是设备自己开一个热点,手机连上这个热点后访问 192.168.x.x 页面填写 WiFi 信息,填完设备自动去连目标路由器。这种方案叫 SoftAP(Soft Access Point)配网,稳定但麻烦,因为手机要从家庭 WiFi 切换到设备热点,很多手机在无互联网的热点下会提示"是否保持连接",交互很繁琐。
还有一种思路是广播配网(SmartConfig 一类方案):手机和设备在同一个路由器下,手机把密码编码到 UDP 广播包里,设备抓包后解析。这个方案在老 ESP8266/ESP32 时代很流行,但它依赖路由器环境,5G/2.4G 频段隔离、AP 隔离、甚至某些路由器广播风暴抑制都会导致失败。
BLE 配网走的是另一条路:设备以低功耗蓝牙广播自己的配网服务,手机通过蓝牙通道和设备建立点对点连接,直接发送 WiFi 凭据。它不依赖路由器,不受频段隔离影响,交互也最接近"扫码配对"的现代体验。对于 RDK X5 这种本身带 WiFi 和蓝牙二合一模块的 Linux 开发板来说,BLE 配网几乎是为它量身定做的。
1.2 几种配网方式的优缺点对比
我把我实际用过的几种配网方式整理成一张表,方便你选型的时候直接对照:
| 配网方式 | 大致原理 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|
| SoftAP配网 | 设备开热点,手机连热点后Web页面配置 | 依赖少,协议简单 | 手机要切换网络,交互笨重 | 摄像头、打印机 |
| SmartConfig/广播配网 | 手机向路由器广播加密报文 | 不需切换网络 | 依赖路由器广播,失败率高 | 老款IoT设备 |
| BLE配网 | 蓝牙点对点GATT通道下发WiFi信息 | 不依赖路由器、交互好、可双向反馈 | 需要设备额外支持BLE,手机权限复杂 | 智能家居、机器人开发板 |
| 串口/命令行配网 | 通过USB串口执行配置命令 | 最直接,可控性高 | 非技术人员没法用 | 开发调试、工业设备 |
RDK X5 作为一块面向开发者、也会被做成机器人产品原型的主板,BLE 配网的优势非常明显:开发者自己调试用 App 很方便;如果是做给最终用户的产品,不需要对方会敲命令行,也不需要对方理解"热点"是什么概念,打开 App 点两下就行。
1.3 BLE配网能走通的条件
这里我要泼一盆冷水:BLE 配网不是万能的,它有自己的一堆前提条件。
第一,设备端必须真的支持 BLE 从机角色,并且系统里有可用的蓝牙协议栈。RDK X5 的板载 WiFi/BT 模组支持蓝牙 5.2,系统跑的是 Ubuntu,标配 BlueZ 协议栈,这个条件天然满足。如果你用的是某块精简版开发板,系统裁剪掉蓝牙栈,那就得先补这一环。
第二,手机和设备的 BLE 必须兼容。BLE 本身是标准协议,但各厂商实现存在差异,特别是 Android 各家的系统蓝牙栈经常有诡异 bug,后面我专门开一节讲。
第三,配网过程要处理好"切换网络"这个动作。设备收到 WiFi 信息后,要断开或保持蓝牙连接的同时尝试连接路由器。蓝牙和 WiFi 通常共用一根天线或共用射频,RDK X5 这类模块在连接 WiFi 时会短暂影响蓝牙连接,所以设备端要先确认数据收完整,再去连 WiFi,不能一边收数据一边去连 WiFi。
第四,也是最容易被忽略的:配网只解决了"告诉设备密码"的问题,后续设备能否真正上网,还取决于路由器配置、频段、无线加密方式等。这些我都会在常见问题里展开。
1.4 RDK X5的硬件能力决定了配网可以这样做
RDK X5 的定位是机器人开发套件,核心是地平线旭日X5,8核 ARM CPU 加 BPU,系统跑 Ubuntu 22.04,外设接口齐全。它板载的无线模组集成了 WiFi 和蓝牙,蓝牙支持 BLE 5.2。这意味着它既能作为 BLE 外围设备(Peripheral)对外提供 GATT 服务,也能用成熟的 Linux 网络管理工具(NetworkManager / wpa_supplicant)去连接目标 WiFi。
在 BLE 配网这件事上,我不需要额外购买任何模块,系统自带的 BlueZ 就能完成大部分工作。BlueZ 是 Linux 下的官方蓝牙协议栈,它对外提供 D-Bus 接口,我们通过 D-Bus 可以动态创建 GATT 服务、管理广播、处理设备连接。这也是在 RDK X5 上实现配网的主要技术路径。相比嵌入式 MCU 上常用的 ESP-IDF 或 Zephyr 协议栈,Linux + BlueZ 的方法更接近"在电脑上写服务"的思路,代码量不大,但需要理解 D-Bus 的接口模型。
2. BLE配网背后的协议流程拆解:从广播到WiFi回连
2.1 先搞懂BLE的物理基础和三个层次
配网代码不多,但如果不懂 BLE 的基本结构,遇到问题会完全不知道从哪里排查。我先快速过一遍必须知道的概念。
BLE 工作在 2.4GHz ISM 频段,这是它和 WiFi 2.4G 共享的物理频段。BLE 把频段划分成了 40 个信道,每个信道间隔 2MHz。其中三个信道(37、38、39)专门用来广播,其余 37 个信道用于连接后的数据通信,并且采用跳频机制,每次通信在多个信道之间切换,这样能降低干扰。这就是为什么 BLE 抗干扰能力还不错的原因。
然后是协议栈的三个层面:
- GAP(Generic Access Profile)负责设备发现和连接广播。设备以可连接广播方式(Advertising)对外宣告自己的存在,里面会带设备名、广播数据、服务 UUID。
- GATT(Generic Attribute Profile)定义数据交互规范。连接建立后,设备之间通过属性(Attribute)交换数据,属性按 Service(服务)和 Characteristic(特征值)组织。每个 Service/Characteristic 都有一个 UUID 来标识,0x1800、0x180A 这类是标准 UUID,厂商自定义服务通常用 0xFFF0 到 0xFFFF。
- ATT(Attribute Protocol)是 GATT 的底层传输协议。Characteristic 上的 Read、Write、Notify、Indicate 都是 ATT 层的操作。手机订阅设备的 Notify,设备就能主动把数据推给手机。
配网就是围绕这三层展开的:GAP 让手机发现设备,GATT 让双方建立"数据通道",ATT 负责把 WiFi 凭据和状态消息传过去。
2.2 一次BLE配网的完整时序
一次标准配网,从设备上电到手机 App 显示配网成功,通常会走下面这 12 步:
- RDK X5 启动后,通过 BlueZ 开启 BLE 广播,广播名类似 RDK5-XXXXXXXX,厂商数据里带上配网标识位。
- 手机 App 调用系统蓝牙 API 开始扫描。
- App 扫描到设备,过滤广播名或者 Service UUID,锁定目标。
- App 发起连接,设备端接受连接。
- 连接建立后,App 发现 GATT 服务,订阅配网状态特征的 Notify。
- App 向配网控制特征写入第一条命令(例如请求设备信息,或直接写 WiFi SSID)。
- 设备解析并响应 ACK,表明数据已收到。
- App 写入 WiFi 密码。
- 设备确认数据完整后,主动去连接目标 WiFi。
- 连接 WiFi 是异步过程,设备通过状态特征 Notify 手机上"正在连接"的状态。
- WiFi 连接成功(或失败),设备通过 Notify 通知手机结果和当前 IP。
- App 提示用户配网成功,设备端关闭配网广播或者退出配网模式。
注意第 5 步很多人会忘记订阅 Notify,导致后面设备向手机发消息,手机根本收不到。Android 侧尤其明显,iOS 侧漏订阅也不会报错,但回调就是不来。
2.3 配网消息格式怎么设计
配网通道里传的最主要是两类数据:手机下发的 WiFi 凭据,设备返回的状态。数据格式可以很简单,也可以做得很复杂,取决于你对安全性和扩展性的要求。
最简单的方案是直接传 JSON 字符串。比如手机写入控制特征的数据是:
{"type": "set_wifi", "ssid": "HomeWiFi", "password": "12345678"}设备解析 JSON,拿到 SSID 和密码,调用 NetworkManager 连接。JSON 的好处是调试方便,手机上随便一个 BLE 调试工具(比如 nRF Connect)就能直接写入,设备端用 Python 的 json 模块解析就能用。
坏处是,JSON 文本没有强约束,字段拼错、编码不对、长度超了都可能出问题。而且 BLE 单次写入的数据受到 MTU(Maximum Transmission Unit)限制,默认 MTU 为 23 字节,其中有效载荷只有 20 字节。就算协商到较大 MTU(例如 Android 常见 247 字节),一个包含中文 SSID 和特殊字符密码的 JSON 也经常需要分多次写入。所以工程上更常见的是 TLV 格式或者固定字段的二进制格式,比如用 1 字节 type、1 字节 length、N 字节 value 固定一套协议,每次只传一个字段,由手机发完 SSID 再发密码,设备侧维护一个接收状态机。
乐鑫的 ESP-BLE 配网协议就是类似思路,它用 Protobuf 定义消息,消息里分阶段传递版本信息、安全会话、WiFi SSID、密码、校验信息等,优点是可扩展性强、安全性高,缺点是协议偏重。RDK X5 上跑的是 Linux,处理器性能完全够,我建议不要一上来就上 Protobuf,先拿 JSON 跑通全流程,再根据产品需求决定要不要换成更紧凑的协议。我自己就是这么做的,先在调试阶段图省事用 JSON,后面产品化时再换成 TLV。
2.4 安全问题不能忽略:明文密码直传的风险
很多开发者做原型时直接把 SSID 和密码明文写在 Write 请求里,这在局域网调试环境没问题,但如果是做产品,黑客完全可以拿一台支持 BLE 嗅探的设备监听配网过程,拿到明文密码。
要解决这个问题,方案大致有三档:
- 最基础:在配网广播包或设备外壳上提供一次性随机码(PIN Code),手机和设备通过数值比较或固定 PIN 完成 BLE 配对绑定时,BlueZ 会自动对链路数据做加密,这时候数据在空口是加密的。
- 进阶:建立独立的应用层安全协商,类似乐鑫配网里的 Security 会话。手机和设备先交换公钥,协商出会话密钥,之后所有 WiFi 凭据都加密后再传输。
- 最省事:如果 RDK X5 是给开发调试用的,或者只在受控网络里使用,可以直接依赖 BLE 链路层的加密配对,而不是在应用层重复造轮子。
有一说一,RDK X5 这类开发板大多数时候是在实验室或者开发者手里,很多项目根本不加锁。但我还是建议至少把 Just Works 配对打开,让链路层把数据加密了,成本很低。真做产品再考虑更重的安全方案。
3. 在RDK X5上用BlueZ实现BLE配网的实操过程
3.1 环境准备:确认蓝牙设备和BlueZ工具链
RDK X5 刷好官方 Ubuntu 系统后,蓝牙驱动和 BlueZ 一般已经装好。上电进入系统,我们先做三件事:
第一,确认蓝牙控制器是否存在:
hciconfig -a如果输出里有 hci0 并且 Type 为 Primary,说明蓝牙控制器正常。
第二,确认 BlueZ 服务在运行:
systemctl status bluetooth没启动的话执行sudo systemctl start bluetooth,并设置开机自启。
第三,用 bluetoothctl 简单验证扫描能力:
bluetoothctl [bluetooth]# power on [bluetooth]# scan on看到手机或周围蓝牙设备不断刷新,说明蓝牙收发通路没问题。
提示:如果
hciconfig看不到 hci0,先检查 dmesg 有没有蓝牙 USB/MMC/SDIO 报错。RDK X5 的无线模块是板载的,极少出现硬件问题,大部分是驱动被系统裁剪或固件缺失。此时执行sudo apt install bluez bluez-tools并检查/lib/firmware下有没有对应固件。
3.2 用Python D-Bus创建一个GATT配网服务
BlueZ 从 5.x 开始,推荐用 D-Bus API 管理蓝牙,包括创建 GATT 服务。流程是:写一个 D-Bus 服务,提供 GattService1 和 GattCharacteristic1 接口,再注册到 BlueZ 的 GattManager1。
我这里给出一个精简版实现思路,完整代码我会拆成三个部分讲。
先引入依赖并连上系统总线:
import asyncio import json import subprocess from dbus_next.aio import MessageBus from dbus_next.service import ServiceInterface, method, dbus_property from dbus_next import Variant, DBusError from dbus_next.signature import Variant第一段:定义配网服务的 Service 接口,UUID 用厂商自定义范围内的值。
PROVISION_SERVICE_UUID = '0000fff0-0000-1000-8000-00805f9b34fb' PROVISION_CMD_UUID = '0000fff1-0000-1000-8000-00805f9b34fb' PROVISION_STATUS_UUID = '0000fff2-0000-1000-8000-00805f9b34fb' class ProvisionService(ServiceInterface): def __init__(self): super().__init__('org.bluez.GattService1') self.uuid = PROVISION_SERVICE_UUID self.primary = True @dbus_property def UUID(self) -> 's': return self.uuid @dbus_property def Primary(self) -> 'b': return self.primary @dbus_property def Includes(self) -> 'ao': return []第二段:定义控制特征,最主要的是 WriteValue 的回调方法。BlueZ D-Bus 接口里,手机端写入数据会触发 WriteValue。
class ProvisionCmdCharacteristic(ServiceInterface): def __init__(self, service_path): super().__init__('org.bluez.GattCharacteristic1') self.uuid = PROVISION_CMD_UUID self.service_path = service_path self._notify_cb = None @dbus_property def UUID(self) -> 's': return self.uuid @dbus_property def Service(self) -> 'o': return self.service_path @dbus_property def Flags(self) -> 'as': return ['write', 'notify'] @method() def WriteValue(self, value: 'ay', options: 'a{sv}'): raw = bytes(value).decode('utf-8', errors='ignore') print(f'[provision] received: {raw}') msg = json.loads(raw) if msg.get('type') == 'set_wifi': asyncio.get_event_loop().create_task(self._connect_wifi(msg)) async def _connect_wifi(self, msg): ssid = msg.get('ssid', '') password = msg.get('password', '') try: proc = await asyncio.create_subprocess_exec( 'sudo', 'nmcli', 'device', 'wifi', 'connect', ssid, 'password', password, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if proc.returncode == 0: await self._notify_status('connected') else: await self._notify_status('failed') except Exception as e: await self._notify_status('failed')这里注意几个细节:
WriteValue回调里不要直接做耗时操作,WiFi 连接可能 3 到 5 秒,必须用create_task异步掉,否则 BLE 数据通道会卡死,手机端表现为"写入后无响应"。- nmcli 连接需要 root 权限,推荐在系统里配置免密 sudo 的 nmcli 命令,否则 Python 进程无法执行。
- 设备连上目标 WiFi 后,之前的 BLE 连接通常还在,但载波会被 WiFi 占用,特别是共用天线方案,此时 BLE 通信延迟很大。所以状态反馈比命令下发更值得用 Notify 多推几次。
3.3 注册GATT服务到BlueZ
创建好类之后,要把它注册到 BlueZ:
async def main(): bus = await MessageBus().connect() service = ProvisionService() cmd_char = ProvisionCmdCharacteristic('/com/example/provision/service') status_char = ProvisionStatusCharacteristic('/com/example/provision/service') # 导出对象 bus.export('/com/example/provision/service', service) bus.export('/com/example/provision/service/char_cmd', cmd_char) bus.export('/com/example/provision/service/char_status', status_char) # 获取GattManager1并注册 gatt_manager = bus.get_proxy_object('org.bluez', '/org/bluez/hci0', 'org.bluez.GattManager1') await gatt_manager.RegisterApplication('/com/example/provision', {}) print('GATT provisioning service registered') asyncio.run(main())这段代码跑起来之后,用手机上的 nRF Connect 扫描,应该能看到一个 UUID 为 FFF0 的 Service。连接后就能向 FFF1 写入数据。
提示:注册 Application 时注意路径不能与已有对象冲突。如果你同时跑了其他 GATT 服务,
RegisterApplication会报 AlreadyExists。调试时先停掉其他服务,或者把 Service 的路径做得更独特一些。
3.4 配网状态反馈怎么做才靠谱
配网过程中用户最关心的是"连上了吗"。设备侧一般用 Notify 主动推状态,而不是让手机反复 Read。状态消息建议用受限的枚举类型,不要传自由文本。我常用的状态取值:
| 状态码 | 含义 | 手机端展示 |
|---|---|---|
| 0 | idle | 等待配网 |
| 1 | receiving | 已收到WiFi信息 |
| 2 | connecting | 正在连接WiFi |
| 3 | connected | 配网成功 |
| 4 | failed | 配网失败 |
| 5 | wifi_disabled | 设备WiFi不可用 |
实现 Notify 的思路是:在状态特征里维护一个_notify_cb,当设备侧收到订阅(StartNotify)时,BlueZ 会调用这个回调,后续设备调用_notify_cb把状态值推给手机。推送的频率不要太快,状态变化再推一次即可。连续推 10 次相同状态,手机端可能出现通知风暴,个别手机甚至触发系统级蓝牙异常。
3.5 配网完成后设备端应该做什么
配网成功不等于任务结束,设备侧还有三步收尾工作:
第一,保存 WiFi 配置。nmcli 连接成功后,配置会自动存到 NetworkManager 的配置目录,下次开机自动连接,不需要额外处理。但如果你后面要支持"恢复出厂"或者"切换热点",建议在配网状态文件里记录一个标志位,比如/etc/rdk_provisioned,每次开机时检查这个文件。
第二,决定何时关闭 BLE 广播。这里有个取舍:一直开着广播,方便用户重新配网,但耗电且存在被恶意连接的风险;配网成功立刻关闭广播,体验安全,但用户想改 WiFi 就得先进入配网模式。我建议在 RDK X5 上做"按住物理按键 3 秒进入配网模式"的机制,平时不开 BLE 广播,用户需要配网时按一下按键再开广播。这样是最稳妥的产品化做法。开发调试阶段则无所谓,保持常开就行。
第三,如果配网失败,要把失败原因带给用户。nmcli 失败时 stderr 会有详细原因,诸如 "secrets were required, but not provided" 表示密码错误,或 "No network with SSID" 表示没扫到这个网络。设备端可以把错误码拼到状态消息里,方便 App 做针对性提示。
4. 手机App端做配网的关键点
4.1 Android和iOS的BLE差异:权限与deviceId
配网的另一端是手机 App。这一端踩坑的数量一点都不比设备端少,核心原因就是 Android 和 iOS 对 BLE 的封装和权限模型完全不同。
先说权限。Android 从 6.0 开始,扫描 BLE 设备需要定位权限,因为蓝牙扫描能推断出设备位置;从 Android 12 开始,又引入了单独的蓝牙权限BLUETOOTH_SCAN和BLUETOOTH_CONNECT,并且运行时还要授权。很多小白在 Android 手机上扫不到设备,第一反应是设备广播有问题,实际是 App 没有申请权限,或者用户在系统设置里只给了"粗略位置"权限。
iOS 这边相对简单,只需要在 Info.plist 里声明NSBluetoothAlwaysUsageDescription,首次使用蓝牙时弹出授权框。但 iOS 的权限框出现时机是"App 访问蓝牙时",如果你在进入页面后才调用蓝牙 API,授权框会晚于用户预期,影响体验。我更推荐在 App 启动时就请求蓝牙权限。
再说 deviceId。这是很多人搞混的概念。Android 和 iOS 的 BLE API 里都叫 deviceId,但是含义完全不同。
在 iOS 上,deviceId是CBCentralManager为每个外设生成的 UUID 字符串,不是蓝牙 MAC 地址,而且这个 UUID 是系统级的,App 可以保存它并在下次启动时直接用它连接设备。唯一的问题是,如果用户在 iOS 设置里"还原网络设置"或者系统蓝牙缓存被清掉,这个 UUID 会变,保存的旧 ID 会失效。
在 Android 上,deviceId原本就是蓝牙 MAC 地址。但从 Android 6 开始,App 无法直接通过 API 拿到 MAC 地址(除非有系统权限),系统扫描返回的 deviceId 实际是一个随机生成的标识,而且扫描到的新设备每次可能会变。如果你把 Android 的 deviceId 存起来下次连接,大概率连不上。
所以回到网上那个高频问题:"uni-app BLE iOS 可以根据蓝牙的 deviceid 建立连接吗?"答案是可以。iOS 的 deviceId 是稳定合法的标识,uni.createBLEConnection({ deviceId })在 iOS 上完全没问题。Android 则不建议长期保存 deviceId,更稳妥的方式是每次重新扫描,或者按设备广播名过滤,得到最新的 deviceId 再连接。
4.2 uni-app等跨平台框架里做BLE配网的注意点
跨平台框架里,uni-app 的 BLE 封装算是比较好用的,但它的 API 设计屏蔽了一些底层细节,容易让人忽略关键的坑。我列出几个亲测有效的点:
扫描的时候一定要过滤服务。uni.scanBLEFromCentral({ services: ['FFF0'] })会比不过滤更快、更准确。不过要小心,iOS 对扫描参数里的 services 过滤比较严格,如果设备广播里没正确带上 Service UUID,直接过滤会扫不到。折中方案是先不过滤扫一轮,拿到数据后再在前端按 localName 或 service ID 过滤。
连接后订阅特征值要放在正确位置。正确流程是:createBLEConnection成功 ->getBLEDeviceServices->getBLEDeviceCharacteristics->notifyBLECharacteristicValueChange。很多人只调了notifyBLECharacteristicValueChange,但没等getBLEDeviceServices完成,导致订阅失败,设备发来的状态根本收不到。这一步最好写成 Promise 链,确保每一步都成功后进入下一步。
写入数据时注意字节和分包。uni-app 的writeBLECharacteristicValue接收的是 ArrayBuffer,如果写入超过 MTU 的数据,Android 一般能自动分割(也有可能报错),iOS 会直接要求你按 MTU 分包。最常见的错误是直接JSON.stringify后转 UTF-8 写入,密码是中文或带 emoji 时长度暴涨,超出 MTU 就静默失败。解决办法是我在 2.3 说的:别一次写完整个 JSON,按字段分步下发,每步只写几十字节,并且设备端每收到一块就 ACK 一次。
4.3 配网交互的状态机与超时设计
App 端的配网交互,本质是一个有限状态机。状态有:初始化、扫描中、连接中、写凭据中、等待设备回连 WiFi、成功、失败。必须给每个状态设超时时间,否则用户会陷入"不知道卡在哪一步"的迷茫。我常用的超时配置:
- 扫描超时:15 秒。超过后提示用户检查设备是否进入配网模式、是否离得太远。
- 连接超时:10 秒。广播能看到但连不上,通常是设备端 GATT 注册失败或者有别的手机已经连着设备。
- 整体配网超时:60 秒。设备收到凭据后连接 WiFi 的过程可能比较慢,60 秒内没收到成功或失败状态,就要提示重新尝试。
另外,配网界面的文案很重要。很多 App 配网失败,其实设备已经连上 WiFi,只是设备回推状态时手机蓝牙正好断了一下,App 却直接判定失败。所以收到"成功"状态时再放行,收到"失败"时也先稍微等一下,看看是否有后续状态补充,避免误判。
5. 配网常见问题与排查技巧实录
5.1 手机扫描不到RDK X5的广播
这个问题出现的频率最高。原因通常不是蓝牙坏了,而是设备没有在广播。
先从设备端排查:bluetoothctl里执行advertise on,如果报错,看sudo journalctl -u bluetooth -f的日志,确认广播类型是否被系统限制。如果你用程序注册了 GATT 服务但没注册广播数据,很多调试工具默认可能不显示没有广播包内容的设备。
再从手机端排查:Android 手机如果之前配对过该设备,蓝牙设置里会有缓存条目,扫描结果有时会被系统隐藏。到系统蓝牙设置里"取消配对"或"忽略此设备"再扫。iOS 用户如果试过很多次扫描不到,重启一次手机蓝牙或飞行模式开关,这是个老毛病。
还有一点很容易忽略:设备的广播功率。BLE 广播有效距离通常 10 米内,但是穿墙后基本不可用。配网时手机最好放在设备半米以内。别隔着墙扫,否则时有时无,特别打击信心。
5.2 BLE连接成功但写入数据没反应
连接正常、服务也能看到,但一写入数据,设备端没有任何日志。这种情况第一嫌疑是 MTU 和分包问题。默认 MTU 23 字节,如果你一次性写了超过 20 字节的载荷,部分 Android 手机会直接失败并回调错误码 133(GATT_ERROR),iOS 则可能分成多包发送,而设备端如果按"一次 write 就是一条完整消息"来解析,就会解析出乱码。
解决思路:App 端主动请求大 MTU。Android 上系统会自动协商,也可以在onMtuChanged回调里确认;iOS 的setMTU在 CoreBluetooth 里会自动处理。但设备端绝不能假设对端 MTU 是 247。最稳妥的方式是在协议层面约定"单条消息最大长度不超过 20 字节",超长消息由手机端分段,设备端按序号重组。这个设计虽然看起来土,但跨 Android/iOS/嵌入式三种环境都稳定。
另外检查设备端 GATT 服务的 Flags 是否包含write。如果 Characteristic 的 Flags 只配了read或write-without-response,手机写入请求会被拒绝或者静默丢弃。注意区分write和write-without-response:前者需要设备 ACK,性能稍慢但可靠;后者更快,但发完就完,设备没收到也不会有反馈。配网这种关键消息强烈建议用带响应的 Write。
5.3 配网成功后设备连不上路由器:WiFi频段和路由配置问题
RDK X5 的 WiFi 模组支持 5GHz 频段,但实际配网失败案例里,很大一部分是这两个原因。
第一个是设备扫描不到目标 SSID。比如手机连的是一个 5GHz 频段的 WiFi,配网信息里下发的是同一个 SSID,但路由器开了 Wi-Fi 6 的某些私有模式,或者 5GHz 信道在 DFS(动态频率选择)范围内,设备模组扫描可能需要更长时间,甚至扫不到。我的建议是:配网时强制让用户选择 2.4GHz 网络,或者至少提示"如果失败请尝试 2.4GHz"。
第二个是路由器开了 AP 隔离或者访客网络隔离。AP 隔离开启后,设备即使连上 WiFi,也无法访问局域网内的其他设备,体现为"配网显示成功但 App 找不到设备"。这个问题不是 BLE 配网本身能解决的,需要在路由设置里关闭 AP 隔离。
还有一个隐蔽问题:WPA2/WPA3 混合模式兼容性。有些较老的 WiFi 模组驱动对 WPA3 支持不完整,在混合模式下反而无法连接。RDK X5 驱动较新,一般不踩这个坑,但如果你在别的平台做配网,建议优先选择 WPA2-PSK。
5.4 低功耗模式下的BLE问题:从ESP32轻度睡眠说起
网上有大量关于 "ESP32 轻度睡眠打开 BLE" 的提问,这个问题本质上跟 BLE 配网的低功耗策略有关。ESP32 在 light sleep 模式下,CPU 停止,WiFi/BLE 模组进入低功耗状态,BLE 广播会停止,已经建立的连接也会断开。要保活 BLE,必须启用 modem sleep 或单独配置电源管理,让蓝牙在睡眠时保持唤醒,但这会牺牲功耗收益。
RDK X5 是 Linux 开发板,默认功耗比 ESP32 高好几个数量级,一般不会特意进 light sleep。但如果你在 RDK X5 或者其他 Linux 平台上做低功耗机器人,要记住:BLE 连接建立的瞬间,WiFi 和蓝牙可能共用天线,这时候 WiFi 扫描或者连接操作会干扰 BLE 包收发。我实测过,设备一边用 nmcli 连接 WiFi 一边用 BLE 传大文件,蓝牙丢包率明显上升。所以低功耗设备上做配网,要么先收完配网数据再启 WiFi,要么在连接 WiFi 期间关闭不必要的 BLE 通知。
5.5 手机端奇葩问题:iOS缓存与Android后台杀进程
iOS 有个让人抓狂的问题:如果同一个设备之前配过网,后来设备端改了广播名或者升级了蓝牙配置,iPhone 扫描时依然可能显示旧的设备名。这其实是 iOS 系统级蓝牙缓存,App 层面无法强制刷新。遇到这种情况,我一般让用户在 iOS 设置里关闭蓝牙再打开,或者重启 App。设备端则尽量避免在同一个 Mac 地址上频繁修改广播名。
Android 的奇葩问题通常是后台杀进程。扫到设备、连接成功,然后用户把 App 切到后台回了个微信,再切回来发现连接断了。这不是 App 代码的问题,是国内厂商系统激进的后台管理策略。解决办法是配网过程中用前台服务保活,或者在关键步骤提醒用户不要切换 App。有些系统还需要在电池优化白名单里加入配网 App,否则一旦黑屏,蓝牙就断了。
6. BLE通道不能只用来配网:后续管理能力的扩展
BLE 配网完成后,很多项目直接把蓝牙关掉,我觉得有点浪费。BLE 在 RDK X5 这类设备上还能承担很多"近场管理"职责,尤其是设备临时离线、没有网络的情况下,BLE 是唯一可靠的管理入口。
比如设备配网后连不上路由器,用户拿着手机站在设备旁边,如果设备还开着 BLE 服务,App 就能直接读取设备的网络状态日志、诊断为什么连不上,而不是让用户把设备寄回来。再比如 OTA 升级,如果设备 WiFi 有问题,但蓝牙还能用,可以通过 BLE 下发固件或至少触发设备进入恢复模式。甚至可以在 BLE 服务里加一个"恢复出厂"特征,用户长按按键触发配网广播后,手机通过 BLE 清零设备配置,重新开始配网。这些功能都不复杂,只需要在原来的 GATT 服务里增加几个 Characteristic,但产品体验会好非常多。
所以我的建议是:在做设备端配网 GATT 服务时,不要把代码写死成"配网专用"。把 Service UUID、Command 结构、状态上报机制设计得通用一点,后续加诊断、加日志、加升级指令,都只是新增一个 Characteristic 或者扩展消息类型的事情,不需要推翻重来。
从我个人的实际体验来说,在 RDK X5 上做 BLE 配网,最难的不是写广播、不是注册 GATT,也不是处理 MTU 分包,而是你要同时把 Linux 蓝牙协议栈、WiFi 管理、手机系统差异三个领域的问题揉在一起调试。三层里任何一层出问题,表象都是"手机连不上设备"或者"配网没反应"。我建议你按这个顺序排错:先确保蓝牙物理链路通(能扫描、能连接),再确保障 GATT 服务读写正常,最后才去做 WiFi 回连逻辑。一层一层验证,会比一次性写完再从头调试高效得多。
最后分享一个调试阶段的小技巧:把日志打印放在所有关键动作的前后,包括广播开启、连接请求、Characteristic 写入、WiFi 命令执行、状态推送。手机上用 nRF Connect 手动写入一把数据,观察设备端日志走到哪一步,能定位大部分问题。这个习惯帮我省下了大量时间,希望也能帮到你。