|
@@ -0,0 +1,3230 @@
|
|
|
|
|
+---
|
|
|
|
|
+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 应用开发知识<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 交叉编译流程如下:
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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。
|
|
|
|
|
+
|
|
|
|
|
+```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 <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 的控制接口为:
|
|
|
|
|
+
|
|
|
|
|
+```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 <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**
|
|
|
|
|
+
|
|
|
|
|
+```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 完全一致:
|
|
|
|
|
+
|
|
|
|
|
+```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 <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` 例程结构完全一致,仅设备节点与提示文本不同)
|
|
|
|
|
+
|
|
|
|
|
+```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 <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**
|
|
|
|
|
+
|
|
|
|
|
+```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 开启:
|
|
|
|
|
+
|
|
|
|
|
+```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 <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`)
|
|
|
|
|
+
|
|
|
|
|
+```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)
|
|
|
|
|
+
|
|
|
|
|
+```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 <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**
|
|
|
|
|
+
|
|
|
|
|
+```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**
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+#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**(核心槽函数;布局初始化见例程源码)
|
|
|
|
|
+
|
|
|
|
|
+```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 个信号槽把界面与逻辑连起来:
|
|
|
|
|
+
|
|
|
|
|
+```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<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` + 扫描/停止/连接/断开按钮。核心槽函数如下:
|
|
|
|
|
+
|
|
|
|
|
+```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<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`)
|
|
|
|
|
+
|
|
|
|
|
+```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 <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**
|
|
|
|
|
+
|
|
|
|
|
+```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 驱动并注册为**杂项设备**,节点位于:
|
|
|
|
|
+
|
|
|
|
|
+```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 <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
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+// 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)
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+// 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
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```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 <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
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+// 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**(把三个数据分别映射成弧形角度)
|
|
|
|
|
+
|
|
|
|
|
+```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 <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
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+// 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 系统流程
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+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。
|
|
|
|
|
+
|
|
|
|
|
+```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["播放提示音<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`)
|
|
|
|
|
+
|
|
|
|
|
+```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 读取<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`)
|
|
|
|
|
+
|
|
|
|
|
+```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 摄像头<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`)
|
|
|
|
|
+
|
|
|
|
|
+```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` 部分为扩展知识。
|