找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2198

积分

0

好友

316

主题
发表于 前天 16:06 | 查看: 8| 回复: 0

ESPectre项目地址

https://github.com/francescopace/espectre

用Wi-Fi“看见”人

这是一种基于 Wi-Fi CSI(信道状态信息) 的室内空间被动感知系统。它利用普通的2.4GHz Wi-Fi路由器和ESP32设备,捕捉无线信道在时间维度上的微小变化,从而实现对室内活动状态的判断。

ESpectre系统监控界面截图,显示物体移动度波形图与CSI子载波星座图

原理简述

当人在房间内移动时,身体会像障碍物一样影响Wi-Fi无线电波的传播路径。Wi-Fi本质上是持续发射的无线电波,就像湖面的水波。人体的移动会改变“回波”的相位与幅度。

ESPectre的核心正是捕获Wi-Fi信道状态信息(CSI)的这些微小波动,并通过算法分析来判断环境中是否存在运动行为。

这与传统方案有根本区别:

方案 感知方式
摄像头 采集光学图像
红外 检测温度变化
毫米波 主动发射雷达信号
espectre 被动测量 Wi-Fi 信道结构变化

技术核心

在Wi-Fi的OFDM物理层中,信道被划分为数十个子载波,每个子载波都有一个复数形式的信道响应:

Wi-Fi信道响应数学公式,表示为H(f,t)=A(f,t)e^{jθ(f,t)}

这个响应包含了幅度(A)和相位(θ)。当人体进入、移动、呼吸或改变姿态时,相当于对信道响应进行了拉伸、扭曲或反射。ESPectre通过分析这些CSI数据的微小变化,来判断环境中的物理运动。

如果你对网络底层的协议和信号处理原理感兴趣,可以在 网络/系统 板块找到更多深入探讨TCP/IP、HTTP及无线通信相关的资料。

局限性

  1. 频段拥塞:2.4GHz频段极易受到蓝牙设备、微波炉等干扰。建议尝试切换信道。
    • 根据802.11协议,每个信道通常占用22MHz带宽。
    • 在2.4GHz频段,只有第1、6、11信道是完全不重叠的,这是避开干扰的首选。

2.4GHz Wi-Fi信道分布图,显示14个信道及其22MHz带宽

  1. 多路径自干扰:在极小空间内(如金属柜),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-S3ESP32-C6,正好是之前做其他项目时购入的开发板,资源相对充裕。

购物应用交易记录截图,显示购买的ESP32和STM32开发板

手持ESP32-S3开发板照片,背景为代码编辑器界面


快速复现(ESP32-S3 实测)

ESPectre提供 开发版稳定测试版 两套配置,这里直接使用作者维护的官方示例。


第一步:安装 ESPHome

pip3 install esphome

终端使用pip3安装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

终端使用wget命令从GitHub下载配置文件的截图

第三步:刷写固件

将 ESP32-S3 通过 USB 连接至 Linux 主机,执行:

esphome run espectre-s3.yaml

终端显示ESPHome编译ESP32过程的截图

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

终端显示ESP32-S3编程过程,包括连接、配置闪存和写入数据

程序写入成功。

终端显示ESP32-S3编程成功并开始输出日志

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

cat /dev/ttyACM0

终端显示ESP8266设备Wi-Fi CSI运动检测系统的配置界面

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

终端日志截图,内容涉及WiFi连接状态和传感器数据


环境部署与 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 命令查看容器状态。

终端运行docker ps命令,列出正在运行的容器

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

Home Assistant网页界面概览,显示ESPectre设备状态

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

Home Assistant中显示的运动得分折线图

物联网和智能家居的整合是当前的热点,更多关于 Docker、云计算和智能化应用部署的实践,可以关注 智能 & 数据 & 云 板块。

深度二开:CSI 原始数据流的可视化

在稳定版的基础上,可以通过修改底层代码,打通从原始信号采样到前端可视化显示的完整链路。

克隆项目

git clone https://github.com/francescopace/espectre.git

底层源码修改

在开发版代码中修改 CSIManager.cpp,实现周期性输出子载波的原始 I/Q 数据。

代码编辑器界面截图,显示打开的文件路径

在底层的 process_packet 函数中,直接提取 I/Q 信号分量。需要注意的是:ESP32 输出的 CSI 数据是 int8_t 类型的有符号整型。

物理意义

在前端生成的星座图中,每一个点代表一个子载波的相量状态。

  1. 点簇集中:代表信道静态稳定。
  2. 点簇扩散/旋转:代表环境中有物理遮挡改变了相位偏移,这是判断微小运动(如呼吸)的关键指标。

CSI子载波星座散点图,横轴为I(实部),纵轴为Q(虚部)

修改 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>

运行前端页面后,最终可以看到一个完整的实时监控系统界面。

ESpectre运动监测系统Web界面最终效果图

免责声明

本文所涉及的技术与系统仅用于 学习、研究与技术验证目的

尽管 CSI 数据本身不包含音频、视频或直接的通信内容,但该系统 仍然具备强大的空间感知能力 ,可能被用于:

  • 未经同意的人员存在检测。
  • 行为模式分析(作息、活动规律)。
  • 私人空间内的持续监控。

请在使用和部署本系统时,严格遵守当地法律法规及隐私保护要求。无线感知技术也常应用于安全研究领域,例如无线侧信道分析,如果你想深入了解此类前沿攻防技术,可以访问 安全/渗透/逆向 板块进行交流。

希望这篇在 云栈社区 分享的详细教程,能帮助你成功复现并理解基于Wi-Fi CSI的无线感知技术。




上一篇:Anthropic封杀Claude API第三方工具,开发工作流受影响
下一篇:Linux故障定位与性能调优实战指南:从CPU、内存到网络的全方位解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 18:40 , Processed in 0.378566 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表