08-嵌入式Qt交叉编译与项目实战.md 111 KB


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 开发板的出厂系统(驱动与应用必须配套,请勿随意更换内核或设备树)。

学习地图

flowchart TD
    A["Qt 应用开发知识<br/>(第 1~7 篇)"] --> B["第十四章:I.MX6U Qt 开发<br/>交叉编译环境"]
    B --> C1["命令行编译"]
    B --> C2["Qt Creator 交叉套件"]
    C1 --> D["Qt 控制硬件<br/>第十五~二十三章"]
    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["综合项目<br/>第二十四~二十八章"]
    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 交叉编译流程如下:

flowchart LR
    A["Ubuntu 上的<br/>Qt 工程 .pro"] --> B["交叉 qmake<br/>生成 Makefile"]
    B --> C["make<br/>调用 arm 交叉编译器"]
    C --> D["生成 ARM 可执行文件"]
    D --> E["scp / U 盘 / NFS<br/>拷贝到开发板"]
    E --> F["开发板上运行<br/>./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。

# 交叉工具链中的 qmake(路径以实际安装为准)
/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh &   # 带环境变量启动 Qt Creator

工程文件 .pro 是最核心的配置,几个常用条目:

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 可多核并行编译:

mkdir build && cd build
/opt/Qt5.12.9/Tools/QtCreator/bin/qmake ..   # 或交叉工具链中的 qmake
make -j8

2.3 命令行编译的整体步骤

# 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 上最常用的是 linuxfbeglfs,可通过 -platform 参数指定:

./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 工程为例:

#include "mainwindow.h"
#include <QApplication>

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 的控制接口为:

/sys/devices/platform/leds/leds/sys-led/brightness

向该节点写 "1" 点亮,写 "0" 熄灭。注意出厂系统默认把 sys-led 的触发方式设为心跳(heartbeat),要想手动控制,需先把触发方式改为 none

echo none > /sys/class/leds/sys-led/trigger

5.2 完整源码

01_led.pro(模块:core gui widgets

QT       += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG   += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include <QFile>

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

#include "mainwindow.h"
#include <QDebug>
#include <QGuiApplication>
#include <QScreen>
#include <QRect>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    /* 获取屏幕的分辨率,Qt 官方建议使用这种方法获取屏幕分辨率,
     * 防止多屏设备导致对应不上。注意,这是获取整个桌面系统的分辨率
     */
    QList<QScreen *> 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 完全一致:

/sys/devices/platform/leds/leds/beep/brightness

6.2 完整源码

02_beep.pro:模块同 01_ledcore gui widgets)。

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include <QFile>

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 例程结构完全一致,仅设备节点与提示文本不同)

/* 与 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

# 添加串口模块支持
QT       += core gui serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG   += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QPushButton>
#include <QTextBrowser>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QGridLayout>
#include <QMessageBox>
#include <QDebug>

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

#include "mainwindow.h"
#include <QDebug>
#include <QGuiApplication>
#include <QScreen>
#include <QRect>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    layoutInit();          /* 布局初始化 */

    scanSerialPort();      /* 扫描系统的串口 */

    baudRateItemInit();    /* 波特率项初始化 */
    dataBitsItemInit();    /* 数据位项初始化 */
    parityItemInit();      /* 校验位项初始化 */
    stopBitsItemInit();    /* 停止位项初始化 */
}

void MainWindow::layoutInit()
{
    QList<QScreen *> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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 开启:

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

QT       += core gui serialbus
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG   += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QCanBusDevice>
#include <QCanBus>
#include <QPushButton>
#include <QTextBrowser>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QGridLayout>
#include <QMessageBox>
#include <QDebug>

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

#include "mainwindow.h"
#include <QGuiApplication>
#include <QScreen>

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<QCanBusDeviceInfo> 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<int> 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)

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

#ifndef CAMERA_H
#define CAMERA_H

#include <QImage>
#include <QTimer>

/* 使用命名空间 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

#include "camera.h"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <QImage>
#include <QDebug>

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

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QComboBox>
#include <QPushButton>
#include <QLabel>
#include <QScrollArea>
#include <QDebug>

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(核心槽函数;布局初始化见例程源码)

#include "mainwindow.h"
#include <QGuiApplication>
#include <QScreen>
#include <QFile>
#include <QPixmap>
#include <QBuffer>
#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 个信号槽把界面与逻辑连起来:

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

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(客户端,核心代码)

/* 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(服务端,核心代码)

/* 创建 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(远程发现,核心代码)

/* 初始化本地蓝牙服务发现代理 */
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<QString, QBluetoothServiceInfo> 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 + 扫描/停止/连接/断开按钮。核心槽函数如下:

/* 初始化本地蓝牙,同时作为服务端 */
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<QString,QBluetoothServiceInfo>
            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

QT       += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG   += c++11
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QKeyEvent>
#include <QLabel>
#include <QDebug>
#include <QEvent>

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

#include "mainwindow.h"
#include <QGuiApplication>
#include <QScreen>
#include <QRect>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QList<QScreen *> 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 驱动并注册为杂项设备,节点位于:

/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

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 节点)

// ap3216c.h
#ifndef AP3216C_H
#define AP3216C_H

#include <QObject>
#include <QTimer>

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
// ap3216c.cpp
#include "ap3216c.h"
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <QDebug>

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)

// arcgraph.h
#ifndef ARCGRAPH_H
#define ARCGRAPH_H

#include <QWidget>
#include <QPainter>
#include <QPaintEvent>

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
// 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 做模糊发光)

// glowtext.h
#ifndef GLOWTEXT_H
#define GLOWTEXT_H

#include <QWidget>
#include <QLabel>

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
// glowtext.cpp(关键:同一段文字用两个 QLabel 叠加,底层加模糊特效做发光)
#include "glowtext.h"
#include <QGraphicsBlurEffect>

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(把三个数据分别映射成弧形角度)

// 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();
};
// 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

// icm20608.h
#ifndef ICM20608_H
#define ICM20608_H

#include <QObject>
#include <QTimer>

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
// icm20608.cpp
#include "icm20608.h"
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <QDebug>

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 系统流程

flowchart LR
    subgraph Dev["设备端(开发板 / Ubuntu)"]
        E["ESP8266 模块<br/>AT 指令"]
        L["LED(gpio-leds)"]
        E <-->|串口 ttymxc2 / ttyUSB0| L
    end
    subgraph Cloud["原子云"]
        API["HTTPS API<br/>组织/分组/设备/状态"]
        WS["WebSocket<br/>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。

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 鉴权并订阅设备消息。

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 综合测试

  • Ubuntuesp8266.pro01_smarthome.pro 分别在两个 Qt Creator 中运行。设备端连上云后每 15s 发心跳;控制端订阅到“在线”后显示状态。点击开关即可远程控制,前提是 WIFI 账号密码、原子云 token、设备编号都改成自己的。
  • 开发板:主控板先联网(网线 / USB WIFI / 4G / SDIO WIFI),可把 ESP8266 插在同一块板上,点击 UI 开关即经原子云转发控制板载 LED。

15. 语音识别项目(第二十五章)

15.1 流程与约束

使用百度语音识别开放平台:注册账号、领取免费额度、创建“中文普通话”应用,得到 API KeySecret Key。识别流程为:录一段 wav → 用 Key 换 access_token → 把音频 POST 到识别服务器 → 解析返回的 JSON 文本。

硬性约束(百度短语音识别):采样率仅支持 16000 / 800016bit 单声道、时长 ≤ 60 秒、格式支持 wav/pcm。正点原子出厂系统支持 wav 录制与播放,正好满足。注意 Linux C++ SDK 仅支持 x86-64,不支持 ARM,因此在开发板上要用 HTTP API,而非 SDK。

flowchart LR
    A["点击界面图标"] --> B["播放提示音<br/>QSound"]
    B --> C["延时 1.5s 后录音 8s<br/>QAudioRecorder → 16k.wav"]
    C --> D["getTheResult()<br/>读取 wav 字节流"]
    D --> E["HTTPS 换取 access_token"]
    E --> F["POST audio/pcm;rate=16000<br/>到 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

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

/* 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

/* 点击图标:播放音效,延时 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 控制当前页,松手后按滑动距离判断翻页方向并做动画。

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

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。

flowchart LR
    A["image/carlpr.jpg"] --> B["QImage/QFile 读取<br/>toBase64().toPercentEncoding()"]
    B --> C["image= + base64<br/>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

/* 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

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 广播,可有多个客户端同时接收。

flowchart LR
    CAM["OV5640 摄像头<br/>/dev/video1"] -->|V4L2 采集 RGB565| TH["CaptureThread<br/>(QThread 子线程)"]
    TH -->|imageReady| LOCAL["服务端本地显示"]
    TH -->|JPEG→base64→UDP 广播:8888| NET(("局域网"))
    NET --> C1["客户端1<br/>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

/* 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; }
};
/* 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

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/triggersetLedState()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/setFlowControlopen(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 时需设置 QSslConfigurationVerifyNoneTlsV1SslV3);原子云的实时消息用 QWebSocketwss://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/03Qt/04;平台插件 -platform linuxfb/-platform eglfs 部分为扩展知识。