简介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 用来做低频控制命令比如查询设备属性、调用 RPCMQTT 用来做高频遥测推送两条链路各管各的。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连接被拒绝 }参数设置里有三个地方需要专门说明。第一个是 keepaliveThingsboard 服务端的会话超时默认是 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 建议用 0Thingsboard 对 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_caststd::chrono::milliseconds(duration).count(); } // 构造带时间戳的遥测数据 std::string payload {\ts\: std::to_string(getCurrentTimestampMs()) , \values\: {\temperature\: 25.3}};注意这里values是一个嵌套对象ts在外层。如果搞反了把ts塞到 values 里面Thingsboard 会把它当作一个普通属性字段存储而不是数据时间导致时序图上的点全挤在同一时刻。3. 规则链与服务端 RPCC 设备如何响应平台控制指令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::vectorstd::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默认账号是系统管理员用户名和密码以镜像文档为准一般是sysadminthingsboard.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 日志针对性就强很多了。希望帮到你。本文还有配套的精品资源点击获取