用 AI 辅助做风扇振动监控,第2期。硬件测试通了,接下来要把数据从 ESP32 传到服务器,做个 Web 看板实时监控。全链路打通的过程中,遇到了几个典型问题,逐一解决。

一、项目背景

上期搞定了 MPU6050 传感器,能读到三轴加速度和温度了。但数据留在 ESP32 本地没什么用,得传到服务器上才能做分析、做展示。

这期的目标很明确:

ESP32(采集) → HTTP POST → Flask 服务器 → SQLite 存储
                                              ↓
                                        Web 看板(Chart.js)
  1. ESP32 通过 WiFi 上传传感器数据(振动、转速、温度)
  2. Flask 服务器接收数据,存入 SQLite
  3. Web 看板:实时显示数据曲线

链路看着清晰,让AI完成代码,很快就写完了。但是,真调起来也有几个需要注意的地方。

二、问题解决

问题 1:WiFi 连接时 ESP32 反复重启

一启动 WiFi,ESP32 就复位,串口反复打印 Brownout detector was triggered

-> 50%
Brownout detector was triggered

ets Jul 29 2019 12:21:46
rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)

Brownout detector 是 ESP32 的欠压保护。WiFi 启动时电流峰值约 200~300mA,USB 供电一旦跟不上,电压跌到 2.5V 以下就触发复位。

先从供电这条线排查,换 USB 线、插电脑后置的 USB 3.0 口,还是 Brownout;把风扇拔了只留 ESP32,依旧 Brownout。说明光靠换线换口喂不饱它,总是欠压。

于是,惹不起,躲得起。开发阶段先用代码把 Brownout 检测关掉绕过去:

#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>

void setup() {
  // 关闭欠压检测(供电不足时临时绕过)
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
  
  Serial.begin(115200);
  // ...
}

这只是调试期暂时这么处理。ESP32 的 WiFi 模块功耗大,如果要做产品还是得解决供电问题,独立 5V 供电或者在电源上加个电容顶住峰值电流。

问题 2:HTTP 连接被拒绝

WiFi 通了,接着让 ESP32 把数据 POST 到服务器,结果连上 WiFi 却报 connection refused

已连接,IP: 192.168.0.100
系统就绪
RPM:0 VIB:1.168g T:46.1C FAULT:0 PWM:50
HTTP 失败: connection refused

这个问题如果做过web应用部署,一看就能想到是防火墙。之前搭建过无数的应用,各种端口都得服务器上放行,云平台上放行,所以这其实也不算个问题。

加一条防火墙入站规则放行 6006 端口:

# 管理员 PowerShell 执行
netsh advfirewall firewall add rule name="Flask 6006" dir=in action=allow protocol=tcp localport=6006

ESP32 串口随即回了:

HTTP 响应: {"status": "OK"}

Flask 监听 0.0.0.0 只是说程序愿意接所有网卡的连接,防火墙放不放行是另一回事。本机浏览器一测正常,很容易误判成服务器没毛病。

问题 3:Web 看板数据错位

链路通了,做出第一版 Web 看板,数据却对不上:温度显示成了转速,转速显示成了振动。

服务器端 /api/query 用 SQL 查数据返回给前端,问题就出在 SQL 查询的列顺序和前端解析的字段索引没对齐:

# 服务器端查询
c.execute('''
    SELECT id, device_id, timestamp, ax, ay, az, 
           vibMagnitude, fanRPM, temperature, faultStatus, pwmDuty, mode, received_at
    FROM sensor_data 
    ORDER BY id DESC 
    LIMIT 30
''')

前端按位置取值,只要列的顺序和它心里预期的索引错一位,后面全跟着串位。改法是后端返回时就用字典把字段名和值绑死,不让前端去数第几列:

result = []
for row in rows:
    result.append({
        "id": row[0],
        "device_id": row[1],
        "timestamp": row[2],
        "ax": row[3],
        "ay": row[4],
        "az": row[5],
        "vibMagnitude": row[6],
        "fanRPM": row[7],
        "temperature": row[8],
        "faultStatus": row[9],
        "pwmDuty": row[10],
        "mode": row[11],
        "received_at": row[12]
    })

return jsonify({"count": len(result), "data": result})

前端直接用字段名取:

data.data.forEach(item => {
    labels.push(item.received_at);
    vibData.push(item.vibMagnitude);
    rpmData.push(item.fanRPM);
    tempData.push(item.temperature);
});

SELECT * 或者按位置取值,表结构一改就出这种错。前后端统一走 JSON key 对齐,不依赖列顺序,以后加字段、调顺序都不会再串位。

三、最终方案

数据流

ESP32(每3秒上报)
  ↓ HTTP POST JSON
Flask 服务器(/api/data)
  ↓ 存储
SQLite 数据库(sensor_data 表)
  ↓ 查询 API(/api/query)
Web 看板(Chart.js 实时刷新)

ESP32 端:HTTP POST 上报
 

void sendData(float ax, float ay, float az, float vib, 
              uint16_t fanRpm, float temp, uint8_t fault, 
              uint8_t pwm, const char* mode) 
{
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi 断开,跳过上报");
    return;
  }

  HTTPClient http;
  http.setConnectTimeout(2000);   // TCP 连接超时 2 秒
  http.setTimeout(2000);          // HTTP 响应超时 2 秒
  http.begin(serverUrl);
  http.addHeader("Content-Type", "application/json");

  // ArduinoJson 打包
  StaticJsonDocument<512> doc;
  doc["device_id"] = "esp32_fan_01";
  doc["timestamp"] = millis();
  doc["ax"] = round(ax * 1000) / 1000.0;
  doc["ay"] = round(ay * 1000) / 1000.0;
  doc["az"] = round(az * 1000) / 1000.0;
  doc["vibMagnitude"] = round(vib * 1000) / 1000.0;
  doc["fanRPM"] = fanRpm;
  doc["temperature"] = round(temp * 100) / 100.0;
  doc["faultStatus"] = fault;
  doc["pwmDuty"] = pwm;
  doc["mode"] = mode;

  String payload;
  serializeJson(doc, payload);

  int httpCode = http.POST(payload);
  if (httpCode > 0) {
    String response = http.getString();
    Serial.print("HTTP 响应: ");
    Serial.println(response);
  } else {
    Serial.print("HTTP 失败: ");
    Serial.println(http.errorToString(httpCode));
  }

  http.end();
}

服务器端:Flask + SQLite

from flask import Flask, request, jsonify
import sqlite3
from datetime import datetime

app = Flask(__name__)
DB_PATH = 'fan_data.db'

@app.route('/api/data', methods=['POST'])
def receive_data():
    """接收 ESP32 上报的传感器数据"""
    data = request.get_json()
    
    print(f"\n[{datetime.now().strftime('%H:%M:%S')}] 收到数据:")
    print(f"  设备: {data.get('device_id')}")
    print(f"  振动: {data.get('vibMagnitude')}g | 转速: {data.get('fanRPM')} RPM")
    print(f"  温度: {data.get('temperature')}°C | PWM: {data.get('pwmDuty')}%")
    
    # 存入数据库
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        INSERT INTO sensor_data 
        (device_id, timestamp, ax, ay, az, vibMagnitude, fanRPM, 
         temperature, faultStatus, pwmDuty, mode, received_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ''', (
        data.get('device_id'),
        data.get('timestamp'),
        data.get('ax'), data.get('ay'), data.get('az'),
        data.get('vibMagnitude'),
        data.get('fanRPM'),
        data.get('temperature'),
        data.get('faultStatus'),
        data.get('pwmDuty'),
        data.get('mode'),
        datetime.now().isoformat()
    ))
    conn.commit()
    conn.close()
    
    return jsonify({"status": "OK"}), 200

@app.route('/api/query', methods=['GET'])
def query_data():
    """查询最近 30 条数据(看板刷新用)"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        SELECT id, device_id, timestamp, ax, ay, az, 
               vibMagnitude, fanRPM, temperature, faultStatus, pwmDuty, mode, received_at
        FROM sensor_data 
        ORDER BY id DESC 
        LIMIT 30
    ''')
    rows = c.fetchall()
    conn.close()
    
    result = []
    for row in rows:
        result.append({
            "id": row[0],
            "device_id": row[1],
            "timestamp": row[2],
            "vibMagnitude": row[6],
            "fanRPM": row[7],
            "temperature": row[8],
            "faultStatus": row[9],
            "pwmDuty": row[10],
            "received_at": row[12]
        })
    
    return jsonify({"count": len(result), "data": result})

Web 看板:Chart.js 实时刷新

// 每 3 秒刷新数据
setInterval(async () => {
    const res = await fetch('/api/query');
    const json = await res.json();
    
    const data = json.data.reverse();  // 按时间正序
    
    const labels = data.map(d => d.received_at.split('T')[1].substring(0, 8));
    const vibData = data.map(d => d.vibMagnitude);
    const rpmData = data.map(d => d.fanRPM);
    const tempData = data.map(d => d.temperature);
    
    // 更新振动曲线
    vibrationChart.data.labels = labels;
    vibrationChart.data.datasets[0].data = vibData;
    vibrationChart.update('none');
    
    // 更新转速曲线
    rpmChart.data.labels = labels;
    rpmChart.data.datasets[0].data = rpmData;
    rpmChart.update('none');
    
    // 更新温度曲线
    tempChart.data.labels = labels;
    tempChart.data.datasets[0].data = tempData;
    tempChart.update('none');
}, 3000);

四、问题解决总结

问题 原因 解决方案
WiFi Brownout 复位 USB 供电不足,WiFi 启动电流峰值触发欠压保护 代码关闭 Brownout 检测(临时)或独立供电(产品)
HTTP connection refused Windows 防火墙拦截局域网入站连接 添加防火墙入站规则,放行 6006 端口
看板数据错位 SQL 列索引和前端字段映射不匹配 用字典映射代替位置索引,前后端通过 JSON key 对齐

五、几点经验

AI写代码,速度真的快,但是有会出问题,需要人来判断。

ESP32 的 WiFi 功耗比想象中大,开发时关掉 Brownout 检测能应付,但产品上必须把供电做扎实,不然现场一上电就复位。

防火墙问题,用到哪个端口,先把规则给加上,省的有问题再去怀疑别的。

前后端对数据,宁可用字段名也别用 SQL 列索引,字典映射加 JSON key 对齐,表结构一变也不会串位。

调试一定要分段,ESP32、网络、服务器、数据库、前端一段段验证,才能快速定位问题卡在哪一环。

下期预告:风扇控制 + 转速采集。ESP32 通过 PWM 控制风扇转速,同时读取 TACH 信号计算 RPM。结果发现中断方式采集转速根本不靠谱——风扇减速时的反电动势噪声会把中断搞疯。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐