第七章 车辆信息获取:从传感器到车身控制

做车载开发,最核心的一件事是什么?

我个人觉得,就是「读懂车」。车在跑多快?电量还剩多少?空调设了多少度?车门关没关好?这些信息,就是我们和车辆对话的基础。

这一章,我们就来聊聊怎么用 Android Automotive 的 API 拿到这些数据。说白了,就是让我们的 App 能「听懂车说的话」。

7.1 CarPropertyManager:车辆属性的统一入口

先问一个问题:车的速度、电量、里程,这些数据从哪里来?

在 Android Automotive 里,它们都通过 CarPropertyManager 这个类来获取。你可以把它理解成一个「万能读卡器」——不管你想读什么车辆属性,都找它。

核心概念:CarPropertyManager 是 Car Service 暴露给应用层的统一属性管理接口。所有车辆传感器数据、车身状态,都通过它来读取或监听。

7.1.1 获取 CarPropertyManager 实例

第一步,先拿到 Car 的连接。我习惯在 Service 或 Activity 的 onCreate 里做这件事:

// 获取 Car 服务
Car car = Car.createCar(context, new Car.CarServiceLifecycleListener() {
    @Override
    public void onCarConnected(Car car) {
        // 连接成功
        CarPropertyManager propertyManager = 
            (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE);
        if (propertyManager != null) {
            // 可以开始读取数据了
        }
    }

    @Override
    public void onCarDisconnected(Car car) {
        // 连接断开,记得清理资源
    }
});

嗯,这里要注意:Car.createCar() 是异步的。你不能在调用后立刻就用 propertyManager,得等回调。我曾经在项目里踩过这个坑——回调还没触发就去读数据,结果拿了个 null。

7.1.2 读取车辆属性

拿到 CarPropertyManager 之后,读数据就简单了。核心方法就两个:

  • getProperty(Class<T> clazz, int propertyId, int areaId) —— 读一次
  • getPropertyList(Class<T> clazz, int propertyId, int areaId) —— 读列表

举个例子,读当前车速:

CarPropertyManager propertyManager = ...;

// 读取车速(单位:m/s)
CarProperty<Float> speedProperty = 
    propertyManager.getProperty(Float.class, 
        VehiclePropertyIds.PERF_VEHICLE_SPEED, 
        VehicleArea.GLOBAL);

if (speedProperty != null) {
    float speedMps = speedProperty.getValue();
    float speedKmph = speedMps * 3.6f;  // 转成 km/h
    Log.d("Speed", "当前车速:" + speedKmph + " km/h");
}

你想想看,为什么车速的单位是 m/s 而不是 km/h?

这是汽车行业的标准。底层 CAN 总线出来的数据,基本都是国际单位制。我们上层展示的时候再转换。我个人习惯在 ViewModel 里做这个转换,保持数据层的纯净。

7.2 车辆传感器数据:速度、电量、里程

这三个数据,是车载应用最常用的。我们一个一个来看。

7.2.1 速度(Vehicle Speed)

速度的 Property ID 是 PERF_VEHICLE_SPEED,类型是 Float,单位 m/s。

但有个坑:不是所有车都支持这个属性。有些车可能只支持 PERF_ODOMETER(里程)或者 PERF_WHEEL_SPEED(轮速)。

避坑指南:我曾经在一款国产电动车上测试,发现 PERF_VEHICLE_SPEED 返回的一直是 0。后来查了 HAL 层才知道,这辆车只上报了轮速,没有上报车速。解决方案是:用轮速取平均值,再根据轮胎半径换算成车速。

7.2.2 电量(Battery Level)

电量的 Property ID 是 EV_BATTERY_LEVEL,类型是 Float,范围 0.0 ~ 1.0。

// 读取电量
CarProperty<Float> batteryProperty = 
    propertyManager.getProperty(Float.class, 
        VehiclePropertyIds.EV_BATTERY_LEVEL, 
        VehicleArea.GLOBAL);

if (batteryProperty != null) {
    float level = batteryProperty.getValue();  // 0.0 ~ 1.0
    int percent = (int) (level * 100);
    Log.d("Battery", "当前电量:" + percent + "%");
}

这里有个细节:EV_BATTERY_LEVEL 是电动车专属的。如果是燃油车,你得读 FUEL_LEVEL。怎么判断车型?可以读 VEHICLE_MODEL 或者 VEHICLE_TYPE 属性。

7.2.3 里程(Odometer)

里程的 Property ID 是 PERF_ODOMETER,类型是 Float,单位 km。

// 读取总里程
CarProperty<Float> odometerProperty = 
    propertyManager.getProperty(Float.class, 
        VehiclePropertyIds.PERF_ODOMETER, 
        VehicleArea.GLOBAL);

if (odometerProperty != null) {
    float totalKm = odometerProperty.getValue();
    Log.d("Odometer", "总里程:" + totalKm + " km");
}

嗯,这里要注意:里程数据是只读的,你不能通过 CarPropertyManager 去修改它。这是安全考虑——里程表造假是违法的。

7.3 空调控制:从读取到设置

空调控制比传感器数据复杂一点。因为它涉及「区域」的概念——主驾、副驾、后排,温度可以不一样。

7.3.1 读取空调状态

空调相关的属性都在 VehiclePropertyIds 里,以 HVAC_ 开头:

属性 ID 类型 说明
HVAC_TEMPERATURE_SET Float 目标温度(单位:摄氏度)
HVAC_FAN_SPEED Integer 风扇档位(0~7)
HVAC_AC_ON Boolean A/C 开关
HVAC_FAN_DIRECTION Integer 出风方向(吹面/吹脚/除霜)

读取主驾温度:

// 读取主驾目标温度
CarProperty<Float> tempProperty = 
    propertyManager.getProperty(Float.class, 
        VehiclePropertyIds.HVAC_TEMPERATURE_SET, 
        VehicleArea.SEAT_ROW_1_LEFT);

if (tempProperty != null) {
    float temp = tempProperty.getValue();
    Log.d("HVAC", "主驾目标温度:" + temp + "°C");
}

你可能会问:为什么用 SEAT_ROW_1_LEFT 而不是 DRIVER

因为有些车是右舵的,主驾在右边。用 SEAT_ROW_1_LEFT 是绝对位置,跟方向盘在哪边无关。我个人建议:涉及座椅区域的,都用绝对位置,别用「主驾」「副驾」这种语义。

7.3.2 设置空调参数

设置属性用 setProperty() 方法:

// 设置主驾温度到 24°C
propertyManager.setProperty(Float.class, 
    VehiclePropertyIds.HVAC_TEMPERATURE_SET, 
    VehicleArea.SEAT_ROW_1_LEFT, 
    24.0f);

// 打开 A/C
propertyManager.setProperty(Boolean.class, 
    VehiclePropertyIds.HVAC_AC_ON, 
    VehicleArea.GLOBAL, 
    true);

// 设置风扇到 3 档
propertyManager.setProperty(Integer.class, 
    VehiclePropertyIds.HVAC_FAN_SPEED, 
    VehicleArea.GLOBAL, 
    3);

小技巧:设置空调参数后,最好等 1~2 秒再读一次,确认设置生效。因为有些车的 CAN 总线有延迟,设置完立刻读取可能还是旧值。我在项目里加了个 500ms 的延迟轮询,效果不错。

7.4 车窗与车门状态

车窗和车门的状态,属于「车身控制」范畴。它们的属性 ID 以 WINDOW_DOOR_ 开头。

7.4.1 车窗状态

车窗的属性是 WINDOW_POS,类型是 Integer,范围 0~100(0 表示完全关闭,100 表示完全打开)。

// 读取主驾车窗位置
CarProperty<Integer> windowProperty = 
    propertyManager.getProperty(Integer.class, 
        VehiclePropertyIds.WINDOW_POS, 
        VehicleArea.WINDOW_ROW_1_LEFT);

if (windowProperty != null) {
    int pos = windowProperty.getValue();
    if (pos == 0) {
        Log.d("Window", "主驾车窗:已关闭");
    } else if (pos == 100) {
        Log.d("Window", "主驾车窗:已完全打开");
    } else {
        Log.d("Window", "主驾车窗:打开 " + pos + "%");
    }
}

控制车窗升降,用 setProperty()

// 关闭主驾车窗
propertyManager.setProperty(Integer.class, 
    VehiclePropertyIds.WINDOW_POS, 
    VehicleArea.WINDOW_ROW_1_LEFT, 
    0);

安全警告:车窗控制涉及安全。很多车机系统要求:只有车辆处于 P 挡(驻车)时,才允许 App 控制车窗。我曾经在项目里没做这个判断,结果测试时车窗在行驶中自动降下来了……嗯,后来被安全团队狠批了一顿。

7.4.2 车门状态

车门的状态属性是 DOOR_POS,类型是 Integer,取值:

  • 0:车门关闭
  • 1:车门打开
  • 2:车门未关紧(半开)
// 读取主驾车门状态
CarProperty<Integer> doorProperty = 
    propertyManager.getProperty(Integer.class, 
        VehiclePropertyIds.DOOR_POS, 
        VehicleArea.DOOR_ROW_1_LEFT);

if (doorProperty != null) {
    int state = doorProperty.getValue();
    switch (state) {
        case 0:
            Log.d("Door", "主驾车门:已关闭");
            break;
        case 1:
            Log.d("Door", "主驾车门:已打开");
            break;
        case 2:
            Log.d("Door", "主驾车门:未关紧");
            break;
    }
}

车门状态通常是只读的。你不能通过 API 去锁门或开门——那是车身域控制器的事,App 层面只能读,不能写。

7.5 监听属性变化

很多时候,我们不想轮询,而是希望数据变化时主动通知我们。这时候就用 CarPropertyEventCallback

// 注册监听
propertyManager.registerCallback(callback, 
    VehiclePropertyIds.PERF_VEHICLE_SPEED, 
    VehicleArea.GLOBAL);

// 回调实现
CarPropertyEventCallback callback = new CarPropertyEventCallback() {
    @Override
    public void onChangeEvent(CarPropertyValue value) {
        // 属性值变化时触发
        float speed = (float) value.getValue();
        Log.d("Speed", "车速变化:" + speed);
    }

    @Override
    public void onErrorEvent(int propertyId, int zone) {
        // 读取出错时触发
        Log.e("CarProperty", "属性读取失败:" + propertyId);
    }
};

// 记得在不用时取消注册
propertyManager.unregisterCallback(callback);

这里有个性能问题:如果你监听太多属性,回调会非常频繁。车速每秒可能变化几十次,电量可能几分钟才变一次。我建议:高频变化的属性(车速、转速)用监听,低频变化的属性(电量、里程)用定时轮询。

7.6 本章知识体系

下面这张图,概括了本章的核心内容:

车辆信息获取体系 CarPropertyManager 传感器数据 速度 电量 里程 空调控制 温度 风扇 A/C 车身状态 车窗 车门 车锁 CarPropertyEventCallback 监听机制

这张图展示了 CarPropertyManager 的三条主线:传感器数据、空调控制、车身状态。底部是监听机制,用于实时获取数据变化。

7.7 避坑总结

最后,把我这些年踩过的坑总结一下:

  1. 不要假设所有属性都存在。 不同车型支持的属性不一样。读之前先检查返回值是否为 null。
  2. 注意区域 ID。 主驾不一定是 ROW_1_LEFT,要看车辆是左舵还是右舵。
  3. 设置属性后要确认。 CAN 总线有延迟,设置完等一会儿再读。
  4. 高频属性用监听,低频属性用轮询。 别反过来,否则性能会崩。
  5. 安全第一。 车窗、车门、空调这些涉及安全的控制,一定要判断车辆状态(P 挡?行驶中?)。

嗯,这一章的内容就到这里。记住一句话:CarPropertyManager 是你和车辆对话的翻译官。用好它,你的 App 就能真正「读懂车」。


公众号:蓝海资料掘金营,微信deep3321