|
@@ -0,0 +1,1543 @@
|
|
|
|
|
+---
|
|
|
|
|
+title: 实战项目MQTT与视频监控
|
|
|
|
|
+tags: [嵌入式Linux, 项目实战, MQTT, mosquitto, paho.mqtt.c, MQTTClient, 发布订阅, QoS, 遗嘱, 保留消息, 物联网, RTMP, Nginx, FFmpeg, VLC, 视频监控, V4L2, MJPEG, Qt, IMX6ULL]
|
|
|
|
|
+created: 2026-09-18
|
|
|
|
|
+updated: 2026-09-18
|
|
|
|
|
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;《嵌入式Qt开发指南》视频监控例程"
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+# 实战项目MQTT与视频监控
|
|
|
|
|
+
|
|
|
|
|
+> 💡 **关联知识**:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]、[[04-网络编程与项目实战/面试-网络编程与项目实战]]
|
|
|
|
|
+
|
|
|
|
|
+本篇把前面的 socket、文件 I/O、CMake 知识落到两个完整项目上:**MQTT 物联网**(开发板作为客户端,远程控制 LED、上报 SoC 温度)和**视频监控**(两条技术路线:Nginx + FFmpeg 的 RTMP 推拉流,以及 V4L2 采集 + JPEG 广播的局域网 MJPEG 方案)。每个项目都给出移植步骤、完整可复制源码与测试方法。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+# 第一部分:MQTT 物联网
|
|
|
|
|
+
|
|
|
|
|
+## 1. MQTT 简介
|
|
|
|
|
+
|
|
|
|
|
+MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种基于**客户端-服务端架构的发布/订阅模式**消息传输协议,与 HTTP 一样工作在 TCP/IP 四层模型的**应用层**,构建于 TCP/IP 之上。它的设计思想是轻巧、开放、简单、规范,易于实现,因此特别适合机器与机器(M2M)、物联网(IoT)等受限环境。
|
|
|
|
|
+
|
|
|
|
|
+### 1.1 主要特性
|
|
|
|
|
+
|
|
|
|
|
+| 特性 | 说明 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| 发布/订阅模式 | 一对多的消息发布,解除应用程序耦合 |
|
|
|
|
|
+| 基于 TCP/IP | 主流 MQTT 基于 TCP;也有基于 UDP 的 MQTT-SN |
|
|
|
|
|
+| 支持 QoS | 按消息重要性设置不同服务质量等级(0/1/2) |
|
|
|
|
|
+| 小型传输 | 开销很小,协议交换最小化,降低网络流量 |
|
|
|
|
|
+| 遗嘱机制(will) | 客户端异常断线时通知其它客户端 |
|
|
|
|
|
+| 主题寻址 | 基于主题发布/订阅,对负载内容屏蔽 |
|
|
|
|
|
+| 心跳机制 | 检测客户端是否在线 |
|
|
|
|
|
+
|
|
|
|
|
+### 1.2 历史与版本
|
|
|
|
|
+
|
|
|
|
|
+- 1999 年由 Andy Stanford-Clark 和 Arlen Nipper 发明,最初用于石油管道传感器与卫星之间的数据传输。
|
|
|
|
|
+- 面向"低带宽、高延迟或不可靠网络",要求:易于实现、服务质量可控、占用带宽小、能适应掉线断网、云端与设备保持长连接。
|
|
|
|
|
+- 主流版本:**MQTT 3.1.1**(2014-10,经典主流)与 **MQTT 5**(2019-03,完全兼容 3.1.1 并增加新功能)。
|
|
|
|
|
+- MQTT 更适合需要实时控制的场合(尤其执行器);由于要保持长连接、定时发心跳,**并不适合低功耗场合**。
|
|
|
|
|
+
|
|
|
|
|
+## 2. MQTT 协议原理
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 三个核心角色
|
|
|
|
|
+
|
|
|
|
|
+| 角色 | 说明 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| 服务端(Broker) | MQTT 信息传输的枢纽,负责把客户端发来的信息转发给其它客户端,并管理客户端 |
|
|
|
|
|
+| 客户端 | 可以发布消息,也可以订阅主题后接收消息;角色不固定,随主题切换 |
|
|
|
|
|
+| 主题(Topic) | 发布/订阅的纽带,服务端按主题决定消息转发给哪些订阅者 |
|
|
|
|
|
+
|
|
|
|
|
+客户端发布消息时要指定主题;订阅者订阅主题后,服务端就把该主题的新消息推送给订阅者。一个客户端对不同主题可以既是发布者又是订阅者。
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+flowchart LR
|
|
|
|
|
+ subgraph P["发布者"]
|
|
|
|
|
+ Dev["开发板客户端"]
|
|
|
|
|
+ end
|
|
|
|
|
+ subgraph B["MQTT 服务端 Broker"]
|
|
|
|
|
+ T1["主题: dt_mqtt/temperature"]
|
|
|
|
|
+ T2["主题: dt_mqtt/led"]
|
|
|
|
|
+ end
|
|
|
|
|
+ subgraph S["订阅者"]
|
|
|
|
|
+ Phone["手机客户端"]
|
|
|
|
|
+ PC["电脑 MQTT.fx"]
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ Dev -->|"发布 temperature"| T1
|
|
|
|
|
+ T1 --> Phone
|
|
|
|
|
+ T1 --> PC
|
|
|
|
|
+ Phone -->|"发布 led=1/0/2"| T2
|
|
|
|
|
+ PC -->|"发布 led=1/0/2"| T2
|
|
|
|
|
+ T2 --> Dev
|
|
|
|
|
+
|
|
|
|
|
+ classDef pub fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
|
|
|
|
|
+ classDef broker fill:#fef9c3,stroke:#ca8a04,color:#713f12
|
|
|
|
|
+ classDef sub fill:#dcfce7,stroke:#16a34a,color:#14532d
|
|
|
|
|
+ class Dev pub
|
|
|
|
|
+ class T1,T2 broker
|
|
|
|
|
+ class Phone,PC sub
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+发布/订阅的三个特性都带一个"可"字,体现灵活性:
|
|
|
|
|
+
|
|
|
|
|
+- **客户端相互独立**:发布者不知道有多少订阅者,订阅者之间也不知道彼此存在。
|
|
|
|
|
+- **空间上可分离**:双方只要联网,无论远近都能通信。
|
|
|
|
|
+- **时间上可异步**:发送与接收无需同步,服务端可暂存离线消息。
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 连接服务端:CONNECT 与 CONNACK
|
|
|
|
|
+
|
|
|
|
|
+客户端连接服务端分两步:客户端发 **CONNECT** 报文 → 服务端回 **CONNACK** 报文。MQTT 报文由**固定头、可变头、消息体(Payload)**三部分组成。
|
|
|
|
|
+
|
|
|
|
|
+CONNECT 报文关键字段:
|
|
|
|
|
+
|
|
|
|
|
+| 字段 | 说明 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| `clientId` | 客户端标识,必须唯一;两个客户端用同一 clientId 会被当成同一个客户端 |
|
|
|
|
|
+| `keepAlive` | 心跳时间间隔,如 60 表示约每 60 秒发一次心跳包 |
|
|
|
|
|
+| `cleanSession` | 清除会话标志 |
|
|
|
|
|
+
|
|
|
|
|
+`cleanSession` 的作用:
|
|
|
|
|
+
|
|
|
|
|
+- `cleanSession = 0`:创建**持久性会话**,客户端再次上线时可收到离线期间所有 QoS>0 的消息;服务端会记住客户端订阅过的主题,直到会话超时注销。
|
|
|
|
|
+- `cleanSession = 1`:创建**临时会话**,不接收离线消息、服务端不保存订阅主题,每次连接都是全新会话,断开后会话自动销毁。
|
|
|
|
|
+
|
|
|
|
|
+CONNACK 报文包含 `returnCode`(连接返回码)与 `sessionPresent`:
|
|
|
|
|
+
|
|
|
|
|
+| returnCode | 说明 |
|
|
|
|
|
+| ---------- | ---- |
|
|
|
|
|
+| 0 | 连接成功 |
|
|
|
|
|
+| 1 | 拒绝:不支持的 MQTT 协议版本 |
|
|
|
|
|
+| 2 | 拒绝:不支持客户端标识符的编码 |
|
|
|
|
|
+| 3 | 拒绝:服务端不可用 |
|
|
|
|
|
+| 4 | 拒绝:用户名或密码无效 |
|
|
|
|
|
+| 5 | 拒绝:客户端未授权 |
|
|
|
|
|
+| 6-255 | 保留备用 |
|
|
|
|
|
+
|
|
|
|
|
+`sessionPresent` 与 `cleanSession` 配合:`cleanSession=0` 时,若服务端保存了上次会话状态则返回 1,否则 0;`cleanSession=1` 时恒为 0。
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 发布、订阅、取消订阅
|
|
|
|
|
+
|
|
|
|
|
+| 报文 | 作用 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| PUBLISH | 发布消息(含 packetId、topicName、payload、qos、retain、dup) |
|
|
|
|
|
+| SUBSCRIBE / SUBACK | 订阅主题 / 订阅确认(一个 SUBSCRIBE 可含多个主题) |
|
|
|
|
|
+| UNSUBSCRIBE / UNSUBACK | 取消订阅 / 取消订阅确认 |
|
|
|
|
|
+| DISCONNECT | 客户端主动断开连接 |
|
|
|
|
|
+
|
|
|
|
|
+SUBACK 的订阅返回码:
|
|
|
|
|
+
|
|
|
|
|
+| 返回码 | 说明 |
|
|
|
|
|
+| ------ | ---- |
|
|
|
|
|
+| 0x00 | 订阅成功——QoS0 |
|
|
|
|
|
+| 0x01 | 订阅成功——QoS1 |
|
|
|
|
|
+| 0x02 | 订阅成功——QoS2 |
|
|
|
|
|
+| 0x80 | 订阅失败 |
|
|
|
|
|
+
|
|
|
|
|
+注意事项:
|
|
|
|
|
+
|
|
|
|
|
+- `packetId`(报文标识符)只有 QoS>0 时才为非零数值。
|
|
|
|
|
+- `dup` 重发标志只在 QoS>0 时使用;接收方没有及时回确认时,发送方重发并置 dup=true。
|
|
|
|
|
+- `retain` 保留标志见 2.6 节。
|
|
|
|
|
+
|
|
|
|
|
+### 2.4 主题进阶
|
|
|
|
|
+
|
|
|
|
|
+- **区分大小写**,可用空格但建议不用,**不要用中文**(多数服务器不支持),建议使用 ASCII 字符。
|
|
|
|
|
+- **分级**:用 `/` 分隔,如 `home/sensor/led/brightness` 共四级;主题不要以 `/` 开头。
|
|
|
|
|
+- **通配符**(只能在订阅时用):
|
|
|
|
|
+ - 单级通配符 `+`:匹配一个主题级别,如 `home/sensor/+/status`。
|
|
|
|
|
+ - 多级通配符 `#`:匹配任意数量级别,如 `home/sensor/#`。
|
|
|
|
|
+- 以 `$` 开头的是服务端系统保留主题(如 `$SYS/#`),客户端不可随意订阅或发布。
|
|
|
|
|
+
|
|
|
|
|
+### 2.5 QoS 服务质量
|
|
|
|
|
+
|
|
|
|
|
+| QoS | 语义 | 传输流程 |
|
|
|
|
|
+| --- | ---- | -------- |
|
|
|
|
|
+| 0 | 最多发一次(最多一次) | 发送即结束,不确认、不重传,靠 TCP 保证 |
|
|
|
|
|
+| 1 | 至少发一次(最少一次) | PUBLISH → PUBACK,超时未收到 PUBACK 则重发,可能重复 |
|
|
|
|
|
+| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP,最安全也最慢 |
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+sequenceDiagram
|
|
|
|
|
+ participant S as 发送端
|
|
|
|
|
+ participant R as 接收端
|
|
|
|
|
+
|
|
|
|
|
+ Note over S,R: QoS 0:最多一次
|
|
|
|
|
+ S->>R: PUBLISH
|
|
|
|
|
+
|
|
|
|
|
+ Note over S,R: QoS 1:至少一次
|
|
|
|
|
+ S->>R: PUBLISH
|
|
|
|
|
+ R-->>S: PUBACK
|
|
|
|
|
+ Note right of S: 未收到 PUBACK 则重发(dup=1)
|
|
|
|
|
+
|
|
|
|
|
+ Note over S,R: QoS 2:保证收一次
|
|
|
|
|
+ S->>R: PUBLISH
|
|
|
|
|
+ R-->>S: PUBREC
|
|
|
|
|
+ S->>R: PUBREL
|
|
|
|
|
+ R-->>S: PUBCOMP
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+要点:
|
|
|
|
|
+
|
|
|
|
|
+- 要实现 QoS>0,连接时 **`cleanSession` 必须设为 false**,否则收不到任何离线消息(含 QoS1/QoS2)。
|
|
|
|
|
+- QoS=1 时协议本身不去重,需应用根据 dup 标志自行处理。
|
|
|
|
|
+- **服务质量降级**:若发布用 QoS2、订阅用 QoS1,服务端会采用两者中**较低**的 QoS 提供服务。
|
|
|
|
|
+
|
|
|
|
|
+### 2.6 保留消息(retain)
|
|
|
|
|
+
|
|
|
|
|
+- 作用:让服务端保留该主题的**最新一条**消息,任何客户端订阅该主题后立即收到它,不必等下一次发布。
|
|
|
|
|
+- 每个主题只能有一条保留消息,发布新的保留消息会覆盖旧的。
|
|
|
|
|
+- 删除保留消息:向该主题发布一条**空的保留消息**即可。
|
|
|
|
|
+
|
|
|
|
|
+### 2.7 心跳机制
|
|
|
|
|
+
|
|
|
|
|
+客户端在空闲时定时发送 **PINGREQ** 报文(心跳请求),服务端收到后回 **PINGRESP**(心跳响应)。服务端据此判断客户端是否在线;客户端若发 PINGREQ 后收不到 PINGRESP,则认为自己已断线。发送间隔由 CONNECT 报文的 `keepAlive` 指定。
|
|
|
|
|
+
|
|
|
|
|
+### 2.8 遗嘱机制(will)
|
|
|
|
|
+
|
|
|
|
|
+客户端在"活着"时通过 CONNECT 报文预先写好遗嘱,一旦**意外断线**(非主动发 DISCONNECT,如断网、断电),服务端就把遗嘱消息发布出去。主动断开不会触发遗嘱。
|
|
|
|
|
+
|
|
|
|
|
+| 字段 | 说明 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| `willTopic` | 遗嘱主题,订阅该主题的客户端才会收到遗嘱消息 |
|
|
|
|
|
+| `willMessage` | 遗嘱内容 |
|
|
|
|
|
+| `willRetain` | 遗嘱的保留标志 |
|
|
|
|
|
+| `willQoS` | 遗嘱的 QoS 等级(0/1/2) |
|
|
|
|
|
+
|
|
|
|
|
+小技巧:客户端上线时主动向自己的遗嘱主题发一条"在线"消息,订阅者即可据此实现上线通知。
|
|
|
|
|
+
|
|
|
|
|
+### 2.9 用户名密码认证
|
|
|
|
|
+
|
|
|
|
|
+CONNECT 报文中的 `username`、`password` 是可选字段。服务端若开启认证,则客户端必须提供正确的用户名密码才能连接;有些服务端还用它识别客户端归属、管理私人主题权限。
|
|
|
|
|
+
|
|
|
|
|
+## 3. 搭建 MQTT 服务器
|
|
|
|
|
+
|
|
|
|
|
+### 3.1 公用 MQTT 服务器
|
|
|
|
|
+
|
|
|
|
|
+| 服务器 | 地址 | 端口 |
|
|
|
|
|
+| ------ | ---- | ---- |
|
|
|
|
|
+| test.mosquitto.org(国外) | `test.mosquitto.org` | 1883(TCP)、8883(TLS)、8080(WebSocket) |
|
|
|
|
|
+| broker.hivemq.com(国外) | `broker.hivemq.com` | 1883、8000(WebSocket) |
|
|
|
|
|
+| 然也物联(国内) | `test.ranye-iot.net` | 1883、8883(TLS) |
|
|
|
|
|
+| 通信猫(国内) | `mq.tongxinmao.com` | 1883 |
|
|
|
|
|
+
|
|
|
|
|
+> 公用服务器仅供学习测试,不可商用;国内服务器连接快、延迟低。
|
|
|
|
|
+
|
|
|
|
|
+### 3.2 用 mosquitto 自建服务器(Ubuntu)
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa # 加入软件源
|
|
|
|
|
+sudo apt-get update # 更新软件源
|
|
|
|
|
+sudo apt-get install mosquitto # 安装 mosquitto 服务端
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`/etc/mosquitto/conf.d/` 下所有 `*.conf` 会被自动加载。新建 `myconfig.conf`:
|
|
|
|
|
+
|
|
|
|
|
+```conf
|
|
|
|
|
+# /etc/mosquitto/conf.d/myconfig.conf
|
|
|
|
|
+# 添加监听端口 1883
|
|
|
|
|
+listener 1883
|
|
|
|
|
+# 关闭匿名访问,客户端必须使用用户名
|
|
|
|
|
+allow_anonymous false
|
|
|
|
|
+# 指定用于存放用户名和密码的文件
|
|
|
|
|
+password_file /etc/mosquitto/pwfile.txt
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+创建用户名密码文件:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+sudo touch /etc/mosquitto/pwfile.txt
|
|
|
|
|
+sudo mosquitto_passwd /etc/mosquitto/pwfile.txt mqtt1 # 用户名 mqtt1,密码 123456(需输入两次)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+启停与查看状态:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+sudo service mosquitto start # 启动服务
|
|
|
|
|
+sudo service mosquitto status # 查看服务状态
|
|
|
|
|
+sudo service mosquitto stop # 停止服务
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 网络要求:开发板、Ubuntu、手机/电脑处于同一局域网且能互相 `ping` 通。
|
|
|
|
|
+
|
|
|
|
|
+### 3.3 用 MQTT.fx 初体验
|
|
|
|
|
+
|
|
|
|
|
+1. 下载安装 MQTT.fx(官网 <http://mqttfx.jensd.de/>,教程使用 1.7.1 版本)。
|
|
|
|
|
+2. 点击齿轮按钮新建配置:填写 Broker 地址(如 `192.168.6.161`,即 Ubuntu 的 IP)、Client ID(如 `test1`)、用户名 `mqtt1`、密码 `123456`,Apply 后连接。
|
|
|
|
|
+3. 订阅主题:切到 Subscribe 页,填写主题(如 `dt2914/testTopic`),点 Subscribe。
|
|
|
|
|
+4. 发布消息:切到 Publish 页,填写主题与内容,点 Publish;订阅同一个主题的客户端即可收到。
|
|
|
|
|
+5. 取消订阅:在已订阅主题上点击取消即可。
|
|
|
|
|
+
|
|
|
|
|
+手机端也可用 MQTT Client 软件连接同一服务器(Client ID 不能与其它客户端重复),实现跨设备收发。
|
|
|
|
|
+
|
|
|
|
|
+## 4. 移植 paho.mqtt.c 客户端库
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 下载源码
|
|
|
|
|
+
|
|
|
|
|
+官网 <https://mqtt.org/> → Software → Client libraries → **Eclipse Paho C**。示例使用 **1.3.8** 版本(当前最新为 1.3.13)。
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+tar -xzf paho.mqtt.c-1.3.8.tar.gz
|
|
|
|
|
+cd paho.mqtt.c-1.3.8
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+该工程顶级目录有 `CMakeLists.txt`,是 CMake 构建的工程。
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 交叉编译
|
|
|
|
|
+
|
|
|
|
|
+在 `cmake/` 目录新建 `arm-linux-setup.cmake`(内容与本库 [[04-网络编程与项目实战/03-CMake入门与进阶]] 第 5.1 节相同,按实际工具链路径修改):
|
|
|
|
|
+
|
|
|
|
|
+```cmake
|
|
|
|
|
+##################################
|
|
|
|
|
+# 配置 ARM 交叉编译
|
|
|
|
|
+#################################
|
|
|
|
|
+set(CMAKE_SYSTEM_NAME Linux) # 设置目标系统名字
|
|
|
|
|
+set(CMAKE_SYSTEM_PROCESSOR arm) # 设置目标处理器架构
|
|
|
|
|
+
|
|
|
|
|
+# 指定编译器的 sysroot 路径
|
|
|
|
|
+set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
|
|
|
|
|
+set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
|
|
|
|
|
+
|
|
|
|
|
+# 指定交叉编译器 arm-linux-gcc
|
|
|
|
|
+set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
|
|
|
|
|
+
|
|
|
|
|
+# 为编译器添加编译选项
|
|
|
|
|
+set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
|
|
|
|
|
+
|
|
|
|
|
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
|
|
|
|
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
|
|
|
|
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
|
|
|
|
+#################################
|
|
|
|
|
+# end
|
|
|
|
|
+##################################
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+mkdir build && cd build
|
|
|
|
|
+
|
|
|
|
|
+cmake -DCMAKE_BUILD_TYPE=Release \
|
|
|
|
|
+ -DCMAKE_INSTALL_PREFIX=~/tools/paho.mqtt.c-1.3.8/install \
|
|
|
|
|
+ -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
|
|
|
|
|
+ -DPAHO_WITH_SSL=TRUE \
|
|
|
|
|
+ -DPAHO_BUILD_SAMPLES=TRUE ..
|
|
|
|
|
+
|
|
|
|
|
+make
|
|
|
|
|
+make install
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `CMAKE_INSTALL_PREFIX` 指定安装路径(示例为源码目录下的 `install`)。
|
|
|
|
|
+- `PAHO_WITH_SSL`、`PAHO_BUILD_SAMPLES` 是缓存变量,含义见工程 `README.md`。
|
|
|
|
|
+- MQTT 库依赖 openssl;示例未单独移植 openssl,因为开发板出厂系统已带 openssl,且交叉编译器会经 sysroot 链接它。
|
|
|
|
|
+
|
|
|
|
|
+### 4.3 安装产物
|
|
|
|
|
+
|
|
|
|
|
+`install/` 下有 `bin`、`include`、`lib`、`share`:
|
|
|
|
|
+
|
|
|
|
|
+| 库文件 | 说明 |
|
|
|
|
|
+| ------ | ---- |
|
|
|
|
|
+| `libpaho-mqtt3a.so` | 异步模式,不支持 SSL |
|
|
|
|
|
+| `libpaho-mqtt3as.so` | 异步模式,支持 SSL |
|
|
|
|
|
+| `libpaho-mqtt3c.so` | 同步模式,不支持 SSL |
|
|
|
|
|
+| `libpaho-mqtt3cs.so` | 同步模式,支持 SSL |
|
|
|
|
|
+
|
|
|
|
|
+头文件:同步模式包含 `MQTTClient.h`,异步模式包含 `MQTTAsync.h`(其它头文件被这两个包含)。本项目使用**同步模式** `libpaho-mqtt3c.so` + `MQTTClient.h`。
|
|
|
|
|
+
|
|
|
|
|
+### 4.4 拷贝库到开发板
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 在 Ubuntu 上打包
|
|
|
|
|
+tar -czf libmqtt.tar.gz -C ~/tools/paho.mqtt.c-1.3.8/install/lib .
|
|
|
|
|
+
|
|
|
|
|
+# 拷贝到开发板 /home/root 后,解压到 /usr/lib
|
|
|
|
|
+tar -xzf libmqtt.tar.gz -C /usr/lib
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 不要破坏库原有的软链接关系,建议打包 `.so` 及其符号链接一起拷贝。
|
|
|
|
|
+
|
|
|
|
|
+## 5. MQTT 客户端库 API(同步模式)
|
|
|
|
|
+
|
|
|
|
|
+以下函数定义在 `MQTTClient.h`。文档见源码 `docs/MQTTClient/html/index.html`。
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 `MQTTClient_message` 结构体
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+typedef struct
|
|
|
|
|
+{
|
|
|
|
|
+ int payloadlen; // 负载长度
|
|
|
|
|
+ void* payload; // 负载
|
|
|
|
|
+ int qos; // 消息的 qos 等级
|
|
|
|
|
+ int retained; // 消息的保留标志
|
|
|
|
|
+ int dup; // dup 标志(重复标志)
|
|
|
|
|
+ int msgid; // 消息标识符,即 packetId
|
|
|
|
|
+ /* ... */
|
|
|
|
|
+} MQTTClient_message;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+发布消息需实例化一个 `MQTTClient_message` 对象;接收消息也是收到该对象。常用 `MQTTClient_message_initializer` 宏初始化。
|
|
|
|
|
+
|
|
|
|
|
+### 5.2 创建客户端对象
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+int MQTTClient_create(MQTTClient *handle,
|
|
|
|
|
+ const char *serverURI,
|
|
|
|
|
+ const char *clientId,
|
|
|
|
|
+ int persistence_type,
|
|
|
|
|
+ void *persistence_context);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| 参数 | 说明 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| `handle` | MQTT 客户端句柄 |
|
|
|
|
|
+| `serverURI` | MQTT 服务器地址 |
|
|
|
|
|
+| `clientId` | 客户端 ID |
|
|
|
|
|
+| `persistence_type` | 持久化类型,见下表 |
|
|
|
|
|
+| `persistence_context` | `NONE` 时传 `NULL`;`DEFAULT` 时为持久化目录(传 `NULL` 表示工作目录) |
|
|
|
|
|
+
|
|
|
|
|
+| persistence_type | 说明 |
|
|
|
|
|
+| ---------------- | ---- |
|
|
|
|
|
+| `MQTTCLIENT_PERSISTENCE_NONE` | 内存持久化,系统故障时传输中消息状态可能丢失 |
|
|
|
|
|
+| `MQTTCLIENT_PERSISTENCE_DEFAULT` | 默认(基于文件系统)持久化,提供一定防丢失保护 |
|
|
|
|
|
+| `MQTTCLIENT_PERSISTENCE_USER` | 应用自定义持久化实现 |
|
|
|
|
|
+
|
|
|
|
|
+返回值:成功 `MQTTCLIENT_SUCCESS`,失败返回错误码。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+MQTTClient client;
|
|
|
|
|
+int rc;
|
|
|
|
|
+if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_create(&client, "192.168.6.161", "dt_mqtt_2_id",
|
|
|
|
|
+ MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
|
|
|
|
|
+ printf("Failed to create client, return code %d\n", rc);
|
|
|
|
|
+ return EXIT_FAILURE;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5.3 连接服务端
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+int MQTTClient_connect(MQTTClient handle, MQTTClient_connectOptions *options);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+typedef struct
|
|
|
|
|
+{
|
|
|
|
|
+ int keepAliveInterval; // keepAlive
|
|
|
|
|
+ int cleansession; // cleanSession
|
|
|
|
|
+ MQTTClient_willOptions *will; // 遗嘱相关
|
|
|
|
|
+ const char *username; // 用户名
|
|
|
|
|
+ const char *password; // 密码
|
|
|
|
|
+ int reliable; // 同步/异步发布控制(默认 1=同步)
|
|
|
|
|
+ /* ... */
|
|
|
|
|
+} MQTTClient_connectOptions;
|
|
|
|
|
+
|
|
|
|
|
+typedef struct
|
|
|
|
|
+{
|
|
|
|
|
+ const char *topicName; // 遗嘱主题
|
|
|
|
|
+ const char *message; // 遗嘱内容
|
|
|
|
|
+ int retained; // 遗嘱消息的保留标志
|
|
|
|
|
+ int qos; // 遗嘱消息的 QoS 等级
|
|
|
|
|
+ /* ... */
|
|
|
|
|
+} MQTTClient_willOptions;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+返回值错误码与 CONNACK 的连接返回码一致(1~5)。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
|
|
|
|
|
+MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
|
|
|
|
|
+
|
|
|
|
|
+will_opts.topicName = "dt_mqtt/willTopic";
|
|
|
|
|
+will_opts.message = "Abnormally dropped";
|
|
|
|
|
+will_opts.retained = 1;
|
|
|
|
|
+will_opts.qos = 0;
|
|
|
|
|
+
|
|
|
|
|
+conn_opts.will = &will_opts;
|
|
|
|
|
+conn_opts.keepAliveInterval = 30;
|
|
|
|
|
+conn_opts.cleansession = 0;
|
|
|
|
|
+conn_opts.username = "mqtt1";
|
|
|
|
|
+conn_opts.password = "123456";
|
|
|
|
|
+
|
|
|
|
|
+if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_connect(client, &conn_opts))) {
|
|
|
|
|
+ printf("Failed to connect, return code %d\n", rc);
|
|
|
|
|
+ return EXIT_FAILURE;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5.4 设置回调
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+int MQTTClient_setCallbacks(MQTTClient handle,
|
|
|
|
|
+ void *context,
|
|
|
|
|
+ MQTTClient_connectionLost *cl,
|
|
|
|
|
+ MQTTClient_messageArrived *ma,
|
|
|
|
|
+ MQTTClient_deliveryComplete *dc);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+| 回调 | 类型 | 说明 |
|
|
|
|
|
+| ---- | ---- | ---- |
|
|
|
|
|
+| `cl` | `void (void *context, char *cause)` | 断线时执行;设 `NULL` 表示不处理断线 |
|
|
|
|
|
+| `ma` | `int (void *context, char *topicName, int topicLen, MQTTClient_message *message)` | 收到消息时执行,**必须设置** |
|
|
|
|
|
+| `dc` | `void (void *context, MQTTClient_deliveryToken dt)` | 发布确认时执行,可设 `NULL` |
|
|
|
|
|
+
|
|
|
|
|
+`ma` 回调的两个关键点:
|
|
|
|
|
+
|
|
|
|
|
+- 退出前必须释放内存:`MQTTClient_freeMessage(&message)` 与 `MQTTClient_free(topicName)`。
|
|
|
|
|
+- 返回值必须是 0 或 1:返回 1 表示处理成功;返回 0 表示处理有问题,客户端库会**重新投递**,此时不要释放内存,否则重新投递失败。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+static void delivered(void *context, MQTTClient_deliveryToken dt)
|
|
|
|
|
+{
|
|
|
|
|
+ printf("Message with token value %d delivery confirmed\n", dt);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static int msgarrvd(void *context, char *topicName, int topicLen,
|
|
|
|
|
+ MQTTClient_message *message)
|
|
|
|
|
+{
|
|
|
|
|
+ printf("Message arrived\n");
|
|
|
|
|
+ printf("topic: %s\n", topicName);
|
|
|
|
|
+ printf("message: <%d>%s\n", message->payloadlen, (char *)message->payload);
|
|
|
|
|
+ MQTTClient_freeMessage(&message);
|
|
|
|
|
+ MQTTClient_free(topicName);
|
|
|
|
|
+ return 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static void connlost(void *context, char *cause)
|
|
|
|
|
+{
|
|
|
|
|
+ printf("\nConnection lost\n");
|
|
|
|
|
+ printf(" cause: %s \n", cause);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> `MQTTClient_setCallbacks` 必须在连接服务器**之前**调用。`reliable`(默认 1)控制同步/异步发布:`reliable=1` 时同步发布,必须完成当前消息确认后才能发布下一条。
|
|
|
|
|
+
|
|
|
|
|
+### 5.5 发布消息
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+int MQTTClient_publishMessage(MQTTClient handle,
|
|
|
|
|
+ const char *topicName,
|
|
|
|
|
+ MQTTClient_message *msg,
|
|
|
|
|
+ MQTTClient_deliveryToken *dt);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+MQTTClient_message pubmsg = MQTTClient_message_initializer;
|
|
|
|
|
+MQTTClient_deliveryToken token;
|
|
|
|
|
+
|
|
|
|
|
+pubmsg.payload = "online";
|
|
|
|
|
+pubmsg.payloadlen = 6;
|
|
|
|
|
+pubmsg.qos = 0;
|
|
|
|
|
+pubmsg.retained = 1;
|
|
|
|
|
+if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_publishMessage(client, "dt_mqtt/testTopic", &pubmsg, &token))) {
|
|
|
|
|
+ printf("Failed to publish message, return code %d\n", rc);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5.6 订阅 / 取消订阅 / 断开 / 销毁
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+int MQTTClient_subscribe(MQTTClient handle, const char *topic, int qos);
|
|
|
|
|
+int MQTTClient_unsubscribe(MQTTClient handle, const char *topic);
|
|
|
|
|
+int MQTTClient_disconnect(MQTTClient handle, int timeout); // timeout 单位毫秒
|
|
|
|
|
+void MQTTClient_destroy(MQTTClient *handle);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+MQTTClient_subscribe(client, "dt_mqtt/testTopic", 0);
|
|
|
|
|
+MQTTClient_unsubscribe(client, "dt_mqtt/testTopic");
|
|
|
|
|
+MQTTClient_disconnect(client, 10000);
|
|
|
|
|
+MQTTClient_destroy(&client);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 6. 完整可复制工程:mqtt_prj
|
|
|
|
|
+
|
|
|
|
|
+工程路径:开发板光盘 → `11、Linux C 应用编程例程源码` → `33_mqtt/mqtt_prj`。功能设计:
|
|
|
|
|
+
|
|
|
|
|
+- 基于 mosquitto 搭建的本地服务器,实现个人物联网小项目;
|
|
|
|
|
+- 手机/电脑可远程控制开发板上的一颗 LED;
|
|
|
|
|
+- 开发板每隔 30 秒向服务端发布 SoC 温度,手机/电脑可订阅查看。
|
|
|
|
|
+
|
|
|
|
|
+### 6.1 工程目录结构
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+mqtt_prj/
|
|
|
|
|
+├── CMakeLists.txt # 顶层 CMakeLists
|
|
|
|
|
+├── cmake/
|
|
|
|
|
+│ └── arm-linux-setup.cmake # 交叉编译工具链文件
|
|
|
|
|
+├── mqttClient.c # 客户端应用程序
|
|
|
|
|
+└── build/ # out-of-source 构建目录
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 6.2 `cmake/arm-linux-setup.cmake`
|
|
|
|
|
+
|
|
|
|
|
+```cmake
|
|
|
|
|
+##################################
|
|
|
|
|
+# 配置 ARM 交叉编译
|
|
|
|
|
+#################################
|
|
|
|
|
+set(CMAKE_SYSTEM_NAME Linux) #设置目标系统名字
|
|
|
|
|
+set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构
|
|
|
|
|
+
|
|
|
|
|
+# 指定编译器的sysroot路径
|
|
|
|
|
+set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
|
|
|
|
|
+set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
|
|
|
|
|
+
|
|
|
|
|
+# 指定交叉编译器arm-linux-gcc
|
|
|
|
|
+set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
|
|
|
|
|
+
|
|
|
|
|
+# 为编译器添加编译选项
|
|
|
|
|
+set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
|
|
|
|
|
+
|
|
|
|
|
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
|
|
|
|
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
|
|
|
|
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
|
|
|
|
+#################################
|
|
|
|
|
+# end
|
|
|
|
|
+##################################
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 6.3 `CMakeLists.txt`
|
|
|
|
|
+
|
|
|
|
|
+```cmake
|
|
|
|
|
+cmake_minimum_required(VERSION 2.8.12)
|
|
|
|
|
+project(MQTTClient C)
|
|
|
|
|
+message(STATUS "CMake version: " ${CMAKE_VERSION})
|
|
|
|
|
+message(STATUS "CMake system name: " ${CMAKE_SYSTEM_NAME})
|
|
|
|
|
+message(STATUS "CMake system processor: " ${CMAKE_SYSTEM_PROCESSOR})
|
|
|
|
|
+
|
|
|
|
|
+# 设置可执行文件输出路径
|
|
|
|
|
+set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
|
|
|
|
|
+
|
|
|
|
|
+# 定义可执行文件目标
|
|
|
|
|
+add_executable(mqttClient mqttClient.c)
|
|
|
|
|
+
|
|
|
|
|
+# 指定MQTT客户端库头文件路径、库路径以及链接库
|
|
|
|
|
+# ***大家需要根据MQTT的实际安装路径设置***
|
|
|
|
|
+target_include_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/include) #MQTT头文件搜索路径
|
|
|
|
|
+target_link_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/lib) #MQTT库文件搜索路径
|
|
|
|
|
+target_link_libraries(mqttClient PRIVATE paho-mqtt3c) #MQTT链接库 libpaho-mqtt3c.so
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+逐段解释:
|
|
|
|
|
+
|
|
|
|
|
+- `cmake_minimum_required(VERSION 2.8.12)`:设置最低 CMake 版本要求。
|
|
|
|
|
+- `project(MQTTClient C)`:工程名为 `MQTTClient`,语言 C。
|
|
|
|
|
+- `message(STATUS ...)`:打印 CMake 版本、目标系统名、目标处理器,确认交叉编译配置生效。
|
|
|
|
|
+- `set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)`:可执行文件输出到 `build/bin`。
|
|
|
|
|
+- `add_executable(mqttClient mqttClient.c)`:定义目标 `mqttClient`。
|
|
|
|
|
+- `target_include_directories(... PRIVATE ...)`:只给该目标加 MQTT 头文件搜索路径。
|
|
|
|
|
+- `target_link_directories(... PRIVATE ...)`:只给该目标加 MQTT 库搜索路径。
|
|
|
|
|
+- `target_link_libraries(mqttClient PRIVATE paho-mqtt3c)`:链接同步模式 MQTT 库。
|
|
|
|
|
+
|
|
|
|
|
+### 6.4 `mqttClient.c`(完整源码)
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#include <stdio.h>
|
|
|
|
|
+#include <stdlib.h>
|
|
|
|
|
+#include <string.h>
|
|
|
|
|
+#include <unistd.h>
|
|
|
|
|
+#include <sys/types.h>
|
|
|
|
|
+#include <sys/stat.h>
|
|
|
|
|
+#include <fcntl.h>
|
|
|
|
|
+#include "MQTTClient.h" //包含MQTT客户端库头文件
|
|
|
|
|
+
|
|
|
|
|
+/* ########################宏定义##################### */
|
|
|
|
|
+/*
|
|
|
|
|
+ * 因为本地服务器是搭建在您的ubuntu上,所以此处的服务器地址
|
|
|
|
|
+ * 要填写您ubuntu的ip地址,笔者ubuntu的ip地址为192.168.6.161,
|
|
|
|
|
+ * 你们填写自己的ip地址即可
|
|
|
|
|
+ */
|
|
|
|
|
+#define BROKER_ADDRESS "192.168.6.161" //服务器地址
|
|
|
|
|
+
|
|
|
|
|
+/*
|
|
|
|
|
+ * 客户端id、用户名、密码 *
|
|
|
|
|
+ * 客户端id可以随意填写,只需要保证跟MQTT.fx客户端使用的不同,
|
|
|
|
|
+ * 用户名要使用MQTT服务端注册的用户,即 mqtt1
|
|
|
|
|
+ * 密码也是之前设置的 123456
|
|
|
|
|
+ */
|
|
|
|
|
+#define CLIENTID "test3" //客户端id
|
|
|
|
|
+#define USERNAME "mqtt1" //用户名
|
|
|
|
|
+#define PASSWORD "123456" //密码
|
|
|
|
|
+
|
|
|
|
|
+/*
|
|
|
|
|
+ * 以下 dt_mqtt/ 便是笔者的个人主题级别
|
|
|
|
|
+ */
|
|
|
|
|
+#define WILL_TOPIC "dt_mqtt/will" //遗嘱主题
|
|
|
|
|
+#define LED_TOPIC "dt_mqtt/led" //LED主题
|
|
|
|
|
+#define TEMP_TOPIC "dt_mqtt/temperature" //温度主题
|
|
|
|
|
+/* ################################################# */
|
|
|
|
|
+
|
|
|
|
|
+static int msgarrvd(void *context, char *topicName, int topicLen,
|
|
|
|
|
+ MQTTClient_message *message)
|
|
|
|
|
+{
|
|
|
|
|
+ if (!strcmp(topicName, LED_TOPIC)) { //校验消息的主题
|
|
|
|
|
+ if (!strcmp("2", message->payload)) //如果接收到的消息是"2"则设置LED为呼吸灯模式
|
|
|
|
|
+ system("echo heartbeat > /sys/class/leds/sys-led/trigger");
|
|
|
|
|
+ if (!strcmp("1", message->payload)) { //如果是"1"则LED常亮
|
|
|
|
|
+ system("echo none > /sys/class/leds/sys-led/trigger");
|
|
|
|
|
+ system("echo 1 > /sys/class/leds/sys-led/brightness");
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (!strcmp("0", message->payload)) { //如果是"0"则LED熄灭
|
|
|
|
|
+ system("echo none > /sys/class/leds/sys-led/trigger");
|
|
|
|
|
+ system("echo 0 > /sys/class/leds/sys-led/brightness");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 接收到其它数据 不做处理
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 释放占用的内存空间 */
|
|
|
|
|
+ MQTTClient_freeMessage(&message);
|
|
|
|
|
+ MQTTClient_free(topicName);
|
|
|
|
|
+
|
|
|
|
|
+ /* 退出 */
|
|
|
|
|
+ return 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static void connlost(void *context, char *cause)
|
|
|
|
|
+{
|
|
|
|
|
+ printf("\nConnection lost\n");
|
|
|
|
|
+ printf(" cause: %s \n", cause);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+int main(int argc, char *argv[])
|
|
|
|
|
+{
|
|
|
|
|
+ MQTTClient client;
|
|
|
|
|
+ MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
|
|
|
|
|
+ MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
|
|
|
|
|
+ MQTTClient_message pubmsg = MQTTClient_message_initializer;
|
|
|
|
|
+ int rc;
|
|
|
|
|
+
|
|
|
|
|
+ /* 创建mqtt客户端对象 */
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_create(&client, BROKER_ADDRESS, CLIENTID,
|
|
|
|
|
+ MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
|
|
|
|
|
+ printf("Failed to create client, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ goto exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 设置回调 */
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_setCallbacks(client, NULL, connlost,
|
|
|
|
|
+ msgarrvd, NULL))) {
|
|
|
|
|
+ printf("Failed to set callbacks, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ goto destroy_exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 连接MQTT服务器 */
|
|
|
|
|
+ will_opts.topicName = WILL_TOPIC; //遗嘱主题
|
|
|
|
|
+ will_opts.message = "Unexpected disconnection"; //遗嘱消息
|
|
|
|
|
+ will_opts.retained = 1; //保留消息
|
|
|
|
|
+ will_opts.qos = 0; //QoS0
|
|
|
|
|
+
|
|
|
|
|
+ conn_opts.will = &will_opts;
|
|
|
|
|
+ conn_opts.keepAliveInterval = 30; //心跳包间隔时间
|
|
|
|
|
+ conn_opts.cleansession = 0; //cleanSession标志
|
|
|
|
|
+ conn_opts.username = USERNAME; //用户名
|
|
|
|
|
+ conn_opts.password = PASSWORD; //密码
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_connect(client, &conn_opts))) {
|
|
|
|
|
+ printf("Failed to connect, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ goto destroy_exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ printf("MQTT服务器连接成功!\n");
|
|
|
|
|
+
|
|
|
|
|
+ /* 发布上线消息 */
|
|
|
|
|
+ pubmsg.payload = "Online"; //消息的内容
|
|
|
|
|
+ pubmsg.payloadlen = 6; //内容的长度
|
|
|
|
|
+ pubmsg.qos = 0; //QoS等级
|
|
|
|
|
+ pubmsg.retained = 1; //保留消息
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_publishMessage(client, WILL_TOPIC, &pubmsg, NULL))) {
|
|
|
|
|
+ printf("Failed to publish message, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ goto disconnect_exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 订阅主题 dt_mqtt/led */
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_subscribe(client, LED_TOPIC, 0))) {
|
|
|
|
|
+ printf("Failed to subscribe, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ goto disconnect_exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 向服务端发布芯片温度信息 */
|
|
|
|
|
+ for ( ; ; ) {
|
|
|
|
|
+
|
|
|
|
|
+ MQTTClient_message tempmsg = MQTTClient_message_initializer;
|
|
|
|
|
+ char temp_str[10] = {0};
|
|
|
|
|
+ int fd;
|
|
|
|
|
+
|
|
|
|
|
+ /* 读取温度值 */
|
|
|
|
|
+ fd = open("/sys/class/thermal/thermal_zone0/temp", O_RDONLY);
|
|
|
|
|
+ read(fd, temp_str, sizeof(temp_str)); //读取temp属性文件即可获取温度
|
|
|
|
|
+ close(fd);
|
|
|
|
|
+
|
|
|
|
|
+ /* 发布温度信息 */
|
|
|
|
|
+ tempmsg.payload = temp_str; //消息的内容
|
|
|
|
|
+ tempmsg.payloadlen = strlen(temp_str); //内容的长度
|
|
|
|
|
+ tempmsg.qos = 0; //QoS等级
|
|
|
|
|
+ tempmsg.retained = 1; //保留消息
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_publishMessage(client, TEMP_TOPIC, &tempmsg, NULL))) {
|
|
|
|
|
+ printf("Failed to publish message, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ goto unsubscribe_exit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sleep(30); //每隔30秒 更新一次数据
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+unsubscribe_exit:
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_unsubscribe(client, LED_TOPIC))) {
|
|
|
|
|
+ printf("Failed to unsubscribe, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ }
|
|
|
|
|
+disconnect_exit:
|
|
|
|
|
+ if (MQTTCLIENT_SUCCESS !=
|
|
|
|
|
+ (rc = MQTTClient_disconnect(client, 10000))) {
|
|
|
|
|
+ printf("Failed to disconnect, return code %d\n", rc);
|
|
|
|
|
+ rc = EXIT_FAILURE;
|
|
|
|
|
+ }
|
|
|
|
|
+destroy_exit:
|
|
|
|
|
+ MQTTClient_destroy(&client);
|
|
|
|
|
+exit:
|
|
|
|
|
+ return rc;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+三个主题的作用:
|
|
|
|
|
+
|
|
|
|
|
+| 宏 | 主题 | 作用 |
|
|
|
|
|
+| -- | ---- | ---- |
|
|
|
|
|
+| `WILL_TOPIC` | `dt_mqtt/will` | 遗嘱主题,正常上线时发布 "Online",意外断线时由服务端发布遗嘱消息 |
|
|
|
|
|
+| `LED_TOPIC` | `dt_mqtt/led` | 开发板订阅;收到 "0"/"1"/"2" 分别控制 LED 熄灭/常亮/呼吸灯 |
|
|
|
|
|
+| `TEMP_TOPIC` | `dt_mqtt/temperature` | 开发板每 30 秒发布一次 SoC 温度(读 `/sys/class/thermal/thermal_zone0/temp`) |
|
|
|
|
|
+
|
|
|
|
|
+### 6.5 构建与编译
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd mqtt_prj/build
|
|
|
|
|
+~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake \
|
|
|
|
|
+ -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
|
|
|
|
|
+ -DCMAKE_BUILD_TYPE=Release ..
|
|
|
|
|
+make
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+编译成功后生成 `build/bin/mqttClient`,拷贝到开发板 `/home/root`。
|
|
|
|
|
+
|
|
|
|
|
+### 6.6 测试演示
|
|
|
|
|
+
|
|
|
|
|
+1. 确保开发板与 Ubuntu 在同一局域网、能互相 `ping` 通。
|
|
|
|
|
+2. 开发板执行 `./mqttClient`,打印"MQTT服务器连接成功!"。
|
|
|
|
|
+3. 用 MQTT.fx(Client ID 用 `test1`)连接服务器,订阅 `dt_mqtt/temperature`,立即收到保留温度值,之后每 30 秒收到一次。
|
|
|
|
|
+4. 向 `dt_mqtt/led` 发布 `0`/`1`/`2`,观察开发板 LED 熄灭/常亮/呼吸灯。
|
|
|
|
|
+5. 也可用手机 MQTT Client(Client ID 不能重复)订阅温度、发布 LED 控制。
|
|
|
|
|
+
|
|
|
|
|
+## 7. 扩展:JSON 数据处理与云平台接入
|
|
|
|
|
+
|
|
|
|
|
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。教材示例仅上报纯文本温度值,接入真实云平台时通常改为 JSON 格式并增加设备认证。
|
|
|
|
|
+
|
|
|
|
|
+**JSON 数据处理(cJSON / parson)**:物联网平台多要求负载为 JSON。可在开发板端引入轻量 JSON 库(如 cJSON、parson),把温度、状态打包成 JSON 发布,订阅端再解析。典型负载:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{ "temp": 45, "led": 1, "ts": 1730000000 }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+cJSON 的典型用法(需自行移植该库并链接):
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+/* 扩展示例,非教材内容 */
|
|
|
|
|
+cJSON *root = cJSON_CreateObject();
|
|
|
|
|
+cJSON_AddNumberToObject(root, "temp", temperature);
|
|
|
|
|
+cJSON_AddNumberToObject(root, "led", led_state);
|
|
|
|
|
+char *out = cJSON_PrintUnformatted(root);
|
|
|
|
|
+/* out 作为 pubmsg.payload 发布,payloadlen = strlen(out) */
|
|
|
|
|
+cJSON_Delete(root);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**原子云接入**:正点原子原子云提供 MQTT 接入(设备三元组:产品 ID / 设备名 / 设备密钥),流程是先用三元组连接云平台,再按平台规定的主题上下行数据。具体主题、鉴权算法以原子云官方文档为准,此处不展开。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+# 第二部分:视频监控
|
|
|
|
|
+
|
|
|
|
|
+## 8. 方案概览
|
|
|
|
|
+
|
|
|
|
|
+常见视频监控/直播使用 **RTMP** 和 **RTSP** 流媒体传输协议:
|
|
|
|
|
+
|
|
|
|
|
+| 协议 | 提出方 | 特点 | 适用 |
|
|
|
|
|
+| ---- | ------ | ---- | ---- |
|
|
|
|
|
+| RTSP(Real-Time Stream Protocol) | Real Networks 与 Netscape | 基于文本的多媒体播放控制协议,定义流格式,流数据经 RTP 传输;实时性最好 | 视频聊天、视频监控 |
|
|
|
|
|
+| RTMP(Real Time Message Protocol) | Adobe | 解决流媒体多路复用与分包,低延迟、稳定性高,支持所有摄像头格式,浏览器加载 flash 插件可直接播放 | 直播、推流 |
|
|
|
|
|
+
|
|
|
|
|
+选择:RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、生态成熟。本章用 **FFmpeg + Nginx** 通过 RTMP 推流实现视频监控/直播。
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+flowchart LR
|
|
|
|
|
+ Push["推流端<br/>开发板 (FFmpeg)"] -->|"RTMP 推流"| Srv["流媒体服务器<br/>Nginx + nginx-rtmp-module"]
|
|
|
|
|
+ Srv -->|"RTMP 拉流"| Pull["拉流端<br/>Windows VLC"]
|
|
|
|
|
+
|
|
|
|
|
+ classDef client fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
|
|
|
|
|
+ classDef server fill:#fef9c3,stroke:#ca8a04,color:#713f12
|
|
|
|
|
+ class Push,Pull client
|
|
|
|
|
+ class Srv server
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+三部分:推流客户端(FFmpeg)、流媒体服务器(Nginx + RTMP 模块)、拉流客户端(VLC)。
|
|
|
|
|
+
|
|
|
|
|
+## 9. Nginx 移植
|
|
|
|
|
+
|
|
|
|
|
+> 说明:本方案中开发板既是流媒体服务器又是推流端。出厂系统通常已移植好 Nginx 并在启动时自动运行,这里为了演示,重新手动移植。
|
|
|
|
|
+
|
|
|
|
|
+### 9.1 下载源码与 RTMP 模块
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+wget http://nginx.org/download/nginx-1.20.0.tar.gz
|
|
|
|
|
+git clone https://github.com/arut/nginx-rtmp-module.git
|
|
|
|
|
+tar -xzf nginx-1.20.0.tar.gz
|
|
|
|
|
+cd nginx-1.20.0
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+原生 Nginx 不支持 RTMP,需要第三方模块 `nginx-rtmp-module`。
|
|
|
|
|
+
|
|
|
|
|
+### 9.2 交叉编译前的源码修改
|
|
|
|
|
+
|
|
|
|
|
+初始化交叉编译环境:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+修改两处源码,否则配置通不过:
|
|
|
|
|
+
|
|
|
|
|
+| 文件 | 位置 | 修改 |
|
|
|
|
|
+| ---- | ---- | ---- |
|
|
|
|
|
+| `auto/cc/name` | 第 21 行 | 注释掉 `exit 1` |
|
|
|
|
|
+| `auto/types/sizeof` | 第 15 行 | `ngx_size=` 改为 `ngx_size=4` |
|
|
|
|
|
+| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc` |
|
|
|
|
|
+
|
|
|
|
|
+### 9.3 配置、编译、安装
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+./configure --prefix=/home/dt/tools/nginx-1.20.0/install \
|
|
|
|
|
+ --with-http_ssl_module \
|
|
|
|
|
+ --with-http_mp4_module \
|
|
|
|
|
+ --with-http_v2_module \
|
|
|
|
|
+ --without-http_upstream_zone_module \
|
|
|
|
|
+ --add-module=/home/dt/tools/nginx-rtmp-module
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `--prefix`:Nginx 安装路径。
|
|
|
|
|
+- `--add-module`:添加第三方模块(指向 `nginx-rtmp-module` 源码路径)。
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+make
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+首次 `make` 会报错,此时编辑 `objs/ngx_auto_config.h`,添加:
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#ifndef NGX_HAVE_SYSVSHM
|
|
|
|
|
+#define NGX_HAVE_SYSVSHM 1
|
|
|
|
|
+#endif
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+再次 `make` 即可编译成功,然后:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+make install
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 9.4 部署到开发板
|
|
|
|
|
+
|
|
|
|
|
+安装目录下:
|
|
|
|
|
+
|
|
|
|
|
+- `conf/`:配置文件,重点是 `nginx.conf`;
|
|
|
|
|
+- `sbin/nginx`:可执行程序("not stripped",约 7.5MB,可执行 `arm-poky-linux-gnueabi-strip --strip-debug nginx` 去掉调试信息);
|
|
|
|
|
+- `logs/`、`html/`。
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 开发板上先移除出厂系统自带的 nginx
|
|
|
|
|
+rm -rf /usr/sbin/nginx
|
|
|
|
|
+rm -rf /etc/nginx/*
|
|
|
|
|
+
|
|
|
|
|
+# 拷贝新编译的 nginx 到 /home/root
|
|
|
|
|
+# 拷贝 conf、logs、html 到 /etc/nginx
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 9.5 测试 Nginx
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+./nginx -V # 查看版本信息
|
|
|
|
|
+./nginx -h # 查看帮助
|
|
|
|
|
+./nginx -p /etc/nginx # 启动(-p 指定前缀路径)
|
|
|
|
|
+ps -aux # 查看服务进程
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+在电脑浏览器输入开发板 IP,出现 "welcome to nginx" 即正常。
|
|
|
|
|
+
|
|
|
|
|
+### 9.6 配置 RTMP
|
|
|
|
|
+
|
|
|
|
|
+编辑 `/etc/nginx/conf/nginx.conf`,添加:
|
|
|
|
|
+
|
|
|
|
|
+```conf
|
|
|
|
|
+rtmp {
|
|
|
|
|
+ server {
|
|
|
|
|
+ listen 1935; # 监听 1935 端口
|
|
|
|
|
+ chunk_size 4096;
|
|
|
|
|
+
|
|
|
|
|
+ application live{
|
|
|
|
|
+ allow publish 127.0.0.1;
|
|
|
|
|
+ allow play all;
|
|
|
|
|
+ live on; # 打开直播
|
|
|
|
|
+ record off; # 关闭 record
|
|
|
|
|
+ meta copy;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ application hls {
|
|
|
|
|
+ live on;
|
|
|
|
|
+ hls on;
|
|
|
|
|
+ hls_path /tmp/hls;
|
|
|
|
|
+ hls_fragment 8s;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+./nginx -p /etc/nginx -s reload # 重载配置
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 10. 用 FFmpeg 推流、VLC 拉流
|
|
|
|
|
+
|
|
|
|
|
+### 10.1 推送视频文件
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+ffmpeg -re -i /run/media/mmcblk0p1/testVideo.mp4 -c:av copy -f flv rtmp://127.0.0.1/live/mytest
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `-re`:按原始帧率读取(实时推流)。
|
|
|
|
|
+- `-i`:输入视频文件。
|
|
|
|
|
+- `rtmp://127.0.0.1/live/mytest`:推给本机流媒体服务器的 `live` 应用,流名 `mytest`(服务器与推流端都是开发板,故 IP 为 127.0.0.1)。
|
|
|
|
|
+
|
|
|
|
|
+拉流:Windows 上打开 VLC → 媒体 → 打开网络串流 → 输入 `rtmp://<开发板IP>/live/mytest` → 播放,既有画面也有声音。
|
|
|
|
|
+
|
|
|
|
|
+### 10.2 推送摄像头(USB 摄像头)
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtmp://127.0.0.1/live/mytest
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `-f v4l2`:使用 V4L2 采集。
|
|
|
|
|
+- `-video_size 320x240 -framerate 15`:分辨率与帧率。
|
|
|
|
|
+- `-i /dev/video2`:摄像头设备节点(USB 摄像头编号需按实际确认)。
|
|
|
|
|
+- `-q 10`:视频质量参数。
|
|
|
|
|
+- `-f flv`:输出 FLV 封装,便于 RTMP 传输。
|
|
|
|
|
+
|
|
|
|
|
+在 Windows 下用 VLC 拉流即可看到摄像头画面。
|
|
|
|
|
+
|
|
|
|
|
+### 10.3 延迟分析
|
|
|
|
|
+
|
|
|
|
|
+实测延迟约 5~6 秒,原因:
|
|
|
|
|
+
|
|
|
|
|
+- I.MX6U 没有硬件视频解码,FFmpeg 内部对音视频的处理全靠软件,耗时大;
|
|
|
|
|
+- 服务器和推流端都在同一块性能较弱的开发板上,负担更重。
|
|
|
|
|
+
|
|
|
|
|
+结论:普通开发板软编解码能力有限,追求低延迟应使用带硬件编解码的更高性能平台。
|
|
|
|
|
+
|
|
|
|
|
+## 11. 方案二:V4L2 + JPEG 广播的局域网 MJPEG 监控
|
|
|
|
|
+
|
|
|
|
|
+> 该实现来自本知识库配套的 Qt 例程 `Embedded-Qt-Tutorial/Qt/04/05_video_surveillance`,与 PDF 第三十四章的 Nginx 方案是两条不同路线:**Nginx 方案**适合广域/公网推拉流,**本方案**适合开发板与客户端同网段的轻量预览,结构简单、无需服务器进程。
|
|
|
|
|
+> 摄像头采集部分"请参考正点原子 C 应用编程 V4L2 章节"(源码注释原文)。
|
|
|
|
|
+
|
|
|
|
|
+### 11.1 整体结构
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+flowchart LR
|
|
|
|
|
+ subgraph Server["video_server(开发板)"]
|
|
|
|
|
+ V4L2["/dev/video1<br/>V4L2 采集 RGB565"] --> QImg["QImage (640x480)"]
|
|
|
|
|
+ QImg -->|"save JPEG"| B64["base64 编码"]
|
|
|
|
|
+ B64 -->|"UDP 广播 :8888"| Net["QUdpSocket"]
|
|
|
|
|
+ end
|
|
|
|
|
+ subgraph Client["video_client(PC / 开发板)"]
|
|
|
|
|
+ Rcv["QUdpSocket bind :8888"] --> Dec["base64 解码 + loadFromData"]
|
|
|
|
|
+ Dec --> Show["QLabel 显示"]
|
|
|
|
|
+ end
|
|
|
|
|
+ Net --> Rcv
|
|
|
|
|
+
|
|
|
|
|
+ classDef dev fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
|
|
|
|
|
+ classDef pc fill:#dcfce7,stroke:#16a34a,color:#14532d
|
|
|
|
|
+ class V4L2,QImg,B64,Net dev
|
|
|
|
|
+ class Rcv,Dec,Show pc
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- 服务端:V4L2 采集一帧 RGB565 → 转 `QImage` → 编码 JPEG → base64 → UDP 广播到 8888 端口。
|
|
|
|
|
+- 客户端:绑定 8888 端口接收 UDP 数据报 → base64 解码 → 从数据加载 `QImage` → 显示。
|
|
|
|
|
+
|
|
|
|
|
+### 11.2 服务端采集线程 `capture_thread.h`
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+#ifndef CAPTURE_THREAD_H
|
|
|
|
|
+#define CAPTURE_THREAD_H
|
|
|
|
|
+
|
|
|
|
|
+#include <sys/types.h>
|
|
|
|
|
+#include <sys/stat.h>
|
|
|
|
|
+#include <fcntl.h>
|
|
|
|
|
+#include <stdio.h>
|
|
|
|
|
+#include <unistd.h>
|
|
|
|
|
+#include <string.h>
|
|
|
|
|
+#include <pthread.h>
|
|
|
|
|
+#ifdef linux
|
|
|
|
|
+#include <linux/fb.h>
|
|
|
|
|
+#include <sys/ioctl.h>
|
|
|
|
|
+#include <sys/mman.h>
|
|
|
|
|
+#include <linux/videodev2.h>
|
|
|
|
|
+#include <linux/input.h>
|
|
|
|
|
+#endif
|
|
|
|
|
+
|
|
|
|
|
+#include <QThread>
|
|
|
|
|
+#include <QDebug>
|
|
|
|
|
+#include <QPushButton>
|
|
|
|
|
+#include <QImage>
|
|
|
|
|
+#include <QByteArray>
|
|
|
|
|
+#include <QBuffer>
|
|
|
|
|
+#include <QTime>
|
|
|
|
|
+#include <QUdpSocket>
|
|
|
|
|
+
|
|
|
|
|
+#define VIDEO_DEV "/dev/video1"
|
|
|
|
|
+#define FB_DEV "/dev/fb0"
|
|
|
|
|
+#define VIDEO_BUFFER_COUNT 3
|
|
|
|
|
+
|
|
|
|
|
+struct buffer_info {
|
|
|
|
|
+ void *start;
|
|
|
|
|
+ unsigned int length;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+class CaptureThread : public QThread
|
|
|
|
|
+{
|
|
|
|
|
+ Q_OBJECT
|
|
|
|
|
+
|
|
|
|
|
+signals:
|
|
|
|
|
+ void imageReady(QImage);
|
|
|
|
|
+ void sendImage(QImage);
|
|
|
|
|
+
|
|
|
|
|
+private:
|
|
|
|
|
+ bool startFlag = false; // 线程开启flag
|
|
|
|
|
+ bool startBroadcast = false; // 开启广播flag
|
|
|
|
|
+ bool startLocalDisplay = false; // 本地显示flag
|
|
|
|
|
+ void run() override;
|
|
|
|
|
+
|
|
|
|
|
+public:
|
|
|
|
|
+ CaptureThread(QObject *parent = nullptr) {
|
|
|
|
|
+ Q_UNUSED(parent);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+public slots:
|
|
|
|
|
+ void setThreadStart(bool start) {
|
|
|
|
|
+ startFlag = start;
|
|
|
|
|
+ if (start) {
|
|
|
|
|
+ if (!this->isRunning())
|
|
|
|
|
+ this->start();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this->quit();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ void setBroadcast(bool start) { startBroadcast = start; }
|
|
|
|
|
+ void setLocalDisplay(bool start) { startLocalDisplay = start; }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+#endif // CAPTURE_THREAD_H
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 11.3 服务端采集线程 `capture_thread.cpp`(V4L2 + UDP 广播核心)
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+#include "capture_thread.h"
|
|
|
|
|
+
|
|
|
|
|
+void CaptureThread::run()
|
|
|
|
|
+{
|
|
|
|
|
+ /* 下面的代码请参考正点原子C应用编程V4L2章节,摄像头编程,这里不作解释 */
|
|
|
|
|
+#ifdef linux
|
|
|
|
|
+#ifndef __arm__
|
|
|
|
|
+ return;
|
|
|
|
|
+#endif
|
|
|
|
|
+ int video_fd = -1;
|
|
|
|
|
+ struct v4l2_format fmt;
|
|
|
|
|
+ struct v4l2_requestbuffers req_bufs;
|
|
|
|
|
+ static struct v4l2_buffer buf;
|
|
|
|
|
+ int n_buf;
|
|
|
|
|
+ struct buffer_info bufs_info[VIDEO_BUFFER_COUNT];
|
|
|
|
|
+ enum v4l2_buf_type type;
|
|
|
|
|
+
|
|
|
|
|
+ video_fd = open(VIDEO_DEV, O_RDWR);
|
|
|
|
|
+ if (0 > video_fd) {
|
|
|
|
|
+ printf("ERROR: failed to open video device %s\n", VIDEO_DEV);
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
+ fmt.fmt.pix.width = 640;
|
|
|
|
|
+ fmt.fmt.pix.height = 480;
|
|
|
|
|
+ fmt.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
|
|
|
|
|
+ fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
|
|
|
|
|
+
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_S_FMT, &fmt)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_S_FMT\n");
|
|
|
|
|
+ close(video_fd);
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req_bufs.count = VIDEO_BUFFER_COUNT;
|
|
|
|
|
+ req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
+ req_bufs.memory = V4L2_MEMORY_MMAP;
|
|
|
|
|
+
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_REQBUFS\n");
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
+ buf.memory = V4L2_MEMORY_MMAP;
|
|
|
|
|
+ for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
|
|
|
|
|
+
|
|
|
|
|
+ buf.index = n_buf;
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_QUERYBUF, &buf)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_QUERYBUF\n");
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ bufs_info[n_buf].length = buf.length;
|
|
|
|
|
+ bufs_info[n_buf].start = mmap(NULL, buf.length,
|
|
|
|
|
+ PROT_READ | PROT_WRITE, MAP_SHARED,
|
|
|
|
|
+ video_fd, buf.m.offset);
|
|
|
|
|
+ if (MAP_FAILED == bufs_info[n_buf].start) {
|
|
|
|
|
+ printf("ERROR: failed to mmap video buffer, size 0x%x\n", buf.length);
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
|
|
|
|
|
+ buf.index = n_buf;
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_QBUF, &buf)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_QBUF\n");
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_STREAMON, &type)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_STREAMON\n");
|
|
|
|
|
+ return ;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ while (startFlag) {
|
|
|
|
|
+
|
|
|
|
|
+ for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
|
|
|
|
|
+
|
|
|
|
|
+ buf.index = n_buf;
|
|
|
|
|
+
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_DQBUF, &buf)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_DQBUF\n");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QImage qImage((unsigned char*)bufs_info[n_buf].start,
|
|
|
|
|
+ fmt.fmt.pix.width, fmt.fmt.pix.height,
|
|
|
|
|
+ QImage::Format_RGB16);
|
|
|
|
|
+
|
|
|
|
|
+ /* 是否开启本地显示,开启本地显示可能会导致开启广播卡顿,它们互相制约 */
|
|
|
|
|
+ if (startLocalDisplay)
|
|
|
|
|
+ emit imageReady(qImage);
|
|
|
|
|
+
|
|
|
|
|
+ /* 是否开启广播,开启广播会导致本地显示卡顿,它们互相制约 */
|
|
|
|
|
+ if (startBroadcast) {
|
|
|
|
|
+ QUdpSocket udpSocket; /* udp套接字 */
|
|
|
|
|
+ QByteArray byte; /* QByteArray类型 */
|
|
|
|
|
+ QBuffer buff(&byte); /* 建立一个用于IO读写的缓冲区 */
|
|
|
|
|
+
|
|
|
|
|
+ qImage.save(&buff, "JPEG", -1); /* image转为byte的类型,再存入buff */
|
|
|
|
|
+ QByteArray base64Byte = byte.toBase64(); /* 转换为base64Byte类型 */
|
|
|
|
|
+
|
|
|
|
|
+ /* 由udpSocket以广播的形式传输数据,端口号为8888 */
|
|
|
|
|
+ udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(),
|
|
|
|
|
+ QHostAddress::Broadcast, 8888);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (0 > ioctl(video_fd, VIDIOC_QBUF, &buf)) {
|
|
|
|
|
+ printf("ERROR: failed to VIDIOC_QBUF\n");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ msleep(800);//at lease 650
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
|
|
|
|
|
+ munmap(bufs_info[i].start, buf.length);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ close(video_fd);
|
|
|
|
|
+#endif
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+V4L2 采集流程(与 C 应用编程 V4L2 章节一致):
|
|
|
|
|
+
|
|
|
|
|
+| 步骤 | ioctl / 操作 | 说明 |
|
|
|
|
|
+| ---- | ------------ | ---- |
|
|
|
|
|
+| 打开设备 | `open(VIDEO_DEV, O_RDWR)` | `/dev/video1` 为摄像头节点(按实际确认) |
|
|
|
|
|
+| 设置格式 | `VIDIOC_S_FMT` | 640×480、`V4L2_PIX_FMT_RGB565` |
|
|
|
|
|
+| 申请缓冲 | `VIDIOC_REQBUFS` | 3 个缓冲区,`V4L2_MEMORY_MMAP` |
|
|
|
|
|
+| 查询并映射 | `VIDIOC_QUERYBUF` + `mmap` | 得到每个缓冲区的地址与长度 |
|
|
|
|
|
+| 入队 | `VIDIOC_QBUF` | 把所有缓冲区放入采集队列 |
|
|
|
|
|
+| 启动流 | `VIDIOC_STREAMON` | 开始采集 |
|
|
|
|
|
+| 循环取帧 | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出一帧、转 `QImage`、处理完再入队 |
|
|
|
|
|
+| 停止 | 循环退出后 `munmap` + `close` | `while(startFlag)` 控制 |
|
|
|
|
|
+
|
|
|
|
|
+广播处理链:`QImage` → `QBuffer`(内存 IO)→ `save(..., "JPEG", -1)` 编码为 JPEG → `toBase64()` → `QUdpSocket::writeDatagram(..., QHostAddress::Broadcast, 8888)`。
|
|
|
|
|
+
|
|
|
|
|
+> 注意:本地显示与广播会互相制约(同一线程里串行处理),源码注释也明确说明开启广播会导致本地显示卡顿,反之亦然。
|
|
|
|
|
+
|
|
|
|
|
+### 11.4 服务端窗口 `mainwindow.cpp`
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+#include "mainwindow.h"
|
|
|
|
|
+
|
|
|
|
|
+MainWindow::MainWindow(QWidget *parent)
|
|
|
|
|
+ : QMainWindow(parent)
|
|
|
|
|
+{
|
|
|
|
|
+ this->setGeometry(0, 0, 800, 480);
|
|
|
|
|
+
|
|
|
|
|
+ videoLabel = new QLabel(this);
|
|
|
|
|
+ videoLabel->setText("未获取到图像数据或未开启本地显示");
|
|
|
|
|
+ videoLabel->setStyleSheet("QWidget {color: white;}");
|
|
|
|
|
+ videoLabel->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ videoLabel->resize(640, 480);
|
|
|
|
|
+
|
|
|
|
|
+ checkBox1 = new QCheckBox(this);
|
|
|
|
|
+ checkBox2 = new QCheckBox(this);
|
|
|
|
|
+ checkBox1->resize(120, 50);
|
|
|
|
|
+ checkBox2->resize(120, 50);
|
|
|
|
|
+ checkBox1->setText("本地显示");
|
|
|
|
|
+ checkBox2->setText("开启广播");
|
|
|
|
|
+
|
|
|
|
|
+ checkBox1->setStyleSheet("QCheckBox {color: yellow;}"
|
|
|
|
|
+ "QCheckBox:indicator {width: 40; height: 40;}");
|
|
|
|
|
+ checkBox2->setStyleSheet("QCheckBox {color: yellow;}"
|
|
|
|
|
+ "QCheckBox:indicator {width: 40; height: 40}");
|
|
|
|
|
+
|
|
|
|
|
+ startCaptureButton = new QPushButton(this);
|
|
|
|
|
+ startCaptureButton->setCheckable(true);
|
|
|
|
|
+ startCaptureButton->setText("开始采集摄像头数据");
|
|
|
|
|
+
|
|
|
|
|
+ QColor color = QColor(Qt::black);
|
|
|
|
|
+ QPalette p;
|
|
|
|
|
+ p.setColor(QPalette::Window, color);
|
|
|
|
|
+ this->setPalette(p);
|
|
|
|
|
+
|
|
|
|
|
+ startCaptureButton->setStyleSheet("QPushButton {background-color: white; border-radius: 30}"
|
|
|
|
|
+ "QPushButton:pressed {background-color: red;}");
|
|
|
|
|
+
|
|
|
|
|
+ captureThread = new CaptureThread(this);
|
|
|
|
|
+
|
|
|
|
|
+ connect(startCaptureButton, SIGNAL(clicked(bool)), captureThread, SLOT(setThreadStart(bool)));
|
|
|
|
|
+ connect(startCaptureButton, SIGNAL(clicked(bool)), this, SLOT(startCaptureButtonClicked(bool)));
|
|
|
|
|
+ connect(captureThread, SIGNAL(imageReady(QImage)), this, SLOT(showImage(QImage)));
|
|
|
|
|
+ connect(checkBox1, SIGNAL(clicked(bool)), captureThread, SLOT(setLocalDisplay(bool)));
|
|
|
|
|
+ connect(checkBox2, SIGNAL(clicked(bool)), captureThread, SLOT(setBroadcast(bool)));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+MainWindow::~MainWindow() {}
|
|
|
|
|
+
|
|
|
|
|
+void MainWindow::showImage(QImage image)
|
|
|
|
|
+{
|
|
|
|
|
+ videoLabel->setPixmap(QPixmap::fromImage(image));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void MainWindow::startCaptureButtonClicked(bool start)
|
|
|
|
|
+{
|
|
|
|
|
+ if (start)
|
|
|
|
|
+ startCaptureButton->setText("停止采集摄像头数据");
|
|
|
|
|
+ else
|
|
|
|
|
+ startCaptureButton->setText("开始采集摄像头数据");
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 11.5 客户端 `mainwindow.cpp`(接收并显示)
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+#include "mainwindow.h"
|
|
|
|
|
+#include <QDebug>
|
|
|
|
|
+
|
|
|
|
|
+MainWindow::MainWindow(QWidget *parent)
|
|
|
|
|
+ : QMainWindow(parent)
|
|
|
|
|
+{
|
|
|
|
|
+ QColor color = QColor(Qt::black);
|
|
|
|
|
+ QPalette p;
|
|
|
|
|
+ p.setColor(QPalette::Window, color);
|
|
|
|
|
+ this->setPalette(p);
|
|
|
|
|
+
|
|
|
|
|
+ udpSocket = new QUdpSocket(this);
|
|
|
|
|
+ udpSocket->bind(QHostAddress::Any, 8888); /* 绑定端口号 */
|
|
|
|
|
+
|
|
|
|
|
+ videoLabel = new QLabel(this);
|
|
|
|
|
+ videoLabel->resize(640, 480);
|
|
|
|
|
+ videoLabel->setText("未获取到图像数据");
|
|
|
|
|
+ videoLabel->setStyleSheet("QWidget {color: white;}");
|
|
|
|
|
+ videoLabel->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ connect(udpSocket, SIGNAL(readyRead()), this, SLOT(videoUpdate()));
|
|
|
|
|
+
|
|
|
|
|
+ this->setGeometry(0, 0, 800, 480);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+MainWindow::~MainWindow() {}
|
|
|
|
|
+
|
|
|
|
|
+void MainWindow::videoUpdate()
|
|
|
|
|
+{
|
|
|
|
|
+ QByteArray datagram;
|
|
|
|
|
+ datagram.resize(udpSocket->pendingDatagramSize()); /* 数据大小重置 */
|
|
|
|
|
+ udpSocket->readDatagram(datagram.data(), datagram.size()); /* 读入datagram */
|
|
|
|
|
+
|
|
|
|
|
+ QByteArray decryptedByte;
|
|
|
|
|
+ decryptedByte = QByteArray::fromBase64(datagram.data()); /* base64 解码 */
|
|
|
|
|
+
|
|
|
|
|
+ QImage image;
|
|
|
|
|
+ image.loadFromData(decryptedByte); /* 从数据加载图像 */
|
|
|
|
|
+
|
|
|
|
|
+ videoLabel->setPixmap(QPixmap::fromImage(image)); /* 显示图像 */
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 11.6 工程文件(`.pro`)
|
|
|
|
|
+
|
|
|
|
|
+`video_server.pro` 与 `video_client.pro` 都需要网络模块:
|
|
|
|
|
+
|
|
|
|
|
+```pro
|
|
|
|
|
+QT += core gui network
|
|
|
|
|
+
|
|
|
|
|
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
|
|
|
|
+
|
|
|
|
|
+CONFIG += c++11
|
|
|
|
|
+
|
|
|
|
|
+# video_server.pro
|
|
|
|
|
+SOURCES += \
|
|
|
|
|
+ capture_thread.cpp \
|
|
|
|
|
+ main.cpp \
|
|
|
|
|
+ mainwindow.cpp
|
|
|
|
|
+
|
|
|
|
|
+HEADERS += \
|
|
|
|
|
+ capture_thread.h \
|
|
|
|
|
+ mainwindow.h
|
|
|
|
|
+
|
|
|
|
|
+# video_client.pro
|
|
|
|
|
+# SOURCES += main.cpp mainwindow.cpp
|
|
|
|
|
+# HEADERS += mainwindow.h
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 11.7 运行与测试
|
|
|
|
|
+
|
|
|
|
|
+1. 在开发板上交叉编译、运行 `video_server`(需摄像头 `/dev/video1`,且仅 ARM 平台编译采集代码)。
|
|
|
|
|
+2. 勾选"本地显示"在开发板屏幕预览;勾选"开启广播"后,同网段客户端才能收到画面。
|
|
|
|
|
+3. 在 PC(或另一块开发板)上运行 `video_client`,绑定 8888 端口接收广播并显示。
|
|
|
|
|
+4. 注意:本地显示与广播互相制约,追求流畅广播时建议关闭本地显示。
|
|
|
|
|
+
|
|
|
|
|
+## 12. 扩展:H.264 编码与低延迟方向
|
|
|
|
|
+
|
|
|
|
|
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
|
|
|
|
|
+
|
|
|
|
|
+RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJPEG 风格),优点是实现简单、无需编码库,缺点是带宽占用大、无法跨公网高效传输。若要在开发板上做低延迟、低带宽的监控,思路是:
|
|
|
|
|
+
|
|
|
|
|
+- 采集端用 V4L2 取 YUV 帧,经硬件/软件 H.264 编码器压缩后再传输;
|
|
|
|
|
+- 传输可用 RTSP/RTMP(配合 FFmpeg/Nginx)或自定义 TCP/UDP 协议;
|
|
|
|
|
+- I.MX6U 无硬件视频编解码单元,软编码 H.264 耗 CPU,实际低延迟方案应评估平台能力。
|
|
|
|
|
+
|
|
|
|
|
+## 13. 实验步骤与调试
|
|
|
|
|
+
|
|
|
|
|
+| 现象 | 原因 | 处理 |
|
|
|
|
|
+| ---- | ---- | ---- |
|
|
|
|
|
+| 开发板连不上 MQTT | 服务器 IP/端口错、用户名密码错、不在同一网段 | 确认 `BROKER_ADDRESS`、`ping` 通、`mosquitto` 已启动 |
|
|
|
|
|
+| 连接返回码 4/5 | 用户名或密码无效 / 未授权 | 用 `mosquitto_passwd` 重新注册,核对 `myconfig.conf` |
|
|
|
|
|
+| 编译 mqttClient 找不到 `MQTTClient.h` | 头文件路径未指定 | `target_include_directories` 指向 install/include |
|
|
|
|
|
+| 链接时报找不到 `paho-mqtt3c` | 库路径未指定或库未安装 | `target_link_directories` + `target_link_libraries`,并 `make install` |
|
|
|
|
|
+| 开发板运行报找不到 `libpaho-mqtt3c.so` | 动态库未拷入 | 打包 `.so` 解压到 `/usr/lib`,或设置 `LD_LIBRARY_PATH` |
|
|
|
|
|
+| VLC 拉不到 RTMP 流 | nginx 未启动/未 reload、端口被占、推流地址错 | `ps` 查进程、`nginx -s reload`、核对 `1935` 与流名 |
|
|
|
|
|
+| 视频延迟大 | 软编解码 + 服务器与推流端同板 | 降低分辨率/帧率,或换高性能平台 |
|
|
|
|
|
+| ARM 上采集线程直接返回 | 采集代码用 `#ifndef __arm__ return;` 限定 ARM | 确认交叉编译宏 `__arm__`,PC 上不采集 |
|
|
|
|
|
+
|
|
|
|
|
+## 14. 跨平台对比
|
|
|
|
|
+
|
|
|
|
|
+| 项目 | IMX6U(本方案) | STM32(如 F4/F7 + LwIP) | 高性能 ARM Linux(RK3568 等) |
|
|
|
|
|
+| ---- | --------------- | ------------------------ | ----------------------------- |
|
|
|
|
|
+| MQTT 客户端 | paho.mqtt.c(POSIX,同步/异步) | MQTT 嵌入式库(如 Paho Embedded / coreMQTT) | 同 paho / 各云 SDK |
|
|
|
|
|
+| 视频方案 | Nginx+FFmpeg(软编) 或 V4L2+JPEG 广播 | 一般无 Linux,靠专用编码/传输 | 硬件 H.264 编码 + RTSP/RTMP,低延迟 |
|
|
|
|
|
+| 网络栈 | 内核 TCP/IP + BSD socket | LwIP | 内核 TCP/IP |
|
|
|
|
|
+| 构建 | CMake + 交叉工具链 | Keil/IAR/Makefile | CMake/Yocto/Buildroot |
|
|
|
|
|
+
|
|
|
|
|
+## 15. 面试精选(5 题)
|
|
|
|
|
+
|
|
|
|
|
+### Q1 简述 MQTT 的发布/订阅模型与三大角色。它和 HTTP 的区别?
|
|
|
|
|
+
|
|
|
|
|
+**要点**:Broker 为枢纽,客户端可发布/订阅,围绕主题通信;发布/订阅解耦,且可空间分离、时间异步。
|
|
|
|
|
+
|
|
|
|
|
+**详解**:发布者把消息发到某个主题,Broker 按订阅关系转发给订阅者;客户端角色随主题切换,彼此无需知道对方存在。相比 HTTP 的请求-响应、短连接,MQTT 保持长连接、低开销、支持一对多推送与离线消息,更适合物联网。区别:
|
|
|
|
|
+
|
|
|
|
|
+| 维度 | MQTT | HTTP |
|
|
|
|
|
+| ---- | ---- | ---- |
|
|
|
|
|
+| 模型 | 发布/订阅 | 请求/响应 |
|
|
|
|
|
+| 连接 | 长连接 + 心跳 | 多为短连接 |
|
|
|
|
|
+| 开销 | 极小 | 头部冗余大 |
|
|
|
|
|
+| 推送 | 服务端可主动推送 | 需轮询/长轮询/WS |
|
|
|
|
|
+
|
|
|
|
|
+**追问**:
|
|
|
|
|
+1. 发布/订阅的"三个可"是什么?(客户端相互独立、空间可分离、时间可异步)
|
|
|
|
|
+2. Broker 的职责有哪些?(接收、存储、转发消息,管理客户端与会话)
|
|
|
|
|
+
|
|
|
|
|
+### Q2 MQTT 的 QoS 0/1/2 分别是什么?如何选择?
|
|
|
|
|
+
|
|
|
|
|
+**要点**:0 最多一次、1 至少一次、2 保证一次;越高质量越安全也越慢。
|
|
|
|
|
+
|
|
|
|
|
+**详解**:QoS0 发完即弃,靠 TCP;QoS1 用 PUBACK 确认,超时重发,可能重复(协议不去重,需按 dup 处理);QoS2 经 PUBREC/PUBREL/PUBCOMP 四次交互,保证接收端只收一次。实现 QoS>0 必须 `cleanSession=false`。发布与订阅 QoS 不同时,服务端按较低值降级。
|
|
|
|
|
+
|
|
|
|
|
+**追问**:
|
|
|
|
|
+1. 为什么 QoS1 需要应用自己去重?(协议只保证至少一次,重复由重发引起)
|
|
|
|
|
+2. QoS2 的报文顺序?(PUBLISH → PUBREC → PUBREL → PUBCOMP)
|
|
|
|
|
+
|
|
|
|
|
+### Q3 `cleanSession` 和会话保持有什么关系?保留消息有什么用?
|
|
|
|
|
+
|
|
|
|
|
+**要点**:`cleanSession=0` 建持久会话、可收离线消息并保留订阅;保留消息让新订阅者立刻拿到该主题最新值。
|
|
|
|
|
+
|
|
|
|
|
+**详解**:`cleanSession=1` 每次连接都是全新会话,不保存订阅、不收离线消息,`sessionPresent` 恒为 0;`cleanSession=0` 时服务端保存会话,`sessionPresent` 表示是否恢复。保留消息针对"发布在前、订阅在后"的场景,每个主题只保留一条最新值,发布空保留消息可删除。
|
|
|
|
|
+
|
|
|
|
|
+**追问**:
|
|
|
|
|
+1. 想接收离线 QoS1/QoS2 消息该怎样设置?(`cleanSession=false`)
|
|
|
|
|
+2. 如何删除某主题的保留消息?(向该主题发布一条 payload 为空的保留消息)
|
|
|
|
|
+
|
|
|
|
|
+### Q4 MQTT 心跳与遗嘱机制分别在解决什么问题?
|
|
|
|
|
+
|
|
|
|
|
+**要点**:心跳检测客户端在线状态;遗嘱在客户端意外断线时通知其它客户端。
|
|
|
|
|
+
|
|
|
|
|
+**详解**:客户端空闲时按 `keepAlive` 定时发 PINGREQ,服务端回 PINGRESP,长时间收不到即判定掉线;客户端发 PINGREQ 收不到响应则认为自身断线。遗嘱在 CONNECT 时设置(willTopic/willMessage/willRetain/willQoS),仅**意外断线**触发,主动 DISCONNECT 不触发。
|
|
|
|
|
+
|
|
|
|
|
+**追问**:
|
|
|
|
|
+1. 遗嘱在什么情况下不会发布?(客户端主动发送 DISCONNECT 断开)
|
|
|
|
|
+2. 如何用遗嘱实现上线通知?(上线时主动向自己的遗嘱主题发布一条"在线"消息)
|
|
|
|
|
+
|
|
|
|
|
+### Q5 RTMP 视频监控方案由哪几部分组成?延迟来源是什么?另一条 V4L2+JPEG 路线有何特点?
|
|
|
|
|
+
|
|
|
|
|
+**要点**:推流端 FFmpeg + 服务器 Nginx(rtmp 模块) + 拉流端 VLC;延迟主要来自软编解码和同板部署。
|
|
|
|
|
+
|
|
|
|
|
+**详解**:`ffmpeg -re -i ... -f flv rtmp://.../live/xxx` 推流,VLC 拉流;Nginx 加 `nginx-rtmp-module` 并配置 `rtmp { server { application live {...} } }`。延迟原因:I.MX6U 无硬件视频解码,全靠软件处理,且服务器与推流端在同一弱性能板子上。另一条 V4L2+JPEG 广播路线(Qt 例程)无需流媒体服务器:采集 RGB565 → JPEG → base64 → UDP 广播,客户端解码显示;实现简单、适合同网段,但带宽大、本地显示与广播互相制约。
|
|
|
|
|
+
|
|
|
|
|
+**追问**:
|
|
|
|
|
+1. RTMP 与 RTSP 的主要区别?(RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、低延迟、生态成熟)
|
|
|
|
|
+2. 为什么 Qt 方案选择 UDP 广播而不是 TCP?(一对多、无连接、实现简单;代价是可能丢包)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;例程 `33_mqtt/mqtt_prj`(CMakeLists.txt、mqttClient.c、cmake/arm-linux-setup.cmake);Qt 例程 `Embedded-Qt-Tutorial/Qt/04/05_video_surveillance`(video_server、video_client)。第 7 节(JSON/原子云)与第 12 节(H.264)为扩展知识。
|