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

前言

前面我们介绍过 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 和上述的通知类型,即可完成该属性的监听。

kotlin 复制代码
java
复制代码
     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 如下,比如前排、驾驶侧、副驾驶侧、乘客侧、天窗、挡风玻璃等。

java 复制代码
java
复制代码
     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;
     }

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

scss 复制代码
java
复制代码
 Thread().run {
     carPropertyManager.setIntProperty(
         VehiclePropertyIds.WINDOW_POS,
         VehicleAreaWindow.WINDOW_ROW_1_LEFT,
         0
    )
 }

工作原理

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

ini 复制代码
java
复制代码
 // 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(),不过指定了返回的数据类型。

scala 复制代码
java
复制代码
     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。

scala 复制代码
java
复制代码
     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 按照如下步骤进行:

  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 继续发出读取的调用

scala 复制代码
java
复制代码

     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 类型返回。

scala 复制代码
java
复制代码
     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 来处理。

java 复制代码
java
复制代码
     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 出去。

ini 复制代码
java
复制代码
     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 侧获取相应数据。

java 复制代码
java
复制代码
     public abstract class VehicleStub {
         ...
         @Nullable
         public abstract HalPropValue get(HalPropValue requestedPropValue)
                 throws RemoteException, ServiceSpecificException;
         ...
     }

写入

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

  1. 事先构建待写入的属性实例 CarPropertyValue 并传入
  2. 传入属性变化时 callback 用的 CarPropertyEventListenerToService 实例
scala 复制代码
java
复制代码

     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 缓存起来,在属性变化的时候用。

ini 复制代码
java
复制代码
     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 侧。

scala 复制代码
java
复制代码
     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。

java 复制代码
java
复制代码
     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;
         ...
     }

结语

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

  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 的相关属性

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

推荐阅读

参考资料

作者:TechMerger

链接:juejin.cn/post/729200...

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐
木子欢儿2 小时前
Zabbix 7.4 Alpine 版 Docker Compose 部署
android·运维·docker·容器·zabbix
素师良码3 小时前
第9篇:Android驱动踩坑实录:DSI RX FIFO前置脏0、数据错位复盘
android
夜雪一千4 小时前
如何在MySQL中实现字段级加密
android·数据库·mysql
夜雪一千4 小时前
MySQL 如何存储经纬度坐标:选型、建表、空间查询实战
android·mysql·adb
JMchen4 小时前
集成第三方 C/C++ 库到 Android NDK 项目:OpenCV 与 FFmpeg 实战指南
android·opencv·cmake
有毒的教程5 小时前
Dev-C++ 使用扩展库两种方式:DevPak一键安装 / 手动配置(重点)
android·c++·adb
mmsx6 小时前
Android 测绘数值基础:测量单位系统与七四参数、高程拟合
android
郑州光合科技余经理6 小时前
出海外卖平台:支付对接为什么要和下单收款分开
android·java·开发语言·前端·后端·php·ai编程
夜雪一千6 小时前
MySQL 如何提高查询速度:从索引、SQL、表结构到实战调优
android·sql·mysql
终端安全笔记6 小时前
配置锁状态那一栏查的是什么:报告里几项各自指向什么风险
android·前端·安全·ios