Границы универсального, или Интеграция протокола Matter over Wi-Fi в интеллектуальные колонки Сбер

Недавно мы выпустили интеллектуальные колонки СберБум 2.0 — хаб умного дома, который поддерживает протоколы Zigbee и Matter over Wi-Fi. Zigbee у нас был и прежде, а вот Matter over Wi-Fi — нет. Внедрение оказалось предсказуемо сложным: требовалась архитектура, глубоко интегрированная в наш бэкенд. Тем не менее, получилось:
перевести генерацию операционных кредов из библиотеки в бэкенд;
реализовать передачу роли контроллера между смартфоном и колонкой, чтобы пользователю было удобнее подключать устройства;
решить проблему типизации end-устройств в Matter (да, это проблема), переиспользовав собственный скриптовый язык Fortuna, который применяем в хабе.
Для начала — четыре абзаца базовой информации о протоколе. Если вы уже знакомы с ним, эту часть можно пропустить.
Matter
Открытый протокол связи для устройств умного дома, Matter разработан консорциумом ведущих технологических компаний: Google, Amazon, Apple, Samsung и другими. Его появление должно (было) решить проблему множества закрытых протоколов и стандартов умного дома. До этого пользователей запирали в рамках одной платформы; при покупке колонки приходилось покупать только совместимые с ней устройства. Обеспечение совместимости оставалось за производителем; к примеру, об интеграции новых брендов в Умный дом Сбер рассказывали здесь.
Matter — это попытка создать единый протокол, единый язык общения для всех умных устройств. Одно end-устройство в нём способно подчиняться множеству контроллеров: Matter-лампочку можно подключить к любой экосистеме, которая поддерживает протокол, и управлять ею одновременно из нескольких экосистем.
Большой плюс протокола — способность Matter-устройств функционировать без подключения к интернету. Matter работает поверх IP-протокола (Internet Protocol), это делает его универсальным для большинства современных сетевых технологий. Выбор конкретного протокола зависит от задачи:
Matter over Wi-Fi — самый распространённый вариант для домашних устройств;
Matter over Thread — mesh-сеть с низким энергопотреблением, идеальна для устройств на батарейках;
Matter over Ethernet — для стационарных устройств типа хабов.
Кто может стать комиссионером
Во вселенной Matter ноды, они же узлы сети, могут быть end-устройством или контроллером. Как правило, контроллер также выполняет роль комиссионера: добавляет end-устройства в сеть как новые ноды в Matter Fabric — доверенную группу устройств. Этот процесс называется комиссионингом (commissioning).
Во время подключения устройство-комиссионер передаёт данные по Bluetooth LE (или Wi-Fi Soft AP), после переключается на IP-соединение. Но это не слишком удобно. Радиус действия BLE ограничивают физические препятствия, которых в квартире предостаточно, и сама мощность BLE-модуля устройства. Чтобы подключать уже установленные в доме Matter-устройства, придётся перемещать контроллер ближе к ним, то есть носить за собой колонку по всей квартире.
Интегрируя протокол Matter в Умный дом Сбер, мы решили упростить флоу для пользователя. Колонка по-прежнему остаётся контроллером, но в ходе подключения роль комиссионера выполняет смартфон с приложением Салют — можно просто ходить с ним по квартире, подключая Matter-устройства. Как выглядит процесс для пользователя:
он подносит смартфон к условной Matter-лампе и проводит первичное подключение по Bluetooth;
после успешного комиссионинга администратором становится хаб-колонка;
вся коммуникация идёт между хабом и устройством по Wi-Fi (IP-протоколу).
Как выглядит процесс с технической точки зрения, многим уже известно. Под спойлером кратко процесс и основные термины.
Больше базы
Onboarding payload
Чтобы подключить Matter-устройство, необходимо знать его начальные параметры безопасности. Обычно они представлены в виде QR-кода на самом устройстве либо упаковке. Часто QR-код дублируется цифровым кодом; это нужно на случай, если нет возможности отсканировать код или сканер не может его распознать. Цифровой код содержит меньше информации, но всё ещё достаточно для подключения.
Device discovery
Запускается поиск устройства — чаще всего по BLE. Из взятого до этого QR-кода извлекается дискриминатор, по которому можно идентифицировать устройство, так как рекламный пакет BLE также содержит это значение.
Passcode Authenticated Session Establishment (PASE)
Первичное подключение к устройству. Для организации защищённой сессии используется PIN-код из QR-кода. Без этого кода никто другой не сможет подключить устройство. Цель — проверить владельца.
Get device information
Получение базовой информации об устройстве: производитель, модель, возможности и другое.
Validate attestation
На этапе валидации устройство отдаёт сертификаты, которые подтверждают его подлинность:
Device Attestation Certificate (DAC) — уникален для конкретного устройства;
Product Attestation Intermediate (PAI) — промежуточный для линейки или всего бренда.
По ним комиссионер может проверить цепочку доверия, убедиться в корректности данных и определить, проходило ли устройство сертификацию.
Add node operation certificate
Устройство генерирует пару эксплуатационных ключей и отправляет комиссионеру запрос на выпуск сертификата (CSR). Комиссионер передаёт CSR центру сертификации конкретного fabric. Тот на основе CSR выпускает операционный сертификат — Node operation certification (NOC) — который устанавливается на устройство.
NOC содержит FabricID — это общий для всех устройств идентификатор всего умного дома — и NodeID: идентификатор конкретного устройства в конкретном fabric.
Network Provisioning
Передача данных сети Wi-Fi. Если end-устройство уже подключено к локальной сети, этот шаг пропускается.
Operational Discovery
Поиск устройства в сети посредством DNS-SD. Устройства регистрируются в сервисе. Они используют свой идентификатор узла, состоящий из FabricID и NodeID.
Cеrtificate Authenticated Session Establishment (CASE)
Установка защищенной сессии с использованием постоянных ключей шифрования, определяется окончательное доверие между узлами сети.
Готово: устройство успешно подключено к Matter-сети. Теоретически.
3 проблемы интеграции Matter over Wi-Fi
Использовать уже имеющееся решение — Google Home Mobile SDK — мы не могли. Этот SDK связан с Google Mobile Services, которые есть не на всех смартфонах либо из-за старых версий не содержат поддержку Matter. Выходило, что для подключения Matter-устройств к СберБум 2.0 нам придётся просить часть пользователей установить или обновить необходимые компоненты. Либо вообще купить другой смартфон.
В репозитории, курируемом Connectivity Standards Alliance (CSA), нашлась вся необходимая кодовая база — исходники для как для разработки собственных устройств, так и для приложений, и даже пример приложения для Android.
Репозиторий загружен, демо-проект под Android открыт. Инструкция по сборке перед глазами. Правда, она далеко не исчерпывающая — приходилось доустанавливать компоненты на основе информации из логов сборки, которые кидались огромными stack-trace падения скриптов, а причина падения не всегда была очевидна.
Первый опыт запуска приложения с Matter-устройством, которое было под рукой, оказался на удивление успешным: устройство подключилось и принимало команды, а приложение считывало данные! Вся функциональность нам не требовалась — только подключение, минимальный работающий код, вокруг которого можно выстраивать бизнес-логику.
Проблема #1: как конфигурировать Matter Controller?
В примере казалось, что ничего особенного делать не нужно — всё работает из коробки. Сначала необходимо создать ChipDeviceController с определенными параметрами работы. Перед нами лежал Builder, в котором можно было указать произвольный набор данных. Базово он выглядел так:
ControllerParams.newBuilder()
.setControllerVendorId(SBER_VENDOR_ID)
.build()Всё в общем-то работало… но радость быстро прошла. В Builder никак не контролируется обязательность указания дополнительных данных. Что-то не передали? Значение некорректно? В середине или конце процесса подключения возникает ошибка из разряда «Что-то пошло не так». Сделать по ней выводы, что именно не так, нельзя.
Помогли коллеги, которые экспериментировали с подключением Matter на iOS (результатами изысканий поделятся отдельно). Оказалось, что во фреймворке Apple для Matter есть чёткие контракты для кастомных решений сертификации. Теперь мы знали, какие параметры нужно передать в контроллер, и настройки стали выглядеть так:
ControllerParams.newBuilder()
.setControllerVendorId(SBER_VENDOR_ID)
.setFabricId(controllerParams.fabricId)
.setIpk(controllerParams.ipk)
.setOperationalCertificate(controllerParams.operationalCertificate)
.setIntermediateCertificate(controllerParams.intermediateCertificate)
.setRootCertificate(controllerParams.rootCertificate)
.setKeypairDelegate(matterKeyPairDelegate)
.build()Проблема #2: а что делать с сертификатами?
Как ясно из кода выше и из описания процесса в целом, для организации доверия между устройствами Matter-сети необходимы сертификаты. В демо-проекте мы ещё не задумывались о том, откуда их брать и какими они должны быть — ведь всё работало, устройство подключалось успешно. Как? Оказывается, библиотека содержит набор ключей и для таких случаев генерирует все недостающие параметры, чтобы обеспечить демо. В проде это использовать, разумеется, нельзя.
Для безопасности и стабильности решения генерацию сертификатов потребовалось вынести на бэкенд. Это решение позволило:
связать приложение, контроллер (в нашем случае это колонка) и устройство умного дома;
сохранять данные при сбросе или случайном удалении контроллера из аккаунта.
В результате бэкенд — это и мастер-система по структуре домов и устройств пользователей, и единое хранилище секретов для формирования сертификатов. Для этого на бэкенде появился специальный микросервис network-manager. Именно он генерирует и хранит необходимые Matter сущности — Fabric ID, Node ID — и генерирует в процессе подключения Node Operational Certificate (NOC) для устройства. Он же управляет сертификатами узлов сети.
И ещё один бонус
Хранение сертификатов на бэкенде позволяет реализовать Matter-пейринг через веб-приложение, у которого нет прямого доступа в домашнюю сеть.
По сути мы распределили функции комиссионера между приложением Салют, бэкендом и колонкой. Бэкенд отвечает за fabric, привязку её значения к аккаунту, ключи и сертификаты, чтобы устройство попадало в указанную сеть. Мобильное приложение сначала пейрит устройство в собственную сеть, а потом, получив от бэкенда fabric и сертификаты, назначает устройство в целевую. В случае пейринга через веб-приложение примерно то же самое делает колонка — только сразу в целевую сеть.
ControllerParams.newBuilder()
...
.setOperationalCertificate(deviceParams.operationalCertificate)
...
.build()На этом этапе начало ломаться примерно всё: необходимо было четкое соответствие всех данных внутри сертификата. Любое отклонение в полях FabricID или NodeID — и подключение заканчивалось ошибкой. Вдобавок мы реализовывали в разработке флоу, где конечным управляющим устройством выступает колонка; это тоже создавало небольшую путаницу, где и какой идентификатор использовать.
Когда мы успешно разобрались с данными и сертификатами и реализовали комиссионинг через промежуточное устройство-комиссионер, подключение начало выдавать ошибку лишь в 50% случаев. Успешный успех.
Оказалось, что несмотря на то, что значения идентификаторов являются uint64, вторая половина диапазона ломала работу криптобиблиотек. Пришлось вдвое ограничить значения, генерируемые бэкендом. Вдобавок Android и С++ работают с криптографией в разных форматах; то, что генерировалось на бэкенде или локально в Android, не подходило для передачи в библиотеку. Сертификаты требовалось адаптировать к соответствующему виду.
Мы понимали, что необходимо передать какой-то ключ, но формат описан не был. Пришлось подсматривать у iOS, какого рода данные передаются SDK, затем долго и сложно искать решение… которое оказалось простым: из всей последовательности байтов, кодирующих ключ в одной последовательности, нужно отрезать публичную часть, которая шла первыми 65 байтами. И всё заработало.
Проблема #3: как передавать управление?
В новом флоу требовалось каким-то образом передавать права новому контроллеру. При этом танцы вокруг setAdminSubject в параметрах контроллера ни к чему не привели.
Идея: раз мы являемся администратором, то можем попробовать дать новые права — подключиться к устройству и поработать с кластером прав доступа!
С точки зрения кода процесс выглядит просто: прочитать ACL, добавить колонку в качестве администратора, удалить текущий смартфон из списка администраторов, записать список прав доступа.
matterAclManager.readAcl(devicePointer)
.map { acl ->
acl.appendAdminSubject(connectInfo.adminSubject)
.deleteAdminSubject(connectInfo.nodeId)
}
.flatMap { newAcl ->
matterAclManager.writeAcl(devicePointer, newAcl)
}После этого новый контроллер получает команду, что может начать работать с подключаемым устройством самостоятельно. Он осуществляет подключение, опрашивает на тему поддерживаемых фич; устройство прописывается на бэкенде с нужным типом и возможностями контроля и управления. Приложение дожидается статуса, как все прошло, и сообщает пользователю о результате.
Теория и практика
Концепция Matter — полноценная локальная работа устройств в единой сети независимо от бренда. По замыслу протокол должен обеспечить их универсальную совместимость без привязки к экосистеме. Matter-устройство организовано иерархически. Оно состоит из трёх уровней:
Node — узел. Физическое Matter-устройство целиком. При этом Matter-девайсы могут быть составными и содержать несколько логических устройств.
Endpoint — конечная точка. Логическая функция внутри устройства. Каждый endpoint представляет отдельное устройство или функцию. Например, есть умная розетка с датчиком температуры. Внутри три endpoint: 0 (обязательный) — служебная информация, идентификация устройства. 1 — управление розеткой (вкл/выкл). 3 — датчик температуры. (Как можно понять здесь, нумерация endpoint произвольна, и полагаться на неё нельзя; проиллюстрируем тезис ниже).
Cluster — кластер. Набор функциональности, атрибутов и команд для управления определённой возможностью устройства. Кластеры содержат атрибуты, команды, события. Атрибуты — это характеристики устройства, например, текущая яркость. Команды — действия, которые можно выполнить (например, включить/выключить). События — уведомления об изменениях. Примеры кластеров: On/Off Cluster — включение/выключение устройства. Level Control Cluster — управление яркостью (uint8, значения 1..254). Color Control Cluster — управление цветом освещения (оттенок с насыщенностью, координаты XY, цветовая температура). Access Control Cluster — управление правами доступа.
Например, какой может быть конфигурация умной RGB-лампочки:
Node (лампочка)
├── Endpoint 0 (корневой)
│ └── Basic Information Cluster
└── Endpoint 1 (освещение)
├── On/Off Cluster
├── Level Control Cluster
└── Color Control Cluster
К интеграции мы приступали, полные оптимизма: раз есть спецификация с обязательными пунктами и есть сертификация, то модель этих устройств можно читать как контракт! Далее — отрицание, гнев, торг, депрессия, реализация проекта. У практической реализации Matter в устройствах разных вендоров, мягко говоря, множество тонкостей. (Ради справедливости, ситуация намного лучше, чем у Zigbee, а в целом проблемы типизации есть примерно у всех). Пять Matter-ламп могут заметно отличаться друг от друга. Одна будет поддерживать только включение и яркость, вторая — цветовую температуру, третья будет объявлять возможности неожиданным образом, у четвертой будут нетипичные значения атрибутов, а пятая вообще потребует собственных отдельных правил преобразования данных.
Можно решать каждое такое отличие через код контроллера — if (model == …) … else if (vendor == …). Но это быстро превратилось бы в слой C++ кода под конкретные модели устройств: нашёл новую особенность устройства — переписываешь код, тестируешь контролер, выпускаешь новую прошивку.
Получалось, что нужно отделить логику работы с конкретным устройством от самого контроллера. Здесь мы решили использовать сходство пользовательского сценария и сценария управления устройством. С точки зрения хаба они и вправду похожи: событие — условие — текущее состояние — действие над устройством. Поэтому в решении для Matter переиспользовали скриптовый язык, который ранее разработали для локальных сценариев умного дома.
В основу нашего языка, который мы назвали Fortuna, лёг стековый язык Forth. Он использует польскую запись, в которой действие располагается после аргументов. Например, вот кусок скрипта, который по команде из «облака» включает и выключает устройство:
{ON_OFF_CMD} CHANGED IF
{ON_OFF_CMD} READ_STATE 0 EQ IF {cmd.OFF} SEND_TO_DEVICE THEN
{ON_OFF_CMD} READ_STATE 1 EQ IF {cmd.ON} SEND_TO_DEVICE THEN
THEN
Если ячейка с командой изменилась, прочитать её; если там ноль, отправить устройству команду на выключение, а если единица — на включение. IF снимает флаг со стека, THEN закрывает ветку.
Fortuna изначально появился для устройств с очень небольшим объёмом свободной оперативной памяти: тогда нам нужен был механизм, который позволил бы исполнять сценарии прямо на хабе, но занимал бы минимум места — то есть Lua и тем более Javascript-движок не подходили. Fortuna не использует текст во время исполнения скриптов: компилятор один раз переводит скрипт в байткод, далее байткод исполняется из буфера.
В хабе Fortuna работала с заранее выделенными буферами фиксированного размера. На байткод всех активных сценариев отводилось 4 Кб, а глубина стека интерпретатора ограничивалась 32 значениями.

Благодаря Fortuna для устройства с нестандартной реализацией его особенности фиксируются в шаблоне прямо в момент его подключения. Хаб опрашивает устройство и выбирает подходящий шаблон, а из него генерирует небольшой скрипт именно для этого устройства. Шаблонизатором выступает наш инструмент под названием BART: на входе — описание устройства, полученное при опросе, на выходе — готовый скрипт Fortuna плюс список возможностей устройства, который отправляется в «облако».
Во время опроса с нулевого endpoint читается список всех endpoint устройства и с каждого из них — список серверных кластеров. Дополнительно по каждому кластеру подтягивается FeatureMap: битовая маска, в которой устройство сообщает, какие необязательные возможности оно реально реализовало.
По набору кластеров подбирается шаблон; так, шаблон 0x0006 читается как «любой endpoint, у которого есть серверный кластер On/Off». На каждый подходящий endpoint из шаблона генерируется небольшой скрипт (а всего для Matter-устройств сейчас шесть шаблонов).
Внутри шаблона ветвление идёт по битам FeatureMap. К примеру, в шаблоне цвета это выглядит так:
HAS_COLOR = бит 0 FeatureMap
HAS_COLOR_TEMP = бит 4 FeatureMapАтрибуты и команды цветного режима попадают в скрипт при первом бите, режимы цветовой температуры — при пятом. Таким образом, лампа с настройкой температуры света и RGB-лампа проходят через один шаблон, но на выходе получают разные скрипты. Получается своего рода слой адаптации: Matter-устройство — шаблон — локальный скрипт — единая модель устройства Сбера. А контроллер остаётся универсальным.
3 примера, где Matter — не то, чем кажется
На device type и номера endpoint не опирается ни один из шаблонов: в ходе реализации проекта мы усвоили, что тип устройства и нумерация endpoint в Matter-устройстве — это скорее утверждение производителя о себе, чем факт. С чем персонально пришлось столкнуться при интеграции:
Неверный тип устройства, который ограничивает управление ими
Например, реле, которое при подключении «представляется» лампой с одним управляющим endpoint — «вкл» и «выкл».
Кастомные кластеры и атрибуты
Настройки, которые не прокидываются в стороннюю систему или работают не так, как запланировано в типизации. В бета-тестировании мы встретили захватывающий пример этой проблемы: яркость филаментной лампы при длительной работе менялась без действия пользователя. Человек ничего не трогал, лампа жила своей жизнью.
Начали изучать проблему. Во-первых, поняли, что команды на лампу всё же шли, просто не от пользователя: на устройстве был включён адаптивный свет — режим, в котором хаб меняет цветовую температуру под время суток. Во-вторых, обнаружили, что у этой модели филаментной лампы максимальная достижимая яркость физически зависит от цветовой температуры. Замер на живой лампе:

При тёплом свете лампа отдаёт 65% яркости по сравнению с холодным, то есть фокус в светодиодах.
Почему хаб заводит лампу за пределы её физических способностей? Дело в том, как именно устройство сообщает о них. В Matter это атрибуты ColorTempPhysicalMinMireds и ColorTempPhysicalMaxMireds. Мы читаем их и пересчитываем в свою шкалу по формуле mired = 500 − 0,347 × light_colour_temp (о пересчёте из шкалы в шкалу будет ниже). При этом у разных ламп могут быть разный диапазон mired — это нормально. Но встречаются и устройства, где значения mired — это просто значения по умолчанию из спецификации, например, 0 и 65279. Выходит, что на «Какие у тебя границы?» лампа отвечает: «Границы есть!».
Для уже проданных ламп пришлось проставить диапазоны вручную в каталоге устройств. А вот для моделей, которых мы ещё не видели, научили хаб читать границы с устройства при подключении и публиковать их вместе со списком возможностей. В шаблоне это выглядит так:
limits:
min: "{MIN_TEMP} decode"
max: "{MAX_TEMP} decode"
when:
- "MIN_TEMP EXISTS"
- "MAX_TEMP EXISTS"Условие EXISTS позволяет защититься от устройств, которые не отдают границы: они подключаются к умному дому без объявленного диапазона, а не с воображаемым.
Zigbee-кластер внутри Matter
Часть Matter-розеток сообщает напряжение и ток через кластер 0x0B04. При этом в Matter его нет: штатный кластер для этих случаев — Electrical Power Measurement, номер 0x0090, а 0x0B04 — это Electrical Measurement из библиотеки кластеров Zigbee. Опять же, производителей винить не в чем. До версии Matter 1.3 в стандарте не существовало способа сообщить напряжение, ток или мощность. Кластер 0x0090 появился только в версии 1.3 (а таблица его ревизий до сих пор состоит из одной строки — «Initial revision»). При этом розетки с замером потребления производители уже выпускали, просто вынося наружу Zigbee-кластер из прошивки. Когда стандарт догнал рынок, парк устройств уже стоял в квартирах. Для таких случаев у нас два шаблона, и оба дают на выходе одинаковый набор возможностей: напряжение, ток, мощность.
Свет как число и кластер
В модели умного дома Сбера свет — это одна сущность с одним значением. Всё упаковано в тридцать два бита:
биты 31..30 режим: 0 — белый свет, 1 — цветной
биты 29..20 оттенок (0..360)
биты 19..10 насыщенность в цветном режиме, цветовая температура в белом (0..1000)
биты 09..00 яркость (0..1000)
Одно число подаётся из приложения в «облако» и из «облака» — на СберБум 2.0; одно число СберБум 2.0 отдаёт обратно как текущее состояние лампы. Поле, забитое единицами, означает «значение не задано».
В Matter свет — это три независимых кластера. Какие из них лампа поддерживает, она сообщает в FeatureMap, а в каком находится сейчас — в отдельном атрибуте ColorMode:
On/Off отвечает за включение.
Level Control — за яркость, отдельным атрибутом CurrentLevel.
Color Control — за цвет. Внутри него, как уже упоминалось, свои режимы: оттенок с насыщенностью, координаты XY, цветовая температура.
Общего между этими двумя представлениями — только физический смысл. Одно значение у нас — три кластера в Matter; упакованные биты — отдельные атрибуты. Наша шкала 0–1000 против 1–254 в протоколе и наш признак режима в двух старших битах против атрибута ColorMode в кластере. Чтобы переводить А в Б, а Б в А, мы используем склейку в шаблоне. Она состоит из шести строк — по три на направление.

Когда данные подаются снизу вверх, от устройства к облаку, мы собираем одно число из двух источников, если изменилось хоть что-то из цвета или яркости.
{mtr_light_color} CHANGED {local_light_level} CHANGED OR IF
{mtr_light_color} READ_STATE {local_light_level} READ_STATE OR {LIGHT} WRITE_STATE
THENЕсли данные идут сверху вниз, от облака к устройству, одно число разбираем на два. DUP дублирует вершину стека, чтобы прочитать значение дважды; 0x3FF AND оставляет нижние десять бит — это яркость.
{LIGHT} CHANGED IF {LIGHT} READ_STATE
DUP {mtr_light_color} WRITE_STATE
0x3FF AND {local_light_level} WRITE_STATE
THENДальше данные разъезжаются по кластерам: цвет — в Color Control (в тот из его режимов, который лампа объявила в FeatureMap), яркость — в Level Control с пересчётом шкалы.
Заключение
Интегрировав Matter over Wi-Fi, мы переходим к следующему этапу: Matter Bridge. Как уже понятно из описания, Matter — далеко не самый лёгкий протокол для разработчика. Зато самый понятный для пользователя с точки зрения UX: можно взять с полки любое Matter-устройство и подключить к любому умному дому, который поддерживает этот протокол (или к двум умным домам. Или к трём). Это просто, и именно поэтому рынок Matter-устройств стабильно растёт. В конечном итоге, пользователь хочет получить не «как можно больше протоколов», а «как можно более понятный и автономный умный дом». И мы упорно движемся в эту сторону.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.