---
title: 嵌入式Qt交叉编译与项目实战
tags:
[
嵌入式Linux,
Qt,
I.MX6U,
IMX6ULL,
ALPHA,
交叉编译,
Qt Creator,
qmake,
make,
linuxfb,
eglfs,
Qt控制LED,
Qt控制BEEP,
QFile,
QSerialPort,
串口,
CAN,
QCanBus,
SocketCAN,
QCanBusFrame,
QCamera,
OpenCV,
摄像头,
蓝牙,
QBluetoothServer,
QBluetoothSocket,
BlueZ,
Rfcomm,
USER-KEY,
QKeyEvent,
Key_VolumeDown,
AP3216C,
I2C,
杂项设备,
ICM20608,
SPI,
智能家居,
ESP8266,
原子云,
WebSocket,
QWebSocket,
语音识别,
百度AI,
ASR,
车牌识别,
OCR,
APP主界面,
QScroller,
QPropertyAnimation,
视频监控,
V4L2,
UDP广播,
项目实战,
]
created: 2026-09-18
updated: 2026-09-18
pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十四章 I.MX6U Qt开发 ~ 第二十八章 视频监控项目;配套例程 Embedded-Qt-Tutorial/Qt/03、Qt/04"
---
# 嵌入式Qt交叉编译与项目实战
> 💡 **关联知识**:[[05-Qt应用开发/01-C++基础与面向对象]]、[[05-Qt应用开发/02-Qt入门与项目结构]]、[[05-Qt应用开发/03-信号与槽]]、[[05-Qt应用开发/04-Qt控件详解]]、[[05-Qt应用开发/05-布局容器与模型视图]]、[[05-Qt应用开发/06-文本绘图与图表]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]]、[[05-Qt应用开发/面试-Qt应用开发]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/1. Qt概述]]、[[实际项目开发文档/加解密客户端(Qt+LibreSSL)/1.项目源代码]]
前面七篇都在讲 Qt 的通用知识:C++、信号与槽、控件、布局、绘图、多线程、网络。本篇把这些知识真正“落到板子上”——先搭建 I.MX6U 的交叉编译环境,再让 Qt 去操作开发板上的 LED、蜂鸣器、串口、CAN、摄像头、蓝牙、按键和各类传感器,最后完成五个综合项目。本篇所有代码均来自正点原子《I.MX6U嵌入式Qt开发指南》配套例程,运行环境为正点原子 I.MX6U ALPHA/MINI 开发板的**出厂系统**(驱动与应用必须配套,请勿随意更换内核或设备树)。
## 学习地图
```mermaid
flowchart TD
A["Qt 应用开发知识
(第 1~7 篇)"] --> B["第十四章:I.MX6U Qt 开发
交叉编译环境"]
B --> C1["命令行编译"]
B --> C2["Qt Creator 交叉套件"]
C1 --> D["Qt 控制硬件
第十五~二十三章"]
C2 --> D
D --> E1["LED / BEEP(gpio-leds)"]
D --> E2["Serial Port(QSerialPort)"]
D --> E3["CAN Bus(QCanBus)"]
D --> E4["Camera(OpenCV + V4L2)"]
D --> E5["USB Bluetooth(BlueZ)"]
D --> E6["USER-KEY(QKeyEvent)"]
D --> E7["AP3216C(I2C)/ ICM20608(SPI)"]
D --> F["综合项目
第二十四~二十八章"]
F --> G1["智能家居物联网(原子云)"]
F --> G2["语音识别(百度 AI)"]
F --> G3["APP 主界面(滑动页面)"]
F --> G4["车牌识别(百度 OCR)"]
F --> G5["视频监控(UDP 广播)"]
classDef base fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
classDef hw fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef proj fill:#fef9c3,stroke:#ca8a04,color:#713f12
class A,B,C1,C2 base
class D,E1,E2,E3,E4,E5,E6,E7 hw
class F,G1,G2,G3,G4,G5 proj
```
---
# 第一部分:交叉编译环境搭建(第十四章)
## 1. 两种交叉编译方式
正点原子的出厂系统运行的是 ARM 架构的 Qt 程序,而我们在 Ubuntu 上编写、编译代码(x86-64 架构)。要让 PC 上生成的程序能在 ARM 开发板上运行,必须使用**交叉编译器**:在一种平台上编译出另一种平台可执行文件的工具链。
《I.MX6U嵌入式Qt开发指南》第十四章明确指出,交叉编译 Qt 应用程序有两种方法:
| 方式 | 说明 | 优点 | 缺点 |
| ----------------------- | ------------------------------------------------------- | ---------------------------- | ---------------------------------- |
| **命令行编译** | 直接用交叉工具链的 `qmake` + `make` 编译 | 简单、可控、适合脚本化 | 需要手动拷贝可执行文件到板子运行 |
| **Qt Creator 交叉套件** | 在 Qt Creator 中配置 ARM 的 Kit(Qt Versions + 编译器) | 可远程连接开发板一键运行调试 | 环境变量、路径等配置不对时较难排查 |
教程建议:**先在 Ubuntu 上运行仿真**(x86 套件),确认界面与逻辑正确后,再**交叉编译到开发板**查看实际效果。若 Qt Creator 远程运行结果不正确(例如缺少环境变量或路径),**直接拷贝可执行文件到开发板上运行**更稳妥。
> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。第十四章本身只给出了“参考《I.MX6U 用户快速体验》4.6 节命令行编译”与“参考《出厂系统 Qt 交叉编译环境搭建》”的指引,下面补充通用的交叉编译工作流,便于初学者理解原理。
## 2. 通用交叉编译工作流
一个典型的嵌入式 Qt 交叉编译流程如下:
```mermaid
flowchart LR
A["Ubuntu 上的
Qt 工程 .pro"] --> B["交叉 qmake
生成 Makefile"]
B --> C["make
调用 arm 交叉编译器"]
C --> D["生成 ARM 可执行文件"]
D --> E["scp / U 盘 / NFS
拷贝到开发板"]
E --> F["开发板上运行
./app -platform eglfs"]
classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
class A,B,C,D,E,F step
```
### 2.1 qmake
`qmake` 是 Qt 的项目构建工具,它读取 `*.pro` 文件,根据当前使用的 Qt 套件生成平台相关的 `Makefile`。同一个工程在 PC 上用 Desktop 套件的 qmake 生成 x86 的 Makefile,用 ARM 套件的 qmake 就生成 ARM 的 Makefile。
```bash
# 交叉工具链中的 qmake(路径以实际安装为准)
/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh & # 带环境变量启动 Qt Creator
```
工程文件 `.pro` 是最核心的配置,几个常用条目:
```qmake
QT += core gui # 使用的 Qt 模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
TARGET = 01_led # 目标可执行文件名,默认取工程名
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h
```
### 2.2 make
`qmake` 生成 `Makefile` 后,执行 `make` 完成真正的编译与链接,`make -j N` 可多核并行编译:
```bash
mkdir build && cd build
/opt/Qt5.12.9/Tools/QtCreator/bin/qmake .. # 或交叉工具链中的 qmake
make -j8
```
### 2.3 命令行编译的整体步骤
```bash
# 1. 进入 Qt 工程目录
cd 01_led
# 2. 用交叉 qmake 生成 Makefile(先 source 交叉环境变量)
/path/to/arm-qmake 01_led.pro
# 3. 编译
make -j8
# 4. 拷贝到开发板(假设板子 IP 为 192.168.1.100)
scp 01_led root@192.168.1.100:/home/root/
# 5. 在开发板串口终端运行
cd /home/root
./01_led
```
## 3. 平台插件:-platform linuxfb / eglfs
Qt 程序在不同平台上显示,依赖不同的**平台插件(platform plugin)**。在嵌入式 Linux 上最常用的是 `linuxfb` 与 `eglfs`,可通过 `-platform` 参数指定:
```bash
./01_led -platform linuxfb # 使用 Linux Framebuffer 直接输出
./01_led -platform eglfs # 使用 EGL + OpenGL ES 全屏输出
```
| 平台插件 | 全称 | 依赖 | 适用场景 |
| --------- | ----------------- | ----------------------- | -------------------------------------------- |
| `linuxfb` | Linux Framebuffer | `/dev/fb0`,无需 GPU | 没有 GPU、或只想在帧缓冲上直接显示 |
| `eglfs` | EGL Full Screen | EGL/OpenGL ES 驱动、GPU | 有 GPU 加速(I.MX6ULL 出厂系统常用),性能好 |
| `xcb` | X11 | X Server | 桌面 Linux(Ubuntu 仿真时) |
> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。第十四章正文并未展开平台插件,此处补充 `-platform` 的工作原理与常见取值,方便调试“程序能跑但屏幕没反应”的问题。在 Ubuntu 上仿真时默认使用 `xcb`,在开发板上运行时 Qt 会依据 `QT_QPA_PLATFORM` 环境变量或 `-platform` 参数选择插件。
## 4. 第一个可编译模板:main.cpp
任何 Qt Widgets 程序的入口都遵循同一结构。以 LED 工程为例:
```cpp
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv); /* 每个 Qt Widgets 程序必须有且仅有一个 QApplication */
MainWindow w;
w.show(); /* 显示主窗口 */
return a.exec(); /* 进入事件循环,处理用户与系统事件 */
}
```
---
# 第二部分:Qt 控制硬件(第十五 ~ 二十三章)
> 说明:本部分所有例程均基于正点原子 I.MX6ULL ALPHA/MINI 开发板的出厂系统。出厂内核已把常用外设注册成标准设备节点,因此 Qt 应用无需自己写驱动,只要“打开文件、读写数据”即可。这正是 Linux“一切皆文件”思想的体现。
## 5. Qt 控制 LED(第十五章)
### 5.1 资源简介
ALPHA 和 MINI 开发板板载各有一个 LED。出厂内核已把它注册为 **gpio-leds** 类型设备,应用层可直接操作其亮度节点。LED 的控制接口为:
```text
/sys/devices/platform/leds/leds/sys-led/brightness
```
向该节点写 `"1"` 点亮,写 `"0"` 熄灭。注意出厂系统默认把 `sys-led` 的触发方式设为**心跳(heartbeat)**,要想手动控制,需先把触发方式改为 `none`:
```bash
echo none > /sys/class/leds/sys-led/trigger
```
### 5.2 完整源码
**01_led.pro**(模块:`core gui widgets`)
```qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h
```
**mainwindow.h**
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 按钮 */
QPushButton *pushButton;
/* 文件,用于访问 LED 设备节点 */
QFile file;
/* 设置 LED 的状态 */
void setLedState();
/* 获取 LED 的状态 */
bool getLedState();
private slots:
void pushButtonClicked();
};
#endif // MAINWINDOW_H
```
**mainwindow.cpp**
```cpp
#include "mainwindow.h"
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 获取屏幕的分辨率,Qt 官方建议使用这种方法获取屏幕分辨率,
* 防止多屏设备导致对应不上。注意,这是获取整个桌面系统的分辨率
*/
QList list_screen = QGuiApplication::screens();
/* 如果是 ARM 平台,直接设置大小为屏幕的大小 */
#if __arm__
/* 重设大小 */
this->resize(list_screen.at(0)->geometry().width(),
list_screen.at(0)->geometry().height());
/* 默认是出厂系统的 LED 心跳触发方式,想要控制 LED,
* 需要改变 LED 的触发方式,改为 none,即无触发
*/
system("echo none > /sys/class/leds/sys-led/trigger");
#else
/* 否则设置主窗体大小为 800x480 */
this->resize(800, 480);
#endif
pushButton = new QPushButton(this);
/* 居中显示 */
pushButton->setMinimumSize(200, 50);
pushButton->setGeometry((this->width() - pushButton->width()) / 2,
(this->height() - pushButton->height()) / 2,
pushButton->width(),
pushButton->height());
/* 开发板的 LED 控制接口 */
file.setFileName("/sys/devices/platform/leds/leds/sys-led/brightness");
if (!file.exists())
/* 设置按钮的初始化文本 */
pushButton->setText("未获取到 LED 设备!");
/* 获取 LED 的状态 */
getLedState();
/* 信号槽连接 */
connect(pushButton, SIGNAL(clicked()),
this, SLOT(pushButtonClicked()));
}
MainWindow::~MainWindow()
{
}
void MainWindow::setLedState()
{
/* 在设置 LED 状态时先读取,防止外界改变了 LED 状态 */
bool state = getLedState();
/* 如果文件不存在,则返回 */
if (!file.exists())
return;
if (!file.open(QIODevice::ReadWrite))
qDebug() << file.errorString();
QByteArray buf[2] = {"0", "1"};
/* 写 0 或 1 */
if (state)
file.write(buf[0]);
else
file.write(buf[1]);
/* 关闭文件 */
file.close();
/* 重新获取 LED 的状态 */
getLedState();
}
bool MainWindow::getLedState()
{
/* 如果文件不存在,则返回 */
if (!file.exists())
return false;
if (!file.open(QIODevice::ReadWrite))
qDebug() << file.errorString();
QTextStream in(&file);
/* 读取文件所有数据 */
QString buf = in.readLine();
/* 打印出读出的值 */
qDebug() << "buf: " << buf << endl;
file.close();
if (buf == "1") {
pushButton->setText("LED 点亮");
return true;
} else {
pushButton->setText("LED 熄灭");
return false;
}
}
void MainWindow::pushButtonClicked()
{
/* 设置 LED 的状态 */
setLedState();
}
```
### 5.3 运行效果
- **Ubuntu 仿真**:由于 Ubuntu 不是开发板,找不到 LED 设备节点,故按钮初始化时显示“未获取到 LED 设备!”。
- **开发板运行**:交叉编译后拷贝到板上运行,点击按钮即可在“LED 点亮 / LED 熄灭”之间切换,LED 状态随之反转。
> 编程要点:`setLedState()` 在写入前先读取当前状态,再取反写入。这种 get/set 分离、对外留接口的写法,是嵌入式控制类代码的基本素养——即使外部程序改过 LED,反转逻辑也不会错乱。
## 6. Qt 控制 BEEP(第十六章)
### 6.1 资源简介
ALPHA 和 MINI 板载一个蜂鸣器(BEEP)。它直接接在一个 GPIO 上(并非 PWM,受管脚资源限制),出厂内核同样将其注册为 **gpio-leds** 类型设备,因此控制方法与 LED 完全一致:
```text
/sys/devices/platform/leds/leds/beep/brightness
```
### 6.2 完整源码
**02_beep.pro**:模块同 `01_led`(`core gui widgets`)。
**mainwindow.h**
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QPushButton *pushButton; /* 按钮 */
QFile file; /* 文件 */
void setBeepState(); /* 设置 BEEP 的状态 */
bool getBeepState(); /* 获取 BEEP 的状态 */
private slots:
void pushButtonClicked(); /* 槽函数 */
};
#endif // MAINWINDOW_H
```
**mainwindow.cpp**(与 `01_led` 例程结构完全一致,仅设备节点与提示文本不同)
```cpp
/* 与 01_led 的 mainwindow.cpp 相同,只需改动两处 */
/* 1. 设备节点改为蜂鸣器 */
file.setFileName("/sys/devices/platform/leds/leds/beep/brightness");
/* 2. getBeepState() 中按钮文本改为 */
pushButton->setText(buf == "1" ? "BEEP 开" : "BEEP 关");
```
### 6.3 运行效果
- **Ubuntu**:读不到 BEEP 节点,初始化显示“未获取到 BEEP 设备!”。
- **开发板**:点击按钮在“BEEP 开 / BEEP 关”之间切换,蜂鸣器响/停。
> 归纳:LED 与 BEEP 都是 **gpio-leds 子系统**设备,Qt 侧统一用 `QFile` 读写 `brightness` 节点。掌握这两个例子,就掌握了“用 Qt 点灯”的标准套路——这也是一切的起点。
## 7. Qt 串口编程 Serial Port(第十七章)
### 7.1 资源简介
I.MX6U 出厂系统默认配置了两路可用串口:
| 串口 | 设备节点 | 说明 |
| ----- | -------------- | -------------------------------- |
| UART1 | `/dev/ttymxc0` | 已作为**调试串口**使用,不可占用 |
| UART3 | `/dev/ttymxc2` | 留给用户编程使用 |
因此 Qt 程序只能对 `ttymxc2` 编程。默认参数:波特率 115200、数据位 8、无校验、停止位 1、无流控。
### 7.2 完整源码
**03_serialport.pro**(模块:`core gui serialport widgets`)
```qmake
# 添加串口模块支持
QT += core gui serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h
```
**mainwindow.h**
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QSerialPort *serialPort; /* 串口对象 */
QTextBrowser *textBrowser; /* 用作接收数据 */
QTextEdit *textEdit; /* 用作发送数据 */
QPushButton *pushButton[2]; /* 按钮:发送、打开串口 */
QComboBox *comboBox[5]; /* 串口号/波特率/数据位/校验位/停止位 */
QLabel *label[5]; /* 标签 */
QVBoxLayout *vboxLayout; /* 垂直布局 */
QGridLayout *gridLayout; /* 网格布局 */
QWidget *mainWidget; /* 主布局容器 */
QWidget *funcWidget; /* 功能区域容器 */
void layoutInit(); /* 布局初始化 */
void scanSerialPort(); /* 扫描系统可用串口 */
void baudRateItemInit(); /* 波特率项初始化 */
void dataBitsItemInit(); /* 数据位项初始化 */
void parityItemInit(); /* 校验位项初始化 */
void stopBitsItemInit(); /* 停止位项初始化 */
private slots:
void sendPushButtonClicked(); /* 发送数据 */
void openSerialPortPushButtonClicked(); /* 打开/关闭串口 */
void serialPortReadyRead(); /* 读取串口数据 */
};
#endif // MAINWINDOW_H
```
**mainwindow.cpp**
```cpp
#include "mainwindow.h"
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
layoutInit(); /* 布局初始化 */
scanSerialPort(); /* 扫描系统的串口 */
baudRateItemInit(); /* 波特率项初始化 */
dataBitsItemInit(); /* 数据位项初始化 */
parityItemInit(); /* 校验位项初始化 */
stopBitsItemInit(); /* 停止位项初始化 */
}
void MainWindow::layoutInit()
{
QList list_screen = QGuiApplication::screens();
#if __arm__
this->resize(list_screen.at(0)->geometry().width(),
list_screen.at(0)->geometry().height());
#else
this->resize(800, 480);
#endif
/* 初始化 */
serialPort = new QSerialPort(this);
textBrowser = new QTextBrowser();
textEdit = new QTextEdit();
vboxLayout = new QVBoxLayout();
funcWidget = new QWidget();
mainWidget = new QWidget();
gridLayout = new QGridLayout();
/* QList 链表,字符串类型 */
QList list1;
list1 << "串口号:" << "波特率:" << "数据位:" << "校验位:" << "停止位:";
for (int i = 0; i < 5; i++) {
label[i] = new QLabel(list1[i]);
label[i]->setMinimumSize(80, 30);
label[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
gridLayout->addWidget(label[i], 0, i); /* 添加至网格坐标 (0, i) */
}
for (int i = 0; i < 5; i++) {
comboBox[i] = new QComboBox();
comboBox[i]->setMinimumSize(80, 30);
comboBox[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
gridLayout->addWidget(comboBox[i], 1, i); /* 添加至网格坐标 (1, i) */
}
QList list2;
list2 << "发送" << "打开串口";
for (int i = 0; i < 2; i++) {
pushButton[i] = new QPushButton(list2[i]);
pushButton[i]->setMinimumSize(80, 30);
pushButton[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
gridLayout->addWidget(pushButton[i], i, 5); /* 添加至网格坐标 (i, 5) */
}
pushButton[0]->setEnabled(false);
/* 布局 */
vboxLayout->addWidget(textBrowser);
vboxLayout->addWidget(textEdit);
funcWidget->setLayout(gridLayout);
vboxLayout->addWidget(funcWidget);
mainWidget->setLayout(vboxLayout);
this->setCentralWidget(mainWidget);
/* 占位文本 */
textBrowser->setPlaceholderText("接收到的消息");
textEdit->setText("www.openedv.com");
/* 信号槽连接 */
connect(pushButton[0], SIGNAL(clicked()),
this, SLOT(sendPushButtonClicked()));
connect(pushButton[1], SIGNAL(clicked()),
this, SLOT(openSerialPortPushButtonClicked()));
connect(serialPort, SIGNAL(readyRead()),
this, SLOT(serialPortReadyRead()));
}
/* 查找可用串口并添加到 comboBox[0] */
void MainWindow::scanSerialPort()
{
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
comboBox[0]->addItem(info.portName());
}
}
/* 波特率项初始化,默认 115200 */
void MainWindow::baudRateItemInit()
{
QList list;
list << "1200" << "2400" << "4800" << "9600"
<< "19200" << "38400" << "57600"
<< "115200" << "230400" << "460800"
<< "921600";
for (int i = 0; i < 11; i++)
comboBox[1]->addItem(list[i]);
comboBox[1]->setCurrentIndex(7); /* 115200 */
}
/* 数据位项初始化,默认 8 位 */
void MainWindow::dataBitsItemInit()
{
QList list;
list << "5" << "6" << "7" << "8";
for (int i = 0; i < 4; i++)
comboBox[2]->addItem(list[i]);
comboBox[2]->setCurrentIndex(3); /* 8 */
}
/* 校验位项初始化,默认无校验 */
void MainWindow::parityItemInit()
{
QList list;
list << "None" << "Even" << "Odd" << "Space" << "Mark";
for (int i = 0; i < 5; i++)
comboBox[3]->addItem(list[i]);
comboBox[3]->setCurrentIndex(0); /* None */
}
/* 停止位项初始化,默认 1 位 */
void MainWindow::stopBitsItemInit()
{
QList list;
list << "1" << "2";
for (int i = 0; i < 2; i++)
comboBox[4]->addItem(list[i]);
comboBox[4]->setCurrentIndex(0); /* 1 */
}
/* 发送数据:读取 textEdit 内容并转为 utf8 字节流写出 */
void MainWindow::sendPushButtonClicked()
{
QByteArray data = textEdit->toPlainText().toUtf8();
serialPort->write(data);
}
/* 打开或关闭串口 */
void MainWindow::openSerialPortPushButtonClicked()
{
if (pushButton[1]->text() == "打开串口") {
/* 设置串口名 */
serialPort->setPortName(comboBox[0]->currentText());
/* 设置波特率 */
serialPort->setBaudRate(comboBox[1]->currentText().toInt());
/* 设置数据位数 */
switch (comboBox[2]->currentText().toInt()) {
case 5: serialPort->setDataBits(QSerialPort::Data5); break;
case 6: serialPort->setDataBits(QSerialPort::Data6); break;
case 7: serialPort->setDataBits(QSerialPort::Data7); break;
case 8: serialPort->setDataBits(QSerialPort::Data8); break;
default: break;
}
/* 设置奇偶校验 */
switch (comboBox[3]->currentIndex()) {
case 0: serialPort->setParity(QSerialPort::NoParity); break;
case 1: serialPort->setParity(QSerialPort::EvenParity); break;
case 2: serialPort->setParity(QSerialPort::OddParity); break;
case 3: serialPort->setParity(QSerialPort::SpaceParity); break;
case 4: serialPort->setParity(QSerialPort::MarkParity); break;
default: break;
}
/* 设置停止位 */
switch (comboBox[4]->currentText().toInt()) {
case 1: serialPort->setStopBits(QSerialPort::OneStop); break;
case 2: serialPort->setStopBits(QSerialPort::TwoStop); break;
default: break;
}
/* 设置流控制 */
serialPort->setFlowControl(QSerialPort::NoFlowControl);
if (!serialPort->open(QIODevice::ReadWrite)) {
QMessageBox::about(NULL, "错误",
"串口无法打开!可能串口已经被占用!");
} else {
for (int i = 0; i < 5; i++)
comboBox[i]->setEnabled(false);
pushButton[1]->setText("关闭串口");
pushButton[0]->setEnabled(true);
}
} else {
serialPort->close();
for (int i = 0; i < 5; i++)
comboBox[i]->setEnabled(true);
pushButton[1]->setText("打开串口");
pushButton[0]->setEnabled(false);
}
}
/* 从接收缓冲区读取数据并显示 */
void MainWindow::serialPortReadyRead()
{
QByteArray buf = serialPort->readAll();
textBrowser->insertPlainText(QString(buf));
}
MainWindow::~MainWindow()
{
}
```
### 7.3 运行效果
- **Ubuntu**:可用 USB 转串口模块与另一台设备互发消息测试界面。
- **开发板**:把串口线接到 UART3(`ttymxc2`),用电脑上的正点原子 XCOM 上位机,参数设为 115200-8-N-1、无流控,点击“打开串口”即可收发。默认下拉选项必须选 `ttymxc2`,而不是被调试占用的 `ttymxc0`。
> 归纳:Qt 串口编程的骨架是 **`QSerialPortInfo::availablePorts()` 扫描 → `QSerialPort` 配置并 `open()` → `readyRead()` 收 / `write()` 发**。这与裸机 Linux 串口编程(`termios`)目标是等价的,只是 Qt 封装得更直观。
## 8. Qt CAN 编程 CAN Bus(第十八章)
### 8.1 资源简介
从 Qt5.8 开始提供了 CAN Bus 类,正点原子 I.MX6U 出厂系统的 Qt 版本为 **Qt5.12.9**,可直接使用。底板上预留一路 CAN 接口(I.MX6U 芯片最大支持两路),为**普通 CAN(非 FD CAN)**,最大比特率 1000 kBit/s。
使用前必须先由系统初始化 CAN。要让 CAN 在总线关闭错误后 100ms 自动恢复,并以比特率 1000000 开启:
```bash
ip link set up can0 type can bitrate 1000000 restart-ms 100
```
> 注意:Windows 没有 CAN 设备(外接 USB CAN 模块需厂商固件),因此本例如在 Windows 上运行,可用接口列表为空。
### 8.2 完整源码
**04_socketcan.pro**(模块:`core gui serialbus widgets`)
```qmake
QT += core gui serialbus
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h
```
**mainwindow.h**
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QCanBusDevice *canDevice; /* CAN 设备 */
QTextBrowser *textBrowser; /* 接收数据 */
QLineEdit *lineEdit; /* 发送数据 */
QPushButton *pushButton[2]; /* 发送、连接 CAN */
QComboBox *comboBox[3]; /* 插件类型、可用接口、比特率 */
QLabel *label[4]; /* 标签 */
QVBoxLayout *vboxLayout;
QGridLayout *gridLayout;
QWidget *mainWidget;
QWidget *funcWidget;
void layoutInit(); /* 布局初始化 */
void pluginItemInit(); /* 插件类型项初始化 */
void bitrateItemInit(); /* 比特率项初始化 */
private slots:
void sendFrame(); /* 发送消息 */
void receivedFrames(); /* 接收消息 */
void pluginChanged(int); /* 插件发生改变 */
void canDeviceErrors(QCanBusDevice::CanBusError) const; /* 处理 CAN 错误 */
void connectDevice(); /* 连接或断开 CAN */
};
#endif // MAINWINDOW_H
```
**mainwindow.cpp**(核心功能代码,界面布局部分与串口例程同构,完整代码见 `Qt/03/04_socketcan/mainwindow.cpp`)
```cpp
#include "mainwindow.h"
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 使用系统指令初始化 CAN,默认为 1000000bits/s。
* 先关闭 CAN,才能以新的速率重新开启 */
system("ifconfig can0 down");
system("ip link set up can0 type can bitrate 1000000 restart-ms 100");
layoutInit(); /* 布局初始化 */
pluginItemInit(); /* 可用插件初始化 */
/* 可用接口项初始化 */
pluginChanged(comboBox[0]->currentIndex());
bitrateItemInit(); /* 比特率项初始化 */
}
MainWindow::~MainWindow()
{
}
/* 格式化接收到的帧标志 */
static QString frameFlags(const QCanBusFrame &frame)
{
QString result = QLatin1String(" --- ");
if (frame.hasBitrateSwitch())
result[1] = QLatin1Char('B');
if (frame.hasErrorStateIndicator())
result[2] = QLatin1Char('E');
if (frame.hasLocalEcho())
result[3] = QLatin1Char('L');
return result;
}
/* 发送消息:第一个字段为帧 ID(16 进制),其后 8 个字节为数据 */
void MainWindow::sendFrame()
{
if (!canDevice)
return;
QString str = lineEdit->text();
QByteArray data = 0;
QString strTemp = nullptr;
QStringList strlist = str.split(' ');
for (int i = 1; i < strlist.count(); i++)
strTemp = strTemp + strlist[i];
/* 将字符串内容转为 QByteArray */
data = QByteArray::fromHex(strTemp.toLatin1());
bool ok;
/* 以 16 进制读取第一个数据作为帧 ID */
int framId = strlist[0].toInt(&ok, 16);
QCanBusFrame frame = QCanBusFrame(framId, data);
/* 写入帧 */
canDevice->writeFrame(frame);
}
/* 接收消息 */
void MainWindow::receivedFrames()
{
if (!canDevice)
return;
while (canDevice->framesAvailable()) {
const QCanBusFrame frame = canDevice->readFrame();
QString view;
if (frame.frameType() == QCanBusFrame::ErrorFrame)
view = canDevice->interpretErrorFrame(frame);
else
view = frame.toString();
const QString time = QString::fromLatin1("%1.%2 ")
.arg(frame.timeStamp().seconds(), 10, 10, QLatin1Char(' '))
.arg(frame.timeStamp().microSeconds() / 100, 4, 10, QLatin1Char('0'));
const QString flags = frameFlags(frame);
textBrowser->insertPlainText(time + flags + view + "\n");
}
}
/* 从系统读取可用的插件并显示到 comboBox[0],默认选中 socketcan */
void MainWindow::pluginItemInit()
{
comboBox[0]->addItems(QCanBus::instance()->plugins());
for (int i = 0; i < QCanBus::instance()->plugins().count(); i++) {
if (QCanBus::instance()->plugins().at(i) == "socketcan")
comboBox[0]->setCurrentIndex(i);
}
connect(comboBox[0], SIGNAL(currentIndexChanged(int)),
this, SLOT(pluginChanged(int)));
}
/* 插件类型改变:需要同步刷新可用接口 */
void MainWindow::pluginChanged(int)
{
QList interfaces;
comboBox[1]->clear();
interfaces = QCanBus::instance()
->availableDevices(comboBox[0]->currentText());
for (const QCanBusDeviceInfo &info : qAsConst(interfaces)) {
comboBox[1]->addItem(info.name());
}
}
/* 常用比特率初始化,默认 1000000 */
void MainWindow::bitrateItemInit()
{
const QList rates = {
10000, 20000, 50000, 100000, 125000,
250000, 500000, 800000, 1000000
};
for (int rate : rates)
comboBox[2]->addItem(QString::number(rate), rate);
comboBox[2]->setCurrentIndex(8);
}
/* 连接或断开 CAN */
void MainWindow::connectDevice()
{
if (pushButton[1]->text() == "连接CAN") {
/* 注意:Qt 的 QCanBusDevice::BitRateKey 不能设置比特率,
* 系统 CAN 必须使用 ip 指令以指定比特率初始化后再操作 */
QString cmd1 = tr("ifconfig %1 down")
.arg(comboBox[1]->currentText());
QString cmd2 = tr("ip link set up %1 type can bitrate %2 restart-ms 100")
.arg(comboBox[1]->currentText())
.arg(comboBox[2]->currentText());
system(cmd1.toStdString().c_str());
system(cmd2.toStdString().c_str());
QString errorString;
canDevice = QCanBus::instance()->
createDevice(comboBox[0]->currentText(),
comboBox[1]->currentText(),
&errorString);
if (!canDevice) {
label[3]->setText(
tr("Error creating device '%1', reason: '%2'")
.arg(comboBox[0]->currentText()).arg(errorString));
return;
}
if (!canDevice->connectDevice()) {
label[3]->setText(tr("Connection error: %1")
.arg(canDevice->errorString()));
delete canDevice;
canDevice = nullptr;
return;
}
connect(canDevice, SIGNAL(framesReceived()),
this, SLOT(receivedFrames()));
connect(canDevice,
SIGNAL(errorOccurred(QCanBusDevice::CanBusError)),
this,
SLOT(canDeviceErrors(QCanBusDevice::CanBusError)));
label[3]->setText(
tr("插件类型为: %1, 已连接到 %2, 比特率为 %3 kBit/s")
.arg(comboBox[0]->currentText())
.arg(comboBox[1]->currentText())
.arg(comboBox[2]->currentText().toInt() / 1000));
pushButton[1]->setText("断开CAN");
pushButton[0]->setEnabled(true);
comboBox[0]->setEnabled(false);
comboBox[1]->setEnabled(false);
comboBox[2]->setEnabled(false);
} else {
if (!canDevice)
return;
canDevice->disconnectDevice();
delete canDevice;
canDevice = nullptr;
pushButton[1]->setText("连接CAN");
pushButton[0]->setEnabled(false);
label[3]->setText(tr("未连接!"));
comboBox[0]->setEnabled(true);
comboBox[1]->setEnabled(true);
comboBox[2]->setEnabled(true);
}
}
/* 错误处理 */
void MainWindow::canDeviceErrors(QCanBusDevice::CanBusError error) const
{
switch (error) {
case QCanBusDevice::ReadError:
case QCanBusDevice::WriteError:
case QCanBusDevice::ConnectionError:
case QCanBusDevice::ConfigurationError:
case QCanBusDevice::UnknownError:
label[3]->setText(canDevice->errorString());
break;
default:
break;
}
}
```
`layoutInit()` 负责摆放“插件类型 / 可用接口 / 比特率”三个下拉框与“发送 / 连接CAN”两个按钮,其结构与串口例程相同;其中 `lineEdit` 默认文本为 `123 aa 77 66 55 44 33 22 11`,即帧 ID `0x123` + 8 字节数据。
### 8.3 运行效果
- **Ubuntu**:没有 CAN 设备,可用接口处不可选。
- **开发板**:交叉编译后运行,插件类型默认(也必须)为 `socketcan`,可用接口为 `can0`。与 CAN 仪器以相同比特率通信,即可收发。发送格式每个字节用空格隔开,如 `123 aa 77 66 55 44 33 22 11`。
> 关键点:`QCanBusDevice::BitRateKey` **不能**设置比特率,必须先用 `ip link set up can0 type can bitrate ...` 让系统 CAN 起来;Linux 下唯一可用插件是 `socketcan`。
## 9. Qt + OpenCV 摄像头 Camera(第十九章)
### 9.1 资源简介
底板上有一路 CSI 摄像头接口,支持正点原子 OV5640、OV2640、OV7725(不带 FIFO),同时也有 USB 接口可接 USB 免驱摄像头。例程同时兼容这两类摄像头。
**为什么不直接用 QCamera?** QCamera 在某些驱动上读取 RGB 格式数据,而 OV5640/OV2640/OV7725 的驱动默认输出 YUYV,格式对不上会无法使用。此外 V4L2 直接把数据送到 `fb0` 显示会比“经 Qt 控件处理”更流畅。因此本例采用 **Qt + OpenCV** 调用摄像头。
出厂系统内置 OpenCV 版本为 3.1.0;若要在 Ubuntu 上仿真,可自行安装 OpenCV 3.4.1。使用交叉编译工具链时其中已包含 OpenCV,无需再装。
### 9.2 完整源码
**05_opencv_camera.pro**(模块:`core gui widgets` + OpenCV)
```qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
TARGET_ARCH = $${QT_ARCH}
contains(TARGET_ARCH, arm){
# ARM 套件:交叉工具链自带 OpenCV
CONFIG += link_pkgconfig
PKGCONFIG += opencv
INCLUDEPATH += /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/include
} else {
# 桌面套件:链接 Ubuntu 本地安装的 OpenCV
LIBS += -L/usr/local/lib \
-lopencv_core \
-lopencv_highgui \
-lopencv_imgproc \
-lopencv_videoio \
-lopencv_imgcodecs
INCLUDEPATH += /usr/local/include
}
SOURCES += camera.cpp main.cpp mainwindow.cpp
HEADERS += camera.h mainwindow.h
```
**camera.h**
```cpp
#ifndef CAMERA_H
#define CAMERA_H
#include
#include
/* 使用命名空间 cv 下的 VideoCapture 与 Mat 类 */
namespace cv {
class VideoCapture;
class Mat;
}
class Camera : public QObject
{
Q_OBJECT
public:
explicit Camera(QObject *parent = nullptr);
~Camera();
signals:
/* 有图片信号时发出,用于显示图像 */
void readyImage(const QImage&);
public slots:
bool cameraProcess(bool); /* 开启 / 关闭定时器 */
void selectCameraDevice(int); /* 选择摄像头 */
private slots:
void timerTimeOut(); /* 定时器到,发送图像数据 */
private:
cv::VideoCapture *capture; /* OpenCV 的 VideoCapture 对象 */
QTimer *timer; /* 定时器 */
QImage matToQImage(const cv::Mat&); /* 图像转换 */
};
#endif // CAMERA_H
```
**camera.cpp**
```cpp
#include "camera.h"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
Camera::Camera(QObject *parent) : QObject(parent)
{
capture = new cv::VideoCapture();
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeOut()));
}
Camera::~Camera()
{
delete capture;
capture = NULL;
}
void Camera::selectCameraDevice(int index)
{
/* 如果已有其它摄像头打开,先释放 */
if (capture->isOpened())
capture->release();
/* 打开摄像头设备 */
capture->open(index);
}
bool Camera::cameraProcess(bool bl)
{
if (bl) {
/* 33ms 约等于 30 帧,即一秒最多显示 30 帧 */
timer->start(33);
} else {
timer->stop();
}
return capture->isOpened();
}
void Camera::timerTimeOut()
{
if (!capture->isOpened()) {
timer->stop();
return;
}
static cv::Mat frame;
*capture >> frame;
if (frame.cols)
emit readyImage(matToQImage(frame));
}
QImage Camera::matToQImage(const cv::Mat &img)
{
/* USB 摄像头和 OV5640 等都是 RGB 三通道,不考虑单/四通道摄像头 */
if (img.type() == CV_8UC3) {
const uchar *pimg = (const uchar*)img.data;
QImage qImage(pimg, img.cols, img.rows, img.step,
QImage::Format_RGB888);
/* 不改变图像数据的前提下交换红蓝通道 */
return qImage.rgbSwapped();
}
return QImage();
}
```
**mainwindow.h**
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
#include
#include
#include
#include
class Camera;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QWidget *mainWidget; /* 主容器 */
QScrollArea *scrollArea; /* 滚动区域,方便高分辨率 */
QLabel *displayLabel; /* 显示采集到的图像 */
QHBoxLayout *hboxLayout;
QVBoxLayout *vboxLayout;
QWidget *rightWidget; /* 右侧区域容器 */
QLabel *photoLabel; /* 显示拍照的缩略图 */
QComboBox *comboBox; /* 摄像头设备选择框 */
QPushButton *pushButton[2]; /* 拍照、开始 */
QImage saveImage; /* 拍照保存的照片 */
Camera *camera; /* 摄像头对象 */
void layoutInit();
void scanCameraDevice();
private slots:
void showImage(const QImage&); /* 显示图像 */
void setButtonText(bool); /* 设置按钮文本 */
void saveImageToLocal(); /* 保存照片到本地 */
};
#endif // MAINWINDOW_H
```
**mainwindow.cpp**(核心槽函数;布局初始化见例程源码)
```cpp
#include "mainwindow.h"
#include
#include
#include
#include
#include
#include "camera.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
layoutInit(); /* 布局初始化 */
scanCameraDevice(); /* 扫描摄像头 */
}
MainWindow::~MainWindow()
{
}
/* 扫描 Linux 下的 /dev/video* 节点 */
void MainWindow::scanCameraDevice()
{
#if win32
comboBox->addItem("windows 摄像头0");
connect(comboBox, SIGNAL(currentIndexChanged(int)),
camera, SLOT(selectCameraDevice(int)));
#else
QFile file("/dev/video0");
if (file.exists())
comboBox->addItem("video0");
else {
displayLabel->setText("无摄像头设备");
return;
}
file.setFileName("/dev/video1");
if (file.exists()) {
comboBox->addItem("video1");
/* 开发板 ov5640 等设备是 1 */
comboBox->setCurrentIndex(1);
}
file.setFileName("/dev/video2");
if (file.exists())
/* 开发板 USB 摄像头设备是 2 */
comboBox->addItem("video2");
#if !__arm__
/* Ubuntu 的 USB 摄像头一般是 0 */
comboBox->setCurrentIndex(0);
#endif
connect(comboBox, SIGNAL(currentIndexChanged(int)),
camera, SLOT(selectCameraDevice(int)));
#endif
}
/* 显示图像并更新拍照按钮 */
void MainWindow::showImage(const QImage &image)
{
displayLabel->setPixmap(QPixmap::fromImage(image));
saveImage = image;
if (!saveImage.isNull())
pushButton[0]->setEnabled(true);
else
pushButton[0]->setEnabled(false);
}
/* 开始 / 关闭摄像头 */
void MainWindow::setButtonText(bool bl)
{
if (bl) {
camera->selectCameraDevice(comboBox->currentIndex());
pushButton[1]->setText("关闭");
} else {
pushButton[0]->setEnabled(false);
pushButton[1]->setText("开始");
}
}
/* 保存照片到可执行程序目录下的 test.png */
void MainWindow::saveImageToLocal()
{
if (!saveImage.isNull()) {
QString fileName =
QCoreApplication::applicationDirPath() + "/test.png";
qDebug() << "正在保存" << fileName << "图片,请稍候..." << endl;
/* save(路径, 类型, 质量等级) */
saveImage.save(fileName, "PNG", -1);
photoLabel->setPixmap(QPixmap::fromImage(QImage(fileName)));
qDebug() << "保存完成!" << endl;
}
}
```
`layoutInit()` 中通过 5 个信号槽把界面与逻辑连起来:
```cpp
camera = new Camera(this);
connect(camera, SIGNAL(readyImage(QImage)), this, SLOT(showImage(QImage)));
connect(pushButton[1], SIGNAL(clicked(bool)), camera, SLOT(cameraProcess(bool)));
connect(pushButton[1], SIGNAL(clicked(bool)), this, SLOT(setButtonText(bool)));
connect(pushButton[0], SIGNAL(clicked()), this, SLOT(saveImageToLocal()));
```
### 9.3 运行效果
- 选择摄像头设备(开发板上 `video0` 是 NXP 的 pxp 驱动节点,**不要选**;OV5640 为 `video1`,USB 摄像头为 `video2`),点击“开始”即可预览,点击“拍照”在程序目录生成 `test.png` 并显示缩略图。
- OpenCV 不设置分辨率时默认 640×480、30fps。USB 免驱摄像头可用 `capture->set(CV_CAP_PROP_FRAME_WIDTH, 320)` 修改分辨率;正点原子 OV5640/OV2640/OV7725 的驱动设置分辨率方式与标准 V4L2 有差异,可直接用 `camera_settings` 工具设置。
## 10. Qt 蓝牙聊天 USB Bluetooth(第二十章)
### 10.1 资源简介
开发板无板载蓝牙,但可外接**免驱 USB 蓝牙**模块。Qt 蓝牙依赖 Linux 的 **BlueZ** 协议栈,因此**Windows 无法使用**(需自行移植),Ubuntu 与 I.MX6U 出厂系统均可。
例程把本地蓝牙做成**服务端**(手机作为客户端连接),同时也支持扫描并作为**客户端**连接他人,实现聊天。通信基于 **RFCOMM/SPP(串口端口)** 协议。
### 10.2 项目结构与模块
**06_bluetooth_chat.pro**(模块:`core gui bluetooth widgets`)
```qmake
QT += core gui bluetooth
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += chatclient.cpp chatserver.cpp main.cpp mainwindow.cpp remoteselector.cpp
HEADERS += chatclient.h chatserver.h mainwindow.h remoteselector.h
```
项目分为四大部分:
| 文件 | 职责 |
| ------------------ | ---------------------------------------------- |
| `chatserver.*` | 服务端:创建 RFCOMM 服务器、注册服务、收发消息 |
| `chatclient.*` | 客户端:连接远端服务、收发消息 |
| `remoteselector.*` | 远程选择器:扫描发现周围蓝牙服务 |
| `mainwindow.*` | 主界面:Tab 页面、按钮逻辑,串联上述三类 |
服务 UUID 定义为:`e8e10f95-1a70-4b27-9ccf-02010264e9c8`。
### 10.3 完整源码
**chatclient.h / chatclient.cpp(客户端,核心代码)**
```cpp
/* chatclient.h 关键声明 */
class ChatClient : public QObject {
Q_OBJECT
public:
void startClient(const QBluetoothServiceInfo &remoteService);
void stopClient();
public slots:
void sendMessage(const QString &message);
void disconnect();
signals:
void messageReceived(const QString &sender, const QString &message);
void connected(const QString &name);
void disconnected();
private slots:
void readSocket();
void connected();
private:
QBluetoothSocket *socket;
};
/* chatclient.cpp:用扫描到的服务信息建立 RFCOMM socket 并连接 */
void ChatClient::startClient(const QBluetoothServiceInfo &remoteService)
{
if (socket) return;
socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
socket->connectToService(remoteService);
connect(socket, SIGNAL(readyRead()), this, SLOT(readSocket()));
connect(socket, SIGNAL(connected()), this, SLOT(connected()));
connect(socket, SIGNAL(disconnected()), this, SIGNAL(disconnected()));
}
void ChatClient::readSocket()
{
while (socket->canReadLine()) {
QByteArray line = socket->readLine();
emit messageReceived(socket->peerName(),
QString::fromUtf8(line.constData(), line.length()));
}
}
void ChatClient::sendMessage(const QString &message)
{
socket->write(message.toUtf8() + '\n');
}
```
**chatserver.h / chatserver.cpp(服务端,核心代码)**
```cpp
/* 创建 RFCOMM 服务器并注册 SPP 服务,最后 registerService 才生效 */
void ChatServer::startServer(const QBluetoothAddress& localAdapter)
{
if (rfcommServer) return;
rfcommServer = new QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
connect(rfcommServer, SIGNAL(newConnection()), this, SLOT(clientConnected()));
if (!rfcommServer->listen(localAdapter)) return; /* 监听失败直接返回 */
/* 声明为串行端口(SPP),并绑定自定义 serviceUuid */
QBluetoothServiceInfo::Sequence classId;
classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort));
serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList, classId);
classId.prepend(QVariant::fromValue(QBluetoothUuid(serviceUuid)));
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);
serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid));
/* 还需设置 ServiceName / ServiceDescription / ServiceProvider /
* BrowseGroupList / ProtocolDescriptorList(L2cap + Rfcomm),
* 完整代码见 Qt/03/06_bluetooth_chat/chatserver.cpp */
serviceInfo.registerService(localAdapter); /* 必须注册才会生效 */
}
void ChatServer::clientConnected()
{
QBluetoothSocket *socket = rfcommServer->nextPendingConnection();
if (!socket) return;
connect(socket, SIGNAL(readyRead()), this, SLOT(readSocket()));
connect(socket, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
clientSockets.append(socket);
socketsPeername.append(socket->peerName());
emit clientConnected(socket->peerName());
}
```
**remoteselector.h / remoteselector.cpp(远程发现,核心代码)**
```cpp
/* 初始化本地蓝牙服务发现代理 */
RemoteSelector::RemoteSelector(QBluetoothAddress &localAdapter, QObject *parent)
: QObject(parent)
{
m_discoveryAgent = new QBluetoothServiceDiscoveryAgent(localAdapter);
connect(m_discoveryAgent, SIGNAL(serviceDiscovered(QBluetoothServiceInfo)),
this, SLOT(serviceDiscovered(QBluetoothServiceInfo)));
connect(m_discoveryAgent, SIGNAL(finished()), this, SLOT(discoveryFinished()));
connect(m_discoveryAgent, SIGNAL(canceled()), this, SLOT(discoveryFinished()));
}
/* 注意:不要设置 uuid 过滤,否则搜不到手机 */
void RemoteSelector::startDiscovery(const QBluetoothUuid &uuid)
{
Q_UNUSED(uuid)
if (m_discoveryAgent->isActive())
m_discoveryAgent->stop();
// m_discoveryAgent->setUuidFilter(uuid);
m_discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
}
/* 发现一个服务,按设备地址去重后加入列表 */
void RemoteSelector::serviceDiscovered(const QBluetoothServiceInfo &serviceInfo)
{
QMapIterator i(m_discoveredServices);
while (i.hasNext()) {
i.next();
if (serviceInfo.device().address() == i.value().device().address())
return;
}
QString remoteName = serviceInfo.device().name().isEmpty()
? serviceInfo.device().address().toString()
: serviceInfo.device().name();
QListWidgetItem *item = new QListWidgetItem(
QString::fromLatin1("%1%2").arg(remoteName, serviceInfo.serviceName()));
m_discoveredServices.insert(remoteName, serviceInfo);
emit newServiceFound(item);
}
```
**mainwindow.h / mainwindow.cpp(关键逻辑)**
`mainwindow` 使用一个 `QTabWidget` 分成“蓝牙聊天”和“蓝牙列表”两页,前者是 `QTextBrowser` + `QLineEdit` + 发送按钮,后者是 `QListWidget` + 扫描/停止/连接/断开按钮。核心槽函数如下:
```cpp
/* 初始化本地蓝牙,同时作为服务端 */
void MainWindow::localAdapterInit()
{
localAdapters = QBluetoothLocalDevice::allDevices();
QBluetoothLocalDevice localDevice;
/* 让自己可被其他设备发现 */
localDevice.setHostMode(QBluetoothLocalDevice::HostDiscoverable);
QBluetoothAddress adapter = QBluetoothAddress();
remoteSelector = new RemoteSelector(adapter, this);
connect(remoteSelector, SIGNAL(newServiceFound(QListWidgetItem*)),
this, SLOT(newServiceFound(QListWidgetItem*)));
server = new ChatServer(this);
connect(server, SIGNAL(clientConnected(QString)),
this, SLOT(connected(QString)));
connect(server, SIGNAL(clientDisconnected(QString)),
this, SLOT(disconnected(QString)));
connect(server, SIGNAL(messageReceived(QString,QString)),
this, SLOT(showMessage(QString,QString)));
connect(this, SIGNAL(sendMessage(QString)),
server, SLOT(sendMessage(QString)));
connect(this, SIGNAL(disconnect()),
server, SLOT(disconnect()));
server->startServer();
localName = QBluetoothLocalDevice().name();
}
/* 本地蓝牙作为客户端连接远端服务 */
void MainWindow::connectToDevice()
{
if (listWidget->currentRow() == -1)
return;
QString name = listWidget->currentItem()->text();
QBluetoothServiceInfo service;
bool found = false;
QMapIterator
i(remoteSelector->m_discoveredServices);
while (i.hasNext()) {
i.next();
if (i.key() == name) {
service = i.value();
found = true;
break;
}
}
if (found) {
ChatClient *client = new ChatClient(this);
connect(client, SIGNAL(messageReceived(QString,QString)),
this, SLOT(showMessage(QString,QString)));
connect(client, SIGNAL(disconnected()),
this, SLOT(clientDisconnected()));
connect(client, SIGNAL(connected(QString)),
this, SLOT(connected(QString)));
connect(this, SIGNAL(sendMessage(QString)),
client, SLOT(sendMessage(QString)));
connect(this, SIGNAL(disconnect()),
client, SLOT(disconnect()));
client->startClient(service);
clients.append(client);
}
}
/* 开始扫描周围蓝牙 */
void MainWindow::searchForDevices()
{
listWidget->clear();
if (remoteSelector) {
delete remoteSelector;
remoteSelector = NULL;
}
QBluetoothAddress adapter = QBluetoothAddress();
remoteSelector = new RemoteSelector(adapter, this);
connect(remoteSelector, SIGNAL(newServiceFound(QListWidgetItem*)),
this, SLOT(newServiceFound(QListWidgetItem*)));
remoteSelector->m_discoveredServices.clear();
remoteSelector->startDiscovery(QBluetoothUuid(serviceUuid));
}
/* 收到消息则显示,切到聊天页 */
void MainWindow::showMessage(const QString &sender, const QString &message)
{
textBrowser->insertPlainText(QString::fromLatin1("%1: %2\n")
.arg(sender, message));
tabWidget->setCurrentIndex(0);
textBrowser->moveCursor(QTextCursor::End);
}
/* 发送消息:先本地显示,再 emit 给服务端和所有客户端 */
void MainWindow::sendMessage()
{
showMessage(localName, lineEdit->text());
emit sendMessage(lineEdit->text());
}
```
### 10.4 运行效果
运行后默认开启服务端模式。手机安装蓝牙调试软件(安卓可用“蓝牙调试宝”“蓝牙串口助手”),配对后即可聊天。开发板上需先按《I.MX6U 用户快速体验》第 3.29 节开启蓝牙并允许被扫描,先配对再连接。若连接失败,常见原因是蓝牙质量、系统蓝牙软件未开启、或手机端调试软件不支持 SPP。Ubuntu 虚拟机上蓝牙反应可能较慢,开发板上更稳定。
> 编程要点:Qt 蓝牙服务端必须 `registerService()`;`RemoteSelector` 中**不能**设置 UUID 过滤,否则手机扫不出来;扫描到的名字是“设备名 + 服务名”拼接,连接时用该名字去 `m_discoveredServices` 里取回完整的 `QBluetoothServiceInfo`。
## 11. Qt 监测 USER-KEY(第二十一章)
### 11.1 资源简介
开发板板载一个按键 KEY0,出厂内核已将其注册为 **gpio-keys** 类型设备,键值为 **114**,对应的正是 Qt 的 `Qt::Key_VolumeDown`(音量减)。也就是说,KEY0 可以当作普通键盘的“音量减键”来用。在 Windows/Ubuntu 上测试时使用方向键“↓”(`Qt::Key_Down`)代替。
### 11.2 完整源码
**07_key.pro**(模块:`core gui widgets`)
```qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h
```
**mainwindow.h**
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QLabel *label; /* 标签文本 */
void keyPressEvent(QKeyEvent *event); /* 重写按键按下事件 */
void keyReleaseEvent(QKeyEvent *event); /* 重写按键松开事件 */
};
#endif // MAINWINDOW_H
```
**mainwindow.cpp**
```cpp
#include "mainwindow.h"
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QList list_screen = QGuiApplication::screens();
#if __arm__
this->resize(list_screen.at(0)->geometry().width(),
list_screen.at(0)->geometry().height());
#else
this->setGeometry(0, 0, 800, 480);
#endif
label = new QLabel(this);
#if __arm__
label->setText("VolumeDown 松开状态");
#else
label->setText("Down 按键松开");
#endif
label->setAlignment(Qt::AlignCenter);
setCentralWidget(label);
}
MainWindow::~MainWindow()
{
}
void MainWindow::keyPressEvent(QKeyEvent *event)
{
#if __arm__
/* 板子 KEY0 按键对应 VolumeDown */
if (event->key() == Qt::Key_VolumeDown)
label->setText("VolumeDown 按键按下");
#else
if (event->key() == Qt::Key_Down)
label->setText("Down 按键按下");
#endif
QWidget::keyPressEvent(event); /* 保留默认事件处理 */
}
void MainWindow::keyReleaseEvent(QKeyEvent *event)
{
#if __arm__
if (event->key() == Qt::Key_VolumeDown)
label->setText("VolumeDown 按键松开");
#else
if (event->key() == Qt::Key_Down)
label->setText("Down 按键松开");
#endif
QWidget::keyReleaseEvent(event);
}
```
### 11.3 运行效果
- **Ubuntu/Windows**:窗口获得焦点时按“↓”显示“Down 按键按下”,松开显示“Down 按键松开”。
- **开发板**:按 KEY0 显示“VolumeDown 按键按下”,松开显示“VolumeDown 按键松开”。
> 归纳:只要设备被注册成标准的输入设备(gpio-keys),Qt 就能通过**重写 `keyPressEvent()` / `keyReleaseEvent()`** 捕获按键,无需自己写线程轮询 `/dev/input/eventX`。
## 12. Qt 读取 AP3216C 三合一环境传感器(第二十二章)
### 12.1 资源简介
AP3216C 是**三合一环境传感器**(环境光 ALS + 接近 PS + 红外 IR),采用 **I2C** 接口,仅 ALPHA 底板有,MINI 没有。出厂系统已写好 AP3216C 驱动并注册为**杂项设备**,节点位于:
```text
/sys/class/misc/ap3216c/als
/sys/class/misc/ap3216c/ps
/sys/class/misc/ap3216c/ir
```
本例与 LED 的区别在于:读取方式改用 **C 语言的 `open()/read()/close()`** 而非 QFile,并且设计了一个蓝色科技感的弧形仪表界面,把界面拆成 `ArcGraph`(弧形图)、`GlowText`(发光文字)、`HeadView`(表头)等自定义类。分辨率关系:ALS 为 16 位(65535),PS/IR 为 10 位(1023)。
### 12.2 完整源码
**08_ii2c_ap3216c_sensor.pro**(模块:`core gui widgets`,含 `headview/headview.pri`)
```qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += ap3216c.cpp arcgraph.cpp glowtext.cpp main.cpp mainwindow.cpp
HEADERS += ap3216c.h arcgraph.h glowtext.h mainwindow.h
include(headview/headview.pri)
```
> `.pri` 文件语法与 `.pro` 相同,类似 C++ 的头文件,用 `include()` 引入,便于复用与管理大量项目文件。
**ap3216c.h / ap3216c.cpp**(数据来源:C 语言接口读取 sysfs 节点)
```cpp
// ap3216c.h
#ifndef AP3216C_H
#define AP3216C_H
#include
#include
class Ap3216c : public QObject
{
Q_OBJECT
public:
explicit Ap3216c(QObject *parent = 0);
~Ap3216c();
Q_INVOKABLE void setCapture(bool str);
QString alsData();
QString psData();
QString irData();
Q_PROPERTY(QString alsData READ alsData NOTIFY ap3216cDataChanged)
Q_PROPERTY(QString psData READ psData NOTIFY ap3216cDataChanged)
Q_PROPERTY(QString irData READ irData NOTIFY ap3216cDataChanged)
private:
QTimer *timer;
QString alsdata, psdata, irdata;
QString readAlsData();
QString readPsData();
QString readIrData();
public slots:
void timer_timeout();
signals:
void ap3216cDataChanged();
};
#endif // AP3216C_H
```
```cpp
// ap3216c.cpp
#include "ap3216c.h"
#include
#include
#include
#include
#include
#include
#include
Ap3216c::Ap3216c(QObject *parent) : QObject(parent)
{
timer = new QTimer();
connect(timer, SIGNAL(timeout()), this, SLOT(timer_timeout()));
}
Ap3216c::~Ap3216c()
{
}
void Ap3216c::timer_timeout()
{
alsdata = readAlsData();
psdata = readPsData();
irdata = readIrData();
emit ap3216cDataChanged();
}
QString Ap3216c::readIrData()
{
char const *filename = "/sys/class/misc/ap3216c/ir";
int err = 0, fd;
char buf[10];
fd = open(filename, O_RDONLY);
if (fd < 0) {
close(fd);
return "open file error!";
}
err = read(fd, buf, sizeof(buf));
if (err < 0) {
close(fd);
return "read data error!";
}
close(fd);
QString irValue = buf;
QStringList list = irValue.split("\n");
return list[0];
}
QString Ap3216c::readPsData()
{
char const *filename = "/sys/class/misc/ap3216c/ps";
int err = 0, fd;
char buf[10];
fd = open(filename, O_RDONLY);
if (fd < 0) { close(fd); return "open file error!"; }
err = read(fd, buf, sizeof(buf));
if (err < 0) { close(fd); return "read data error!"; }
close(fd);
QString psValue = buf;
return psValue.split("\n")[0];
}
QString Ap3216c::readAlsData()
{
char const *filename = "/sys/class/misc/ap3216c/als";
int err = 0, fd;
char buf[10];
fd = open(filename, O_RDONLY);
if (fd < 0) { close(fd); return "open file error!"; }
err = read(fd, buf, sizeof(buf));
if (err < 0) { close(fd); return "read data error!"; }
close(fd);
QString alsValue = buf;
return alsValue.split("\n")[0];
}
QString Ap3216c::alsData() { return alsdata; }
QString Ap3216c::irData() { return irdata; }
QString Ap3216c::psData() { return psdata; }
void Ap3216c::setCapture(bool str)
{
if (str)
timer->start(500); /* 默认 500ms 采集一次 */
else
timer->stop();
}
```
**arcgraph.h / arcgraph.cpp**(蓝色科技感弧形视图,基于 QPainter)
```cpp
// arcgraph.h
#ifndef ARCGRAPH_H
#define ARCGRAPH_H
#include
#include
#include
class ArcGraph : public QWidget
{
Q_OBJECT
public:
ArcGraph(QWidget *parent = nullptr);
~ArcGraph();
void setstartAngle(int);
void setangleLength(int);
private:
void paintEvent(QPaintEvent *event);
int startAngle;
int angleLength;
};
#endif // ARCGRAPH_H
```
```cpp
// arcgraph.cpp(关键:QRadialGradient 画发光圆环,QPainterPath::arcTo 画弧)
#include "arcgraph.h"
ArcGraph::ArcGraph(QWidget *parent)
: QWidget(parent), startAngle(90), angleLength(100)
{
this->setMinimumSize(100, 100);
setAttribute(Qt::WA_TranslucentBackground, true);
}
void ArcGraph::setstartAngle(int angle) { startAngle = angle; this->repaint(); }
void ArcGraph::setangleLength(int length) { angleLength = length; this->repaint(); }
void ArcGraph::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.save();
painter.setRenderHints(QPainter::Antialiasing, true);
QRect drawRect = event->rect();
/* 两层径向渐变圆环:靠 setColorAt 在 0.5/0.51、0.42/0.43 处突变出环带 */
QRadialGradient g1(drawRect.center(), drawRect.width()/2, drawRect.center());
g1.setColorAt(0, Qt::transparent);
g1.setColorAt(0.51, QColor("#00237f"));
g1.setColorAt(0.59, Qt::transparent);
painter.setBrush(g1); painter.setPen(Qt::NoPen);
painter.drawEllipse(drawRect);
QRadialGradient g2(drawRect.center(), drawRect.width()/2, drawRect.center());
g2.setColorAt(0.421, QColor("#885881e3"));
g2.setColorAt(0.430, QColor("#5881e3"));
g2.setColorAt(0.441, Qt::transparent);
painter.setBrush(g2);
painter.drawEllipse(drawRect);
/* 中间显示百分比数字 */
QFont font; font.setPixelSize(drawRect.width()/10);
painter.setPen(Qt::white); painter.setFont(font);
painter.drawText(drawRect, Qt::AlignCenter,
QString::number(angleLength * 100 / 360) + "%");
/* 平移到中心后 arcTo(矩形, 起始角, 扫过角),负值表示顺时针 */
painter.translate(drawRect.width() >> 1, drawRect.height() >> 1);
int radius = drawRect.width()/2;
QRectF rect(-radius, -radius, radius << 1, radius << 1);
QRadialGradient g3(0, 0, radius);
g3.setColorAt(0.51, QColor("#500194d3"));
g3.setColorAt(0.55, QColor("#22c1f3f9"));
g3.setColorAt(0.58, QColor("#500194d3"));
g3.setColorAt(0.68, Qt::transparent);
painter.setBrush(g3);
QPainterPath path1;
path1.arcTo(rect, startAngle, -angleLength);
painter.drawPath(path1);
QRadialGradient g4(0, 0, radius);
g4.setColorAt(0.50, QColor("#4bf3f9"));
g4.setColorAt(0.59, QColor("#4bf3f9"));
g4.setColorAt(0.60, Qt::transparent);
painter.setBrush(g4);
QPainterPath path2;
path2.arcTo(rect, startAngle, -angleLength);
painter.drawPath(path2);
painter.restore();
event->accept();
}
```
**glowtext.h / glowtext.cpp**(发光文字:用 `QGraphicsBlurEffect` 做模糊发光)
```cpp
// glowtext.h
#ifndef GLOWTEXT_H
#define GLOWTEXT_H
#include
#include
class GlowText : public QWidget
{
Q_OBJECT
public:
GlowText(QWidget *parent = nullptr);
~GlowText();
void setTextColor(QColor);
void setFontSize(int);
void setTextData(QString);
private:
QLabel *textLabelbg; /* 文本背景(用于发光) */
QLabel *textLabel; /* 文本标签 */
QColor textColor;
int fontSize;
QString textData;
};
#endif // GLOWTEXT_H
```
```cpp
// glowtext.cpp(关键:同一段文字用两个 QLabel 叠加,底层加模糊特效做发光)
#include "glowtext.h"
#include
GlowText::GlowText(QWidget *parent)
: QWidget(parent), textColor("#4bf3f9"), fontSize(18), textData("100")
{
QFont font; font.setPixelSize(fontSize);
QPalette pal; pal.setColor(QPalette::WindowText, textColor);
/* 底层:模糊发光的文字 */
textLabelbg = new QLabel(this);
textLabelbg->setAttribute(Qt::WA_TranslucentBackground, true);
textLabelbg->setPalette(pal);
textLabelbg->setFont(font);
textLabelbg->setText(textData);
textLabelbg->setAlignment(Qt::AlignCenter);
QGraphicsBlurEffect *ef = new QGraphicsBlurEffect();
ef->setBlurRadius(25);
ef->setBlurHints(QGraphicsBlurEffect::QualityHint);
textLabelbg->setGraphicsEffect(ef); /* 关键:模糊特效 */
/* 顶层:清晰文字 */
textLabel = new QLabel(this);
textLabel->setAttribute(Qt::WA_TranslucentBackground, true);
textLabel->setPalette(pal);
textLabel->setFont(font);
textLabel->setText(textData);
textLabel->setAlignment(Qt::AlignCenter);
textLabelbg->adjustSize();
textLabel->adjustSize();
this->resize(textLabel->size().width() + 10, textLabel->size().height() + 10);
this->setAttribute(Qt::WA_TranslucentBackground, true);
}
/* setTextColor / setFontSize / setTextData 均同时更新两层 QLabel 并 adjustSize(),
* 完整代码见 Qt/03/08_ii2c_ap3216c_sensor/glowtext.cpp */
```
**mainwindow.h / mainwindow.cpp**(把三个数据分别映射成弧形角度)
```cpp
// mainwindow.h(节选)
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
ArcGraph *arcGraph[3];
GlowText *glowText[3];
QVBoxLayout *vBoxLayout;
QHBoxLayout *hBoxLayout[5];
QWidget *widget[6];
QLabel *label[3];
Ap3216c *ap3216c;
HeadView *headView;
private slots:
void getAp3216cData();
};
```
```cpp
// mainwindow.cpp(核心部分)
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
this->resize(800, 480);
this->setStyleSheet("background:#011753");
/* …… 大量布局代码(widget/arcGraph/label/glowText 的实例化与嵌套布局),
* 完整代码见 Qt/03/08_ii2c_ap3216c_sensor/mainwindow.cpp …… */
ap3216c = new Ap3216c(this);
/* 只有在开发板上才能获取数据,Ubuntu 上没有 ap3216c 传感器 */
#if __arm__
ap3216c->setCapture(true);
#endif
connect(ap3216c, SIGNAL(ap3216cDataChanged()),
this, SLOT(getAp3216cData()));
}
void MainWindow::getAp3216cData()
{
/* 环境光 16 位,最大 65535 */
static QString als = ap3216c->alsData();
if (als != ap3216c->alsData()) {
als = ap3216c->alsData();
arcGraph[0]->setangleLength(als.toUInt() * 360 / 65535);
}
/* 接近距离 10 位,最大 1023 */
static QString ps = ap3216c->psData();
if (ps != ap3216c->psData()) {
ps = ap3216c->psData();
arcGraph[1]->setangleLength(ps.toUInt() * 360 / 1023);
}
/* 红外强度 10 位,最大 1023 */
static QString ir = ap3216c->irData();
if (ir != ap3216c->irData()) {
ir = ap3216c->irData();
arcGraph[2]->setangleLength(ir.toUInt() * 360 / 1023);
}
glowText[0]->setTextData(als);
glowText[1]->setTextData(ps);
glowText[2]->setTextData(ir);
}
```
`HeadView`(数据可视化表头)是一个继承 `QWidget` 的透明组件,内部用 `QLabel` 显示图标与“数据可视化情况”文字,底部加一条 2px 的分隔线,可通过 `.pri` 整体移植到其他项目。
### 12.3 运行效果
- **Ubuntu**:显示初始化数据(读不到节点)。
- **开发板 ALPHA**:手接近传感器时,三个弧形图的弧长与发光数字实时变化,默认每 500ms 采集一次。
## 13. Qt 读取 ICM20608 六轴传感器(第二十三章)
### 13.1 资源简介
ICM20608 是**六轴 MEMS 传感器**(三轴加速度 + 三轴陀螺仪),采用 **SPI** 接口,仅 ALPHA 底板有。出厂系统提供了设备节点 `/dev/icm20608`。本章与上一章类似,只提供读取接口,不再重复设计界面。原理可参考《I.MX6U 嵌入式 Linux 驱动开发指南》Linux SPI 驱动实验。
### 13.2 完整源码
**icm20608.h / icm20608.cpp**
```cpp
// icm20608.h
#ifndef ICM20608_H
#define ICM20608_H
#include
#include
class Icm20608 : public QObject
{
Q_OBJECT
public:
explicit Icm20608(QObject *parent = 0);
~Icm20608();
Q_INVOKABLE void setCapture(bool str);
Q_PROPERTY(QString gxData READ gxData NOTIFY icm20608DataChanged)
Q_PROPERTY(QString gyData READ gyData NOTIFY icm20608DataChanged)
Q_PROPERTY(QString gzData READ gzData NOTIFY icm20608DataChanged)
Q_PROPERTY(QString axData READ axData NOTIFY icm20608DataChanged)
Q_PROPERTY(QString ayData READ ayData NOTIFY icm20608DataChanged)
Q_PROPERTY(QString azData READ azData NOTIFY icm20608DataChanged)
Q_PROPERTY(QString tempData READ tempData NOTIFY icm20608DataChanged)
private:
QTimer *timer;
QString gxdata, gydata, gzdata;
QString axdata, aydata, azdata;
QString tempdata;
QString gxData() { return gxdata; }
QString gyData() { return gydata; }
QString gzData() { return gzdata; }
QString axData() { return axdata; }
QString ayData() { return aydata; }
QString azData() { return azdata; }
QString tempData() { return tempdata; }
void icm20608ReadData();
public slots:
void timer_timeout();
signals:
void icm20608DataChanged();
};
#endif // ICM20608_H
```
```cpp
// icm20608.cpp
#include "icm20608.h"
#include
#include
#include
#include
#include
#include
#include
Icm20608::Icm20608(QObject *parent) : QObject(parent)
{
timer = new QTimer();
#if __arm__
/* 开发板上需要先加载 ICM20608 驱动模块 */
system("insmod /home/root/driver/icm20608/icm20608.ko");
#endif
connect(timer, SIGNAL(timeout()), this, SLOT(timer_timeout()));
}
Icm20608::~Icm20608()
{
}
void Icm20608::icm20608ReadData()
{
int fd;
char const *filename = "/dev/icm20608";
signed int databuf[7];
unsigned char data[14];
signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
signed int accel_x_adc, accel_y_adc, accel_z_adc;
signed int temp_adc;
float gyro_x_act, gyro_y_act, gyro_z_act;
float accel_x_act, accel_y_act, accel_z_act;
float temp_act;
int ret = 0;
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("can't open file %s\r\n", filename);
return;
}
ret = read(fd, databuf, sizeof(databuf));
/* 读取成功 */
if (ret == 0) {
gyro_x_adc = databuf[0];
gyro_y_adc = databuf[1];
gyro_z_adc = databuf[2];
accel_x_adc = databuf[3];
accel_y_adc = databuf[4];
accel_z_adc = databuf[5];
temp_adc = databuf[6];
/* 按量程换算为实际值 */
gyro_x_act = (float)(gyro_x_adc) / 16.4;
gyro_y_act = (float)(gyro_y_adc) / 16.4;
gyro_z_act = (float)(gyro_z_adc) / 16.4;
accel_x_act = (float)(accel_x_adc) / 2048;
accel_y_act = (float)(accel_y_adc) / 2048;
accel_z_act = (float)(accel_z_adc) / 2048;
temp_act = ((float)(temp_adc) - 25) / 326.8 + 25;
gxdata = QString::number(gyro_x_act, 'f', 2);
gydata = QString::number(gyro_y_act, 'f', 2);
gzdata = QString::number(gyro_z_act, 'f', 2);
axdata = QString::number(accel_x_act, 'f', 2);
aydata = QString::number(accel_y_act, 'f', 2);
azdata = QString::number(accel_z_act, 'f', 2);
tempdata = QString::number(temp_act, 'f', 2);
}
close(fd);
emit icm20608DataChanged();
}
void Icm20608::timer_timeout()
{
icm20608ReadData();
}
void Icm20608::setCapture(bool str)
{
if (str)
timer->start(500);
else
timer->stop();
}
```
### 13.3 运行效果
在开发板 ALPHA 上加载驱动模块后,每 500ms 读取一次六轴数据与温度,并发出 `icm20608DataChanged()` 信号;界面部分可复用第二十二章的弧形/发光组件自行搭建。
> 归纳:从第十五章到第二十三章,Qt 操作硬件的统一模式是——**确认设备节点 → 定时器 / 事件触发 → C 语言或 QFile 读写 → 通过信号把数据推送给界面**。区别只在于节点路径、数据格式与换算公式。
---
# 第三部分:综合项目实战(第二十四 ~ 二十八章)
> 项目篇不再引入新语法,重点在**流程**:把设备端、云端、客户端串起来。以下每项给出流程图与核心源码;完整工程见 `Qt/04/`。
## 14. 智能家居物联网项目(第二十四章)
### 14.1 项目硬件与准备
- 必备:正点原子 **ATK-ESP8266** 串口转 WIFI 模块(可免费接入原子云)、USB-TTL 模块、T 口 USB 线。PC 上可用串口调试助手直接测试模块。
- 可选:可直接插在 ALPHA/Mini 底板的 ATK-MODULE 串口座上。
- 注意:其他 WIFI 模块(如 Linux USB WIFI)或市售 WIFI 插座**不能**替代,因为它们需要刷特定固件才能接入对应云平台,无法二次开发。
- 原子云上需要预先**分组**,并至少有一个命名为“客厅灯”的设备,记录其**设备编号**与**密码**(默认 `12345678`)。
- Ubuntu 上访问 USB 模块需先放开权限:`sudo chmod 777 /dev/ttyUSB0`。
### 14.2 系统流程
```mermaid
flowchart LR
subgraph Dev["设备端(开发板 / Ubuntu)"]
E["ESP8266 模块
AT 指令"]
L["LED(gpio-leds)"]
E <-->|串口 ttymxc2 / ttyUSB0| L
end
subgraph Cloud["原子云"]
API["HTTPS API
组织/分组/设备/状态"]
WS["WebSocket
wss://cloud.alientek.com"]
end
subgraph App["控制端 UI(01_smarthome)"]
U["开关按钮"]
end
E <-->|WIFI| Cloud
U <-->|HTTPS + WebSocket| Cloud
classDef dev fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef cloud fill:#fef9c3,stroke:#ca8a04,color:#713f12
classDef app fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
class E,L dev
class API,WS cloud
class U app
```
### 14.3 设备端:ESP8266 串口通信(`04/01_smarthome/esp8266/esp8266.cpp`)
默认波特率 115200;开发板上串口用 `ttymxc2`,Ubuntu 上用 `ttyUSB0`。核心是通过 AT 指令依次完成:复位 → 设为 STA 模式 → 连 WIFI → 连原子云 → 定时发心跳 → 接收“开/关”控制 LED。
```cpp
Esp82266::Esp82266(QWidget *parent)
{
Q_UNUSED(parent)
serialPort = new QSerialPort(this); /* 与 ESP8266 通信的串口 */
timer = new QTimer(); /* 定时发送在线心跳包 */
led = new Led(this); /* 收到云指令后控制板子 LED */
#if __arm__
serialPort->setPortName("ttymxc2");
#else
serialPort->setPortName("ttyUSB0");
#endif
serialPort->setBaudRate(115200);
serialPort->setDataBits(QSerialPort::Data8);
serialPort->setParity(QSerialPort::NoParity);
serialPort->setStopBits(QSerialPort::OneStop);
serialPort->setFlowControl(QSerialPort::NoFlowControl);
if (!serialPort->open(QIODevice::ReadWrite))
qDebug() << "串口无法打开!可能正在被使用!" << endl;
connectToClound(); /* 开始连接云 */
connect(serialPort, SIGNAL(readyRead()), this, SLOT(serialPortReadyRead()));
connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
}
/* 解析模块回显,按状态机推进 AT 流程 */
void Esp82266::serialPortReadyRead()
{
QByteArray buf = serialPort->readAll();
QString temp = QString(buf);
readData.append(temp);
qDebug() << temp << endl;
if (readData.contains("ready")) { /* 复位成功 */
sendCmdToEsp8266("AT+CWMODE=1"); /* 设为 STA 模式 */
readData.clear();
}
if (readData.contains("OK") && readData.contains("AT+CWMODE")) {
qDebug() << "设置STA 模式成功" << endl;
/* 请修改为自己的 WIFI 名称与密码 */
sendCmdToEsp8266("AT+CWJAP=\"ALIENTEK-YF\",\"15902020353\"");
readData.clear();
}
if (temp.contains("WIFI GOT IP")) { /* 连上 WIFI */
sleep(2);
/* 原子云设备编号与密码,请改成自己的 */
sendCmdToEsp8266("AT+ATKCLDSTA=\"02314701717851074890\",\"12345678\"");
}
if (temp.contains("CLOUD CONNECTED")) { /* 连上原子云 */
sleep(2);
timer->start(15000); /* 每 15s 发一次心跳 */
}
if (temp == "开")
led->setLedState(true);
else if (temp == "关")
led->setLedState(false);
}
void Esp82266::sendCmdToEsp8266(QString cmd)
{
cmd = cmd + "\r\n";
serialPort->write(cmd.toUtf8());
}
void Esp82266::connectToClound()
{
/* 复位模块(若已连过云,需断电或短接 RST 复位) */
sendCmdToEsp8266("AT+RST");
}
void Esp82266::onTimerTimeOut()
{
sendTextMessage("online"); /* 心跳包 */
qDebug() << "发送设备在线心跳包" << endl;
}
```
### 14.4 控制端:原子云 API + WebSocket(`04/01_smarthome/webapi/webapi.cpp`)
原子云的**账号与设备信息走 HTTPS**,**实时通信用 WebSocket**。流程:用 `token` 请求组织列表 → 取 `org_id` → 请求分组列表 → 取 `group_id` → 请求设备列表 → 取 `device_id / number / name` → 请求设备连接状态 → 用 WebSocket 鉴权并订阅设备消息。
```cpp
Webapi::Webapi(QObject *parent)
{
this->setParent(parent);
groupID.clear(); deviceID.clear(); deviceNumber.clear();
timer = new QTimer();
connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
networkAccessManager = new QNetworkAccessManager(this);
orgURL = "https://cloud.alientek.com/api/orgs";
api_token = "请填写自己的原子云 API TOKEN"; /* 原子云》账号信息 处查看 */
getOrgURL(); /* 获取账号机构列表 */
QUuid uuid = QUuid::createUuid();
random_token = uuid.toString();
webSocket = new QWebSocket();
/* 访问 https/wss 需要加安全配置 */
QSslConfiguration config;
config.setPeerVerifyMode(QSslSocket::VerifyNone);
config.setProtocol(QSsl::TlsV1SslV3);
webSocket->setSslConfiguration(config);
connect(webSocket, SIGNAL(connected()), this, SLOT(webSocketConnected()));
connect(webSocket, SIGNAL(binaryMessageReceived(QByteArray)),
this, SLOT(onBinaryMessageReceived(QByteArray)));
}
/* 从云服务器获取数据(HTTPS GET) */
void Webapi::getDataFromWeb(QUrl url)
{
QNetworkRequest networkRequest;
QSslConfiguration config;
config.setPeerVerifyMode(QSslSocket::VerifyNone);
config.setProtocol(QSsl::TlsV1SslV3);
networkRequest.setSslConfiguration(config);
networkRequest.setUrl(url);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader,
"application/json;charset=UTF-8");
networkRequest.setRawHeader("token", api_token.toLatin1()); /* 账号 token */
QNetworkReply *newReply = networkAccessManager->get(networkRequest);
connect(newReply, SIGNAL(finished()), this, SLOT(replyFinished()));
connect(newReply, SIGNAL(readyRead()), this, SLOT(readyReadData()));
}
/* 拿到 org_id 后拼出分组列表地址,同时用 token 打开 WebSocket 鉴权 */
void Webapi::getID(QString data, QNetworkReply *reply)
{
QRegularExpression pattern("\"id\":(\\d+)");
QRegularExpressionMatchIterator i = pattern.globalMatch(data);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
if (match.hasMatch()) {
if (reply->url() == QUrl(orgURL)) {
org_id = match.captured(1);
groupListUrl = "https://cloud.alientek.com/api/orgs/"
+ org_id + "/grouplist";
getGroupListUrl();
webSocket->open(QUrl(QString(
"wss://cloud.alientek.com/connection/%1/org/%2?token=%3")
.arg(api_token).arg(org_id).arg(random_token)));
}
if (reply->url() == QUrl(groupListUrl)) {
groupID.append(match.captured(1)); /* 存组 ID */
}
if (reply->url() == QUrl(devOfGroupUrl)) {
deviceID.append(match.captured(1)); /* 存设备 ID */
}
}
}
}
/* 定时器:依次拉取“分组下设备 → 设备连接状态 → 订阅设备消息” */
void Webapi::onTimerTimeOut()
{
static int i = 0;
if (i < groupID.count()) {
devOfGroupUrl = "https://cloud.alientek.com/api/orgs/"
+ org_id + "/groups/" + groupID[i] + "/devices";
dataString.clear();
getDevOfGroupUrl();
} else if (i >= groupID.count()
&& i < groupID.count() + deviceID.count()) {
timer->start(1000);
conStateUrl = "https://cloud.alientek.com/api/orgs/"
+ org_id + "/devicestate/" + deviceID[i - groupID.count()];
getConStateUrl();
} else {
/* 订阅所有设备的消息,命令字 0x01 */
for (int j = 0; j < deviceNumber.count(); j++) {
QByteArray cmd;
cmd[0] = 0x01;
sendCmd(deviceNumber[j], cmd);
}
timer->stop();
}
i++;
}
/* 向指定编号设备发送指令:命令字 0x03 + 消息内容(如“开”“关”) */
void Webapi::whichDeviceNameSendCmd(QString name, QString message)
{
for (int i = 0; i < deviceName.count(); i++) {
if (name == deviceName[i]) {
QByteArray cmd;
cmd[0] = 0x03;
sendCmdMessage(deviceNumber[i], cmd, message);
break;
}
}
}
```
控制端 UI(`01_smarthome`)用 `QListWidget` + 自定义 `SwitchButton` 排列“客厅灯/台灯/空调”三个开关,切换时调用 `webapi->whichDeviceNameSendCmd(设备名, "开"/"关")`;收到 `deviceStateChanged` 信号后把对应标签改成“客厅灯|在线”并高亮显示。
### 14.5 综合测试
- **Ubuntu**:`esp8266.pro` 与 `01_smarthome.pro` 分别在两个 Qt Creator 中运行。设备端连上云后每 15s 发心跳;控制端订阅到“在线”后显示状态。点击开关即可远程控制,前提是 WIFI 账号密码、原子云 token、设备编号都改成自己的。
- **开发板**:主控板先联网(网线 / USB WIFI / 4G / SDIO WIFI),可把 ESP8266 插在同一块板上,点击 UI 开关即经原子云转发控制板载 LED。
## 15. 语音识别项目(第二十五章)
### 15.1 流程与约束
使用**百度语音识别**开放平台:注册账号、领取免费额度、创建“中文普通话”应用,得到 **API Key** 与 **Secret Key**。识别流程为:录一段 wav → 用 Key 换 `access_token` → 把音频 POST 到识别服务器 → 解析返回的 JSON 文本。
硬性约束(百度短语音识别):采样率仅支持 **16000 / 8000**、**16bit 单声道**、时长 **≤ 60 秒**、格式支持 **wav/pcm**。正点原子出厂系统支持 wav 录制与播放,正好满足。注意 Linux C++ SDK 仅支持 x86-64,**不支持 ARM**,因此在开发板上要用 HTTP API,而非 SDK。
```mermaid
flowchart LR
A["点击界面图标"] --> B["播放提示音
QSound"]
B --> C["延时 1.5s 后录音 8s
QAudioRecorder → 16k.wav"]
C --> D["getTheResult()
读取 wav 字节流"]
D --> E["HTTPS 换取 access_token"]
E --> F["POST audio/pcm;rate=16000
到 vop.baidu.com/server_api"]
F --> G["解析 result 字段"]
G --> H{"含 开灯/关灯?"}
H -->|开灯| I["LED 点亮"]
H -->|关灯| J["LED 熄灭"]
classDef s fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
class A,B,C,D,E,F,G,H,I,J s
```
### 15.2 录音端:改为 16k 单声道 wav(`04/02_asr_demo/audiorecorder/audiorecorder.cpp`)
```cpp
void AudioRecorder::startRecorder()
{
if (m_audioRecorder->state() == QMediaRecorder::StoppedState) {
m_audioRecorder->setAudioInput(devicesVar.at(0).toString());
QAudioEncoderSettings settings;
settings.setCodec(codecsVar.at(0).toString());
settings.setSampleRate(sampleRateVar[2].toInt()); /* 16000 采样率 */
settings.setBitRate(bitratesVar[0].toInt());
settings.setChannelCount(channelsVar[1].toInt()); /* 1 声道 */
settings.setQuality(QMultimedia::EncodingQuality(qualityVar[0].toInt()));
settings.setEncodingMode(QMultimedia::ConstantQualityEncoding);
/* I.MX6ULL 第 20 个支持的容器格式为 audio/x-wav */
QString container = containersVar.at(20).toString();
m_audioRecorder->setEncodingSettings(settings,
QVideoEncoderSettings(),
container);
/* 录音保存为当前可执行程序目录下的 16k.wav */
m_audioRecorder->setOutputLocation(QUrl::fromLocalFile(tr("./16k.wav")));
m_audioRecorder->record();
}
}
```
### 15.3 识别端:换取 token 并发送音频(`04/02_asr_demo/asr/asr.cpp`)
```cpp
/* asr.h 中定义的关键地址 */
// 换取 token 的接口
const QString token_org =
"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
"&client_id=%1&client_secret=%2&";
const QString api_key = "填写自己的 API Key";
const QString secret_key = "填写自己的 Secret Key";
// 识别服务器接口,dev_pid=1537 表示普通话
const QString server_api =
"http://vop.baidu.com/server_api?dev_pid=1537&cuid=%1&token=%2";
/* 发起网络请求:token 走 GET,识别走 POST */
void Asr::requestNetwork(QString url, QByteArray requestData)
{
QNetworkRequest networkRequest;
QSslConfiguration config;
config.setPeerVerifyMode(QSslSocket::VerifyNone);
config.setProtocol(QSsl::TlsV1SslV3);
networkRequest.setSslConfiguration(config);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader,
"application/json;charset=UTF-8");
/* 音频为 pcm、采样率 16000 */
networkRequest.setRawHeader("Content-Type",
QString("audio/pcm;rate=16000").toLatin1());
networkRequest.setUrl(url);
QNetworkReply *newReply =
networkAccessManager->post(networkRequest, requestData);
connect(newReply, SIGNAL(finished()), this, SLOT(replyFinished()));
connect(newReply, SIGNAL(readyRead()), this, SLOT(readyReadData()));
}
/* 读取响应:token 响应取 access_token;识别响应取 result */
void Asr::readyReadData()
{
QNetworkReply *reply = (QNetworkReply *)sender();
QByteArray data = reply->readAll();
if (reply->url() == QUrl(tokenUrl)) {
accessToken = getJsonValue(data, "access_token");
qDebug() << "获取token成功,可以调用getTheResult设置语音路径返回结果" << endl;
}
if (reply->url() == QUrl(serverApiUrl)) {
QString temp = getJsonValue(data, "result");
emit asrReadyData(temp);
}
}
/* 指定录音文件,读取字节流并 POST 到识别服务器 */
void Asr::getTheResult(QString fileName)
{
file.setFileName(fileName);
if (!file.exists()) { qDebug() << "文件" << fileName << "不存在" << endl; return; }
file.open(QIODevice::ReadOnly);
QByteArray requestData = file.readAll();
file.close();
serverApiUrl = QString(server_api)
.arg(QHostInfo::localHostName()).arg(accessToken);
requestNetwork(serverApiUrl, requestData);
}
```
### 15.4 UI 与联动(`02_asr_demo/mainwindow.cpp`)
```cpp
/* 点击图标:播放音效,延时 1.5s 再录音,避免把提示音录进去 */
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
if (watched == movieLabel && event->type() == QEvent::MouseButtonPress) {
QSound::play(":/audio/sound.wav");
if (myMovie->state() != QMovie::Running) {
timer2->start(1500);
textLabel->setText("正在听您说话,请继续...");
myMovie->start();
}
}
return QWidget::eventFilter(watched, event);
}
void MainWindow::onTimer2TimeOut() /* 1.5s 后开始录音 */
{
timer1->start(8000); /* 录 8s 短语音 */
myAudioRecorder->startRecorder();
timer2->stop();
}
void MainWindow::onTimer1TimeOut() /* 8s 到,停止录音并识别 */
{
myAudioRecorder->stopRecorder();
textLabel->setText("正在识别,请稍候...");
timer1->stop();
myMovie->stop();
myAsr->getTheResult(QCoreApplication::applicationDirPath() + "/16k.wav");
timer3->start(30000);
}
/* 识别结果联动 LED */
void MainWindow::onAsrReadyData(QString str)
{
if (str.contains("开灯"))
myLed->setLedState(true);
else if (str.contains("关灯"))
myLed->setLedState(false);
textLabel->setText("识别结果是:\n" + str);
textLabel->adjustSize();
}
```
运行前需在开发板运行 `/home/root/shell/audio/mic_in_config.sh` 打开麦头,并确保开发板联网。点击后等 1.5~2s 再说话,约 8s 后返回识别结果,准确率较高。
## 16. APP 主界面开发项目(第二十六章)
### 16.1 滑动页面(`slidepage`)
Qt C++(非 QML)没有现成的手机式滑动页面。正点原子封装了一个 `SlidePage` 类:**`QScrollArea` + `QScroller` 手势 + `QPropertyAnimation` 动画**,通过 `addPage()` 添加页面,页面数 ≥ 2 即可滑动。原理是用水平滚动条的 `value` 控制当前页,松手后按滑动距离判断翻页方向并做动画。
```cpp
SlidePage::SlidePage(QWidget *parent)
: QWidget(parent), pageIndex(0), pageCount(0), draggingFlag(false)
{
pageIndicator.clear();
this->setMinimumSize(400, 300);
this->setAttribute(Qt::WA_TranslucentBackground, true);
scrollArea = new QScrollArea(this);
scrollArea->setAlignment(Qt::AlignCenter);
mainWidget = new QWidget();
mainWidget->setStyleSheet("background: transparent");
scrollArea->setWidget(mainWidget);
/* 关闭滚动条显示 */
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
/* 关键:用 QScroller 让 QScrollArea 支持手势滑动 */
scroller = QScroller::scroller(scrollArea);
QScroller::ScrollerGestureType gesture = QScroller::LeftMouseButtonGesture;
scroller->grabGesture(scrollArea, gesture);
QScrollerProperties properties = scroller->scrollerProperties();
properties.setScrollMetric(QScrollerProperties::SnapTime, 0.5); /* 滑动时间 */
properties.setScrollMetric(QScrollerProperties::MinimumVelocity, 1); /* 滑动速度 */
scroller->setScrollerProperties(properties);
hBoxLayout = new QHBoxLayout();
hBoxLayout->setContentsMargins(0, 0, 0, 0);
hBoxLayout->setSpacing(0);
mainWidget->setLayout(hBoxLayout);
/* 定时器:按下超过 300ms 视为拖动而非点击 */
timer = new QTimer(this);
connect(scrollArea->horizontalScrollBar(), SIGNAL(valueChanged(int)),
this, SLOT(hScrollBarValueChanged(int)));
connect(scroller, SIGNAL(stateChanged(QScroller::State)),
this, SLOT(onStateChanged(QScroller::State)));
connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
connect(this, SIGNAL(currentPageIndexChanged(int)),
this, SLOT(onCurrentPageIndexChanged(int)));
}
/* 添加一页,并在底部放一个分页指示点 */
void SlidePage::addPage(QWidget *w)
{
hBoxLayout->addWidget(w);
pageCount++;
QLabel *label = new QLabel();
label->setPixmap(QPixmap(":/icons/indicator1.png"));
pageIndicator.append(label);
bottomHBoxLayout->addWidget(label);
}
void SlidePage::resizeEvent(QResizeEvent *event)
{
Q_UNUSED(event)
scrollArea->resize(this->size());
/* mainWidget 需要按页数横向撑开 */
mainWidget->resize(this->width() * pageCount, this->height() - 4);
if (pageCount == 0)
qDebug() << "当前页面总数为0,请使用addPage()方法添加页面再使用!" << endl;
else
onCurrentPageIndexChanged(0);
bottomWidget->setGeometry(0, this->height() - 20, this->width(), 20);
}
/* 松手:判断翻页方向并用属性动画平滑滚动 */
void SlidePage::onStateChanged(QScroller::State state)
{
static int pressedValue = 0, releasedValue = 0, currentPageIndex = 0;
if (pageCount == 0) return;
if (state == QScroller::Inactive) { /* 松开 */
timer->stop();
releasedValue = QCursor::pos().x();
if (pressedValue == releasedValue) return;
if (!draggingFlag) {
if (pressedValue - releasedValue > 20 && currentPageIndex == pageIndex)
pageIndex++;
else
pageIndex--;
}
if (pageIndex == -1) pageIndex = 0;
if (pageIndex >= pageCount) pageIndex = pageCount - 1;
QPropertyAnimation *animation =
new QPropertyAnimation(scrollArea->horizontalScrollBar(), "value");
animation->setDuration(200);
animation->setStartValue(scrollArea->horizontalScrollBar()->value());
animation->setEasingCurve(QEasingCurve::OutCurve);
animation->setEndValue(pageIndex * this->width());
animation->start();
if (currentPageIndex != pageIndex)
emit currentPageIndexChanged(pageIndex);
pressedValue = releasedValue = 0;
draggingFlag = false;
}
if (state == QScroller::Pressed) { /* 按下 */
pressedValue = QCursor::pos().x();
currentPageIndex = scrollArea->horizontalScrollBar()->value() / this->width();
timer->start(300); /* 超过 300ms 视为拖动 */
}
}
```
### 16.2 主界面组装(`03_appmainview/widget.cpp`)
```cpp
AppMainView::AppMainView(QWidget *parent)
{
this->setParent(parent);
this->setGeometry(0, 0, 800, 480);
this->setMinimumSize(800, 480);
bgWidget = new QWidget(this);
bgWidget->setStyleSheet("border-image: url(:/images/bg.png)");
mySlidePage = new SlidePage(this); /* 使用滑动页面类 */
mySlidePage->resize(this->size());
for (int i = 0; i < 3; i++) {
appDemo[i] = new AppDemo();
mySlidePage->addPage(appDemo[i]); /* 直接 addPage 添加页面 */
}
}
```
`appdemo` 是一个车载音乐主界面的**纯界面**示例(顶部导航、中间专辑/分类/榜单、底部功能按钮),主要用嵌套布局与 `style.qss` 美化实现,不实现功能。底部三个分页逗点表示共有 3 页,左右滑动即可切换。
## 17. 车牌识别项目(第二十七章)
### 17.1 流程
与语音识别类似,调用**百度 OCR 车牌识别**接口:把本地车牌图片(JPG)转成 base64 后 POST 到服务器,解析返回的 `words_result.number` 得到车牌号。使用前需在百度文字识别控制台创建应用,领取“车牌识别”免费资源,并填写自己的 API Key 与 Secret Key。
```mermaid
flowchart LR
A["image/carlpr.jpg"] --> B["QImage/QFile 读取
toBase64().toPercentEncoding()"]
B --> C["image= + base64
POST license_plate 接口"]
C --> D["解析 words_result"]
D --> E["取 number 字段"]
E --> F["界面显示车牌号"]
classDef s fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
class A,B,C,D,E,F s
```
### 17.2 识别端核心代码(`04/04_lpr_demo/ocr/ocr.cpp`)
```cpp
/* ocr.h 中的关键地址 */
const QString token_org =
"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
"&client_id=%1&client_secret=%2&";
const QString api_key = "填写自己的APP KEY";
const QString secret_key = "填写自己的APP SECRET";
const QString server_api =
"https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate?access_token=%1";
/* 读取图片文件,转 base64 后作为表单体发送 */
void Ocr::getTheResult(QString fileName)
{
QFile file(fileName);
if (!file.exists()) { qDebug() << fileName << "不存在!" << endl; return; }
QByteArray requestData;
file.open(QIODevice::ReadOnly);
requestData = file.readAll();
file.close();
/* 转成 base64 再做 URL 百分号编码 */
QByteArray buf64 = requestData.toBase64().toPercentEncoding();
QByteArray body = "image=" + buf64;
serverApiUrl = QString(server_api).arg(accessToken);
requestNetwork(serverApiUrl, body);
}
/* 也支持直接传 QImage(可用摄像头采集的图片) */
void Ocr::getTheResult(QImage image)
{
QByteArray requestData;
QBuffer buffer(&requestData);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "JPEG", -1);
QByteArray buf64 = requestData.toBase64().toPercentEncoding();
QByteArray body = "image=" + buf64;
serverApiUrl = QString(server_api).arg(accessToken);
requestNetwork(serverApiUrl, body);
}
/* 解析两级 JSON:words_result -> number */
QString Ocr::getJsonValue(QByteArray ba, QString key1, QString key2)
{
QJsonParseError parseError;
QJsonDocument jsonDocument = QJsonDocument::fromJson(ba, &parseError);
if (parseError.error == QJsonParseError::NoError
&& jsonDocument.isObject()) {
QJsonObject jsonObj = jsonDocument.object();
if (jsonObj.contains(key1)) {
QJsonObject jsonArrObj = jsonObj.value(key1).toObject();
if (jsonArrObj.contains(key2))
return jsonArrObj.value(key2).toString();
}
}
return nullptr;
}
```
### 17.3 界面与测试(`04_lpr_demo/mainwindow.cpp`)
```cpp
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
ocr = new Ocr(this);
this->setGeometry(0, 0, 800, 480);
QLabel *label = new QLabel(this);
resultLabel = new QLabel(this);
label->setAlignment(Qt::AlignCenter);
pushButton = new QPushButton(this);
pushButton->setText("开始识别");
resultLabel->move(pushButton->width(), 0);
resultLabel->resize(300, 30);
QPixmap pixmap;
if (pixmap.load("image/carlpr.jpg")) {
label->setPixmap(pixmap);
} else {
qDebug() << "未找要识别的车牌图片!请检查可执行程序下是否有image/carlpr.jpg" << endl;
}
this->setCentralWidget(label);
connect(pushButton, SIGNAL(clicked(bool)), this, SLOT(pushButtonClicked()));
connect(ocr, SIGNAL(ocrReadyData(QString)), this, SLOT(disPlaylprResult(QString)));
}
void MainWindow::pushButtonClicked()
{
/* 识别可执行程序同级目录 image/carlpr.jpg */
ocr->readyToDetection("image/carlpr.jpg");
}
```
运行前把 `image` 文件夹拷到可执行程序同级目录;需联网;适用于 Ubuntu18 与 I.MX6U 开发板(Ubuntu16 会报 OpenSSL 版本错误)。若识别不到则提示“未识别到车牌!”。也可改为用摄像头拍照后传 `QImage`。
## 18. 视频监控项目(第二十八章)
### 18.1 技术选型与流程
流媒体领域常用 **RTSP**(基于文本、实时性好、适合视频监控/聊天,但实现复杂)与 **RTMP**(Adobe 提出、浏览器加载 flash 即可播放、低延迟)。I.MX6U 出厂系统支持 `RTMP + FFmpeg + Nginx` 推流。但本章用**最简单的方法**:服务端采集摄像头帧,经 UDP 广播发给客户端显示,不做视频编码,适合学习与毕设,缺点是数据量大。
使用 **OV5640 摄像头**(支持 RGB 格式采集,与 Qt 显示格式一致,CPU 开销小);USB 摄像头采集 YUYV,转 RGB 会大量消耗 CPU,故不适配。使用 `QUdpSocket` 广播,可有多个客户端同时接收。
```mermaid
flowchart LR
CAM["OV5640 摄像头
/dev/video1"] -->|V4L2 采集 RGB565| TH["CaptureThread
(QThread 子线程)"]
TH -->|imageReady| LOCAL["服务端本地显示"]
TH -->|JPEG→base64→UDP 广播:8888| NET(("局域网"))
NET --> C1["客户端1
QUdpSocket bind 8888"]
NET --> C2["客户端2"]
NET --> C3["客户端3"]
classDef s fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
class CAM,TH,LOCAL,C1,C2,C3 s
```
### 18.2 服务端:采集线程(`video_server/capture_thread.h/.cpp`)
```cpp
/* capture_thread.h 关键定义 */
#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, startBroadcast = false, startLocalDisplay = false;
void run() override;
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; }
};
```
```cpp
/* capture_thread.cpp:V4L2 采集主循环(简化自源码,原理同 C 应用编程 V4L2 章节) */
void CaptureThread::run()
{
#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) return;
/* 设置格式:640x480,RGB565 */
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)) { close(video_fd); return; }
/* 申请 3 个 mmap 缓冲区 */
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)) 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;
ioctl(video_fd, VIDIOC_QUERYBUF, &buf);
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) return;
}
for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
buf.index = n_buf;
ioctl(video_fd, VIDIOC_QBUF, &buf);
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(video_fd, VIDIOC_STREAMON, &type);
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)) 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;
QByteArray byte;
QBuffer buff(&byte);
qImage.save(&buff, "JPEG", -1); /* 压缩为 JPEG */
QByteArray base64Byte = byte.toBase64(); /* 转 base64 */
/* 以广播形式发送到 8888 端口 */
udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(),
QHostAddress::Broadcast, 8888);
}
ioctl(video_fd, VIDIOC_QBUF, &buf);
}
}
msleep(800);
for (int i = 0; i < VIDEO_BUFFER_COUNT; i++)
munmap(bufs_info[i].start, buf.length);
close(video_fd);
#endif
}
```
服务端界面用两个 `QCheckBox` 分别控制“本地图像显示”和“UDP 广播”,一个按钮“开启采集摄像头数据”。注意:同时开启本地显示与广播会因 I.MX6U 性能有限而略卡,单独开启任一功能则很流畅。
### 18.3 客户端:接收并显示(`video_client/mainwindow.cpp`)
```cpp
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);
}
void MainWindow::videoUpdate()
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
udpSocket->readDatagram(datagram.data(), datagram.size());
/* base64 解码后再还原为图像 */
QByteArray decryptedByte = QByteArray::fromBase64(datagram.data());
QImage image;
image.loadFromData(decryptedByte);
videoLabel->setPixmap(QPixmap::fromImage(image));
}
```
客户端可在另一块开发板、Ubuntu 或 Windows 上运行,且可多个客户端同时运行;需与服务端在同一局域网。客户端显示的图像与服务端同步,效果良好。
---
# 面试题(5 道)
### Q1. 嵌入式 Qt 交叉编译有哪两种方式?qmake 与 make 各自做什么?
**答案要点**:命令行编译和 Qt Creator 交叉套件;qmake 读 `.pro` 生成 Makefile,make 调用交叉编译器完成编译链接。
**详细解答**:命令行方式用交叉工具链的 `qmake` 生成 Makefile 后 `make`;Qt Creator 方式配置 ARM Kit(Qt Versions + 编译器)后可远程运行调试。跨平台的关键正是“换一个 qmake 就生成另一平台的 Makefile”。运行时用 `-platform linuxfb`(帧缓冲)或 `-platform eglfs`(EGL/OpenGL ES 全屏)选择显示插件。若远程运行结果异常(环境变量/路径问题),直接拷贝可执行文件到板上运行更稳妥。
> ⚠️ **来源说明**:平台插件部分不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
### Q2. 为什么 Qt 控制 LED/BEEP 只要读写一个文件?写出关键步骤。
**答案要点**:Linux 一切皆文件,出厂内核把外设注册为 gpio-leds;Qt 用 QFile 读写 `brightness` 节点,写 "1"/"0" 控制。
**详细解答**:LED 节点 `/sys/devices/platform/leds/leds/sys-led/brightness`,BEEP 节点 `.../beep/brightness`。出厂系统 LED 默认是心跳触发,需先 `echo none > /sys/class/leds/sys-led/trigger`。`setLedState()` 先 `getLedState()` 读取当前值再取反写入,避免外部改动导致反转失效。AP3216C 等传感器也可用 C 的 `open()/read()` 读取 `/sys/class/misc/ap3216c/*`。
### Q3. Qt 串口与 CAN 编程的流程分别是什么?各需要哪些 .pro 模块?
**答案要点**:串口用 `QSerialPortInfo` 扫描 + `QSerialPort` 配置打开,模块 `serialport`;CAN 用 `QCanBus`/`QCanBusDevice`,模块 `serialbus`,Linux 下插件必须是 socketcan。
**详细解答**:串口流程:`availablePorts()` 列串口 → `setPortName/setBaudRate/setDataBits/setParity/setStopBits/setFlowControl` → `open(QIODevice::ReadWrite)` → `readyRead()` 收、`write()` 发。I.MX6U 上只能用 `ttymxc2`。CAN 流程:先 `ip link set up can0 type can bitrate 1000000 restart-ms 100` 初始化系统 CAN(`QCanBusDevice::BitRateKey` 不能设比特率),再用 `QCanBus::instance()->createDevice("socketcan","can0")` 并 `connectDevice()`,`framesReceived()` 收、`writeFrame()` 发,帧 ID 用 16 进制、数据每字节空格分隔。
### Q4. 视频监控/摄像头为什么必须开子线程?Qt 两种线程写法有何区别?
**答案要点**:采集与编码耗时会阻塞主线程导致界面卡顿;继承 QThread 只有 run() 在新线程,moveToThread 可让整个 QObject 的槽函数都在新线程执行。
**详细解答**:I.MX6U 是单核 A7,处理摄像头大图像吃力,必须把 V4L2 采集放在 `CaptureThread::run()` 中,界面通过信号接收 `QImage`。第二种写法是写一个继承 QObject 的 Worker,用 `moveToThread()` 把它移到 QThread,所有耗时操作写成槽函数供主线程调用,更灵活。退出线程用 `quit()` + `wait()`,不要用不安全的 `terminate()`。
### Q5. 嵌入式 Qt 如何调用云端 API(智能家居/语音识别/车牌识别)?
**答案要点**:用 Qt Network 走 HTTPS 获取 token 与数据,实时通信用 QWebSocket;音频/图片需符合接口的格式约束。
**详细解答**:`QNetworkAccessManager` + `QNetworkRequest`/`QNetworkReply` 发 HTTPS 请求,访问 https 时需设置 `QSslConfiguration`(`VerifyNone`、`TlsV1SslV3`);原子云的实时消息用 `QWebSocket`(`wss://cloud.alientek.com/connection/...`)并鉴权后订阅(命令字 0x01)、下发(0x03)。百度语音要求 16000/8000 采样率、16bit 单声道、≤60s 的 wav,用 `audio/pcm;rate=16000`;车牌识别把图片 `toBase64().toPercentEncoding()` 后 POST。`.pro` 需加 `network` 模块。
---
**内容来源**:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第十四章 I.MX6U Qt 开发、第十五章 Qt 控制 LED、第十六章 Qt 控制 BEEP、第十七章 Serial Port、第十八章 CAN Bus、第十九章 Camera、第二十章 USB Bluetooth、第二十一章 USER-KEY、第二十二章 AP3216C、第二十三章 ICM20608、第二十四章 智能家居物联网项目、第二十五章 语音识别项目、第二十六章 APP主界面开发项目、第二十七章 车牌识别项目、第二十八章 视频监控项目;配套例程 `Embedded-Qt-Tutorial/Qt/03`、`Qt/04`;平台插件 `-platform linuxfb/-platform eglfs` 部分为扩展知识。