实战项目一:构建一个简单的车载空调控制应用

说实话,车载空调控制这个场景,是我在接触 Android Automotive OS 时第一个想到的实战项目。为什么?因为它太典型了。你想想看,空调控制涉及温度、风量、模式这些物理参数,而这些参数恰恰是通过 CarPropertyService 与 VHAL 来管理的。说白了,这就是一个「应用层 ↔ 服务层 ↔ 硬件层」的完整链路。

我个人习惯,每学一个新系统,先拿一个具体的硬件控制项目练手。今天我们就来做一个车载空调控制应用,麻雀虽小,五脏俱全。

项目背景与目标

我们要实现的功能很简单:

  • 显示当前车内温度
  • 显示当前设定的目标温度
  • 提供温度加减按钮
  • 提供风量调节按钮
  • 显示空调开关状态

嗯,听起来像是一个普通的 App?但关键在于,这些数据不是从本地数据库读的,而是从汽车的 VHAL(Vehicle Hardware Abstraction Layer)获取的。我们通过 CarPropertyService 来读写这些属性。

整体架构图

先来看一下我们这个项目的整体架构。我画了一张图,帮你理清各个模块之间的关系:

空调控制应用 CarPropertyService VHAL 空调硬件 getProperty / setProperty 属性读写 硬件控制 数据流向说明: 1. 应用通过 CarPropertyService.getProperty() 读取空调状态 2. 应用通过 CarPropertyService.setProperty() 设置目标温度 3. CarPropertyService 将请求转发给 VHAL 4. VHAL 与底层硬件通信,执行实际控制

第一步:获取 Car 实例

在 Android Automotive OS 中,所有车辆相关的服务都通过 Car 对象来访问。你需要先获取 Car 实例,然后才能拿到 CarPropertyService。

// 在 Activity 或 Fragment 中
private Car car;
private CarPropertyService propertyService;

private void initCar() {
    car = Car.createCar(this, new Car.CarServiceLifecycleListener() {
        @Override
        public void onConnected(Car car) {
            // 连接成功,获取 CarPropertyService
            propertyService = (CarPropertyService) car.getCarManager(
                android.car.Car.PROPERTY_SERVICE
            );
            if (propertyService != null) {
                Log.d(TAG, "CarPropertyService 连接成功");
                loadCurrentAcState();
            }
        }

        @Override
        public void onDisconnected(Car car) {
            propertyService = null;
            Log.w(TAG, "Car 服务断开连接");
        }
    });
    car.connect();
}
个人经验:我在项目中遇到过 Car 连接失败的情况,通常是因为应用没有声明 android.car.permission.CAR_PROPERTY_SERVICE 权限。记得在 AndroidManifest.xml 中添加这个权限,否则 onConnected 永远不会回调。

第二步:读取空调属性

空调相关的属性定义在 android.car.VehiclePropertyIds 中。常用的有:

属性 ID 说明 数据类型
HVAC_TEMPERATURE_CURRENT 当前车内温度 float(摄氏度)
HVAC_TEMPERATURE_SET 目标设定温度 float(摄氏度)
HVAC_FAN_SPEED 风扇转速 int(0-100)
HVAC_POWER_ON 空调开关 boolean

读取当前温度的代码:

private float getCurrentTemperature() {
    if (propertyService == null) return -1f;
    try {
        // 注意:需要指定区域 ID,通常驾驶位是 0
        CarPropertyValue<Float> value = propertyService.getProperty(
            VehiclePropertyIds.HVAC_TEMPERATURE_CURRENT,
            0  // 区域 ID,0 表示驾驶位
        );
        return value != null ? value.getValue() : -1f;
    } catch (CarPropertyNotAvailableException e) {
        Log.e(TAG, "读取温度失败", e);
        return -1f;
    }
}
注意:我曾经踩过一个坑——没有处理 CarPropertyNotAvailableException。在车辆启动初期,某些属性可能暂时不可用。如果你不捕获这个异常,应用会直接崩溃。所以,所有 getProperty 调用都要包在 try-catch 里。

第三步:设置空调属性

设置属性比读取稍微复杂一点。你需要构造一个 CarPropertyValue 对象,然后调用 setProperty

private void setTargetTemperature(float temp) {
    if (propertyService == null) return;
    try {
        // 构造属性值对象
        CarPropertyValue<Float> value = new CarPropertyValue<>(
            VehiclePropertyIds.HVAC_TEMPERATURE_SET,
            0,    // 区域 ID
            temp  // 目标温度
        );
        propertyService.setProperty(value);
        Log.d(TAG, "设置目标温度: " + temp);
    } catch (CarPropertyNotAvailableException e) {
        Log.e(TAG, "设置温度失败", e);
    }
}

你可能会问:为什么需要区域 ID?嗯,这是因为汽车空调通常支持分区控制——驾驶位和副驾驶位可以设置不同的温度。区域 ID 0 表示驾驶位,1 表示副驾驶位。

第四步:监听属性变化

光能读写还不够,我们还需要实时监听空调状态的变化。比如用户按了物理按键,或者系统自动调节了温度,我们的应用要能感知到。

private CarPropertyService.CarPropertyEventCallback callback;

private void registerCallback() {
    if (propertyService == null) return;
    
    callback = new CarPropertyService.CarPropertyEventCallback() {
        @Override
        public void onChangeEvent(CarPropertyValue value) {
            // 属性值发生变化时回调
            int propertyId = value.getPropertyId();
            switch (propertyId) {
                case VehiclePropertyIds.HVAC_TEMPERATURE_CURRENT:
                    float currentTemp = (float) value.getValue();
                    updateCurrentTempDisplay(currentTemp);
                    break;
                case VehiclePropertyIds.HVAC_TEMPERATURE_SET:
                    float setTemp = (float) value.getValue();
                    updateSetTempDisplay(setTemp);
                    break;
                case VehiclePropertyIds.HVAC_POWER_ON:
                    boolean isOn = (boolean) value.getValue();
                    updatePowerStatus(isOn);
                    break;
            }
        }

        @Override
        public void onErrorEvent(int propertyId, int zone) {
            Log.w(TAG, "属性监听出错: " + propertyId);
        }
    };

    // 注册监听,需要指定要监听的属性列表
    List<Integer> propertyIds = Arrays.asList(
        VehiclePropertyIds.HVAC_TEMPERATURE_CURRENT,
        VehiclePropertyIds.HVAC_TEMPERATURE_SET,
        VehiclePropertyIds.HVAC_FAN_SPEED,
        VehiclePropertyIds.HVAC_POWER_ON
    );
    propertyService.registerCallback(callback, propertyIds);
}
避坑指南:我曾经忘记在 onDestroy 中注销回调,导致 Activity 销毁后还在接收事件,引发内存泄漏。记得在生命周期结束时调用 propertyService.unregisterCallback(callback)

第五步:完整的 UI 交互

最后,我们把所有功能串起来,做一个简单的 UI。布局文件就不贴了,直接看核心逻辑:

public class AcControlActivity extends AppCompatActivity {
    private TextView tvCurrentTemp, tvSetTemp, tvFanSpeed;
    private Button btnTempUp, btnTempDown, btnFanUp, btnFanDown;
    private Switch switchPower;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ac_control);
        
        initViews();
        initCar();
        setupListeners();
    }

    private void setupListeners() {
        btnTempUp.setOnClickListener(v -> {
            float current = getSetTemperature();
            setTargetTemperature(current + 0.5f);
        });
        
        btnTempDown.setOnClickListener(v -> {
            float current = getSetTemperature();
            setTargetTemperature(current - 0.5f);
        });
        
        switchPower.setOnCheckedChangeListener((buttonView, isChecked) -> {
            setAcPower(isChecked);
        });
    }

    private void loadCurrentAcState() {
        // 读取所有空调状态并更新 UI
        float currentTemp = getCurrentTemperature();
        float setTemp = getSetTemperature();
        int fanSpeed = getFanSpeed();
        boolean isOn = getAcPower();
        
        runOnUiThread(() -> {
            tvCurrentTemp.setText(String.format("%.1f°C", currentTemp));
            tvSetTemp.setText(String.format("%.1f°C", setTemp));
            tvFanSpeed.setText(fanSpeed + "%");
            switchPower.setChecked(isOn);
        });
    }
}

小结

这个实战项目虽然简单,但覆盖了 CarPropertyService 的核心用法:连接服务、读写属性、监听变化。你想想看,掌握了这些,任何车载硬件控制应用你都能做——座椅调节、车窗控制、氛围灯,原理都是一样的。

我个人觉得,做车载开发最爽的一点就是:你写的代码能直接控制物理世界。按下按钮,空调真的吹出冷风,这种即时反馈是普通 App 开发给不了的。

核心要点回顾:
  • 通过 Car.createCar() 获取 Car 实例,再获取 CarPropertyService
  • 使用 getProperty() 读取属性,setProperty() 写入属性
  • 每个属性都需要指定区域 ID(zone ID)
  • 注册 CarPropertyEventCallback 监听属性变化
  • 别忘了处理 CarPropertyNotAvailableException
  • 生命周期管理:及时注销回调,避免内存泄漏

嗯,这个项目就到这里。代码量不大,但每一行都有它的分量。如果你在实际开发中遇到问题,欢迎随时交流。


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