从车窗升降一探 Android 车机的重要 API:车辆属性 CarProperty

在这里插入图片描述

前言

前面我们介绍过 Android 车机 Automotive OS 的几块重要内容:

  • 一文了解 Android 车机如何处理中控的旋钮输入
  • 从实体按键看 Android 车机的自定义事件机制
  • 深度入门 Android 车机核心 CarService 的构成和链路

本篇文章我们聚焦 Android 车机上最重要、最常用的接口:即车辆属性 CarPropertyManager。

并结合车窗升降这种典型的场景来探究它的完整链路。

实现车窗升降

CarPropertyManager 通常针对某个 Property 发起读写,这些属性有很多,从车窗到空调、油量到续航等等。

想要控制它们,得需要知道它的唯一标识,并和系统定义的 ID 保持一致。那么车窗对应的 ID 为 VehiclePropertyIds 中的 WINDOW_POS,其要求 app 拥有专用的权限:

android.car.Car.PERMISSION_CONTROL_CAR_WINDOWS

属性监听

当目标属性发生变化,可以通过 CarPropertyEventCallback 通知到请求 App,为了满足各种场景,系统提供了设置通知频次的可能。

总共有如下几种:

通知频次类型 频次(HZ)
SENSOR_RATE_ONCHANGE
SENSOR_RATE_FASTEST 100
SENSOR_RATE_FAST 10
SENSOR_RATE_NORMAL 1
SENSOR_RATE_UI 5

对于车窗、入座这些即时信号,采用 SENSOR_RATE_ONCHANGE 类型即可,意味着只在变化的时候通知。当然,注册的时候会立即回调一次以通知当前的数值。

代码很简单,构建 CarPropertyEventCallback 实例,并传递目标 Property ID 和上述的通知类型,即可完成该属性的监听。

class CarEventCallBack: CarPropertyManager.CarPropertyEventCallback {
    override fun onChangeEvent(value: CarPropertyValue?) { }
}

val car = Car.createCar(context)
val carPropertyManager =
    car?.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager

carPropertyManager.registerCallback(
    CarEventCallBack(),
    VehiclePropertyIds.WINDOW_POS,
    CarPropertyManager.SENSOR_RATE_ONCHANGE
)

属性读写

对于车窗硬件来说,用户关心的是其升降的状况,系统用 0~100 来进行定义,继而决定了它的值为 Int 型。

那么读取的 API 为 getIntProperty(),参数:

  • prop:希望读取的属性 ID,比如上面的车窗 Property ID:WINDOW_POS
  • area:希望读取属性的位置信息 zone,对应到 VehicleAreaWindow 类型中常量

注意:该方法是同步的,而且因为车窗等属性的操作耗时,建议在子线程 invoke。

写入的 API 为 setIntProperty(),参数:

  • prop:希望改写的属性 ID,
  • areaId:该属性对应的位置薪资
  • val:Value to set,比如车窗即 0 ~ 100,对应着完全打开到完全关闭

和 getIntProperty() 一样,set 一样耗时,需要同样运行在子线程中。

系统预设的和 Window 相关的 zone areaId 如下,比如前排、驾驶侧、副驾驶侧、乘客侧、天窗、挡风玻璃等。

package android.hardware.automotive.vehicle;

public @interface VehicleAreaWindow {
  public static final int FRONT_WINDSHIELD = 1;
  public static final int REAR_WINDSHIELD = 2;
  public static final int ROW_1_LEFT = 16;
  public static final int ROW_1_RIGHT = 64;
  public static final int ROW_2_LEFT = 256;
  public static final int ROW_2_RIGHT = 1024;
  public static final int ROW_3_LEFT = 4096;
  public static final int ROW_3_RIGHT = 16384;
  public static final int ROOF_TOP_1 = 65536;
  public static final int ROOF_TOP_2 = 131072;
}

如下代码展示如何了完全打开驾驶位车窗。

Thread().run {
    carPropertyManager.setIntProperty(
        VehiclePropertyIds.WINDOW_POS,
        VehicleAreaWindow.WINDOW_ROW_1_LEFT,
        0
   )
}

工作原理

首先,车窗相关的 area 在 HAL 层有相应的定义:

// android/hardware/automotive/vehicle/2.0/types.h 

/**
 * Various windshields/windows in the car.
 */
enum class VehicleAreaWindow : int32_t {
    FRONT_WINDSHIELD = 1 /* 0x00000001 */,
    REAR_WINDSHIELD = 2 /* 0x00000002 */,
    ROW_1_LEFT = 16 /* 0x00000010 */,
    ROW_1_RIGHT = 64 /* 0x00000040 */,
    ROW_2_LEFT = 256 /* 0x00000100 */,
    ROW_2_RIGHT = 1024 /* 0x00000400 */,
    ROW_3_LEFT = 4096 /* 0x00001000 */,
    ROW_3_RIGHT = 16384 /* 0x00004000 */,
    ROOF_TOP_1 = 65536 /* 0x00010000 */,
    ROOF_TOP_2 = 131072 /* 0x00020000 */,
};

读取

直接看 getIntProperty(),首先调用 checkSupportedProperty() 检查是否支持该属性,当不支持的话抛出:

IllegalArgumentException: “Unsupported property:xxx”

接着调用 getProperty(),不过指定了返回的数据类型。

public class CarPropertyManager extends CarManagerBase {
    public int getIntProperty(int prop, int area) {
        checkSupportedProperty(prop);
        CarPropertyValue carProp = getProperty(Integer.class, prop, area);
        return handleNullAndPropertyStatus(carProp, area, 0);
    }

    private void checkSupportedProperty(int propId) {
        switch (propId) {
            case VehiclePropertyIds.INITIAL_USER_INFO:
            case VehiclePropertyIds.SWITCH_USER:
            case VehiclePropertyIds.CREATE_USER:
            case VehiclePropertyIds.REMOVE_USER:
            case VehiclePropertyIds.USER_IDENTIFICATION_ASSOCIATION:
                throw new IllegalArgumentException("Unsupported property: "
                        + VehiclePropertyIds.toString(propId) + " (" + propId + ")");
        }
    }
    ...
}

getProperty() 的实现在于 CarPropertyService。

public class CarPropertyManager extends CarManagerBase {
    public  CarPropertyValue getProperty(@NonNull Class clazz, int propId, int areaId) {
        checkSupportedProperty(propId);

        try {
            CarPropertyValue propVal = mService.getProperty(propId, areaId);
            if (propVal != null && propVal.getValue() != null) {
                Class actualClass = propVal.getValue().getClass();
            }
            return propVal;
        }
        ...
    }
    ...
}

CarPropertyService 按照如下步骤进行:

  1. 先到存放所有 Property ID 的 SparseArray 中检查是否确实存在该 Property,如果不存在的话打印 error 提醒并结束

  2. 获取该 Property 的 permission 配置,如果不存在的话,抛出:

    SecurityException: Platform does not have permission to read value for property Id: 0x…

  3. assertPermission() 检查当前 CarService 是否确实被授予了如上 permission

  4. 最后调用持有的 PropertyHalService 继续发出读取的调用

public class CarPropertyService extends ICarProperty.Stub
        implements CarServiceBase, PropertyHalService.PropertyHalListener {
    @Override
    public CarPropertyValue getProperty(int prop, int zone) ... {
        synchronized (mLock) {
            if (mConfigs.get(prop) == null) {
                // Do not attempt to register an invalid propId
                Slogf.e(TAG, "getProperty: propId is not in config list:0x" + toHexString(prop));
                return null;
            }
        }

        // Checks if android has permission to read property.
        String permission = mHal.getReadPermission(prop);
        if (permission == null) {
            throw new SecurityException("Platform does not have permission to read value for "
                    + "property Id: 0x" + Integer.toHexString(prop));
        }
        CarServiceUtils.assertPermission(mContext, permission);
        return runSyncOperationCheckLimit(() -> {
            return mHal.getProperty(prop, zone);
        });
    }
    ...
}

PropertyHalService 首先调用 managerToHalPropId() 将 Property ID 转为 HAL 中该 ID 的定义,并再度检查该 HAL ID 是否确实存在。如果不存在的话亦抛出:

IllegalArgumentException:Invalid property Id : 0x…

接着,通过 VehicleHal 传递 HAL 中 ID 继续读取得到 HalPropValue,当读取的 value 存在的话,首先得获取该 Property 在 HAL 层和上层定义的 HalPropConfig 规则。

最后依据 config 将 value 解析成 CarPropertyValue 类型返回。

public class PropertyHalService extends HalServiceBase {
'/ '    ...
    public CarPropertyValue getProperty(int mgrPropId, int areaId)
            throws IllegalArgumentException, ServiceSpecificException {
        int halPropId = managerToHalPropId(mgrPropId);
        if (!isPropertySupportedInVehicle(halPropId)) {
            throw new IllegalArgumentException("Invalid property Id : 0x" + toHexString(mgrPropId));
        }

        // CarPropertyManager catches and rethrows exception, no need to handle here.
        HalPropValue value = mVehicleHal.get(halPropId, areaId);
        if (value == null) {
            return null;
        }
        HalPropConfig propConfig;
        synchronized (mLock) {
            propConfig = mHalPropIdToPropConfig.get(halPropId);
        }
        return value.toCarPropertyValue(mgrPropId, propConfig);
    }
    ...
}

其实 VehicleHal 并未做太多处理就直接交给了 HalClient 来处理。

public class VehicleHal implements HalClientCallback {
    ...
    public HalPropValue get(int propertyId)
            throws IllegalArgumentException, ServiceSpecificException {
        return get(propertyId, NO_AREA);
    }
    ...
    public HalPropValue get(int propertyId, int areaId)
            throws IllegalArgumentException, ServiceSpecificException {
        return mHalClient.getValue(mPropValueBuilder.build(propertyId, areaId));
    }
    ...
}

HalClient 通过 invokeRetriable() 进行超时为 50ms 的 internalGet() 调用:如果结果是 TRY_AGAIN 并且尚未超时的话,再次调用;反之已经超时或者结果成功获取到的话,即结束。

后续会再次检查该 Result 中的 status,是否是不合法的、空的值等等,通过检查的话则返回 HalPropValue 出去。

final class HalClient {
    ...
    private static final int SLEEP_BETWEEN_RETRIABLE_INVOKES_MS = 50;

    HalPropValue getValue(HalPropValue requestedPropValue)
            throws IllegalArgumentException, ServiceSpecificException {
        ObjectWrapper resultWrapper = new ObjectWrapper();
        resultWrapper.object = new ValueResult();
        int status = invokeRetriable(() -> {
            resultWrapper.object = internalGet(requestedPropValue);
            return resultWrapper.object.status;
        }, mWaitCapMs, mSleepMs);

        ValueResult result = resultWrapper.object;

        if (StatusCode.INVALID_ARG == status) {
            throw new IllegalArgumentException(
                    getValueErrorMessage("get", requestedPropValue, result.errorMsg));
        }

        if (StatusCode.OK != status || result.propValue == null) {
            if (StatusCode.OK == status) {
                status = StatusCode.NOT_AVAILABLE;
            }
            throw new ServiceSpecificException(
                    status, getValueErrorMessage("get", requestedPropValue, result.errorMsg));
        }

        return result.propValue;
    }

    private ValueResult internalGet(HalPropValue requestedPropValue) {
        final ValueResult result = new ValueResult();
        try {
            result.propValue = mVehicle.get(requestedPropValue);
            result.status = StatusCode.OK;
            result.errorMsg = new String();
        }
        ...
        return result;
    }
    ...
}

internalGet() 的实现由持有的 VehicleStub 实例的 get 方法完成,其实现对应于依据 HIDL 的配置调用 HAL 侧获取相应数据。

public abstract class VehicleStub {
    ...
    @Nullable
    public abstract HalPropValue get(HalPropValue requestedPropValue)
            throws RemoteException, ServiceSpecificException;
    ...
}

写入

set 写入的链路和 get 大同小异,主要区别是:

  1. 事先构建待写入的属性实例 CarPropertyValue 并传入
  2. 传入属性变化时 callback 用的 CarPropertyEventListenerToService 实例
public class CarPropertyManager extends CarManagerBase {
    public void setIntProperty(int prop, int areaId, int val) {
        setProperty(Integer.class, prop, areaId, val);
    }

    public  void setProperty(@NonNull Class clazz, int propId, int areaId, @NonNull E val) {
        checkSupportedProperty(propId);
        try {
            runSyncOperation(() -> {
                mService.setProperty(new CarPropertyValue(propId, areaId, val),
                        mCarPropertyEventToService);
                return null;
            });
        }
        ...
    }
}

下一层 CarPropertyService 的实现也是通过 PropertyHalService 进行。

传入的 CarPropertyEventListenerToService 其实是 ICarPropertyEventListener AIDL 代理,这里会将其转为 Binder 对象,按照调用的源头 client 缓存起来,在属性变化的时候用。

public class CarPropertyService extends ICarProperty.Stub
        implements CarServiceBase, PropertyHalService.PropertyHalListener {
    public void setProperty(CarPropertyValue prop, ICarPropertyEventListener listener)
            throws IllegalArgumentException, ServiceSpecificException {
        int propId = prop.getPropertyId();
        ...
        runSyncOperationCheckLimit(() -> {
            mHal.setProperty(prop);
            return null;
        });
        IBinder listenerBinder = listener.asBinder();
        synchronized (mLock) {
            Client client = mClientMap.get(listenerBinder);
            if (client == null) {
                client = new Client(listener);
            }
            if (client.isDead()) {
                Slogf.w(TAG, "the ICarPropertyEventListener is already dead");
                return;
            }
            mClientMap.put(listenerBinder, client);
            updateSetOperationRecorderLocked(propId, prop.getAreaId(), client);
        }
    }
    ...
}

继续分发到 VehicleHal 侧。

public class PropertyHalService extends HalServiceBase {
    public void setProperty(CarPropertyValue prop)
            throws IllegalArgumentException, ServiceSpecificException {
        int halPropId = managerToHalPropId(prop.getPropertyId());
        ...
        HalPropValue halPropValue = mPropValueBuilder.build(prop, halPropId, propConfig);
        // CarPropertyManager catches and rethrows exception, no need to handle here.
        mVehicleHal.set(halPropValue);
    }
    ...
}

后续一样的是通过 VehicleHal 到 HalClient,再到 VehicleStub,最后抵达 HAL。

public class VehicleHal implements HalClientCallback {
    ...
    public void set(HalPropValue propValue)
            throws IllegalArgumentException, ServiceSpecificException {
        mHalClient.setValue(propValue);
    }
}

final class HalClient {
    ...
    public void setValue(HalPropValue propValue)
            throws IllegalArgumentException, ServiceSpecificException {
        ObjectWrapper errorMsgWrapper = new ObjectWrapper();
        errorMsgWrapper.object = new String();

        int status = invokeRetriable(() -> {
            try {
                mVehicle.set(propValue);
                errorMsgWrapper.object = new String();
                return StatusCode.OK;
            }
            ...
        }, mWaitCapMs, mSleepMs);
        ...
    }
    ...
}

public abstract class VehicleStub {
    ...
    public abstract void set(HalPropValue propValue)
            throws RemoteException, ServiceSpecificException;
    ...
}

结语

在这里插入图片描述

结合一张图回顾下整个过程:

  1. App 先通过 Car lib 拿到 CarService 的 Car 实例,CarService 会初始化所有 Car 相关的实现,比如其中车辆属性的化,会初始化 CarPropertyService 和 PropertyHalService 等
  2. 接着,App 会从 Car 实例获取车辆某个接口的实例,比如控制车辆属性的话,需要获取 CarPropertyManager,CarService 则会从初始化完成的 map 里返回已准备好的对应对象
  3. App 的属性读写会通过 AIDL 接口抵达直接负责的 CarPropertyService,然后到与 HAL 中车辆属性模块交互的 PropertyHalService,再到综合的 VehicleHal,最后通过 HIDL 接口抵达以及更下面的 Hal,并按照定义的数据类型更改 ECU 的相关属性

希望本文能言简意赅地带你了解车辆属性的大体全貌,感谢阅读。

推荐阅读

  • 一文了解 Android 车机如何处理中控的旋钮输入
  • 从实体按键看 Android 车机的自定义事件机制
  • 深度入门 Android 车机核心 CarService 的构成和链路
  • Android 车机初体验:Auto,Automotive 傻傻分不清楚?

参考资料

  • CarPropertyManager

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://www.net2asp.com/9dd21c33e6.html