ThingsBoard TBEL 二进制载荷解码实战:从 LoRaWAN HEX 报文到设备遥测的完整转换指南
ThingsBoard TBEL 二进制载荷解码实战:从 LoRaWAN HEX 报文到设备遥测的完整转换指南
导读
在 ThingsBoard 的集成(Integration)与数据转换(Converter)体系中,解码函数是承接"设备上报原始字节流"与"平台统一数据模型"之间的关键桥梁。本文以仓库内置的 simple-binary 示例 为核心骨架,完整拆解一条真实 LoRaWAN 上报报文 01ed03335f0e4c63 的二进制解析过程:包括 HEX/Base64 载荷格式、逐字节位域划分、TBEL 解码函数实现、Decoder 输出与 Converter 输出的差异,以及 parseBytesToInt 移位拼接原理。读完本文,你将能够独立编写适用于自有二进制协议的解码函数,并理解平台对解码结果的结构化校验要求。
一、示例背景:一条真实的 LoRaWAN 上行报文
示例数据来源是一个典型的 LoRaWAN(LORIOT 网络服务器)传感器上报场景。设备每次上报携带以下业务字段:
- 设备序列号(4 字节)
- 电池电量(1 字节)
- 温度(2 字节,放大 100 倍)
- 血氧饱和度(1 字节)
1.1 HEX 表示
原始载荷的十六进制表示为(见 payload.md):
01ed03335f0e4c63
该字符串共 8 字节(16 个十六进制字符),按位域划分如下:
| 位域 | 字节范围 | 字段含义 | 解析结果 |
|---|---|---|---|
01ed0333 |
0–3(4 字节) | 设备序列号 | 十进制 32310067 |
5F |
4(1 字节) | 电池电量(%) | 十进制 95 |
0E4C |
5–6(2 字节) | 温度值 × 100 | 十进制 3660,即 36.60 ℃ |
63 |
7(1 字节) | 血氧饱和度(%) | 十进制 99 |
编码约定:多字节字段采用大端序(Big-Endian),高位字节在前。例如序列号
01 ed 03 33→0x01ED0333 = 32310067;温度0E 4C→0x0E4C = 3660,除以 100 得到36.6。该约定必须与设备固件端的打包方式保持一致。
1.2 Base64 表示
当在集成或转换器中开启调试(Debug)时,调试事件中可见的载荷是 Base64 编码形式:
Ae0DM18OTGM=
HEX 01ed03335f0e4c63 与 Base64 Ae0DM18OTGM= 是同一字节序列的两种表示:前者便于人工阅读与协议文档交流,后者是 ThingsBoard 调试事件与消息队列中实际传输的默认编码形态(解码函数中的 payload 参数默认即 Base64 解码后的字节数组)。
二、TBEL 解码函数:逐字节解析实现
仓库为 decoder_v2(新版 TBEL 转换器)提供了完整的解码函数示例,见 decoder_fn.md:
function decodePayload(input) {
var result = { attributes: {}, telemetry: {}};
result.attributes.sn = parseBytesToInt(input, 0, 4);
var timestamp = metadata.ts;
var values = {};
values.battery = parseBytesToInt(input, 4, 1);
values.temperature = parseBytesToInt(input, 5, 2) / 100.0;
values.saturation = parseBytesToInt(input, 7, 1);
result.telemetry = {
ts: timestamp,
values: values
};
return result;
}
var result = decodePayload(payload);
return result;
function parseBytesToInt(input, offset, length) {
var result = 0;
for (var i = offset; i < offset + length; i++) {
result = (result << 8) | (input[i] & 0xFF);
}
return result;
}
2.1 核心工具函数 parseBytesToInt
function parseBytesToInt(input, offset, length) {
var result = 0;
for (var i = offset; i < offset + length; i++) {
result = (result << 8) | (input[i] & 0xFF);
}
return result;
}
其原理是标准的"移位 + 按位或"大端拼接:
result << 8:把已累积的字节整体左移 8 位,为下一个字节腾出低位空间;input[i] & 0xFF:屏蔽符号位,确保字节被当作无符号 0–255 处理(JS 字节数组元素可能以有符号数返回);|:将当前字节拼入低位。
例如解析 0E 4C 两字节:
- 初始
result = 0;读入0x0E→(0 << 8) | 0x0E = 14; - 读入
0x4C→(14 << 8) | 0x4C = 0x0E4C = 3660。
该函数高度可复用:只需调整 offset 与 length 即可适配任意固定长度字段。若协议使用小端序,可将循环改为反向遍历(input[offset + length - 1 - i])即可。
2.2 字段解析对照
| 调用 | 参数 | 含义 | 解析值 |
|---|---|---|---|
parseBytesToInt(input, 0, 4) |
偏移 0,长度 4 | 设备序列号 | 32310067 |
parseBytesToInt(input, 4, 1) |
偏移 4,长度 1 | 电池电量 | 95 |
parseBytesToInt(input, 5, 2) / 100.0 |
偏移 5,长度 2 | 温度 ÷ 100 | 36.6 |
parseBytesToInt(input, 7, 1) |
偏移 7,长度 1 | 血氧饱和度 | 99 |
温度除以 100 是典型的定点数还原:设备端为节省带宽将浮点温度放大 100 倍为整数传输,解码端再还原。
2.3 时间戳来源
var timestamp = metadata.ts;
时间戳直接取自集成消息的元数据 metadata.ts(示例中为 1684478801936,毫秒级 Unix 时间戳,见 metadata.md)。这意味着遥测点的 ts 使用网关/网络服务器上报的时间,而非解码执行时刻。
三、解码输出(Decoder Output)
解码函数直接返回的结构即为 Decoder Output,见 decoder_output.md:
{
"attributes": {
"sn": 32310067
},
"telemetry": {
"ts": 1684478801936,
"values": {
"battery": 95,
"temperature": 36.6,
"saturation": 99
}
}
}
根据 decoder_fn_v2.md 中定义的解码函数契约,Decoder Output 必须满足:
attributes(必填):至少包含一个键值对,用于描述设备/资产属性。本例将sn序列号放入属性,避免其进入遥测历史序列;telemetry(必填):对象或数组,至少包含一条时序数据点。本例为单条带ts的记录;name(可选):用于在租户范围内定位或创建设备/资产,常使用 eui、MAC 等硬件唯一标识;type(可选):Asset或Device;profile(可选):未指定时默认使用default;customer/group/label(可选):仅在集成首次创建设备/资产时生效。
更完整的 Decoder Output 形态(含 label、customer、group 及遥测数组)参见 extended_decoder_output.md。
四、转换器输出(Converter Output):预配置与动态结果的融合
Converter Output 是"转换器预配置 + 解码函数结果"合并后的最终 JSON,见 converter_output.md:
{
"entityType": "DEVICE",
"name": "Device 1000000000000001",
"profile": "default",
"telemetry": {
"ts": 1684478801936,
"values": {
"battery": 95,
"temperature": 36.6,
"saturation": 99,
"rssi": -21,
"data": "01ed03335f0e4c63",
"snr": 10,
"fСnt": 2
}
},
"attributes": {
"sn": 32310067,
"fPort": 85,
"dr": "SF9 BW125 4/5",
"frequency": 867500000,
"eui": 1000000000000001
}
}
4.1 两类输出的区别
| 维度 | Decoder Output | Converter Output |
|---|---|---|
| 来源 | 解码函数直接返回 | 解码函数结果 + 转换器预配置 |
| 实体信息 | 函数内部自定(可选) | 由预配置的 entityType/name/profile 等决定 |
| 覆盖规则 | — | 解码函数可覆盖预配置的键值 |
预配置定义了默认键值,解码函数若返回同名键则可覆盖,从而实现"静态模板 + 动态数据"的灵活融合。
4.2 网络服务器元数据注入
对比 Decoder 与 Converter 输出可见,Converter 输出中的 rssi、snr、fCnt、fPort、dr、frequency、eui 等字段来自网络服务器元数据(参见 metadata.md 中的 rssi=-21、snr=10、fCnt=2、fPort=85、dr=SF9 BW125 4/5、frequency=867500000、eui=1000000000000001)。这些 LoRa 射频链路质量指标被直接透传为设备遥测/属性,是典型"信号质量监测"应用的基础。
此外 entityType/name/profile 来自转换器预配置,而 attributes.sn 则被解码函数覆盖为解析出的 32310067——这正体现了第 4.1 节描述的覆盖机制。
五、端到端数据流梳理
结合 decoder_fn_v2.md 对解码函数签名的定义:
function payloadDecoder(payload, metadata): object | object[]
payload:集成消息携带的编码数据字节数组,默认以 Base64 编码传输(调试事件中可见);metadata:集成消息的键值元数据,可在集成详情中追加自定义项;
整个处理链为:
- 网络服务器(如 LORIOT/ChirpStack)上报 HEX 载荷
01ed03335f0e4c63及元数据; - 集成以 Base64 字节数组形式将其交给解码函数;
- TBEL 解码函数按位域解析出
sn/battery/temperature/saturation,生成 Decoder Output; - 平台将解码结果与转换器预配置合并,生成 Converter Output;
- 平台据此创建/定位设备,写入属性(
sn)与遥测(电量、温度、饱和度等),并附带 LoRa 链路指标。
六、实战要点与扩展建议
- 位域布局必须与固件一致:大端/小端、字节序、放大系数(如温度 ×100)均需与设备端协议文档核对,否则解析结果会错位。
- 复用
parseBytesToInt:对固定长度字段,只需调整offset/length;对小端协议调整循环方向即可。 - 区分"该进属性还是遥测":序列号、eui 等静态标识放入
attributes;电量、温度等随时间变化的值放入telemetry。 - 善用时间戳:
metadata.ts保留网关上报时间,也可在解码函数内使用Date.now()替换。 - 开启调试验证:在集成或转换器开启 Debug 后,可在调试事件中查看 Base64 载荷与最终输出,快速定位解析错误。
- 输入校验:可先判断
input.length是否达到 8 字节再解析,避免收到异常短报文时产生错误数据。
仓库内的完整配套示例(含 payload、decoder、两种输出)位于 decoder_v2/simple-binary,JSON 变体示例见 decoder_v2/simple-json,解码函数通用契约详见 decoder_fn_v2.md,可作为继续深入的阅读路径。