Android Auto в Baidu CarLife: я подружил два протокола и что сломалось по дороге

—

от автора

Почти месяц назад я забрал Honda Vezel 2024 года для китайского рынка. В штатном ГУ оказалось только Baidu CarLife. Android Auto нет и не будет. Дилер ничего не прошил, да и вряд ли смог бы. Без активации Honda Connect, которую невозможно сделать за границами Китая, по факту это ГУ которое умеет проигрывать радио/usb/bluetooth музыку. Поискал решения: либо коробочка wizcar и т.п., либо на реддите один иракец продавал приложение. А раз он справился и адаптер это уже умеет, значит, справилюсь и я. Если бы лет 10-15 назад я бы декомпилировал руками apk, то сейчас расчехляем Claude Code и поехали.

Так появился Headswap — приложение на телефоне, которое для ГУ выглядит как телефон с CarLife, а для Android Auto на том же телефоне выглядит как головное устройство. Код открыт (GPL-3.0), репозиторий на GitHub.

Далее расскажу про то как всё усторенно и какие с какими проблемами столкнулся за три с половиной недели. Наивный я, пологал, что сделал только для хондоводов, пока через несколько дней не присоединились сотни машин 🙂

Часть 1. Два протокола

Android Auto и Baidu CarLife

В обоих протоколах видео кодирует телефон, ГУ только декодирует и показывает. Различается всё остальное.

Android Auto (AAP)

Baidu CarLife

Транспорт

TCP, кадр [chan][flags][len BE16] поверх TLS

USB AOA, bulk-пара, заголовок [chan u32][len u32]. По воздуху — шесть отдельных TCP-сокетов

Каналы

ГУ сама объявляет сервисы в ServiceDiscoveryResponse

шесть фиксированных: CMD, VIDEO, MEDIA, TTS, VR, TOUCH

Сообщения

protobuf

protobuf proto2

Рукопожатие

Version → TLS → AuthComplete → ServiceDiscovery → ChannelOpen

ГУ первой шлёт HU_PROTOCOL_VERSION, дальше MATCH → INFO → старт видео

Защита

ГУ обязана предъявить сертификат, подписанный Google Automotive Link

TLS нет; телефон подписывает ответ на HU_AUTHEN_REQUEST ключом Baidu

Контроль потока

ack на каждое медиа-сообщение, окно unacked

ack нет; живость держится видеоканалом

Звук

PCM 16 кГц моно (речь) и 48 кГц стерео (медиа)

PCM: музыка 48/2/16, TTS 16 кГц, микрофон 16 кГц

Тач

InputReport в пикселях кадра

TOUCH_ACTION в пикселях закодированного видео, до двух пальцев

У Android Auto есть недокументированный режим разработчика: «Запустить сервер головного устройства». После его запуска AA слушает 127.0.0.1:5277 и принимает подключение от любой программы, которая проходит рукопожатие головного устройства. Этим пользуются Desktop Head Unit от Google и открытые проекты вроде aasdk и headunit-revived. Сертификат головного устройства в этих проектах публичный, его когда-то выпустили для JVC Kenwood. Свой сертификат телефона Android Auto магнитоле предъявляет, но проверять его никто не обязан, и мы не проверяем.

Получается схема:

ГУ ⇄ CarLife (USB AOA или Wi-Fi) ⇄ Headswap ⇄ TCP 127.0.0.1:5277 ⇄ Android Auto

Разбор протокола CarLife

Протокол CarLife частично открыт: Baidu когда-то выложила исходники магнитольной стороны (apollo-DuerOS, 2015–2017) и обрезанную библиотеку телефона. Этого хватило на таблицы сообщений и каналов, но не на поведение. Протокол старый, а магнитолы в машинах 2024–2026 годов живут на CarLife 2.0 / 2.1 / 3.1, о которых в открытом коде ничего нет.

Поэтому эталоном поведения стал настоящий Baidu CarLife 8.6.7 из китайского магазина. Его разобрали jadx, а нативную libencryption.so дизассемблером. Символы в ней не срезаны, что сильно упростило жизнь.

Отдельно про авторизацию. На HU_AUTHEN_REQUEST официальное приложение отвечает RSA-подписью (PKCS#1) приватным ключом, который лежит прямо в APK. Мы делаем то же самое — тем же ключом. Другого способа убедить ГУ, что перед ней «настоящий» телефон, нет: ключ общий на все копии приложения у всех пользователей в мире, и ГУ проверяет именно его. Ключ лежит константой в Handshake.kt, открыто, как и весь остальной код. Прятать его смысла нет: он и так в каждой копии официального APK, а jadx находит его за несколько минут.

Очень быстро сложилось правило, которое потом записали в ADR: поведение Baidu 8.6.7 — эталон, любое отличие от него считается багом, пока не доказано обратное. Это правило окупилось раз пять.

Для отладки без походов в машину сделал два эмулятора на Python. hu.py (140 строк) подключается к серверу Android Auto на телефоне через adb forward и пишет видео в файл — проверить сторону AA отдельно от ГУ. carlife_hu.py (1000 строк, только stdlib) изображает магнитолу CarLife: Headswap подключается к нему через adb reverse, можно гонять паузы, обрывы каналов, беспроводной режим.

Видео: перекодировать или пробрасывать?

Android Auto отдаёт H.264, CarLife принимает H.264. Казалось бы, просто переложить NAL из одного протокола в другой… На Honda Vezel я сразу же столкнулся с чёрным экраном, отвалом после перехода из настроек ГУ или камеры заднего вида обратно в CarLife. Позже уже стало понятно, что декодеры разных ГУ капризны к профилю, уровню, размеру кадра, частоте ключевых кадров. Поэтому в первых версиях я сразу реализовал перекодирование: декодер → OpenGL → энкодер с настройками «как у Baidu» (Baseline, VBR, ключевой кадр раз в секунду, повтор кадра каждые 100 мс). Понятно, что такое перекодирование оказалось ощутимой нагрузкой для некоторых смартфоном. У кого-то сильно грелись батарейки, поэтому проц сразу уходил в тротлинг?.

В предстоящем обновлении 3.0 была решена эта проблема и прямой проброс стал настройкой по умолчанию, а перекодирование осталось для магнитол, которые иначе не умеют (как выяснилось — это все корейские марки со слабыми ГУ. У кого-то на Android 4.2.2 KitKat!!!). Но об этом далее.

Сам Headswap — около 27 тысяч строк Kotlin без AndroidX, Compose и protobuf-библиотек: свой минимальный protobuf, программные View. APK весит 3,5 МБ. Как шутят мои подписчики: “я не понимаю почему Headswap весит всего 3 МБ? любое приложение уже по полгигабайта блин”.

Часть 2. Пятнадцать проблем

Почти всё ниже диагностировано по логам пользователей. У меня одна машина. У них — сотни, и магнитолы в них от Pioneer, Mobis, Desay, LG и других поставщиков. Каждая реализует CarLife немного по-своему, привет китайцам-программистам.

1. Ровно 30 секунд до обрыва

Симптом. Honda CR-V 2026 пишет «устройство не авторизовано Baidu CarLife». У владельца Mazda CX-50 связь рвётся с ошибкой EIO ровно через 30 секунд после HU_AUTHEN_REQUEST. Пятнадцать сессий из пятнадцати.

Поиск. В логе видно, что ГУ присылает randomValue вида "2.0;20261012", а Headswap на него молчит: на моей Vezel HU_AUTHEN вообще не приходит, этого я вообще не реализовывал)) В открытых исходниках Baidu такого поведения тоже нет.

Причина. В libencryption.so нашлась функция для этого случая. Если в строке есть ;, ответ — не RSA-подпись, а md5hex("CARLIFE" + brand + model + sdk + вторая_часть).

Результат. Сессия 996 секунд вместо 30. Заодно выяснилось, что «обрывы раз в 40 секунд», на которые жаловались другие, были той же авторизацией.

Побочная находка. На этом же логе вылез второй баг: энкодер Kirin отдаёт SPS с байтами 42 C0 32, наша функция починки SPS на нём возвращала исходник без изменений, а следующий за ней first {} падал с NoSuchElementException. Падал в отдельной нити отправки, молча. Ноль кадров, никакой ошибки в UI.

2. «Кабель отваливается, когда ухожу в меню»

Симптом. VW, Skoda, Mazda по воздуху: стоит открыть штатное меню магнитолы, через несколько секунд связь пропадает. Пользователи меняли кабели.

Поиск. Когда ГУ показывает своё меню, она шлёт VIDEO_ENCODER_PAUSE и видео больше не ждёт. Через 3,6–6 секунд (у VW стабильно 5,6–5,8) она закрывает USB. Пустой VIDEO_DATA как keepalive не помогает, а на одной VW даже рвёт связь быстрее.

Причина. Официальный CarLife во время паузы раз в 100 мс шлёт VIDEO_HEARTBEAT (0x20002) — пустое сообщение специального типа. Без него ГУ считает телефон мёртвым.

Решение. Heartbeat сразу после паузы и каждые 100 мс, как у Baidu.

Урок, оплаченный дорого. Первый вариант keepalive в версии 1.1.6 был глобальным, и Headswap сразу же перестал работать на моей же Honda Vezel. В итоге теперь я всегда каждую сборку тестирую в своей машине, и ввёл правило: обход странностей конкретной магнитолы включается только флагом этой магнитолы. Магнитолу опознаём по STATISTIC_INFO.channel — восьмизначному коду поставщика прошивки (моя Vezel — 20832125). Флагов сейчас двенадцать, и это самая грязная часть кода.

3. Чёрный экран на Samsung

Симптом. Mazda CX-5, телефон Samsung Galaxy A55: связь есть, тач работает, экран чёрный.

Причина. Энкодер c2.exynos подписывает Baseline-поток странным SPS: constraint-флаги 0x00 и level_idc = 52. Уровень 5.2 для 720p — это заявка «мне нужен декодер для 4K». Декодер магнитолы в отказ.

Решение. Если level_idc > 41, переписываем SPS: ставим constraint_set0_flag и уровень 3.1 или 4.0. Картинка появилась. Теперь все NAL в сторону магнитолы проходят через починку SPS.

Похожий баг был со Snapdragon c2.qti на нестандартном разрешении 2240×976: там SPS уже был нормальный, функция возвращала копию, и мы снова упирались в first {} на пустом списке. Холодный старт чёрный, тёплый почему-то рабочий, 13 ошибок на 13 сессий.

4. ГУ перестаёт читать USB после одного пакета

Симптом. Mazda 3 для Китая: «Connecting 100%», потом «CarLife disconnected». ГУ принимает два кадра видео и встаёт.

Поиск. Разрешение ни при чём: 1280×720, 800×480 и 1280×480 ведут себя одинаково. Перепись SPS не помогла. Помог лог второго пользователя с той же прошивкой: у него кадры 7889 и 16 163 байта проходили, а на 16 518 байтах связь умирала. Порог где-то между ними. Подозрительно близко к 16 КиБ = 16 384.

Потом выяснилось, что дело не в видео: обложка альбома в MEDIA_INFO весом 24 КБ убивала связь точно так же. Любой пакет больше 16 КиБ — и USB-стек магнитолы замолкал навсегда.

Решение. Для этой магнитолы видео режется на куски не больше 15 000 байт, обложки ужимаются до 12 КБ, битрейт ограничен 1,5 Мбит/с. В итоге всем ГУ пакет не больше 16 КиБ.

Контрпример. На магнитоле LG Pivi (Range Rover Evoque для Китая) тот же порог, но другая реакция: после большого пакета она перестаёт забирать видео, но GPS от неё продолжает идти. Нарезку она не склеивает — «секунду картинка, потом кубики». Здесь кадр больше 15 000 байт выбрасывается целиком и Headswap ждёт следующего ключевого. Эта история пока открыта.

5. «Работает 5–15 секунд, и обрыв по кругу»

Симптом. VW ID.6, ID. UNYX, Mazda по воздуху: связь рвётся то через 5 секунд, то через 15. Опять кабеля…

Поиск. Взял последний отправленный перед обрывом NAL. Картина по 34 сессиям: EIO через 0,55–0,68 с после ключевого кадра (IDR) размером от 616 КБ. Кадры до 608 КБ проходят. При 8 Мбит/с и IDR раз в секунду ключевые кадры к 10–20-й секунде дорастают до 650–900 КБ, и ГУ падает. В открытом коде магнитольной стороны буфер кадра — 600 КиБ. Сошлось.

Решение. Потолок битрейта 4 Мбит/с. Если IDR вырос больше 450 КБ, потолок умножается на 0,75, но не ниже 1,5 Мбит/с. Потом добавили стартовый потолок 2 Мбит/с: первые 10–15 секунд, пока адаптация не набрала статистику, сессии всё ещё рвались.

Результат. 17 минут без обрывов, страховка сама опустила битрейт до 1,7 Мбит/с.

Ловушка. Первым делом хотелось сделать белый список «VW с такими-то именами Bluetooth». Не работает: у каждой машины свой порог, а владелец VW с другим Bluetooth-именем на том же коде поставщика спокойно держит 8 Мбит/с. Следующий шаг — самообучение: запомнить размер IDR, после которого ГУ упала, и больше не подходить к нему.

6. Mobis: ГУ, которая хочет наоборот

Симптом. Hyundai Tucson с магнитолой Mobis рвёт каждую сессию за 6–9 секунд. После первого кадра ГУ раз в секунду четыре раза шлёт VIDEO_ENCODER_START, потом EIO. Официальный CarLife на этой машине работает.

Тупики. Широкий режим 1920×720, проброс без перекодирования, молчание на авторизацию, ответ на Bluetooth-опознание — обрывы те же.

Решение. Взяли декомпилят Baidu и сравнили построчно, что он делает иначе. Нашлось четыре вещи:

  • заголовок и тело сообщения уходят одним write() (Mazda из истории 4 хотела обратного — нарезки);

  • если видео нет 200 мс, уходит пустой VIDEO_DATA;

  • пакеты микрофона фиксированного размера;

  • перекодирование в 20 fps с потолком IDR 300 КБ.

Всё это включено флагом магнитолы Mobis.

Результат. Сессии 97–276 секунд вместо 8, потом поездка 25 минут без обрыва.

Хвост. После этого картинка «уехала вверх». Перекодировщик резал центр кадра, а Android Auto клал поля вниз. Два раунда: поправили геометрию, потом выяснилось, что в нашем GL-проходе ось v растёт вверх, а не вниз. На третьей тестовой сборке картинка встала на место.

7. Музыка, которая душит видео

Симптом. Hyundai Elantra (CN7 — самые слабые ГУ): музыка «тянется», через минуту ГУ перестаёт забирать видео (скорость чтения падает с 600 до 30 КиБ/с), через две минуты обрыв.

Причина. Эта ГУ не справляется с музыкой 48 кГц по USB одновременно с видео. Что именно у неё внутри захлёбывается, снаружи не видно, но связь звука и видео в логах однозначная.

Решение. Введена новая настройка «Частоты музыки» и ресемплинг в 32 кГц для этой магнитолы. «С музыкой всё супер». Попутно попробовали IDR раз в 4 секунды для её же видео — не помогло.

8. Сервер на 5277: хрупкий и одноразовый

Симптом. «Android Auto отключился, перезапускаю сервер головного устройства» — по кругу. На VW Tiguan AA принимает подключение и сразу закрывает: 65 сессий из 75 не дошли до рукопожатия.

Поиск. Тогда Headswap сам перезапускал сервер AA через службу специальных возможностей: открывал меню разработчика и жал «Остановить» и «Включить». На каждый ранний обрыв — перезапуск, и так до бесконечности. Первый фикс: перезапуск только после реально установленной сессии, backoff 3 → 30 с.

Потом выяснилось, что сервер на 5277 хрупкий в принципе. Голый connect/close его вешает: порт принимает, VERSION_RESPONSE не приходит, а после примерно пятидесяти таких подключений начинаются таймауты. Четыре nc -z подряд — и сервер приходится перезапускать руками. Короткая проба «рукопожатие → сразу ByeBye» тоже вредит: следующее подключение получает 27 секунд ECONNREFUSED.

На стенде проверили гипотезы по одной. Вредна не короткость сессии, а закрытие посреди настройки. Короткая, но полная сессия (каналы открыты, вежливый ByeBye) проходит шесть раз из шести.

Решение. Проба = полноценная сессия с фокусом NATIVE и ByeBye через секунду после AuthComplete. А службу спецвозможностей я удалил насовсем, т.к. некоторые пользователи не хотели выдавать приложению такие права, на Android 13+ это ещё и квест с «разрешить ограниченные настройки». Теперь сервер включается руками один раз и больше не гасится: штатный ByeBye он переживает, за поездку пять «стопов» и два EIO — и он жив. У меня неделю может работать и я сяду подключу заблокированный телефон и всё работает. У кого-то сам Андроид гасит и Headswap и AA при нехватке памяти. Тут ничего не поделаешь. “Замочек” для приложения вроде помогает, но это не точно.

9. Honda молча сбрасывает декодер

Это главная история, без неё бы прямой проброс я бы вообще не стал реализовывать.

Симптом. Vezel и Civic 11 в режиме проброса: после камеры заднего хода или штатного меню — чёрный экран. Тач уходит, Android Auto жив, картинки нет.

Поиск. При включении камеры ГУ не шлёт ни PAUSE, ни START, ни переключения модулей — никакого сигнала. USB читает без задержек. Но декодер внутри она сбрасывает и дальше ждёт ключевой кадр. Android Auto в режиме проброса шлёт IDR раз в минуту, а на статичной картинке при 3 fps — раз в десять минут.

Тупики. Восемь тестовых сборок мимо. Мягкий запрос ключевого кадра у AA — ноль IDR из девяти попыток за 1,5 с. Полный цикл фокуса NATIVE → PROJECTED даёт IDR через 850–900 мс, но с мерцанием и потерей нажатий, а по таймеру раз в 5 секунд пользоваться невозможно. Передача через car-data подвешивала Vezel.

Решение. Нашлись несколько патентов про встраивание своего ключегого кадра в видео поток на лету. Взял за основу идею и теперь ключевой кадр делает сам телефон. Headswap параллельно декодирует поток Android Auto и раз в 2 секунды собирает из последней картинки собственный IDR, который вставляет в поток. Бит-в-бит без потерь он не влезает: 0,9–2,4 МБ при буфере магнитолы около 560 КБ. Поэтому кадр кодируется макроблоками I16x16 со сжатием: в первой рабочей сборке QP 18–20 давал 140–180 КБ, сейчас QP подбирается в пределах 18–40 и кадр кодируется четырьмя слайсами параллельно. Кадр чуть мыльнее, но через 2 секунды его заменят обычные кадры AA.

Побочный эффект, который вылез на след день. Наш IDR добавлялся в поток лишним кадром. ГУ показывает кадры с постоянной частотой, поэтому каждый лишний кадр — плюс одна позиция в её очереди. За поездку задержка отклика на тач уплывала до 5–6 секунд, а после заднего хода становилась мгновенной (очередь сбрасывалась). Если бы я не запомнил, что делал с телефоном в этот момент, искал бы причину очень долго. Решение — «выброс P+1»: наш IDR заменяет два кадра Android Auto, а не добавляется к ним.

Результат. Vezel, 42 минуты по кабелю: задержка стабильно около секунды, выброшено около 970 кадров, картинка после заднего хода возвращается. Потом подтвердили владельцы Mazda и Civic.

10. Кроп без перекодирования: правим SPS

Широкие экраны (1920×720, 2240×976) Android Auto сам не умеет: он рисует 16:9, и сверху и снизу остаются поля. Раньше поля резал перекодировщик. Участник чата предложил: в SPS есть поля frame_crop_*, пусть режет декодер магнитолы, без нагрева и задержки.

Проверка. Наш переписчик SPS сверили с ffmpeg -bsf:v h264_metadata на четырёх потоках. На одном из них 268 кадров после кропа совпали по md5.

Сюрприз. Vezel кроп снизу и справа применяет, а смещение сверху игнорирует. Интерфейс съезжает, низ срезан, тач мимо.

Решение. Попросить Android Auto рисовать интерфейс в левом верхнем углу, а поля класть справа и снизу. Для этого в VideoConfiguration есть поле 11, ui_config.margins, найденное декомпиляцией AA. Тогда SPS режет только справа и снизу. Оказалось такое умеют все магнитолы.

Контрнаходка. Центральный кроп ломал тач: у одного VW нажатие попадало на 122 пикселя выше, потому что Android Auto на его OnePlus игнорирует устаревший вариант margins. Переключатель убрали, кроп всегда в одном варианте.

Побочно. Заглушка «нет картинки» другого размера давала на Mazda «кашу из белых точек»: смену размера кадра посреди потока ГУ не переживает. Теперь заглушку кодируем в той же геометрии, что и поток AA.

Результат. Кроп включён всегда с 30 октября, подтверждён на пяти разных ГУ.

11. Инверсия замков в перекодировщике

Симптом. При перекодировании видео периодически подвисает на доли секунды.

Причина. Цепочка энкодер → GL → декодер → dequeueInputBuffer вся крутилась под монитором Headswap. Туда же попал createDecoder, который занимает 50–300 мс, а на 1080p — 740. Плюс колбэки кодеков брали замки в другом порядке.

Решение. Кормление декодера вынесено в отдельную нить с очередью на 16 кадров (FeedQueue).

Результат. Vezel, 27 минут: потерь ноль, очередь максимум 2 кадра, медиана декодера 18 мс, энкодера 31 мс. Худший случай — пересоздание декодера на 1080p, очередь доросла до девяти. Лимит в 16 кадров ни разу не переполнился.

Идея этой правки пришла из внешнего пулл-реквеста от человека, который форкнул проект. Сам PR мы не взяли, переделали по-своему, но из него же влили исправления утечек кодеков, EGL и микрофона.

12. Mazda по воздуху: переподключение, которое рвало само себя

Симптом. Mazda с магнитолой Desay по Wi-Fi: после заднего хода связь пропадает и возвращается только после сна машины.

Поиск в три слоя.

  1. Во время паузы ГУ через 24–28 секунд разбирает группу Wi-Fi Direct, а Bluetooth RFCOMM остаётся живым. Новые параметры сети (TARGET_INFO) она присылает только при новом Bluetooth-подключении. Сделали как Baidu — сами шлём TARGET_INFO_REQUEST.

  2. В логе 35 попыток входа подряд, ни одной удачной. Каждый ответ TARGET_INFO перезапускал процедуру входа в группу и убивал предыдущее приглашение. Фикс: одинаковый TARGET_INFO моложе 30 секунд игнорируется.

  3. На Samsung S23 с Android 17 ГУ поднимает Wi-Fi Direct только на новом Bluetooth-подключении. Поэтому шаг переподключения теперь рвёт и заново поднимает RFCOMM.

Результат. «Теперь всё работает» от юзера.

13. Подсказки навигации на приборке Honda и формула floor(L/2) − 1

Симптом. CarLife умеет выводить подсказки навигатора на приборную панель. На Honda названия улиц на приборке пустые или обрезанные, или вообще пропадали.

Метод. Сначала я и другой разраб пробовали что-то поправить и покататься. В какой-то момент меня осенило: вытягиваем все названия улиц из всех 800+ логов, эмулируем навигацию и шлём каждые 5 секунд очередную строку. Параллельно я запускал диктофон и проговоривал что вижу. Далее агент сравнивал что и когда не отображалось.

Находка. Прошивка берёт строку как C-строку (до первого NUL), получает её длину S в байтах UTF-8, а потом считает эти байты, как будто это UTF-16. Видно floor(S/2) − 1 символов: A1234567 (8 байт) превращается в A12, «Мира» (8 байт) в «Мир». Латиница теряет половину, кириллица, где буква и так два байта, — один символ. Выше 31 байта начинается мусор: S = 32 даёт пустоту, 33 — один символ, 34 — два. Кириллица от 30 байт отбрасывается целиком, и на приборке остаётся предыдущее название. Иконки манёвров приборка рисует свои по коду, наши картинки ей не нужны.

Решение. Раз прошивка срезает половину длины, длину надо добить. NUL для этого не годится — на нём строка кончается. А хвостовые пробелы работают: «Мира» + 2 пробела показывается целиком, 14 латинских символов, добитых пробелами до 30 байт, тоже. Итоговый рецепт для Honda: латиница до 14 символов, кириллица до 12 букв, пробелами добить до 2·(символов + 1) байт, но не больше 31. В итоге все Honda начали правильно показывать подсказки навигации.

14. Курица и яйцо с навигацией

Симптом. VW ID.4: подсказки навигатора на приборке появляются, только если запустить маршрут в Google Maps. В других навигаторах — никогда.

Причина. ГУ подписывается на навигационные события (SUBSCRIBE_START) только после того, как телефон объявит MODULE_STATUS NAVI=1. А Headswap ставил NAVI=1, когда получал подписку или перед первой голосовой подсказкой. Google Maps говорит сразу на старте маршрута, остальные молчат — и ждали друг друга вечно.

Решение. NAVI=1 на старте маршрута без ожидания подписки.

Там же, в логах ID.4: обрывы по воздуху были от перегрева телефона — запас по температуре 0,87 через 22 секунды, критический уровень через три минуты. Скорее всего, беспроводная зарядка в машине. Это уже не про код.

15. Регрессия нагрева, которой не было

Симптом. «Версия 2.5.0 греет телефон, а 2.4.4 с теми же настройками — нет». Два лога, всё выглядит убедительно.

Разбор. Код перекодирования между версиями не менялся. Логи несопоставимы: 2.4.4 снят на статичном экране (вход Android Auto 4,4 fps, битрейт 2 Мбит/с), 2.5.0 — на живой картинке (28,8 fps, 4 Мбит/с, телефон уже на ступени WARM). Попутно нашёл мелочь: на статике повторы кадров в GL дают на выходе 18–21 fps при цели 15.

Итог. Регрессии нет. Есть троттлинг по температуре, который уже был: на WARM частота не выше 20 fps и битрейт ×0,75, на HOT — 15 fps и ×0,5, возврат по ступеням через 60 секунд. Урок: A/B по логам пользователей бессмысленен без одинакового сценария.

Открытая загадка

Весь процесс на телефоне иногда встаёт на 1,0–1,5 секунды, бывает строго раз в 10,6 секунды. Звук проваливается, ГУ рвёт и Bluetooth, и USB. Видели на Mazda и на Civic с телефоном realme. Это не наш IDR: такие стопы были и в версиях до него. Гипотезы — синхронный логгер на ~800 строк в секунду, сборщик мусора, троттлинг вендора. Причину не нашёл: обходной совет «звук по Bluetooth» работает. Если узнаёте симптом — напишите.

Часть 3. Тупики

То, что не сработало или от чего отказался сознательно.

  • Подделать статус движения, чтобы видео в сторонних AA-приложениях не блокировалось на ходу. Рекомендую смотреть на дорогу во время движения.

  • Запросить данные автомобиля (топливо, передача) через CAR_DATA_START. На Vezel через 2–4 секунды ГУ перестаёт читать USB. Три варианта, все с тем же итогом. Baidu шлёт этот запрос только в ответ на запрос магнитолы и только на старших версиях протокола.

  • 2560×1440. Android Auto на OnePlus закрывает сессию через 3 секунды без единого кадра, 15 попыток. Режим удалили, вместо него 1920×836 — ГУ растягивает сама. Т.е. в АА вроде заявлено 2к, но телефоны его не тянут.

  • IDR без потерь не влезает в буфер магнитолы (0,9–2,4 МБ против 560 КБ). Отсюда сжатый I16x16 в истории 9.

  • Беспроводной CarLife на Vezel. Пассивная проба: четыре RFCOMM-сервера, SDP, Wi-Fi Direct, UDP-маяк. Беспровода в моей машине нет.

  • J-Link в Jetta VS5 оказался не CarLife, а WeLink: свой протокол с магией WL и JSON. Стек не переиспользовать, не поддерживаем. Как и hicar от Huawei и прочие.

  • Эмулятор магнитолы в Android-эмуляторе. Поднял китайскую ГУ с CarLife 4.0.0 в AVD через WLAN-режим, Android Auto через Headswap заработал. Но эмулятор так тормозит, что отлаживать видео на нём бессмысленно.

  • Bluetooth-музыка параллельно с CarLife на Honda CR-V. A2DP рвётся через секунду после старта при любом варианте опознания. ГУ считает их взаимоисключающими.

Часть 4. Как я это делаю со своей Лили

Headswap пишу только я. За редким исключением увижу PR от кого-то. Код пишет Claude Code: из 345 коммитов в main (без мерджей) 344 с пометкой Co-Authored-By: Claude. Я ставлю задачи, принимаю решения, читаю диффы и проверяю на своей машине. Это не «ИИ всё сделал сам» и не «я всё сделал сам». Ниже о том, как это устроено, потому что без конвейера проект утонул бы в первую же неделю.

Масштаб входящего потока

После первых постов на автофорумах в Telegram-канал пришли владельцы. Ну как пришли, набежали. За три недели — почти 8,5 тысячи сообщений, пик — 806 в день. Там вперемешку «у меня Mazda, не работает», вопросы про установку APK, логи поездок по 4 МБ, фотографии экранов с иероглифами и видео приборки. Вручную это не разобрать, даже если не работать и не спать. Причём это же пет-проект, помимо основной работы)

Дежурный агент и карточки

Одна сессия дежурит по чату. Если бота тегнуть, то не ответит. Дежурная ведёт канбан доску:

  • каждое сообщение о проблеме превращается в карточку hNNN с машиной, магнитолой, телефоном, версией, типом подключения и тегами (обрывы, звук, чёрный экран, навигация…);

  • сообщения от одного человека собираются в существующую карточку;

  • пришёл лог — агент проверяет только версию в нём. Если она старая, отвечает в чат шаблоном «обновитесь», если актуальная — шлёт мне одну строку в личку;

  • на типовые вопросы («Play Защита не даёт установить», «2ГИС нет в Android Auto», «громкость навигатора») агент отвечает одним из 18 шаблонов, которые я одобрил заранее. Не уверен, подходит ли шаблон, — не отвечает.

За три недели — 639 карточек, 186 из них с логами, 619 закрыты. Больше всего — Volkswagen (173), Mazda (136), Honda (119).

Карточки видны на канбан-доске благодаря mod-ам — небольшой мод, который читает тот же JSON. Статусы «ждёт меня», «ждёт пользователя», «готово». У карточки, которую разбирает отдельная сессия, на доске есть кнопка перехода в неё, чтобы это всё не терять и быстро находить сессию/сообщение юзера.

Разбор карточки

Каждая нетривиальная карточка — отдельная сессия в своём git worktree с веткой claude/.... В основном checkout параллельно работают другие сессии, поэтому туда не коммитит никто. Сессия читает лог, при необходимости собирает тестовую сборку hNNNtestK и выкладывает её в GitHub Releases как pre-release — приложение такие не предлагает в автообновлении, их ставит только тот, кому дали ссылку. Ссылку в чат отправляет дежурный агент, и только после моего прямого «да». Пробовал “на автомате”, начинается сущий ад, так что пока ИИ до этого не дорос.

Самая длинная карточка дошла до h295test8 — восемь тестовых сборок одному человеку (история 9). Без владельцев, которые ставят сырые сборки и катаются с ними ради лога, половины этой статьи не было бы.

Логи без adb

Просить пользователя запустить adb logcat бесполезно: из сотни владельцев это сделали двое (и то после моей подробной инструкции). Поэтому всё, что нужно для диагностики, приложение пишет в свой лог, а кнопка «Отправить отчёт» собирает ZIP. Правило: если данных в логе не хватило — добавляем логирование в приложение, а не просим человека о ручных шагах. Формат строк лога считается контрактом, маркеры нельзя переименовывать — на них держится диагностика.

Сводку поездки делает logdigest.py: сессии, обрывы, fps, звук, навигация. Причину обрыва — logdiag.py: правила лежат данными в JSON, каждое с сигнатурой в логе, советом и версией, в которой исправлено. Тот же JSON лежит в приложении, и оно само показывает «почему не подключается». На 339 накопленных логах диагност нашёл причину или «всё работало» в 338. Python- и Kotlin-реализации совпали по тройке вероятных причин на 340 логах. Иногда он ошибается: однажды сказал «сбоев нет» там, где сигнатуры просто не было в правилах.

База магнитол

Из карточек выросла база кейсов: ГУ опознаётся по коду поставщика, к коду привязаны симптомы, флаги и история. Перед разбором нового лога агент сначала ищет по ней. Чаще всего оказывается, что такая ГУ уже была, с другим именем машины.

ADR и ошибки процесса

Архитектурные решения (их 18) записаны ADR коротко: проблема, решение, почему так. Писали их задним числом, после ревью — зато теперь новая сессия, которая видит странный флаг под VW, не выпиливает его как мусор.

Ошибки процесса тоже были. Сертификаты для AA лежат вне git, и в новый worktree они не попадали. Сборка проходила без них молча, приложение не могло подключиться к Android Auto. Так ушли две тестовые сборки, а потом и релиз 2.4.2 — пришлось выпускать хотфикс. Теперь gradle сам подтягивает сертификаты, а release-сборка без них падает. Правило простое: наступил дважды — автоматизируй.

Сколько это заняло

Первая живая сборка 7 октября через два дня как получил машину. Первый коммит — 8 октября, сейчас 1 ноября. 504 коммита, 23 релиза и pre-release, 27 тысяч строк Kotlin, 143 тестовых файла. Подтверждено около 40 моделей машин, от Honda Vezel до Mercedes E-Class для Европы. Релизные файлы на GitHub скачали 4796 раз, это вместе с автообновлениями, не уникальные люди.

Что дальше

Ядро Headswap за три недели обросло флагами магнитол. После 3.0 его ждёт рефакторинг: иммутабельный профиль магнитолы на сессию вместо двенадцати разбросанных флагов, выбор «проброс или перекодирование» в одном месте вместо четырёх. Потолок размера ключевого кадра должен стать самообучаемым (история 5).

Если у вас машина для Китая с Baidu CarLife и Android-телефон — репозиторий, там же ссылки на сайт и чат.

Если вы писали под CarLife или знаете, почему телефон встаёт на полторы секунды раз в 10,6 секунды, — буду рад в комментариях.

ссылка на оригинал статьи https://habr.com/ru/articles/1104086/