11、BLE实战-温湿度传感器:解析环境传感服务(Environmental Sensing Service 0x181A)、数据格式转换(IEEE 11073)、定时轮询与通知模式对比

好,咱们今天聊点硬核的。温湿度传感器,这玩意儿在物联网项目里太常见了。你买个蓝牙温湿度计,连上手机,就能看到温度和湿度。但背后的数据是怎么来的?怎么解析的?我当年第一次做这个的时候,也踩了不少坑。

今天这一章,咱们就手把手把环境传感服务(0x181A)扒开来看。包括IEEE 11073这个奇怪的数据格式怎么转成正常人能看懂的数值,以及轮询和通知两种模式到底该怎么选。

11.1 环境传感服务(0x181A)长什么样?

蓝牙官方定义的环境传感服务,UUID是0x181A。它里面包含了一堆特征值,比如温度、湿度、气压、光照等等。咱们最常用的就是温度和湿度。

我习惯先看GATT结构。连上设备后,你会在服务列表里找到这个服务。它下面通常有两个特征值:

  • 温度特征值:UUID = 0x2A6E(Temperature)
  • 湿度特征值:UUID = 0x2A6F(Humidity)

嗯,这里要注意。有些厂商会自定义UUID,但正规的蓝牙温湿度计都会遵循这个标准。我在项目里遇到过一款设备,它把温度和湿度合并到一个特征值里了,那就不符合规范,需要单独处理。

核心要点:环境传感服务(0x181A)是标准服务,温度和湿度特征值分别是0x2A6E和0x2A6F。如果设备不按这个来,你得找厂商要文档。

11.2 IEEE 11073数据格式转换

好,现在你从特征值里读到了一串字节。比如温度特征值返回了[0x00, 0x00, 0x00, 0x00, 0x16, 0x0A]。这啥意思?

这就是IEEE 11073格式。说白了,它是一种用于医疗和健康设备的浮点数表示法。蓝牙规范里规定,环境传感服务的数据必须用这个格式传输。

IEEE 11073-20601标准定义了一种叫做SFLOAT(短浮点数)的类型。它占用16位(2个字节),结构如下:

含义
15 符号位(0正1负)
14-12 指数(3位,二进制补码)
11-0 尾数(12位,二进制补码)

计算公式:值 = 尾数 × 10^指数

举个例子。刚才的[0x16, 0x0A],咱们把它拼成16位:0x0A16(注意字节序,通常是低位在前)。

二进制:0000 1010 0001 0110

  • 符号位:0(正数)
  • 指数:000(0)
  • 尾数:1010 0001 0110 = 0xA16 = 2582

计算结果:2582 × 10^0 = 2582。但温度单位是0.01摄氏度,所以实际温度是 25.82°C

你看,其实不复杂。我刚开始做的时候,直接拿计算器按了半天,后来写了个工具类,一行代码搞定。

我的习惯:在Android里写一个IEEE11073Utils工具类,专门处理SFLOAT和FLOAT(32位)的转换。这样所有解析逻辑都集中在一起,好维护。

下面是我常用的转换代码:

public class IEEE11073Utils {

    /**
     * 将两个字节的SFLOAT转换为float
     * @param data 字节数组,至少2字节
     * @param offset 起始偏移
     * @return 转换后的浮点值
     */
    public static float sFloatToFloat(byte[] data, int offset) {
        // 注意:蓝牙数据通常是小端字节序
        int sfloat = (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
        int mantissa = sfloat & 0x0FFF;  // 低12位是尾数
        int exponent = (sfloat >> 12) & 0x0F; // 高4位是指数(含符号位)
        
        // 处理符号位(第15位)
        if ((sfloat & 0x8000) != 0) {
            mantissa = mantissa - 4096; // 12位补码转负数
        }
        
        // 指数是4位补码,最高位是符号位
        if ((exponent & 0x08) != 0) {
            exponent = exponent - 16;
        }
        
        return (float) (mantissa * Math.pow(10, exponent));
    }
    
    /**
     * 将4个字节的FLOAT(32位)转换为float
     */
    public static float floatToFloat(byte[] data, int offset) {
        // 类似处理,32位版本,这里省略
        // 实际项目中我直接用了,原理一样
        return 0.0f;
    }
}

我曾经踩过的坑:字节序问题。有些设备用大端,有些用小端。我遇到过一款设备,文档写的是小端,实际发出来是大端。结果温度显示成了八千多度。排查了半天,最后发现是字节序反了。所以,拿到数据后,先打印原始字节,确认一下。

11.3 定时轮询 vs 通知模式

好,数据格式搞定了。接下来要考虑怎么获取数据。两种方式:轮询和通知。

你想想看,温湿度传感器这种数据,变化不快。温度可能几分钟才变0.1度。那你是每隔几秒去读一次,还是让设备主动告诉你?

咱们对比一下:

对比项 定时轮询 通知模式
实现复杂度 简单,定时器+读特征值 中等,需要开启通知+回调
功耗 高,每次读都需要交互 低,设备只在数据变化时发送
实时性 取决于轮询间隔 高,变化即通知
数据完整性 可能漏掉中间变化 不会漏,但可能丢包
适用场景 数据变化慢,对实时性要求不高 需要实时监控,或数据变化频繁

我个人习惯是:能用通知就用通知。为什么呢?

第一,省电。蓝牙设备通常电池容量小,轮询会让它频繁唤醒,电池撑不了多久。第二,实时性好。温度突然升高,通知能立刻告诉你,轮询可能要等好几秒。

但通知也有缺点。如果蓝牙连接不稳定,可能会丢包。我在一个仓库监控项目里遇到过,设备每隔5分钟上报一次温度,但有时候收不到。后来加了重试机制,才解决。

轮询也有它的用处。比如你只是想看一眼当前温度,不需要持续监控。那轮询一次就够了,简单粗暴。

下面是我实现通知模式的代码片段:

// 开启温度特征值的通知
BluetoothGattCharacteristic tempChar = 
    gattService.getCharacteristic(UUID.fromString("00002A6E-0000-1000-8000-00805F9B34FB"));
gatt.setCharacteristicNotification(tempChar, true);

// 同时需要配置CCC描述符(0x2902)
BluetoothGattDescriptor descriptor = tempChar.getDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);

// 在onCharacteristicChanged回调中接收数据
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, 
                                     BluetoothGattCharacteristic characteristic) {
    byte[] data = characteristic.getValue();
    float temperature = IEEE11073Utils.sFloatToFloat(data, 0);
    // 更新UI
}

小技巧:开启通知后,记得在onDescriptorWrite回调里确认是否成功。我曾经遇到过某些设备对CCC描述符写入有延迟,需要等待一下才能收到通知。

11.4 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图。它展示了从连接设备到解析数据的完整流程。

温湿度传感器数据获取与解析流程 连接BLE设备 发现环境传感服务 (0x181A) 获取温度(0x2A6E) / 湿度(0x2A6F) 方式一:定时轮询 方式二:通知模式 IEEE 11073 数据格式转换 显示温度/湿度值

从图上你可以看到,整个流程分三步:连接设备、发现服务、获取数据。获取数据又分两种方式,最终都要经过IEEE 11073转换,才能显示成我们看得懂的温度和湿度。

11.5 实战建议

最后,给你几个实战中的建议:

  • 先读文档:拿到设备后,先看厂商提供的GATT服务表。确认特征值UUID和数据格式。别像我一样,上来就猜,结果猜错了。
  • 打印原始数据:在解析之前,先把原始字节打印出来。用Log.d("BLE", bytesToHex(data))。这样能快速定位问题。
  • 处理异常值:IEEE 11073里有一些特殊值,比如0x07FF表示“无效值”,0x0800表示“超出范围”。解析时要判断,别把异常数据显示给用户。
  • 通知模式记得取消:页面销毁时,记得关闭通知。否则下次连接可能收不到数据。我遇到过这个问题,折腾了好久才发现是CCC描述符没重置。

一句话总结:环境传感服务(0x181A)是标准服务,数据用IEEE 11073 SFLOAT格式传输。推荐用通知模式获取数据,省电又实时。解析时注意字节序和异常值处理。

好了,这一章的内容就到这里。代码和思路都给你了,剩下的就是动手实践。找个温湿度传感器连上试试,你会发现,其实没那么难。

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