Esp32s-micropython开发番外——Web控制小灯亮度

所使用的开发板: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()

核心功能

  1. LED控制

    • 开关控制(开灯/关灯按钮)

    • 0-1023范围亮度调节(滑杆控制)

  2. 实时状态显示

    • LED状态(开启/关闭)

    • 当前亮度百分比

    • 亮度值实时更新

  3. 网络功能

    • 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解析响应并更新界面
}

创新点与亮点

  1. 无刷新操作体验

    • 使用AJAX实现所有操作无需页面刷新

    • 实时状态同步更新

  2. 专业UI设计

    • 玻璃拟态效果(毛玻璃效果)

    • 响应式布局适配各种设备

    • 动态阴影和悬停效果

    • 精心设计的色彩方案

  3. 实时反馈机制

    • 操作过程中的"设置中"提示

    • 滑杆值实时显示

    • 状态卡片即时更新

  4. 错误恢复机制

    • 操作失败时自动刷新页面

    • 异常时设备自动重启

  5. 性能优化

    • 请求防抖动(300ms延迟)

    • 高效的状态管理

    • 精简的DOM操作

项目结构

ESP32_LED_Control/
├── main.py              # 主程序
├── index.html           # 网页模板(嵌入式)
├── styles.css           # 样式表(嵌入式)
└── scripts.js           # JavaScript(嵌入式)

使用方法

  1. 硬件连接

    • LED正极 → GPIO2 (220Ω电阻)

    • LED负极 → GND

  2. 软件部署

    • 修改代码中的WiFi凭据

    • 上传代码到ESP32

    • 通过串口监视器查看分配的IP地址

  3. 访问控制面板

    • 浏览器访问显示的IP地址

    • 使用按钮控制开关

    • 拖动滑杆调节亮度

常见问题解决方案

  1. 网页乱码

    # 确保正确设置字符集
    conn.send('Content-Type: text/html; charset=utf-8\r\n')
    response = web_page(...).encode('utf-8')
  2. 状态不同步

    • 使用AJAX统一所有操作

    • 实现全面的状态同步机制

  3. 连接不稳定

    • 增加WiFi连接等待时间

    • 添加异常重启机制

扩展方向

  1. 多设备控制

    • 扩展支持多个GPIO控制

    • 添加设备选择界面

  2. 安全增强

    • 添加用户认证

    • HTTPS支持

  3. 高级功能

    • 定时任务

    • 亮度模式预设

    • 能耗统计

  4. 移动应用

    • 开发配套手机APP

    • 添加推送通知功能

项目总结

本项目展示了如何使用MicroPython在ESP32上构建一个功能完善的Web控制面板。通过结合硬件控制、网络通信和Web技术,实现了一个用户友好的LED控制系统。项目亮点包括:

  • 专业美观的UI设计:采用现代CSS技术实现玻璃拟态效果

  • 流畅的用户体验:无刷新操作和实时状态更新

  • 健壮的架构:全面的错误处理和恢复机制

  • 高效通信:优化的AJAX请求和状态同步

这个项目不仅是一个实用的LED控制器,也是一个很好的物联网开发模板,可以扩展到各种智能家居和工业控制场景。完整代码已优化并经过充分测试,可直接部署使用。

Web端

优雅地玩弄一颗小LED!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值