配置和校准 (ConCal)

软件定义汽车 (SDV) 平台上的配置和校准服务 (ConCal) 能够根据汽车规格、国家/地区法规和客户订购的功能配置 SDV 服务。 此服务是 SDV 平台的基本构建块,让原始设备制造商 (OEM) 能够通过配置在多辆汽车之间重复使用相同的服务代码,并支持从多个来源(例如,在工厂、服务中心、云端)重新配置。

SDV 平台提供面向服务的 API,用于配置和校准特定汽车上的服务软件包。借助此接口,OEM 可以实现 OEM 特有的配置和校准逻辑。

配置和校准服务包括以下流程:

  • 配置,涉及定义汽车的基本属性和 行为,可能取决于多种因素,例如汽车 位置、用户订购的选项或国家/地区法规。它确定了组件之间的交互方式,并规定了影响汽车整体功能的软件设置,例如软件变体、网络连接和初始操作参数。

  • 校准,用于在预配置的 范围内微调系统参数。例如,校准可调整传感器和执行器的准确性,优化发动机性能以控制排放,并改进驾驶性能和安全系统响应。配置为汽车的功能设置了基本框架,而校准则在该框架内优化了汽车的行为。这两者对于确保汽车符合排放法规、最大限度地提高性能、增强安全性以及弥补长期磨损至关重要。

通过提供标准的 SDV 范围内的 ConCal API,我们简化了 SDV 服务软件包的实现,避免了为在不同 OEM 的不同汽车上运行而重新实现配置和校准功能。

架构

每个服务软件包都可以拥有一个或多个配置工件。

配置工件

配置工件(配置)由一个或多个配置参数及其值组成。配置是一种特定于服务的 protobuf 消息,其字段可以包含嵌套的 protobuf 消息(结构体)、映射、数组、int、浮点数、bool、字节或字符串参数。

// 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 个字符。
  • 对于同一服务软件包,配置名称在同一虚拟机上必须是唯一的。

在 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 平台上维护一个或多个服务器实例。服务软件包应发现并使用最近的 ConCal 服务器。例如,如果每个 ECU 部署一个 ConCal,则服务软件包应能够访问在同一 ECU 上运行的 ConCal 实例。这样,服务软件包就可以及时检索配置。如果 ConCal 实例没有请求的配置(因为它属于另一个 ConCal 的责任范围),则联系的 ConCal 服务器会从拥有 ConCal 实例请求该配置,并将其重定向到服务软件包。

配置自定义

能够将同一软件重复用于不同的汽车是 SDV 的主要优势之一。软件开发一次,然后重复用于多辆汽车,我们可以根据汽车的具体情况调整软件的行为。这是 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. 创建一个 build 目标,用于生成允许检索配置类型的运行时库。在 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 配置注册和检索服务的客户端。如需大致了解如何使用 VSIDLC 为软件包生成 RPC 客户端绑定,请参阅 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 服务器保留的值。这表示系统可以指定出厂配置的方式之一。还可以进行其他设置。

检索配置

注册后,检索配置。由于配置之前已注册,因此保证此调用成功。

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 会继续将这些字节解析为预期的配置类型。