Tuesday, June 27, 2023

Android CarPropertyManager: 車両データ連携の核心

現代の自動車は、単なる移動手段から、インターネットに接続された高度なコンピューティングデバイスへと進化しています。この進化の中心にあるのが、Android Automotive OS (AAOS) です。AAOSは、Googleが開発した車載インフォテインメント(IVI)システム向けのオペレーティングシステムであり、開発者はAndroidの強力なエコシステムを活用して、リッチで統合された車載アプリケーションを構築できます。しかし、自動車固有の機能、例えばエアコンの温度設定、車速の取得、燃料残量の確認などをアプリケーションから安全かつ標準化された方法で操作するには、特別な仕組みが必要です。ここで登場するのが CarPropertyManager です。

CarPropertyManager は、Android Car API の中核をなすクラスであり、アプリケーションと車両のハードウェア(ECUなど)との間の橋渡し役を担います。これにより、開発者は車両のプロパティ(特性や状態)を読み取ったり、設定を変更したりすることができます。この記事では、CarPropertyManager のアーキテクチャから基本的な使い方、リアルタイムデータの処理、そして高度なベストプラクティスまでを体系的に解説し、AAOSアプリケーション開発の可能性を最大限に引き出すための知識を提供します。

第1章: CarPropertyManagerのアーキテクチャと基本概念

CarPropertyManager の具体的な使い方に入る前に、それがAAOSのどの部分に位置し、どのように機能するのか、その全体像を理解することが重要です。この理解は、より堅牢で効率的なアプリケーションを設計するための基礎となります。

1.1. AAOSにおけるCarPropertyManagerの役割

Android Automotive OSのアーキテクチャは、階層構造になっています。最上層にはユーザーが直接触れるアプリケーションがあり、最下層には車両の物理的なコンポーネントを制御するECU(Electronic Control Unit)やCAN(Controller Area Network)バスが存在します。CarPropertyManager は、これらの層を繋ぐ重要な役割を果たします。

  1. アプリケーション層 (Application Layer): 開発者が作成するメディアアプリ、ナビゲーションアプリ、車両制御アプリなどがこの層に属します。これらのアプリは、Car API を通じて車両機能にアクセスします。
  2. Car API フレームワーク (Car API Framework): CarPropertyManager, CarSensorManager, CarPowerManager など、アプリケーションに公開されるAPI群です。これにより、開発者はハードウェアの複雑さを意識することなく、標準化されたインターフェースで車両を操作できます。
  3. Car Service: Androidシステム内で動作するコアサービスです。Car APIからのリクエストを受け取り、パーミッションをチェックし、後述するVehicle HALとの通信を管理します。
  4. Vehicle HAL (Hardware Abstraction Layer): 自動車メーカー(OEM)が実装する層です。Car Serviceからの抽象的なリクエスト(例:「HVAC温度を22度に設定」)を、特定の車種のCANバス信号のような具体的なハードウェアコマンドに変換します。このHALの存在により、Androidフレームワークは特定のハードウェアに依存せず、多様な車種に対応できます。
  5. 物理層 (Physical Layer): CANバス、ECU、各種センサーやアクチュエーターなどの物理的なハードウェアです。

つまり、アプリケーションが CarPropertyManager を使って「現在の車速を取得する」というリクエストを出すと、そのリクエストは Car Service → Vehicle HAL → CANバスへと伝わり、ECUから取得された実際の車速データが逆の経路を辿ってアプリケーションに返されるのです。

1.2. CarPropertyManagerインスタンスの取得と接続管理

CarPropertyManager を使用するには、まず Car オブジェクトへの接続を確立する必要があります。これは非同期のプロセスであり、車両サービスへの接続が完了するまで待機する必要があります。ライフサイクルを意識した正しい接続管理は、アプリケーションの安定性に不可欠です。


import android.car.Car;
import android.car.VehiclePropertyIds;
import android.car.hardware.property.CarPropertyManager;
import android.content.Context;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "CarPropertyExample";
    private Car mCar;
    private CarPropertyManager mCarPropertyManager;

    private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener =
            new Car.CarServiceLifecycleListener() {
                @Override
                public void onLifecycleChanged(Car car, boolean ready) {
                    if (ready) {
                        Log.d(TAG, "Car service connected");
                        mCarPropertyManager = (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE);
                        // 接続が確立された後に、プロパティの読み書きやリスナーの登録を行う
                        readCurrentSpeed();
                    } else {
                        Log.d(TAG, "Car service disconnected");
                        mCarPropertyManager = null;
                    }
                }
            };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Carサービスへの接続を開始
        mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
    }

    @Override
    protected void onStop() {
        super.onStop();
        // Carサービスから切断
        if (mCar != null) {
            mCar.disconnect();
        }
    }
    
    private void readCurrentSpeed() {
        // このメソッドの実装は後の章で解説
    }
}

上記の例では、onStart()Car.createCar() を呼び出して非同期に接続を開始し、CarServiceLifecycleListener で接続完了の通知を受け取ります。接続が完了した時点で CarPropertyManager のインスタンスを取得しています。そして、onStop()mCar.disconnect() を呼び出してリソースを解放します。このライフサイクルに合わせた接続・切断処理は、メモリリークや意図しない動作を防ぐための基本です。

1.3. 車両プロパティの構造と分類

CarPropertyManager が扱う「プロパティ」は、単なるキーと値のペアではありません。各プロパティは、以下の要素によって定義されます。

  • プロパティID (Property ID): 各プロパティを一位に識別するための整数値です。VehiclePropertyIdsクラスにPERF_VEHICLE_SPEED(車速)、INFO_FUEL_LEVEL(燃料残量)、HVAC_TEMPERATURE_SET(HVAC設定温度)など、多くの定数が定義されています。
  • エリアID (Area ID): 車両内のどの領域に対応するプロパティかを示します。例えば、HVAC_TEMPERATURE_SET は運転席側と助手席側で異なる値を持ち得ます。この場合、VehicleAreaSeat.SEAT_ROW_1_LEFTVehicleAreaSeat.SEAT_ROW_1_RIGHT といったエリアIDで区別されます。単一の値しか持たないプロパティ(例:車速)は、グローバルなエリアIDを持ちます。
  • データ型 (Data Type): プロパティが持つ値の型です。Float, Integer, Boolean, String, byte[] など、様々な型が存在します。
  • アクセス権 (Access): プロパティが読み取り専用(Read-only)、書き込み専用(Write-only)、または読み書き可能(Read-Write)かを示します。
  • 変更モード (Change Mode): プロパティの値がいつ更新されるかを示します。ON_CHANGE(値が変化した時のみ通知)と CONTINUOUS(定期的に通知)の2種類があります。

これらのプロパティは、その性質からさらに以下のように分類できます。

  • 静的プロパティ (Static Properties): 車両の製造元、モデル名、燃料の種類など、一度設定されたら変化しない情報です。通常、特別なパーミッションなしで読み取ることができます。(例: INFO_MAKE, INFO_MODEL
  • 動的プロパティ (Dynamic Properties): 車速、エンジン回転数、現在のギアなど、運転状況に応じて変化する情報です。これらのプロパティを読み書きするには、AndroidManifest.xml で適切なパーミッション(例: android.car.permission.CAR_SPEED)を宣言する必要があります。

第2章: 車両プロパティの読み取りと書き込み

CarPropertyManager のインスタンスを取得し、プロパティの基本構造を理解したところで、次はいよいよ具体的なプロパティの読み書き操作を見ていきましょう。操作は同期的(即座に結果を要求する)と非同期的(結果を後で受け取る)の両方で行うことができます。

2.1. プロパティの同期的な読み取り

特定のプロパティの現在の値を即座に取得したい場合、getProperty() メソッドを使用します。このメソッドは CarPropertyValue オブジェクトを返します。このオブジェクトには、値だけでなく、タイムスタンプやステータス情報も含まれており、データの信頼性を判断する上で重要です。


// このメソッドは、Carサービス接続後に呼び出されることを想定
private void readFuelLevel() {
    if (mCarPropertyManager == null) {
        Log.e(TAG, "CarPropertyManager is not available");
        return;
    }

    try {
        // INFO_FUEL_LEVELはFloat型、エリアIDはグローバルのため0
        CarPropertyValue<Float> fuelLevelProp = mCarPropertyManager.getProperty(
                VehiclePropertyIds.INFO_FUEL_LEVEL, 0);

        if (fuelLevelProp != null && fuelLevelProp.getStatus() == CarPropertyValue.STATUS_AVAILABLE) {
            long timestamp = fuelLevelProp.getTimestamp();
            Float fuelLevel = fuelLevelProp.getValue();
            Log.d(TAG, "Fuel Level: " + fuelLevel + "% at " + timestamp);
            // ここでUIを更新するなどの処理を行う
            // updateFuelUi(fuelLevel);
        } else {
            Log.w(TAG, "Fuel Level property is not available.");
        }
    } catch (SecurityException e) {
        Log.e(TAG, "Lacked permission to read fuel level.", e);
    } catch (IllegalArgumentException e) {
        Log.e(TAG, "Invalid property ID or area ID.", e);
    }
}

getProperty() を呼び出す際には、適切なパーミッション(この例では android.car.permission.CAR_INFO)が AndroidManifest.xml に記述されている必要があります。パーミッションが不足している場合、SecurityException がスローされます。

CarPropertyValue オブジェクトの getStatus() メソッドは特に重要です。

  • STATUS_AVAILABLE: 値は有効で利用可能です。
  • STATUS_UNAVAILABLE: 車両がオフラインである、またはセンサーが利用できないため、プロパティは現在利用できません。
  • STATUS_ERROR: センサーの故障など、何らかのエラーにより値を取得できませんでした。
値を使用する前には、必ずこのステータスを確認するべきです。

2.2. プロパティの書き込み

車両の状態を変更する(例:エアコンの温度を設定する)には、setProperty() メソッドを使用します。書き込み操作は、読み取りよりも厳格なパーミッションが要求されることが多く、車両の安全性に影響を与える可能性があるため、慎重に実装する必要があります。


// このメソッドは、Carサービス接続後に呼び出されることを想定
private void setHvacTemperature(float temperature) {
    if (mCarPropertyManager == null) {
        Log.e(TAG, "CarPropertyManager is not available");
        return;
    }

    try {
        // 運転席側の温度を設定する
        // 必要なパーミッション: android.car.permission.CONTROL_CAR_CLIMATE
        mCarPropertyManager.setProperty(
                Float.class, // 設定する値の型
                VehiclePropertyIds.HVAC_TEMPERATURE_SET, // プロパティID
                VehicleAreaSeat.SEAT_ROW_1_LEFT, // エリアID
                temperature); // 設定する値
        
        Log.d(TAG, "Successfully set driver side HVAC temperature to " + temperature);

    } catch (SecurityException e) {
        Log.e(TAG, "Lacked permission to control HVAC.", e);
    } catch (IllegalArgumentException e) {
        // 車両がその温度設定をサポートしていない場合など
        Log.e(TAG, "Invalid arguments for setting HVAC temperature.", e);
    }
}

setProperty() メソッドは、引数としてプロパティID、エリアID、設定したい値に加え、値の型(Float.class)を渡す必要があります。これにより、フレームワークが正しいデータ型でVehicle HALに値を渡すことができます。不正な値(例えば、サポート範囲外の温度)を渡そうとすると IllegalArgumentException が発生する可能性があります。

第3章: リアルタイムデータストリーミング:イベントリスナーの実装

車速やエンジン回転数のように、常に変動するデータを扱う場合、getProperty() を短い間隔でポーリング(繰り返し呼び出す)するのは非効率的であり、システムに不要な負荷をかけます。このようなユースケースのために、CarPropertyManager はイベント駆動型のアプローチを提供しています。プロパティの値が変化したときにコールバックを受け取るリスナーを登録することができます。

3.1. CarPropertyEventCallbackの登録

リアルタイムでプロパティの更新を受け取るには、CarPropertyManager.CarPropertyEventCallback インターフェースを実装し、registerCallback() メソッドで登録します。このコールバックには、値が変更された際の onChangeEvent() と、エラーが発生した際の onErrorEvent() の2つのメソッドがあります。


private CarPropertyManager.CarPropertyEventCallback mSpeedCallback;

// このメソッドは、Carサービス接続後に呼び出されることを想定
private void registerSpeedListener() {
    if (mCarPropertyManager == null) {
        Log.e(TAG, "CarPropertyManager is not available");
        return;
    }

    mSpeedCallback = new CarPropertyManager.CarPropertyEventCallback() {
        @Override
        public void onChangeEvent(CarPropertyValue value) {
            if (value.getPropertyId() == VehiclePropertyIds.PERF_VEHICLE_SPEED) {
                float speedInMps = (Float) value.getValue(); // メートル/秒
                float speedInKph = speedInMps * 3.6f;
                Log.d(TAG, "Current speed: " + speedInKph + " km/h");
                
                // UIの更新はメインスレッドで行う必要がある
                runOnUiThread(() -> {
                    // speedTextView.setText(String.format("%.1f km/h", speedInKph));
                });
            }
        }

        @Override
        public void onErrorEvent(int propId, int zone) {
            Log.e(TAG, "Received error event for property: " +
                    VehiclePropertyIds.toString(propId) + " in area: " + zone);
        }
    };

    // 車速プロパティの変更を購読する
    // 第3引数は更新頻度。SENSOR_RATE_NORMALは一般的なUI更新に適している
    mCarPropertyManager.registerCallback(
            mSpeedCallback,
            VehiclePropertyIds.PERF_VEHICLE_SPEED,
            CarPropertyManager.SENSOR_RATE_NORMAL);
}

重要な点が2つあります。 第一に、registerCallback() の第3引数である更新レートです。これはプロパティの更新をどれくらいの頻度で受け取りたいかのヒントをシステムに与えます。

  • SENSOR_RATE_NORMAL: 約5Hz(1秒間に5回)。UI表示など、一般的な用途に適しています。
  • SENSOR_RATE_UI: 約10-15Hz。より滑らかなUIアニメーションなどに適しています。
  • SENSOR_RATE_FAST: 約50Hz。
  • SENSOR_RATE_FASTEST: 可能な限り最速。デバッグやデータロギングなど、特殊な用途向けです。システムへの負荷が高くなるため、通常の使用は避けるべきです。
適切なレートを選択することは、パフォーマンスとバッテリー消費のバランスを取る上で非常に重要です。

第二に、コールバックのスレッドです。onChangeEvent() はメインスレッド(UIスレッド)ではなく、Binderスレッドで呼び出されます。そのため、コールバック内でUIコンポーネントを直接更新しようとすると、CalledFromWrongThreadException が発生します。UIの更新は、上記の例のように Activity.runOnUiThread()Handler を使ってメインスレッドに処理をポストする必要があります。

3.2. リスナーの登録解除とライフサイクル管理

リスナーは、不要になったら必ず登録を解除する必要があります。これを怠ると、アクティビティが破棄された後もコールバックが呼び出され続け、メモリリークやアプリケーションのクラッシュの原因となります。リスナーの登録と解除は、コンポーネントのライフサイクルに合わせるのがベストプラクティスです。


// onStart()やonResume()でリスナーを登録
@Override
protected void onStart() {
    super.onStart();
    mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
    // ... CarServiceLifecycleListener内で接続完了後に registerSpeedListener() を呼び出す
}

// onStop()やonPause()でリスナーを登録解除
@Override
protected void onStop() {
    super.onStop();
    if (mCarPropertyManager != null && mSpeedCallback != null) {
        mCarPropertyManager.unregisterCallback(mSpeedCallback);
        Log.d(TAG, "Speed callback unregistered.");
    }
    if (mCar != null) {
        mCar.disconnect();
    }
}

一般的に、画面が表示されている間だけデータを必要とする場合は onResume() で登録し onPause() で解除します。アプリケーションがフォアグラウンドにある間(画面が非表示でも)データを必要とする場合は onStart() で登録し onStop() で解除します。

第4章: 高度なトピックとベストプラクティス

基本的な読み書きとリスナーの実装方法を学んだ上で、より柔軟で堅牢なアプリケーションを構築するための高度なテクニックと考慮事項を見ていきましょう。

4.1. プロパティ構成の動的な取得

すべての車両が同じプロパティをサポートしているわけではありません。また、同じプロパティでも、車種によってサポートする値の範囲(例:設定可能な最低・最高温度)が異なる場合があります。アプリケーションが特定の車種に依存しないようにするには、プロパティの構成情報を実行時に動的に取得する必要があります。

getCarPropertyConfig() メソッドは、指定されたプロパティIDの CarPropertyConfig オブジェクトを返します。これには、プロパティのアクセス権、変更モード、サポートされるエリアID、そして最小値・最大値などの情報が含まれています。


private void checkHvacTempRange() {
    if (mCarPropertyManager == null) return;

    CarPropertyConfig config = mCarPropertyManager.getCarPropertyConfig(VehiclePropertyIds.HVAC_TEMPERATURE_SET);
    if (config == null) {
        Log.w(TAG, "HVAC_TEMPERATURE_SET config not found.");
        return;
    }

    // このプロパティがサポートするエリアIDのリストを取得
    for (int areaId : config.getAreaIds()) {
        Float minValue = (Float) config.getMinValue(areaId);
        Float maxValue = (Float) config.getMaxValue(areaId);
        Log.d(TAG, "Area " + areaId + " supports temperature range: " + minValue + " to " + maxValue);
    }
}

この機能を利用することで、例えばUIのスライダーの範囲を車両が実際にサポートする温度範囲に動的に設定したり、そもそも特定の機能が利用可能かどうかを判断したりすることができます。

4.2. エリアIDの活用

前述の通り、多くのプロパティはエリアIDによって区別されます。例えば、ウィンドウの開閉(WINDOW_POS)は、各ドアのウィンドウごとにエリアID(例:VehicleAreaWindow.WINDOW_ROW_1_LEFT)を持ちます。アプリケーションが特定の座席やエリアに関連する機能を実装する場合、このエリアIDを正しく指定する必要があります。

利用可能なエリアIDは、前述の CarPropertyConfig.getAreaIds() で取得できます。これにより、例えば「全ウィンドウを閉じる」機能を実装する際には、取得したすべてのエリアIDに対してループ処理で setProperty() を呼び出す、といった実装が可能になります。

4.3. エラーハンドリングとデバッグ

車両との通信は、様々な要因で失敗する可能性があります。堅牢なアプリケーションは、これらのエラーを適切に処理できなければなりません。

  • CarNotConnectedException: Carサービスが切断されている状態でAPIを呼び出すと発生します。非同期接続の完了を待たずにAPIを呼び出した場合や、サービスがクラッシュした場合に発生する可能性があります。常に接続状態を確認し、この例外をcatchする準備をしておくべきです。
  • SecurityException: 必要なパーミッションがマニフェストにない場合に発生します。開発中は、この例外を見逃さないようにしましょう。
  • IllegalArgumentException: 無効なプロパティIDやエリアID、または車両がサポートしていない値を指定した場合に発生します。getCarPropertyConfig() を使って事前チェックを行うことで、このエラーをある程度防ぐことができます。

デバッグの際には、Androidのadbコマンドが非常に役立ちます。以下のコマンドは、現在のすべてのプロパティの状態をダンプします。


adb shell dumpsys car_service --properties

このコマンドを使うことで、アプリケーションがプロパティを正しく読み書きできているか、またVehicle HALが期待通りに値を更新しているかを確認できます。

まとめ

CarPropertyManager は、Android Automotive OSアプリケーション開発において、車両との対話を実現するための強力かつ不可欠なツールです。本記事では、そのアーキテクチャ上の役割から、基本的な読み書き、リアルタイムデータ処理のためのリスナー実装、そしてプロパティ構成の動的取得やエラーハンドリングといった高度なトピックまでを網羅的に解説しました。

成功の鍵は、車両プロパティの非同期的な性質とライフサイクルを深く理解し、適切なパーミッション管理とエラーハンドリングを実装することにあります。CarPropertyManager を使いこなすことで、単なるインフォテインメントの枠を超え、車両の状態と深く連携した、安全で直感的、そして革新的な車内体験を創造することが可能になります。


0 개의 댓글:

Post a Comment