設定與校正 (ConCal)

軟體定義車輛 (SDV) 平台上的設定與校準服務 (ConCal) 可根據車輛規格、國家/地區法規和客戶訂購的功能,設定 SDV 服務。這項服務是 SDV 平台的基本建構區塊,可讓 OEM 透過設定多個來源 (例如工廠、服務中心、雲端) 的重新設定功能,在多部車輛之間重複使用相同的服務程式碼。

SDV 平台提供面向服務的 API,用於設定及校正特定車輛上的服務套裝組合。OEM 可以透過這個介面實作 OEM 專屬的設定和校正邏輯。

設定和校正服務包含下列程序:

  • 設定:定義車輛的基本屬性和行為,可能取決於多項因素,例如車輛位置、使用者訂購的選項或國家/地區法規。這項架構會建立元件的互動方式,並決定影響車輛整體功能的軟體設定,例如軟體變體、網路連線和初始作業參數。

  • 校準:在預先設定的範圍內微調系統參數。舉例來說,校準可調整感應器和致動器的準確度、針對排放控制最佳化引擎效能,以及改善可駕駛性和安全系統回應。設定會為車輛運作方式建立基本架構,校準則會根據該架構最佳化車輛行為。兩者對於確保車輛符合排放法規、發揮最大效能、提升安全性,以及補償長期磨損至關重要。

我們提供標準的 SDV 全面 ConCal API,簡化 SDV 服務套件的實作程序,避免重新實作設定和校準功能,以便在不同 OEM 的不同車輛上執行。

建築

每個服務套裝組合可擁有一或多個設定構件。

設定構件

設定構件 (設定) 包含一或多個設定參數及其值。設定是服務專屬的 protobuf 訊息,欄位可包含巢狀 protobuf 訊息 (struct)、對應、陣列、int、float、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;
}

設定 ID

每項設定都有專屬 ID。這個 ID 包含服務套件擁有者的完整執行個體名稱和設定名稱。設定名稱必須容易辨識,且每個服務套裝組合不得重複,並遵循「服務套裝組合命名慣例」中定義的命名標準,例如 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. 註冊預設設定,以及擷取自訂設定。

這樣一來,原始設備製造商就能實作一次服務套裝組合,並在多部車輛上執行。

部署作業

ConCal 可以在 SDV 平台上維護一或多個伺服器執行個體。服務套件應探索並使用最接近的 ConCal 伺服器。舉例來說,如果每個 ECU 部署一個 ConCal,服務套件應可存取在相同 ECU 上執行的 ConCal 執行個體。這可讓服務套件及時擷取設定。如果 ConCal 執行個體沒有要求設定 (因為屬於其他 ConCal 的責任範圍),則聯絡的 ConCal 伺服器會向擁有 ConCal 執行個體要求設定,並將其重新導向至服務套件。

自訂設定

能夠在不同車輛重複使用相同軟體,是 SDV 的主要優勢之一。軟體開發完成後,即可重複用於多部車輛,且我們可根據車輛規格調整軟體行為。這就是 ConCal 的主要用途,可根據車輛屬性並透過設定覆寫,計算服務設定。

ConfigOverride 是 protobuf 訊息,說明如何針對特定車輛調整設定。其中包含覆寫 ID (由提供覆寫的實體唯一定義)、設定 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 設定註冊和擷取服務的用戶端。如要瞭解如何使用 VSIDLC 為套件產生 RPC 用戶端繫結,請參閱 VSIDL 和中介軟體總覽

初始化中介軟體,以擷取 RPC 設定

在執行階段,初始化 ConCal RPC 呼叫所需的 Middleware 元件。 在本範例中,我們會在啟動套件時非同步初始化。

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 功能:伺服器可能要等到套件啟動後才會可用。因此,範例程式碼會等待 RPC 伺服器使用 Service Discovery API 註冊。

初始化設定

向 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 伺服器內部持續性邏輯中的 ID。
  • 預設值是在 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 會繼續將這些值剖析為預期的設定類型。