简介:RBI_Thingsboard是一套基于C++开发的物联网平台源码包,面向具备一定C++基础、希望学习IoT设备管理与数据可视化实现的开发者。资源以主分支源码形式呈现,压缩包内共有83个文件,以cpp源文件、h头文件、o编译产物为主,另含Makefile、pro工程文件、UI界面文件、qcustomplot绘图库、串口与MQTT通信相关代码,以及两份PDF说明文档,整体约1016KB。项目覆盖设备管理、数据采集、实时监控和历史分析等核心模块,并充分利用C++高性能、跨平台和系统级编程特性来实现并发连接与高速数据流处理。通过解压并阅读源码,可以了解基于Qt的网关程序如何组织数据存储、网络通信与可视化展示,也可参考其中利用Boost、Poco等库进行线程管理和数据库访问的实践。目前已有191人浏览学习,适合用于物联网平台源码研读、C++后端架构分析以及自定义IoT功能开发等场景。
1. 用 C++ 对接 Thingsboard:设备接入网关的选型与第一道坎
做 IoT 设备接入的人,十有八九会在 Thingsboard 面前卡一下。这个开源 IoT 平台确实漂亮,仪表盘、规则链、设备管理一条龙,但团队里如果主力是 C++,就立刻面临一个尴尬:官方 SDK 大多在 Java、Python 生态里,留给 C++ 的文档少得可怜。RBI_Thingsboard 这类资源的价值就在这——它把 C++ 直连 Thingsboard 的路径从「翻文档翻到眼瞎」压缩成「照着跑就行」。它能解决的是设备端到平台端的 MQTT 接入、遥测上报、属性同步和设备命令下发,适合做网关、边缘控制器或者硬件设备的嵌入式工程师。核心结论先摆出来:C++ 和 Thingsboard 之间没有黑匣子,走 MQTT over TLS 加 JSON 负载,完全是可控的,但真要跑通,选对库和处理好心跳机制就是第一道门槛。
2. 通信协议与权限拉起:REST 和 MQTT 该怎么分工
2.1 C++ 侧的 REST 调用:先从获取 Token 讲起
Thingsboard 的设备接入有两种常用路径:REST API 拉取数据和 MQTT 上报数据。刚接触时容易混着用,我的建议是 REST 用来做低频控制命令(比如查询设备属性、调用 RPC),MQTT 用来做高频遥测推送,两条链路各管各的。
REST 路径第一步是拿设备的访问令牌(access token)。Thingsboard 的设备凭证默认是一个 Token,设备用这个 Token 走 API 时,不需要再走用户名密码流程。常见做法是设备启动后先请求一次:
std::string getDeviceToken(const std::string& deviceId, const std::string& username, const std::string& password) { std::string url = "http://your-thingsboard-host:8080/api/auth/login"; std::string body = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}"; // 用 libcurl 发起 POST 请求 std::string response = httpPost(url, body); // 解析 JSON,取出 token 字段 return parseTokenFromJson(response); }这段代码的逻辑是:先用系统管理员或租户管理员的账号登录,换取一个 JWT Token,再用这个 Token 调用设备管理接口,拿到设备的 access token。参数说明里有两个常见坑:一是 Thingsboard 默认端口是 8080,但如果前面挂了 Nginx 做 SSL 终结,就要换成 443 的 https 地址;二是 login 接口的 body 必须是 JSON 格式,Content-Type 没设成 application/json 会直接返回 400。
拿到设备 Token 之后,后续请求都要在请求头里带X-Authorization: Bearer <token>。这一步是 C++ 开发者最容易漏的,因为 Thingsboard 的 HTTP 接口对 JWT 的校验非常严格,少这个头就是 401 Unauthorized。
2.2 MQTT 接入:连接参数与 Topic 设计
MQTT 是 Thingsboard 主力接入方式,也是 RBI_Thingsboard 这类资源里实现最重的部分。连接时直接把设备 Token 当作 MQTT 的用户名,密码可以留空,这是 Thingsboard 的固定约定。
// 使用 mosquitto C++ 客户端的核心连接参数 mosqpp::mosquittopp* client = new mosqpp::mosquittopp("device-client-001"); client->username_pw_set(deviceToken.c_str(), nullptr); // 连接到 Thingsboard int keepalive = 60; int rc = client->connect(host.c_str(), port, keepalive); if (rc != MOSQ_ERR_SUCCESS) { // 失败时打印错误码,常见的是 5(连接被拒绝) }参数设置里有三个地方需要专门说明。第一个是 keepalive,Thingsboard 服务端的会话超时默认是 120 秒,客户端的 keepalive 必须小于这个值,设成 60 秒比较稳妥。第二个是端口,默认 MQTT 端口 1883 不带加密,如果 Thingsboard 配了 TLS,就要改成 8883 并且加载 CA 证书,下面再展开。第三个是 client id,设备注册后 Thingsboard 会校验这个 ID 是否合法,不能随便起名。
Topic 路径设计直接影响数据能不能落到正确的设备上。上报遥测数据用v1/devices/me/telemetry,上报设备属性用v1/devices/me/attributes,请求服务端 RPC 应答用v1/devices/me/rpc/response/<requestId>。这些 Topic 是 Thingsboard 的设备 API 规范,不是随便定的,写死在资源代码里的路径通常就是这套。
2.3 遥测上报的 JSON 结构与时间戳陷阱
遥测数据在 Thingsboard 侧是以 key-value 形式存储的,所以 C++ 端发的内容必须是一个 JSON 对象,不能是数组或者裸字符串。
void publishTelemetry(mosqpp::mosquittopp* client, const std::string& jsonPayload) { int qos = 0; bool retain = false; std::string topic = "v1/devices/me/telemetry"; client->publish(nullptr, topic.c_str(), jsonPayload.size(), jsonPayload.c_str(), qos, retain); } // 典型用法 std::string payload = "{\"temperature\":36.5,\"humidity\":73.2}"; publishTelemetry(client, payload);这段代码里 publish 的四个参数分别对应消息 ID、Topic、消息体长度、消息体内容、QoS 和 retain 标志。QoS 建议用 0,Thingsboard 对 QoS 0 的支持最顺畅,处理速度也最快;retain 必须设 false,否则会导致新设备上线后收到旧数据。
时间戳是个隐蔽陷阱。如果只想让 Thingsboard 用服务器接收时间作为数据时间,上面的写法没问题。但如果需要设备本地时间作为数据时间,就得在 JSON 里加ts字段,格式必须是毫秒级时间戳。C++ 里用std::chrono获取当前毫秒数的代码如下:
int64_t getCurrentTimestampMs() { auto now = std::chrono::system_clock::now(); auto duration = now.time_since_epoch(); return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count(); } // 构造带时间戳的遥测数据 std::string payload = "{\"ts\": " + std::to_string(getCurrentTimestampMs()) + ", \"values\": {\"temperature\": 25.3}}";注意这里values是一个嵌套对象,ts在外层。如果搞反了,把ts塞到 values 里面,Thingsboard 会把它当作一个普通属性字段存储,而不是数据时间,导致时序图上的点全挤在同一时刻。
3. 规则链与服务端 RPC:C++ 设备如何响应平台控制指令
3.1 订阅 RPC Topic 的完整流程
Thingsboard 的服务端 RPC 是平台主动找设备要数据或者下发控制指令的核心机制。C++ 设备需要先订阅v1/devices/me/rpc/request/+,然后解析请求 ID 和请求方法名,分门别类处理。
void on_message(const struct mosquitto_message* msg) override { std::string topic = msg->topic; std::string payload = (char*)msg->payload; if (topic.find("v1/devices/me/rpc/request/") == 0) { // 解析 request ID size_t pos = topic.rfind('/'); std::string requestId = topic.substr(pos + 1); // 解析请求内容 // 请求体形如 {"method":"getStatus","params":{"arg":1}} std::string responseTopic = "v1/devices/me/rpc/response/" + requestId; std::string response = "{\"success\":true,\"result\":\"device-ok\"}"; publish(nullptr, responseTopic.c_str(), response.size(), response.c_str(), 0, false); } }这段代码的要点是 request ID 必须从 Topic 里提取,而不是从 JSON 体提取。因为 Thingsboard 发来的请求消息,Topic 末尾那串数字是唯一关联标识,应答时如果不带这个 ID,平台那边无法把响应关联到具体的 RPC 调用。
另一个细节是 SUBSCRIBE 要在连接建立之后立即执行,不要在等第一条消息到达时才去订阅,否则会出现平台下发指令时设备还没订阅上的窗口期,那段时间的 RPC 请求会被直接丢弃。
3.2 RPC 超时与重试策略
Thingsboard 的服务端 RPC 默认超时时间是 10 秒,设备如果没有在 10 秒内返回响应,平台会标记为 RPC 超时,并返回错误给调用方。
// 伪代码描述策略 uint8_t retryTimes = 3; for (int i = 0; i < retryTimes; i++) { if (handleRpcRequest(requestId, methodName, params)) { break; } // 间隔 1 秒重试,或者缓存请求待下次连接恢复时处理 sleep(1); }我见过不少 C++ 实现里的做法是:收到 RPC 请求后直接硬编码处理,如果处理函数涉及外部硬件操作(比如控制电机或者读取传感器),执行时间一旦超过 10 秒,响应就会过期。这里有一个实践原则:对耗时操作,先回一个「正在执行」的应答,再通过后续遥测数据上报执行结果。Thingsboard 的规则链可以监听那个示范属性来更新设备状态,不必非得在一个 RPC 往返里完成所有事。
3.3 客户端 RPC 与属性共享
除了服务端 RPC,还有客户端 RPC 方向:设备主动请求平台的数据。这个场景在 C++ 嵌入式设备里用得少,但有些项目确实需要设备去平台拿一份配置参数。
// 设备向平台发 RPC 请求 std::string topic = "v1/devices/me/rpc/request/42"; std::string payload = "{\"method\":\"getServerConfig\",\"params\":{}}"; client->publish(nullptr, topic.c_str(), payload.size(), payload.c_str(), 0, false); // 平台响应会发回 v1/devices/me/rpc/response/42请求 ID 这次由设备自己定义,只要保证当前会话内唯一即可。平台的处理结果会通过规则链的 RPC 节点路由回来,设备在订阅v1/devices/me/rpc/response/+之后就能收到。这里有一个实用的点:客户端 RPC 比「共享属性订阅」更灵活,因为请求方可以直接带参数过去,不必先在平台侧把属性写死。
4. 避坑排查:TLS 证书、JSON 解析与心跳断连的疑难杂症
4.1 连接被拒(错误码 5)与防火墙玄学
现象:MQTT 客户端调用 connect 后返回 MOSQ_ERR_ERRNO(错误码 5),日志显示 Connection refused。
原因:最常见的是端口错了或者防火墙没放行。有些同事会拿服务器本地 IP 测试,本机跑通了,部署到边缘网关后就连不上,查到最后是云主机的安全组规则只放行了 22 和 8080,没放行 1883。
解决:端口先从服务端确认。在 Thingsboard 所在机器上执行netstat -tulpn | grep 1883,确认 MQTT 监听正常后,再用telnet <host> 1883从设备侧测连通性。如果安全组规则不好改,也可以在网关服务器上用 Nginx 做 TCP 转发,把外网 8883 映射到内网 1883,这样就绕开了防火墙配置。
4.2 TLS 握手失败:证书链不完整引发的「坑王」
现象:连接 8883 端口时一直卡在 SSL handshake,日志里出现certificate verify failed或者self-signed certificate。
原因:Thingsboard 默认生成的证书是自签名 CA 签发的,设备端必须把这个 CA 证书加到本地信任链里。很多 C++ 开发者只传了服务器证书,没传 CA 根证书,握手自然失败。
解决:把 Thingsboard 拉取的 CA 证书(比如ca.pem)保存到设备本地,在 mosquitto 客户端里设置:
client->tls_set("/path/to/ca.pem", nullptr, nullptr, nullptr, nullptr);注意第二个参数是路径目录,传nullptr表示不启用客户端证书认证。如果 Thingsboard 服务端配置了双向 TLS,就要额外提供客户端证书和私钥,三个参数依次是 CA 文件、客户端证书文件、客户端私钥文件。
4.3 心跳失效:keepalive 与 broker 超时的连锁反应
现象:设备运行一段时间后不再收发消息,但 MQTT 连接没有主动断开,过几分钟后平台显示设备离线。
原因:Thingsboard 的会话超时设置是 120 秒,而 C++ 端的 keepalive 如果设成 180 秒,中间存在交错窗口。broker 在 120 秒内没收到任何包(包括 PINGREQ),就会主动把会话断掉。设备端如果依赖底层 TCP 超时才发现异常,这段时间就是黑洞。
解决:keepalive 统一设成 45 秒到 60 秒之间,同时在上报循环里定期发 PINGREQ。mosquitto 客户端会自动处理 PINGREQ,前提是 keepalive 值没写错。另一个实践细节:发布遥测数据的频率如果超过 keepalive 间隔,broker 不会认为连接活跃,因为 PUBLISH 包会重置 keepalive 计时,所以高频上报的设备反而不容易掉线。
4.4 JSON 解析失败:转义与浮点精度
现象:遥测数据偶尔上报失败,平台侧显示消息格式错误,但 C++ 端打印出来的 JSON 看起来没问题。
原因:C++ 的字符串拼接很容易漏转义,尤其是传感器读数恰好是NaN或inf时。Thingsboard 的 JSON 解析器要求数据必须是合法的 IEEE 754 有限数,NaN直接会被拒绝。
解决:上报前做一个数值检查,把非有限值过滤或者替换成字符串:
#include <cmath> bool isValidNumber(double val) { return std::isfinite(val); } // 构造负载前检查 double temp = sensorRead(); if (!isValidNumber(temp)) { temp = 0.0; logWarning("temperature sensor returned NaN"); }浮点精度方面,JSON 序列化时用std::to_string会损失精度(只有 6 位有效数字),改用snprintf保留 4 位小数即可:
char buf[32]; snprintf(buf, sizeof(buf), "%.4f", temp); std::string payload = "{\"temperature\":" + std::string(buf) + "}";5. 离线数据缓存与 QoS 保障:不丢点的工程化实现
5.1 内存队列 vs 磁盘缓存
设备一旦断网或者平台重启,MQTT 连接会中断。如果此时正在采集高频数据,全丢了用户肯定不接受。工程上必须做一个本地缓存,最简单的是内存环形队列,但它扛不住进程重启。如果设备掉电,内存数据全没,所以关键场景一定要落盘。
我一般会这样做:用 SQLite 存最近 10000 条遥测记录,字段只有seq(自增)、ts(时间戳)、payload(原始 JSON),避免引入冗余的协议解析。
CREATE TABLE IF NOT EXISTS telemetry_cache ( seq INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, payload TEXT );5.2 断线重连与批量补报
// 断线重连后的补报逻辑 void resendPendingTelemetry(mosqpp::mosquittopp* client) { // 从数据库读取未上报的遥测 std::vector<std::string> pending = dql("SELECT payload FROM telemetry_cache ORDER BY seq LIMIT 100"); for (auto& p : pending) { std::string topic = "v1/devices/me/telemetry"; client->publish(nullptr, topic.c_str(), p.size(), p.c_str(), 1, false); } // 上报完成后清理缓存 }QoS 在这里我建议升到 1,因为补报的数据如果丢失,很难再精确追踪。但要注意 QoS 1 的 PUBACK 确认机制会成倍增加 broker 的负载,大批量补报时不要一次发太多,上面代码里限制 100 条一批是合理的做法。补报时间点选择在重连成功并订阅了 RPC Topic 之后,确保链路完全就绪再发。
5.3 属性持久化与设备重启恢复
设备属性(比如固件版本、工作模式)在上次运行后需要恢复。Thingsboard 支持v1/devices/me/attributes的共享属性读取,但设备重启时网络还没就绪,本地保存的副本就是「后悔药」。建议在每次成功上报属性后,把当前属性 JSON 写入本地文件,启动时先读取本地文件,再用 MQTT 去同步一次平台侧的数据作为校正。
6. 用 Docker 本地复现 Thingsboard:五分钟起一套验证环境
6.1 快速启动 Thingsboard 单机版
要验证上面的 C++ 代码,没有真实服务器也能跑。用 Docker 起一个 Thingsboard 最简单,不需要提前装 PostgreSQL 和 Cassandra,官方镜像会把依赖一起带起来。
# 拉取 Thingsboard 镜像并启动 docker run -d --name tb \ -p 8080:9090 \ -p 1883:1883 \ -p 7070:7070 \ thingsboard/tb-postgres:latest参数说明:-p 8080:9090把容器的 HTTP 端口映射到主机 8080;-p 1883:1883暴露 MQTT 端口;7070是 MQTT over TLS 的端口。启动后等一两分钟,浏览器打开http://localhost:8080,默认账号是系统管理员(用户名和密码以镜像文档为准,一般是sysadmin@thingsboard.org和sysadmin)。
6.2 创建设备并取得 Token
登录后:设备 → 新建设备 → 记下设备名称 → 打开设备详情 → 凭证 → 复制 access token。这个 Token 就是前面 C++ 代码里要填的deviceToken。
6.3 用命令行快速验证连通性
在没有编译 C++ 客户端之前,先用 mosquitto_pub 验证平台是否正常:
mosquitto_pub -h localhost -p 1883 \ -u <你的设备Token> \ -t v1/devices/me/telemetry \ -m '{"temperature":26.8,"humidity":60.1}'如果 Thingsboard 设备列表里能看到最新遥测数据,说明链路通了,再回头编译 C++ 代码。这一步可以省掉大量排错时间,因为 MQTT 协议本身的问题(比如 Topic 拼错、Token 粘了空格)会最先暴露出来。
6.4 移植时的路径问题
在本地 Docker 验证通过后,移植到真实设备环境时,唯一要改的是三处:host 地址从 localhost 换成服务器实际 IP 或域名;TLS 连接时把ca.pem拷贝到设备指定目录;端口按云平台实际暴露情况填写。
从那以后,我每接一个 Thingsboard 项目,都强制先跑一遍「Docker 环境 + mosquitto_pub + 最小 C++ 客户端」这个黄金三步。这套流程能过滤掉八成的基础连接坑,剩下来的问题再去看规则链日志和 broker 日志,针对性就强很多了。希望帮到你。
本文还有配套的精品资源,点击获取