第七章 车辆信息获取:从传感器到车身控制
做车载开发,最核心的一件事是什么?
我个人觉得,就是「读懂车」。车在跑多快?电量还剩多少?空调设了多少度?车门关没关好?这些信息,就是我们和车辆对话的基础。
这一章,我们就来聊聊怎么用 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 的三条主线:传感器数据、空调控制、车身状态。底部是监听机制,用于实时获取数据变化。
7.7 避坑总结
最后,把我这些年踩过的坑总结一下:
- 不要假设所有属性都存在。 不同车型支持的属性不一样。读之前先检查返回值是否为 null。
- 注意区域 ID。 主驾不一定是
ROW_1_LEFT,要看车辆是左舵还是右舵。 - 设置属性后要确认。 CAN 总线有延迟,设置完等一会儿再读。
- 高频属性用监听,低频属性用轮询。 别反过来,否则性能会崩。
- 安全第一。 车窗、车门、空调这些涉及安全的控制,一定要判断车辆状态(P 挡?行驶中?)。
嗯,这一章的内容就到这里。记住一句话:CarPropertyManager 是你和车辆对话的翻译官。用好它,你的 App 就能真正「读懂车」。
公众号:蓝海资料掘金营,微信deep3321