**前言:**本文为手把手教学的基于 ESP32 与 Qt Creator 的 WIFI 空间透视项目,项目使用的 MCU 为乐鑫的 ESP32_CAM 搭配 Qt Creator 制作上位机,Qt 的版本为 Qt 5.9.0。本项目利用 ESP32_CAM 连接指定 WiFi,实时采集 RSSI 数据,计算 RSSI 波动值,以固定格式通过串口输出,为 Qt 端可视化热力图提供原始数据,本质是"利用 RSSI 波动替代视觉,实现无接触空间感知"。本项目制作的为简陋版本,最终的 WIFI 空间透视能力一般。希望这篇博文能给读者朋友的工程项目给予些许帮助,Respect(代码开源)!
**硬件与软件:**ESP32_CAM、iKun ESP32 WIFI Perspective、Arduino IDE、Qt 5.9.0
项目结果图:


一、ESP32 WIFI 空间透视概述
1.1 WIFI 空间透视介绍
WiFi 空间透视技术(又称 WiFi 射频感知 / 穿墙透视),是利用普通 WiFi 信号的信道状态信息(CSI),结合 AI 算法,实现无摄像头、穿墙、非接触式的空间感知与人体姿态识别的技术。它并非真正的 "视觉穿透",而是通过解析无线电波的扰动来 "看见" 空间与人体。

★WIFI 空间透视的核心原理:
1、多径传播:信号从发射端(路由器 / ESP32)到接收端(ESP32 / 手机),除了直射路径,还会经墙壁、家具、人体反射 / 散射形成 "多径信号";
2、信号调制:人体 / 物体的存在会改变多径信号的传播路径、强度和相位 ------ 比如人体遮挡会导致直射信号衰减,人体移动会改变反射信号的相位;
3、信号反推:通过采集 WiFi 信号的特征参数(RSSI/CSI),分析其变化规律,即可反推出空间内 "扰动源"(人体 / 物体)的状态。
总而言之,所谓的 WIFI 空间透视技术就是利用 WIFI 射频信号的特殊物理量来判断所探测空间内是否存在干扰,并利用视觉推测机制将干扰源给标定为有物体的存在。进阶版本的 WIFI 空间透视可以利用多节点传感器融合和 AI 大模型推理的加持下可以很精确的透视出空间内各个物体的外形特征!

1.2 ESP32 WIFI 空间透视项目概述
本篇博客作者制作的是非常简陋版本的 ESP32 WIFI 空间透视项目,故 ESP32 WIFI 空间透视的效果非常的一般,更多的检测到探测空间是否存在运动的物体。读者朋友们可以简单的复刻感受一下,有能力的话可以在优化一下代码或者 WIFI 信号反馈的信息量实现更精准的空间透视!
本项目是一款低成本、入门级 WiFi 空间透视系统,基于 ESP32_CAM 开发板、Arduino 开发环境和 Qt 可视化平台,实现无摄像头、非接触式的空间感知功能。项目无需复杂硬件改造,无需专业技术储备,新手可快速复刻,核心是利用 WiFi 信号的反射、衰减特性,捕捉人体/物体对信号的干扰,通过可视化方式呈现空间内的信号分布,间接实现"透视"效果。
项目整体架构分为两大模块,协同工作完成空间感知与可视化:
数据采集模块(ESP32_CAM + Arduino):负责扫描指定 WiFi 热点,实时采集信号强度(RSSI)数据,通过串口以 JSON 格式发送至上位机,确保数据传输稳定、格式规范,为后续可视化提供基础数据源。
数据可视化模块(Qt):负责接收串口传输的 RSSI 数据,完成数据解析、实时绘制空间热图渲染,直观呈现信号波动规律,让用户清晰感知空间内人体/物体的存在与移动。

二、ESP32 WIFI 空间透视代码
2.1 代码库文件引入与变量定义
1、本篇博客工程代码的基本框架可以从 Arduino IDE 的示例教程中获取,后续的项目代码在此基础上进行修改删减即可。读者朋友在正确导入 ESP32 工程库后,按照下图去创建项目的基础框架:

2、点击「工具」→「开发板」→「开发板管理器」,选择自己拥有的 ESP32 开发板即可,作者这边选择乐鑫的 ESP32_CAM;

2.2 ESP32 WIFI RSSI 数值函数
ESP32_CAM 代码的核心:利用 ESP32_CAM 连接指定 WiFi,实时采集 RSSI 数据,计算 RSSI 波动值,以固定格式通过串口输出,为 Qt 端可视化(热图、曲线)提供原始数据,本质是 "用 RSSI 波动替代视觉,实现无接触空间感知"。
作者简单介绍一下 ESP32_CAM 端的核心代码,核心算法:RSSI 波动计算(关键函数),calculate_rssi_fluctuation() 是空间感知的核心,作用是通过历史 RSSI 数据计算 "波动值",判断空间是否有物体移动:1、先计算 50 个历史 RSSI 的平均值;2、再计算每个历史数据与平均值的方差(方差越大,说明 RSSI 变化越剧烈);3、最终返回方差结果(波动值)------ 波动值越大,代表空间内有物体移动 / 遮挡(物体遮挡会导致 RSSI 突变)。
cpp
// 空间感知参数
int rssi_history[50]; // RSSI历史数据(用于计算波动)
int history_index = 0;
const int SAMPLE_NUM = 50; // 采样数
// 计算RSSI波动值(模拟空间物体遮挡/移动)
int calculate_rssi_fluctuation() {
int avg = 0;
for (int i = 0; i < SAMPLE_NUM; i++) {
avg += rssi_history[i];
}
avg /= SAMPLE_NUM;
// 计算方差(波动越大,说明空间有物体移动)
int var = 0;
for (int i = 0; i < SAMPLE_NUM; i++) {
var += (rssi_history[i] - avg) * (rssi_history[i] - avg);
}
return var / SAMPLE_NUM;
}
// 1. 读取当前RSSI(WiFi信号强度,负值,越接近0信号越强)
int current_rssi = WiFi.RSSI();
// 2. 更新RSSI历史数据
rssi_history[history_index++] = current_rssi;
if (history_index >= SAMPLE_NUM) {
history_index = 0;
}
// 3. 计算波动值(模拟空间特征)
int fluctuation = calculate_rssi_fluctuation();
2.3 ESP32_CAM 部分的代码
cpp
/********************************** (C) COPYRIGHT *******************************
* File Name : ESP32_WIFI_Perspective.ino
* Author : 混分巨兽龙某某
* Version : V1.0.0
* Date : 2026/03/05
* Description : Project code for WIFI spatial perspective based on ESP32 and Qt Creator
********************************************************************************/
#include "WiFi.h"
// WiFi配置(修改为你的路由器信息)
// const char* WIFI_SSID = "NJUST";
const char* WIFI_SSID = "iPhone 18 Pro Max";
const char* WIFI_PWD = "**********";
// 空间感知参数
int rssi_history[50]; // RSSI历史数据(用于计算波动)
int history_index = 0;
const int SAMPLE_NUM = 50; // 采样数
// 计算RSSI波动值(模拟空间物体遮挡/移动)
int calculate_rssi_fluctuation() {
int avg = 0;
for (int i = 0; i < SAMPLE_NUM; i++) {
avg += rssi_history[i];
}
avg /= SAMPLE_NUM;
// 计算方差(波动越大,说明空间有物体移动)
int var = 0;
for (int i = 0; i < SAMPLE_NUM; i++) {
var += (rssi_history[i] - avg) * (rssi_history[i] - avg);
}
return var / SAMPLE_NUM;
}
// 初始化WiFi(无需CSI,仅用RSSI做空间感知)
void init_wifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PWD);
// 等待WiFi连接
int connect_cnt = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
connect_cnt++;
if (connect_cnt > 20) {
Serial.println("\nWiFi连接失败!检查SSID/密码");
while(1); // 卡死等待重启
}
}
Serial.println("\nWiFi连接成功,IP:" + WiFi.localIP().toString());
// 初始化RSSI历史数组
for (int i = 0; i < SAMPLE_NUM; i++) {
rssi_history[i] = WiFi.RSSI();
}
}
void setup() {
// 初始化串口(ESP32-CAM默认串口,波特率115200)
Serial.begin(115200);
delay(1000); // 串口稳定延时
Serial.println("ESP32-CAM 空间感知初始化...");
// 启动WiFi
init_wifi();
}
void loop() {
// 1. 读取当前RSSI(WiFi信号强度,负值,越接近0信号越强)
int current_rssi = WiFi.RSSI();
// 2. 更新RSSI历史数据
rssi_history[history_index++] = current_rssi;
if (history_index >= SAMPLE_NUM) {
history_index = 0;
}
// 3. 计算波动值(模拟空间特征)
int fluctuation = calculate_rssi_fluctuation();
// 4. 串口输出空间感知数据(格式:RSSI,波动值,方便Qt解析)
// RSSI:反映距离(值越大→距离越近)
// 波动值:反映空间物体移动(值越大→移动越明显)
Serial.printf("SPACE,%d,%d\n", current_rssi, fluctuation);
// 5. 心跳日志(每5秒打印一次)
static unsigned long last_log = 0;
if (millis() - last_log > 5000) {
Serial.printf("运行中 - 当前RSSI:%d | 空间波动:%d\n", current_rssi, fluctuation);
last_log = millis();
}
delay(100); // 10Hz刷新率,匹配Qt端
}
三、iKun ESP32 WIFI Perspective 代码
3.1 ParseSerialData 串口数据解析函数
这是数据处理的核心入口,负责将 ESP32 串口发送的原始字符串(格式:SPACE,RSSI,波动值)解析为可用于可视化的数值,并更新热力图数据源,是 "原始数据" 到 "可视化数据" 的桥梁。

cpp
/**
* @brief 解析串口接收的单行数据
* @param data 待解析的单行字符串(格式:SPACE,RSSI,波动值)
* @details 1. 校验数据格式(是否以SPACE,开头)
* 2. 分割数据并转换为整数(RSSI/波动值)
* 3. 更新数据缓存(先进先出,保持100个点)
* 4. 根据波动值更新热力图矩阵(模拟空间物体移动)
* 5. 更新UI标签显示当前RSSI/波动值
*/
void MainWindow::parseSerialData(const QString &data)
{
// 调试输出:当前解析的行数据(开发阶段排查解析异常)
qDebug() << "Parsing line:" << data;
// 第一步:格式校验(过滤无效数据)
// 仅处理以"SPACE,"开头的行(ESP32端约定的有效数据标识)
if (data.startsWith("SPACE,")) {
qDebug() << "Line starts with SPACE,";
// 第二步:按逗号分割字符串(拆分标识/RSSI/波动值)
QStringList parts = data.split(",");
qDebug() << "Parts count:" << parts.size();
// 第三步:分割结果校验(必须拆分为3部分才有效)
if (parts.size() == 3) {
// 定义转换状态变量(判断字符串转整数是否成功)
bool rssiOk, fluctuationOk;
// 第四步:字符串转整数(提取RSSI值,rssiOk标记是否转换成功)
int rssi = parts[1].toInt(&rssiOk);
// 提取波动值,fluctuationOk标记转换状态
int fluctuation = parts[2].toInt(&fluctuationOk);
// 调试输出转换结果(方便排查"数字格式错误"类问题)
qDebug() << "RSSI:" << rssi << "(ok:" << rssiOk << "), Fluctuation:" << fluctuation << "(ok:" << fluctuationOk << ")";
// 第五步:转换成功则处理数据(避免无效数值污染缓存)
if (rssiOk && fluctuationOk) {
// 1. 更新RSSI缓存(先进先出FIFO:移除最旧值,添加最新值)
// maxDataPoints=100,保证缓存始终只有100个最新数据点
rssiValues.removeFirst();
rssiValues.append(rssi);
// 2. 同理更新波动值缓存
fluctuationValues.removeFirst();
fluctuationValues.append(fluctuation);
// 3. 计算活动等级(限制最大值为100,防止热力图数值溢出)
// 波动值可能很大(如方差上千),限制后适配0-100的热力图强度范围
int activityLevel = qMin(fluctuation, 100);
qDebug() << "Activity level:" << activityLevel;
// 4. 核心逻辑:根据活动等级更新热力图矩阵(40x40)
// 矩阵每个元素代表对应位置的"物体移动强度"
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
// 计算当前网格点到热力图中心的欧几里得距离
// 模拟WiFi信号"中心强、边缘弱"的衰减特性
int distance = qSqrt(qPow(i - gridSize/2, 2) + qPow(j - gridSize/2, 2));
// 计算当前网格点的强度:距离越远,强度越低
// 1 - distance/(gridSize/2):保证中心强度=1,边缘=0
int intensity = activityLevel * (1 - distance / (gridSize/2));
// 强度不能为负(边缘点distance=gridSize/2时,intensity=0)
intensity = qMax(0, intensity);
// 更新热力图矩阵(这是后续绘制热力图的数据源)
heatMapData[i][j] = intensity;
}
}
// 5. 更新UI标签(实时显示当前RSSI和波动值,方便用户查看)
ui->rssiLabel->setText(QString("RSSI: %1").arg(rssi));
ui->fluctuationLabel->setText(QString("Fluctuation: %1").arg(fluctuation));
qDebug() << "Data updated successfully";
} else {
// 转换失败(如字符串不是数字),输出调试信息
qDebug() << "Failed to parse RSSI or fluctuation values";
}
} else {
// 分割后不是3部分(如格式错误:SPACE,123 或 SPACE,123,456,789)
qDebug() << "Incorrect number of parts";
}
} else {
// 非SPACE开头的行(如ESP32的心跳日志、错误信息),直接跳过
qDebug() << "Line does not start with SPACE, - skipping";
}
}
3.2 UpdateHeatMap 热力图绘制函数
这是可视化渲染的核心,负责将 parseSerialData 更新的热力图矩阵(heatMapData)转换为直观的彩色热力图,核心是 "将数值转换为颜色 + 空间位置",并通过视觉效果区分 "物体遮挡 / 无遮挡"。

cpp
/**
* @brief 绘制热力图核心逻辑
* @details 1. 计算网格单元格尺寸(自适应视图大小)
* 2. 遍历网格矩阵,计算每个单元格的信号强度/颜色
* 3. 绘制单元格(带渐变效果,区分信号强度)
* 4. 检测是否有物体干扰,绘制提示文本
* 5. 绘制遮挡叠加层(区分强/中遮挡)
*/
void MainWindow::updateHeatMap()
{
// 第一步:计算单元格尺寸(自适应视图大小,窗口缩放时热力图同步缩放)
// scene->width()/height()是可视化场景的尺寸(与graphicsView绑定)
int cellWidth = scene->width() / gridSize;
int cellHeight = scene->height() / gridSize;
// 第二步:定义WiFi热点位置(热力图中心,模拟真实WiFi信号源)
int hotspotX = gridSize / 2;
int hotspotY = gridSize / 2;
// 第三步:遍历40x40网格,逐个绘制单元格
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
// 1. 计算当前单元格到热点的欧几里得距离(模拟信号衰减)
double distance = qSqrt(qPow(i - hotspotY, 2) + qPow(j - hotspotX, 2));
// 2. 获取当前单元格的基础强度(来自parseSerialData更新的heatMapData)
int baseIntensity = heatMapData[i][j];
// 3. 核心计算:最终信号强度(结合距离衰减+物体遮挡)
double baseSignal = 100.0 / (1.0 + distance * distance / 10.0);
// 解释:平方反比定律(真实WiFi信号衰减规律):距离越远,信号越弱
// 分母+10是为了避免distance=0时分母为1,保证baseSignal最大为100
// 4. 叠加物体遮挡效果(根据波动值判断遮挡程度)
double signalStrength;
if (baseIntensity > 50) {
// 高波动:强遮挡(如手在WiFi和ESP32之间移动)→ 信号大幅衰减
signalStrength = baseSignal * (0.2 + (50.0 - baseIntensity) / 200.0);
} else if (baseIntensity > 10) {
// 中波动:部分遮挡(有物体但未完全遮挡)→ 信号中度衰减
signalStrength = baseSignal * (0.6 + (10.0 - baseIntensity) / 100.0);
} else {
// 低波动:无遮挡 → 信号仅受距离影响
signalStrength = baseSignal;
}
// 5. 限制信号强度范围(0-100),避免颜色计算异常
signalStrength = qMax(0.0, qMin(100.0, signalStrength));
// 第四步:根据信号强度映射颜色(核心视觉区分逻辑)
QColor color;
if (signalStrength < 20) {
// 极弱信号:深蓝色(大概率遮挡)
int blue = 100 + static_cast<int>(signalStrength * 7.75);
color = QColor(0, 0, blue);
} else if (signalStrength < 40) {
// 弱信号:蓝色(可能遮挡)
int blue = 255;
int green = static_cast<int>((signalStrength - 20) * 6.25);
color = QColor(0, green, blue);
} else if (signalStrength < 60) {
// 中等信号:青色(部分遮挡)
int green = 255;
int red = static_cast<int>((signalStrength - 40) * 6.25);
color = QColor(red, green, 255);
} else if (signalStrength < 80) {
// 强信号:黄色(无遮挡)
int red = 255;
int green = 255;
int blue = static_cast<int>(255 - (signalStrength - 60) * 12.75);
color = QColor(red, green, blue);
} else {
// 极强信号:红色(无遮挡,靠近热点)
int green = static_cast<int>(255 - (signalStrength - 80) * 12.75);
color = QColor(255, green, 0);
}
// 第五步:创建单元格图形项(定义位置和尺寸)
// 注意:Qt图形坐标中,j是列(x轴),i是行(y轴)
QGraphicsRectItem *cell = new QGraphicsRectItem(
j * cellWidth, i * cellHeight, cellWidth, cellHeight
);
// 第六步:添加渐变效果(增强视觉层次感,区分遮挡区域)
QLinearGradient gradient(j * cellWidth, i * cellHeight,
j * cellWidth + cellWidth, i * cellHeight + cellHeight);
if (signalStrength < 40) {
// 遮挡区域:深色渐变(更醒目)
gradient.setColorAt(0, color.darker(120));
gradient.setColorAt(1, color.darker(150));
} else {
// 无遮挡区域:亮色渐变
gradient.setColorAt(0, color.lighter(110));
gradient.setColorAt(1, color.darker(110));
}
cell->setBrush(QBrush(gradient)); // 设置单元格填充为渐变
// 第七步:设置单元格边框(遮挡区域边框更粗,强化视觉区分)
if (signalStrength < 40) {
cell->setPen(QPen(Qt::darkGray, 0.5));
} else {
cell->setPen(QPen(Qt::gray, 0.2));
}
// 第八步:将单元格添加到场景(最终显示在graphicsView中)
scene->addItem(cell);
}
}
// 第四步:全局干扰检测(判断是否有物体移动)
bool hasInterference = false;
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
// 只要有一个网格点波动值>15,判定为有物体移动
if (heatMapData[i][j] > 15) {
hasInterference = true;
break;
}
}
if (hasInterference) break;
}
// 绘制干扰提示文本(右上角红色加粗,用户直观感知)
if (hasInterference) {
QGraphicsTextItem *interferenceText = new QGraphicsTextItem("检测到物体移动干扰");
interferenceText->setPos(scene->width() - 200, 10); // 位置:右上角
interferenceText->setDefaultTextColor(Qt::red); // 红色字体
QFont font = interferenceText->font();
font.setBold(true); // 加粗
font.setPointSize(12); // 字号12
interferenceText->setFont(font);
scene->addItem(interferenceText);
}
// 第五步:绘制遮挡叠加层(强化遮挡区域的视觉提示)
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
int baseIntensity = heatMapData[i][j];
if (baseIntensity > 50) {
// 强遮挡:红色半透明叠加层(透明度40,不遮挡底层颜色)
QGraphicsRectItem *obstructionOverlay = new QGraphicsRectItem(
j * cellWidth, i * cellHeight, cellWidth, cellHeight
);
obstructionOverlay->setBrush(QBrush(QColor(255, 0, 0, 40)));
obstructionOverlay->setPen(QPen(Qt::red, 0.5));
scene->addItem(obstructionOverlay);
} else if (baseIntensity > 10) {
// 中遮挡:黄色半透明叠加层(透明度30)
QGraphicsRectItem *obstructionOverlay = new QGraphicsRectItem(
j * cellWidth, i * cellHeight, cellWidth, cellHeight
);
obstructionOverlay->setBrush(QBrush(QColor(255, 255, 0, 30)));
obstructionOverlay->setPen(QPen(Qt::yellow, 0.3));
scene->addItem(obstructionOverlay);
}
}
}
}
3.3 iKun ESP32 WIFI Perspective 部分的代码
iKun ESP32 WIFI Perspective 的 GUI 风格界面如下所示:

cpp
/********************************** (C) COPYRIGHT *******************************
* File Name : mainwindow.cpp
* Author : 混分巨兽龙某某
* Version : V1.0.0
* Date : 2026/03/05
* Description : Qt端ESP32 WiFi空间透视可视化主窗口实现文件
* 功能:串口通信、RSSI/波动值解析、热力图可视化、物体遮挡检测
********************************************************************************/
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QtMath> // 数学函数库,用于计算距离/强度等
#include <QDebug> // 调试输出,便于开发阶段排查问题
#include <QTime> // 时间函数,用于时间相关逻辑(预留扩展)
/**
* @file mainwindow.cpp
* @brief ESP32 WiFi空间透视应用程序主窗口实现
* @details 实现了基于ESP32的WiFi信号强度(RSSI)和波动值监测,
* 通过热力图可视化展示空间物体遮挡/移动情况,支持串口连接/断开、
* 实时数据解析、动态热力图渲染等核心功能
*/
/**
* @brief MainWindow构造函数
* @param parent 父窗口指针,遵循Qt父子对象机制
* @details 初始化主窗口核心组件:
* 1. UI界面初始化
* 2. 串口参数/可视化场景/定时器初始化
* 3. 信号与槽连接(串口数据接收、定时器刷新、按钮点击)
* 4. 启动10Hz刷新定时器(与ESP32端刷新率匹配)
*/
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
serialPort(new QSerialPort(this)), // 串口通信对象
scene(new QGraphicsScene(this)), // 热力图绘制场景
updateTimer(new QTimer(this)), // 可视化刷新定时器
maxDataPoints(100), // 数据缓存最大点数(曲线/波动计算)
gridSize(40) // 热力图网格密度(40x40)
{
ui->setupUi(this); // 初始化UI界面
setupSerialPort(); // 初始化串口配置(端口列表/默认参数)
setupVisualization(); // 初始化可视化场景(数据缓存/热力图矩阵)
// 信号槽连接:核心业务逻辑联动
connect(updateTimer, &QTimer::timeout, this, &MainWindow::onUpdateVisualization); // 定时刷新可视化
connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::onSerialDataReady); // 串口数据接收
connect(ui->connectButton, &QPushButton::clicked, this, &MainWindow::onConnectButtonClicked); // 连接串口
connect(ui->disconnectButton, &QPushButton::clicked, this, &MainWindow::onDisconnectButtonClicked); // 断开串口
// 启动可视化刷新定时器 (10Hz,与ESP32端100ms采样间隔匹配)
updateTimer->start(100);
}
/**
* @brief MainWindow析构函数
* @details 释放资源:关闭串口连接、销毁UI对象,避免内存泄漏
*/
MainWindow::~MainWindow()
{
if (serialPort->isOpen()) // 安全关闭串口
serialPort->close();
delete ui; // 销毁UI对象
}
/**
* @brief 初始化串口配置
* @details 1. 枚举系统可用串口并填充到下拉框
* 2. 设置默认波特率(115200,与ESP32端一致)
*/
void MainWindow::setupSerialPort()
{
// 获取系统可用串口列表
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
foreach (const QSerialPortInfo &port, ports) {
ui->portComboBox->addItem(port.portName()); // 填充串口名到下拉框
}
// 设置默认波特率选项(仅添加115200,与ESP32端匹配)
ui->baudRateComboBox->addItem("115200");
ui->baudRateComboBox->setCurrentIndex(0); // 默认选中115200
}
/**
* @brief 初始化可视化相关组件
* @details 1. 设置图形视图与场景的关联
* 2. 初始化RSSI/波动值数据缓存(固定长度100)
* 3. 初始化热力图数据矩阵(40x40),初始值为0
*/
void MainWindow::setupVisualization()
{
// 将图形场景绑定到视图组件
ui->graphicsView->setScene(scene);
// 初始化RSSI/波动值数据缓存(固定长度,先进先出)
rssiValues.resize(maxDataPoints);
rssiValues.fill(0); // 初始值填充为0
fluctuationValues.resize(maxDataPoints);
fluctuationValues.fill(0);
// 初始化热力图数据矩阵(gridSize x gridSize)
heatMapData.resize(gridSize);
for (int i = 0; i < gridSize; i++) {
heatMapData[i].resize(gridSize);
for (int j = 0; j < gridSize; j++) {
heatMapData[i][j] = 0; // 初始无信号波动
}
}
}
/**
* @brief 串口连接按钮点击事件处理
* @details 1. 关闭已有连接(避免重复连接)
* 2. 配置串口参数(端口名/波特率/数据位/校验位/停止位)
* 3. 打开串口并禁用DTR/RTS(防止ESP32进入下载模式)
* 4. 更新UI状态(按钮禁用/启用、状态栏提示)
*/
void MainWindow::onConnectButtonClicked()
{
// 关闭已有串口连接(安全检查)
if (serialPort->isOpen()) {
serialPort->close();
qDebug() << "Closed existing connection";
}
// 配置串口参数
serialPort->setPortName(ui->portComboBox->currentText()); // 选中的串口名
int baudRate = ui->baudRateComboBox->currentText().toInt();// 选中的波特率
serialPort->setBaudRate(baudRate); // 波特率
serialPort->setDataBits(QSerialPort::Data8); // 8位数据位
serialPort->setParity(QSerialPort::NoParity); // 无校验位
serialPort->setStopBits(QSerialPort::OneStop); // 1位停止位
serialPort->setFlowControl(QSerialPort::NoFlowControl); // 无流控
// 调试输出:串口配置信息
qDebug() << "Connecting to port:" << ui->portComboBox->currentText();
qDebug() << "Baud rate:" << baudRate;
// 打开串口(只读模式,仅接收ESP32数据)
if (serialPort->open(QIODevice::ReadOnly)) {
// 禁用DTR/RTS信号,防止ESP32误进入下载模式
serialPort->setDataTerminalReady(false);
serialPort->setRequestToSend(false);
qDebug() << "DTR and RTS signals disabled";
// 清空串口缓冲区(避免初始脏数据)
serialPort->clear();
qDebug() << "Serial buffer cleared";
// 更新UI状态:连接成功
ui->statusLabel->setText("Connected to " + ui->portComboBox->currentText());
ui->connectButton->setEnabled(false); // 禁用连接按钮
ui->disconnectButton->setEnabled(true); // 启用断开按钮
qDebug() << "Serial port opened successfully";
} else {
// 连接失败:更新状态栏提示错误信息
ui->statusLabel->setText("Failed to connect: " + serialPort->errorString());
qDebug() << "Failed to open serial port:" << serialPort->errorString();
}
}
/**
* @brief 串口断开按钮点击事件处理
* @details 1. 关闭串口连接
* 2. 更新UI状态(按钮禁用/启用、状态栏提示)
*/
void MainWindow::onDisconnectButtonClicked()
{
if (serialPort->isOpen()) { // 安全关闭串口
serialPort->close();
ui->statusLabel->setText("Disconnected"); // 更新状态栏
ui->connectButton->setEnabled(true); // 启用连接按钮
ui->disconnectButton->setEnabled(false); // 禁用断开按钮
}
}
/**
* @brief 串口数据接收事件处理
* @details 1. 读取串口缓冲区数据并拼接(处理粘包/拆包)
* 2. 按行分割数据(支持\n/\r\n换行符)
* 3. 逐行解析有效数据(调用parseSerialData)
* 4. 调试输出原始数据/缓冲区状态,便于排查问题
*/
void MainWindow::onSerialDataReady()
{
if (serialPort->isOpen()) {
static QString buffer; // 静态缓冲区,处理跨包数据
QByteArray data = serialPort->readAll(); // 读取所有可用数据
// 调试输出:原始接收数据
if (!data.isEmpty()) {
qDebug() << "Raw bytes received:" << data.toHex(); // 十六进制原始数据
qDebug() << "Received data length:" << data.length(); // 数据长度
qDebug() << "Received data:" << QString::fromLatin1(data);// 字符串形式数据
}
// 拼接数据到缓冲区(使用Latin1编码,适配串口二进制数据)
buffer += QString::fromLatin1(data);
qDebug() << "Buffer after append:" << buffer;
// 调试:检查缓冲区是否包含目标数据标识
if (buffer.contains("SPACE,")) {
qDebug() << "Buffer contains 'SPACE,'";
}
// 按换行符分割缓冲区(支持\n/\r\n)
int newlineIndex = buffer.indexOf(QRegExp("[\\n\\r]+"));
qDebug() << "Newline index:" << newlineIndex;
// 循环解析所有完整行数据
while (newlineIndex != -1) {
QString line = buffer.left(newlineIndex).trimmed(); // 提取单行并去除首尾空白
buffer = buffer.mid(newlineIndex + 1); // 移除已解析行
qDebug() << "Extracted line:" << line;
qDebug() << "Buffer after extraction:" << buffer;
if (!line.isEmpty()) { // 跳过空行
qDebug() << "Processing line:" << line;
parseSerialData(line); // 解析单行数据
}
// 查找下一个换行符
newlineIndex = buffer.indexOf(QRegExp("[\\n\\r]+"));
qDebug() << "Next newline index:" << newlineIndex;
}
} else {
qDebug() << "Serial port is not open";
}
}
/**
* @brief 解析串口接收的单行数据
* @param data 待解析的单行字符串(格式:SPACE,RSSI,波动值)
* @details 1. 校验数据格式(是否以SPACE,开头)
* 2. 分割数据并转换为整数(RSSI/波动值)
* 3. 更新数据缓存(先进先出,保持100个点)
* 4. 根据波动值更新热力图矩阵(模拟空间物体移动)
* 5. 更新UI标签显示当前RSSI/波动值
*/
void MainWindow::parseSerialData(const QString &data)
{
// 调试输出:当前解析的行数据
qDebug() << "Parsing line:" << data;
// 校验数据格式:必须以"SPACE,"开头(ESP32端约定格式)
if (data.startsWith("SPACE,")) {
qDebug() << "Line starts with SPACE,";
QStringList parts = data.split(","); // 按逗号分割数据
qDebug() << "Parts count:" << parts.size();
// 校验分割后数据长度(必须为3:SPACE + RSSI + 波动值)
if (parts.size() == 3) {
bool rssiOk, fluctuationOk;
int rssi = parts[1].toInt(&rssiOk); // 转换RSSI值
int fluctuation = parts[2].toInt(&fluctuationOk); // 转换波动值
// 调试输出:转换结果
qDebug() << "RSSI:" << rssi << "(ok:" << rssiOk << "), Fluctuation:" << fluctuation << "(ok:" << fluctuationOk << ")";
// 转换成功则更新数据
if (rssiOk && fluctuationOk) {
// 更新RSSI缓存:移除第一个元素,添加新值(先进先出)
rssiValues.removeFirst();
rssiValues.append(rssi);
// 更新波动值缓存:同上
fluctuationValues.removeFirst();
fluctuationValues.append(fluctuation);
// 计算活动等级(限制最大值为100,避免热力图溢出)
int activityLevel = qMin(fluctuation, 100);
qDebug() << "Activity level:" << activityLevel;
// 根据活动等级更新热力图矩阵(模拟空间物体移动)
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
// 计算当前网格点到中心的欧几里得距离(模拟WiFi信号衰减)
int distance = qSqrt(qPow(i - gridSize/2, 2) + qPow(j - gridSize/2, 2));
// 计算当前网格点强度:距离越远,强度越低
int intensity = activityLevel * (1 - distance / (gridSize/2));
intensity = qMax(0, intensity); // 强度不小于0
heatMapData[i][j] = intensity; // 更新热力图矩阵
}
}
// 更新UI标签显示当前值
ui->rssiLabel->setText(QString("RSSI: %1").arg(rssi));
ui->fluctuationLabel->setText(QString("Fluctuation: %1").arg(fluctuation));
qDebug() << "Data updated successfully";
} else {
qDebug() << "Failed to parse RSSI or fluctuation values";
}
} else {
qDebug() << "Incorrect number of parts";
}
} else {
qDebug() << "Line does not start with SPACE, - skipping";
}
}
/**
* @brief 定时更新可视化界面
* @details 1. 同步场景尺寸与视图尺寸
* 2. 清空场景原有内容
* 3. 重新绘制热力图(调用updateHeatMap)
*/
void MainWindow::onUpdateVisualization()
{
// 同步场景尺寸到视图尺寸(自适应窗口大小)
QRect viewRect = ui->graphicsView->rect();
scene->setSceneRect(0, 0, viewRect.width(), viewRect.height());
// 清空场景(避免重复绘制)
scene->clear();
// 重新绘制热力图
updateHeatMap();
}
/**
* @brief 绘制热力图核心逻辑
* @details 1. 计算网格单元格尺寸(自适应视图大小)
* 2. 遍历网格矩阵,计算每个单元格的信号强度/颜色
* 3. 绘制单元格(带渐变效果,区分信号强度)
* 4. 检测是否有物体干扰,绘制提示文本
* 5. 绘制遮挡叠加层(区分强/中遮挡)
*/
void MainWindow::updateHeatMap()
{
// 计算每个网格单元格的尺寸(自适应视图)
int cellWidth = scene->width() / gridSize;
int cellHeight = scene->height() / gridSize;
// WiFi热点位置(热力图中心)
int hotspotX = gridSize / 2;
int hotspotY = gridSize / 2;
// 遍历网格矩阵,绘制每个单元格
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
// 计算当前单元格到热点的欧几里得距离(模拟信号衰减)
double distance = qSqrt(qPow(i - hotspotY, 2) + qPow(j - hotspotX, 2));
// 获取热力图矩阵中当前单元格的基础强度(波动值)
int baseIntensity = heatMapData[i][j];
// 计算信号强度:
// 1. 基础信号强度(距离越远,强度越低,遵循平方反比定律)
double baseSignal = 100.0 / (1.0 + distance * distance / 10.0);
// 2. 叠加遮挡效果(根据波动值判断物体遮挡)
double signalStrength;
if (baseIntensity > 50) {
// 高波动:强遮挡(如手在WiFi和ESP32之间移动),信号大幅衰减
signalStrength = baseSignal * (0.2 + (50.0 - baseIntensity) / 200.0);
} else if (baseIntensity > 10) {
// 中波动:部分遮挡(有物体但未完全遮挡),信号中度衰减
signalStrength = baseSignal * (0.6 + (10.0 - baseIntensity) / 100.0);
} else {
// 低波动:无遮挡(信号仅受距离影响)
signalStrength = baseSignal;
}
// 限制信号强度范围(0-100)
signalStrength = qMax(0.0, qMin(100.0, signalStrength));
// 根据信号强度设置颜色(优化的遮挡检测配色方案)
QColor color;
if (signalStrength < 20) {
// 极弱信号:深蓝色(大概率遮挡)
int blue = 100 + static_cast<int>(signalStrength * 7.75);
color = QColor(0, 0, blue);
} else if (signalStrength < 40) {
// 弱信号:蓝色(可能遮挡)
int blue = 255;
int green = static_cast<int>((signalStrength - 20) * 6.25);
color = QColor(0, green, blue);
} else if (signalStrength < 60) {
// 中等信号:青色(部分遮挡)
int green = 255;
int red = static_cast<int>((signalStrength - 40) * 6.25);
color = QColor(red, green, 255);
} else if (signalStrength < 80) {
// 强信号:黄色(无遮挡)
int red = 255;
int green = 255;
int blue = static_cast<int>(255 - (signalStrength - 60) * 12.75);
color = QColor(red, green, blue);
} else {
// 极强信号:红色(无遮挡,靠近热点)
int green = static_cast<int>(255 - (signalStrength - 80) * 12.75);
color = QColor(255, green, 0);
}
// 创建单元格矩形(位置:j*cellWidth, i*cellHeight,尺寸:cellWidth x cellHeight)
QGraphicsRectItem *cell = new QGraphicsRectItem(
j * cellWidth, i * cellHeight, cellWidth, cellHeight
);
// 设置单元格渐变效果(增强视觉层次感)
QLinearGradient gradient(j * cellWidth, i * cellHeight,
j * cellWidth + cellWidth, i * cellHeight + cellHeight);
// 根据信号强度调整渐变(遮挡区域更暗,无遮挡区域更亮)
if (signalStrength < 40) {
// 遮挡区域:深色渐变
gradient.setColorAt(0, color.darker(120));
gradient.setColorAt(1, color.darker(150));
} else {
// 无遮挡区域:亮色渐变
gradient.setColorAt(0, color.lighter(110));
gradient.setColorAt(1, color.darker(110));
}
cell->setBrush(QBrush(gradient)); // 设置渐变填充
// 设置单元格边框(遮挡区域边框更明显)
if (signalStrength < 40) {
cell->setPen(QPen(Qt::darkGray, 0.5));
} else {
cell->setPen(QPen(Qt::gray, 0.2));
}
// 将单元格添加到场景
scene->addItem(cell);
}
}
// 检测是否有物体移动干扰(波动值>15判定为有干扰)
bool hasInterference = false;
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
if (heatMapData[i][j] > 15) {
hasInterference = true;
break;
}
}
if (hasInterference) break;
}
// 绘制干扰提示文本(右上角)
if (hasInterference) {
QGraphicsTextItem *interferenceText = new QGraphicsTextItem("检测到物体移动干扰");
interferenceText->setPos(scene->width() - 200, 10); // 位置:右上角
interferenceText->setDefaultTextColor(Qt::red); // 红色字体
QFont font = interferenceText->font();
font.setBold(true); // 加粗
font.setPointSize(12); // 字号12
interferenceText->setFont(font);
scene->addItem(interferenceText);
}
// 绘制遮挡叠加层(区分强/中遮挡)
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
int baseIntensity = heatMapData[i][j];
// 强波动(>50):红色半透明叠加层(手移动)
if (baseIntensity > 50) {
QGraphicsRectItem *obstructionOverlay = new QGraphicsRectItem(
j * cellWidth, i * cellHeight, cellWidth, cellHeight
);
obstructionOverlay->setBrush(QBrush(QColor(255, 0, 0, 40))); // 红色,透明度40
obstructionOverlay->setPen(QPen(Qt::red, 0.5));
scene->addItem(obstructionOverlay);
}
// 中波动(10-50):黄色半透明叠加层(有物体)
else if (baseIntensity > 10) {
QGraphicsRectItem *obstructionOverlay = new QGraphicsRectItem(
j * cellWidth, i * cellHeight, cellWidth, cellHeight
);
obstructionOverlay->setBrush(QBrush(QColor(255, 255, 0, 30))); // 黄色,透明度30
obstructionOverlay->setPen(QPen(Qt::yellow, 0.3));
scene->addItem(obstructionOverlay);
}
// 低波动:无叠加层
}
}
}
四、ESP32 WIFI 空间透视演示
作者这边就是利用 RSSI 和 Fluctuation 的数值进行空间透视的。可以RSSI大致判断出物体的远近程度,而 Fluctuation 数值偏大的时候代表被测空间存在物体的移动,综合这两个数值进行热力图的绘制实现空间透视效果!

1、空间有物体进行干扰的时候:

2、空间无物体进行干扰的时候:

五、代码开源
代码地址: 基于ESP32与QtCreator的WIFI空间透视项目代码资源-CSDN下载
如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!