Сразу оговорюсь - информация в статье ниже, получена путем реверс инжиниринга, моих догадок, а также из информации, ставшей публичной. Часть догадок может быть ошибочной, а часть утверждений может измениться в будущем. Так что относитесь к статье, как к любому другому непроверенному источнику.
Как многие, интересующиеся устройствами Яндекс, знают, ядро "умной колонки" составляет некий quasar, который технически представляет собой набор из нескольких десятков сервисов, крутящихся как локально на устройстве, так и взаимодействующие с бэкэндом в облаке. Некоторые (скорее всего самые древние) сервисы упоминаются в quasar.cfg - это maind, aliced, brickd, glagold и др. Часть из них (тот же glagold) ранее была реализована в файле /system/vendor/quasar/maind, другая часть (aliced) - в ServicesIosdk.apk. После крупного обновления Яндекс.Станции Max в августе 2024г, модули, ранее находившиеся в maind, перекочевали в libquasar_daemons.so в ServicesIosdk.apk.
Общение между сервисами производится путем асинхронного IPC, который может работать как в пределах одного устройства, так и по сети. Внутри IPC используется механизм сообщений на основе Google Protobuf.
В релизных версиях прошивок сеть слушает только glagold (строка "externalPort" : 1961 в quasar.cfg). Остальные порты в релизе доступны только с localhost.
Существует утилита protodump, позволяющая создавать .proto файлы, скормив ей скомпилированные бинарники. В утилите есть пара багов, из-за чего она пропускает некоторые протобуфы (к слову, очень редко) и иногда создает некомпилирующиеся файлы (которые можно поправить и вручную). Как-нибудь я выложу свои изменения на гит. Также утилита не дампит синонимы для названий полей, что иногда запутывает.
Есть еще нюанс при использовании утилиты. Если из файлов, скомпилированных из C++ или GO, протобуфы дампятся без особых проблем, то из .DEX файлов так просто сдампить не получится. Из-за ограничений явы, скомпилированные протобуфы оказываются разбиты на несколько строк, которые в .DEX файле идут далеко не подряд - и их приходится собирать в один кусок самостоятельно. Если кто-нибудь напишет для этого утилиту - будет неплохо.
Файлы .proto содержат в себе очень много интересного. А если сравнить их дампы из разных версий прошивок, то можно увидеть текущие тренды в разработке. Например, не так давно Яндекс добавил Ясмину (турецкий голосовой помощник), с рядом функций, интересных мусульманскому сообществу.
Всего дамп протобуфов выходит больше чем на 1 мегабайт. Я рекомендую начать чтение с файла \yandex_io\protos\quasar_proto.proto с классом QuasarMessage.
Так как единственный доступный по сети сервис - это glagold, то его и буду использовать для взаимодействия со Станцией. Информации по нему немного, в основном есть в исходниках dd-alicization, плагина Яндекс.Станции для HomeAssistant (локальный режим там работает через glagold).
А пример, как отправлять команды glagold с помощью curl есть тут - собственно, этот способ я и использую.
Если посмотреть в maind или libquasar_daemons.so команды из примеров взаимодействия с glagold, приведенные на указанных выше ресурсах - можно увидеть еще несколько интересных команд, которые не были упомянуты: "showAliceVisualState", "aliceStateBypass", "externalCommandBypass", "leaderOverrideBypass", "clusterMessage", и это еще не все.
Для затравки приведу пример взаимодействия с glagold, с которого я сам начинал.
Предположим, мы хотим вывести на LED экран Яндекс.Станции свою картинку. Для этого используется команда "externalCommandBypass". Эта команда отправляет ExternalCommandMessage в сервис aliced:
В данном случае достаточно заполнить только поля:
name = имя команды, это "draw_led_screen",
is_route_locally = true,
payload = параметры команды "draw_led_screen" (о них будет ниже).
Для примера, выводим на экран картинку https://ru.cab/images/video.gif, взятую из одной из старых версий прошивки Яндекс.Станции. Если будете делать свою - картинка должна быть в оттенках серого, 25х16 точек. Цветную картинку должно автоматом сконвертировать - но я не проверял насколько это будет качественно.
Чтобы получить бинарный protobuf для ExternalCommandMessage, нам надо его закодировать. Для этого потребуется поработать питоном (желающие могут использовать C++ или Java).
Сперва дампим все .proto файлы из libquasar_daemons.so утилитой protodump:
protodump.exe -file libquasar_daemons.so -output proto
Будет создана папка proto.
Копируем всё что в папке proto\a.yandex-team.ru на уровень выше (в папку proto). Почему - иначе будут ошибки с ненайденными файлами. Мне проще скопировать, чем разбираться.
Далее генерируем исходники на питоне для всех .proto файлов (можно, конечно, только для используемых - но проще сделать сразу для всех):
protoc.exe --proto_path=proto;proto\a.yandex-team.ru --python_out=путь_куда_генерировать proto\a.yandex-team.ru\alice\protos\api\rpc\historical_events\historical_events.proto
Тут "proto\a.yandex-team.ru\alice\protos\api\rpc\historical_events\historical_events.proto" указано в качестве примера. Требуется выполнить данную команду для всех .proto файлов в папке proto и вложенных.
Далее создаем файл, я назвал его "_make_obj_extcmdmsg.py" с таким содержимым:
Увы, текст привожу скриншотом, так как Дзен не позволяет вставлять код, выровненный пробелами.
Создаем файл led-image.txt, который будет скомпилирован в бинарный protobuf такого содержимого:
{
"is_route_locally": true,
"name": "draw_led_screen",
"payload": "{\"animation_sequence\" : [ { \"frontal_led_image\":\"https://ru.cab/images/video.gif\",\"endless\":true } ] } "
}
Если убрать "endless" - анимация будет проиграна только один раз.
В payload можно указать в массиве несколько GIF файлов. В таком случае "endless" должна присутствовать только в последнем. Как указать длительность показа одной картинки - я не изучал.
Также можно добавить булевое поле "till_end_of_speech". В таком случае можно сперва послать Алису говорить какой-то текст и запустить анимацию, которая прекратится как только Станция замолчит.
Кодируем в бинарник так:
python _make_obj_extcmdmsg.py led-image.txt
На экран будет выведено:
Нас интересует срока base64, которая находится между апострофами.
Далее выполняем команду в cmd.exe:
command - отправляемая команда ("externalCommandBypass").
from_device_id - используется для получения ответа от Станции. В нашем случае может быть просто случайный GUID, так как запрос отправляется через curl, а не от другой Станции. К счастью, Станция не проверяет существование устройства с данным GUID, также можно всегда использовать одно и то же значение.
data - строка со скриншота выше.
И команда не заработает - так как я забыл написать, что требуется сперва запустить утилиту dd-alicization :)
Эта утилита упрощает взаимодействие с glagold, беря на себя вопросы авторизации, шифрования и тд.
Для запуска утилиты потребуется:
- Создаем файл ".env.local" с таким содержимым:
YANDEX_OAUTH_TOKEN=y0_AgAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxk
GLAGOL_USE_STATION_CONFIG=1
GLAGOL_STATION_ID=XK0000000000000xxxxxxxxxxxxxxxxx0
GLAGOL_STATION_ADDRESS=192.168.1.116
GLAGOL_STATION_PORT=1961
GLAGOL_CONFIRM_CONNECTION=0
HTTP_HOST=:58080 - YANDEX_OAUTH_TOKEN - ваш токен Яндекс.Музыки,
- GLAGOL_STATION_ID - идентификатор устройства,
- GLAGOL_STATION_ADDRESS - IP адрес устройства в сети.
- Запускаем dd-alicization.exe
Вот после этого - уже можно запускать curl. И после его выполнения - на экране Яндекс.Станции запустится летучая мышь.
Если не запустилось - тут я ничего не подскажу. Логи все есть на Станции, но доступа к ней (пока) нет.
Продолжение о том, какой командой запустить Intent напишу позже.