AI编程实现ESP32 + MPU6050 风扇振动监控,上传到服务器到做数据可视化
用 AI 辅助做风扇振动监控,第2期。硬件测试通了,接下来要把数据从 ESP32 传到服务器,做个 Web 看板实时监控。全链路打通的过程中,遇到了几个典型问题,逐一解决。
一、项目背景
上期搞定了 MPU6050 传感器,能读到三轴加速度和温度了。但数据留在 ESP32 本地没什么用,得传到服务器上才能做分析、做展示。
这期的目标很明确:
ESP32(采集) → HTTP POST → Flask 服务器 → SQLite 存储
↓
Web 看板(Chart.js)
- ESP32 通过 WiFi 上传传感器数据(振动、转速、温度)
- Flask 服务器接收数据,存入 SQLite
- 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。结果发现中断方式采集转速根本不靠谱——风扇减速时的反电动势噪声会把中断搞疯。
更多推荐


所有评论(0)