蓝牙灯控实战:Lighting Control
好,咱们今天聊点真正能上手玩的东西。蓝牙灯控,说白了就是让你的手机能控制灯泡的亮度、色温,甚至让几个灯一起联动。我在好几个智能家居项目里都做过类似的功能,踩过不少坑,今天把这些经验都抖出来。
1. 灯控协议的设计思路
先想一个问题:灯泡和手机怎么沟通?BLE 通信说白了就是读写特征值。我们需要定义一套协议,让手机发指令,灯泡执行。
我个人习惯用自定义 Service UUID 和 Characteristic UUID。比如:
// 灯控服务 UUID
val LIGHT_SERVICE_UUID = "0000FFE0-0000-1000-8000-00805F9B34FB"
// 亮度特征值 UUID
val BRIGHTNESS_CHAR_UUID = "0000FFE1-0000-1000-8000-00805F9B34FB"
// 色温特征值 UUID
val COLOR_TEMP_CHAR_UUID = "0000FFE2-0000-1000-8000-00805F9B34FB"
// 场景联动特征值 UUID
val SCENE_CHAR_UUID = "0000FFE3-0000-1000-8000-00805F9B34FB"
为什么这么设计?因为标准服务有时候不够灵活。我在项目中遇到过,有些灯泡厂商用 0xFFE0 这个私有服务,兼容性反而更好。
2. 亮度调节的实现
亮度调节,说白了就是写一个 0-100 的数值到特征值里。但这里有个细节:很多灯泡用的是 0-255 的 PWM 值。你想想看,手机端显示 0-100%,实际发出去要映射到 0-255。
核心逻辑:手机端用百分比,蓝牙传输用原始值,灯泡端再映射到 PWM。
fun setBrightness(device: BluetoothDevice, brightness: Int) {
// brightness: 0-100
val mappedValue = (brightness * 255 / 100).coerceIn(0, 255)
val data = byteArrayOf(mappedValue.toByte())
val gatt = device.connectGatt(context, false, gattCallback)
// ... 连接成功后写入特征值
gatt.writeCharacteristic(
gatt.getService(LIGHT_SERVICE_UUID)
.getCharacteristic(BRIGHTNESS_CHAR_UUID)
.apply { value = data }
)
}
嗯,这里要注意:coerceIn 是 Kotlin 的语法糖,防止数值越界。我曾经因为没做边界检查,传了个 101 进去,灯泡直接闪了一下然后灭了……后来查了半天才发现是数值溢出。
3. 色温控制
色温控制比亮度复杂一点。色温通常用开尔文(K)表示,范围从 2700K(暖黄)到 6500K(冷白)。但蓝牙传输时,一般用两个字节表示。
我建议这样设计:
fun setColorTemperature(device: BluetoothDevice, kelvin: Int) {
// kelvin: 2700-6500
val highByte = (kelvin shr 8).toByte()
val lowByte = (kelvin and 0xFF).toByte()
val data = byteArrayOf(highByte, lowByte)
// 写入色温特征值
writeCharacteristic(device, COLOR_TEMP_CHAR_UUID, data)
}
为什么用两个字节?因为 6500 大于 255,一个字节装不下。你想想看,如果用单字节,最大只能表示 255K,那灯泡就只能发出一种颜色了。
小技巧:有些灯泡支持色温和亮度一起发送,用一个特征值包含两个字段。比如第一个字节是亮度,第二、三个字节是色温。这样一次写入就能同时调节两个参数,减少蓝牙通信次数。
4. 场景联动
场景联动,说白了就是让多个灯泡同时执行一个预设方案。比如「阅读模式」:主灯亮度 80%,色温 4000K;台灯亮度 60%,色温 5000K。
实现方式有两种:
- 手机端联动:手机逐个连接每个灯泡,发送指令。优点是灵活,缺点是慢。
- 灯泡端联动:手机只发一个场景编号,灯泡自己内部协调。优点是快,缺点是灯泡需要支持组播。
我在项目中更倾向于第一种,因为实现简单,而且兼容性好。第二种需要灯泡固件支持,不是所有厂商都开放。
data class Scene(
val name: String,
val brightness: Int, // 0-100
val colorTemp: Int // 2700-6500
)
fun applyScene(devices: List<BluetoothDevice>, scene: Scene) {
devices.forEach { device ->
setBrightness(device, scene.brightness)
setColorTemperature(device, scene.colorTemp)
}
}
嗯,这里有个坑:如果灯泡数量多,比如 10 个,逐个连接发送指令会很慢。我曾经试过,10 个灯泡全部设置完需要 5-6 秒。用户体验很差。
避坑指南:我曾经在项目中遇到一个问题——同时连接多个 BLE 设备,Android 系统会限制并发连接数(通常是 6-7 个)。超过这个数,后面的连接会失败。解决方案是:用队列管理连接,一个一个来,或者断开不用的设备。
5. 实战中的性能优化
灯控场景对实时性要求很高。你滑动亮度条,灯泡应该立刻响应。但 BLE 通信有延迟,怎么优化?
我总结了几点:
- 减少通信次数:把亮度和色温合并到一个特征值里,一次写入搞定。
- 使用 Write Without Response:如果不需要确认,就用这个模式,速度更快。
- 预连接:在用户打开控制界面时,提前连接好灯泡,不要等用户滑动条了才去连。
// 使用 Write Without Response
fun writeFast(device: BluetoothDevice, uuid: UUID, data: ByteArray) {
val gatt = device.connectGatt(context, false, gattCallback)
val char = gatt.getService(LIGHT_SERVICE_UUID).getCharacteristic(uuid)
gatt.beginReliableWrite() // 可选,用于批量写入
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
char.value = data
gatt.writeCharacteristic(char)
}
你想想看,如果每次滑动都等确认,那延迟至少 50ms,用户会感觉卡顿。用无响应写入,延迟能降到 10ms 以内。
6. 知识体系总览
下面这张图总结了蓝牙灯控的核心流程:
7. 总结与经验
蓝牙灯控看起来简单,但要做好其实不容易。我做了几个项目后,最大的感受是:协议设计决定上限,性能优化决定下限。
如果你刚开始做,建议先实现单灯控制,再扩展到多灯联动。不要一上来就想搞复杂的场景联动,容易把自己绕进去。
嗯,最后说一句:调试的时候,买个便宜的蓝牙抓包工具,能省你很多时间。我曾经为了查一个指令没响应的 bug,折腾了两天,最后发现是特征值 UUID 写错了……
核心要点回顾:
- 亮度用 0-100% 映射到 0-255
- 色温用两个字节传输,范围 2700-6500K
- 场景联动用手机端逐个控制,简单可靠
- 性能优化用无响应写入和预连接
- 注意 Android 并发连接数限制