第4章 体温计规范解读:IEEE 11073-20601标准、Health Thermometer Profile、Temperature Measurement Characteristic
好,咱们进入正题。这一章要啃的,是体温计蓝牙通信的“宪法”——IEEE 11073-20601标准,以及它衍生出来的Health Thermometer Profile和Temperature Measurement Characteristic。说实话,我刚入行那会儿,看到这些标准文档头都大了,几百页的PDF,全是术语和状态机。但后来我发现,搞懂这三个东西,体温计的蓝牙开发就通了七成。
4.1 IEEE 11073-20601:医疗设备的“通用语言”
IEEE 11073-20601,名字很长,你记不住没关系。你只要知道,它是专门给医疗设备通信用的应用层协议。为什么要有它?因为蓝牙GATT虽然定义了数据怎么传,但没规定数据长什么样。比如一个体温值,A厂商用字节0x1A表示36.5℃,B厂商用0xE8表示,那手机App就乱套了。
11073-20601就是来解决这个问题的。它定义了医疗数据的“语法”和“语义”。说白了,它规定了一个体温数据包应该包含哪些字段,每个字段占几个字节,单位是什么,精度是多少。
核心要点:IEEE 11073-20601是医疗设备互操作性的基石。没有它,不同品牌的体温计和手机App就没法“对话”。
我个人习惯把11073-20601理解成一个“数据集装箱”。它把体温、时间戳、单位这些信息打包成一个标准化的结构体。蓝牙底层只负责把这个结构体完整地搬过去,至于里面装的是什么,由11073-20601来解释。
4.2 Health Thermometer Profile:体温计的“行为规范”
Profile,翻译过来叫“规范”或“配置文件”。Health Thermometer Profile(HTP)就是专门给体温计定的一套行为准则。它告诉你:体温计应该支持哪些服务(Service),应该包含哪些特征(Characteristic),以及这些特征之间怎么配合工作。
HTP的核心服务是Health Thermometer Service(服务UUID: 0x1809)。这个服务里必须包含一个Temperature Measurement特征(UUID: 0x2A1C),用来上报体温数据。另外,还可以选配Temperature Type特征(UUID: 0x2A1D),用来指示测量部位(比如口腔、腋下、直肠)。
| 特征名称 | UUID | 必选/可选 | 说明 |
|---|---|---|---|
| Temperature Measurement | 0x2A1C | 必选 | 上报体温测量值,包含时间戳、单位等信息 |
| Temperature Type | 0x2A1D | 可选 | 指示测量部位,如口腔(1)、腋下(2)、直肠(3) |
| Measurement Interval | 0x2A21 | 可选 | 连续测量时的间隔时间 |
嗯,这里要注意。虽然Temperature Type是可选的,但我建议你加上。为什么?我在项目中遇到过,有些手机App会通过这个特征来判断测量部位,如果没有,App可能默认显示“未知部位”,用户体验不好。
4.3 Temperature Measurement Characteristic:数据包的“解剖”
这是最核心的部分。Temperature Measurement特征的数据格式,完全遵循IEEE 11073-20601的规范。我们把它拆开来看。
一个完整的体温数据包,结构如下:
字节0: 标志位 (Flags)
字节1-4: 体温值 (IEEE 11073-20601 32位浮点数格式)
字节5-7: 时间戳 (可选,根据标志位决定)
字节8: 测量部位 (可选,根据标志位决定)
咱们重点看标志位。它决定了后面跟着哪些字段:
- Bit 0:体温值单位。0表示摄氏度(℃),1表示华氏度(℉)。
- Bit 1:时间戳标志。1表示后面跟着时间戳字段。
- Bit 2:测量部位标志。1表示后面跟着测量部位字段。
- Bit 3-7:保留位,必须为0。
举个例子。假设标志位是0x06(二进制00000110),表示:单位是摄氏度(Bit0=0),有时间戳(Bit1=1),有测量部位(Bit2=1)。
接下来是体温值。它采用IEEE 11073-20601定义的32位浮点数格式,不是标准的IEEE 754浮点数。这个格式有点特殊,我简单说一下:
- 它用4个字节表示一个浮点数。
- 前2个字节是整数部分(16位有符号整数)。
- 后2个字节是小数部分(16位无符号整数,表示百万分之一)。
比如体温36.5℃,整数部分是36(0x0024),小数部分是500000(0x07A120)。组合起来就是0x0024 0x07A120。你想想看,这种格式的好处是什么?避免了浮点数精度丢失的问题。
小技巧:在固件里,我一般直接用整数运算来拼这个数据包。比如体温值乘以1000000,然后拆成高低两部分。这样比调用浮点库快得多,也省ROM。
4.4 实战:组装一个体温数据包
光说不练假把式。咱们写一段伪代码,演示如何组装一个36.5℃的体温数据包,带时间戳和测量部位(口腔)。
// 假设当前体温是36.5℃,测量部位是口腔(1)
uint8_t flags = 0x06; // 单位℃, 有时间戳, 有部位
// 体温值: 36.5℃
// 整数部分: 36
// 小数部分: 500000 (0.5 * 1000000)
uint16_t int_part = 36;
uint16_t frac_part = 500000;
// 时间戳: 假设是2024年1月15日 10:30:00
// 格式: 年(2字节), 月(1字节), 日(1字节), 时(1字节), 分(1字节), 秒(1字节)
uint8_t timestamp[6] = {0x07, 0xE8, 0x01, 0x0F, 0x0A, 0x1E, 0x00};
// 注意: 年份是相对于1900年的偏移, 2024-1900=124, 即0x007C
// 测量部位: 口腔 = 1
uint8_t body_site = 0x01;
// 组装数据包
uint8_t packet[11];
packet[0] = flags;
packet[1] = (int_part >> 8) & 0xFF; // 整数高字节
packet[2] = int_part & 0xFF; // 整数低字节
packet[3] = (frac_part >> 8) & 0xFF; // 小数高字节
packet[4] = frac_part & 0xFF; // 小数低字节
// 注意: 这里简化了, 实际小数部分需要拆成2字节
// 时间戳6字节
memcpy(&packet[5], timestamp, 6);
// 测量部位
packet[11] = body_site;
避坑指南:我曾经犯过一个低级错误——时间戳的年份字段忘了加1900偏移。结果手机App显示体温是3924年测量的,用户直接投诉说“你们体温计穿越了”。从那以后,我每次写时间戳都会反复确认基准年份。
4.5 与蓝牙GATT的映射关系
最后,咱们看看这些规范怎么落地到蓝牙协议栈里。在蓝牙GATT层面,Health Thermometer Service就是一个Service,Temperature Measurement就是它下面的一个Characteristic。
这个Characteristic的属性通常是Notify(通知),也就是说体温计主动把数据推送给手机。当然,也可以设置为Read(读取),让手机主动来读。但我建议用Notify,因为体温数据是实时变化的,Notify效率更高。
嗯,这里有个细节。Notify模式下,手机需要先写CCCD(Client Characteristic Configuration Descriptor)来使能通知。如果手机没使能,体温计发了数据它也收不到。我在调试时经常遇到这个问题——数据发了,手机没反应,一查发现CCCD没配。
好了,这一章的内容就到这里。总结一下:IEEE 11073-20601定义了数据格式,Health Thermometer Profile定义了服务结构,Temperature Measurement Characteristic是具体的数据载体。三者缺一不可。下一章,咱们开始动手写代码,把理论变成实际能跑的固件。