HarmonyOS开发:设备发现——设备扫描与配网
HarmonyOS开发:设备发现——设备扫描与配网
📌 核心要点:设备发现是智能家居的第一步,WiFi扫描发现局域网设备,BLE扫描发现蓝牙设备,SoftAP和蓝牙配网让设备连上WiFi,配网成功率直接决定用户体验。
背景与动机
用户买了个智能灯泡,拆开包装,装上灯座,打开你的App——然后呢?
灯泡没连WiFi,App找不到灯泡,灯泡也连不上服务器。用户面对一个"未发现设备"的空页面,一脸懵。
这就是设备发现和配网要解决的问题。设备发现让App找到设备,配网让设备连上WiFi,绑定让设备和用户关联。 这三步走不通,后面什么控制、场景、定时全是白搭。
配网这事儿说起来简单:把WiFi密码告诉设备,设备连上WiFi,完事。但实际操作起来,坑多到你怀疑人生。2.4G和5G的兼容问题、路由器信道问题、设备热点连不上、蓝牙配网超时——每一个都是用户投诉的重灾区。
核心原理
设备发现流程
设备发现有两种主要方式:WiFi局域网扫描和BLE蓝牙扫描。
flowchart TD
A[用户点击添加设备] --> B{选择发现方式}
B -->|WiFi扫描| C[启动mDNS/SSDP扫描]
B -->|BLE扫描| D[启动BLE广播扫描]
C --> E[发现局域网设备]
D --> F[发现蓝牙设备]
E --> G{设备是否已配网?}
F --> G
G -->|已配网| H[直接绑定设备]
G -->|未配网| I{选择配网方式}
I -->|SoftAP配网| J[连接设备热点]
I -->|蓝牙配网| K[通过BLE传输WiFi信息]
J --> L[发送WiFi密码给设备]
K --> L
L --> M[设备连接WiFi]
M --> N{连接成功?}
N -->|是| O[设备上线,完成绑定]
N -->|否| P[配网失败,重试]
P --> I
H --> O
classDef scan fill:#1565C0,color:#fff,stroke:#0D47A1
classDef discover fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef config fill:#E65100,color:#fff,stroke:#BF360C
classDef result fill:#6A1B9A,color:#fff,stroke:#4A148C
classDef error fill:#C62828,color:#fff,stroke:#B71C1C
class A,B,C,D,scan
class E,F,G,H,discover
class I,J,K,L,M,config
class O,result
class P,error
WiFi扫描:mDNS和SSDP
mDNS(Multicast DNS) 是局域网设备发现的主流方案。设备在局域网内广播自己的服务信息(如_smartlight._tcp.local),App监听广播,发现设备。
SSDP(Simple Service Discovery Protocol) 是UPnP协议族的发现机制,原理类似,但报文格式不同。
大多数IoT设备使用mDNS,因为它更轻量,适合资源受限的嵌入式设备。
BLE扫描
BLE(低功耗蓝牙)扫描适合未配网的设备。设备上电后广播自己的存在(包含设备类型、MAC地址等信息),App扫描到广播包后,建立BLE连接进行配网。
BLE扫描的优势:不需要设备和手机在同一WiFi网络,甚至设备还没连WiFi就能发现。劣势:距离有限(通常10米以内),且Android/iOS的BLE API限制较多。
SoftAP配网
SoftAP配网的流程:设备开启WiFi热点 → 手机连接设备热点 → 手机通过HTTP把WiFi信息发给设备 → 设备关闭热点,连接WiFi。
这是最传统、兼容性最好的配网方式。缺点是用户体验差——需要手动切换WiFi连接,Android 10+还限制了后台WiFi切换。
蓝牙配网
蓝牙配网的流程:手机扫描到BLE设备 → 建立BLE连接 → 通过BLE通道把WiFi信息(SSID+密码)发给设备 → 设备连接WiFi。
体验更好,不需要切换WiFi,但要求设备支持BLE,且BLE传输速度有限(大密码或复杂加密可能超时)。
代码实战
基础用法:BLE设备扫描
用鸿蒙的@ohos.bluetooth.bleAPI扫描周围的BLE设备。
// services/DeviceDiscovery.ets
// 设备发现服务 - BLE扫描与WiFi扫描
import { ble } from '@kit.ConnectivityKit'
import { wifiManager } from '@kit.WifiKit'
// 发现的设备信息
interface DiscoveredDevice {
deviceId: string // 设备ID(MAC地址)
deviceName: string // 设备名称
deviceType: string // 设备类型(从广播数据解析)
rssi: number // 信号强度
isConfigured: boolean // 是否已配网
discoverWay: 'ble' | 'wifi' // 发现方式
}
class DeviceDiscoveryService {
private bleScanner?: ble.BLEScanner
private foundDevices: Map<string, DiscoveredDevice> = new Map()
private scanTimeout: number = 30000 // 扫描超时30秒
// 启动BLE扫描
startBleScan(onDeviceFound: (device: DiscoveredDevice) => void): void {
try {
this.bleScanner = ble.createBLEScanner()
// 配置扫描参数
const scanConfig: ble.ScanOptions = {
interval: 0, // 扫描间隔,0表示连续扫描
dutyMode: ble.ScanDuty.SCAN_MODE_LOW_LATENCY, // 低延迟模式
matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE // 宽松匹配,多发现设备
}
// 设置扫描过滤器 - 只扫描IoT设备的广播
const filters: Array<ble.ScanFilter> = [
{
// 过滤厂商特定数据,0xXXXX是你们公司的厂商ID
manufactureId: 0xXXXX,
manufactureData: undefined,
manufactureDataMask: undefined
}
]
// 监听扫描结果
this.bleScanner.on('bleDeviceFind', (results: Array<ble.ScanResult>) => {
results.forEach((result: ble.ScanResult) => {
const device = this.parseScanResult(result)
if (device && !this.foundDevices.has(device.deviceId)) {
this.foundDevices.set(device.deviceId, device)
onDeviceFound(device)
}
})
})
// 启动扫描
this.bleScanner.startScan(filters, scanConfig)
// 设置超时自动停止
setTimeout(() => {
this.stopBleScan()
}, this.scanTimeout)
} catch (err) {
console.error(`BLE扫描启动失败: ${JSON.stringify(err)}`)
}
}
// 解析BLE广播数据
private parseScanResult(result: ble.ScanResult): DiscoveredDevice | null {
try {
const deviceId = result.deviceId
const rssi = result.rssi
// 从广播数据中解析设备信息
// 实际项目中根据你们设备的广播协议解析
let deviceName = '未知设备'
let deviceType = 'unknown'
let isConfigured = false
// 遍历广播数据中的ServiceData
if (result.serviceData && result.serviceData.length > 0) {
const serviceData = result.serviceData[0]
// 解析自定义广播协议
const data = new Uint8Array(serviceData.serviceData.buffer)
if (data.length >= 4) {
deviceType = this.parseDeviceType(data[0])
isConfigured = (data[1] & 0x01) === 1
}
}
// 从localName获取设备名称
if (result.deviceName) {
deviceName = result.deviceName
}
return {
deviceId,
deviceName,
deviceType,
rssi,
isConfigured,
discoverWay: 'ble'
}
} catch (err) {
return null
}
}
// 解析设备类型码
private parseDeviceType(typeCode: number): string {
const typeMap: Record<number, string> = {
0x01: 'light',
0x02: 'switch',
0x03: 'air-conditioner',
0x04: 'curtain',
0x05: 'sensor'
}
return typeMap[typeCode] || 'unknown'
}
// 停止BLE扫描
stopBleScan(): void {
if (this.bleScanner) {
this.bleScanner.off('bleDeviceFind')
this.bleScanner.stopScan()
this.bleScanner = undefined
}
}
}
export default new DeviceDiscoveryService()
进阶用法:SoftAP配网
SoftAP配网是最传统的配网方式,兼容性最好。流程:连接设备热点 → 发送WiFi信息 → 等待设备连接WiFi。
// services/SoftApConfig.ets
// SoftAP配网服务
import { wifiManager } from '@kit.WifiKit'
import { http } from '@kit.NetworkKit'
// 配网结果
interface ConfigResult {
success: boolean
message: string
deviceIp?: string
}
class SoftApConfigService {
// 设备热点名称前缀,用于识别设备热点
private readonly AP_PREFIX = 'SmartDevice_'
// 执行SoftAP配网
async configDevice(ssid: string, password: string): Promise<ConfigResult> {
try {
// 第一步:连接设备热点
const connected = await this.connectToDeviceAp()
if (!connected) {
return { success: false, message: '无法连接设备热点' }
}
// 第二步:通过HTTP发送WiFi信息给设备
const sent = await this.sendWifiInfoToDevice(ssid, password)
if (!sent) {
return { success: false, message: '发送WiFi信息失败' }
}
// 第三步:等待设备连接WiFi(设备会关闭热点)
const deviceOnline = await this.waitForDeviceOnline(ssid)
if (!deviceOnline) {
return { success: false, message: '设备连接WiFi超时' }
}
return { success: true, message: '配网成功', deviceIp: '192.168.1.XXX' }
} catch (err) {
return { success: false, message: `配网异常: ${JSON.stringify(err)}` }
}
}
// 连接设备热点
private async connectToDeviceAp(): Promise<boolean> {
return new Promise((resolve) => {
try {
// 获取扫描到的WiFi列表,找到设备热点
const scanInfos = wifiManager.getScanInfoListSync()
const deviceAp = scanInfos.find(info =>
info.ssid.startsWith(this.AP_PREFIX)
)
if (!deviceAp) {
resolve(false)
return
}
// 连接设备热点(不需要密码或使用固定密码)
const config: wifiManager.WifiDeviceConfig = {
ssid: deviceAp.ssid,
preSharedKey: '12345678', // 设备热点默认密码
securityType: wifiManager.WifiSecurityType.WIFI_SEC_TYPE_PSK
}
wifiManager.connectToDevice(config)
resolve(true)
} catch (err) {
console.error(`连接设备热点失败: ${JSON.stringify(err)}`)
resolve(false)
}
})
}
// 通过HTTP发送WiFi信息给设备
private async sendWifiInfoToDevice(ssid: string, password: string): Promise<boolean> {
return new Promise((resolve) => {
try {
const httpRequest = http.createHttp()
// 设备热点的固定IP,通常是192.168.4.1或192.168.5.1
const url = 'http://192.168.4.1/config'
httpRequest.request(url, {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: {
ssid: ssid,
password: password,
// 可以附加其他信息
token: 'xxxx' // 设备绑定token
}
}, (err, data) => {
if (!err && data.responseCode === 200) {
resolve(true)
} else {
resolve(false)
}
httpRequest.destroy()
})
} catch (err) {
resolve(false)
}
})
}
// 等待设备连接到家庭WiFi后上线
private async waitForDeviceOnline(ssid: string, timeout: number = 30000): Promise<boolean> {
const startTime = Date.now()
return new Promise((resolve) => {
// 先切回家庭WiFi
this.connectToHomeWifi(ssid)
// 轮询检查设备是否上线
const timer = setInterval(() => {
if (Date.now() - startTime > timeout) {
clearInterval(timer)
resolve(false)
return
}
// 实际项目中通过mDNS或云端API检查设备是否上线
// 这里简化为检查WiFi是否连接成功
const linkedInfo = wifiManager.getLinkedInfoSync()
if (linkedInfo.ssid === ssid) {
// WiFi已连接,等待设备上线
// 实际项目中这里应该做设备上线检测
clearInterval(timer)
resolve(true)
}
}, 2000)
})
}
// 连接家庭WiFi
private connectToHomeWifi(ssid: string): void {
try {
const configs = wifiManager.getDeviceConfigsSync()
const homeConfig = configs.find(c => c.ssid === ssid)
if (homeConfig) {
wifiManager.connectToDevice(homeConfig)
}
} catch (err) {
console.error(`连接家庭WiFi失败: ${JSON.stringify(err)}`)
}
}
}
export default new SoftApConfigService()
完整示例:设备发现与配网页面
把BLE扫描、SoftAP配网、蓝牙配网整合到一个完整的设备添加流程中。
// pages/DeviceAddPage.ets
// 设备添加页面 - 扫描发现 + 配网绑定
import DeviceDiscoveryService, { DiscoveredDevice } from '../services/DeviceDiscovery'
import SoftApConfigService from '../services/SoftApConfig'
import { ble } from '@kit.ConnectivityKit'
// 配网步骤
type ConfigStep = 'scanning' | 'found' | 'configuring' | 'success' | 'failed'
@Entry
@Component
struct DeviceAddPage {
@State step: ConfigStep = 'scanning'
@State foundDevices: DiscoveredDevice[] = []
@State selectedDevice: DiscoveredDevice | null = null
@State wifiSsid: string = ''
@State wifiPassword: string = ''
@State configProgress: number = 0
@State errorMessage: string = ''
@State scanSeconds: number = 30
private discoveryService = DeviceDiscoveryService
private softApService = SoftApConfigService
private scanTimer?: number
aboutToAppear() {
this.startScan()
this.getCurrentWifi()
}
aboutToDisappear() {
this.discoveryService.stopBleScan()
if (this.scanTimer) {
clearInterval(this.scanTimer)
}
}
// 获取当前连接的WiFi信息
getCurrentWifi() {
try {
const linkedInfo = wifiManager.getLinkedInfoSync()
this.wifiSsid = linkedInfo.ssid
} catch (err) {
console.error('获取WiFi信息失败')
}
}
// 启动扫描
startScan() {
this.step = 'scanning'
this.foundDevices = []
this.scanSeconds = 30
this.discoveryService.startBleScan((device: DiscoveredDevice) => {
this.foundDevices = [...this.foundDevices, device]
// 发现设备后切换到found步骤
if (this.step === 'scanning') {
this.step = 'found'
}
})
// 倒计时
this.scanTimer = setInterval(() => {
this.scanSeconds--
if (this.scanSeconds <= 0) {
clearInterval(this.scanTimer)
if (this.foundDevices.length === 0) {
this.errorMessage = '未发现设备,请确认设备已通电且在附近'
this.step = 'failed'
}
}
}, 1000)
}
// 选择设备并开始配网
selectDevice(device: DiscoveredDevice) {
this.selectedDevice = device
if (device.isConfigured) {
// 已配网设备直接绑定
this.bindDevice(device)
} else {
// 未配网设备需要配网
this.step = 'configuring'
this.doConfig(device)
}
}
// 执行配网
async doConfig(device: DiscoveredDevice) {
this.configProgress = 10
try {
// 根据设备能力选择配网方式
// 优先蓝牙配网,体验更好
if (device.discoverWay === 'ble') {
this.configProgress = 30
const result = await this.bleConfigDevice(device)
this.configProgress = 80
if (result) {
this.configProgress = 100
this.step = 'success'
} else {
// 蓝牙配网失败,回退到SoftAP
this.configProgress = 30
const softApResult = await this.softApService.configDevice(
this.wifiSsid, this.wifiPassword
)
this.configProgress = 80
if (softApResult.success) {
this.configProgress = 100
this.step = 'success'
} else {
this.errorMessage = softApResult.message
this.step = 'failed'
}
}
} else {
// WiFi发现的设备用SoftAP配网
const result = await this.softApService.configDevice(
this.wifiSsid, this.wifiPassword
)
if (result.success) {
this.configProgress = 100
this.step = 'success'
} else {
this.errorMessage = result.message
this.step = 'failed'
}
}
} catch (err) {
this.errorMessage = `配网异常: ${JSON.stringify(err)}`
this.step = 'failed'
}
}
// BLE配网
async bleConfigDevice(device: DiscoveredDevice): Promise<boolean> {
try {
// 建立BLE连接
const gattDevice: ble.GattDevice = {
deviceId: device.deviceId
}
const gattClient = ble.createBLEGattClientDevice(gattDevice)
await gattClient.connect()
// 写入WiFi信息到特征值
const wifiInfo = JSON.stringify({
ssid: this.wifiSsid,
password: this.wifiPassword
})
const characteristic: ble.BLECharacteristic = {
serviceUuid: '0000XXXX-0000-1000-8000-00805F9B34FB', // 自定义Service UUID
characteristicUuid: '0000XXXX-0000-1000-8000-00805F9B34FB', // 自定义特征UUID
characteristicValue: stringToArrayBuffer(wifiInfo),
descriptors: []
}
await gattClient.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE)
// 等待设备连接WiFi
await new Promise(resolve => setTimeout(resolve, 5000))
gattClient.disconnect()
return true
} catch (err) {
console.error(`BLE配网失败: ${JSON.stringify(err)}`)
return false
}
}
// 绑定设备(已配网设备)
async bindDevice(device: DiscoveredDevice) {
this.step = 'configuring'
this.configProgress = 50
try {
// 调用后端API绑定设备到用户账号
// 实际项目中是HTTP请求
await new Promise(resolve => setTimeout(resolve, 1500))
this.configProgress = 100
this.step = 'success'
} catch (err) {
this.errorMessage = '设备绑定失败'
this.step = 'failed'
}
}
build() {
Navigation() {
Column() {
// 根据步骤渲染不同UI
if (this.step === 'scanning') {
this.ScanView()
} else if (this.step === 'found') {
this.FoundView()
} else if (this.step === 'configuring') {
this.ConfiguringView()
} else if (this.step === 'success') {
this.SuccessView()
} else {
this.FailedView()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F8F8F8')
}
.title('添加设备')
.titleMode(NavigationTitleMode.Mini)
}
// 扫描中视图
@Builder ScanView() {
Column() {
// 扫描动画
LoadingProgress()
.width(80)
.height(80)
.color('#007DFF')
Text('正在扫描附近设备...')
.fontSize(16)
.fontColor('#333333')
.margin({ top: 24 })
Text(`${this.scanSeconds}秒`)
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
// 提示信息
Column() {
Text('请确保:').fontSize(14).fontColor('#666666').fontWeight(FontWeight.Bold)
Text('1. 设备已通电').fontSize(13).fontColor('#999999').margin({ top: 4 })
Text('2. 设备在手机10米范围内').fontSize(13).fontColor('#999999').margin({ top: 2 })
Text('3. 手机蓝牙已开启').fontSize(13).fontColor('#999999').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ top: 40 })
.padding({ left: 40 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
// 发现设备视图
@Builder FoundView() {
Column() {
Text('发现以下设备')
.fontSize(16)
.fontColor('#333333')
.margin({ top: 16, bottom: 12 })
List({ space: 12 }) {
ForEach(this.foundDevices, (device: DiscoveredDevice) => {
ListItem() {
Row() {
Column() {
Text(device.deviceName).fontSize(15).fontColor('#333333')
Text(device.isConfigured ? '已配网' : '待配网')
.fontSize(12)
.fontColor(device.isConfigured ? '#4CAF50' : '#FF9800')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
// 信号强度指示
Text(`${device.rssi}dBm`)
.fontSize(12)
.fontColor('#999999')
Image($r('sys.media.ohos_ic_public_arrow_right'))
.width(16)
.height(16)
.fillColor('#CCCCCC')
.margin({ left: 8 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.onClick(() => this.selectDevice(device))
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
// 继续扫描按钮
Button('继续扫描')
.width('90%')
.height(44)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.borderRadius(22)
.margin({ bottom: 24 })
.onClick(() => this.startScan())
}
.width('100%')
.height('100%')
}
// 配网中视图
@Builder ConfiguringView() {
Column() {
Progress({ value: this.configProgress, total: 100, type: ProgressType.Ring })
.width(80)
.height(80)
.color('#007DFF')
Text('正在配网...')
.fontSize(16)
.fontColor('#333333')
.margin({ top: 24 })
Text(`${this.configProgress}%`)
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
Text('请勿关闭此页面或切换到其他应用')
.fontSize(13)
.fontColor('#FF9800')
.margin({ top: 24 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
// 成功视图
@Builder SuccessView() {
Column() {
Image($r('sys.media.ohos_ic_public_ok'))
.width(64)
.height(64)
.fillColor('#4CAF50')
Text('配网成功!')
.fontSize(18)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
.margin({ top: 24 })
Text(this.selectedDevice?.deviceName || '')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 })
Button('完成')
.width('90%')
.height(44)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.borderRadius(22)
.margin({ top: 40 })
.onClick(() => {
// 返回主页面
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
// 失败视图
@Builder FailedView() {
Column() {
Image($r('sys.media.ohos_ic_public_fail'))
.width(64)
.height(64)
.fillColor('#F44336')
Text('配网失败')
.fontSize(18)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
.margin({ top: 24 })
Text(this.errorMessage)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 })
.textAlign(TextAlign.Center)
.padding({ left: 40, right: 40 })
Row() {
Button('重试')
.width('40%')
.height(44)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.borderRadius(22)
.onClick(() => this.startScan())
Button('返回')
.width('40%')
.height(44)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.borderRadius(22)
.onClick(() => {})
}
.width('90%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 40 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
// 字符串转ArrayBuffer
function stringToArrayBuffer(str: string): ArrayBuffer {
const encoder = new util.TextEncoder()
return encoder.encodeInto(str)
}
import { util } from '@kit.ArkTS'
import { wifiManager } from '@kit.WifiKit'
踩坑与注意事项
1. 5G WiFi的坑
大量IoT设备只支持2.4G WiFi,不支持5G。但用户家里的路由器通常是2.4G/5G双频合一,手机连着5G,设备只认2.4G。SoftAP配网时,手机把当前WiFi的SSID发给设备,但这个SSID在设备看来可能搜不到(因为设备只扫描2.4G频段)。
解法: 配网前检测当前WiFi频段,如果是5G,提示用户切换到2.4G。或者在路由器设置里把2.4G和5G分成不同SSID。更优雅的方案是在配网页面让用户手动选择WiFi并输入密码,而不是自动读取当前WiFi。
2. BLE扫描的权限问题
BLE扫描需要位置权限(ohos.permission.LOCATION)和蓝牙权限(ohos.permission.ACCESS_BLUETOOTH)。HarmonyOS对权限管控很严格,没有权限直接调用API会抛异常。
更坑的是,即使有权限,Android 12+和HarmonyOS 4+对后台BLE扫描也有限制——App在后台时扫描频率会被大幅降低,甚至完全停止。
解法: 配网页面保持前台运行,不要让App进后台。在页面上加"请勿离开此页面"的提示。
3. SoftAP配网的WiFi切换问题
SoftAP配网需要手机先连设备热点,再切回家庭WiFi。这个过程在Android 10+被限制——App不能自动切换WiFi连接,必须用户手动在系统设置里切换。
解法: 引导用户手动切换WiFi。在配网页面显示步骤指引:“请打开系统WiFi设置 → 连接’SmartDevice_XXXX’热点 → 返回此页面继续配网”。虽然体验差,但兼容性最好。
4. 配网超时与重试
配网不是100%成功的。设备热点连不上、WiFi密码传输失败、设备连WiFi超时——每个环节都可能失败。配网超时设多少?30秒?60秒?设太短用户以为失败了,设太长用户等得不耐烦。
解法: 分阶段超时。连接设备热点5秒超时,发送WiFi信息3秒超时,等待设备上线20秒超时。哪个阶段失败就重试哪个阶段,不要从头来。最多重试3次,3次都失败就提示用户检查设备和WiFi。
5. 设备广播数据解析
不同厂商的BLE广播数据格式不一样,甚至同一厂商的不同产品线也不一样。你不能假设广播数据的格式是固定的。
解法: 定义一套自己的广播协议,让所有接入你平台的设备都按这个协议广播。协议要预留扩展字段,方便后续加新设备类型。如果接入第三方设备,写适配层做协议转换。
HarmonyOS 6适配说明
HarmonyOS 6对设备发现和配网做了几项改进:
-
BLE扫描API增强:新增
ble.ScanFilter的serviceSolicitationUuid字段,支持按Service Solicitation UUID过滤,发现特定服务的设备更精准。 -
WiFi配网简化:新增
wifiManager.startEapolConfig()API,支持EAPOL协议配网,部分设备可以不用SoftAP,直接通过WiFi帧传输配网信息,用户体验更好。 -
NFC碰一碰配网:HarmonyOS 6增强了NFC能力,支持NFC标签触发设备发现和配网流程。用户手机碰一下设备上的NFC标签,自动弹出配网页面,省去了手动扫描的步骤。
-
配网状态通知:新增
wifiManager.on('deviceConfigChange')事件,监听配网状态变化,不需要轮询。
适配代码示例:
// HarmonyOS 6 NFC碰一碰配网
import { tag } from '@kit.ConnectivityKit'
// 在Ability的onNewWant中处理NFC标签
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want.parameters?.['tagTech'] === 'NfcA') {
// 从NFC标签中读取设备信息
const nfcData = want.parameters?.['nfcData'] as string
const deviceInfo = JSON.parse(nfcData)
// 自动跳转到配网页面
// deviceInfo包含设备类型、MAC地址等信息
// 可以直接发起BLE连接进行蓝牙配网
}
}
总结
设备发现和配网是智能家居的"第一公里"。这步走不通,后面全白搭。
BLE扫描发现设备,SoftAP和蓝牙配网让设备连WiFi,绑定让设备和用户关联。流程不复杂,但坑多——5G兼容、权限、WiFi切换、超时重试,每个都是用户投诉的高发区。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ BLE和WiFi的API不难,但配网流程的各种边界情况处理很复杂 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每个新设备都需要配网,是智能家居App的必经之路 |
| 重要程度 | ⭐⭐⭐⭐⭐ 配网成功率直接影响用户留存,配网体验差=用户流失 |
记住:配网体验决定了用户对App的第一印象。 第一印象差了,后面做得再好用户也不给你机会。
- 点赞
- 收藏
- 关注作者
评论(0)