所使用的开发板:esp32s
ESP32 MicroPython Web控制面板开发总结
项目概述
本项目基于ESP32开发板和MicroPython固件,实现了一个功能完整的Web控制面板,用于远程控制GPIO2连接的LED灯。项目结合了WiFi连接、Web服务器开发、PWM调光和AJAX技术,提供了开/关控制、亮度调节和实时状态显示功能。
import network
import socket
import time
from machine import Pin, PWM, reset
import math
# 配置 WiFi 信息
WIFI_SSID = "your_wifi_ssid" # 替换为你的 WiFi 名称
WIFI_PASSWORD = "your_wifi_password" # 替换为你的 WiFi 密码
# 创建 LED 对象 (GPIO2)
led = PWM(Pin(2), freq=1000, duty=0) # 使用 PWM 控制亮度
# 连接 WiFi
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
# 等待连接
max_wait = 20
while max_wait > 0:
if wlan.isconnected():
break
max_wait -= 1
print('等待连接...')
time.sleep(1)
if not wlan.isconnected():
print('连接失败')
return None
else:
ip = wlan.ifconfig()[0]
print('连接成功, IP:', ip)
return ip
# 创建优化的网页
def web_page(led_state, brightness):
"""根据 LED 状态生成 HTML 页面"""
# 计算亮度百分比
brightness_percent = int((brightness / 1023) * 100)
html = """<html>
<head>
<meta charset="UTF-8">
<title>ESP32 LED 控制面板</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #0c1e2d, #1a3a4f, #2c5364);
color: #fff;
min-height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.container {
background: rgba(0, 0, 0, 0.75);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
width: 100%;
max-width: 650px;
padding: 35px;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
font-size: 2.8rem;
margin-bottom: 25px;
color: #4fc3f7;
text-shadow: 0 0 15px rgba(79, 195, 247, 0.7);
letter-spacing: 1px;
}
.status-card {
background: rgba(255, 255, 255, 0.08);
border-radius: 18px;
padding: 28px;
margin: 30px 0;
display: flex;
justify-content: space-around;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.status-item {
text-align: center;
min-width: 150px;
padding: 10px;
}
.status-label {
font-size: 1.2rem;
margin-bottom: 12px;
color: #a0d0f0;
letter-spacing: 0.5px;
}
.status-value {
font-size: 2.4rem;
font-weight: bold;
color: #4fc3f7;
text-shadow: 0 0 10px rgba(79, 195, 247, 0.5);
}
.control-panel {
background: rgba(255, 255, 255, 0.08);
border-radius: 18px;
padding: 35px;
margin: 30px 0;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.btn-group {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 35px;
}
/* 修复下划线问题 */
.btn-link {
text-decoration: none !important;
display: inline-block;
border: none;
outline: none;
}
.btn {
padding: 22px 40px;
font-size: 1.4rem;
border: none;
border-radius: 60px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.4);
text-decoration: none !important;
}
.btn-on {
background: linear-gradient(to right, #00e676, #00c853);
color: white;
}
.btn-off {
background: linear-gradient(to right, #ff5252, #d50000);
color: white;
}
.btn:hover {
transform: translateY(-6px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
}
.brightness-control {
margin-top: 35px;
}
.brightness-header {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
}
.slider-container {
position: relative;
padding: 30px 0;
}
.slider-labels {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
font-size: 1.3rem;
color: #c0e0f0;
}
.slider-labels span {
display: flex;
align-items: center;
gap: 12px;
}
input[type="range"] {
width: 100%;
height: 35px;
-webkit-appearance: none;
background: linear-gradient(to right, #1a237e, #4fc3f7, #ffffff);
border-radius: 18px;
outline: none;
box-shadow: 0 0 15px rgba(79, 195, 247, 0.3);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 50px;
height: 50px;
border-radius: 50%;
background: #ffeb3b;
cursor: pointer;
box-shadow: 0 0 20px rgba(255, 235, 59, 0.9);
border: 4px solid #ff9800;
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 25px rgba(255, 235, 59, 1);
}
.brightness-display {
display: flex;
justify-content: center;
align-items: center;
margin-top: 30px;
gap: 20px;
}
.brightness-value {
font-size: 2.4rem;
font-weight: bold;
color: #ffeb3b;
min-width: 100px;
text-align: center;
text-shadow: 0 0 15px rgba(255, 235, 59, 0.6);
background: rgba(0, 0, 0, 0.3);
padding: 12px 25px;
border-radius: 15px;
}
.loading-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none;
color: #4fc3f7;
font-size: 1.3rem;
background: rgba(0, 0, 0, 0.8);
padding: 15px 30px;
border-radius: 25px;
z-index: 10;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
}
.loading-indicator i {
animation: spin 1.5s linear infinite;
margin-right: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
footer {
margin-top: 35px;
font-size: 1.1rem;
color: #a0d0f0;
text-align: center;
line-height: 1.6;
}
@media (max-width: 700px) {
.container {
padding: 25px 20px;
max-width: 95%;
}
h1 {
font-size: 2.2rem;
}
.btn {
padding: 18px 30px;
font-size: 1.2rem;
}
.status-value {
font-size: 2rem;
}
.btn-group {
flex-direction: column;
gap: 20px;
}
.status-card {
flex-direction: column;
gap: 25px;
}
}
</style>
</head>
<body>
<div class="container">
<h1><i class="fas fa-lightbulb"></i> ESP32 LED 智能控制系统</h1>
<div class="status-card">
<div class="status-item">
<div class="status-label">LED 状态</div>
<div class="status-value" id="statusState">""" + led_state + """</div>
</div>
<div class="status-item">
<div class="status-label">当前亮度</div>
<div class="status-value" id="statusBrightness">""" + str(brightness_percent) + """%</div>
</div>
</div>
<div class="control-panel">
<div class="btn-group">
<a href="javascript:void(0);" onclick="controlLED('on')" class="btn-link">
<button class="btn btn-on"><i class="fas fa-power-off"></i> 开灯</button>
</a>
<a href="javascript:void(0);" onclick="controlLED('off')" class="btn-link">
<button class="btn btn-off"><i class="fas fa-power-off"></i> 关灯</button>
</a>
</div>
<div class="brightness-control">
<div class="brightness-header">
<i class="fas fa-sun fa-2x" style="color: #ffeb3b;"></i>
<h2>亮度调节</h2>
<i class="fas fa-sun fa-2x" style="color: #ffeb3b;"></i>
</div>
<div class="slider-container">
<div class="slider-labels">
<span><i class="fas fa-moon"></i> 低亮度</span>
<span>高亮度 <i class="fas fa-sun"></i></span>
</div>
<input type="range" min="0" max="1023" value='""" + str(brightness) + """'
id="brightnessSlider" oninput="updateBrightness(this.value)">
<div class="loading-indicator" id="loadingIndicator">
<i class="fas fa-spinner"></i> 设置中...
</div>
</div>
<div class="brightness-display">
<div class="brightness-value" id="brightnessValue">""" + str(brightness_percent) + """%</div>
</div>
</div>
</div>
<footer>
<p><i class="fas fa-microchip"></i> MicroPython ESP32 物联网控制系统</p>
<p>GPIO2 LED 控制 | 实时亮度调节 | 专业控制面板</p>
</footer>
</div>
<script>
// 防止快速拖动发送过多请求
let brightnessTimeout = null;
function updateBrightness(value) {
// 更新亮度显示
const percent = Math.round((value / 1023) * 100);
document.getElementById('brightnessValue').textContent = percent + '%';
// 显示加载指示器
const loading = document.getElementById('loadingIndicator');
loading.style.display = 'block';
// 清除之前的超时
if (brightnessTimeout) {
clearTimeout(brightnessTimeout);
}
// 设置新的超时 - 限制请求频率
brightnessTimeout = setTimeout(() => {
// 发送亮度设置请求
const xhr = new XMLHttpRequest();
xhr.open('GET', '/?brightness=' + value, true);
xhr.onload = function() {
// 请求完成后隐藏加载指示器
loading.style.display = 'none';
if (xhr.status === 200) {
// 更新状态显示
updateStatusFromResponse(xhr.responseText);
} else {
console.error('设置亮度失败');
}
};
xhr.onerror = function() {
loading.style.display = 'none';
console.error('请求错误');
};
xhr.send();
}, 300); // 300ms延迟发送请求,防止过快
}
// 新增:控制LED开关的函数
function controlLED(state) {
// 显示加载指示器
const loading = document.getElementById('loadingIndicator');
loading.style.display = 'block';
// 发送LED控制请求
const xhr = new XMLHttpRequest();
xhr.open('GET', '/?led=' + state, true);
xhr.onload = function() {
// 请求完成后隐藏加载指示器
loading.style.display = 'none';
if (xhr.status === 200) {
// 更新状态显示
updateStatusFromResponse(xhr.responseText);
} else {
console.error('控制LED失败');
// 如果失败,刷新整个页面
location.reload();
}
};
xhr.onerror = function() {
loading.style.display = 'none';
console.error('请求错误');
location.reload();
};
xhr.send();
}
// 新增:从响应中提取并更新状态
function updateStatusFromResponse(response) {
// 使用DOM解析器提取状态信息
const parser = new DOMParser();
const doc = parser.parseFromString(response, 'text/html');
// 更新LED状态
const statusState = doc.getElementById('statusState');
if (statusState) {
document.getElementById('statusState').textContent = statusState.textContent;
}
// 更新亮度百分比
const statusBrightness = doc.getElementById('statusBrightness');
if (statusBrightness) {
document.getElementById('statusBrightness').textContent = statusBrightness.textContent;
}
// 更新亮度值显示
const brightnessValue = doc.getElementById('brightnessValue');
if (brightnessValue) {
document.getElementById('brightnessValue').textContent = brightnessValue.textContent;
}
// 更新滑杆位置
const brightnessSlider = doc.getElementById('brightnessSlider');
if (brightnessSlider) {
document.getElementById('brightnessSlider').value = brightnessSlider.value;
}
}
</script>
</body>
</html>"""
return html
# 主程序
try:
ip = connect_wifi()
if ip:
# 创建 socket 服务器
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 80))
s.listen(5)
print('服务器已启动, 访问地址: http://' + ip)
current_brightness = 0 # 当前亮度值 (0-1023)
led_state = "关闭" # 当前LED状态
while True:
conn, addr = s.accept()
print('客户端连接来自:', addr)
request = conn.recv(2048) # 增加缓冲区大小
request = str(request, 'utf-8') # 使用 UTF-8 解码请求
# 解析请求参数
led_on = request.find('/?led=on') > 0
led_off = request.find('/?led=off') > 0
brightness_set = request.find('brightness=') > 0
# 处理亮度设置
if brightness_set:
# 从请求中提取亮度值
brightness_start = request.find('brightness=') + len('brightness=')
brightness_end = request.find(' ', brightness_start)
if brightness_end == -1:
brightness_end = request.find('HTTP', brightness_start)
brightness_str = request[brightness_start:brightness_end].split('&')[0]
try:
new_brightness = int(brightness_str)
if 0 <= new_brightness <= 1023:
current_brightness = new_brightness
led.duty(current_brightness)
print('亮度设置为:', current_brightness)
led_state = "开启" if current_brightness > 0 else "关闭"
except ValueError:
print('无效的亮度值:', brightness_str)
# 处理开灯/关灯请求
if led_on:
print('LED 开启')
current_brightness = 512 # 默认中等亮度
led.duty(current_brightness)
led_state = "开启"
elif led_off:
print('LED 关闭')
current_brightness = 0
led.duty(0)
led_state = "关闭"
# 生成并发送响应
response = web_page(led_state, current_brightness)
conn.send('HTTP/1.1 200 OK\r\n')
conn.send('Content-Type: text/html; charset=utf-8\r\n')
conn.send('Connection: close\r\n\r\n')
conn.sendall(response.encode('utf-8'))
conn.close()
except OSError as e:
print('错误:', e)
print('重启中...')
time.sleep(5)
reset()
except KeyboardInterrupt:
print("程序被用户中断")
except Exception as e:
print('未处理的错误:', e)
print('重启中...')
time.sleep(5)
reset()
核心功能
-
LED控制
-
开关控制(开灯/关灯按钮)
-
0-1023范围亮度调节(滑杆控制)
-
-
实时状态显示
-
LED状态(开启/关闭)
-
当前亮度百分比
-
亮度值实时更新
-
-
网络功能
-
WiFi自动连接
-
Web服务器(HTTP端口80)
-
响应式网页设计
-
技术架构
关键技术点
1. 硬件控制
# PWM控制LED亮度
led = PWM(Pin(2), freq=1000, duty=0)
# 设置亮度
led.duty(512) # 50%亮度
# 开关灯
led.duty(0) # 关灯
led.duty(512) # 开灯(默认50%亮度)
2. 网络连接
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
# 等待连接...
return wlan.ifconfig()[0] # 返回IP地址
3. Web服务器
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 80))
s.listen(5)
while True:
conn, addr = s.accept()
request = conn.recv(2048).decode('utf-8')
# 处理请求...
4. 请求处理
# 解析请求参数
if 'brightness=' in request:
# 处理亮度设置
new_brightness = extract_value(request, 'brightness=')
led.duty(new_brightness)
elif '/?led=on' in request:
# 开灯
led.duty(512)
elif '/?led=off' in request:
# 关灯
led.duty(0)
5. 动态网页生成
def web_page(led_state, brightness):
brightness_percent = int((brightness / 1023) * 100)
html = f"""
<html>
<head>
<!-- 页面结构和样式 -->
</head>
<body>
<!-- 动态内容 -->
<div class="status-value">{led_state}</div>
<div class="status-value">{brightness_percent}%</div>
<input type="range" value="{brightness}" ...>
</body>
</html>
"""
return html
6. AJAX交互
// 亮度控制
function updateBrightness(value) {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/?brightness=' + value, true);
xhr.send();
}
// LED开关控制
function controlLED(state) {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/?led=' + state, true);
xhr.send();
}
// 状态更新
function updateStatusFromResponse(response) {
// 使用DOMParser解析响应并更新界面
}
创新点与亮点
-
无刷新操作体验
-
使用AJAX实现所有操作无需页面刷新
-
实时状态同步更新
-
-
专业UI设计
-
玻璃拟态效果(毛玻璃效果)
-
响应式布局适配各种设备
-
动态阴影和悬停效果
-
精心设计的色彩方案
-
-
实时反馈机制
-
操作过程中的"设置中"提示
-
滑杆值实时显示
-
状态卡片即时更新
-
-
错误恢复机制
-
操作失败时自动刷新页面
-
异常时设备自动重启
-
-
性能优化
-
请求防抖动(300ms延迟)
-
高效的状态管理
-
精简的DOM操作
-
项目结构
ESP32_LED_Control/ ├── main.py # 主程序 ├── index.html # 网页模板(嵌入式) ├── styles.css # 样式表(嵌入式) └── scripts.js # JavaScript(嵌入式)
使用方法
-
硬件连接
-
LED正极 → GPIO2 (220Ω电阻)
-
LED负极 → GND
-
-
软件部署
-
修改代码中的WiFi凭据
-
上传代码到ESP32
-
通过串口监视器查看分配的IP地址
-
-
访问控制面板
-
浏览器访问显示的IP地址
-
使用按钮控制开关
-
拖动滑杆调节亮度
-
常见问题解决方案
-
网页乱码
# 确保正确设置字符集 conn.send('Content-Type: text/html; charset=utf-8\r\n') response = web_page(...).encode('utf-8')
-
状态不同步
-
使用AJAX统一所有操作
-
实现全面的状态同步机制
-
-
连接不稳定
-
增加WiFi连接等待时间
-
添加异常重启机制
-
扩展方向
-
多设备控制
-
扩展支持多个GPIO控制
-
添加设备选择界面
-
-
安全增强
-
添加用户认证
-
HTTPS支持
-
-
高级功能
-
定时任务
-
亮度模式预设
-
能耗统计
-
-
移动应用
-
开发配套手机APP
-
添加推送通知功能
-
项目总结
本项目展示了如何使用MicroPython在ESP32上构建一个功能完善的Web控制面板。通过结合硬件控制、网络通信和Web技术,实现了一个用户友好的LED控制系统。项目亮点包括:
-
专业美观的UI设计:采用现代CSS技术实现玻璃拟态效果
-
流畅的用户体验:无刷新操作和实时状态更新
-
健壮的架构:全面的错误处理和恢复机制
-
高效通信:优化的AJAX请求和状态同步
这个项目不仅是一个实用的LED控制器,也是一个很好的物联网开发模板,可以扩展到各种智能家居和工业控制场景。完整代码已优化并经过充分测试,可直接部署使用。
Web端
优雅地玩弄一颗小LED!