在 IVI 上使用 SDV 网关

车载信息娱乐 (IVI) 系统上的软件定义型汽车 (SDV) 网关有助于 IVI 系统与远程 SDV 服务之间的通信。 借助该网关,OEM Java 应用和原生服务(例如 VHAL)可以与 SDV 服务进行交互。该网关使用既定的通信方法,包括服务注册、发现和 RPC。

此网关满足特定的 AAOS SDV 项目要求,例如使用 SDV 数据隧道为属性信息启用 VHAL 参考实现。它还允许 IVI 上的 Java 和 Kotlin Android 应用使用 SDV Comms 堆栈,注册为服务,查找其他 SDV 服务,并与它们通信。

如需了解 SDV 网关代码位置和 SDV 网关客户端示例,请参阅 代码位置

SDV 网关集成模型

通过 IVI 架构上的 SDV 网关使用 SDV-IVI Comms

SDV 网关与 Java 应用、原生服务、SDV Comms 堆栈和车辆网络进行交互。图 1 展示了这些交互:

SDV 网关互动

图 1. SDV 网关交互。

在此系统图中:

  • 应用通过其客户端服务与 SDV 网关进行交互。
  • AAOS SDV SDK 为您提供:
    • 用于进程间通信的 ISdvGateway AIDL API。
    • 用于网络交互的 Comms 库。
    • 用于原生服务集成的便捷 C API。
  • ISdvGateway AIDL API 由 SDV 网关服务和子系统实现。
  • SDV 网关服务管理以下方面:
    • 双向服务发现。
    • 与远程 SDV 服务通信。
    • 核心业务逻辑。
  • SDV 网关子系统连接到车辆网络。
  • 原生服务(包括 VHAL 实现)可以直接使用 ISdvGateway AIDL API,也可以通过 SDK 的 C API 使用。
  • VHAL 代理充当参考 VHAL 实现,并整合了 VSIDL 映射集成。

IVI 原生服务上的 SDV 网关的集成模型

集成模型如图 2 所示:

SDV 网关集成模型

图 2. SDV 网关集成模型。

使用 IVI 原生服务上的 SDV 网关

图 3 展示了在 IVI 上使用 SDV 网关:

IVI 上的 SDV 网关

图 3. IVI 上的 SDV 网关。

前提条件

启动 Binder 线程池:

  • SDV 网关客户端库需要已启动的 Binder 线程池,才能接收来自 Binder 服务的异步回调。

  • 如果未启动 Binder 线程池,则创建 SDV 网关客户端所需的 API 会失败。

添加原生网关客户端库

原生网关客户端库公开了 C API。添加 libsdvgatewayclient 的实例作为依赖项,以使用 C API:

cc_binary {
    name: "your_binary_name",
    srcs: ["main.cpp"],
    shared_libs: [
        "libsdvgatewayclient",
    ],
}

加载原生网关客户端

#include "libsdvgatewayclient.h"

创建原生客户端实例

ASDVGateway_Client* client;
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_new(
     &client, /*outStatus*/nullptr);

创建客户端后,客户端:

  • 保留与网关服务的所有后续交互的状态。

  • 充当与启用 SDV 的其他服务的所有交互的上下文,并作为第一个参数传递给 C API 函数。

状态代码和错误消息

大多数 C API 函数都有以下定义:

ASDVGateway_StatusCode_t ApiFunctionName(..., ASDVGateway_Status_t* outStatus);

如需评估成功与否,您可以检查返回的状态代码,其类型为 ASDVGateway_StatusCode_t。您还可以将指针传递给一个结构,函数可以在其中填充状态代码和错误消息。该指针作为最后一个参数传递,名为 outStatus。null 值表示不使用输出结构。

调用方必须为错误消息分配内存状态结构。状态结构可以同时保存状态代码和错误消息。您可以查看创建新客户端时检索错误消息的示例。

如需评估成功与否,请执行以下操作:

  1. 检查返回的状态代码,其类型为 ASDVGateway_StatusCode_t

  2. 将指针传递给一个结构,函数可以在其中填充状态代码和错误消息。

    • 该指针作为最后一个参数传递,名为 outStatus
    • null 值表示不使用输出结构。
    • 调用方必须为错误消息分配内存状态结构。
    • 状态结构可以保存状态代码和错误消息。

以下示例展示了如何检索新客户端的错误消息:

#include <iostream>
#include <array>

struct StatusWithErrorMsg : ASDVGateway_Status_t {
    StatusWithErrorMsg() {
        // Ensure the base struct pointers point to our internal buffer
        errorMessage = errorMessageBuffer.data();
        maxErrorMessageSize = errorMessageBuffer.size();

        // Good practice: Zero-initialize the buffer
        errorMessageBuffer.fill(0);
    }

    std::array<char, 256> errorMessageBuffer;
};

// --- Execution ---

ASDVGateway_Client* client = nullptr;
StatusWithErrorMsg status;

// Initialize the client
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_new(&client, &status);

// Log Results
std::cout << "Returned statusCode:    " << statusCode << std::endl;
std::cout << "Status Struct Code:     " << status.statusCode << std::endl;
std::cout << "Status Error Message:   " << status.errorMessage << std::endl;

在此示例中:

  • 状态结构中包含的状态代码与返回的状态代码具有相同的值。

  • 错误消息仅填充到 maxErrorMessageSize 字符限制(包括 null 终止字符 \0)。如果未发生错误(状态代码为 OK),则错误消息为空字符串。

初始化通信

初始化通信会使用 SDV Comm Stack 和 SDV 网关初始化调用方应用与其他应用之间的通信。可以在以下两种情况下调用初始化通信:

  • 创建 客户端后。

  • 在任何数据隧道、RPC 或服务发现交互之前

示例如下:

ASDVGateway_InitCommsParams_t params{
    .packageName         = "android.sdv.samples.gateway.client",
    .serviceBundleName   = "NativeTestApp",
    .serviceInstanceName = "default",
};

// Initialize communications and capture the status code
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_initComms(client, &params, &status);

// Recommended check
if (statusCode != ASDVGateway_StatusCode_OK) {
    std::cerr << "Failed to init comms: " << status.errorMessage << std::endl;
}

服务发现

当注册或取消注册特定类型或名称的服务单元时,您可以接收通知。回调函数会在网关客户端拥有的线程中收到通知。此回调最初是在 C API 调用后立即触发的,其中包含所有已注册的服务单元。初始触发后,通知会继续更新,直到显式取消注册监听器。

class NativeSdvGatewayTestApp {
public:
    static void ServiceUnitChangeListenerCallback(
        const ASDVGateway_ServiceUnitChangeEventType eventType,
        const ASDVGateway_ServiceUnitDefinition* serviceUnitDefinition,
        void* userData
    ) {
        auto* testApp = reinterpret_cast<NativeSdvGatewayTestApp*>(userData);

        if (testApp) {
            // Logic to react when services are registered or unregistered
            // e.g., testApp->handleServiceChange(eventType, serviceUnitDefinition);
        }
    }
};

// --- Listener Registration ---

// Ensure thisApp remains in scope as long as the listener is active
NativeSdvGatewayTestApp* thisApp = get_current_app_context();

ASDVGateway_UnitType_t unitType{
    .sdvPackageName     = "android.sdv.samples.sdv_gateway",
    .serviceBundleName  = "DtPublisher",
    .unitTypeName       = "TirePressure",
};

// Register the listener
ASDVGateway_Client_registerListenerForServiceUnitChangeByType(
    client,
    &unitType,
    NativeSdvGatewayTestApp::ServiceUnitChangeListenerCallback,
    static_cast<void*>(thisApp), // userData
    &listenerHandle,
    &status
);

// --- Cleanup ---

// Unregistering stops all notification to the callback function
ASDVGateway_Client_unregisterListenerForServiceUnitChangeByType(
    client,
    listenerHandle,
    &status
);

如需检索已注册的服务单元而不接收进一步的通知,请使用 ASDVGateway_Client_fetchServiceUnitsByTypeASDVGateway_Client_fetchServiceUnitsByName API。

class NativeSdvGatewayTestApp {
public:
    /**
     * Callback triggered for each service unit found that matches the requested type.
     */
    static void FetchServiceUnitsCallback(
        const ASDVGateway_ServiceUnitDefinition* serviceUnitDefinition,
        void* userData
    ) {
        auto* app = reinterpret_cast<NativeSdvGatewayTestApp*>(userData);

        if (serviceUnitDefinition) {
            // The service unit is registered with service discovery.
            // Example: processServiceUnit(serviceUnitDefinition);
        }
    }
};

// --- Execution ---

ASDVGateway_UnitType_t unitType{
    .sdvPackageName     = "android.sdv.samples.sdv_gateway",
    .serviceBundleName  = "DtPublisher",
    .unitTypeName       = "TirePressure",
};

// Context used to differentiate between various fetchServiceUnits calls
void* userData = static_cast<void*>(thisApp);

ASDVGateway_Client_fetchServiceUnitsByType(
    client,
    &unitType,
    NativeSdvGatewayTestApp::FetchServiceUnitsCallback,
    userData,
    &status
);

ASDVGateway_Client_fetchServiceUnitsByType API 调用期间,回调会在调用方的线程中同步触发。使用 ASDVGateway_Client_fetchServiceUnitsByName 按名称而不是按单元类型获取已注册的服务单元:

ASDVGateway_StatusCode_t ASDVGateway_Client_fetchServiceUnitsByName(
    // [in]  Opaque pointer to a client object.
    const ASDVGateway_Client* client,

    // [in]  Pointer to a structure containing the package name,
    //       service bundle name, and service unit name.
    const ASDVGateway_UnitNameDiscoveryArgs_t* unitName,

    // [in]  Callback function to be called for each service unit
    //       definition registered. Called synchronously in the
    //       caller's thread before the fetch completes.
    ASDVGateway_FetchServiceUnitsCallback callback,

    // [in]  Optional value passed back as a parameter of the callback.
    void* userData,

    // [out] Optional pointer to status structure for result
    //       codes and error messages.
    ASDVGateway_Status_t* outStatus
);

RPC 流程

网关客户端库处理与其他启用 SDV 的应用通信的传输层安全协议 (TLS) 设置。具体而言,它会检索通信所需的 TLS 设置。以下介绍了如何确定 TLS 使用情况:

  • 当 SDV 启动模式为 LOCKED 时,使用 TLS

  • 当 SDV 启动模式为 UNLOCKED 时,使用不安全 通信。使用 TLS 时,网关客户端库会生成一个仅应用进程知道的密钥对。客户端检索 RPC 凭据以创建 RPC 服务器或 RPC 客户端通道。RPC 使用专用的 SDV-RPC VLAN。 网关客户端库会调用 android_setprocnetwork,以将进程的 默认网络切换到 SDV-RPC VLAN,无论是在 ASDVGateway_Client_initComms 调用期间还是之后。

RPC 可用性

配置为在前期启动时启动的客户端可能会在 SDV-RPC VLAN 可用之前启动。在尝试创建任何 RPC 服务器或 RPC 客户端套接字之前,请检查 RPC 是否可用:

// Check if the RPC (Remote Procedure Call) service is available
bool isRpcAvailable = ASDVGateway_Client_isRpcAvailable(client);

if (isRpcAvailable) {
    // Proceed with RPC calls
} else {
    // Handle the case where RPC is not yet ready or available
}

// Check if the process is correctly bound to the SDV RPC Network Interface/VLAN
bool isRpcNetworkBound = ASDVGateway_Client_isProcessBoundToSdvRpcNetworkInterface(client);

if (!isRpcNetworkBound) {
    // Usually implies the process isn't running on the correct network interface
    // or the VLAN configuration is missing.
    std::cerr << "Warning: Process is not bound to the SDV RPC VLAN." << std::endl;
}

使用 ASDVGateway_Client_setClientNotificationCallback 为客户端事件设置监听器,以便在 RPC 可用性状态发生变化时收到通知。 如果应用切换了进程的网络,则首选 ASDVGateway_Client_isProcessBoundToSdvRpcNetworkInterface,因为它会同时检查 RPC 是否可用以及进程是否绑定到 SDV-RPC VLAN。

切换进程的默认网络

您可能需要将进程的默认网络从 SDV-RPC VLAN 切换为打开套接字以进行互联网绑定连接。调用 ASDVGateway_Client_unbindProcessFromSdvRpcNetworkInterfaceASDVGateway_Client_bindProcessToSdvRpcNetworkInterface 以取消绑定进程并将其重新绑定到 SDV-RPC VLAN。这两个调用就像全局开关一样,会切换套接字绑定到的网络接口,以用于进程的所有线程。

// 1. Unbind: Sockets return to the "default" network interface (e.g., wlan0, eth0)
ASDVGateway_StatusCode_t unbindStatus =
    ASDVGateway_Client_unbindProcessFromSdvRpcNetworkInterface(client, &status);

// 2. Bind: Sockets are now bound to the dedicated SDV-RPC VLAN
ASDVGateway_StatusCode_t bindStatus =
    ASDVGateway_Client_bindProcessToSdvRpcNetworkInterface(client, &status);

// Validation
if (bindStatus == ASDVGateway_StatusCode_OK) {
    // Sockets are successfully bound to the SDV-RPC VLAN
}

RPC 通知

设置客户端监听器以接收有关 RPC 可用性更改的通知,以及必须由 RPC 服务器接受的根证书的更新:

class NativeSdvGatewayTestApp {
public:
    static void ClientNotificationCallback(
        ASDVGateway_ClientNotificationType_t notificationType,
        void* userData
    ) {
        auto* testApp = reinterpret_cast<NativeSdvGatewayTestApp*>(userData);
        if (!testApp) return;

        switch (notificationType) {
            case ASDVGateway_ClientNotificationType_RootCertsChanged:
                std::cout << "onClientNotification: Root Certs Changed" << std::endl;
                // Handle certificate rotation logic here
                break;

            case ASDVGateway_ClientNotificationType_RpcAvailabilityChanged:
                std::cout << "onClientNotification: RPC Availability Changed" << std::endl;
                // Handle reconnection or UI updates here
                break;

            default:
                std::cout << "onClientNotification: Received Unknown Notification ("
                          << notificationType << ")" << std::endl;
                break;
        }
    }
};

// --- Registration ---

NativeSdvGatewayTestApp* thisApp = get_current_app_context();

// Register the notification callback to monitor system-level changes
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_setClientNotificationCallback(
    client,
    NativeSdvGatewayTestApp::ClientNotificationCallback,
    static_cast<void*>(thisApp), // userData
    &status
);

RPC 服务器流程

您必须使用凭据来创建 RPC 服务器:

ASDVGateway_RpcCredentials_t* rpcCredentials = nullptr;

// Retrieve the credentials from the client
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_rpcCredentials(client, &rpcCredentials, &status);

// Validation: Differentiating between an error and a deliberate "insecure mode"
if (status.statusCode != ASDVGateway_StatusCode_OK) {
    std::cerr << "Error retrieving credentials: " << status.errorMessage << std::endl;
    return;
}

// Logic for choosing Security Credentials
if (rpcCredentials == nullptr) {
    // If the call succeeded but credentials are null, the system expects insecure communication
    std::cout << "Configuring for Insecure Mode" << std::endl;
} else {
    std::cout << "Configuring for Secure Mode:" << std::endl;
    std::cout << "  Private Key: " << (rpcCredentials->privateKeyPem ? "Present" : "Missing") << std::endl;
    std::cout << "  RootCerts:   " << rpcCredentials->rootCertsPem << std::endl;
    std::cout << "  CertChain:   " << rpcCredentials->certChainPem << std::endl;
    std::cout << "  SAN:         " << rpcCredentials->subjectAlternativeName << std::endl;
}

// --- Cleanup ---

// Release the memory once the RPC server/client is initialized
if (rpcCredentials != nullptr) {
    ASDVGateway_Client_deleteRpcCredentials(client, rpcCredentials);
}

创建 RPC 服务器并知道其监听端口后,请注册 RPC 服务器,以便它可以发现其他应用:

ASDVGateway_RegisterRpcServerParams_t params{
    .serviceUnitName = "android-sdv-samples-sunroof-sunroof",
    .unitType = ASDVGateway_UnitType_t{
        .sdvPackageName     = "android.sdv.samples.sunroof",
        .serviceBundleName  = "SunroofServer",
        .unitTypeName       = "Sunroof",
    },
    .listeningPort = listeningPort,
    .serverUnitMetadata = ASDVGateway_ServerUnitMetadata_t{
        .version = 1,
    },
};

// Register the RPC server with the SDV Gateway
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_registerRpcServer(
    client,
    &params,
    &status
);

// Basic error handling
if (statusCode != ASDVGateway_StatusCode_OK) {
    std::cerr << "Failed to register RPC server: " << status.errorMessage << std::endl;
}

当系统在运行时更新根证书(例如,在虚拟机状态更改期间)时,RPC 服务器必须刷新其接受的证书列表:

  • 使用 ASDVGateway_Client_setClientNotificationCallback 为客户端事件设置监听器,以便在根证书更新后收到通知。

  • 调用 ASDVGateway_Client_rpcCredentials 以获取更新后的根证书。

RPC 客户端流程

以下是 RPC 客户端的流程:

ASDVGateway_FindRpcServerByNameParams_t params{
    .packageName        = "android.sdv.samples.cluster",
    .serviceBundleName  = "ClusterServer",
    .serviceUnitName    = "android-sdv-samples-cluster-cluster",
};

ASDVGateway_SocketAddress_t socketAddress;
ASDVGateway_RpcCredentials_t* rpcCredentials = nullptr;

// Perform the lookup to find the server's location and security requirements
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_findRpcServerByName(
    mClient,
    &params,
    &socketAddress,
    &rpcCredentials,
    &status
);

// Mandatory check: Distinguish between system errors and intentional "Insecure Mode"
if (status.statusCode != ASDVGateway_StatusCode_OK) {
    std::cerr << "Failed to find RPC server: " << status.errorMessage << std::endl;
    return;
}

// Logic for establishing the RPC channel
if (rpcCredentials == nullptr) {
    std::cout << "Connecting via Insecure Mode to "
              << socketAddress.address << ":" << socketAddress.port << std::endl;
} else {
    std::cout << "Connecting via Secure Mode to "
              << socketAddress.address << ":" << socketAddress.port << std::endl;

    std::cout << "  RootCerts: " << rpcCredentials->rootCertsPem << std::endl;
    std::cout << "  SAN:       " << rpcCredentials->subjectAlternativeName << std::endl;
    // Note: privateKeyPem and certChainPem are typically used if the client
    // needs to perform mutual TLS (mTLS).
}

// Cleanup: Release the credential memory once the RPC channel is established
if (rpcCredentials != nullptr) {
    ASDVGateway_Client_deleteRpcCredentials(client, rpcCredentials);
}

创建发布者并发布消息

ASDVGateway_Client_createPublication API 用于通过其 AIDL 接口向 SDV 网关注册发布者服务单元,并在应用进程中创建快速消息队列 (FMQ)。Binder 仅参与设置 FMQ,而不参与写入消息。然后,使用 ASDVGateway_Client_publishMessages API 将消息发布到创建的发布内容。这涉及写入发布内容的 FMQ 并通知已写入消息。

ASDVGateway_CreatePublicationParams_t params{
    .serviceUnitName = "mirror-position-adjust-impl-1",
    .unitType = ASDVGateway_UnitType_t{
        .sdvPackageName     = "android.sdv.samples.sdv_gateway",
        .serviceBundleName  = "DtPublisher",
        .unitTypeName       = "MirrorPositionAdjust",
    },
    .publisherUnitMetadata = ASDVGateway_PublisherUnitMetadata_t{
        .version          = 1,
        .messageSizeBytes = 64,
        .messageCount     = 16,
    },
};

ASDVGateway_PublicationMetadata_t metadata;

// 1. Create the Publication (allocates resources on the gateway)
ASDVGateway_StatusCode_t createStatus = ASDVGateway_Client_createPublication(
    client,
    &params,
    &metadata,
    &status
);

if (status.statusCode != ASDVGateway_StatusCode_OK) {
    std::cerr << "Failed to create publication: " << status.errorMessage << std::endl;
    return;
}

// 2. Publish Messages
// Note: serializedMessage should contain your encoded protobuf data
std::vector<uint8_t> serializedMessage;

ASDVGateway_Client_publishMessages(
    client,
    serializedMessage.data(),
    serializedMessage.size(),
    metadata.publicationId,
    &status
);

使用通知监听器创建订阅者

如需订阅发布内容并接收数据隧道通知(例如消息可用性),请使用 ASDVGateway_Client_subscribeToPublicationByName API。此 API 还会设置 FMQ 以读取已发布的消息。您可以在订阅过程中或之后配置通知回调。

#include <map>
#include <memory>
#include <iostream>

class NativeSdvGatewayTestApp {
public:
    struct SubscriptionContext {
        int32_t subscriptionId;
        std::string topicName;
        // Add other context-specific data here (e.g., counters, buffers)
    };

    /**
     * Callback triggered when new data is published to a subscribed topic.
     */
    static void SubscriptionNotificationCallback(
        const ASDVGateway_SubscriptionNotificationData_t* notification,
        void* userData
    ) {
        // The SDV Gateway client passes back the userData pointer.
        // We ensure validity by managing the lifecycle of SubscriptionContext
        // within the mSubscriptions map.
        auto* ctx = reinterpret_cast<SubscriptionContext*>(userData);

        if (ctx && notification) {
            // React to data being available for the subscription.
            // Example: handleIncomingData(notification->data, notification->size);
            std::cout << "Notification received for sub ID: " << ctx->subscriptionId << std::endl;
        }
    }

private:
    // Maps subscription handles/IDs to their respective contexts
    std::map<int32_t, std::unique_ptr<SubscriptionContext>> mSubscriptions;
};

// --- Usage ---
NativeSdvGatewayTestApp app;

订阅时,您可以指定发布者的服务单元名称以及其他选项。借助这些选项,您可以在订阅之前检索最近发布的消息,并定义通知之间的最短时间间隔。

ASDVGateway_SubscribeToPublicationByNameParams_t params{
    .sdvVmName           = "vm1",
    .packageName         = "test.package.impl.name",
    .serviceBundleName   = "TestBundleImpl",
    .serviceUnitName     = "GrpcServerImpl",
};

ASDVGateway_Subscriber_Options_t options{
    // Ensure we receive the most recent message immediately upon subscribing
    .flags         = ASDVGATEWAY_SUBSCRIBER_OPTIONS_FLAG_FETCHLASTMESSAGE,
    .minIntervalMs = 0, // No rate limiting; receive updates as they happen
};

// 1. Prepare the context for the callback
auto subCtx = std::make_unique<SubscriptionContext>();
ASDVGateway_PublicationMetadata_t metadata{};

// 2. Perform the subscription
ASDVGateway_StatusCode_t statusCode = ASDVGateway_Client_subscribeToPublicationByName(
    client,
    &params,
    &options,
    NativeSdvGatewayTestApp::SubscriptionNotificationCallback,
    static_cast<void*>(subCtx.get()), // Pass the raw pointer as userData
    &metadata,
    &status
);

// 3. Validation and Lifecycle Management
if (status.statusCode != ASDVGateway_StatusCode_OK) {
    // Error handling: subCtx will be automatically deleted here
    std::cerr << "Subscription failed: " << status.errorMessage << std::endl;
    return;
}

// Store the context using the publicationId as the key.
// Once moved, the map owns the lifetime of subCtx.
app.mSubscriptions.emplace(metadata.publicationId, std::move(subCtx));

使用 ASDVGateway_Client_readAvailableMessages API 从发布内容中读取消息:

uint32_t messagesAvailable = 0;

// 1. Check how many messages are waiting in the queue
ASDVGateway_StatusCode_t availStatus = ASDVGateway_Client_availableToRead(
    client,
    metadata.publicationId,
    &messagesAvailable,
    &status
);

if (status.statusCode != ASDVGateway_StatusCode_OK) {
    // Error handling for availability check
    return;
}

if (messagesAvailable == 0) {
    // No messages available for this publication at this time
    return;
}

// 2. Prepare the buffer
// metadata.messageSizeBytes was provided during the publication/subscription setup
std::vector<uint8_t> bytesForMessages;
bytesForMessages.resize(metadata.messageSizeBytes * messagesAvailable);

// 3. Read the messages from the Gateway into your local buffer
uint32_t actualMessageCount = 0; // Filled by the SDK with the number of messages read
ASDVGateway_StatusCode_t readStatus = ASDVGateway_Client_readAvailableMessages(
    client,
    metadata.publicationId,
    bytesForMessages.data(),
    bytesForMessages.size(),
    &actualMessageCount,
    &status
);

if (status.statusCode == ASDVGateway_StatusCode_OK) {
    // Successfully read 'actualMessageCount' messages
    // Process bytesForMessages...
}

如需在订阅后设置回调,请使用 ASDVGateway_Client_setNotificationCallbackForPublicationId API:

ASDVGateway_StatusCode_t ASDVGateway_Client_setNotificationCallbackForPublicationId(
    // [in]  Opaque pointer to the client object.
    const ASDVGateway_Client* client,

    // [in]  Identifies the specific publication to monitor.
    const int32_t publicationId,

    // [in]  Function pointer triggered when new data is available
    //       for the specified publication.
    ASDVGateway_SubscriptionNotificationCallback notificationCallback,

    // [in]  Optional user-defined context passed back to the callback.
    void* notificationCallbackUserData,

    // [out] Optional pointer to a status structure for result codes
    //       and error messages.
    ASDVGateway_Status_t* outStatus
);

设置 init 服务

假设您的服务名为 native_sdv_gateway_client_service, 可执行文件位于 /vendor/bin/native_sdv_gateway_client_service,并且 您使用 vendor_sdv_services 作为运行该服务的 Android UID (AID)。

任何原生服务都不应使用 Android 应用的 AID。此处必须使用供应商服务的预留范围内的 AID。完成此设置后,您可以定义以下 init 服务

service native_sdv_gateway_client_service /vendor/bin/native_sdv_gateway_client_service
    class core
    user vendor_sdv_services
    group inet
    disabled
    oneshot

其中

  • vendor_sdv_services 是为服务创建或选择的 AID。
  • SDV 网关客户端需要 group inet 才能使用网络接口在 SDV 虚拟机之间进行通信。您可以根据需要添加其他群组。
  • 此示例使用 disabledoneshot。您可能需要为您的服务调整服务选项。在 sdv_gateway 之后启动服务。

为服务创建 SELinux 规则

如需使用 SDV 网关 API,您需要为服务提供以下 SELinux 规则:

# Define the domain for the service
type native_sdv_gateway_client_service, domain;

# Define the executable file type on the vendor partition
type native_sdv_gateway_client_service_exec, exec_type, file_type, vendor_file_type;

# Macro to transition from 'init' to this service's domain upon execution
init_daemon_domain(native_sdv_gateway_client_service)

# Macro to grant the necessary permissions to communicate with the SDV Gateway
sdv_gateway_client_domain(native_sdv_gateway_client_service)

在 SELinux 规则中:

  • init_daemon_domain 允许从 init 启动服务。

  • sdv_gateway_client_domain 提供与 SDV 网关交互所需的所有 SELinux 权限。以下行向可执行文件授予这些规则:

    /vendor/bin/native_sdv_gateway_client_service    u:object_r:native_sdv_gateway_client_service_exec:s0
    

代码示例

如需详细了解如何运行原生代码示例来演示 C API 的文档记录方式,请参阅 system/software_defined_vehicle/samples/sdv_gateway/NativeSdvGatewayTestApp/README.md

IVI Java 应用上的 SDV 网关

此图展示了 IVI 上 SDV 网关的交互模型:

IVI Java 应用上的 SDV 网关

图 4. IVI Java 应用上的 SDV 网关。

具体而言,该模型:

  • 将所有调用分派到 JNI 层。
  • 将 Java 和 C API 粘合在一起。
  • 定义与 ISdvGateway 的 AIDL 交互:
    • 初始化通信
    • 查找或创建 RPC 服务器
    • 创建发布/订阅
    • 执行服务发现数据隧道交互
    • 接收通知(例如,数据可用)
    • 读取和写入消息(用于发布/订阅的 FMQ),以创建密钥对和 TLS 证书

添加网关客户端库

libsdvgatewayclient C API 的 Java 库和 JNI 封装容器安装在 IVI 目标上的 APEX 中。向 Java 库存根、必须在运行时使用的 Java 库以及包含 Java 库的所需 APEX 添加编译时依赖项。

android_app {
    name: "YourAppName",
    // ...
    static_libs: [
        "libsdvgatewayclient-java",
    ],
    libs: [
        "libsdvgatewayclient-java-sdk.stubs",
    ],
    uses_libs: [
        "libsdvgatewayclient-java-sdk",
    ],
    required: [
        "com.sdv.google.gateway.client",
    ],
    // ...
}

对于在 SDV 树之外构建的未捆绑应用,该过程类似:

  1. 将生成的 Java 库桩 JAR 和支持 JAR 复制到应用库文件夹中的 RPC。如需了解详情,请参阅 system/software_defined_vehicle/sdv_gateway/libsdvgatewayclient_apex/README.md

  2. 将存根 JAR 添加为仅编译依赖项。例如,更新 Gradle 配置以依赖于存根,方法是将 compileOnly 条目添加到 dependencies 部分:

    dependencies {
        // The library supporting functions for SDK RPC.
        // Statically linked into the app APK.
        implementation(files("libs/libsdvgatewayclient-java.jar"))
    
        // Stub of the SDV-Gateway client library.
        // Used only for compilation; the real implementation is provided 
        // by the com.sdv.google.gateway.client APEX at runtime.
        compileOnly(files("libs/libsdvgatewayclient-java-sdk.jar"))
    }
    
  3. 将 Java 库添加到 AndroidManifest.xml 文件的应用部分。

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
        <application>
    
            <!--
              Declares that this app requires the SDV Gateway client library.
              The 'android:required="true"' attribute ensures the app won't
              install/run if the library is missing from the system.
            -->
            <uses-library
                android:name="libsdvgatewayclient-java-sdk"
                android:required="true" />
    
        </application>
    
    </manifest>
    

加载库

创建 SdvGatewayClient 对象(由客户端库提供):

import google.sdv.gateway.client.SdvGatewayClient;

// --- Inside your Activity, Service, or ViewModel ---

// Initialize the SDV Gateway Client
SdvGatewayClient gatewayClient = new SdvGatewayClient();

初始化通信

使用应用名称作为服务软件包名称调用 initComms()

// Define a unique name for your service bundle (usually constant)
private static final String SERVICE_BUNDLE_NAME = "MySdvServiceBundle";

// ... inside an Activity or Service context ...

try {
    // Initialize communications with the SDV Gateway
    // context.getPackageName() provides "android.sdv.samples.gateway.client" or similar
    gatewayClient.initComms(context.getPackageName(), SERVICE_BUNDLE_NAME);

    Log.i("SDV_GATEWAY", "Communications initialized successfully");
} catch (Exception e) {
    // Unlike the C API which uses status codes,
    // the Java SDK often throws exceptions for initialization failures.
    Log.e("SDV_GATEWAY", "Failed to initialize communications", e);
}

服务发现

Java API 包含以下方法:

  • 在注册或取消注册具有特定单元类型(或匹配特定名称)的服务单元时收到通知。

  • 列出当前具有特定单元类型(或匹配特定服务单元类型名称)的服务单元。如需在服务单元发生更改时收到通知,请创建监听器:

import google.sdv.gateway.client.ServiceUnitChangeListener;
import google.sdv.gateway.client.ServiceUnitChangeEventType;
import google.sdv.gateway.client.ServiceUnitDefinition;

// --- Implementation ---

ServiceUnitChangeListener listener = new ServiceUnitChangeListener() {
    @Override
    public void onServiceUnitChanged(
        ServiceUnitChangeEventType eventType,
        ServiceUnitDefinition serviceUnitDefinition
    ) {
        // This is triggered when services matching your criteria
        // are registered or unregistered on the vehicle network.
        if (eventType == ServiceUnitChangeEventType.REGISTERED) {
            // Handle new service discovery
        } else if (eventType == ServiceUnitChangeEventType.UNREGISTERED) {
            // Handle service removal
        }
    }
};

addListenerForServiceUnitChangeByName Java 方法会通知监听器:

// 1. Register the listener for a specific service unit by name
AutoCloseable handle = gatewayClient.addListenerForServiceUnitChangeByName(
    new UnitNameDiscoveryArgs(
        "",                                   // sdvVmName (empty for local/auto-discovery)
        "android.sdv.samples.cluster",        // sdvPackageName
        "ClusterServer",                      // serviceBundleName
        "android-sdv-samples-cluster-cluster" // serviceUnitName
    ),
    listener
);

// --- Later in the application lifecycle ---

// 2. To stop receiving notifications and clean up resources, close the handle.
try {
    if (handle != null) {
        handle.close();
    }
} catch (Exception e) {
    Log.e("SDV_GATEWAY", "Error while closing the listener handle", e);
}

或者,使用 addListenerForServiceUnitChangeByType Java 方法在注册或取消注册具有指定单元类型的服务时通知监听器:

// 1. Register a listener based on the Service Unit Type
AutoCloseable handle = gatewayClient.addListenerForServiceUnitChangeByType(
    new UnitType(
        "com.android.testapp.sdvcarmonitor", // sdvPackageName
        "SunroofRpcServer",                  // serviceBundleName
        "Sunroof"                            // unitTypeName
    ),
    listener
);

// --- Execution Loop / Lifecycle ---

// 2. To stop receiving notifications and clean up memory, close the handle.
// This effectively unregisters the listener from the SDV Gateway.
try {
    if (handle != null) {
        handle.close();
    }
} catch (Exception e) {
    Log.e("SDV_GATEWAY", "Failed to close the service unit listener handle", e);
}

对于 addListenerForServiceUnitChangeByNameaddListenerForServiceUnitChangeByType,添加后,监听器会收到所有已注册的服务单元的通知。如需仅按名称或类型获取已注册的服务单元,请使用 listServiceUnitsByNamelistServiceUnitsByType Java API:

import google.sdv.gateway.client.ServiceUnitDefinition;
import google.sdv.gateway.client.UnitNameDiscoveryArgs;
import google.sdv.gateway.client.UnitType;

// --- 1. Synchronous Lookup by Specific Name ---
ServiceUnitDefinition[] definitionsByName = gatewayClient.listServiceUnitsByName(
    new UnitNameDiscoveryArgs(
        "",                                   // sdvVmName
        "android.sdv.samples.cluster",        // sdvPackageName
        "ClusterServer",                      // serviceBundleName
        "android-sdv-samples-cluster-cluster" // serviceUnitName
    )
);

// --- 2. Synchronous Lookup by Service Type ---
ServiceUnitDefinition[] definitionsByType = gatewayClient.listServiceUnitsByType(
    new UnitType(
        "com.android.testapp.sdvcarmonitor", // sdvPackageName
        "SunroofRpcServer",                  // serviceBundleName
        "Sunroof"                            // unitTypeName
    )
);

// Example processing
if (definitionsByType.length > 0) {
    ServiceUnitDefinition firstSunroof = definitionsByType[0];
    // Proceed to connect...
}

RPC 服务器流程

IVI 系统中的 SDV 网关客户端使用 Google 远程过程调用 (gRPC) 与 SDV 服务进行通信。这些交互依赖于 VSIDL 目录中的 proto 定义,这些定义与 SDV Core 上使用的定义一致或类似。对于 Java 应用,gRPC-Java 是所选的实现。为应用服务器提供了一个示例服务器 proto 定义 sunroof.proto

service Sunroof {
    /**
     * Retrieves the current state of the sunroof (e.g., position, tilt, status).
     *
     * @param .google.protobuf.Empty - No input parameters required.
     * @return SunroofStateResponse - The current telemetry data for the sunroof.
     */
    rpc GetSunroofState(.google.protobuf.Empty) returns (SunroofStateResponse) {}
}

链接到相应的 proto 库并定义服务:

import com.android.sdv.sdvgrpclibrary.SunroofGrpc;
import com.android.sdv.sdvgrpclibrary.SunroofStateResponse; // Assuming this is the generated class
import com.google.protobuf.Empty;
import io.grpc.stub.StreamObserver;

/**
 * Implementation of the Sunroof gRPC service.
 * This class handles the logic for the RPCs defined in your .proto file.
 */
static class SunroofGrpcImpl extends SunroofGrpc.SunroofImplBase {

    @Override
    public void getSunroofState(Empty request, StreamObserver<SunroofStateResponse> responseObserver) {
        // 1. Fetch current sunroof data (e.g., from a Hardware Abstraction Layer)
        int currentPosition = 50; // Example value: 50% open

        // 2. Build the Protobuf response message
        SunroofStateResponse response = SunroofStateResponse.newBuilder()
                .setPercentageOpen(currentPosition)
                .build();

        // 3. Send the response to the client using the observer
        responseObserver.onNext(response);

        // 4. Close the stream to signal that the RPC is finished
        responseObserver.onCompleted();
    }
}

使用安全和不安全的通道凭据注册 gRPC 服务器:

// 1. Define the type signature for the service
UnitType unitType = new UnitType(
    "com.android.testapp.sdvcarmonitor", // sdvPackageName
    "SunroofRpcServer",                  // serviceBundleName
    "Sunroof"                            // typeName (Unit Type)
);

// 2. Register the RPC server with the Gateway
// The gateway creates a mapping between the ServiceUnitName and your implementation.
server = gatewayClient.registerRpcServer(
    "SunroofRpcServerImpl-1",                       // serviceUnitName (Unique instance name)
    unitType,                                       // unitType defined earlier
    "SUNROOF_GRPC_SERVER_VALUE_HOLDER".getBytes(),  // appMetadataValueHolder (Static discovery data)
    1,                                              // appMetadataVersion
    new SunroofGrpcImpl()                           // The actual gRPC service implementation
);

在内部,客户端库会创建 gRPC 服务器对象 SdvGatewayClient.java,并处理根证书的更新:

// 1. Initialize credentials (Insecure for dev, TLS for production)
ServerCredentials serverCredentials = InsecureServerCredentials.create();

// 2. Build and start the OkHttp-based gRPC server
final int bindAnyPort = 0;
final Server server = OkHttpServerBuilder
    .forPort(bindAnyPort, serverCredentials)
    .addService(gRpcServerImplementation) // Your SunroofGrpcImpl
    .build()
    .start();

// The assigned port can now be retrieved using server.getPort()
int actualPort = server.getPort();

// 3. Prepare the JNI data structure
// This object mirrors the ASDVGateway_ServiceUnitDefinition_t C struct
JniServiceUnitDefinition definition = new JniServiceUnitDefinition();

// Fill the RPC service definition params (Port, Name, Type, etc.)
definition.setPort(actualPort);
definition.setServiceUnitName("SunroofRpcServerImpl-1");

// 4. Perform the cross-language call
// This jumps from Java -> JNI -> ASDVGateway_Client_registerRpcServer (C API)
mJniClient.nativeRegisterRpcServer(definition);

RPC 客户端流程

此代码示例为应用作为客户端连接到的服务器提供了服务器 proto 定义 (tpms.proto):

/**
 * The TPMS service provides real-time pressure and temperature
 * data for all tires on the vehicle.
 */
service Tpms {
    /**
     * Returns the full state of all monitored tires.
     */
    rpc GetTpmsState(.google.protobuf.Empty) returns (TpmsStateResponse) {}

    /**
     * A filtered query that returns only the tires
     * below the recommended pressure threshold.
     */
    rpc GetLowTires(.google.protobuf.Empty) returns (LowTiresResponse) {}
}

链接到相应的 proto 库:

import com.android.sdv.sdvgrpclibrary.TpmsGrpc;
import io.grpc.ManagedChannel;

// --- Inside your Client Application ---

// 1. Request a ManagedChannel from the Gateway for a specific service unit
ManagedChannel managedChannel = gatewayClient.connectToRpcServerByName(
    "",                                     // sdvVmName (empty for local/auto-lookup)
    "android.sdv.samples.cluster",          // packageName
    "ClusterServer",                        // serviceBundleName
    "android-sdv-samples-cluster-cluster"  // serviceUnitName
);

// 2. Use the channel to create a gRPC stub (e.g., for the TPMS service)
TpmsGrpc.TpmsBlockingStub tpmsStub = TpmsGrpc.newBlockingStub(managedChannel);

// 3. Now you can call RPC methods directly
// TpmsStateResponse response = tpmsStub.getTpmsState(Empty.getDefaultInstance());

在内部,系统会调用 ASDVGateway_Client_findRpcServerByName API 来查找 RPC 服务器。如果找到 RPC 服务器,则会以不安全模式创建托管通道,或者指定使用 TLS 配置(类似于 RPC 服务器流程),具体取决于服务发现配置。应用使用 ManagedChannel 对象创建存根并调用服务器方法:

import com.android.sdv.sdvgrpclibrary.TpmsGrpc;
import com.android.sdv.sdvgrpclibrary.TpmsStateResponse;
import com.google.protobuf.Empty;
import io.grpc.stub.MetadataUtils;

// 1. Create a "Blocking Stub" from the existing ManagedChannel.
// We apply an interceptor to attach mandatory metadata (headers)
// required by the SDV Gateway for authorization.
TpmsGrpc.TpmsBlockingStub tpmsStub = TpmsGrpc.newBlockingStub(managedChannel)
    .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(mMetadata));

// 2. Execute the RPC call.
// Because this is a "BlockingStub," the thread will wait here until
// the vehicle service responds or times out.
TpmsStateResponse tpmsStateResponse = tpmsStub.getTpmsState(Empty.getDefaultInstance());

// 3. Extract the domain-specific state object from the Protobuf response.
// 'newState' can now be used to update your UI or application logic.
newState = tpmsStateResponse.getTpmsState();

切换进程的默认网络

您可能需要将进程的默认网络从 SDV-RPC VLAN 切换为打开套接字以进行互联网绑定连接。调用 unbindProcessFromSdvRpcNetworkInterfacebindProcessToSdvRpcNetworkInterface 以取消绑定进程并将其重新绑定到 SDV-RPC VLAN。这两个调用就像全局开关一样,会切换套接字绑定到的网络接口,以用于进程的所有线程。

// 1. Redirect all socket traffic from this process to the SDV-RPC VLAN.
// This call is required for making RPC calls or hosting RPC services for SDV.
gatewayClient.bindProcessToSdvRpcNetworkInterface();

// --- Process is now communicating over the SDV-RPC interface ---

// 2. Revert the process network binding back to the "default" interface.
// This allows the app to access internet resources again.
gatewayClient.unbindProcessFromSdvRpcNetworkInterface();

创建发布者并发布消息

// 1. Define the interface and type for the publication
UnitType unitType = new UnitType(
    "android.sdv.samples.tires.interface", // sdvPackageName
    "TirePressurePublisherInterface",      // serviceBundleName
    "TirePressure"                         // typeName
);

// 2. Configure the Publisher's buffer and message constraints
PublisherUnitMetadata publisherUnitMetadata = new PublisherUnitMetadata(
    1,   // version
    64,  // message size in bytes (fixed size for performance)
    128  // max message count (buffer depth)
);

// 3. Create the Publication instance through the Gateway
Publisher tirePublisher = gatewayClient.createPublication(
    "tire-pressure-service-unit-name",
    unitType,
    publisherUnitMetadata
);

// 4. Prepare and publish a message
// In a real app, you would encode your sensor data into this byte array
byte[] msg = new byte[publisherUnitMetadata.messageSizeBytes];

// ... fill msg with data ...

tirePublisher.publish(msg);

在内部,Java createPublication 和发布 API 依赖于原生 ASDVGateway_Client_createPublicationASDVGateway_Client_readAvailableMessages API。如需详细了解 C API,请参阅使用 IVI 原生服务上的 SDV 网关Publisher 对象提供了一个用于写入消息和管理发布内容生命周期的上下文。

使用通知监听器创建订阅者

Java API 允许将监听器作为参数传递给订阅发布内容方法,并返回 Subscription 对象。

  • 当订阅的发布内容有可用数据时,系统会通知 Listener

  • Subscription 充当上下文对象,可用于读取消息和关闭订阅。

// 1. Define the Listener to handle incoming data notifications
SubscriptionNotificationListener listener = new SubscriptionNotificationListener() {
    @Override
    public void onSubscriptionNotification(
        Subscription subscription,
        SubscriptionNotificationType notificationType
    ) {
        // Only process if the notification indicates new data is ready
        if (notificationType != SubscriptionNotificationType.DataAvailable) {
            return;
        }

        // Read the message from the subscription buffer
        byte[] content = subscription.readMessage();

        // Note: This callback often runs on a background thread provided by the SDK.
        // If you need to update the UI, use a Handler or View.post().
        processTireData(content);
    }
};

// 2. Subscribe to the publication by its unique name
Subscription tireSubscription = gatewayClient.subscribeToPublicationByName(
    "",                                  // sdvVmName (empty for auto-lookup)
    "android.sdv.samples.dt_publisher",  // packageName
    "SdvGatewayDtPublisher",             // serviceBundleName
    "tire",                              // serviceUnitName
    listener
);

// 3. Read Messages
// You can poll or register callbacks for new messages.
// When polling the message, call this method in a loop.
// When registering callbacks, call this method to get the message payload.
byte[] manualContent = tireSubscription.readMessage();

代码示例

监听器回调会在客户端管理的单个线程上调用,以最大限度地减少监听器中的处理,从而防止延迟接收其他订阅的通知。Java 层利用 C API 进行订阅管理、通知处理和消息检索。如需查看 API 用法的演示,请参阅 system/software_defined_vehicle/samples/sdv_gateway/README.md 中的二进制文件提供的 Java 应用示例。

所需权限

调用 SDV 网关客户端 API 不需要特殊权限,但您需要为应用应用适当的 SELinux 规则

  • 对于数据隧道订阅和发布内容,您不需要任何权限。
  • 对于 SDV RPC,您需要以下权限:
    • android.permission.INTERNET
    • android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS

对于 android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,您还需要在与应用相同的分区中的 etc/permissions 下添加许可名单文件,例如,对于 SdvCarMonitorTestApp(软件包名称 com.android.testapp.sdvcarmonitor),该文件如下所示:

<?xml version="1.0" encoding="utf-8"?>

<permissions>
    <privapp-permissions package="com.android.testapp.sdvcarmonitor">
        <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
    </privapp-permissions>
</permissions>

SELinux 规则

如需使用 SDV 网关 API,Java 应用需要与原生服务相同的权限。使用 sdv_gateway_client_domain() SELinux 宏授予这些权限:

sdv_gateway_client_domain(my_oem_sdv_gateway_client_app)

OEM 为允许使用 SDV 网关的 Java 应用定义 my_oem_sdv_gateway_client_app 网域。仅从系统应用和特权应用使用 SDV 网关。

代码位置

system/software_defined_vehicle/sdv_gateway/ 中获取 SDV 网关的源代码。您可以获取以下 SDV 网关示例:

  • 客户端 C API: system/software_defined_vehicle/samples/sdv_gateway/NativeSdvGatewayTestApp/
  • 客户端 Java API: system/software_defined_vehicle/samples/sdv_gateway/SdvCarMonitorTestApp/