Салют, Хабр! 

Я Владислав — iOS-разработчик. Ранее мы рассказывали, как реализовали Matter over Wi-Fi в умном доме Сбер, изменили флоу подключения для более удобного пользовательского пути и интегрировали его в приложение Салют (Android). Параллельно мы ради эксперимента протестировали, насколько сложно интегрировать Matter в iOS. Это был интересный опыт. Спойлер: мы сделали процесс подключения с отображением статусов в реальном времени, который работал идеально... но только у разработчиков. А потом пришлось всё переписать.

Matter-устройство: что внутри?

В предыдущей статье осветили базовую информацию о Matter, поэтому здесь не буду повторяться — подсвечу только важное для нас. Для безопасного подключения каждое Matter-устройство имеет два важных параметра. Обычно оба внесены в QR-код, который наклеен на устройстве.

Discriminator — это 12-битный идентификатор (0-4095), который помогает различать устройства во время поиска по Bluetooth. Когда в комнате несколько устройств в режиме пейринга, discriminator позволяет контроллеру понять, к какому именно устройству подключаться.

Setup PIN Code (Passcode) — 27-битный секретный код (8 цифр при отображении), который используется для аутентификации. Это как пароль для первого подключения. Без знания этого кода нельзя подключиться к устройству, что предотвращает несанкционированный доступ.

Что по инструментам?

Теперь, когда мы разобрались с базовыми концепциями Matter, наша задача —  понять, как реализовать комиссионинг через телефон с передачей управления хабу. Для реализации этой задачи Apple предоставляет два фреймворка: Matter Framework и MatterSupport Framework.

Первый — это более низкоуровневый фреймворк, который даёт прямой доступ к Matter SDK. Основные классы:

MTRDeviceController — контроллер для управления устройствами

MTRDeviceControllerFactory — фабрика для создания контроллеров

MTRDevice — представление Matter-устройства

MTRBaseCluster* — классы для работы с кластерами: наборами функциональности, атрибутов и команд для управления определённой возможностью устройства. 

В документации Apple сказано, что Matter Framework используется для комиссионинга устройств и управления ими.

MatterSupport Framework — это высокоуровневый API для интеграции Matter в пользовательские приложения через App Extension. Основной класс здесь — MatterAddDeviceRequest, то есть запрос на добавление устройства

Изучив документацию, мы предположили, что перспективнее выглядит процесс подключения с использованием Matter Framework: он даёт полный контроль над процессом комиссионинга и позволяет создать кастомный UX. Здесь напомним, как происходит комиссионинг устройства Matter over Wi-Fi к экосистеме умного дома:

  1. Инициализация параметров для подключения

  2. Сопряжение с устройством — установка Bluetooth LE соединения

  3. Установка commissioning-сессии — обмен ключами безопасности (PAKE, Password Authenticated Key Exchange)

  4. Обмен данными — передача ключей, сертификатов, SSID и пароля Wi-Fi

  5. Подключение к сети Wi-Fi — завершение подключения

  6. Изменение Access Control — настройка ACL для администратора хаба

На первом этапе готовятся все необходимые данные:

// Парсим QR-код или ручной код устройства
guard let payload = payloadParser.parseSetupPayload(qrCodeString) else {
   throw MatterError.invalidPayload
}


let vendorId = payload.vendorID      // ID производителя
let productId = payload.productID    // ID продукта
let discriminator = payload.discriminator  // Идентификатор устройства
let setupCode = payload.setupPINCode      // PIN для аутентификации

В предыдущей статье рассказывали, что генерацию операционных кредов перевели на бэкенд. Запрашиваем с бэкенда информацию для подключения: fabric ID, Node ID, cертификаты для установления доверия и IPK (Identity Protection Key).

На втором этапе устанавливаем Bluetooth-соединение с устройством; происходит поиск устройства по discriminator и установка BLE-канала.

// Создаём и инициализируем контроллер
let controller = try factory.createController(onNewFabric: startupParams)


// Устанавливаем себя как делегата для получения событий
controller.setDeviceControllerDelegate(self, queue: delegateQueue)

При установке commissioning-сессии и PAKE происходит обмен ключами безопасности через протокол PAKE. Это обеспечивает защищённое соединение между устройством и контроллером без необходимости передачи пароля в открытом виде. PAKE гарантирует, что даже если кто-то перехватит трафик Bluetooth, то не сможет подключиться к устройству без знания setup-PIN-кода.

try controller.setupCommissioningSession(
   with: setupPayload,
   newNodeID: nodeID
)

На этапе обмена данными передаются:

  • Сертификаты для установления доверия — корневой (Root), промежуточный (Intermediate) и операционный (Operational).

  • Ключи: IPK (Identity Protection Key) — ключ для защиты идентификации устройств в сети. Operational Keypair — ключевая пара для операционной коммуникации

  • Сетевые параметры —  имя и пароль Wi-Fi сети.  

let startupParams = MTRDeviceControllerStartupParams(
   ipk: ipk,
   operationalKeypair: operationalKeypair,
   operationalCertificate: operationalCertificate,
   intermediateCertificate: intermediateCertificate,
   rootCertificate: rootCertificate
)


// Запускаем непосредственно процесс commissioning
try controller.commissionNode(
   withID: nodeID,
   commissioningParams: commissioningParams
)

В финале устройство получает учётные данные Wi-Fi и подключается к домашней сети. После успешного подключения оно становится доступным по IP-адресу; Bluetooth-соединение больше не требуется. На этом этапе происходит переход от BLE к IP-коммуникации: устройство подключается к Wi-Fi и получает IP-адрес по DHCP. Контроллер переключается с Bluetooth на IP-канал. Проверяется доступность устройства в сети.

Главный и критически важный шаг для архитектуры интеграции — это изменение Access Control (ACL). Устройство изначально подключается с правами администратора для смартфона, а значит, управлять им может только он. Но в нашем случае: который мы рассматриваем, управлять устройством должен центральный хаб — интеллектуальная колонка. Передача роли контроллера может быть решена через добавление хаба как второго администратора через Access Control Cluster. После этого шага хаб может полноценно управлять устройством независимо от телефона.

private func changeAdmin(adminSubject: NSNumber) async throws {
   guard let nodeID, let matterDeviceController else {
       throw MatterError.AdminChangingError.noAccessCluster
   }


   // Получаем устройство, которое только что прошло commissioning
   let device = MTRBaseDevice(nodeID: nodeID, controller: matterDeviceController)


   // Получаем Access Control кластер (всегда находится на endpoint 0)
   guard let accessControlCluster = MTRBaseClusterAccessControl(
       device: device,
       endpointID: 0,
       queue: Self.matterManagerQueue
   ) else {
       throw MatterError.AdminChangingError.noAccessCluster
   }


   // Читаем текущий ACL
   let acl = try await accessControlCluster.readAttributeACL(with: .none)


   guard let firstEntry = acl.first as? MTRAccessControlClusterAccessControlEntryStruct else {
       throw MatterError.AdminChangingError.accessClusterEmpty
   }


   // Создаём запись с новым администратором (хабом)
   let newEntry = MTRAccessControlClusterAccessControlEntryStruct()
   newEntry.privilege = NSNumber(value: MTRAccessControlEntryPrivilege.administer.rawValue) // 5 = admin
   newEntry.authMode = NSNumber(value: MTRAccessControlEntryAuthMode.CASE.rawValue) // 2 = CASE
   newEntry.subjects = [adminSubject]  // Subject ID хаба
   newEntry.fabricIndex = firstEntry.fabricIndex
   newEntry.targets = nil  // nil = доступ ко всем endpoint'ам


   // Записываем обновлённый ACL
   try await accessControlCluster.writeAttributeACL(withValue: acl.appending(newEntry))
}
Дополнительно...

Дополнительно на этапе pairing мы делегатами MTRDeviceController можем отслеживать прогресс и обновлять UI!

extension MatterManagerImpl: MTRDeviceControllerDelegate {
   func controller(
       _ controller: MTRDeviceController,
       statusUpdate status: MTRCommissioningStatus
   ) {
       // Получаем обновления статуса commissioning
       logger.debug("Controller status update: \(status)")


       // Обновляем UI в зависимости от статуса
   }


   func controller(
       _ controller: MTRDeviceController,
       commissioningSessionEstablishmentDone error: (any Error)?
   ) {
       logger.debug("Commissioning session established")


       if let error {
           // Показываем ошибку
           updatePhase(.failure(stage: .finding))
           return
       }


       // Переключаемся на "Проверяем подключение..."


       // Запускаем финальный этап commissioning
       let commissioningParams = MTRCommissioningParameters()
       try? controller.commissionNode(
           withID: nodeID,
           commissioningParams: commissioningParams
       )
   }


   func controller(
       _ controller: MTRDeviceController,
       commissioningComplete error: (any Error)?,
       nodeID: NSNumber?
   ) {
       logger.debug("Commissioning complete for nodeID: \(nodeID)")


       if let error {
           updatePhase(.failure(stage: .checking))
           return
       }


       // Показываем "Устройство подключено!"
   }
}

Когда первое решение не подошло

Мы подумали, что хорошо бы уметь давать пользователю прозрачную обратную связь о том, что происходит с его устройством в процессе комиссионинга, а значит, сообщения с анимацией в UI. Всё отлично работало… пока RC сборки не попробовали протестировать. При первом запуске пейринга на устройстве появлялось системное предупреждение:

Мы не придали ему особого значения — решили, что это просто информационное сообщение для разработчиков. Нажали «ОК» и продолжили работу. Но когда отдали RC-сборку на тестирование, у тестировщиков не подключались устройства, при установке Bluetooth-соединения они выдавали ошибки, комиссионинг не стартовал, а попытка найти устройство выдавала Timeout. При этом у разработчиков всё было в порядке.

Корень проблемы в том, что для работы с Matter Framework напрямую нужен специальный сертификат Bluetooth Central Matter Client Developer. Именно он позволит приложению работать с низкоуровневым API напрямую.

Мы обратились в Apple Developer Technical Support с единственным вопросом: «А можно ли использовать Matter Framework напрямую в production-сборках?». И получили ответ.

No(
No(

Оказалось, единственный официально поддерживаемый способ — это использование MatterSupport Framework с Matter App Extension. Matter Framework с прямым доступом предназначен только для внутренней разработки. А это означало, что интеграция Matter в iOS требует другого подхода к комиссионингу.

Переходим на MatterSupport Framework

MatterSupport не выполняет комиссионинг в основном приложении. Процесс выглядит так:

  1. Основное приложение инициирует запрос через MatterAddDeviceRequest

  2. iOS показывает системное окно HomeKit поверх приложения

  3. Matter App Extension выполняет commissioning в изолированной среде

  4. Результат возвращается в основное приложение

Запуск инициации запроса:

import MatterSupport


func startMatterPairing() async {
   // Создаём запрос на добавление устройства
   let request = MatterAddDeviceRequest()


   // Указываем название нашей экосистемы
   request.topology = MatterAddDeviceRequest.Topology(
       ecosystemName: "Salute"
   )


   do {
       // Запускаем процесс — iOS покажет системное окно
       try await request.perform()


       // Если дошли сюда — пейринг успешно завершён
       logger.debug("Matter pairing completed successfully")
   } catch {
       // Обработка ошибки
       logger.error("Matter pairing failed: \(error)")
   }
}

```

При вызове request.perform() iOS показывает системное окно HomeKit, в котором происходит весь процесс комиссионинга. Изменить дизайн этого окна нельзя.

Для реализации App Extension создаём новый target типа Matter Add Device Extension и реализуем MatterAddDeviceExtensionRequestHandler:

import MatterSupport
import SmartHomeMatter


class RequestHandler: MatterAddDeviceExtensionRequestHandler {


   private let container = MatterDependenciesContainer()


   // 1. Валидация учётных данных устройства
   override func validateDeviceCredential(
       _ credential: DeviceCredential
   ) async throws {
       logger.debug("validateDeviceCredential: \(credential)")


       // Здесь можно проверить, что устройство поддерживается
       // Например, проверить vendorID/productID
   }


   // 2. Выбор Wi-Fi сети для устройства
   override func selectWiFiNetwork(
       from networks: [WiFiScanResult]
   ) async throws -> WiFiNetworkAssociation {
       logger.debug("selectWiFiNetwork, available: \(networks.count)")


       // Используем текущую сеть iPhone
       // Пользователю не нужно вводить SSID и пароль!
       return .defaultSystemNetwork
   }


   // 4. Основная логика commissioning
   override func commissionDevice(
       in home: MatterAddDeviceRequest.Home?,
       onboardingPayload: String,
       commissioningID: UUID
   ) async throws {
       // Вызываем нашу логику commissioning из SmartHomeMatter модуля
       try await container.matterService.startPairing(payload: onboardingPayload)
   }


   // 5. Список комнат
   override func rooms(
       in home: MatterAddDeviceRequest.Home?
   ) async -> [MatterAddDeviceRequest.Room] {
       logger.debug("rooms requested")


       // Возвращаем одну комнату, чтобы пропустить экран выбора комнаты
       return [MatterAddDeviceRequest.Room(displayName: "Main Room")]
   }


   // 6. Настройка устройства (сохранение имени)
   override func configureDevice(
       named name: String,
       in room: MatterAddDeviceRequest.Room?
   ) async {
       // Тут мы можем обработать название, которое пользователь ввел в окне HomeKit, для девайса
   }
}

Как показал наш эксперимент, при переходе на MatterSupport исчезнут три важные вещи. Во-первых, контроль над UI. Весь UI процесса commissioning контролируется системным окном iOS, нельзя кастомизировать тексты сообщения или добавить дополнительные экраны.

Во-вторых, нельзя отображать красивые статусы в реальном времени. Extension работает в изолированной среде, и передавать промежуточные состояния обратно в приложение в реальном времени нельзя. Пользователь видит только системное окно с индикатором загрузки, базовую информацию и финальный результат.

В-третьих, нельзя сделать пакетный пейринг нескольких Matter-устройств одновременно, так как каждое запускает отдельное системное окно. Выходит, что для каждого нужно повторять один и тот же процесс.

Зато MatterSupport даёт и два важных преимущества: не нужен экран для ввода Wi-Fi, а значит, ввод SSID и пароля. Система автоматически использует текущую Wi-Fi сеть iPhone (.defaultSystemNetwork).  И всё наконец-то работает!

Заключение

В эксперименте с интеграцией Matter мы протестировали весь путь: от красивого, но работающего только у разработчиков решения до полностью функционального production-ready решения. Надеюсь, это опыт поможет iOS-разработчикам избежать граблей, которые щедро разложены на этом пути!

Полезные ссылки

Matter Specification — официальная спецификация

Apple MatterSupport Framework — документация к фреймворку от Apple

Apple Matter Framework — документация к фреймворку от Apple

WWDC 2022: Meet Matter — введение в Matter

WWDC 2023: Explore Matter for home automation apps — интеграция в приложения

Комментарии (0)