HarmonyOS技术精讲-Connectivity Kit:自动设备发现与一键配网

HarmonyOS NEXT 开发里,distributedDeviceManager 这个 API 经常被误用。很多人第一次接触它时,会发现官方示例能运行,但放到实际项目里并不稳定------要么发现不了设备,要么状态不同步,要么页面返回后回调还在跑。

这个功能本身不复杂,但真正麻烦的是生命周期和状态同步处理。这篇文章会把设备发现、一键配网、跨设备消息发送这三个环节拆开讲,每个环节都带完整代码,并重点说明实际项目里容易翻车的地方。

它解决什么问题

设备发现和配网本质上是两个问题,但很多人把它们混为一谈。

设备发现解决的是"同一 HarmonyOS 生态里的设备如何互相感知"。传统方式需要蓝牙扫描或局域网广播,HarmonyOS 通过分布式软总线实现了更底层的设备发现机制。

一键配网解决的是"新设备如何快速接入家庭网络"。最常见的是智能家居场景:一个刚出厂的音箱,没有 Wi-Fi 配置,需要通过手机把 Wi-Fi 密码传给它。

为什么不推荐传统方案?

方案 缺点
蓝牙扫描 需要额外模块,稳定性一般
局域网 UDP 广播 跨网段失效,不稳定
第三方 SDK 厂商锁定,权限受限
HarmonyOS Connectivity Kit 系统原生支持,权限可控

适合场景:

  • 智能家居设备的自动发现与配网
  • 办公场景下的设备快速连接(投屏、打印)
  • 多设备协同开发时的测试调试

不适合场景:

  • 需要连接非 HarmonyOS 设备(如旧版 Android 设备)
  • 大规模公网设备管理(建议使用云平台方案)

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(24) 及以上
目标设备:手机 + 平板(或手机 + 音箱等支持分布式软总线的设备)

注意:模拟器上软总线能力有限,建议真机测试。

核心实现

1. 权限配置与初始化

module.json5 中添加以下权限:

json 复制代码
{
  "requestPermissions": [
    {
      "name": "ohos.permission.DISTRIBUTED_DATASYNC",
      "reason": "$string:use_distributed_datasync"
    },
    {
      "name": "ohos.permission.ACCESS_SERVICE_DISCOVERY",
      "reason": "$string:use_service_discovery"
    }
  ]
}

初始化 deviceManager:

typescript 复制代码
// common/DeviceManagerHelper.ts
import { distributedDeviceManager } from '@kit.DistributedServiceKit';

export class DeviceManagerHelper {
  private static instance: DeviceManagerHelper;
  private dmInstance: distributedDeviceManager.DeviceManager | null = null;

  public static getInstance(): DeviceManagerHelper {
    if (!DeviceManagerHelper.instance) {
      DeviceManagerHelper.instance = new DeviceManagerHelper();
    }
    return DeviceManagerHelper.instance;
  }

  // 初始化 deviceManager
  public async init(): Promise<distributedDeviceManager.DeviceManager> {
    if (this.dmInstance) {
      return this.dmInstance;
    }
    try {
      // 获取分布式设备管理器实例
      this.dmInstance = distributedDeviceManager.createDeviceManager();
      return this.dmInstance;
    } catch (err) {
      console.error(`DeviceManager init failed: ${JSON.stringify(err)}`);
      throw err;
    }
  }

  // 释放资源
  public release(): void {
    if (this.dmInstance) {
      this.dmInstance.release();
      this.dmInstance = null;
    }
  }
}

这里用单例模式是为了避免多次创建 DeviceManager 实例。实际项目里如果多个页面都需要发现设备,用单例管理生命周期会更稳定。

2. 设备发现页面

typescript 复制代码
// view/DiscoverPage.ets
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { DeviceManagerHelper } from '../common/DeviceManagerHelper';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct DiscoverPage {
  @State deviceList: distributedDeviceManager.DiscoveredDeviceInfo[] = [];
  @State isScanning: boolean = false;
  private dmHelper: DeviceManagerHelper = DeviceManagerHelper.getInstance();
  private dmInstance: distributedDeviceManager.DeviceManager | null = null;
  private subscribeId: number = -1;

  aboutToAppear(): void {
    this.initDeviceManager();
  }

  aboutToDisappear(): void {
    // 页面销毁时停止发现并释放资源
    this.stopDiscovery();
    this.dmInstance?.release();
    this.dmHelper.release();
  }

  // 初始化
  private async initDeviceManager(): Promise<void> {
    try {
      this.dmInstance = await this.dmHelper.init();
    } catch (err) {
      console.error(`initDeviceManager failed: ${JSON.stringify(err)}`);
    }
  }

  // 开始设备发现
  private async startDiscovery(): Promise<void> {
    if (!this.dmInstance) {
      console.error('DeviceManager not ready');
      return;
    }
    try {
      // 创建发现请求参数
      const filterOptions: distributedDeviceManager.DiscoverFilterOptions = {
        discoverTargetType: distributedDeviceManager.DiscoverTargetType.DISCOVER_SUBTYPE_ALL
      };
      // 注册发现回调
      const callback: distributedDeviceManager.IDiscoveryCallback = {
        onDiscoverySuccess: (deviceInfo: distributedDeviceManager.DiscoveredDeviceInfo): void => {
          // 这里需要判断是否已经在列表中,避免重复添加
          const exists = this.deviceList.some(
            (item) => item.deviceId === deviceInfo.deviceId
          );
          if (!exists) {
            this.deviceList = [...this.deviceList, deviceInfo];
          }
        },
        onDiscoveryFailed: (error: BusinessError): void => {
          console.error(`Discovery failed: ${JSON.stringify(error)}`);
        },
        onDeviceStateChanged: (deviceInfo: distributedDeviceManager.DiscoveredDeviceInfo): void => {
          // 设备状态变化(上线/下线)
          if (deviceInfo.deviceState === distributedDeviceManager.DeviceState.OFFLINE) {
            this.deviceList = this.deviceList.filter(
              (item) => item.deviceId !== deviceInfo.deviceId
            );
          }
        }
      };
      // 开始发现
      this.subscribeId = this.dmInstance.startDeviceDiscovery(filterOptions, callback);
      this.isScanning = true;
    } catch (err) {
      console.error(`startDiscovery failed: ${JSON.stringify(err)}`);
    }
  }

  // 停止发现
  private stopDiscovery(): void {
    if (this.dmInstance && this.subscribeId !== -1) {
      this.dmInstance.stopDeviceDiscovery(this.subscribeId);
      this.isScanning = false;
    }
  }

  build() {
    Column() {
      // 顶部标题
      Text('自动设备发现')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 16, bottom: 8 });

      // 扫描按钮
      Button(this.isScanning ? '停止扫描' : '开始扫描')
        .width(200)
        .height(40)
        .type(ButtonType.Capsule)
        .onClick(() => {
          if (this.isScanning) {
            this.stopDiscovery();
          } else {
            this.startDiscovery();
          }
        });

      // 设备列表
      List({ space: 12 }) {
        ForEach(this.deviceList, (device: distributedDeviceManager.DiscoveredDeviceInfo) => {
          ListItem() {
            DeviceCard({ device: device })
          }
        }, (device: distributedDeviceManager.DiscoveredDeviceInfo) => device.deviceId)
      }
      .layoutWeight(1)
      .width('100%')
      .padding({ left: 16, right: 16 })
      .onReachEnd(() => {
        // 列表到底自动停止扫描,节省资源
        this.stopDiscovery();
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

// 设备卡片组件
@Component
struct DeviceCard {
  @Prop device: distributedDeviceManager.DiscoveredDeviceInfo;

  build() {
    Row() {
      Column() {
        Text(this.device.deviceName)
          .fontSize(16)
          .fontWeight(FontWeight.Medium);
        Text(`设备ID: ${this.device.deviceId.substring(0, 8)}...`)
          .fontSize(12)
          .fontColor('#999')
          .margin({ top: 4 });
        Text(`状态: ${this.device.deviceState === 0 ? '在线' : '离线'}`)
          .fontSize(12)
          .fontColor(this.device.deviceState === 0 ? '#52c41a' : '#ff4d4f');
      }
      .alignItems(HorizontalAlign.Start);

      Blank();
      Button('配网')
        .height(32)
        .fontSize(14)
        .type(ButtonType.Capsule)
        .onClick(() => {
          // 跳转到配网页面
          router.pushUrl({
            url: 'pages/ConfigPage',
            params: {
              deviceId: this.device.deviceId,
              deviceName: this.device.deviceName
            }
          });
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.1)' })
  }
}

这段代码有几个关键点要注意:

  1. startDeviceDiscovery 的第一个参数是过滤器,如果不指定设备类型,会搜索所有支持分布式软总线的设备。实际项目中如果只关心特定类型(如音箱、插座),建议指定 discoverTargetType
  2. 回调 onDiscoverySuccess 每次发现设备都会调用,但同一个设备可能会多次发现,所以需要去重。这里用 deviceId 作为唯一标识。
  3. onDeviceStateChanged 用于处理设备上下线,如果不处理,列表中已经离线的设备会一直显示。

3. 一键配网(Wi-Fi QR 码配网)

配网流程分为三步:

  1. 手机端生成包含 Wi-Fi SSID 和密码的 QR 码
  2. 待配网设备(如音箱)扫描 QR 码
  3. 设备连接 Wi-Fi,返回配网结果

这里重点讲手机端生成 QR 码的部分,完整流程需要设备端配合。

typescript 复制代码
// view/ConfigPage.ets
import { image, qrcode } from '@kit.ArkUI';
import { wifiManager } from '@kit.ConnectivityKit';

@Entry
@Component
struct ConfigPage {
  @State deviceId: string = '';
  @State deviceName: string = '';
  @State wifiSSID: string = '';
  @State wifiPassword: string = '';
  @State qrCodeBuffer: image.PixelMap | null = null;
  @State configState: string = 'ready'; // ready | success | failed

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, Object>;
    this.deviceId = params['deviceId'] as string;
    this.deviceName = params['deviceName'] as string;
    // 获取当前 Wi-Fi 信息
    this.getCurrentWifi();
  }

  private getCurrentWifi(): void {
    try {
      const wifiInfo = wifiManager.getLinkedInfo();
      this.wifiSSID = wifiInfo.ssid;
    } catch (err) {
      console.error(`getCurrentWifi failed: ${JSON.stringify(err)}`);
    }
  }

  // 生成配网 QR 码
  private async generateQRCode(): Promise<void> {
    // QR 码内容格式:WIFI:S:<SSID>;T:WPA;P:<PASSWORD>;;
    const qrContent = `WIFI:S:${this.wifiSSID};T:WPA;P:${this.wifiPassword};;`;
    try {
      // 创建 QR 码生成器
      const qrGenerator = new qrcode.QRCodeGenerator();
      qrGenerator.encode(qrContent);
      const matrix = qrGenerator.getMatrix();
      // 转换为 PixelMap(用于在 Image 组件中显示)
      // 这里简化处理,直接使用 Text 组件显示二维码内容
      // 实际项目中建议使用 qrcode 生成图片
      this.qrCodeBuffer = null;
      // 模拟配网过程
      this.configState = 'success';
    } catch (err) {
      console.error(`generateQRCode failed: ${JSON.stringify(err)}`);
      this.configState = 'failed';
    }
  }

  build() {
    Column() {
      // 标题
      Text(`配网:${this.deviceName}`)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 16, bottom: 16 });

      // 当前 Wi-Fi 信息
      Text(`当前 Wi-Fi: ${this.wifiSSID}`)
        .fontSize(14)
        .fontColor('#666')
        .margin({ bottom: 8 });

      // 密码输入
      TextInput({ placeholder: '请输入 Wi-Fi 密码' })
        .width('80%')
        .height(40)
        .type(InputType.Password)
        .onChange((value: string) => {
          this.wifiPassword = value;
        });

      // 生成 QR 码按钮
      Button('生成配网二维码')
        .width('80%')
        .height(40)
        .type(ButtonType.Capsule)
        .margin({ top: 16 })
        .enabled(this.wifiPassword.length > 0)
        .onClick(() => {
          this.generateQRCode();
        });

      // 配网状态显示
      if (this.configState === 'success') {
        Text('配网成功!设备已连接 Wi-Fi')
          .fontSize(14)
          .fontColor('#52c41a')
          .margin({ top: 16 });
      } else if (this.configState === 'failed') {
        Text('配网失败,请重试')
          .fontSize(14)
          .fontColor('#ff4d4f')
          .margin({ top: 16 });
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
  }
}

这里的 QR 码生成使用了 qrcode 模块,但官方文档中 qrcode 模块的输出是矩阵数据,需要转换成图片。实际项目中建议直接使用第三方库生成 QR 码图片,或者使用 @kit.ArkUIQRCode 组件(需要 HarmonyOS 5.0 以上版本)。

配网流程中,手机端生成 QR 码后,待配网设备(如音箱)需要具备摄像头扫描功能。这个流程在实际项目中需要设备厂商配合实现。

4. 跨设备消息发送

typescript 复制代码
// view/MessagePage.ets
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { remoteDeviceManager } from '@kit.ConnectivityKit';
import { DeviceManagerHelper } from '../common/DeviceManagerHelper';

@Entry
@Component
struct MessagePage {
  @State remoteDevice: distributedDeviceManager.DiscoveredDeviceInfo | null = null;
  @State message: string = '';
  @State sendResult: string = '';

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, Object>;
    if (params['device']) {
      this.remoteDevice = params['device'] as distributedDeviceManager.DiscoveredDeviceInfo;
    }
  }

  // 发送消息
  private async sendMessage(): Promise<void> {
    if (!this.remoteDevice || !this.message) {
      return;
    }
    try {
      // 使用 remoteDeviceManager 发送数据
      const session = remoteDeviceManager.createDataSession({
        targetDeviceId: this.remoteDevice.deviceId,
        // 这里需要指定服务 ID,与对端保持一致
        serviceId: 'com.example.connectivitydemo'
      });
      await session.send({
        data: this.message
      });
      this.sendResult = '发送成功';
    } catch (err) {
      this.sendResult = `发送失败: ${JSON.stringify(err)}`;
    }
  }

  build() {
    Column() {
      Text(`与 ${this.remoteDevice?.deviceName} 通信`)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 16, bottom: 16 });

      TextInput({ placeholder: '输入消息' })
        .width('80%')
        .height(40)
        .onChange((value: string) => {
          this.message = value;
        });

      Button('发送')
        .width('80%')
        .height(40)
        .type(ButtonType.Capsule)
        .margin({ top: 16 })
        .enabled(this.message.length > 0)
        .onClick(() => {
          this.sendMessage();
        });

      Text(this.sendResult)
        .fontSize(14)
        .fontColor(this.sendResult.includes('成功') ? '#52c41a' : '#ff4d4f')
        .margin({ top: 16 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center)
  }
}

remoteDeviceManager.createDataSession 需要两端都创建会话,并且 serviceId 必须一致。如果对端设备没有开启对应的服务,消息发送会失败。实际项目中建议先验证对端是否支持通信。

常见问题与踩坑记录

问题 1:startDeviceDiscovery 回调不触发

现象:

调用 startDeviceDiscovery 后,onDiscoverySuccess 一直没有回调,设备列表始终为空。

原因:

  1. 权限未配置或未动态申请。DISTRIBUTED_DATASYNCACCESS_SERVICE_DISCOVERY 都需要在 module.json5 中声明,并且部分版本需要动态权限申请。
  2. 目标设备未开启分布式软总线服务。部分设备(如低版本平板)默认关闭了软总线,需要在设置中手动开启。
  3. 设备不在同一华为账号下。分布式软总线的设备发现依赖于华为账号,跨账号的设备可能无法发现。

解决方案:

  1. 检查权限配置,确保两个权限都已添加,并在 aboutToAppear 中动态申请。
  2. 目标设备需要登录同一华为账号,并确保"多设备协同"功能已开启。
  3. 如果使用模拟器,确认模拟器支持分布式能力(大部分模拟器不支持)。

问题 2:页面返回后发现回调还在执行

现象:

从设备发现页面返回后,onDiscoverySuccess 仍在被调用,甚至可能引发 undefined 错误。

原因:

startDeviceDiscovery 启动后,回调是异步的。页面销毁时没有及时调用 stopDeviceDiscoveryrelease,导致回调继续执行。

解决方案:

aboutToDisappear 中必须停止发现并释放资源。

typescript 复制代码
aboutToDisappear(): void {
  this.stopDiscovery();
  this.dmInstance?.release();
  this.dmHelper.release();
}

这是一个非常容易忽略的问题。如果你的应用中涉及页面跳转和返回,一定要在页面生命周期钩子里处理回调的停止,否则可能出现"页面已销毁但回调仍在更新"的问题。

问题 3:设备状态显示异常

现象:

onDeviceStateChanged 回调中 deviceState 一直返回 0(在线),即使设备已经物理断开。

原因:

onDeviceStateChanged 的触发条件比较严格,只有设备通过分布式软总线主动下线时才会触发。如果设备直接断网或关机,软总线可能来不及触发状态变更。

解决方案:

  1. 不要完全依赖 onDeviceStateChanged 来判断设备在线状态。
  2. 结合心跳机制:定期发送 ping 消息,如果超时未回复则认为设备离线。
  3. 在 UI 层可以增加"刷新"按钮,重新触发设备发现。

最佳实践

  1. 不要在 build() 中频繁操作 deviceList

    build() 是渲染函数,每次修改 @State 变量都会触发重新渲染。如果把发现回调里的添加/删除操作放在 build() 中,会导致无限循环。正确的做法是在回调中修改 @State 变量,让框架自动处理渲染。

  2. @Observed 装饰器管理复杂状态

    如果设备列表需要嵌套对象(如每个设备包含子属性),推荐使用 @Observed 装饰器。直接修改嵌套对象的属性时,@State 可能无法触发渲染。

  3. 异步回调中不要直接修改 UI 状态

    发现回调是在子线程中执行的,直接修改 @State 变量是安全的(框架内部会处理线程同步),但如果回调中调用了 UI 组件的引用(如 this.xxx),可能会在页面销毁后出现空指针。

  4. 不同设备上的行为差异较大,必须真机测试

    分布式软总线的实现在不同设备(手机、平板、音箱)上差异明显。比如音箱的发现延时可能比手机长,部分平板可能不支持某些特性。建议在目标设备列表里的每一个型号都测试一遍。

FAQ

Q:为什么真机可以发现设备,模拟器上不行?

A:模拟器对分布式软总线的支持有限。目前大部分模拟器无法模拟软总线设备发现的功能,建议真机测试。

Q:startDeviceDiscovery 只能发现同一 Wi-Fi 下的设备吗?

A:不是。分布式软总线支持跨网络发现,只要两台设备登录同一华为账号,即使在不同网络下也有可能被发现

相关推荐
不羁的木木6 小时前
HarmonyOS技术精讲-Connectivity Kit:实战——多屏协同与文件快传应用
华为·wpf·harmonyos
ldsweet7 小时前
《HarmonyOS技术精讲-Basic Services Kit》划词服务:构建系统级文本选取能力
华为·harmonyos
tyqtyq229 小时前
HarmonyOS AI 应用开发实战:简历项目经历改写系统
人工智能·学习·华为·生活·harmonyos
2301_7681034910 小时前
HarmonyOS趣味相机实战第16篇:实时识别采样、Busy锁与Generation并发治理
harmonyos·arkts·并发控制·camerakit·corevisionkit
xd18557855510 小时前
表情包配文-基于鸿蒙的AI表情包配文生成应用开发实践
人工智能·华为·harmonyos·鸿蒙
AD022711 小时前
39-设置开关退出就复原-用偏好写队列和回滚状态兜住
harmonyos·arkts·鸿蒙开发
不羁的木木11 小时前
HarmonyOS技术精讲-Connectivity Kit:蓝牙基础——经典蓝牙与BLE入门
华为·harmonyos
雪芽蓝域zzs12 小时前
HarmonyOS开发 指纹验证和ESDSA加密
华为·harmonyos
念雨思12 小时前
HarmonyOS AI 应用开发实战:短视频选题灵感 —— AI 驱动的内容创作引擎
人工智能·学习·华为·harmonyos·鸿蒙