Если вы думаете, что навигационная система в современном автомобиле — это просто красивое приложение, которое рисует синюю линию на карте и говорит «через 200 метров поверните направо», вы застряли в 2010 году.

В Android Automotive OS (AAOS) навигация часто становится одним из самых жирных и критически важных системных компонентов. Это «пространственный мозг» машины. Десятки других модулей постоянно дергают её за рукав с вопросами: «Где мы?», «Какое ограничение скорости?», «Сколько полос на дороге?», «В какой полосе мы сейчас едем?».

В этой статье я поделюсь опытом из своей практики разработки навигационной системы для крупного немецкого автопроизводителя. Мы разберем, как построить надежное межпроцессное взаимодействие (IPC) внутри автомобиля на примере передачи ADAS-атрибутов для японского модуля ETC 2.0.

Проблема: Японские дороги не прощают ошибок

Представьте ситуацию: машина едет по Токио. В Японии работает система ETC 2.0 (Electronic Toll Collection) — умные платные дороги. В отличие от старых систем, где вы останавливаетесь у шлагбаума, ETC 2.0 списывает деньги на лету, анализируя маршрут, пробки и даже конкретную полосу, по которой вы едете.

Аппаратный модуль ETC 2.0 вшит где-то под торпедой. Но сам по себе он «слеп». Ему нужна прецизионная гео-локация и данные ADAS (Advanced Driver Assistance Systems) — уклоны дороги, кривизна поворотов, точное количество полос.

Где взять эти данные? У нашей навигационной системы!

Но тут возникает проблема. Навигация — это приложение A (со своим процессом). Модуль ETC (или его сервис-адаптер в Android) — это приложение B (в другом процессе). Процессы в Android изолированы друг от друга как параноики в бункерах. Нам нужен надежный, быстрый и безопасный мост.

Добро пожаловать в мир AIDL (Android Interface Definition Language).

AIDL: Контракт, подписанный кровью

В мобильной разработке мы привыкли кидаться Intent-ами или использовать EventBus. В Automotive, когда речь идет о безопасности и деньгах (оплата дорог!), такие костыли не работают. Нужен жесткий контракт и строгая типизация.

С помощью AIDL мы описываем интерфейс, который навигационная система (Сервер) предоставляет наружу, а модуль ETC 2.0 (Клиент) может вызывать так, будто это локальный код.

Давайте напишем этот контракт.

Шаг 1: Описываем структуру данных (Parcelable)

Данные между процессами передаются через механизм Binder. Чтобы объект пролез через это «бутылочное горлышко», он должен реализовывать Parcelable.

Создадим файл AdasAttribute.aidl:

// AdasAttribute.aidl
package com.germanauto.nav.api;

// Объявляем, что у нас есть такой Parcelable класс
parcelable AdasAttribute;

И сам класс в Kotlin (в реальном проекте там десятки полей, но для примера упростим):

package com.germanauto.nav.api

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class AdasAttribute(
    val latitude: Double,
    val longitude: Double,
    val currentLane: Int,
    val speedLimit: Int,
    val isTollRoad: Boolean
) : Parcelable

Шаг 2: Создаем интерфейс (AIDL)

Теперь опишем, как ETC-модуль будет получать эти данные. Мы используем паттерн "Подписка" (Observer), так как данные обновляются десятки раз в секунду.

Создаем IAdasCallback.aidl (коллбек от навигации к клиенту):

package com.germanauto.nav.api;
import com.germanauto.nav.api.AdasAttribute;

oneway interface IAdasCallback {
    // oneway означает, что вызов асинхронный и не блокирует поток
    void onAdasDataUpdated(in AdasAttribute data);
}

И главный сервис IAdasProvider.aidl:

package com.germanauto.nav.api;
import com.germanauto.nav.api.IAdasCallback;

interface IAdasProvider {
    void registerCallback(IAdasCallback callback);
    void unregisterCallback(IAdasCallback callback);
}

Когда проект скомпилируется, Android сгенерирует Java-классы (Stub и Proxy), которые возьмут на себя всю грязную работу по маршаллизации данных в байты и обратно.

Шаг 3: Реализация на стороне Навигации (Сервер)

В нашем навигационном приложении мы создаем Service, который реализует этот сгенерированный интерфейс (Stub).

class AdasProviderService : Service() {

    // Потокобезопасный список клиентов (ETC модуль и другие)
    private val callbacks = RemoteCallbackList<IAdasCallback>()

    private val binder = object : IAdasProvider.Stub() {
        override fun registerCallback(callback: IAdasCallback) {
            callbacks.register(callback)
            Log.d("AdasProvider", "Client registered")
        }

        override fun unregisterCallback(callback: IAdasCallback) {
            callbacks.unregister(callback)
        }
    }

    override fun onBind(intent: Intent?): IBinder {
        return binder
    }

    // Эту функцию вызывает внутреннее ядро навигации каждый раз, 
    // когда позиция машины меняется
    fun broadcastAdasData(data: AdasAttribute) {
        val i = callbacks.beginBroadcast()
        for (index in 0 until i) {
            try {
                // Отправляем данные клиенту через IPC
                callbacks.getBroadcastItem(index).onAdasDataUpdated(data)
            } catch (e: RemoteException) {
                // Клиент умер в процессе передачи
                Log.e("AdasProvider", "Failed to send data", e)
            }
        }
        callbacks.finishBroadcast()
    }
}

Боль опыта: Используйте RemoteCallbackList. Он автоматически отписывает клиента, если процесс клиента (ETC) внезапно упал (крашнулся). Иначе вы получите утечку памяти и исключение DeadObjectException.

Шаг 4: Подключение из модуля ETC (Клиент)

Теперь мы — японский модуль оплаты. Нам нужно приконнектиться к немецкой навигации.

class EtcTollManager(private val context: Context) {

    private var adasProvider: IAdasProvider? = null

    // Наш слушатель
    private val adasCallback = object : IAdasCallback.Stub() {
        override fun onAdasDataUpdated(data: AdasAttribute) {
            Log.d("ETC2.0", "Received ADAS. Lane: ${data.currentLane}, Toll: ${data.isTollRoad}")
            // Списываем иены со счета водителя...
        }
    }

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            // Превращаем сырой IBinder в удобный интерфейс
            adasProvider = IAdasProvider.Stub.asInterface(service)
            
            try {
                adasProvider?.registerCallback(adasCallback)
            } catch (e: RemoteException) {
                Log.e("ETC2.0", "Nav service crashed!")
            }
        }

        override fun onServiceDisconnected(name: ComponentName) {
            adasProvider = null // Навигация упала или обновляется
        }
    }

    fun startListening() {
        val intent = Intent("com.germanauto.action.BIND_ADAS_PROVIDER")
        intent.setPackage("com.germanauto.nav")
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }
}

Суровая реальность: Транзакционный лимит

Всё выглядит красиво, пока вы не запустите это в продакшене. В один прекрасный день ваша навигация упадет с ошибкой: TransactionTooLargeException.

Почему? Механизм Binder (на котором работает AIDL) имеет жесткий лимит буфера — 1 мегабайт на ВСЕ активные транзакции ВСЕХ процессов в системе.

Если ваша навигация попытается передать огромный массив с геометрией перекрестка (тысячи точек) через AIDL 10 раз в секунду, буфер переполнится, и система убьет ваш процесс.

Как лечить?

  1. Передавать только изменения (дельту), а не весь объект целиком.

  2. Для тяжелых гео-данных (например, кусков карты) использовать MemoryFile (Shared Memory), а через AIDL передавать только файловый дескриптор (указатель). Но об этом трюке с ��азделяемой памятью мы поговорим в следующих статьях!

Итого

В Android Automotive ваше приложение редко живет в вакууме. Навигационная система — это яркий пример провайдера данных, от которого зависят критически важные системы автомобиля.

Грамотное проектирование AIDL-интерфейсов, понимание жизненного цикла процессов и умение работать с RemoteCallbackList — это база для любого, кто хочет писать надежный софт для автопрома.