--- 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` 部分为扩展知识。