構成と調整(ConCal)

ソフトウェア定義車両(SDV)プラットフォームの構成とキャリブレーション サービス(ConCal)は、車両の仕様、国の規制、お客様が注文した機能に従って SDV サービスを構成する機能を提供します。このサービスは SDV プラットフォームの基本的な構成要素であり、OEM は、複数の車両間で同じサービスコードを再利用できます。これは、サービスコードを構成し、複数のソース(工場、サービス センター、クラウドなど)からの再構成を有効にすることで実現します。

SDV プラットフォームは、特定の車両のサービス バンドルの構成とキャリブレーションのためのサービス向け API を提供します。このインターフェースを使用して、OEM は OEM 固有の構成とキャリブレーションのロジックを実装できます。

構成と調整サービスには、次のプロセスが含まれます。

  • 構成。車両の基本的なプロパティと動作を定義します。車両の位置、ユーザーが注文したオプション、国の規制など、複数の要因に依存する場合があります。コンポーネントの相互作用を確立し、ソフトウェア バリアント、ネットワーク接続、初期運用パラメータなど、車両の全体的な機能に影響を与えるソフトウェア設定を決定します。

  • キャリブレーション: 事前構成された範囲内でシステム パラメータを微調整します。たとえば、キャリブレーションでは、センサーとアクチュエータの精度を調整し、排出ガス制御のためにエンジン性能を最適化し、運転性と安全システムのレスポンスを改善します。構成は車両の機能の基本的なフレームワークを設定し、キャリブレーションはそのフレームワーク内で車両の動作を最適化します。どちらも、車両が排出ガス規制を満たし、パフォーマンスを最大化し、安全性を高め、経年劣化を補償できるようにするために不可欠です。

標準の SDV 全体 ConCal API を提供することで、SDV サービス バンドルの実装を簡素化し、さまざまな OEM のさまざまな車両で実行するために構成とキャリブレーションの機能を再実装する必要がなくなります。

アーキテクチャ

各サービス バンドルは、1 つ以上の構成アーティファクトを所有できます。

構成アーティファクト

構成アーティファクト(構成)は、1 つ以上の構成パラメータとその値で構成されます。構成はサービス固有の protobuf メッセージです。そのフィールドには、ネストされた protobuf メッセージ(構造体)、マップ、配列、int、float、bool、bytes、string パラメータを含めることができます。

// Example of a configuration message.
message SampleServiceBundleConfig
{
 bool bool_parameter = 1;
 int64 int_parameter = 2;
 float float_parameter = 3;
 string str_parameter = 4;
 repeated string list_parameter = 5;
 map<string, int32> map_parameter = 6;
 SomeNestedMessage nested_parameter = 7;
 SomeComplexMessage complex_parameter = 8;
 some.nested.package.SomeNestedMessage nested_package = 9;
 bytes bytes_parameter = 10;
}

構成識別子

各構成には一意の識別子があります。この識別子は、サービス バンドル オーナーの完全修飾インスタンス名と構成名で構成されます。構成名は、人間が読める形式で、サービス バンドルごとに一意であり、サービス バンドルの命名規則で定義されている命名標準(sharedprivatediagnosticscalibration など)に準拠している必要があります。

制限事項:

  • インスタンス名の先頭は文字にする必要があります。
  • すべての文字は、小文字の英数字またはハイフンにする必要があります。
  • 名前のハイフンは、連続して複数回使用できません。
  • 構成名の末尾をハイフンにすることはできません。
  • 構成名は 48 文字以下にしてください。
  • 構成名は、同じサービス バンドルの同じ VM で一意である必要があります。

26Q2 より前では、構成 ID は次のように定義されます。

// Unique identifier for the config.
message ConfigId {
  // The FQIN of the service bundle that owns the configuration.
  com.sdv.google.sd_common.ServiceFqin service_fqin = 1;

  // The name of the config.
  string config_name = 2;
}

起動時の構成オーナーは、構成のスキーマとそのデフォルト値のみを認識します。現在の車両に合わせてサービス バンドルの動作をカスタマイズするには、所有サービス バンドルがスキーマとともにデフォルト構成を登録する必要があります。

デフォルト構成の登録とカスタマイズされた構成の取得

図 1. デフォルト構成の登録とカスタマイズされた構成の取得。

これにより、OEM はサービス バンドルを一度実装するだけで、複数の車両で実行できます。

デプロイ

ConCal は、SDV プラットフォームで 1 つ以上のサーバー インスタンスを維持できます。サービス バンドルは、最も近い ConCal サーバーを検出して使用する必要があります。たとえば、ConCal が ECU ごとに 1 つデプロイされている場合、サービス バンドルは同じ ECU で実行されている ConCal インスタンスにアクセスする必要があります。これにより、サービス バンドルは構成をタイムリーに取得できます。ConCal インスタンスにリクエストされた構成がない場合(別の ConCal の責任範囲に属している場合)、連絡を受けた ConCal サーバーは、所有している ConCal インスタンスからリクエストし、サービス バンドルにリダイレクトします。

構成のカスタマイズ

異なる車両で同じソフトウェアを再利用できることは、SDV の主なメリットの 1 つです。ソフトウェアは一度開発され、複数の車両で再利用されます。また、車両の仕様に基づいてソフトウェアの動作を調整できます。これが ConCal の主な目的です。ConCal は、構成オーバーライドを使用して車両プロパティに基づいてサービス構成を計算します。

ConfigOverride は、特定の車両に合わせて構成を調整する方法を記述する protobuf メッセージです。これは、オーバーライド ID(これを提供するエンティティによって一意に定義される)、構成識別子、ConfigOverrideKeyValuePair のリストで構成されます。ConfigOverride は、更新プロセス中のみ、OEM によってモデル化された許可されたサービスによってのみ提供できます。両方の構造の protobuf 定義を以下に示します。

// Key-value pair to update configuration.
message ConfigOverrideKeyValue {
  string key = 1;

  oneof value {
    string value_txtproto = 2;
    .google.protobuf.Any value_any = 3;
  }
}

// A collection of changes for a specific configuration which should be atomically applied.
message ConfigOverride {
  string override_id = 1;

  ConfigId config_id = 2;

  repeated ConfigOverrideKeyValue pairs = 3;
}

ConfigOverride は、次のオペレーションをサポートしています。

  • 新しい値を割り当てる: 最後の値は非推奨となり、パラメータに新しい値が割り当てられます。たとえば、単純なフィールド(int、string、float、bool、bytes)に新しい値を割り当てたり、マップ、リスト、構造体、完全な構成などの複雑なフィールドを書き換えたりします。

  • 値の削除またはクリーンアップ: このオペレーションは、メッセージ、繰り返しフィールド、マップ、単一フィールド、構成自体など、すべてのタイプに提供されます。このオペレーションは、完全なフィールドまたはメッセージに対して実行できます。つまり、マップ内の特定のキーと、繰り返しフィールドの個々の要素の削除はサポートされていません。

  • マップに新しい値を追加します。

  • マップ内の既存のキーの値を書き換えます(キーが存在しない場合は、マップに新しい値を追加するオペレーションとしても実行できます)。

ConCal を使用してサービス バンドルを構成する

この章では、実行時に構成を取得するサービス バンドルを開発する方法について説明します。構成可能なバンドルの観点から見ると、取得した構成がデフォルトの出荷時設定なのか、ConCal のオーバーライドと調整プロセスを使用した後続の変更なのかは、完全に不透明です。

ドキュメントのベースとなっているサンプルは、system/software_defined_vehicle/samples/concal/src/concal_client で確認できます。

詳細については、サービス バンドルの開発をご覧ください。

サービス バンドルが所有する構成の型を宣言する

サービス バンドルは、取得する構成のタイプを所有します。これにより、構成可能なバンドルを永続化された構成データから独立して更新できます。

  1. 構成型を宣言する protobuf ファイル(拡張子 .proto)を作成します。

    syntax = "proto3";
    
    package android.sdv.demo.config;
    
    message RearViewCamera {
    string model = 1;
    uint64 horizontal_resolution = 2;
    uint64 vertical_resolution = 3;
    float x_axis_field_of_view = 4;
    float y_axis_field_of_view = 5;
    bool is_rgb = 6;
    }
    
  2. 構成タイプの取得を可能にするランタイム ライブラリを生成するビルド ターゲットを作成します。Android.bp ファイルで次のようにします。

    rust_protobuf {
        name: "libsdvtestconcal_proto_rust",
        crate_name: "sdvtestconcal_proto_rust",
        protos: [
            "rear_view_camera.proto",
        ],
        proto_flags: [
            "-I external/protobuf/src",
            "-I .",
        ],
        source_stem: "sdvtestconcal_proto_rust",
        vendor_available: true,
        product_available: true,
        min_sdk_version: "35",
    }
    

バンドル用の ConCal RPC ミドルウェア コードを生成する

サービス バンドルに VSIDL 宣言を追加します。

package: "com.sdv.oem.sample.concal"

service_bundle {
  name: "SampleOemConCalClientServiceBundle"
  client {
    service: "com.sdv.google.concal.ConCalRegistrationService"
  }
}

これは、バンドルが ConCal 構成の登録と取得サービスのクライアントであることを宣言します。バンドルの RPC クライアント バインディングを生成するための VSIDLC の使用の概要については、VSIDL とミドルウェアの概要をご覧ください。

RPC 構成取得用のミドルウェアを初期化

実行時に、ConCal RPC 呼び出しに必要なミドルウェア コンポーネントを初期化します。この例では、バンドルが開始されたときに非同期で初期化します。

pub struct ExampleConcalBundle {
    context: ContextRef,
    runtime: Option<Runtime>,
}

sdv::lifecycle::register_service_bundle!(ExampleConcalBundle);

impl ServiceBundle for ExampleConcalBundle {
    fn new(context: ContextRef) -> ExampleConcalBundle {
        info!("Creating {}.", context.get_self_fqin());

        ExampleConcalBundle { context, runtime: None }
    }

    fn on_start(&mut self) {
        let fqin = self.context.get_self_fqin();
        info!("Starting {}.", fqin);

        let runtime = Builder::new_multi_thread()
            .worker_threads(4)
            .thread_name("tokio-pool")
            .enable_all()
            .build()
            .unwrap();
        let context = self.context;
        runtime.spawn(async move {
            let registration_client = setup_register_config_rpc(context).await;
            /* main SB logic here */
        });
        self.runtime = Some(runtime);
    }

async fn setup_register_config_rpc(context: ContextRef) -> RegistrationClient {
    let sdv_comms = SdvComms { context };
    let sd = ServiceDiscoveryManager::new(context);
    let unit_name_args = UnitNameDiscoveryArgs::new_builder()
        .set_sdv_package_name("com.sdv.oem.sample.concal")
        .set_service_bundle_name("SampleOemConCalServiceBundle")
        .set_service_unit_name(RegistrationClient::DEFAULT_UNIT_NAME)
        .build()
        .unwrap();

    let mut unit_name_stream =
        sd.subscribe_service_unit_change_by_name(&unit_name_args).await.unwrap();

    // wait until RPC servers are registered, if server is not a custom agent
    while let Some(event) = unit_name_stream.next().await {
        if let ServiceUnitChangeEvent::Registered(sud) = event {
            let service_identity = sud.get_service_bundle_identity();
            let fqin = service_identity.get_fqin();
            if fqin.get_sdv_package_name() == "com.sdv.oem.sample.concal"
                && fqin.get_service_bundle_name() == "SampleOemConCalServiceBundle"
                && fqin.get_service_instance_name() == "default"
            {
                break;
            }
        }
    }

    let service_bundle =
        SampleOemConCalClientServiceBundle::new(Arc::new(sdv_comms)).await.unwrap();
    service_bundle
        .create_rpc_client::<RegistrationClient>(
            UnitName::builder()
                .package_name("com.sdv.oem.sample.concal")
                .bundle_name("SampleOemConCalServiceBundle")
                .service_unit_name(RegistrationClient::DEFAULT_UNIT_NAME)
                .build()
                .unwrap(),
            ClientOptions::default(),
        )
        .await
        .expect("Failed to create an RPC client")
}

ここで

  • on_start では、tokio ランタイムを作成し、tokio タスクを生成します。このタスクは、バンドルのメインロジックに進む前に setup_register_rpc を呼び出します。
  • setup_register_rpc は、ミドルウェア RPC バインディングを設定します。この例では、ConCal 機能がエージェントによって実装されていることは想定していません。バンドルが起動した後にのみサーバーが使用可能になる場合があります。そのため、このコード例では、Service Discovery API を使用して RPC サーバーが登録されるまで待機します。

構成を初期化する

ConCal サーバーに ID、構成スキーマ、デフォルトのファクトリー構成値を指定して、構成アーティファクトを登録します。

登録が初めて呼び出された場合、デフォルト値が保持されます。後続のバンドル起動時に登録が呼び出されると、適用された ConCal オーバーライド(存在する場合)を含む永続化されたデフォルト値が取得されます。

アーティファクトがすでに登録されているかどうかに関係なく、ConCal 登録構成呼び出しは失敗しません。

impl ServiceBundle for ExampleConcalBundle{

    /* ... */
    fn on_start(&mut self) {
        /* ... */
        runtime.spawn(async move {
            let registration_client = setup_register_config_rpc(context).await;
            sample_concal_main(fqin, registration_client).await
        });
        self.runtime = Some(runtime);
    }
}

async fn sample_concal_main(
    fqin: ServiceFqin,
    registration_client: RegistrationClient,
) -> sdv::status::SdvResult<()> {
    let config = get_rear_view_camera_factory_config();
    let config_id = get_config_id(&fqin);

    register_config(&registration_client, &config_id, &config).await;
    /* ... */
}

fn get_rear_view_camera_factory_config() -> RearViewCamera {
    RearViewCamera {
        model: String::from("model 1"),
        horizontal_resolution: 720,
        vertical_resolution: 720,
        x_axis_field_of_view: 70.0,
        y_axis_field_of_view: 70.0,
        is_rgb: false,
        ..Default::default()
    }
}

fn get_config_id(fqin: &ServiceFqin) -> ConfigId {
    ConfigId {
        config_name: "config".to_string(),
        service_fqin: MessageField::some(ProtoFqin {
            vm_name: fqin.get_sdv_vm_name().to_string(),
            package_name: fqin.get_sdv_package_name().to_string(),
            service_name: fqin.get_service_bundle_name().to_string(),
            instance_name: fqin.get_service_instance_name().to_string(),
            ..Default::default()
        }),
        ..Default::default()
    }
}

async fn register_config(
    client: &RegistrationClient,
    config_id: &ConfigId,
    config: &RearViewCamera,
) {
    let config_fd = FileDescriptorSet {
        file: vec![RearViewCamera::descriptor().file_descriptor_proto().clone()],
        ..Default::default()
    };
    let config = Any::pack(config).expect("Failed to pack config");
    let config_metadata = ConfigMetadata {
        descriptor_set: MessageField::some(config_fd),
        default_config: MessageField::some(config.clone()),
        ..Default::default()
    };

    client
        .RegisterConfigMetadata(&RegisterConfigMetadataRequest {
            config_id: MessageField::some(config_id.clone()),
            metadata: MessageField::some(config_metadata),
            config_version: String::from("1.0"),
            ..Default::default()
        })
        .await
        .expect(
            "RegisterConfigMetadata should not fail, even if configuration was registered before",
        );
}

ここで

  • on_start で生成されたタスクは、RPC バインディングを取得した後、ビジネス ロジックに進み、sample_concal_main を呼び出します。
  • sample_concal_main は、構成アーティファクトを登録することから始まります。ロジックは register_config に含まれています。
  • 構成を登録するには、バンドルで protobuf 型、ID、デフォルト値を指定する必要があります。
  • config_fd は構成タイプです。構成タイプを所有するバンドルは、APEX の更新後を含め、バンドルが想定するスキーマが常に取得されるようにします。
  • この ID は、ConCal サーバーの内部永続ロジックで識別子として使用されます。
  • デフォルト値は get_rear_view_camera_factory_config で構築されます。以前に何も永続化されていない場合、デフォルト値は ConCal サーバーによって永続化された値です。これは、システムが出荷時の設定を指定できる方法の 1 つです。他の設定も可能です。

構成を取得する

登録後、構成を取得します。構成は以前に登録されているため、この呼び出しは必ず成功します。

async fn sample_concal_main(
    fqin: ServiceFqin,
    registration_client: RegistrationClient,
    update_client: UpdateClient,
) -> sdv::status::SdvResult<()> {
    let config = get_rear_view_camera_factory_config();
    let config_id = get_config_id(&fqin);

    register_config(&registration_client, &config_id, &config).await;
    let config = get_config(&registration_client, &config_id).await;
    info!("Retrieved configuration:\n{config:#?}");

    // Onwards, use configuration in bundle's main business logic
    /* ... */
}

async fn get_config(client: &RegistrationClient, config_id: &ConfigId) -> RearViewCamera {
    let bytes = client
        .GetConfig(&GetConfigRequest {
            config_id: MessageField::some(config_id.clone()),
            ..Default::default()
        })
        .await
        .expect("Get config does not fail, as config was registered before")
        .config;
    RearViewCamera::parse_from_bytes(&bytes).expect("parse_from_bytes failed")
}

ここで

  • 構成は同じバンドルによって事前に登録されているため、GetConfigRequest の呼び出しは必ず成功します。セットアップにより、サービス バンドルは、ファクトリー構成とオーバーライドされた構成の両方のケースを不透明に処理できます。サービス バンドルのビジネス ロジックは変更されません。

  • 呼び出し GetConfigRequest は未加工のバイトを返します。関数 get_config は、それらを解析して想定される構成タイプに変換します。