前言
前面我们介绍过 Android 车机 Automotive OS 的几块重要内容:
本篇文章我们聚焦 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<Integer> 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 <E> CarPropertyValue<E> getProperty(@NonNull Class<E> clazz, int propId, int areaId) {
checkSupportedProperty(propId);
try {
CarPropertyValue<E> propVal = mService.getProperty(propId, areaId);
if (propVal != null && propVal.getValue() != null) {
Class<?> actualClass = propVal.getValue().getClass();
}
return propVal;
}
...
}
...
}
CarPropertyService
按照如下步骤进行:
-
先到存放所有 Property ID 的
SparseArray
中检查是否确实存在该 Property,如果不存在的话打印 error 提醒并结束 -
获取该 Property 的 permission 配置,如果不存在的话,抛出:
SecurityException: Platform does not have permission to read value for property Id: 0x…
-
assertPermission()
检查当前 CarService 是否确实被授予了如上 permission -
最后调用持有的
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<ValueResult> 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 大同小异,主要区别是:
- 事先构建待写入的属性实例
CarPropertyValue
并传入 - 传入属性变化时 callback 用的
CarPropertyEventListenerToService
实例
public class CarPropertyManager extends CarManagerBase {
public void setIntProperty(int prop, int areaId, int val) {
setProperty(Integer.class, prop, areaId, val);
}
public <E> void setProperty(@NonNull Class<E> 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<String> 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;
...
}
结语
结合一张图回顾下整个过程:
- App 先通过 Car lib 拿到
CarService
的Car
实例,CarService 会初始化所有 Car 相关的实现,比如其中车辆属性的化,会初始化CarPropertyService
和PropertyHalService
等 - 接着,App 会从 Car 实例获取车辆某个接口的实例,比如控制车辆属性的话,需要获取
CarPropertyManager
,CarService 则会从初始化完成的 map 里返回已准备好的对应对象 - App 的属性读写会通过 AIDL 接口抵达直接负责的 CarPropertyService,然后到与 HAL 中车辆属性模块交互的
PropertyHalService
,再到综合的VehicleHal
,最后通过 HIDL 接口抵达以及更下面的Hal
,并按照定义的数据类型更改 ECU 的相关属性
希望本文能言简意赅地带你了解车辆属性的大体全貌,感谢阅读。
推荐阅读
- 一文了解 Android 车机如何处理中控的旋钮输入
- 从实体按键看 Android 车机的自定义事件机制
- 深度入门 Android 车机核心 CarService 的构成和链路
- Android 车机初体验:Auto,Automotive 傻傻分不清楚?
文章评论