Konfiguracja i kalibracja (ConCal)

Usługa konfiguracji i kalibracji (ConCal) na platformie pojazdu definiowanego przez oprogramowanie (SDV) umożliwia konfigurowanie usług SDV zgodnie ze specyfikacjami pojazdu, przepisami krajowymi i funkcjami zamówionymi przez klienta. Ta usługa jest podstawowym elementem platformy SDV, który umożliwia producentom OEM ponowne wykorzystywanie tego samego kodu usługi w wielu pojazdach przez ich konfigurowanie i włączanie rekonfiguracji z wielu źródeł (np. w fabryce, w serwisie, w chmurze).

Platforma SDV udostępnia interfejsy API dla usług, które umożliwiają konfigurowanie i kalibrowanie pakietów usług w konkretnym pojeździe. Dzięki temu interfejsowi producenci OEM mogą wdrażać logikę konfiguracji i kalibracji specyficzną dla OEM.

Usługa konfiguracji i kalibracji obejmuje te procesy:

  • Konfiguracja, która polega na określeniu podstawowych właściwości i zachowania pojazdu i może zależeć od wielu czynników, takich jak lokalizacja pojazdu, opcje zamówione przez użytkownika lub przepisy krajowe. Określa, jak komponenty wchodzą ze sobą w interakcje, i dyktuje ustawienia oprogramowania, które wpływają na ogólną funkcjonalność pojazdu, takie jak warianty oprogramowania, połączenia sieciowe i początkowe parametry operacyjne.

  • Kalibracja, która precyzyjnie dostraja parametry systemu w ich wstępnie skonfigurowanych zakresach. Na przykład kalibracja dostosowuje dokładność czujników i siłowników, optymalizuje wydajność silnika pod kątem kontroli emisji oraz udoskonala reakcje układu jezdnego i układu bezpieczeństwa. Konfiguracja określa podstawowe ramy działania pojazdu, a kalibracja optymalizuje jego zachowanie w tych ramach. Oba te procesy są niezbędne, aby pojazdy spełniały wymagania dotyczące emisji, maksymalizowały wydajność, zwiększały bezpieczeństwo i mogły kompensować zużycie w czasie.

Dzięki udostępnieniu standardowego interfejsu ConCal API dla SDV upraszczamy wdrażanie pakietów usług SDV, eliminując konieczność ponownego wdrażania funkcji konfiguracji i kalibracji w celu uruchamiania ich w różnych pojazdach różnych producentów OEM.

Architektura

Każdy pakiet usług może mieć co najmniej 1 artefakt konfiguracji.

Artefakty konfiguracji

Artefakt konfiguracji (config) składa się z co najmniej 1 parametru konfiguracji i jego wartości. Konfiguracja to komunikat protobuf specyficzny dla usługi, którego pola mogą zawierać zagnieżdżone komunikaty protobuf (struct), mapy, tablice, parametry int, float, bool, bytes lub 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;
}

Identyfikator konfiguracji

Każda konfiguracja ma unikalny identyfikator. Ten identyfikator składa się z pełnej nazwy instancji właściciela pakietu usług i nazwy konfiguracji. Nazwa konfiguracji musi być czytelna dla człowieka, unikalna w ramach pakietu usług, i zgodna ze standardami nazewnictwa określonymi w Konwencje nazewnictwa pakietów usług, takimi jak shared, private, diagnostics, i calibration.

Ograniczenia:

  • Nazwa instancji musi zaczynać się literą.
  • Wszystkie znaki muszą być małymi literami alfanumerycznymi lub łącznikiem.
  • Łączniki w nazwie nie mogą występować kolejno więcej niż raz.
  • Nazwa konfiguracji nie może kończyć się łącznikiem.
  • Nazwa konfiguracji nie może mieć więcej niż 48 znaków.
  • Nazwy konfiguracji muszą być unikalne na tej samej maszynie wirtualnej w przypadku tego samego pakietu usług.

Przed 26Q2 identyfikator konfiguracji jest definiowany w ten sposób:

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

Właściciel konfiguracji podczas uruchamiania zna tylko schemat swojej konfiguracji i jej wartości domyślne. Aby dostosować zachowanie pakietu usług do bieżącego pojazdu, pakiet usług będący jego właścicielem musi zarejestrować swoją konfigurację domyślną wraz ze schematem.

Rejestracja konfiguracji domyślnej i pobieranie konfiguracji dostosowanej

Rysunek 1. Rejestracja konfiguracji domyślnej i pobieranie konfiguracji dostosowanej.

Dzięki temu producenci OEM mogą wdrażać pakiety usług raz i uruchamiać je w wielu pojazdach.

Wdrożenie

ConCal może utrzymywać co najmniej 1 instancję serwera na platformie SDV. Pakiety usług powinny wykrywać i używać najbliższego serwera ConCal. Jeśli na przykład ConCal jest wdrażany po jednym na ECU, pakiet usług powinien uzyskać dostęp do instancji ConCal działającej na tym samym ECU. Dzięki temu pakiet usług może szybko pobierać konfigurację. Jeśli instancja ConCal nie ma żądanej konfiguracji (ponieważ należy ona do obszaru odpowiedzialności innego ConCal), skontaktowany serwer ConCal wysyła żądanie do instancji ConCal będącej właścicielem i przekierowuje je do pakietu usług.

Dostosowywanie konfiguracji

Możliwość ponownego wykorzystywania tego samego oprogramowania w różnych pojazdach to jedna z głównych zalet SDV. Oprogramowanie jest opracowywane raz, a następnie ponownie wykorzystywane w wielu pojazdach. Możemy dostosować jego zachowanie do specyfiki pojazdu. To jest główny cel ConCal, który oblicza konfigurację usługi na podstawie właściwości pojazdu za pomocą zastąpień konfiguracji.

ConfigOverride to komunikat protobuf opisujący, jak dostosować konfigurację do konkretnego pojazdu. Składa się z identyfikatora zastąpienia, który jest jednoznacznie określony przez podmiot go udostępniający, identyfikatora konfiguracji i listy ConfigOverrideKeyValuePair. ConfigOverride można podać tylko podczas procesu aktualizacji i tylko przez dozwolone usługi, co jest modelowane przez OEM. Definicje protobuf obu struktur znajdziesz poniżej.

// 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 obsługuje te operacje:

  • Przypisanie nowej wartości: ostatnia wartość jest wycofana, a parametr otrzymuje nową wartość, np. przypisanie nowej wartości do prostego pola (int, string, float, bool, bytes) lub przepisanie złożonych pól, takich jak mapy, listy, struktury lub cała konfiguracja.

  • Usuwanie lub czyszczenie wartości: ta operacja jest dostępna w przypadku wszystkich typów, w tym wiadomości, pól powtarzanych, map, pól pojedynczych i samej konfiguracji. Tę operację można wykonać na całym polu lub wiadomości, co oznacza, że usuwanie konkretnych kluczy w mapie i poszczególnych elementów z pola powtarzanego nie jest obsługiwane.

  • Dodawanie nowej wartości do mapy.

  • Przepisywanie wartości istniejącego klucza w mapie (tę operację można też wykonać jako dodanie nowej wartości do mapy, jeśli klucz nie istnieje).

Konfigurowanie pakietu usług za pomocą ConCal

W tym rozdziale opisujemy, jak opracować pakiet usług, który pobiera swoją konfigurację w czasie działania. Z perspektywy konfigurowalnego pakietu nie ma znaczenia, czy pobrana konfiguracja jest ustawieniem fabrycznym, czy późniejszą modyfikacją za pomocą procesów zastępowania i kalibracji ConCal.

Przykładowy kod, na którym opiera się dokumentacja, znajdziesz w system/software_defined_vehicle/samples/concal/src/concal_client.

Więcej informacji znajdziesz w artykule Tworzenie pakietu usług.

Deklarowanie typu konfiguracji należącej do pakietu usług

Pakiet usług jest właścicielem typu konfiguracji, którą pobiera. Dzięki temu konfigurowalny pakiet można aktualizować niezależnie od utrwalonych danych konfiguracji.

  1. Napisz plik protobuf (z rozszerzeniem .proto) deklarujący typ konfiguracji:

    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. Utwórz cel kompilacji, który generuje bibliotekę środowiska wykonawczego umożliwiającą pobieranie typu konfiguracji. W pliku 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",
    }
    

Generowanie kodu pośredniego ConCal RPC dla pakietu

Dodaj do pakietu usług deklarację VSIDL:

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

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

Deklaruje to, że pakiet jest klientem usługi rejestracji i pobierania konfiguracji ConCal. Omówienie korzystania z VSIDLC do generowania powiązań klienta RPC dla pakietu znajdziesz w artykule VSIDL i omówienie oprogramowania pośredniego.

Inicjowanie oprogramowania pośredniego do pobierania konfiguracji RPC

W czasie działania zainicjuj komponenty oprogramowania pośredniego wymagane do wywołań ConCal RPC. W tym przykładzie inicjujemy asynchronicznie po uruchomieniu pakietu.

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

Gdzie:

  • W on_start tworzymy środowisko wykonawcze tokio i uruchamiamy zadanie tokio. Zadanie wywołuje setup_register_rpc przed przejściem do głównej logiki pakietu.
  • setup_register_rpc konfiguruje powiązanie RPC oprogramowania pośredniego. Pamiętaj, że przykład nie zakłada, że funkcje ConCal są implementowane przez agenta: serwer może stać się dostępny dopiero po uruchomieniu pakietu. Dlatego przykładowy kod czeka na zarejestrowanie serwera RPC za pomocą interfejsu Service Discovery API.

Inicjowanie konfiguracji

Zarejestruj artefakt konfiguracji, podając serwerowi ConCal identyfikator, schemat konfiguracji oraz domyślne wartości konfiguracji fabrycznej.

Jeśli rejestracja jest wywoływana po raz pierwszy, wartość domyślna jest utrwalana. Jeśli rejestracja jest wywoływana podczas kolejnych uruchomień pakietu, pobierana jest utrwalona wartość domyślna z zastosowanymi zastąpieniami ConCal (jeśli takie istnieją).

Wywołanie rejestracji konfiguracji ConCal nigdy się nie powiedzie, niezależnie od tego, czy artefakt jest już zarejestrowany.

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",
        );
}

Gdzie:

  • Zadanie uruchomione w on_start po pobraniu powiązania RPC przechodzi do logiki biznesowej, wywołując sample_concal_main.
  • sample_concal_main zaczyna od zarejestrowania artefaktu konfiguracji. Logika jest zawarta w register_config.
  • Aby zarejestrować konfigurację, pakiet musi określić jej typ protobuf, identyfikator i wartość domyślną.
  • config_fd to typ konfiguracji. Pakiet będący właścicielem typu konfiguracji zapewnia, że zawsze pobierany jest schemat oczekiwany przez pakiet, w tym po aktualizacjach APEX.
  • Identyfikator jest używany jako identyfikator w wewnętrznej logice utrwalania serwera ConCal.
  • Wartość domyślna jest tworzona w get_rear_view_camera_factory_config. Wartość domyślna to wartość utrwalona przez serwer ConCal, jeśli wcześniej nie utrwalono żadnej wartości. Jest to jeden ze sposobów, w jaki system może określać konfiguracje fabryczne. Możliwe są też inne konfiguracje.

Pobieranie konfiguracji

Po rejestracji pobierz konfigurację. Ponieważ konfiguracja została wcześniej zarejestrowana, to wywołanie na pewno się powiedzie.

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

Gdzie:

  • Wywołanie GetConfigRequest na pewno się powiedzie, ponieważ konfiguracja została wcześniej zarejestrowana przez ten sam pakiet. Konfiguracja umożliwia pakietowi usług nieprzezroczyste obsługiwanie zarówno konfiguracji fabrycznych, jak i zastąpionych: logika biznesowa pakietu usług pozostaje niezmieniona.

  • Wywołanie GetConfigRequest zwraca surowe bajty. Funkcja get_config analizuje je do oczekiwanego typu konfiguracji.