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

7 篇文章 0 订阅
5 篇文章 2 订阅

在这里插入图片描述

前言

前面我们介绍过 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_FASTEST100
SENSOR_RATE_FAST10
SENSOR_RATE_NORMAL1
SENSOR_RATE_UI5

对于车窗、入座这些即时信号,采用 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 按照如下步骤进行:

  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<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 大同小异,主要区别是:

  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 <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);
    }
    ...
}

后续一样的是通过 VehicleHalHalClient,再到 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;
    ...
}

结语

在这里插入图片描述

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

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

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

推荐阅读

参考资料

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值