构建 Android Matter 应用

1. 欢迎

Matter 旨在统一物联网标准,可将各种生态系统(如 Google Home、Zigbee、蓝牙网状网络、Z-Wave 等)中的智能家居设备连接起来。

移动设备是与智能家居设备互动的主要方式。如果您想构建自己的 Android 应用来支持 Matter 设备,我们可以帮助您快速入门。

Google Home Sample App for Matter (GHSA for Matter) 展示了 Home Mobile SDK API,可让用户调试和共享设备。您还可以将该示例应用用作学习工具,以便更好地了解关键 Matter 概念,并将其用作调试和排查与 Matter 设备交互问题的工具。

您将执行的操作

在此 Codelab 中,您将下载示例应用的源代码,并学习如何使用 Home Mobile SDK 来配置和共享设备。您还将学习如何使用 Matter 代码库 (connectedhomeip) 中的调试和集群库。

下载示例应用后,我们将在 Android Studio 中查看源代码,并实现以下 Home Mobile SDK API

您还将详细了解调试概念、Matter 织网以及如何控制 Matter 设备。

所需条件

在开始之前,请务必完成以下步骤:

您无需使用 hub(例如 Google Nest Hub [第 2 代])即可通过示例应用调试和控制设备。

2. 进行设置

Codelab 起始应用位于 codelab 分支中。如需开始使用 Codelab 源代码,您可以下载 ZIP 文件。

您将使用此 codelab ZIP 文件构建一个可正常运行的示例。

Codelab 版本

codelab 分支已标记为示例应用的 2.0.0 版本。如需在完成每个步骤后比较更新,您可以下载此版本的完整源代码。

如果您想克隆 GitHub 代码库,请按照示例应用 README 中的说明操作。

依赖项

我们将引导您完成共享和委托设备所需的源代码,但建议您在开始之前了解以下依赖项。请注意,这些依赖项在 libs.versions.toml 文件中声明,其使用情况在 build.gradle.kts 文件中指定

源代码

界面和大部分功能已为您创建完毕。

在此 Codelab 中,我们将向以下文件添加 Matter 功能:

  • java/com/google/homesampleapp/commissioning/AppCommissioningService:允许您将设备委托给开发结构
  • java/com/google/homesampleapp/screens/home/HomeScreenjava/com/google/homesampleapp/screens/home/HomeViewModel.kt:包含 Home 移动 SDK 调试功能
  • java/com/google/homesampleapp/screens/device/DeviceScreenjava/com/google/homesampleapp/screens/device/DeviceViewModel:包含“共享设备”API 调用

每个文件都添加了注释,其中包含您将修改的代码块,例如:

// CODELAB: add commissioningFunction()

这样,您就可以快速找到 Codelab 中的相应部分。

3. 佣金到 Google

在您控制设备并允许它们在同一网状网络内相互通信之前,需要由调试器(在本例中为 Google Home Sample App for Matter 这个示例应用)对它们进行调试。

请务必了解以下有关 Matter 调试的概念:

  • 功能块可让设备彼此通信。
  • 功能块维护一组共享的唯一凭据。
  • 生态系统负责颁发可信的根证书、分配功能块 ID 和分配唯一的节点 ID。生态系统是调试器的后端服务,例如 Google Home 生态系统的 Home Graph。
  • 设备可以委托给多个功能区(多管理员功能)。

如需调试设备,您需要使用 CommissioningClient API。对 .commissionDevice() 的调用会返回一个 IntentSender,该 IntentSender 会在 Google Play 服务中启动相应的 activity:

interface CommissioningClient {
  Task<IntentSender> commissionDevice(CommissioningRequest request);
}

在接下来的部分中,我们将介绍将设备委托给 Google Fabric 所需的最少代码。

第 1 步:Activity 启动器

如需处理来自 CommissioningClientIntentSender,您可以使用 ActivityResultLauncher:

val commissioningLauncher = registerForActivityResult(
    StartIntentSenderForResult()
) { result: ActivityResult ->
    if (result.resultCode == RESULT_OK) {
        Timber.d(TAG, "Commissioning succeeded.")
    } else {
        Timber.d(TAG, "Commissioning failed. " + result.resultCode)
    }
}

第 2 步:调试功能

以下是一个基本示例,该示例使用 CommissioningClient API 将设备委托给 Google Fabric。

  1. 调试过程从 commissionDevice() 函数开始。首先,定义一个 CommissioningRequest。在此默认配置下,设备仅委托给本地 Android 结构。
  2. Matter 是 Home Mobile SDK 的入口点。在下一次调用中,.getCommissioningClient 通过 this (Activity) 获取 CommissioningClient。
  3. .commissionDevice() 接受 CommissioningRequest
  4. 最后,调用 .addOnSuccessListener 来处理 CommissioningResult 并启动 Google Play 服务 (GPS) Commission Device Activity。
private fun commissionDevice() {
    val request: CommissioningRequest = CommissioningRequest.builder().build()
    Matter.getCommissioningClient(this)
        .commissionDevice(request)
        .addOnSuccessListener { result ->
            commissioningLauncher.launch(IntentSenderRequest.Builder(result).build())
        }
}

通过 Android 设置,可以利用本地 Android Fabric 简化将设备委托给其他 Fabric 的流程。

接下来,您将学习如何将设备调试到开发 Fabric。

如需了解调试过程中的界面概览,请参阅 Google Home Matter 示例应用指南

4. 调试到开发结构

设备可以委托给多个功能区。为了管理可信配对,设备会存储一个包含各种 FabricInfo 成员的 FabricTable,例如:

  • Fabric 识别
  • 由光纤网分配给设备的节点 ID
  • 供应商 ID
  • Fabric ID
  • 设备运行凭据

管理网域管理器 (ADM) 定义了光纤网凭据。在上述场景中,Google Play 服务是充当可信根证书授权机构 (CA) 的生态系统。将设备委托给本地 Android 结构时,每个设备都包含相同的结构凭据集和相同的证书授权机构 (CA) 集。

定制调试服务

为了委托给本地 Android 结构,我们使用默认参数在 CommissioningClient API 中构建了 CommissioningRequest

val request: CommissioningRequest = CommissioningRequest.builder().build()

如果您想通过应用控制和管理新设备,则需要创建本地开发 Fabric 并获取操作凭据来调试设备。在此场景中,您的应用会成为一个独特的独立生态系统,可为设备分配相应的节点凭据。

您可以通过向 CommissioningRequest 传递自定义服务,告知 Home Mobile SDK 您希望将设备调试到自己的功能区:

class CommissioningRequest {
  static CommissioningRequest.Builder builder();

  class Builder {
    Builder setCommissioningService(@Nullable ComponentName commissioningService);

    CommissioningRequest build();
  }
}

在接下来的步骤中,我们将修改 commissionDevice() 函数以使用自定义服务。我们还将在主 fragment 中添加一个 Activity 启动器,并使用 LiveData 对象来管理 API 流。

第 1 步:创建 GPS Activity 启动器

首先,我们来创建一个 Activity 启动器,以处理来自 CommissioningClient API 的 IntentSender

  1. 打开 java/com/google/homesampleapp/screens/home/ 文件夹中的 HomeScreen
  2. // CODELAB: commissionDeviceLauncher definition 注释替换为以下代码,以注册和处理调试 activity 结果:
    val commissionDeviceLauncher =
      rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartIntentSenderForResult()
      ) { result ->
        // Commission Device Step 5.
        // The Commission Device activity in GPS (step 4) has completed.
        val resultCode = result.resultCode
        if (resultCode == Activity.RESULT_OK) {
          Timber.d("CommissionDevice: Success")
          // We let the ViewModel know that GPS commissioning has completed successfully.
          // The ViewModel knows that we still need to capture the device name and will\
          // update UI state to trigger the NewDeviceAlertDialog.
          homeViewModel.gpsCommissioningDeviceSucceeded(result)
        } else {
          homeViewModel.commissionDeviceFailed(resultCode)
        }
      }
    

第 2 步:触发调试设备操作

在此步骤中,用户通过点击主屏幕右下角的“+”按钮来触发“调试设备”操作。然后,系统会调用 commissionDevice()

val onCommissionDevice = {
  ...
  commissionDevice(activity!!.applicationContext, commissionDeviceLauncher)
}

第 3 步:调用 API

  1. 仍位于 java/com/google/homesampleapp/screens/home 文件夹中的 HomeScreen.kt 中。
  2. // CODELAB: commissionDevice 注释替换为以下 commissionDeviceRequestsetCommissioningServiceAppCommissioningService 绑定到回调函数中返回的 CommissioningService 实例。当您传递自定义服务时,Home Mobile SDK 会先将设备委托给 Android 本地 Fabric,然后将初始配置载荷发送回 AppCommissioningService
    val commissionDeviceRequest =
        CommissioningRequest.builder()
            .setCommissioningService(ComponentName(
                context, AppCommissioningService::class.java))
            .build()
    
  3. 调用 .getCommissioningClient(),然后调用 .commissionDevice()
Matter.getCommissioningClient(context)
    .commissionDevice(commissionDeviceRequest)

为了完成 commissionDevice 函数,请添加 addOnSuccessListeneraddOnFailureListener

    .addOnSuccessListener { result ->
      commissionDeviceLauncher.launch(IntentSenderRequest.Builder(result).build())
    }
    .addOnFailureListener { error ->
      Timber.e(error)
    }

5. 创建调试服务

commissionDevice() 函数中,我们请求从 CommissioningClient API 获取 CommissioningService。在此流程中,CommissioningClient API 会先将设备委托给本地 Android 结构,然后返回一个包含 CommissioningRequestMetadata 对象的回调:

public interface CommissioningService {
interface Callback {
    void onCommissioningRequested(CommissioningRequestMetadata metadata);
  }
}

现在,我们必须继承 CommissioningService.Callback 并提供将设备委托给示例应用所需的功能。以下是一个基本的 CommissioningService 实现示例:

class MatterCommissioningService : Service(), CommissioningService.Callback {
   private val commissioningServiceDelegate =
     CommissioningService.Builder(this)
       .setCallback(this)
       .build()

   override fun onBind(intent: Intent) = commissioningServiceDelegate.asBinder()

   override fun onCommissioningRequested(metadata: CommissioningRequestMetadata) {
     // perform commissioning

     commissioningServiceDelegate
       .sendCommissioningComplete(CommissioningCompleteMetadata.builder().build())
   }
 }

第 1 步:探索自定义 AppCommissioningService

为了帮助您入门,我们已为自定义 CommissioningService 定义了基本类结构。下面简要介绍一下该服务的功能。如需继续学习,请在 java/commissioning 中打开 AppCommissioningService

我们为 Home Mobile SDK API 添加了以下导入项:

import com.google.android.gms.home.matter.commissioning.CommissioningCompleteMetadata
import com.google.android.gms.home.matter.commissioning.CommissioningRequestMetadata
import com.google.android.gms.home.matter.commissioning.CommissioningService

AppCommissioningService 还包括 Matter 代码库 (connectedhomeip) 中的库:

import com.google.homesampleapp.chip.ChipClient

最后,该服务包含支持 HiltKotlin 协程的导入。

接下来,我们创建构造函数并设置一些内容,包括 commissioningServiceDelegate,我们将使用它来告知 Google Play 服务何时完成调试。

private lateinit var commissioningServiceDelegate: CommissioningService
...
commissioningServiceDelegate = CommissioningService.Builder(this).setCallback(this).build()

现在,该添加调试功能了。

第 2 步:替换 onCommissioningRequested

如需将设备委托给应用的开发 Fabric,请完成以下步骤:

  1. java/commissioning 中打开 AppCommissioningService
  2. 找到 onCommissioningRequested() 函数。我们提供了一条用于输出 CommissioningRequestMetadata 的日志消息。替换 // CODELAB: onCommissioningRequested() 注释以启动 serviceScope 协程并获取 deviceId
    // Perform commissioning on custom fabric for the sample app.
    serviceScope.launch {
      val deviceId = devicesRepository.incrementAndReturnLastDeviceId()
    
  3. 执行调试。在此步骤中,我们可以传递 CommissioningRequestMetadata 对象中返回的设备信息。ChipClient 使用此元数据信息在 GHSA for Matter 应用和您的设备之间创建安全信道。
    try {
      Timber.d(
          "Commissioning: App fabric -> ChipClient.establishPaseConnection(): deviceId [${deviceId}]")
      chipClient.awaitEstablishPaseConnection(
          deviceId,
          metadata.networkLocation.ipAddress.hostAddress!!,
          metadata.networkLocation.port,
          metadata.passcode)
      Timber.d(
          "Commissioning: App fabric -> ChipClient.commissionDevice(): deviceId [${deviceId}]")
      chipClient.awaitCommissionDevice(deviceId, null)
    } catch (e: Exception) {
      Timber.e(e, "onCommissioningRequested() failed")
      // No way to determine whether this was ATTESTATION_FAILED or DEVICE_UNREACHABLE.
      commissioningServiceDelegate
          .sendCommissioningError(CommissioningError.OTHER)
          .addOnSuccessListener {
            Timber.d(
                "Commissioning: commissioningServiceDelegate.sendCommissioningError() succeeded")
          }
          .addOnFailureListener { e2 ->
            Timber.e(e2, "Commissioning: commissioningServiceDelegate.sendCommissioningError() failed")
          }
      return@launch
    }
    
  4. 使用 commissioningServiceDelegate 让 Google Play 服务知道调试已完成。在 .sendCommissioningComplete() 中,传递 CommissioningCompleteMetadata
    commissioningServiceDelegate
        .sendCommissioningComplete(
            CommissioningCompleteMetadata.builder().setToken(deviceId.toString()).build())
        .addOnSuccessListener {
          Timber.d("Commissioning: commissioningServiceDelegate.sendCommissioningComplete() succeeded")
        }
        .addOnFailureListener { e ->
          Timber.e(e, "Commissioning: commissioningServiceDelegate.sendCommissioningComplete() failed")
        }
    }
    

运行应用

现在,我们已准备好所有必需的代码,可以开始调试本地功能区了。选择您的 Android 设备,然后运行该应用。在主屏幕上,点按添加设备,然后完成相应步骤来调试设备。

当调试完成时,设备现在参与到两个功能区中:本地 Android 功能区和本地开发功能区。每个 Fabric 都有自己的一组凭据和一个唯一的 64 位 Fabric ID。

6. 控制设备

调试到开发 Fabric 可让您使用 Matter 代码库 (connectedhomeip) 中的库来控制示例应用中的设备。

我们创建了一些辅助类,以便更轻松地访问设备集群和发送命令。如需了解详情,请在 java/clusters 中打开 ClustersHelper。此 Singleton 帮助程序会导入以下库来访问设备信息:

import chip.devicecontroller.ChipClusters
import chip.devicecontroller.ChipStructs

我们可以使用此类获取设备的开/关集群,然后调用 .toggle

suspend fun toggleDeviceStateOnOffCluster(deviceId: Long, endpoint: Int) {
  Timber.d("toggleDeviceStateOnOffCluster())")
  val connectedDevicePtr =
      try {
        chipClient.getConnectedDevicePointer(deviceId)
      } catch (e: IllegalStateException) {
        Timber.e("Can't get connectedDevicePointer.")
        return
      }
  return suspendCoroutine { continuation ->
    getOnOffClusterForDevice(connectedDevicePtr, endpoint)
        .toggle(
            object : ChipClusters.DefaultClusterCallback {
              override fun onSuccess() {
                continuation.resume(Unit)
              }
              override fun onError(ex: Exception) {
                Timber.e("readOnOffAttribute command failure: $ex")
                continuation.resumeWithException(ex)
              }
            })
  }
}

切换设备

委托设备后,CommissioningResult 中返回的载荷会添加到 DataStore。这样一来,我们的应用便可访问设备信息,以便我们使用这些信息来发送命令。

Matter 应用是事件驱动型应用。当 Matter 堆栈初始化时,集群服务会监听传入的消息。设备完成调试后,Matter 客户端会通过在设备调试期间建立的安全运行通道发送命令。

在设备上,系统会验证、解密数据包,然后通过回调进行调度。回调函数包含可从 attributePath 访问的 EndpointId、ClusterId 和 AttributeId。例如,以下代码可在 Matter 设备上实现:

void MatterPostAttributeChangeCallback(const chip::app::ConcreteAttributePath & attributePath, uint8_t mask, uint8_t type,
                                       uint16_t size, uint8_t * value)
{
    // handle callback
    ClusterId clusterId     = attributePath.mClusterId;
    AttributeId attributeId = attributePath.mAttributeId;
}

在接下来的步骤中,您将使用 Matter SDK 和 ClustersHelper 来切换设备。

  1. 前往 java/screens/device 中的 DeviceViewModel
  2. 找到 updateDeviceStateOn 函数。
  3. // CODELAB: toggle 注释替换为用于调用 clustersHelper 的代码,然后更新设备代码库:
    Timber.d("Handling real device")
        try {
          clustersHelper.setOnOffDeviceStateOnOffCluster(deviceUiModel.device.deviceId, isOn, 1)
          devicesStateRepository.updateDeviceState(deviceUiModel.device.deviceId, true, isOn)
        } catch (e: Throwable) {
          Timber.e("Failed setting on/off state")
        }
    

此函数是从 DeviceScreen 调用的:

// On/Off Switch click.
val onOnOffClick: (value: Boolean) -> Unit = { value ->
  deviceViewModel.updateDeviceStateOn(deviceUiModel!!, value)
}

运行应用

运行应用以重新加载更新。在主屏幕上,开启和关闭设备。

7. 与其他生态系统共享设备

在 Matter 规范中,共享设备称为多管理员流程

在之前的步骤中,我们了解到 Home Mobile SDK 可用于将设备调试到本地 Android 结构,也可调试到示例应用的开发结构。这是一个多管理员流程的示例,其中设备可调试到多个结构。

现在,您可能希望与更多功能平台共享设备,尤其是在家庭成员对应用和平台有各自偏好的情况下。

Home Mobile SDK 在 ShareDeviceRequest API 中提供了此功能,可让您执行以下操作:

  1. 为设备打开临时调试窗口。
  2. 更改设备的状态,使其能够委托给其他功能区。
  3. 通过其他应用和生态系统控制设备。

在接下来的步骤中,您将使用 Home Mobile SDK 来共享设备。

第 1 步:创建 GPS Activity 启动器

与我们在调试到开发 Fabric 时创建的调试 Activity 启动器类似,我们创建了一个“共享设备”Activity 启动器来处理来自 CommissioningClient API 的 IntentSender

  1. 打开 java/com/google/homesampleapp/screens/device/ 文件夹中的 DeviceScreen
  2. // CODELAB: shareDeviceLauncher definition 注释替换为以下代码,以注册和处理 .shareDevice() activity 结果:
    val shareDeviceLauncher = rememberLauncherForActivityResult(
      contract = ActivityResultContracts.StartIntentSenderForResult()
    ) { result ->
      // Commission Device Step 5.
      // The Share Device activity in GPS (step 4) has completed.
      val resultCode = result.resultCode
      if (resultCode == Activity.RESULT_OK) {
        deviceViewModel.shareDeviceSucceeded()
      } else {
        deviceViewModel.shareDeviceFailed(resultCode)
      }
    }
    

第 2 步:触发共享设备操作

在此步骤中,用户通过点击设备屏幕上的“分享”按钮来触发“分享设备”操作。然后,系统会调用 deviceViewModel 以打开配对窗口,以便进行设备共享。

// Share Device button click.
val onShareDevice: () -> Unit = remember {
 {
   deviceViewModel.openPairingWindow(deviceUiModel!!.device.deviceId)
 }
}

成功打开配对窗口后,deviceViewModel 会将此事实传达给界面。ViewModel 和界面之间的通信通过 StateFlow 对象完成。

// Communicate to the UI that the pairing window is open.
// UI can then launch the GPS activity for device sharing.
_pairingWindowOpenForDeviceSharing.value = true

在看到 StateFlow 对象发生变化后,DeviceScreen 会进行以下调用:

shareDevice(activity!!.applicationContext, shareDeviceLauncher, deviceViewModel)

第 3 步:调用 API

现在,您可以开始执行共享设备任务了。

  1. 打开 java/com/google/homesampleapp/screens/device/ 文件夹中的 DeviceScreen.kt
  2. 找到 shareDevice() 函数。将 // CODELAB: shareDevice 注释替换为 ShareDeviceRequestDeviceDescriptor 提供有关设备的具体信息,例如供应商 ID、产品 ID 和 deviceType。在此示例中,我们对值进行了硬编码。
    val shareDeviceRequest =
      ShareDeviceRequest.builder()
        .setDeviceDescriptor(DeviceDescriptor.builder().build())
        .setDeviceName("GHSAFM temp device name")
    
  3. 设置 CommissioningWindow参数
        .setCommissioningWindow(
            CommissioningWindow.builder()
                .setDiscriminator(Discriminator.forLongValue(DISCRIMINATOR))
                .setPasscode(SETUP_PIN_CODE)
                .setWindowOpenMillis(SystemClock.elapsedRealtime())
                .setDurationSeconds(OPEN_COMMISSIONING_WINDOW_DURATION_SECONDS.toLong())
                .build())
        .build()
    
  4. 调用 .getCommissioningClient(),但这次使用 .shareDevice() API。
    Matter.getCommissioningClient(context)
        .shareDevice(shareDeviceRequest)
    

commissioningClient.shareDevice() API 的成功回调提供 IntentSender,用于在 Google Play 服务中启动“共享设备”activity。

  1. 为了完成 shareDevice 函数,请添加 addOnSuccessListeneraddOnFailureListener。成功后,系统会在 shareDeviceLauncher 上调用 launch 以启动用于设备共享的 GPS activity。
        .addOnSuccessListener { result ->
          Timber.d("ShareDevice: Success getting the IntentSender: result [${result}]")
          shareDeviceLauncher.launch(IntentSenderRequest.Builder(result).build())
        }
        .addOnFailureListener { error ->
          Timber.e(error)
          deviceViewModel.showMsgDialog("Share device failed", error.toString())
        }
    

运行应用

如需与其他生态系统共享 Matter 设备,您需要在 Android 设备上安装其他平台。我们已创建了另一个示例应用实例,您可以将其用作目标委托方。

在 Android 设备上安装目标调试器后,验证您是否可以共享 Matter 设备。目标调试器应用标记为 GHSAFM-TC

您的设备现在可以加入以下三种功能区:

  1. 本地 Android Fabric。
  2. 您的开发结构(此应用)。
  3. 您刚刚与之分享设备的第三个功能区。

8. 后续步骤

恭喜

恭喜,您已成功完成此 Codelab,并学习了如何使用 Home Mobile SDK 调试和共享设备。

如果您在运行示例应用时遇到问题,请尝试完成以下步骤来验证您的环境:

如果您在使用示例应用时遇到问题或发现代码 bug,可以向 GitHub 代码库中的问题跟踪器提交问题:

如需就技术问题获取 Google 的官方指导,请使用智能家居开发者论坛:

如需从社区获取技术支持,请在 Stack Overflow 上使用 google-smart-home 标记: