הגדרה וכיול (ConCal)

שירות ההגדרה והכיול (ConCal) בפלטפורמת הרכב מוגדרת התוכנה (SDV) מספק יכולות להגדרת שירותי SDV בהתאם למפרטי הרכב, לתקנות במדינה ולתכונות שהלקוח הזמין. השירות הזה הוא אבן בניין בסיסית בפלטפורמת SDV, והוא מאפשר ליצרני ציוד מקורי לעשות שימוש חוזר באותו קוד שירות בכמה כלי רכב. כדי לעשות זאת, הם צריכים להגדיר את כלי הרכב ולאפשר הגדרה מחדש ממקורות שונים (לדוגמה, במפעל, במרכז שירות או מהענן).

פלטפורמת ה-SDV מספקת ממשקי API שפונים לשירותים לצורך הגדרה וכיול של חבילות שירותים ברכב ספציפי. באמצעות הממשק הזה, יצרני ציוד מקורי יכולים להטמיע לוגיקה של הגדרה וכיול ספציפיים ל-OEM.

שירות ההגדרה והכיול כולל את התהליכים הבאים:

  • ההגדרה, שכוללת הגדרה של המאפיינים וההתנהגות הבסיסיים של הרכב, ועשויה להיות תלויה בכמה גורמים כמו מיקום הרכב, אפשרויות שהמשתמש הזמין או תקנות במדינה. היא מגדירה את האינטראקציה בין הרכיבים ומכתיבה את הגדרות התוכנה שמשפיעות על הפונקציונליות הכוללת של הרכב, כמו גרסאות תוכנה, חיבורי רשת ופרמטרים תפעוליים ראשוניים.

  • כיול, שבו מתבצעת התאמה מדויקת של פרמטרים במערכת בטווחים שהוגדרו מראש. לדוגמה, כיול משפר את הדיוק של חיישנים ומפעילי מנגנון, מבצע אופטימיזציה של ביצועי המנוע לצורך בקרת פליטות, ומשפר את יכולת הנהיגה ואת התגובות של מערכת הבטיחות. ההגדרה קובעת את המסגרת הבסיסית של אופן הפעולה של הרכב, והכיול מייעל את ההתנהגות שלו במסגרת הזו. שניהם חיוניים כדי לוודא שהרכבים עומדים בתקנות בנושא פליטות, למקסם את הביצועים, לשפר את הבטיחות ולפצות על בלאי לאורך זמן.

באמצעות ConCal API סטנדרטי לכלל ה-SDV, אנחנו מפשטים את ההטמעה של חבילות שירות SDV, וכך נמנע הצורך להטמיע מחדש יכולות של הגדרה וכיול כדי להפעיל אותן בכלי רכב שונים מיצרני OEM שונים.

ארכיטקטורה

כל חבילת שירות יכולה להיות הבעלים של פריט הגדרה אחד או יותר.

פריטי הגדרה

ארטיפקט של הגדרה (config) מורכב מפרמטר הגדרה אחד או יותר והערכים שלהם. ההגדרה היא הודעת protobuf ספציפית לשירות, והשדות שלה יכולים לכלול הודעות protobuf מוטמעות (struct), מפות, מערכים, פרמטרים מסוג int, ‏ float, ‏ bool,‏ bytes או 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;
}

מזהה ההגדרה

לכל הגדרה יש מזהה ייחודי. המזהה הזה מורכב משם המופע המלא של הבעלים של חבילת השירותים ומשם ההגדרה. שם ההגדרה צריך להיות קריא, ייחודי לכל חבילת שירותים ולעמוד בתקני השמות שמוגדרים במוסכמות למתן שמות לחבילות שירותים, כמו shared,‏ private,‏ diagnostics ו-calibration.

הגבלות:

  • שם המופע חייב להתחיל באות.
  • כל התווים חייבים להיות אלפאנומריים באותיות קטנות או מקף.
  • אסור שיופיעו יותר מפעם אחת מקפים ברצף בשם.
  • שם ההגדרה לא יכול להסתיים במקף.
  • שם ההגדרה לא יכול להיות ארוך מ-48 תווים.
  • השמות של ההגדרות צריכים להיות ייחודיים באותה מכונה וירטואלית עבור אותו חבילת שירותים.

לפני רבעון 2 בשנת 2026, מזהה ההגדרה מוגדר כך:

// 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 הקרוב ביותר ולהשתמש בו. לדוגמה, אם ConCal נפרס פעם אחת לכל ECU, חבילת שירות צריכה לקבל גישה למופע ConCal שפועל באותו ECU. כך חבילת השירות יכולה לאחזר את ההגדרות בזמן. אם למופע ConCal אין הגדרה מבוקשת (כי הוא שייך לתחום האחריות של ConCal אחר), שרת ConCal שאליו פונים מבקש אותה ממופע ConCal שבבעלותו ומפנה אותה לחבילת השירות.

התאמה אישית של ההגדרות

אחת מהיתרונות העיקריים של SDV היא האפשרות להשתמש שוב באותה תוכנה עם כלי רכב שונים. התוכנה מפותחת פעם אחת ואז נעשה בה שימוש חוזר בכלי רכב רבים, ואנחנו יכולים להתאים את התנהגות התוכנה בהתאם למאפיינים הספציפיים של כלי הרכב. זו המטרה העיקרית של ConCal, שמחשב את הגדרות השירות על סמך מאפייני הרכב בעזרת החלפות של הגדרות.

ConfigOverride היא הודעת protobuf שמתארת איך להתאים את ההגדרה לרכב הספציפי. הוא כולל מזהה של ביטול ברירת המחדל, שמוגדר באופן ייחודי על ידי הישות שמספקת אותו, מזהה תצורה ורשימה של ConfigOverrideKeyValuePair. אפשר לספק את ConfigOverride רק במהלך תהליך העדכון ורק על ידי השירותים המורשים, שמוגדרים על ידי יצרן הציוד המקורי. בהמשך מפורטות הגדרות ה-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. במאמר סקירה כללית על VSIDL ותוכנת ביניים מוסבר איך להשתמש ב-VSIDLC כדי ליצור קשרי לקוח של RPC עבור החבילה.

אתחול תוכנת ביניים לאחזור הגדרות 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 יירשם באמצעות Service Discovery API.

הפעלת ההגדרות

רושמים את ארטיפקט ההגדרה על ידי ציון מזהה לשרת ConCal, סכימת ההגדרה וערכי ההגדרה שמוגדרים כברירת מחדל.

אם קוראים לרישום בפעם הראשונה, ערך ברירת המחדל נשמר. אם קוראים לפונקציה registration בהפעלות הבאות של החבילה, יאוחזר ערך ברירת המחדל שנשמר עם החלפות של 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 מובטחת להצליח, כי ההגדרה נרשמה קודם על ידי אותו חבילה. ההגדרה מאפשרת לחבילת השירות לטפל באופן אטום בשני המקרים של הגדרות המקוריות והגדרות שהוחלפו: הלוגיקה העסקית של חבילת השירות נשארת ללא שינוי.

  • הקריאה GetConfigRequest מחזירה בייטים גולמיים. הפונקציה get_config ממשיכה בניתוח שלהם לסוג ההגדרה הצפוי.