蓝牙温湿度计实战:Environmental Sensing Service、定期上报、阈值告警
好,咱们今天来搞点实在的。前面讲了那么多 BLE 的基础概念、Service 和 Characteristic 的读写套路,是时候拿一个真实设备练练手了。我个人觉得,温湿度计是入门 BLE 开发最好的玩具——逻辑简单、数据直观、反馈即时。你写完代码,手机一贴,温度数字跳出来,那种成就感是很直接的。
Environmental Sensing Service 是什么?
蓝牙联盟定义了一套标准服务,叫 Environmental Sensing Service,简称 ESS。它的 UUID 是 0x181A。说白了,就是专门给环境传感器用的——温度、湿度、气压、光照,统统归它管。
我刚开始接触 BLE 时,看到一堆标准 Service 列表,头都大了。后来发现,其实你只需要记住几个常用的就行。ESS 就是其中之一,而且它的结构非常清晰。
| Characteristic | UUID | 说明 |
|---|---|---|
| Temperature | 0x2A6E | 当前温度值,单位摄氏度 |
| Humidity | 0x2A6F | 当前湿度值,单位百分比 |
| Pressure | 0x2A6D | 大气压,单位帕斯卡 |
嗯,这里要注意:温度 Characteristic 的数据格式是 INT16,单位是 0.01 摄氏度。也就是说,设备发过来的原始值是 2536,实际温度是 25.36°C。这个换算关系,我在项目里吃过亏——有一次直接拿原始值显示,结果温度飙到 2536 度,差点以为实验室着火了。
连接与发现 Service
连接设备后,第一步就是发现 Service。这个过程其实很简单,调用 BluetoothGatt.discoverServices() 就行。但有个坑:这个方法是异步的,结果会通过回调返回。
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "Service discovery failed")
return
}
val essService = gatt.getService(UUID.fromString("0000181a-0000-1000-8000-00805f9b34fb"))
if (essService == null) {
Log.e(TAG, "ESS not found")
return
}
tempCharacteristic = essService.getCharacteristic(UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb"))
humCharacteristic = essService.getCharacteristic(UUID.fromString("00002a6f-0000-1000-8000-00805f9b34fb"))
}
你想想看,如果设备不支持 ESS,你硬要去读,那肯定报错。所以每次都要判空。我曾经在代码里漏了判空,结果上线后用户反馈 App 闪退,查了半天才发现是某款廉价温湿度计压根没实现标准 Service。
定期上报:Notification 的正确打开方式
温湿度计这种设备,你不会想一直轮询去读数据。那样太费电,也太费流量。正确的做法是让设备主动上报——也就是开启 Notification。
开启 Notification 需要两步:
- 写入 Client Characteristic Configuration Descriptor(CCCD),值为
0x0001表示开启通知。 - 在
onCharacteristicChanged回调里接收数据。
fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val success = gatt.setCharacteristicNotification(characteristic, true)
if (!success) {
Log.w(TAG, "setCharacteristicNotification failed")
return
}
val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
这里有个细节:setCharacteristicNotification 只是本地注册,真正让设备开始发数据的是写 CCCD。两个步骤缺一不可。我见过有人只调了第一个方法,然后一直等不到回调,急得团团转。
收到数据后,解析也很直接:
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val rawValue = characteristic.value
if (rawValue == null) return
when (characteristic.uuid.toString()) {
"00002a6e-0000-1000-8000-00805f9b34fb" -> {
val tempC = rawValue.toShort() / 100.0f
Log.d(TAG, "Temperature: $tempC °C")
}
"00002a6f-0000-1000-8000-00805f9b34fb" -> {
val humidity = rawValue.toShort() / 100.0f
Log.d(TAG, "Humidity: $humidity %")
}
}
}
阈值告警:让 App 主动提醒你
光显示数据还不够,咱们得加点智能。比如温度超过 30°C 就弹个通知,湿度低于 20% 就提醒加湿。这个逻辑不复杂,但要注意几点:
- 阈值要可配置,别写死在代码里
- 告警不要重复触发,否则用户会被烦死
- 断开连接后要重置状态
data class ThresholdConfig(
val tempMin: Float = 18.0f,
val tempMax: Float = 28.0f,
val humMin: Float = 30.0f,
val humMax: Float = 70.0f
)
class AlertManager(private val context: Context) {
private var lastAlertTime = mutableMapOf<String, Long>()
private val cooldownMs = 60_000L // 同一告警一分钟内不重复
fun checkAndAlert(temp: Float, hum: Float, config: ThresholdConfig) {
if (temp < config.tempMin) {
triggerAlert("温度过低", "$temp °C 低于阈值 ${config.tempMin} °C")
} else if (temp > config.tempMax) {
triggerAlert("温度过高", "$temp °C 超过阈值 ${config.tempMax} °C")
}
if (hum < config.humMin) {
triggerAlert("湿度过低", "$hum % 低于阈值 ${config.humMin} %")
} else if (hum > config.humMax) {
triggerAlert("湿度过高", "$hum % 超过阈值 ${config.humMax} %")
}
}
private fun triggerAlert(title: String, message: String) {
val now = System.currentTimeMillis()
val key = "$title:$message"
if (now - (lastAlertTime[key] ?: 0L) < cooldownMs) return
lastAlertTime[key] = now
// 发送系统通知
NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_alert)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
.also { notificationManager.notify(key.hashCode(), it) }
}
}
整体架构图
下面这张图展示了温湿度计 App 的核心数据流。从设备连接、Service 发现,到 Notification 接收、数据解析,再到阈值判断和告警触发,一气呵成。
避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 设备断开后要清理状态:Notification 的注册、阈值告警的冷却时间,都要在
onConnectionStateChange里重置。否则重连后可能收不到数据,或者告警逻辑混乱。 - 不要在主线程做数据解析:虽然 INT16 转浮点数很快,但万一设备上报频率高,或者你后面加了更复杂的计算,UI 线程会卡。建议用
HandlerThread或者 Kotlin 协程。 - 测试不同品牌的设备:有些国产温湿度计虽然宣称支持 ESS,但实现并不标准。比如温度 Characteristic 的 UUID 可能不是 0x2A6E,而是厂商自定义的。遇到这种情况,只能通过抓包或者厂商文档来确认。
嗯,温湿度计这块基本就这些了。代码量不大,但涉及的知识点很典型——Service 发现、Notification 开启、数据解析、业务逻辑判断。把这些搞透了,后面做更复杂的 BLE 设备(比如心率计、体脂秤)就会顺手很多。