پیکربندی و کالیبراسیون (ConCal)

سرویس پیکربندی و کالیبراسیون (ConCal) در پلتفرم خودروی نرم‌افزارمحور (SDV) قابلیت‌هایی را برای پیکربندی سرویس‌های SDV مطابق با مشخصات خودرو، مقررات کشور و ویژگی‌های سفارش داده شده توسط مشتری فراهم می‌کند. این سرویس یک بلوک ساختاری اساسی در پلتفرم SDV است و به تولیدکنندگان اصلی تجهیزات (OEM) اجازه می‌دهد تا با پیکربندی چندین خودرو و فعال کردن پیکربندی مجدد از منابع مختلف (به عنوان مثال، در یک کارخانه، در یک مرکز خدمات، از طریق فضای ابری)، از یک کد سرویس مشابه در بین آنها استفاده مجدد کنند.

پلتفرم SDV، رابط‌های برنامه‌نویسی کاربردی (API) مرتبط با سرویس را برای پیکربندی و کالیبراسیون بسته‌های خدماتی روی یک وسیله نقلیه خاص ارائه می‌دهد. با استفاده از این رابط، تولیدکنندگان اصلی تجهیزات (OEM) می‌توانند منطق پیکربندی و کالیبراسیون مختص به تولیدکننده اصلی تجهیزات (OEM) را پیاده‌سازی کنند.

خدمات پیکربندی و کالیبراسیون شامل فرآیندهای زیر است:

  • پیکربندی ، که شامل تعریف خواص اساسی و رفتار یک وسیله نقلیه است و ممکن است به عوامل متعددی مانند موقعیت مکانی وسیله نقلیه، گزینه‌های سفارش داده شده توسط کاربر یا مقررات کشور بستگی داشته باشد. این بخش نحوه تعامل اجزا را تعیین می‌کند و تنظیمات نرم‌افزاری را که بر عملکرد کلی وسیله نقلیه تأثیر می‌گذارند، مانند انواع نرم‌افزار، اتصالات شبکه و پارامترهای عملیاتی اولیه، دیکته می‌کند.

  • کالیبراسیون ، که پارامترهای سیستم را در محدوده‌های از پیش تعیین‌شده‌شان تنظیم می‌کند. به عنوان مثال، کالیبراسیون دقت حسگرها و محرک‌ها را تنظیم می‌کند، عملکرد موتور را برای کنترل انتشار گازهای گلخانه‌ای بهینه می‌کند و پاسخ‌های سیستم قابلیت رانندگی و ایمنی را اصلاح می‌کند. پیکربندی، چارچوب اساسی نحوه عملکرد یک وسیله نقلیه را تعیین می‌کند، در حالی که کالیبراسیون، رفتار آن را در آن چارچوب بهینه می‌کند. هر دو برای اطمینان از رعایت مقررات انتشار گازهای گلخانه‌ای توسط وسایل نقلیه، به حداکثر رساندن عملکرد، افزایش ایمنی و جبران فرسایش در طول زمان بسیار مهم هستند.

با ارائه یک API استاندارد ConCal در سطح SDV، پیاده‌سازی بسته‌های خدمات SDV را ساده می‌کنیم و از نیاز به پیاده‌سازی مجدد قابلیت‌های پیکربندی و کالیبراسیون برای اجرا بر روی خودروهای مختلف از تولیدکنندگان اصلی تجهیزات (OEM) مختلف جلوگیری می‌کنیم.

معماری

هر بسته سرویس می‌تواند یک یا چند مصنوعات پیکربندی داشته باشد.

مصنوعات پیکربندی

یک مصنوع پیکربندی (config) شامل یک یا چند پارامتر پیکربندی و مقادیر آنهاست. Configuration یک پیام protobuf مخصوص سرویس است که فیلدهای آن می‌توانند شامل پیام‌های protobuf تو در تو (struct)، نقشه‌ها، آرایه‌ها، int، float، bool، bytes یا پارامترهای رشته‌ای باشند.

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

شناسه پیکربندی

هر پیکربندی یک شناسه منحصر به فرد دارد. این شناسه شامل نام نمونه کاملاً واجد شرایط مالک بسته سرویس و یک نام پیکربندی است. نام پیکربندی باید برای انسان قابل خواندن باشد، برای هر بسته سرویس منحصر به فرد باشد و از استانداردهای نامگذاری تعریف شده در قراردادهای نامگذاری بسته سرویس ، مانند shared ، private ، diagnostics و calibration ، پیروی کند.

محدودیت‌ها:

  • نام نمونه باید با یک حرف شروع شود.
  • همه کاراکترها باید از حروف کوچک و عددی یا خط فاصله باشند.
  • خط فاصله در نام نباید بیش از یک بار پشت سر هم ظاهر شود.
  • نام پیکربندی نباید با خط تیره تمام شود.
  • نام پیکربندی نباید بیش از ۴۸ کاراکتر باشد.
  • نام‌های پیکربندی باید در یک ماشین مجازی واحد برای یک بسته سرویس واحد، منحصر به فرد باشند.

قبل از 26Q2، شناسه پیکربندی به صورت زیر تعریف می‌شود:

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

مالک پیکربندی در هنگام بوت، فقط طرحواره پیکربندی و مقادیر پیش‌فرض آن را می‌داند. برای سفارشی‌سازی رفتار بسته سرویس با خودروی فعلی، بسته سرویس مالک باید پیکربندی پیش‌فرض خود را به همراه طرحواره‌اش ثبت کند.

ثبت پیکربندی پیش‌فرض و بازیابی پیکربندی سفارشی

Figure 1. Registration of default configuration and retrieval of customized configuration.

این به تولیدکنندگان اصلی تجهیزات (OEM) اجازه می‌دهد تا بسته‌های خدماتی را یک بار پیاده‌سازی کرده و آنها را روی چندین وسیله نقلیه اجرا کنند.

استقرار

ConCal می‌تواند یک یا چند نمونه سرور را روی پلتفرم SDV نگهداری کند. بسته‌های سرویس باید نزدیکترین سرور ConCal را کشف و استفاده کنند. به عنوان مثال، اگر ConCal به ازای هر ECU یک سرور مستقر شود، یک بسته سرویس باید به نمونه ConCal که روی همان ECU اجرا می‌شود، دسترسی داشته باشد. این امر به بسته سرویس اجازه می‌دهد تا پیکربندی را به موقع بازیابی کند. اگر نمونه ConCal پیکربندی درخواستی نداشته باشد (زیرا متعلق به حوزه مسئولیت ConCal دیگری است)، سرور ConCal که با آن تماس گرفته شده است، آن را از نمونه ConCal مالک درخواست می‌کند و آن را به بسته سرویس هدایت می‌کند.

سفارشی سازی پیکربندی

قابلیت استفاده مجدد از یک نرم‌افزار مشابه با وسایل نقلیه مختلف یکی از مزایای اصلی SDV است. این نرم‌افزار یک بار توسعه داده می‌شود و سپس روی چندین وسیله نقلیه دوباره استفاده می‌شود و ما می‌توانیم رفتار نرم‌افزار را بر اساس مشخصات وسیله نقلیه تنظیم کنیم. این هدف اصلی ConCal است که پیکربندی سرویس را بر اساس ویژگی‌های وسیله نقلیه با کمک لغو پیکربندی محاسبه می‌کند.

ConfigOverride یک پیام protobuf است که نحوه تنظیم پیکربندی را برای وسیله نقلیه خاص شرح می‌دهد. این پیام شامل یک شناسه override است که به طور منحصر به فرد توسط نهاد ارائه دهنده آن تعریف می‌شود، یک شناسه پیکربندی و لیستی از 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) یا بازنویسی فیلدهای پیچیده، مانند mapها، listها، structها یا یک پیکربندی کامل.

  • حذف یا پاک کردن یک مقدار: این عملیات برای همه نوع‌ها از جمله پیام‌ها، فیلدهای تکراری، نقشه‌ها، فیلدهای تکین و خود پیکربندی ارائه شده است. این عملیات می‌تواند روی یک فیلد کامل یا پیام انجام شود، به این معنی که حذف کلیدهای خاص در یک نقشه و عناصر منفرد از یک فیلد تکراری پشتیبانی نمی‌شود.

  • اضافه کردن یک مقدار جدید به نقشه

  • مقدار یک کلید موجود در یک نقشه را بازنویسی می‌کند (اگر کلید وجود نداشته باشد، این عملیات می‌تواند به صورت اضافه کردن یک مقدار جدید به نقشه نیز انجام شود).

پیکربندی بسته خدماتی با استفاده از 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 را مقداردهی اولیه کنید. در این مثال، ما به صورت غیرهمزمان هنگام شروع بسته نرم‌افزاری مقداردهی اولیه را انجام می‌دهیم.

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 با استفاده از API کشف سرویس ثبت شود.

پیکربندی اولیه

با ارائه یک شناسه، طرحواره پیکربندی و مقادیر پیش‌فرض پیکربندی کارخانه به سرور ConCal، مصنوع پیکربندی را ثبت کنید.

اگر ثبت نام برای اولین بار فراخوانی شود، مقدار پیش‌فرض حفظ می‌شود. اگر ثبت نام در شروع بسته‌های بعدی فراخوانی شود، مقدار پیش‌فرض حفظ شده با لغو 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، یک شناسه و یک مقدار پیش‌فرض را مشخص کند.
  • config_fd نوع پیکربندی است. بسته‌ای که این نوع پیکربندی را در اختیار دارد، تضمین می‌کند که طرحواره مورد انتظار بسته، از جمله به‌روزرسانی‌های پس از APEX، همیشه بازیابی شود.
  • این شناسه به عنوان یک شناسه در منطق پایداری داخلی سرور 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 تضمین می‌شود که با موفقیت انجام شود، زیرا پیکربندی قبلاً توسط همان بسته نرم‌افزاری ثبت شده است. Setup به بسته نرم‌افزاری سرویس اجازه می‌دهد تا به طور مبهم هر دو مورد پیکربندی‌های کارخانه‌ای و پیکربندی‌های لغو شده را مدیریت کند: منطق کسب‌وکار بسته نرم‌افزاری سرویس بدون تغییر باقی می‌ماند.

  • فراخوانی GetConfigRequest بایت‌های خام را برمی‌گرداند. تابع get_config با تجزیه آنها به نوع پیکربندی مورد انتظار، ادامه می‌دهد.