最近在折腾 ThingsBoard 设备接入时,我遇到了一个特别基础但特别容易卡壳的问题:设备明明已经连上 MQTT 了,消息也发出去了,可在 ThingsBoard 页面上就是看不到任何数据。排查了半天才发现,我是把“属性数据”和“遥测数据”的上报 topic 混为一谈,属性内容发到了telemetry,自然在属性页里什么都刷不出来。后来把 topic 切换成v1/devices/me/attributes,数据立马出来了,整个过程就像打开了一扇门。
这篇内容就把 ThingsBoard 通过 MQTT 发送属性数据的完整链路讲透:从 MQTT topic 怎么定、payload 怎么组织,到用命令行和 Python 脚本快速上报,再到平台侧怎么查看、怎么用 REST API 读取,以及最容易踩的几个坑。无论你是用 ESP32、树莓派、STM32,还是只在电脑上跑一个模拟器,只要你需要往 ThingsBoard 上报设备型号、固件版本、运行模式、配置参数这类“属性信息”,这篇文章都能直接照着做。
1. 为什么说属性数据是设备接入的第一道门槛
1.1 属性数据和遥测数据到底有什么区别
ThingsBoard 的数据模型里,有两类最常见的的数据,一类叫 Telemetry(遥测),一类叫 Attributes(属性)。很多人第一次接触时都会混淆,包括我自己也犯过这个错误。简单来说,遥测数据是“随着时间不断变化”的采样值,比如温度、湿度、电压、经纬度,它天生就是一条时间序列,平台会把它存成带时间戳的历史记录,并在“最新遥测”页面展示曲线。而属性数据是“描述设备当前状态或配置”的键值对,比如设备序列号、固件版本、安装位置、运行模式、报警阈值,它更像是一张设备档案卡片,存的是最新的“结果”,而不是一段“过程”。
| 对比项 | Telemetry 遥测数据 | Attributes 属性数据 |
|---|---|---|
| 上报 topic | v1/devices/me/telemetry | v1/devices/me/attributes |
| 数据形态 | 时间序列,可带 ts 时间戳 | 键值对 JSON 对象 |
| 存储方式 | 追加存储,保留历史 | 覆盖存储,只保留最新值 |
| 典型用途 | 温度、电量、信号强度、位置轨迹 | 固件版本、序列号、配置项、状态开关 |
| 页面位置 | 设备详情 -> 最新遥测 | 设备详情 -> 属性 |
如果你把属性数据发到 telemetry topic,平台会正常接收,但只会把它当成普通时间序列处理,不会出现在“属性”页面里。反之,把遥测数据发到 attributes topic,平台只保留最后一次上报值,历史曲线就丢了。所以搞清楚“你现在到底要传什么”,是接入 ThingsBoard 的第一道门槛。
从业务角度再打个比方:遥测数据像人的“体温、心率”,需要持续记录变化;属性数据像人的“姓名、工号、部门”,是相对固定的档案。一套设备接入流程里,通常先上报属性让平台认识设备,再持续上报遥测数据做监控。你不能把档案当成体检报告,也不能把体检报告当成档案存。
1.2 MQTT 在 ThingsBoard 接入链路中扮演什么角色
ThingsBoard 本身不是一个 MQTT Broker,但它内置了 MQTT Transport 模块,对外暴露标准的 MQTT 端口,默认 1883,启用 TLS 的话是 8883。设备可以把 MQTT 消息直接发到 ThingsBoard,也可以先发到自己的 EMQX、Mosquitto 这类 Broker,再通过规则链转发给 ThingsBoard。对于大多数中小型项目来说,直接用平台内置的 MQTT 接入就够了,省去一层消息中间件,架构也简单很多。
MQTT 协议本身是一种轻量级发布/订阅协议,特别适合资源受限的设备端。它基于 topic 做消息路由,支持 QoS 0/1/2 三种投递级别,连接占用带宽很小,非常适合弱网环境。在 ThingsBoard 里,topic 不只是消息分类的标签,还是 API 的“路由地址”。你在哪个 topic 上发什么内容,直接决定了平台会把它归为遥测、属性,还是 RPC 响应。这也是为什么我强调“topic 错了,数据就进错门”。
很多初学者喜欢先去搭一个“自己的 MQTT 服务器”,再把 ThingsBoard 接上去,这其实绕了远路。正确思路是:ThingsBoard 已经帮你把 MQTT 服务端和业务解析层打通了,你只需要用任何一款 MQTT 客户端,把自己伪装成一台设备,连接到 ThingsBoard 的 1883 端口,然后按它规定的 topic 格式上报数据即可。消息到了平台后,会被自动解析成设备数据并落库,不需要你再写任何解析逻辑。
2. 发送属性数据前,必须搞清楚的三个关键点
2.1 设备凭证和 Access Token 的正确用法
在 ThingsBoard 里,一台设备要接入平台,必须先通过认证。最常见的认证方式是 Access Token,你可以把它理解成设备的“身份证号”。在设备列表中创建一个设备后,进入设备详情页,点击“管理凭证”,就能看到一串唯一的访问令牌。这串 token 在整个 MQTT 连接过程中起着决定性作用。
使用标准 MQTT 客户端连接 ThingsBoard 时,认证机制和普通 MQTT Broker 有点不一样。大多数情况下,ThingsBoard 要求把 Access Token 填到 Client ID 字段,用户名和密码可以留空,也可以任意填。也就是说,MQTT 客户端连接的client_id必须是设备 token,而不是你自己随便起的“esp32-client”。如果你用mosquitto_pub,对应参数就是-i;如果用 Python 的 paho-mqtt,就是Client(client_id=...);如果用 ESP32 的 PubSubClient,就是setClient里的 client id。
这里有个容易忽略的细节:如果设备被禁用,或者 token 被重置,即使 Client ID 填对了,服务端也会拒绝连接或者不让消息通过。遇到NOT_AUTHORIZED或bad user name or password时,先不要怀疑网络,而是去设备详情里重新复制一遍 token,检查有没有多复制空格或换行。另外,如果同一个 token 被多个连接同时使用,后一个连接可能把前一个踢下线,导致消息一会儿能发一会儿不能发,排查时要留意是不是有多个进程或者多个工具在同时竞争同一个凭证。
2.2 属性上报的 MQTT 主题和 Payload 格式
发送属性数据在 ThingsBoard 里对应的 topic 是v1/devices/me/attributes。在这个 topic 上发布一条 JSON 对象,平台就会把里面的每个键值对作为属性保存下来。常见的 payload 格式看起来是这样的:
{"deviceName":"air-conditioner-01","firmwareVersion":"2.0.1","signal":-65,"settings":{"mode":"cool","fanSpeed":3}}发送时注意三件事。第一,payload 必须是合法的 JSON 对象,最外层一定是{和},不能是数组,也不能是裸字符串。属性上报不支持类似[{"ts":..., "values":{...}}]这种遥测批量格式,如果你把一个数组发到这个 topic,平台大概率会直接丢弃或者返回错误。第二,字符串值必须用双引号,不能用单引号。在命令行里实测的时候,很多人会被 shell 转义坑到,我后面会专门写。第三,属性值可以是嵌套 JSON,比如上面例子里的settings是一个子对象,平台会把它当成一个独立的属性值存起来,读取时拿到的就是整体对象。
要特别注意的是,属性上报没有时间戳字段,平台只保存最新上报的键值对。同一条属性如果被重复上报,新值会覆盖旧值,并且更新时间会刷新。如果你需要保留属性变化的历史记录,就必须自己在规则链里做配置,比如把属性变化写入遥测,或者在平台外部单独存一份日志。这个“只留最新值”的特性,也是属性数据和遥测数据最本质的区别。
| 功能 | topic | payload 要求 |
|---|---|---|
| 客户端属性上报 | v1/devices/me/attributes | JSON 对象,键值对形式 |
| 遥测数据上报 | v1/devices/me/telemetry | JSON 对象或数组,可带时间戳 |
| RPC 响应 | v1/devices/me/rpc/response/{requestId} | JSON 对象,响应具体命令 |
| 共享属性订阅 | v1/devices/me/attributes | 订阅后接收平台下发内容 |
2.3 属性上报用 QoS 0 还是 QoS 1
MQTT 有 QoS 0、1、2 三种投递级别,分别对应“最多一次”“至少一次”“仅一次”。ThingsBoard 的 MQTT Transport 是支持 QoS 0 和 QoS 1 的,但我不建议在属性上报时使用 QoS 2,因为属性数据本身是“只保留最新值”的逻辑,为了它付出两次确认的带宽和延迟成本并不划算。
那到底选哪个?我的经验是:属性数据通常包含设备身份、固件版本、关键配置等“必须别丢”的信息,所以优先用 QoS 1。QoS 1 保证消息至少送达一次,即使网络临时抖动,客户端会在重连后把未确认的报文重新投递,这对关键属性来说相当重要。如果是一天上报一次配置、上线时上报一次固件版本这种低频场景,直接 QoS 1 就够了。
反过来,遥测数据如果上报频率很高,比如每秒一条温度,用 QoS 0 会更轻盈,因为丢掉一秒的数据可能不影响整体趋势。属性数据不是这样的,它本身就是低频高价值的信息,丢了可能就要等设备下次重启才能再拿到。我之前调试一台设备时,因为图省事把所有消息都发成 QoS 0,结果网络抽风正好把固件版本上报这条消息丢了,平台侧一直显示旧版本,排查了很久才意识到是 QoS 级别太低导致消息没送达。从那以后,属性上报我统一用 QoS 1,宁可多一点网络开销,也要让设备“身份信息”可靠到达。
3. 手把手实现:用命令行与脚本发送属性数据
3.1 最快验证:用 mosquitto_pub 一条命令发布属性
如果你只是想快速验证 ThingsBoard 的属性上报链路是否通,不要急着写代码,先用命令行工具跑通再说。推荐安装mosquitto-clients工具,里面包含mosquitto_pub和mosquitto_sub。在 Ubuntu/Debian 上执行sudo apt install mosquitto-clients,macOS 上执行brew install mosquitto,Windows 用户可以用安装包或 WSL。
假设 ThingsBoard 服务地址是localhost,端口是默认的 1883,设备 token 是abcd1234,那么发布一条属性数据的命令如下:
mosquitto_pub -d -q 1 -h localhost -p 1883 -i "abcd1234" -t "v1/devices/me/attributes" -m '{"firmwareVersion":"2.0.1","active":true}'这里-d打开调试日志,你会看到完整的 MQTT 交互报文,包括连接确认、PUBLISH 报文等;-q 1指定 QoS 级别;-h和-p指定服务地址与端口;-i "abcd1234"是关键,必须把设备的 Access Token 作为 Client ID;-t是属性上报 topic;-m是 payload 内容。
执行之后,打开 ThingsBoard 设备详情页,切到“属性”页签,如果能看到firmwareVersion=2.0.1和active=true两个键值,说明整个链路已经通了。如果你的 ThingsBoard 不在本机,把localhost换成服务器 IP 或域名即可。这里最常见的错误是-m里的 JSON 用了双引号包整体,然后里面属性名也想用双引号,结果被 shell 解构了;所以我在命令里特意用了单引号包整体,属性名双引号保内层,这个习惯能从根源上避开转义问题。
3.2 用 Python + paho-mqtt 编写属性上报脚本
命令行工具适合验证,但真正接入设备或做模拟测试时,还是要写脚本。Python 生态里最常用的 MQTT 客户端库是paho-mqtt,安装只需pip install paho-mqtt。下面这个脚本是属性上报的最小可用版本,我在多个版本上实测过,直接改 token 和设备信息就能跑起来。
import json import time import paho.mqtt.client as mqtt BROKER = "localhost" PORT = 1883 DEVICE_TOKEN = "abcd1234" client = mqtt.Client(client_id=DEVICE_TOKEN, protocol=mqtt.MQTTv311) client.connect(BROKER, PORT, 60) payload = { "deviceName": "air-conditioner-01", "firmware": "2.0.1", "signal": -65, "settings": {"mode": "cool", "fanSpeed": 3} } client.publish("v1/devices/me/attributes", json.dumps(payload), qos=1) time.sleep(1) client.disconnect()脚本的思路很简单:创建客户端时把client_id设为设备 token,连接 ThingsBoard,然后把一个字典用json.dumps序列化成字符串,发布到属性上报 topic。发布之后sleep(1)是为了给底层网络一点时间把消息推送出去,否则立刻disconnect()可能导致数据没发完就断开了。
如果你的 ThingsBoard 版本比较老,或者连接时遇到认证问题,可以在connect之前加一行client.username_pw_set(DEVICE_TOKEN, ""),把 token 同时填到用户名里,密码留空。这种兼容性写法在对接不同 MQTT Transport 版本时很有用。跑完脚本后,再到“属性”页签刷新,看到的数据应该和代码里定义的键值完全一致。如果你习惯用 MQTTX 这类图形化工具,思路一模一样:新建连接时 Client ID 填 token,topic 填v1/devices/me/attributes,payload 填 JSON,点发送即可。
3.3 从设备端上报到平台侧校验的完整链路
在实际项目里,我们不只在电脑上模拟,还要让真实设备上报属性。不管你是用 ESP32、树莓派还是 STM32 加 4G 模块,核心逻辑都是一样的:调用 MQTT 库,用 token 作为 Client ID 连接 ThingsBroker,然后 publish 到v1/devices/me/attributes。比如 ESP32 上的 PubSubClient 代码片段大概是这样:
client.setServer("your-thingsboard-ip", 1883); client.connect("abcd1234"); char payload[] = "{\"firmwareVersion\":\"2.0.1\"}"; client.publish("v1/devices/me/attributes", payload);设备端上报后,建议经过这样一条“自检链路”来确认数据真的被平台收到:第一步,检查设备端 MQTT 日志里有没有PUBLISH成功回执,重点是看有没有出现qos1的PUBACK。第二步,到 ThingsBoard 设备详情页的“属性”页签,确认键值已经出现。第三步,如果你在平台侧配置了规则链,再看消息是否进入了规则引擎。第四步,通过 REST API 拉取属性值,与设备端上报内容做一次一致性核对。很多问题都出在第二步和第四步之间,比如数据明明在页面上看到了,但规则链没触发,或者外部系统读不到,这时候你就要往规则链和 API 权限上排查。
一个很容易被忽略的细节是:设备上报属性后,如果你在同一个连接里紧接着上报遥测,这两个请求是相互独立的,不要把它们写进同一条消息里。属性归属性,遥测归遥测,topic 不同,解析路径也不同。把这条链路走通之后,你再去看 ThingsBoard 的 RPC 下发命令、共享属性更新,会发现它们都是同一套 MQTT 通道上的不同主题而已,思路完全可以复用。
4. 平台侧如何查收和二次使用这些属性数据
4.1 在设备详情页面定位属性数据
很多用户上报成功后,去页面找半天找不到数据,不是因为传输失败,而是没找对位置。ThingsBoard 设备详情页默认展示的是“最新遥测”页签,里面只能看到带时间戳的遥测值。你要切到“属性”页签才能看到客户端上报的属性。在“属性”页签里通常还能切换client、shared、server三种作用域,设备自己上报的属性属于client作用域,平台主动配置下发的属于shared,服务端内部记录的是server。
如果你上报的数据在“属性”页签下没有立刻刷新,可以手动刷新一下浏览器,或者把“属性”页签关掉重新打开。还是看不到的话,再用 REST API 查一次,确认是不是 UI 缓存问题。另外,属性页签里的键值可以是字符串、数字、布尔值或嵌套对象,页面会根据 JSON 类型自动展示,但不一定会做漂亮的图表,因为它本身就不是为曲线图设计的。如果你需要把属性变化画成图表,就得额外配置规则链把它落到遥测存储里。
这里我再分享一个工作习惯:给设备做属性上报时,尽量用一套固定的命名规范,比如fwVersion、serialNumber、hwModel,避免同一个设备一会儿上报firmwareVersion,一会儿上报firmware_version,导致平台侧属性键混乱。属性键一旦脏了,后期做规则链筛选和数据治理会非常痛苦。
4.2 通过 REST API 读取客户端属性值
在页面查看很方便,但自动化运维或集成场景下,我们更希望用 REST API 直接拉取属性。ThingsBoard 提供了GET /api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}这个接口。其中entityType一般是DEVICE,entityId是设备在平台里的 UUID,不是设备名称,scope填CLIENT_SCOPE、SHARED_SCOPE或SERVER_SCOPE。
调用这个接口时必须带 Authorization 请求头。最方便的方式是先用账号密码调用登录接口拿到 JWT token,再带着 token 去查属性。用一个简单的 curl 示例说明:
# 1. 登录获取 token curl -X POST -H "Content-Type: application/json" \ -d '{"username":"tenant@thingsboard.org","password":"your-password"}' \ http://localhost:8080/api/auth/login # 2. 用 token 查询设备属性,把 {deviceId} 替换成设备 UUID curl -X GET \ -H "X-Authorization: Bearer YOUR_JWT_TOKEN" \ http://localhost:8080/api/plugins/telemetry/DEVICE/{deviceId}/values/attributes/CLIENT_SCOPE返回结果是一组包含 key、value、lastUpdateTs 的数组,比如[{"key":"firmwareVersion","value":"2.0.1","lastUpdateTs":1710000000000}]。这个接口很适合对接外部系统,比如设备资产管理系统可以定时同步设备固件版本,运维平台可以拉取所有设备的配置项。另一个常用接口是GET /api/plugins/telemetry/{entityType}/{entityId}/values/timeseries,这个是查遥测数据的,不要混了。
需要注意的是,用 REST API 读取属性时,token 的生命周期有限,过期后需要重新登录获取。如果你在脚本里定时拉取,建议先做一次403判断,发现 token 失效就自动重新登录。否则本地缓存着一把过期 token,会白白多出很多 401 请求。
4.3 属性变化事件与规则链联动
属性数据不仅仅是“存起来看看”,它更重要的作用是驱动规则链。每次设备上报属性,ThingsBoard 会生成一条类型为POST_ATTRIBUTES的消息进入规则引擎。你可以利用这条消息做很多事情,比如判断设备是否在线、检查固件版本是否需要升级、把属性变化同步到另一个实体、或者触发告警。
我举一个实际用过的场景:设备启动后会上报一条属性{"online":true,"ip":"192.168.1.100"}。我在规则链里加了一个“消息类型筛选器”,筛选POST_ATTRIBUTES,接着用“脚本”节点判断消息里的online是否为true,如果为真,就把设备的另一个属性lastSeenAt更新成当前时间。这样一来,即使设备不上报遥测,平台也能通过属性变化知道设备最后一次活跃时间,后续做离线判定就方便多了。
属性变化和规则链联动还有一个常见用途:配置漂移检测。假设设备当前上报的fanSpeed是 2,但平台希望所有设备都运行在 3 档,你可以在规则链里做一个比较,发现不一致时自动下发新的共享属性给设备,或者生成一条告警让运维介入。这种“设备上报属性 -> 规则链判断 -> 平台下发新配置”的闭环,是 ThingsBoard 最典型的自动化场景之一。要强调的是,属性上报频率不能太高,否则规则引擎压力和被触发的动作都会成倍增加,后面我会专门说频率控制的问题。
5. 常见问题与排查技巧实录
5.1 数据发了,但页面就是没有任何变化
这个问题出现频率最高,原因也最多。首先检查 topic 是不是v1/devices/me/attributes,很多同学会把属性数据发到v1/devices/me/telemetry,结果页面“最新遥测”有数据,但“属性”页签一片空白,看起来像“数据没发出去”,其实是数据进错了门。其次检查设备 token 有没有设置成 Client ID,如果连接时随意写了一个 client id,ThingsBoard 会直接拒绝连接,或者连接后无法通过设备认证,消息自然上不去。
还有一种情况是消息已经发成功了,但你看的是设备组层面的聚合页面,而不是设备详情页。属性数据是挂在具体设备下面的,你得先进入设备详情,再切到“属性”页签。如果你在浏览器里开了开发者工具,可以顺手看一下设备详情页发起的 API 请求,正常情况下会请求values/attributes/CLIENT_SCOPE,响应里应该能看到刚刚上报告的数据。如果 API 响应没有,那就是服务端没收到,重点回到 MQTT 连接和 topic 上排查。
5.2 JSON 格式和特殊字符引起的诡异问题
命令行里发属性时,shell 解析规则经常会让人抓狂。比如我在 Windows 的 CMD 和 PowerShell 下用 mosquitto_pub,单双引号的处理方式和 Linux 完全不一样,稍不注意 JSON 就被拆成了多段,平台收到的根本不是合法 JSON。最简单的办法是:在 Linux 上用单引号包整个 JSON,在 CMD 下用双引号包整个 JSON,然后内部的属性名和字符串值用反斜杠转义。如果你觉得转义麻烦,干脆把 JSON 内容写进一个文件,用mosquitto_pub -f payload.json指定文件,这样最不容易出错。
除了转义问题,属性值类型也容易出错。比如{"active": true}会被解析成布尔值,{"active": "true"}会被解析成字符串,二者在规则链里判断时的写法完全不同。如果你在脚本里用 Python 发送,json.dumps会自动处理好类型,但如果你在调试工具里手写 JSON,务必检查数字、布尔值不要加引号,字符串必须加双引号。另外,如果你上报的属性值里包含中文,最好确保 MQTT 客户端使用 UTF-8 编码,否则平台侧可能显示乱码。
5.3 属性上报频率与平台存储压力怎么平衡
属性数据虽然“只保留最新值”,但每次上报都会触发网络传输、数据解析、规则链处理,如果频率控制不好,照样会对平台产生压力。有些开发者误以为属性上报和遥测一样可以每秒一次,结果几千台设备同时高频上报属性,直接把规则引擎和数据库的连接池打满。属性数据的本质是“低频、高价值”,正确的上报策略是“变化时上报”,比如设备启动时上报一次、配置变更时上报一次、状态切换时上报一次,而不是定时每秒上报。
实际项目中,我通常会给属性上报加一个“变化判断”的过滤器,只有当前值和上次值不同才真正发布消息。这样既能保证平台侧属性始终是最新值,又能大幅减少无效消息。如果你确实需要高频记录某些状态,那应该走遥测通道,而不是属性通道。记住一个判断口诀:需要画历史曲线、需要保留过程的,走 telemetry;只需要知道最新状态、需要被配置系统读取的,走 attributes。二者配合使用,才能让 ThingsBoard 在数据量上来之后依然保持流畅。
我个人的习惯是,刚开始接入 ThingsBoard 时不要急着写大段代码,先用 mosquitto_pub 手动把一条属性刷上去,页面确认能看到数据,再写脚本、再集成设备。这个“先手动后自动”的习惯帮我节省了大量调试时间,尤其当你需要同时排查网络、鉴权、数据格式等多层问题时,一次只验证一个环节是最有效率的。等属性上报这条链路彻底跑顺了,再去看 RPC 下发命令、共享属性双向同步,你会发现整个 ThingsBoard 的 MQTT 接入体系都是互通的,一通百通。