ESPectre项目地址
https://github.com/francescopace/espectre
用Wi-Fi“看见”人
这是一种基于 Wi-Fi CSI(信道状态信息) 的室内空间被动感知系统。它利用普通的2.4GHz Wi-Fi路由器和ESP32设备,捕捉无线信道在时间维度上的微小变化,从而实现对室内活动状态的判断。

原理简述
当人在房间内移动时,身体会像障碍物一样影响Wi-Fi无线电波的传播路径。Wi-Fi本质上是持续发射的无线电波,就像湖面的水波。人体的移动会改变“回波”的相位与幅度。
ESPectre的核心正是捕获Wi-Fi信道状态信息(CSI)的这些微小波动,并通过算法分析来判断环境中是否存在运动行为。
这与传统方案有根本区别:
| 方案 |
感知方式 |
| 摄像头 |
采集光学图像 |
| 红外 |
检测温度变化 |
| 毫米波 |
主动发射雷达信号 |
| espectre |
被动测量 Wi-Fi 信道结构变化 |
技术核心
在Wi-Fi的OFDM物理层中,信道被划分为数十个子载波,每个子载波都有一个复数形式的信道响应:

这个响应包含了幅度(A)和相位(θ)。当人体进入、移动、呼吸或改变姿态时,相当于对信道响应进行了拉伸、扭曲或反射。ESPectre通过分析这些CSI数据的微小变化,来判断环境中的物理运动。
如果你对网络底层的协议和信号处理原理感兴趣,可以在 网络/系统 板块找到更多深入探讨TCP/IP、HTTP及无线通信相关的资料。
局限性
- 频段拥塞:2.4GHz频段极易受到蓝牙设备、微波炉等干扰。建议尝试切换信道。
- 根据802.11协议,每个信道通常占用22MHz带宽。
- 在2.4GHz频段,只有第1、6、11信道是完全不重叠的,这是避开干扰的首选。

- 多路径自干扰:在极小空间内(如金属柜),Wi-Fi信号反射过于强烈可能导致全信道饱和。建议适当降低发射功率。
需要的材料与环境准备
硬件准备
- Wi-Fi 路由器:
- 频段:2.4GHz
- 协议支持:802.11b/g/n/ax
- 要求:普通家用路由器即可,无需特殊固件。
软件/个人实验环境
- KALi Linux 系统
- Python 3.13
- ESPHome 2025.12.2 及以上
- Home Assistant (可选,用于联动)
ESP32 模块选择建议
项目作者对不同 ESP32 系列给出了明确定位:
| 型号 |
架构 |
Wi-Fi |
| ESP32-C6 |
RISC-V |
Wi-Fi 6 |
| ESP32-S3 |
Xtensa |
Wi-Fi 4 |
| ESP32-C3 |
RISC-V |
Wi-Fi 4 |
| ESP32 |
Xtensa |
Wi-Fi 4 |
| ESP32-C5 |
RISC-V |
Wi-Fi 6 |
| ESP32-S2 |
Xtensa |
Wi-Fi 4 |
本次复现选择的是 ESP32-S3 和 ESP32-C6,正好是之前做其他项目时购入的开发板,资源相对充裕。


快速复现(ESP32-S3 实测)
ESPectre提供 开发版 与 稳定测试版 两套配置,这里直接使用作者维护的官方示例。
第一步:安装 ESPHome
pip3 install esphome

第二步:选择并下载配置文件
官方已为不同芯片准备好 YAML 配置文件:
| 芯片 |
配置文件 |
| ESP32-C6 |
espectre-c6.yaml |
| ESP32-S3 |
espectre-s3.yaml |
| ESP32-C3 |
espectre-c3.yaml |
| ESP32 |
espectre-esp32.yaml |
| ESP32-C5 |
espectre-c5.yaml |
| ESP32-S2 |
espectre-s2.yaml |
本次使用 ESP32-S3,下载对应的配置文件:
wget https://raw.githubusercontent.com/francescopace/espectre/main/examples/espectre-s3.yaml

第三步:刷写固件
将 ESP32-S3 通过 USB 连接至 Linux 主机,执行:
esphome run espectre-s3.yaml

选择选项 1 刷入到 /dev/ttyACM0 (USB JTAG/serial debug unit)。

程序写入成功。

可以通过串口查看设备输出的数据:
cat /dev/ttyACM0

等待片刻,系统运行成功后,终端会持续输出传感器数据和状态。

环境部署与 HA 联动(可选)
使用Docker部署Home Assistant作为智能家居中控中心:
sudo docker run -d --name homeassistant --privileged --restart unless-stopped --network host -e TZ=Asia/Shanghai -v $HOME/homeassistant:/config homeassistant/home-assistant:stable
运行成功后,可以通过 docker ps 命令查看容器状态。

访问 Home Assistant 的 Web 界面:http://localhost:8123/。在同一个网络下,Home Assistant 通常能自动发现 ESPectre 设备,添加即可。

在 Home Assistant 中可以查看运动得分的时序图表。

物联网和智能家居的整合是当前的热点,更多关于 Docker、云计算和智能化应用部署的实践,可以关注 智能 & 数据 & 云 板块。
深度二开:CSI 原始数据流的可视化
在稳定版的基础上,可以通过修改底层代码,打通从原始信号采样到前端可视化显示的完整链路。
克隆项目
git clone https://github.com/francescopace/espectre.git
底层源码修改
在开发版代码中修改 CSIManager.cpp,实现周期性输出子载波的原始 I/Q 数据。

在底层的 process_packet 函数中,直接提取 I/Q 信号分量。需要注意的是:ESP32 输出的 CSI 数据是 int8_t 类型的有符号整型。
物理意义:
在前端生成的星座图中,每一个点代表一个子载波的相量状态。
- 点簇集中:代表信道静态稳定。
- 点簇扩散/旋转:代表环境中有物理遮挡改变了相位偏移,这是判断微小运动(如呼吸)的关键指标。

修改 process_packet 函数,加入周期性打印 I/Q 数据的代码片段:
void CSIManager::process_packet(wifi_csi_info_t* data) {
// ... 原有的数据检查、增益锁定、校准等代码 ...
// =====================================================
// CSI I/Q 数据周期性输出(调试用)
// =====================================================
// 兼容 ESP-IDF / ESPHome 日志系统
// 用于观察各子载波 I/Q 波动情况
static int64_t last_print_us = 0;
int64_t now_us = esp_timer_get_time(); // 当前时间(微秒)
if (now_us - last_print_us > 5000000) { // 每 5 秒输出一次
last_print_us = now_us;
printf("CSI_IQ");
for (int i = 0; i < NUM_SUBCARRIERS; i++) {
uint8_t sc = selected_subcarriers_[i];
int idx = sc * 2;
// CSI 数据格式:I/Q 交错存储
if (idx + 1 < csi_len) {
int8_t I = csi_data[idx];
int8_t Q = csi_data[idx + 1];
printf(" %d:%d,%d", sc, I, Q);
}
}
printf("\n");
}
// =====================================================
// ... 原有的游戏模式回调、状态发布等后续代码 ...
}
添加Wi-Fi连接配置文件
在 examples/ 目录下创建一个 secrets.yaml 文件,内容如下:
wifi_ssid: "你的Wi-Fi名称"
wifi_password: "你的Wi-Fi密码"
运行并写入固件
esphome run examples/espectre-s3-dev.yaml
编译并刷写成功后,重新插拔USB设备即可使用。
后端数据分发 (Python + WebSocket)
需要一个Python后端服务,读取ESP32通过串口输出的CSI数据,并通过WebSocket分发给前端网页。
import asyncio
import serial
import websockets
import json
import traceback
from datetime import datetime
# 配置串口
SERIAL_PORT = '/dev/ttyACM0' # 根据你的设备调整
BAUD_RATE = 115200
# 存储所有连接的网页客户端
connected_clients = set()
async def read_serial_and_broadcast():
"""读取串口并发送给所有 WebSocket 客户端"""
ser = None
while True:
try:
if ser is None or not ser.is_open:
print(f"尝试连接串口: {SERIAL_PORT}")
ser = serial.Serial(
port=SERIAL_PORT,
baudrate=BAUD_RATE,
timeout=0.1,
bytesize=8,
parity='N',
stopbits=1
)
print(f"成功连接串口: {SERIAL_PORT}")
if ser.in_waiting > 0:
try:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if line and len(line) > 1: # 过滤空行
print(f"收到数据: {line}")
# 如果有网页连着,就发过去
if connected_clients:
# 添加时间戳
timestamp = datetime.now().strftime("%H:%M:%S")
data_with_time = f"[{timestamp}] {line}"
# 广播给所有客户端
disconnected_clients = set()
for client in connected_clients:
try:
await client.send(data_with_time)
except:
disconnected_clients.add(client)
# 移除断开连接的客户端
for client in disconnected_clients:
connected_clients.remove(client)
except UnicodeDecodeError:
# 有时会有乱码,忽略
pass
await asyncio.sleep(0.01)
except serial.SerialException as e:
print(f"串口错误: {e}")
if ser:
ser.close()
ser = None
await asyncio.sleep(2) # 等待2秒后重试
except Exception as e:
print(f"其他错误: {e}")
traceback.print_exc()
await asyncio.sleep(1)
async def handler(websocket, path):
"""管理 WebSocket 连接"""
connected_clients.add(websocket)
client_ip = websocket.remote_address[0]
print(f"网页已连接 - IP: {client_ip} (当前连接数: {len(connected_clients)})")
try:
# 发送欢迎消息
await websocket.send("已连接到运动监测系统")
# 保持连接
async for message in websocket:
# 处理客户端消息(如果需要)
pass
except websockets.exceptions.ConnectionClosed:
print(f"网页连接断开")
finally:
connected_clients.discard(websocket)
print(f"当前连接数: {len(connected_clients)}")
async def health_check():
"""定期检查连接状态"""
while True:
print(f"系统状态 - 连接数: {len(connected_clients)}")
await asyncio.sleep(10)
async def main():
# 启动健康检查
asyncio.create_task(health_check())
# 启动 WebSocket 服务
# 注意:这里使用 0.0.0.0 允许所有IP连接
server = await websockets.serve(
handler,
"0.0.0.0", # 允许所有IP访问
8765,
ping_interval=20,
ping_timeout=10
)
print("WebSocket 服务已启动")
print("本地地址: ws://localhost:8765")
print("网络地址: ws://YOUR_IP:8765")
# 启动串口读取
await read_serial_and_broadcast()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n服务已停止")
运行后端服务后,可以在终端看到实时的运动数据输出。

前端实时监测系统
创建一个HTML页面,通过WebSocket连接后端,实时展示波形图、CSI星座图、数据面板和系统日志。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>运动强度监测系统 - 专业版</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.30.0/dist/date-fns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 样式代码较长,主要定义了深色主题的仪表板、卡片、图表和交互控件 */
/* 此处省略详细CSS代码,其功能是构建一个美观的实时数据监控界面 */
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--dark-bg: #0f172a;
--card-bg: rgba(30, 41, 59, 0.8);
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
}
body {
font-family: 'Segoe UI', 'Inter', sans-serif;
background: var(--dark-bg);
color: var(--text-primary);
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 28px;
border: 1px solid rgba(148, 163, 184, 0.1);
backdrop-filter: blur(10px);
}
/* ... 更多样式定义 ... */
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<header>
<h1><i class="fas fa-wifi"></i> ESPectre <i class="fas fa-ghost"></i></h1>
<div id="connectionStatus" class="connection-status">
<span class="status-indicator"></span>
<span>正在初始化传感器连接...</span>
</div>
</header>
<!-- 警告横幅 -->
<div id="alertBanner" class="alert-banner" style="display: none;">
<i class="fas fa-exclamation-triangle fa-lg"></i>
<div><strong>高强度物体移动警报!</strong></div>
</div>
<!-- 主仪表板 -->
<div class="dashboard">
<!-- 左侧主图表区 -->
<div class="main-charts">
<!-- 波形图卡片 -->
<div class="card chart-container">
<div class="card-header">
<div class="card-title"><i class="fas fa-wave-square"></i> 物体移动度波形图</div>
<div class="time-range-buttons">
<button class="time-btn active" onclick="setHistoryRange(30, this)">30秒</button>
<button class="time-btn" onclick="setHistoryRange(60, this)">1分钟</button>
<button class="time-btn" onclick="setHistoryRange(300, this)">5分钟</button>
</div>
</div>
<canvas id="waveformChart"></canvas>
</div>
<!-- CSI 星座图 -->
<div class="card chart-container">
<div class="card-header">
<div class="card-title"><i class="fas fa-braille"></i> CSI 子载波星座图</div>
</div>
<canvas id="constellationChart"></canvas>
</div>
</div>
<!-- 右侧侧边栏 -->
<div class="side-panel">
<!-- 实时数据面板 -->
<div class="card">
<div class="card-header"><div class="card-title"><i class="fas fa-tachometer-alt"></i> 实时数据面板</div></div>
<div class="data-grid">
<div class="data-card"><div class="data-label"><i class="fas fa-bolt"></i> 当前强度</div><div class="data-value" id="currentMotion">0.00<span class="data-unit">/10</span></div></div>
<div class="data-card"><div class="data-label"><i class="fas fa-mountain"></i> 峰值强度</div><div class="data-value" id="maxMotion">0.00<span class="data-unit">/10</span></div></div>
<div class="data-card"><div class="data-label"><i class="fas fa-calculator"></i> 平均强度</div><div class="data-value" id="avgMotion">0.00<span class="data-unit">/10</span></div></div>
<div class="data-card"><div class="data-label"><i class="fas fa-clock"></i> 运行时间</div><div class="data-value" id="uptime">00:00</div></div>
</div>
<!-- 强度可视化条 -->
<div class="intensity-container">
<div class="data-label"><i class="fas fa-chart-line"></i> 强度可视化</div>
<div class="intensity-bar"><div id="intensityFill" class="intensity-fill"></div></div>
<div class="intensity-labels"><span>低</span><span>中</span><span>高</span><span>极高</span></div>
</div>
<!-- 阈值设置滑块 -->
<div class="threshold-container">
<div class="slider-header">
<div class="data-label"><i class="fas fa-exclamation-circle"></i> 警报阈值</div>
<div class="slider-value" id="thresholdValue">6.0</div>
</div>
<input type="range" id="thresholdSlider" min="1" max="10" step="0.5" value="6">
<div style="display: flex; justify-content: space-between; margin-top: 8px; font-size: 0.8rem; color: var(--text-secondary);">
<span>安全</span><span>警告</span><span>危险</span>
</div>
</div>
</div>
<!-- 系统日志卡片 -->
<div class="card log-container">
<div class="log-header">
<div class="card-title"><i class="fas fa-clipboard-list"></i> 系统日志</div>
<button onclick="clearLogs()" class="btn btn-outline" style="padding: 6px 12px; font-size: 0.85rem;"><i class="fas fa-trash-alt"></i> 清空日志</button>
</div>
<div class="log-entries" id="logEntries">
<div class="log-entry success">
<div class="log-time">系统初始化完成</div>
<div class="log-message">运动强度监测系统已启动</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部信息 -->
<div class="footer-info">
<p>© 2023 运动强度监测系统 | 版本 2.0 | 数据更新间隔: 1秒 | 最后更新: <span id="lastUpdate">--:--:--</span></p>
</div>
</div>
<script>
// JavaScript 代码核心功能:
// 1. 初始化 Chart.js 图表(波形图、星座图)。
// 2. 建立 WebSocket 连接,接收后端推送的串口数据。
// 3. 解析数据(运动强度 mvmt 和 CSI_IQ 原始数据)。
// 4. 实时更新图表、数据面板、强度条和系统日志。
// 5. 处理用户交互,如调整时间范围、阈值滑块。
// 代码逻辑较长,主要实现了数据的实时可视化与交互。
const config = {
maxHistorySeconds: 300,
historyRange: 30,
maxMotionValue: 10.0
};
let motionData = [];
let stats = { max: 0, sum: 0, count: 0, startTime: Date.now() };
let waveformChart, constellationChart;
function initCharts() {
// 初始化波形图
waveformChart = new Chart(document.getElementById('waveformChart'), {
type: 'line',
data: { datasets: [{ label: '运动强度', data: [], borderColor: '#4facfe', backgroundColor: 'rgba(79, 172, 254, 0.1)', fill: true, tension: 0.4, pointRadius: 0 }] },
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
scales: {
x: { type: 'time', time: { unit: 'second', tooltipFormat: 'PPpp' } },
y: { min: 0, max: 10, ticks: { callback: (v) => v.toFixed(1) + '/10' } }
},
plugins: { legend: { display: false } }
}
});
// 初始化CSI星座图
constellationChart = new Chart(document.getElementById('constellationChart'), {
type: 'scatter',
data: { datasets: [{ label: 'CSI IQ', data: [], pointRadius: 3, backgroundColor: 'rgba(56, 189, 248, 0.8)' }] },
options: {
responsive: true,
animation: false,
scales: {
x: { min: -130, max: 130, title: { display: true, text: 'I' } },
y: { min: -130, max: 130, title: { display: true, text: 'Q' } }
},
plugins: { legend: { display: false } }
}
});
}
// 解析CSI_IQ数据行
function parseCSI(line) {
if (!line.startsWith("CSI_IQ")) return null;
const points = [];
const parts = line.replace("CSI_IQ", "").trim().split(" ");
parts.forEach(p => {
const [sub, iq] = p.split(":");
if (!iq) return;
const [i, q] = iq.split(",").map(Number);
if (isNaN(i) || isNaN(q)) return;
points.push({ x: i, y: q });
});
return points;
}
// 处理接收到的运动数据
function handleIncomingData(rawLine) {
const mvmtMatch = rawLine.match(/mvmt:(\d+\.\d+)/);
if (!mvmtMatch) return;
const val = parseFloat(mvmtMatch[1]);
const now = Date.now();
motionData.push({ x: now, y: val });
// 清理旧数据
const limit = now - (config.maxHistorySeconds * 1000);
while (motionData.length > 0 && motionData[0].x < limit) { motionData.shift(); }
// 更新统计
if (val > stats.max) stats.max = val;
stats.sum += val;
stats.count++;
// 刷新UI
refreshUI(val, now);
}
function refreshUI(currentVal, timestamp) {
// 更新数据面板
document.getElementById('currentMotion').textContent = currentVal.toFixed(2);
document.getElementById('maxMotion').textContent = stats.max.toFixed(2);
document.getElementById('avgMotion').textContent = (stats.count > 0 ? stats.sum / stats.count : 0).toFixed(2);
// 更新运行时间
const elapsed = Math.floor((Date.now() - stats.startTime) / 1000);
const hours = Math.floor(elapsed / 3600);
const minutes = Math.floor((elapsed % 3600) / 60);
const seconds = elapsed % 60;
let uptimeStr = (hours > 0) ? `${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}:${String(seconds).padStart(2,'0')}` : `${String(minutes).padStart(2,'0')}:${String(seconds).padStart(2,'0')}`;
document.getElementById('uptime').textContent = uptimeStr;
// 更新强度条
const fill = document.getElementById('intensityFill');
const fillPercent = Math.min(currentVal * 10, 100);
fill.style.width = `${fillPercent}%`;
if (currentVal < 4) fill.style.background = 'linear-gradient(90deg, #4facfe, #00f2fe)';
else if (currentVal < 7) fill.style.background = 'linear-gradient(90deg, #f59e0b, #f97316)';
else fill.style.background = 'linear-gradient(90deg, #ef4444, #dc2626)';
// 警报检测
const threshold = parseFloat(document.getElementById('thresholdSlider').value);
const alertBanner = document.getElementById('alertBanner');
if (currentVal > threshold) {
alertBanner.style.display = 'flex';
alertBanner.innerHTML = `<i class="fas fa-exclamation-triangle fa-lg"></i><div><strong>有人在移动警报!</strong><div style="font-size: 0.9rem; opacity: 0.9;">当前强度: ${currentVal.toFixed(2)} | 阈值: ${threshold.toFixed(1)}</div></div>`;
} else { alertBanner.style.display = 'none'; }
// 更新图表
updateCharts();
// 添加日志
addLog(currentVal, threshold);
}
function updateCharts() {
const now = Date.now();
// 更新波形图
waveformChart.data.datasets[0].data = motionData;
waveformChart.options.scales.x.min = now - (config.historyRange * 1000);
waveformChart.options.scales.x.max = now;
waveformChart.update('quiet');
}
function setHistoryRange(seconds, btn) {
config.historyRange = seconds;
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
updateCharts();
}
function addLog(val, threshold) {
const logBox = document.getElementById('logEntries');
const entry = document.createElement('div');
let logClass = 'success';
if (val > threshold) { logClass = 'danger'; } else if (val > threshold * 0.8) { logClass = 'warning'; }
const now = new Date();
entry.className = `log-entry ${logClass}`;
entry.innerHTML = `<div class="log-time">${now.toLocaleTimeString('zh-CN', {hour12: false})}</div><div class="log-message">运动强度: <strong>${val.toFixed(2)}</strong> / 阈值: ${threshold.toFixed(1)}</div>`;
logBox.prepend(entry);
if (logBox.children.length > 30) { logBox.removeChild(logBox.lastChild); }
}
function clearLogs() {
const logBox = document.getElementById('logEntries');
const nowStr = new Date().toLocaleTimeString('zh-CN', {hour12: false});
logBox.innerHTML = `<div class="log-entry success"><div class="log-time">系统日志已清空</div><div class="log-message">日志在 ${nowStr} 被清除</div></div>`;
}
function updateConstellation(points) { constellationChart.data.datasets[0].data = points; constellationChart.update('none'); }
// 连接WebSocket后端
function connect() {
const status = document.getElementById('connectionStatus');
status.innerHTML = `<span class="status-indicator" style="background: #f59e0b;"></span><span>正在连接服务器...</span>`;
const ws = new WebSocket(`ws://127.0.0.1:8765`);
ws.onopen = () => {
status.innerHTML = `<span class="status-indicator" style="background: #10b981;"></span><span>实时连接已建立 | 数据接收中...</span>`;
addLog(0, 6);
};
ws.onmessage = (e) => {
const line = e.data;
handleIncomingData(line); // 处理运动数据
const csiPoints = parseCSI(line); // 处理CSI数据
if (csiPoints) { updateConstellation(csiPoints); }
};
ws.onclose = () => {
status.innerHTML = `<span class="status-indicator" style="background: #ef4444;"></span><span>连接断开,5秒后重连...</span>`;
setTimeout(connect, 5000);
};
// 模拟数据(当WebSocket无法连接时启用)
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
console.log("启动模拟数据...");
let simulatedValue = 3.0;
setInterval(() => {
const change = (Math.random() - 0.5) * 2;
simulatedValue = Math.max(0, Math.min(10, simulatedValue + change));
const fakeData = `mvmt:${simulatedValue.toFixed(4)} thr:1.20 pkt/s:20 rssi:-30`;
handleIncomingData(fakeData);
}, 1000);
}
}, 2000);
}
// 页面加载初始化
window.onload = () => {
initCharts();
connect();
// 阈值滑块事件
document.getElementById('thresholdSlider').oninput = function() {
const value = parseFloat(this.value).toFixed(1);
document.getElementById('thresholdValue').textContent = value;
const currentVal = parseFloat(document.getElementById('currentMotion').textContent);
if (currentVal > value) { document.getElementById('alertBanner').style.display = 'flex'; }
};
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('zh-CN', {hour12: false});
};
</script>
</body>
</html>
运行前端页面后,最终可以看到一个完整的实时监控系统界面。

免责声明
本文所涉及的技术与系统仅用于 学习、研究与技术验证目的。
尽管 CSI 数据本身不包含音频、视频或直接的通信内容,但该系统 仍然具备强大的空间感知能力 ,可能被用于:
- 未经同意的人员存在检测。
- 行为模式分析(作息、活动规律)。
- 私人空间内的持续监控。
请在使用和部署本系统时,严格遵守当地法律法规及隐私保护要求。无线感知技术也常应用于安全研究领域,例如无线侧信道分析,如果你想深入了解此类前沿攻防技术,可以访问 安全/渗透/逆向 板块进行交流。
希望这篇在 云栈社区 分享的详细教程,能帮助你成功复现并理解基于Wi-Fi CSI的无线感知技术。