ユーザー設定のアーキテクチャと実装ガイド

このページでは、SDV ユーザー設定システムのアーキテクチャのガイドと、ユーザーが制御可能なサービスとクライアントを実装する手順について説明します。

アーキテクチャの概要

ユーザー設定システムは、ユーザー設定の保存と管理を、それらの設定の適用と実施から切り離します。主なアーキテクチャ用語を次の表にまとめます。

機能説明ロール責任範囲
ユーザー制御可能なサービス 特定のドメイン(暖房換気空調システム、カーシート、オーディオなど)を管理する標準の SDV サービス バンドル 制御するハードウェアまたはサブシステムの機能と制約の意図された状態を提供します。
  • 登録: ユーザー設定エージェントに、サポートする設定(メタデータ、デフォルト、制約)を通知します。
  • 適用と自律性: 設定変更のリクエストを受け取り、現在の状態と安全ルールに照らして検証し、ハードウェアに適用します。完全な自律性を維持し、設定変更が承認されるか拒否されるかを指定します。
ユーザー設定エージェント 中央のオーケストレーター 一元的なストレージ、ユーザー プロファイル管理、通知ハブを提供します。
  • ストレージ: ユーザー(ドライバー、乗客など)ごとに設定を保持します。
  • ルーティング: プロキシは、クライアント(ヒューマン マシン インターフェース(HMI)など)からのリクエストを適切なユーザー制御可能なサービスに変更します。
  • 通知: ChangeNotifier インターフェースを使用して、変更を関係者(ビューまたは HMI)にブロードキャストします。
  • ユーザー管理: ユーザーの切り替えと、登録されているすべてのサービスへの正しい永続状態の適用を処理します。
ユーザー設定クライアント ユーザー インターフェースを提供するアプリまたはサービス(HMI やユーザー設定とやり取りする必要があるその他のロジックを備えた IVI アプリなど) ユーザーとやり取りし、設定を表示して、変更リクエストを開始します。
  • 表示: 現在の設定と制約をユーザーに提示します。
  • 変更をリクエスト: ユーザーが開始した設定変更をユーザー設定エージェントに送信します。
  • 通知を受け取る: 設定のリアルタイム更新を購読し、それに対応します。

ユーザー制御可能なサービスを実装する

どのキーが存在するか、そのデータ型、デフォルト値、制約(最小値や最大値など)を User Preferences エージェントに通知するには、サービス バンドルが UserPreferencesRegistryService インターフェースとやり取りする必要があります。サービスが起動するとき、公開する設定を登録する必要があります。ユーザーが設定を変更しようとしたとき(またはユーザーを切り替えたとき)、エージェントはサービスで RequestSettingsChange を呼び出します。

このセクションの手順に沿って、ユーザー設定(および HMI を持つユーザー)がサービスを制御できるようにします。

  1. VSIDL ファイルでサービス インターフェースを定義します。

    • サーバーとして、com.sdv.google.user_preferences.user_controllable.UserControllableService を実装します。これにより、エージェントから変更リクエストが送信されます。
    • クライアントとして com.sdv.google.user_preferences.UserPreferencesRegistryService を使用します。これにより、起動時に設定が登録されます。

    次の例は、ユーザーが制御可能なサービスの service_bundle.vsidl を示しています。

    service_bundle {
        name: "MyFeatureService"
        server {
            service: "com.sdv.google.user_preferences.user_controllable.UserControllableService"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
        }
    }
    
  2. 起動時にキー proto user_preferences_registry_service.proto で設定を登録します。

    1. UserPreferencesRegistryService に接続します。
    2. RegisterSettingsRequest のインスタンスを作成します。
    3. SettingsGroup(設定の論理コレクション)のインスタンスを定義します。
    4. 設定ごとに、次の項目を定義します。

      *   **Key:** Unique string ID (for example, `TEMPERATURE`)
      *   **Kind:** `PER_USER` (stored per user profile) or `SHARED`
          (global)
      *   **Default value:** Initial value if no user preference exists
      *   **Constraints:** (optional) Validation rules (for example, Min
          16, Max 32 for HVAC).
      
    5. RegisterSettings() を呼び出します。

    次の例は概念的な Rust です。

    let temperature_setting = SettingDefinition {
        name: "TEMPERATURE".to_string(),
        kind: SettingKind::PER_USER.into(),
        default_value: Value::Int64(22), // Default 22 degrees
        constraint: Some(Constraints::Int64Constraints(Int64Constraints {
            min_value: Some(16),
            max_value: Some(32),
            ..Default::default()
        })),
        ..Default::default()
    };
    
    registry_client.RegisterSettings(&RegisterSettingsRequest {
        group_name: "HVAC".to_string(),
        version: "1.0".to_string(),
        settings_definitions: vec![temperature_setting],
    }).await?;
    
  3. user_controllable_service.proto で設定変更リクエストを処理します。

    1. RequestSettingsChange RPC を実装します。
    2. リクエストされた値が現在のコンテキスト内で有効かどうか(ハードウェアの準備ができているかどうかなど)を検証します。
    3. 変更を適用する特定のロジックを適用します(シートを移動する、ファンの速度を変更するなど)。
    4. RequestSettingsChangeResponse で適用された値を返します。
    5. 変更を承認した場合は、新しい値を返します。
    6. 値を拒否またはクランプした場合は、設定(または保持)した値を返します。

    次の例は概念的な Rust です。

    async fn RequestSettingsChange(
        &self,
        _caller_id: ServiceFqin,
        request: &RequestSettingsChangeRequest
    ) -> SdvResult<RequestSettingsChangeResponse> {
        let mut applied_settings = Vec::new();
    
        for setting in &request.settings {
            if self.hardware.set_value(setting.key, setting.value).is_ok() {
                // Change accepted
                applied_settings.push(setting.clone());
            } else {
                // Change rejected, return current actual value
                let current_val = self.hardware.get_value(setting.key);
                applied_settings.push(create_setting(setting.key, current_val));
            }
        }
    
        Ok(RequestSettingsChangeResponse {
            settings: applied_settings,
        })
    }
    
  4. FactoryReset RPC を実装して、サブシステムをクリーンな状態に戻します。

    1. すべての設定をデフォルト値(コード構成で定義されている値)にリセットします。
    2. レジストリ サービスで UpdateSettings を呼び出し、値が外部で変更されたこと(ユーザー リクエストではなくリセットによる変更)をエージェントに通知します。

下位互換性とスキーマの進化

ユーザーが制御可能なサービスによって定義された設定のスキーマは、公開インターフェースと見なされます。このインターフェースは、ユーザー設定エージェントとさまざまなクライアント(HMI など)によって使用されます。これらのリリース スケジュールは異なる場合があります。したがって、システムの不安定化やクライアントの破損を防ぐには、厳密な下位互換性を維持することが不可欠です。

互換性のある変更

ユーザーが制御可能なサービスでは、設定スキーマに互換性のある変更のみを導入する必要があります。これらの変更により、古いクライアントはアップデートなしで正しく動作し続けます。

  • 妥当なデフォルト値を持つ新しい省略可能な設定を追加します。

    • この設定は省略可能であるため、クライアントは、サービスのさまざまなバージョンをサポートするために、この設定の存在を確認する必要があります。
    • クライアントがこの設定を認識するように更新されていない場合、この設定は無視されます。
    • 新しい設定のユーザー設定が存在しない場合、エージェントは指定されたデフォルト値を使用します。

互換性のない変更

次の変更は破壊的と見なされ、古いクライアントをすぐに侵害するため禁止されています。

  • 既存の設定を削除します。

  • 既存の設定の意味、単位、データ型を変更する(たとえば、摂氏の温度を表す整数から、パスカルの圧力を表す浮動小数点数に変更する)。

  • 既存の設定のデフォルト値を変更します。デフォルト値の変更が禁止されている主な理由は、永続データの移行が複雑になる可能性があるためです。エージェントは、サービスの登録スキーマに基づいて設定を保存します。デフォルト値を変更するには、既存のすべてのユーザー プロファイルを新しいデフォルトに移行するための複雑なロジックが必要になります。また、設定を明示的に設定したことがないユーザーに対して技術的に正しくない値を使用するリスクもあります。

必要な互換性を破る変更に対応する

破壊的変更が必要な場合、サービスは既存の設定を変更してはなりません。互換性を維持しながらこの移行を管理するアプローチは、主にユーザーが制御可能なサービス ロジックにあります。

  1. 必要な新しい定義、キー、単位を使用して新しい設定を作成します。
  2. 互換性用に登録して、既存の設定を非推奨にします。新しいクライアントを開発する際は、この新しい設定を使用します。
  3. 新しいクライアントには、ユーザー制御可能なサービスのビジネス ロジック内に互換性ロジックを実装するようアドバイスします。

サービスの RequestSettingsChange 実装では、一方の設定の変更が非推奨の対応する設定に反映され、対応する設定の変更が設定に反映されるようにする必要があります。

互換性ロジックの例:

  1. サービスは古い設定 TEMPERATURE_C(摂氏)を非推奨とし、TEMPERATURE_K(ケルビン)を導入します。

  2. エージェントが新しい設定の更新リクエストを送信した場合:

    • サービスは TEMPERATURE_K(295.15 K など)のリクエストを受信します。

    • サービスのロジックは、これを摂氏(22°C)に変換し、内部で両方の値を更新して永続化し、以前のクライアントとの整合性を維持します。

  3. エージェントが以前のクライアントから非推奨の設定のリクエストを送信した場合:

    • サービスは TEMPERATURE_C(24°C など)のリクエストを受信します。
    • サービスはこれをケルビン(297.15 K)に変換し、両方の値を内部的に更新して保持します。

このデュアル書き込み戦略により、リリース スケジュールに関係なく、すべてのクライアントが一貫性のある正確なデータを読み取り、外部リリースの不一致による破損を回避できます。

クライアントを実装する

ユーザー設定エージェントとやり取りするクライアント(HMI など)を実装するには:

  1. 特定のユーザー設定インターフェースとやり取りするようにクライアント サービス バンドルを定義します。VSIDL ファイルで、次のようにします。

    • サーバーとして com.sdv.google.user_preferences.view.ChangeNotifier を実装し、エージェントが設定変更に関するリアルタイム通知をクライアントに送信できるようにします。
    • クライアントとして、com.sdv.google.user_preferences.UserPreferencesManagementService を使用して設定の変更をリクエストし、更新をサブスクライブします。
    • クライアントとして、com.sdv.google.user_preferences.UserPreferencesAdminService を使用してユーザー プロファイルを管理します(作成、選択、削除、出荷時の設定にリセットなど)。

    次の例は、クライアントの service_bundle.vsidl ファイルを示しています。

    service_bundle {
        name: "MyHmiClient"
        server {
            service: "com.sdv.google.user_preferences.view.ChangeNotifier"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
        }
    }
    

    必要に応じて認可ポリシーを追加します。ユーザー設定エージェントの通信を許可するには、指定されたサーバーのクライアントを作成します。次に例を示します。

    server {
        service: "com.sdv.google.user_preferences.view.ChangeNotifier"
        allow_all_channels: true
    }
    client {
        service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
        allow_all_channels: true
    }
    client {
        service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
        allow_all_channels: true
    }
    
  2. リクエストの設定は UserPreferencesManagementService で変更できます。

    1. UserPreferencesManagementService に接続します。
    2. RequestSettingsChangeRequest のインスタンスを作成し、SettingsGroupId と必要な設定を指定します。
    3. 省略可。ChangePersistencePolicyPERSISTENT_CHANGE(デフォルト)または NON_PERSISTENT_CHANGE に設定します。
    4. RequestSettingsChange() を呼び出します。

    次の例は概念的な Rust です。

    management_client.RequestSettingsChange(&RequestSettingsChangeRequest {
        settings_group_id: Some(SettingsGroupId {
            service_fqin: hvac_service_fqin.to_string(),
            name: "HVAC".to_string(),
            ..Default::default()
        }).into(),
        settings: vec![Setting {
            key: "TEMPERATURE".to_string(),
            value: Some(Value::Int64(24)),
            ..Default::default()
        }],
        change_persistence_policy: ChangePersistencePolicy::PERSISTENT_CHANGE.into(),
        ..Default::default()
    }).await?;
    
  3. user_preferences_management_service.protochange_notifier.proto を使用して設定の変更をサブスクライブします。

    1. サービス バンドル内の ChangeNotifier インターフェースから OnSettingsChange RPC を実装します。ユーザー設定エージェントは、変更が発生するとこのメソッドを呼び出します。
    2. UserPreferencesManagementService に接続します。
    3. モニタリングする SettingsGroupId を指定して SubscribeToSettingsChangeAndGetSettingsRequest を作成します。
    4. 設定の状態を返し、OnSettingsChange 実装を通じて今後の更新を送信するには、SubscribeToSettingsChangeAndGetSettings() を呼び出します。

    ChangeNotifier インターフェースの実装例を次に示します。これは概念的な Rust です。

    #[async_trait]
    impl ChangeNotifier for MyHmiServiceImpl {
        async fn OnSettingsChange(
            &self,
            _caller_id: ServiceFqin,
            request: &OnSettingsChangeRequest,
        ) -> SdvResult<OnSettingsChangeResponse> {
            // Process the active_settings, pending_changes, and persisted_settings
            // Update your UI or internal state accordingly.
            info!("Received settings change for group: {}", request.settings_group_id.name);
            // ...
            Ok(OnSettingsChangeResponse::new())
        }
    }
    

    次のサブスクライブの例は、概念的な Rust で記述されています。

    management_client.SubscribeToSettingsChangeAndGetSettings(&SubscribeToSettingsChangeAndGetSettingsRequest {
        settings_group_id: Some(SettingsGroupId {
            service_fqin: hvac_service_fqin.to_string(),
            name: "HVAC".to_string(),
            ..Default::default()
        }).into(),
        ..Default::default()
    }).await?;
    
  4. 省略可: ユーザー プロファイルを管理するクライアント(専用の設定アプリなど)は、user_preferences_admin_service.proto を使用して管理サービスとやり取りできます。

    1. UserPreferencesAdminService に接続します。
    2. CreateUserSelectUserDeleteUserFactoryResetListUsers などの RPC を使用して、ユーザー プロファイルを管理します。

    ユーザーを作成する次の例は、概念的な Rust で記述されています。

    admin_service_client.CreateUser(&CreateUserRequest {
        user: Some(User {
            id: 1,
            flags: UserFlags::DRIVER.value(),
            ..Default::default()
        }).into(),
        ..Default::default()
    }).await?;
    
  5. user_preferences_admin_service.proto を使用して、管理サービスで利用可能な設定とユーザーを検出します。

    1. UserPreferencesAdminServiceListUsers() を呼び出して、登録されているすべてのユーザーのリストを取得します。
    2. UserPreferencesAdminServiceGetUserSettings(user_id) を呼び出して、特定のユーザーの設定を取得します。

    ユーザーを一覧表示して設定を取得する次の例は、概念的な Rust で記述されています。

    // List all users
    let list_users_response = admin_service_client.ListUsers(&ListUsersRequest::new()).await?;
    info!("Available users: {:?}", list_users_response.users);
    
    // Get settings for a specific user (for example, user with ID 1)
    if let Some(user_id) = list_users_response.users.first().map(|u| u.id) {
        let get_settings_response = admin_service_client.GetUserSettings(&GetUserSettingsRequest {
            user_id,
            ..Default::default()
        }).await?;
        info!("Settings for user {}: {:?}", user_id, get_settings_response.groups);
    }
    

フローの概要

  1. クライアントが起動し、エージェントの管理サービスと管理サービスに接続します。
  2. クライアントは設定グループを検出して登録し、初期状態を表示します。
  3. クライアントがエージェントに RequestSettingsChange を送信します。
  4. エージェントは、更新された設定と状態を含む OnSettingsChange 通知をクライアントに送信します。

完全な動作例については、@samples/user_preferences/v1/HMIService をご覧ください。

エージェントを実装する

オーケストレーターが起動したサービス バンドルは、ユーザー設定エージェントを実装します。便宜上、リファレンス実装が提供されています。

また、エージェント用のサンプル サービス バンドル user_preferences_sample.vsidl ファイルも提供されます。

package: "com.sdv.oem.user_preferences"

service_bundle {
    name: "UserPreferencesServiceBundle"
    server {
        service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
    }
    server {
        service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
    }
    server {
        service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
    }
    client {
        service: "com.sdv.google.user_preferences.view.ChangeNotifier"
    }
    client {
        service: "com.sdv.google.user_preferences.user_controllable.UserControllableService"
    }
}

実装では、生成されたミドルウェアを使用でき、ユーザー設定エージェントを実装するサービス バンドルによって参照される rust_ffi_shared にコンパイルされる必要があります。

sdv_service_bundle_metadata {
  # This name must match the agent's Bundle Name
  name: "UserPreferencesServiceBundle"
  version_number: 1
  version_name: "1"

  native_library_path: "lib64/<USER-PREFERENCES-FFI-LIB>.so"

  orchestration_config_path: "etc/user_preferences_service_bundle/<USER-PREFERENCES-ORCHESTRATION>.textproto"

  authorization_policy_path: "etc/user_preferences_service_bundle/permissions.textproto"
}

クライアントの認可ポリシー .textproto ファイルには、必要な権限が必要です。

client {
    service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
    allow_all_channels: true
}

client {
    service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
    allow_all_channels: true
}

client {
    service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
    allow_all_channels: true
}

付録: 主要コンセプト

このセクションでは、いくつかの重要なコンセプトについて説明します。

ユーザー

各車両ユーザー(User クラス)は、設定を保存できるアカウントを作成できます。ユーザー設定は、車両の運転手のアカウントのみをサポートします。ゲスト ドライバーは、1 回の使用後に自動的に削除される一時アカウントを作成できます。

message User {
  // Required.
  // A unique ID for the user.
  int32 id = 1;

  // Bit flags that define properties of user. Integer values have to be powers of 2 as they are used as
  // a bit mask.
  enum UserFlags {
    // Due to Protobuff requirement to have the first enum option set to 0,
    // Assign 0 to an unused flag
    UNSET = 0x0;
    // Marks the user as vehicle driver
    DRIVER = 0x01;
    // Ephemeral users have non-persistent state, once another user is selected
    // the profile is deleted automatically
    EPHEMERAL = 0x02;
  }

  // Required.
  // Bitmask for the user flags defined above
  int32 flags = 2;
}

設定グループ

設定グループ(SettingsGroup クラス)は、関連する設定を整理して管理します。車両内の構成可能な各コンポーネントは、Key-Value ペアとして表される設定のリストで構成されるグループとして設定を定義します。たとえば、電動シートを備えた車両では、運転席用の設定グループと助手席用の設定グループを定義できます。両方のグループには、同じ名前の設定が含まれます。

message SettingsGroup {
  // Required.
  // The identifier for this group.
  SettingsGroupId id = 1;

  // Required.
  // The version number of schema used by this setting group.
  string version = 2;

  // Required.
  // The list of settings within the setting group.
  repeated Setting settings = 3;
}

設定

設定(Setting クラス)は、SettingsGroup 内の単一の設定を表します。このメッセージは、設定の名前であるキーと、複数のタイプのいずれかである値で構成されます。

次の例は、設定がキーと値でどのように表されるかを示しています。

// A key value pair representing a setting within a UserControllableService SettingsGroup.
message Setting {
  // Required.
  // A name that uniquely identifies a setting within a SettingsGroup.
  string key = 1;

  // Required.
  // New value of the setting.
  oneof value {
    bool bool = 2;
    float float = 3;
    int32 int32 = 4;
    int64 int64 = 5;
    bytes blob = 6;
    int32 enum = 7;
  }
}

設定の定義

設定定義(SettingDefinition クラス)は、設定グループ内の個々の設定のテンプレートとして機能します。設定がすべてのユーザーで共有されるか、各ユーザーに固有か、外部で管理されるか(パススルー)など、設定の基本的な特性を指定します。

重要なのは、設定定義では、最小値と最大値、許容される増分、オプションの制限付きセットなど、設定値の制約も定義することです。これらの制約により、各設定のデータの完全性と整合性が確保されます。

// The definition of a setting, along with its properties such as type and constraints
message SettingDefinition {
  // Required.
  SettingKind kind = 1;

  // Required.
  SettingWithConstraints setting_with_constraints = 2;
}

enum SettingKind {
  // A setting which is applied for all vehicle users.
  SHARED = 0;
  // Store the value of the setting for each vehicle user separately.
  PER_USER = 1;
  // UserControllableService is fully responsible for the storage of PASSTHROUGH setting.
  // User Preferences only notifies UserControllableService when the value is explicitly set by
  // user. User Preferences does not attempt to request changes based on user change or service
  // registration.
  PASSTHROUGH = 2;
};

message SettingWithConstraints {
  // Required
  Setting setting = 1;

  // Required.
  // Defines restrictions on the setting's value
  oneof constraints {
    FloatConstraints float_constraints = 2;
    Int32Constraints int32_constraints = 3;
    Int64Constraints int64_constraints = 4;
    EnumConstraints enum_constraints = 5;
  }
}

message Int32Constraints {
  // The minimum value that a setting can have.
  optional int32 min_value = 1;
  // The maximum value that a setting can have.
  optional int32 max_value = 2;
  // The step by which a setting's value can be increased or decreased.
  optional int32 step = 3;
}

message Int64Constraints {
  // The minimum value that a setting can have.
  optional int64 min_value = 1;
  // The maximum value that a setting can have.
  optional int64 max_value = 2;
  // The step by which a setting's value can be increased or decreased.
  optional int64 step = 3;
}

message FloatConstraints {
  // The minimum value that a setting can have.
  optional float min_value = 1;
  // The maximum value that a setting can have.
  optional float max_value = 2;
  // The step by which a setting's value can be increased or decreased.
  optional float step = 3;
}

message EnumConstraints {
  // Required.
  // List of unique values sorted in ascending order.
  repeated int32 possible_values = 1;
}

ユーザーが設定を更新したときのイベントのシーケンスの例

この例は、ユーザーが Android Automotive OS(AAOS)の車載インフォテインメント(IVI)を使用して設定を更新したときに発生する一連のイベントを示しています。

ユーザーが設定を変更したときのイベント

図 1. ユーザーが設定を変更したときのイベント。

車両の始動時のイベント シーケンスの例

この例は、車両の起動時に SDV ユーザー設定がユーザーの優先設定を適用する方法を示しています。

車両の起動時のイベント

図 2. 車両の始動時のイベント。