🏎️

Androidと自動車を接続してみよう!

2023/09/28に公開1

はじめに

Turing株式会社 UXチームでインターンをしている東京大学3年の勝見とエンジニアの佐々木(@kento_sasaki1) です。

UXチームでは、Androidを採用して独自のIVI (車載インフォテイメント) の開発を行なっています。本記事では、AOSP (Android Open Source Project) の枠組みで車両と接続するのに肝となるVHAL (Vehicle Hardware Abstraction Layer) とCar APIについて概説し、Androidと自動車を接続する方法について紹介します。なお、本記事はAOSPのソースコード (Android12.1.0 rivision11) を適宜参照しながらご覧ください。

概要図:Android Automotive OSは車載ECUとCANプロトコルで情報を送受信する
概要図:Android Automotive OSは車載ECUとCANプロトコルで情報を送受信する

Android Automotive OSのアーキテクチャ

Android Automotive OS (AAOS) は、自動車特有の要件や機能を追加したAndroidであり、一般的なスマートフォンやタブレット端末と同じAOSPの枠組みで開発が行われています。Android Automotive OSには下図の階層構造があり、VHAL, Car Service, Car APIと呼ばれるコンポーネントが含まれます。
Android Automotive OSのアーキテクチャ
Android Automotive OSのアーキテクチャ

VHALは車載ハードウェアの機能を抽象化し、車載ハードウェアからの情報をアプリケーションが解釈可能な形式に変換します。これにより、アプリケーションは特定のハードウェアの詳細を気にせずに、情報のやり取りを行うことができます。こうして抽象化された情報は、System Servicesとして提供されるCar APIを通じて、アプリケーションとやり取りします。すなわち、アプリケーションから見ると、BluetoothやGPSデバイスなど他のハードウェアと同様に車載ハードウェアを扱うことが出来ます。

では早速、どのように車載ハードウェアからアプリケーションに情報が伝達されるのか具体的に見ていきましょう。まずは、Androidと自動車の接続においてよく用いられるCANプロトコル、車両プロパティについて概説したのち、VHAL, Car APIについて深掘りしていきます。

CANプロトコル

自動車には、100個を超えるECU (Electronic Control Unit) という電子制御コンピュータが搭載されており、CAN (Controller Area Network), LIN (Local Interconnect Network), FlexRayなどの車載ネットワークを用いて通信が行われています。Android Automotive OSは、これら車載ネットワークを用いて自動車と通信することが出来ます。本記事では、自動車業界において標準であるCANプロトコルを用います。

CANは、主に車両情報のやり取りに用いられています。例えば、車速や加速度などの運動の情報、アクセル、ブレーキ、ハンドル、ギアなどの状態、シートベルトの着脱状況やドアの開閉状態などがCAN信号として流れています。CAN通信は、2本の通信線の電圧差をビットに見立てる通信方式で行われます。そして、CAN信号はフレーム構造を持ち、データ内容や送信ノードの識別、通信調停の優先順位を決定するID、送信データを含むデータフィールドなどが含まれます。
ECU間のCAN通信の様子
ECU間のCAN通信の様子(日経クロステック:車載ネットワークより引用)

車両プロパティ

Android Automotive OSでは車載ハードウェアからCAN信号を受け取り、アプリケーションが解釈できる形式の車両プロパティに変換します。車両プロパティは、Property ID, VehiclePropertyGroup, VehiclePropertyType, VehicleAreaなどの値(属性)を含み、.halというファイルに定義します。自動車メーカーは、(1)車両プロパティの定義、(2)CAN信号と車両プロパティの変換アルゴリズムを実装します。

車両プロパティの定義は、hardware/interfaces/automotive/vehicle/2.0/types.halに参照実装がありますので、適宜ご覧ください。以下では、車両プロパティの主な属性について紹介します。

  • Property ID
    Property IDは、各プロパティの識別子として使用され、0x0401のように表されます。この場合、0xがHex, 04がCategory, 01がnumberを意味します。types.halの参照実装では、GEARやParking Brakeに関するプロパティは、category : 04で表されています。また、categoryに属するプロパティに対して、numberがGEAR_SELECTION : 0x0400, CURRENT_GEAR : 0x0401, PARKING_BRAKE_ON : 0x0402のように慣習的に連番で付与されます。
    Property ID
    Property ID

  • VehiclePropertyGroup

    VehiclePropertyGroupは、PropertyがAndroid Automotive OSで定義されるプロパティのSystemProperty、もしくはAndroidのベンダー実装やデバイス固有の情報を持つプロパティのVendorPropertyなのかを識別します。

    hardware/interfaces/automotive/vehicle/types.hal
    enum VehiclePropertyGroup : int32_t {
        /**
         * Properties declared in AOSP must use this flag.
         */
        SYSTEM      = 0x10000000,
    
        /**
         * Properties declared by vendors must use this flag.
         */
        VENDOR      = 0x20000000,
    
        MASK        = 0xf0000000,
    };
    
  • VehiclePropertyType

    VehiclePropertyTypeは、Propertyの型を表します。例えば、GEARの値であれば、INT32の型を持ちます。

    hardware/interfaces/automotive/vehicle/types.hal
    /**
     * Enumerates supported data type for VehicleProperty.
     *
     * Used to create property ID in VehicleProperty enum.
     */
    enum VehiclePropertyType : int32_t {
        STRING          = 0x00100000,
        BOOLEAN         = 0x00200000,
        INT32           = 0x00400000,
        INT32_VEC       = 0x00410000,
        INT64           = 0x00500000,
        INT64_VEC       = 0x00510000,
        FLOAT           = 0x00600000,
        FLOAT_VEC       = 0x00610000,
        BYTES           = 0x00700000,
    };
    
  • VehicleArea

    VehicleAreaは、文字通り車両の様々な領域を示すものであり、各値は車両の特定の部分 (WINDOW, MIRROR, etc…) を表します。例えば、現在のエアコン設定温度はVehicleArea: SEAT、現在のギア情報はVehicleArea: GLOBALとして表されます。

    hardware/interfaces/automotive/vehicle/types.hal
    enum VehicleArea : int32_t {
        GLOBAL      = 0x01000000,
        /** WINDOW maps to enum VehicleAreaWindow */
        WINDOW      = 0x03000000,
        /** MIRROR maps to enum VehicleAreaMirror */
        MIRROR      = 0x04000000,
        /** SEAT maps to enum VehicleAreaSeat */
        SEAT        = 0x05000000,
        /** DOOR maps to enum VehicleAreaDoor */
        DOOR        = 0x06000000,
        /** WHEEL maps to enum VehicleAreaWheel */
        WHEEL       = 0x07000000,
    
        MASK        = 0x0f000000,
    };
    

これらの値の論理和により、車両プロパティは表現されます。例えば、現在のギアの状態を表すGEAR_SELECTIONは、以下のように定義され、GEAR_SELECTION = 289408000 (0x11400400)と値が決まります。

hardware/interfaces/automotive/vehicle/types.hal
GEAR_SELECTION = (
        0x0400
        | VehiclePropertyGroup:SYSTEM // 0x10000000
        | VehiclePropertyType:INT32 // 0x00400000
        | VehicleArea:GLOBAL), // 0x01000000

これまで、Androidと自動車を接続するために前提となるCANプロトコルと車両プロパティについて確認しました。次に、車載ハードウェアからCAN信号を受信し、車両プロパティに変換する仕組みについて見ていきましょう。

CAN信号と車両プロパティの変換

CAN HALでCAN信号を受信し、VHALで車両プロパティに変換する
CAN HALでCAN信号を受信し、VHALで車両プロパティに変換する

Android Automotive OSには、CANバスの制御と信号の送受信を行うCAN HALが含まれており、これを用いることでCAN通信を行うことが出来ます。CAN HALのソースコードは、hardware/interfaces/automotive/can/1.0/を参照ください。上図のように、VHALはCAN HALを通じてCANの送受信を行い、車両プロパティに変換します。CAN信号と車両プロパティの変換アルゴリズムは、対象とする自動車のモデルに合わせて実装が必要となります。

次にいよいよアプリケーションが車両プロパティを受け取る部分について見ていきます。

アプリケーションとVHAL間のやり取り

アプリケーションとVHAL間の全体像を下図に示します。VHALでCAN信号から変換した車両プロパティはCar Serviceが受け取ります。Car Serviceは文字通り自動車との通信を管轄するサービスであり、 System Servicesの1つです。Car Serviceでアプリケーションに車両プロパティを渡すためにはCarPropertyService.java, PropertyHalService.java, VehicleHal.java, HalClient.javaが主な役割を担っています。

アプリケーションとVHAL間の全体像
アプリケーションとVHAL間の全体像

VHALでは、 IVehicle.halに公開するメソッドをインターフェースとして定義しています。これには、車両プロパティの値の取得や設定、購読のメソッドが含まれています。Javaからは、バイトコード (.class拡張子のファイル) としてこのインターフェースにアクセスでき、Car Serviceの HalClient.javaで、VHALにアクセスしています。

Car Serviceは、Car APIともやり取りします。こちらでは、Car ServiceがAIDL (Android Interface Definition Language) を用いて、Car APIが利用できるインターフェースを実装しています。AIDLとはAndroidのプロセス間通信 (IPC) において広く用いられているインタフェース定義言語です。Car APIを呼び出すアプリケーションと Car Serviceは別プロセスとして動いているため、プロセス間通信を用いてやり取りします。車両プロパティのやり取りには、ICarProperty.aidlというインターフェースが定義されています。また、Car APIはアプリケーション側からJavaやKotlinのコードでimport android.carをすると簡単に利用できるクラスを提供しています。

このような階層を経て、車載デバイスからの信号がアプリケーションに伝わります。それでは最後に、実際に車のギアの変更をアプリケーションで表示するデモアプリを作ってみます。

デモ:自動車のギアの変更をアプリケーションで表示する

デモ概要図
デモ概要図(画像の出典は脚注[1]に記載)

車体側から流れてきたギアの変更をCar APIを用いてアプリケーション側で表示してみます。アプリケーションが動く様子は以下の動画をご覧ください。Raspberry Pi (自動車の代替) からギアの変更をキーボード入力で行い、Android Automotive OSでCAN信号を受信し、ギアの値を表示しています。このように、VHALとCar APIを用いることでAndroidと自動車を接続することができます。

あらかじめ、CAN信号を車体プロパティに変換する部分を、VHALで実装しておきます。
アプリケーションでは、次の準備が必要になります。

  • AndroidManifest.xmlに<uses-permission android:name="android.car.permission.CAR_POWERTRAIN" />を記述する。

    AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
        <!-- ... -->
      <uses-permission android:name="android.car.permission.CAR_POWERTRAIN" />
      <!-- ... -->
    </manifest>
    
  • build.gradleにuseLibrary 'android.car'を追加し、import android.carができるようにする。

    build.gradle
    android {
      // ...
      useLibrary 'android.car'
      // ...
    }
    

Kotlinでの実装例を示します。

MainActivity.kt
import android.car.Car
import android.car.Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT
import android.car.VehiclePropertyIds
import android.car.hardware.CarPropertyValue
import android.car.hardware.property.CarPropertyManager
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback
// ...

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		// ...
		val carPropertyManager =
			Car.createCar(
				this,
				null,
				CAR_WAIT_TIMEOUT_DO_NOT_WAIT
		) { car, ready ->
			// 状態が変化したら呼ばれる。準備ができたら ready = true になる。
		}.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
			
		carPropertyManager.registerCallback(object : CarPropertyEventCallback {
			override fun onChangeEvent(carPropertyValue: CarPropertyValue<*>) {
                	// ギアの変更があったときに呼ばれる。新しい値は、carPropertyValue.value として取得できる。
			}
						
			override fun onErrorEvent(propId: Int, zone: Int) {
                		// エラーが発生したときに呼ばれる。
			}
		}, VehiclePropertyIds.GEAR_SELECTION, CarPropertyManager.SENSOR_RATE_ONCHANGE)
        	// ...
	}
}

あらかじめ、carPropertyManager.registerCallbackを呼び出し、ギアの変更があった場合に登録したコールバックが呼ばれるようにします。

コード内で登録しているコールバックにより、車のギアが変わるとonChangeEventの中身が呼ばれるようになります。ここでは、VehiclePropertyIds.GEAR_SELECTIONを指定していますが、この値は289408000すなわち0x11400400です。これは、車両プロパティで定義した値と同一です。Car APIでもこのように車両プロパティが指定できます。

今回の実装では、ギアの値の読み取りを対象としましたが、車速やシートベルトを着用しているかなどの情報を同様に扱うことができます。さらに、アプリケーションから値を設定することもできます。例えば、Android Automotive OSでは、System UIからエアコンの温度を変更することができます。これはCar APIを通して、HVAC_TEMPERATURE_SETというプロパティを変更しています[2]。他にも、窓やドアの開閉などの設定も可能であり、多くの機能をIVIに集約することができます。

Android Automotive OSのUIでエアコンを設定する画面
Android Automotive OSのUIでエアコンを設定する画面

まとめ

本記事では、Androidと自動車を接続する方法を概説しました。CAN HAL, VHAL, Car Service, Car APIを通じて、車両の情報がアプリケーションに届く仕組みを確認しました。Androidの複数のレイヤーを縦断する様子が分かっていただけたと思います。

Turing UXチームでは、AndroidをベースにIVIシステムを開発しています。Turingが開発する車両でのユーザ体験をより良いものとするため、自動車向けアプリケーションを開発するAndroidエンジニアを募集しています。ご興味のある方は、Turing公式サイト採用情報をご覧ください。DMも解放していますので、本記事に関する疑問点や感想などは佐々木(@kento_sasaki1)まで気軽にお寄せください。また、UXチームに限らずTuringについてお話しできる場として「Turingカジュアル面談の部屋」もありますので、お気軽にご連絡ください。

関連記事

https://zenn.dev/turing_motors/articles/9c18325ffdfb57
https://zenn.dev/turing_motors/articles/da663f81d871da

脚注
  1. RaspBerry Pi 4B, USB-CANモジュール ↩︎

  2. 車両プロパティの変更は特別な権限が必要であり、特権アプリでないとできません。 ↩︎

Tech Blog - Turing

Discussion

seijiseiji

貴重な情報ありがとうございます。このような情報が他に見つからなかったので大変勉強になりました。
私は趣味でラズパイにAAOSを入れて、アプリからAAOSのCarAPIを使って、GPIO経由でラジコン制御するような製作を行っており、以下のような手順で実現できるのではと考えていますが、ご助言をいただけると大変ありがたいです。

①CANHALと同じようにGPIOHALを新たに定義する。GPIOHALでは、GPIOのPINを以下のpropertyに対応させる。
https://android.googlesource.com/platform/hardware/interfaces/+/main/automotive/vehicle/2.0/types.hal
②GPIOHALを既存のソフト(IVehicle.hal?)に組み込む

②について、GPIOHALをどのように組み込めばよいかわからず、既存の仕組みではCANHALはどこのファイルから呼ばれているかなど、教えていただきたいです。