Guía de implementación y arquitectura de las preferencias del usuario

En esta página, se proporciona una guía sobre la arquitectura del sistema de preferencias del usuario de SDV y se incluyen instrucciones para implementar un servicio y un cliente controlables por el usuario.

Descripción general de la arquitectura

El sistema de preferencias del usuario desacopla el almacenamiento y la administración de la configuración del usuario de la aplicación y el cumplimiento de esa configuración. En la siguiente tabla, se resumen los términos arquitectónicos clave:

FunciónDescripciónFunciónResponsabilidad
Servicio controlable por el usuario Un paquete de servicios de SDV estándar que administra un dominio específico (por ejemplo, HVAC, asientos de automóvil, audio) Proporciona el estado previsto de las capacidades y las restricciones del hardware o subsistema que controla.
  • Registro: Informa al agente de preferencias del usuario sobre la configuración que admite (metadatos, valores predeterminados y restricciones).
  • Aplicación y autonomía: Recibe solicitudes para cambiar la configuración, las valida según el estado actual y las reglas de seguridad, y las aplica al hardware. Mantiene la autonomía total y especifica si se acepta o rechaza un cambio de configuración.
Agente de preferencias del usuario El organizador central Proporciona almacenamiento centralizado, administración de perfiles de usuario y centro de notificaciones.
  • Almacenamiento: Conserva la configuración por usuario (por ejemplo, conductor, pasajero).
  • Enrutamiento: Los proxies cambian las solicitudes de los clientes (por ejemplo, la interfaz hombre-máquina [HMI]) al servicio apropiado que el usuario puede controlar.
  • Notificaciones: Difunde los cambios a las partes interesadas (vistas o HMI) a través de la interfaz de ChangeNotifier.
  • Administración de usuarios: Controla el cambio de usuario y aplica el estado persistente correcto a todos los servicios registrados.
Cliente de preferencias del usuario Una app o un servicio que proporciona una interfaz de usuario, por ejemplo, una app de IVI con una HMI o alguna otra lógica que necesita interactuar con las preferencias del usuario Interactúa con los usuarios, muestra la configuración e inicia solicitudes de cambio.
  • Pantalla: Presenta la configuración y las restricciones actuales al usuario.
  • Solicitar cambios: Envía al agente de preferencias del usuario las modificaciones de configuración iniciadas por el usuario.
  • Recibir notificaciones: Se suscribe a las actualizaciones de configuración en tiempo real y reacciona a ellas.

Implementa un servicio controlable por el usuario

Para informar al agente de User Preferences qué claves existen, sus tipos de datos, valores predeterminados y restricciones (por ejemplo, valores mínimos y máximos), tu paquete de servicio debe interactuar con la interfaz UserPreferencesRegistryService. Cuando se inicia tu servicio, debe registrar los parámetros de configuración que expone. El agente llama a RequestSettingsChange en tu servicio cuando un usuario intenta modificar un parámetro de configuración (o cuando cambia de usuario).

Sigue los pasos de esta sección para permitir que las preferencias del usuario (y un usuario con HMI) controlen tu servicio.

  1. Define las interfaces de servicio en el archivo VSIDL:

    • Como servidor, implementa com.sdv.google.user_preferences.user_controllable.UserControllableService. Esto permite que el agente te envíe solicitudes de cambio.
    • Como cliente, consume com.sdv.google.user_preferences.UserPreferencesRegistryService. Esto registra tu configuración al inicio.

    En el siguiente ejemplo, se muestra service_bundle.vsidl para un servicio controlado por el usuario:

    service_bundle {
        name: "MyFeatureService"
        server {
            service: "com.sdv.google.user_preferences.user_controllable.UserControllableService"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
        }
    }
    
  2. Registra la configuración en el inicio del prototipo clave user_preferences_registry_service.proto:

    1. Conéctate a UserPreferencesRegistryService.
    2. Crea una instancia de RegisterSettingsRequest.
    3. Define una instancia de SettingsGroup (una colección lógica de parámetros de configuración).
    4. Para cada parámetro de configuración, define lo siguiente:

      *   **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. Llamar a RegisterSettings()

    El siguiente ejemplo está en Rust conceptual:

    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. Cómo controlar las solicitudes de cambio de configuración en user_controllable_service.proto:

    1. Implementa la RPC de RequestSettingsChange.
    2. Valida si los valores solicitados son válidos en el contexto actual (por ejemplo, si el hardware está listo).
    3. Aplica una lógica específica para aplicar el cambio (por ejemplo, mover el asiento o cambiar la velocidad del ventilador).
    4. Devuelve los valores aplicados en RequestSettingsChangeResponse:
    5. Si aceptaste el cambio, devuelve el valor nuevo.
    6. Si rechazaste o restringiste el valor, devuelve el valor que estableciste (o conservaste).

    El siguiente ejemplo está en Rust conceptual:

    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. Implementa el RPC FactoryReset para revertir tu subsistema a un estado limpio:

    1. Restablece todos los parámetros de configuración a sus valores predeterminados (según se definen en la configuración de tu código).
    2. Llama a UpdateSettings en el servicio de registro para informar al agente que los valores cambiaron de forma externa (por el restablecimiento, no por una solicitud del usuario).

Retrocompatibilidad y evolución del esquema

El esquema de la configuración definida por un servicio controlable por el usuario se considera una interfaz pública. El agente de preferencias del usuario y varios clientes (por ejemplo, las HMI) consumen esta interfaz, que puede tener diferentes cronogramas de lanzamiento. Por lo tanto, mantener una estricta retrocompatibilidad es fundamental para evitar la inestabilidad del sistema y la interrupción del cliente.

Cambios compatibles

Un servicio controlable por el usuario solo debe introducir cambios compatibles en su esquema de configuración. Estos cambios garantizan que los clientes más antiguos sigan funcionando correctamente sin actualizaciones:

  • Agrega un nuevo parámetro de configuración opcional con un valor predeterminado razonable:

    • Este parámetro de configuración es opcional, por lo que los clientes deben verificar su presencia para admitir diferentes versiones del servicio.
    • Los clientes ignoran este parámetro de configuración si no se actualizaron para reconocerlo.
    • Si no existe una preferencia del usuario para el nuevo parámetro de configuración, el agente usa el valor predeterminado proporcionado.

Cambios incompatibles

Los siguientes cambios se consideran rotundos y están prohibidos, ya que comprometen de inmediato a los clientes más antiguos:

  • Quita un parámetro de configuración existente.

  • Cambiar el significado, la unidad o el tipo de datos de un parámetro de configuración existente (por ejemplo, cambiar de un número entero que representa la temperatura en grados Celsius a un número de punto flotante que representa la presión en pascales)

  • Cambiar el valor predeterminado de un parámetro de configuración existente La razón principal para prohibir los cambios en los valores predeterminados es la posible complicación con la migración de datos persistentes. El agente almacena la configuración según el esquema de registro del servicio. Cambiar un valor predeterminado requeriría una lógica compleja para migrar todos los perfiles de usuario existentes al nuevo valor predeterminado o correr el riesgo de usar un valor técnicamente incorrecto para los usuarios que nunca establecieron explícitamente la preferencia.

Controla los cambios rotundos obligatorios

Si se requiere un cambio rotundo, el servicio no debe modificar el parámetro de configuración existente. El enfoque para administrar esta transición y mantener la compatibilidad se encuentra principalmente en la lógica de servicio controlable por el usuario:

  1. Crea un parámetro de configuración nuevo con la definición, la clave o la unidad nuevas deseadas.
  2. Registra el parámetro de configuración existente para la compatibilidad y, luego, descarta su uso. Utiliza este nuevo parámetro de configuración cuando desarrolles clientes nuevos.
  3. Recomienda a los clientes nuevos que implementen lógica de compatibilidad en la lógica empresarial del servicio controlable por el usuario.

La implementación de RequestSettingsChange del servicio debe garantizar que un cambio en un parámetro de configuración se refleje en su contraparte obsoleta, y que un cambio en la contraparte se refleje en el parámetro de configuración.

Ejemplo de lógica de compatibilidad:

  1. Un servicio dejó de usar el parámetro de configuración anterior TEMPERATURE_C (Celsius) y, en su lugar, introdujo TEMPERATURE_K (Kelvin).

  2. Cuando el agente envía una solicitud para actualizar el parámetro de configuración nuevo, sucede lo siguiente:

    • El servicio recibe una solicitud para TEMPERATURE_K (por ejemplo, 295.15 K).

    • La lógica del servicio convierte este valor a grados Celsius (22 °C) y, de forma interna, actualiza y conserva ambos valores para mantener la coherencia en los clientes heredados.

  3. Cuando el agente envía una solicitud para el parámetro de configuración en desuso desde un cliente heredado, sucede lo siguiente:

    • El servicio recibe una solicitud para TEMPERATURE_C (por ejemplo, 24 °C).
    • El servicio convierte este valor a Kelvin (297.15 K) y, de forma interna, actualiza y conserva ambos valores.

Esta estrategia de escritura doble garantiza que todos los clientes, independientemente de su programa de lanzamiento, lean datos coherentes y precisos, lo que evita interrupciones causadas por discrepancias en los lanzamientos externos.

Implementa un cliente

Para implementar un cliente (por ejemplo, una HMI) que interactúe con el agente de User Preferences, haz lo siguiente:

  1. Define tu paquete de servicios del cliente para interactuar con interfaces específicas de User Preferences. En tu archivo VSIDL, haz lo siguiente:

    • Como servidor, implementa com.sdv.google.user_preferences.view.ChangeNotifier para permitir que el agente envíe a tu cliente notificaciones en tiempo real sobre los cambios en la configuración.
    • Como cliente, usa com.sdv.google.user_preferences.UserPreferencesManagementService para solicitar modificaciones de la configuración y suscribirte a las actualizaciones.
    • Como cliente, usa com.sdv.google.user_preferences.UserPreferencesAdminService para administrar perfiles de usuario (por ejemplo, crear, seleccionar, borrar y restablecer la configuración de fábrica).

    En el siguiente ejemplo, se muestra un archivo service_bundle.vsidl para un cliente:

    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"
        }
    }
    

    Agrega las políticas de autorización según corresponda. Para permitir la comunicación del agente de preferencias del usuario, crea un cliente para los servidores especificados. Por ejemplo:

    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. Puedes cambiar la configuración de la solicitud en UserPreferencesManagementService:

    1. Conéctate a UserPreferencesManagementService.
    2. Crea una instancia de RequestSettingsChangeRequest y especifica el SettingsGroupId y la configuración deseada.
    3. Opcional. Establece ChangePersistencePolicy en PERSISTENT_CHANGE (predeterminado) o NON_PERSISTENT_CHANGE.
    4. Llamar a RequestSettingsChange()

    El siguiente ejemplo está en Rust conceptual:

    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. Suscríbete a los cambios de configuración con user_preferences_management_service.proto y change_notifier.proto:

    1. Implementa la RPC de OnSettingsChange desde la interfaz de ChangeNotifier dentro de tu paquete de servicios. El agente de User Preferences invoca este método cuando se produce un cambio.
    2. Conéctate a UserPreferencesManagementService.
    3. Crea un objeto SubscribeToSettingsChangeAndGetSettingsRequest que especifique el objeto SettingsGroupId que deseas supervisar.
    4. Para devolver el estado de la configuración y, luego, enviar actualizaciones futuras a través de tu implementación de OnSettingsChange, llama a SubscribeToSettingsChangeAndGetSettings().

    El siguiente ejemplo de implementación de la interfaz ChangeNotifier está en Rust conceptual:

    #[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())
        }
    }
    

    El siguiente ejemplo de suscripción está en Rust conceptual:

    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. Opcional: Los clientes que administran perfiles de usuario (por ejemplo, una app de configuración dedicada) pueden usar user_preferences_admin_service.proto para interactuar con el servicio de administrador:

    1. Conéctate a UserPreferencesAdminService.
    2. Usa RPCs como CreateUser, SelectUser, DeleteUser, FactoryReset y ListUsers para administrar los perfiles de usuario.

    El siguiente ejemplo de creación de un usuario está en Rust conceptual:

    admin_service_client.CreateUser(&CreateUserRequest {
        user: Some(User {
            id: 1,
            flags: UserFlags::DRIVER.value(),
            ..Default::default()
        }).into(),
        ..Default::default()
    }).await?;
    
  5. Descubre la configuración y los usuarios disponibles a través del servicio de administrador con user_preferences_admin_service.proto:

    1. Llama a ListUsers() en UserPreferencesAdminService para obtener una lista de todos los usuarios registrados.
    2. Para obtener la configuración de un usuario específico, llama a GetUserSettings(user_id) en el UserPreferencesAdminService.

    El siguiente ejemplo de cómo enumerar usuarios y obtener la configuración está en Rust conceptual:

    // 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);
    }
    

Resumen del flujo

  1. El cliente se inicia y se conecta a los servicios administrativos y de administración del agente.
  2. El cliente descubre los grupos de configuración y se suscribe a ellos, y muestra el estado inicial.
  3. El cliente envía un RequestSettingsChange al agente.
  4. El agente envía una notificación OnSettingsChange al cliente con la configuración y el estado actualizados.

Para ver un ejemplo completo, consulta HMIService en @samples/user_preferences/v1/.

Implementa el agente

Un paquete de servicios iniciado por el organizador implementa el agente de preferencias del usuario. Para mayor comodidad, se proporciona una implementación de referencia.

Además, se proporciona este archivo user_preferences_sample.vsidl de paquete de servicio de muestra para el agente:

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"
    }
}

La implementación puede usar el middleware generado y debe compilarse en un rust_ffi_shared al que hace referencia el paquete de servicio que implementa el agente de preferencias del usuario:

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"
}

El archivo de política de autorización .textproto del cliente requiere los permisos necesarios:

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
}

Apéndice: Conceptos clave

En esta sección, se describen algunos conceptos clave.

Usuarios

Cada usuario del vehículo (clase User) puede crear una cuenta en la que puede almacenar sus preferencias. Las preferencias del usuario solo admiten cuentas para conductores de vehículos. Los conductores invitados pueden crear cuentas temporales que se borran automáticamente después de un uso.

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;
}

Grupos de configuración

Los grupos de configuración (clase SettingsGroup) organizan y administran la configuración relacionada. Cada componente configurable del vehículo define sus parámetros de configuración como grupos, que consisten en una lista de parámetros de configuración representados como pares clave-valor. Por ejemplo, los vehículos con asientos eléctricos pueden definir un grupo de configuración para el asiento del conductor y otro para el del pasajero delantero, en los que ambos grupos contienen parámetros de configuración con el mismo nombre.

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;
}

Configuración

El parámetro de configuración, el mensaje (clase Setting), representa un solo parámetro de configuración dentro de SettingsGroup. Este mensaje consta de una clave, que es el nombre del parámetro de configuración, y un valor, que puede ser de varios tipos.

En este ejemplo, se muestra cómo se representa un parámetro de configuración con una clave y un valor:

// 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;
  }
}

Cómo configurar definiciones

Las definiciones de configuración (clase SettingDefinition) sirven como plantillas para la configuración individual dentro de un grupo de configuración. Especifican las características fundamentales del parámetro de configuración, como si se comparte entre todos los usuarios, si es específico para cada usuario o si se administra de forma externa (transferencia).

Es importante destacar que los parámetros de configuración también definen las restricciones sobre el valor del parámetro, como los valores mínimos y máximos, los incrementos permitidos o un conjunto restringido de opciones. Estas restricciones garantizan la integridad y la coherencia de los datos para cada parámetro de configuración.

// 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;
}

Ejemplo de secuencia de eventos cuando un usuario actualiza un parámetro de configuración

En este ejemplo, se ilustra la secuencia de eventos que se producen cuando un usuario usa el sistema de infoentretenimiento (IVI) integrado en el vehículo con el SO Android Automotive (AAOS) para actualizar un parámetro de configuración:

Eventos cuando el usuario cambia la configuración

Figura 1: Son los eventos que se producen cuando un usuario cambia un parámetro de configuración.

Ejemplo de secuencia de eventos cuando se inicia un vehículo

En este ejemplo, se ilustra cómo las preferencias del usuario de SDV aplican la configuración preferida del usuario cuando se inicia un vehículo:

Eventos cuando el vehículo está en marcha

Figura 2: Eventos cuando se pone en marcha un vehículo.