# ESP32-S3 MicroPython 全栈实战指南：刷机 · 点灯 · Wi-Fi 控制 · OTA 远程升级

> 从零到量产：MicroPython 开发环境、ESP32-S3 刷机、LED 控制、Wi-Fi 网页控制、OTA 无线烧录、MQTT 上云与一键远程升级全流程。
> 适用平台：Windows / macOS / Linux（命令以 Windows `COMx` 为例，macOS/Linux 为 `/dev/tty*`）

---

## 目录

1. [硬件与软件准备](#1-硬件与软件准备)
2. [安装串口驱动](#2-安装串口驱动)
3. [刷入 MicroPython 固件（刷机全流程）](#3-刷入-micropython-固件刷机全流程)
4. [开发工具：Thonny 与命令行](#4-开发工具thonny-与命令行)
5. [编写代码并烧录运行](#5-编写代码并烧录运行)
6. [实战：LED 控制](#6-实战led-控制)
7. [常见问题排查](#7-常见问题排查)
8. [常用命令速查表](#8-常用命令速查表)
9. [Wi-Fi 联网 + Web 控制 LED](#9-wi-fi-联网--web-控制-led)
10. [OTA 无线烧录](#10-ota-无线烧录)
11. [MQTT 上云 + 手机 App 控灯](#11-mqtt-上云--手机-app-控灯)
12. [OTA 自动 + 云端版本检查的一键升级流水线](#12-ota-自动--云端版本检查的一键升级流水线)

---

## 1. 硬件与软件准备

### 硬件清单
| 物品 | 说明 |
| --- | --- |
| ESP32-S3 开发板 | 如 `ESP32-S3-DevKitC-1`、`ESP32-S3-WROOM-1` 等 |
| USB 数据线 | **必须能传数据**（很多充电线只能供电） |
| 电脑 | Windows 10+/macOS/Linux |
| 外接 LED + 220Ω 电阻（可选） | 用于外部 LED 实验 |
| 面包板 + 杜邦线（可选） | 外接电路用 |

### 软件清单
| 软件 | 用途 | 下载地址 |
| --- | --- | --- |
| Python 3.8+ | 运行 esptool / 工具 | https://www.python.org/downloads/ |
| esptool | 官方刷机工具 | `pip install esptool` |
| Thonny IDE | 图形化开发/烧录（推荐新手） | https://thonny.org/ |
| mpremote / ampy | 命令行文件管理 | `pip install mpremote` |

---

## 2. 安装串口驱动

ESP32-S3 自带 USB-CDC，很多板子插上就能识别。若设备管理器出现黄色感叹号，安装对应驱动：

- **CH340 / CH343**：https://www.wch.cn/downloads/CH341SER_EXE.html
- **CP210x（Silicon Labs）**：https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers

安装后，插入开发板，在「设备管理器 → 端口(COM 和 LPT)」中确认出现的端口号，例如 `COM3`。

> macOS/Linux 用户用 `ls /dev/tty.*`（macOS）或 `ls /dev/ttyUSB* /dev/ttyACM*`（Linux）查看。

---

## 3. 刷入 MicroPython 固件（刷机全流程）

### 步骤 3.1 下载固件
前往官方下载页：https://micropython.org/download/ESP32_S3/

- 选择 **Generic ESP32-S3** 的最新 `.bin` 固件（如 `ESP32_S3-20240602-v1.23.0.bin`）。
- 若你的板子带 **8MB / 16MB** 以上 Flash 且想用完整功能，选择带文件系统支持的版本即可。

将固件保存到便于访问的目录，例如 `C:\esp\firmware.bin`。

### 步骤 3.2 让开发板进入下载模式
ESP32-S3 进入烧录模式的方式：
1. **按住 `BOOT` 键不放**；
2. 按一下 `RESET`/`EN` 键；
3. 松开 `BOOT` 键。

> 部分板子（带原生 USB-CDC）在断电再插电时会自动进入下载模式，无需按 BOOT。

### 步骤 3.3 擦除 Flash（首次必做）
```bash
# Windows（将 COM3 换成你的端口）
esptool.py --chip esp32s3 --port COM3 erase_flash

# macOS / Linux
esptool.py --chip esp32s3 --port /dev/ttyACM0 erase_flash
```

看到 `Chip erase completed` 即成功。

### 步骤 3.4 烧录固件
```bash
# Windows
esptool.py --chip esp32s3 --port COM3 --baud 460800 write_flash -z 0x0 C:\esp\firmware.bin

# macOS / Linux
esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 460800 write_flash -z 0x0 ./firmware.bin
```

参数说明：
- `--chip esp32s3`：指定芯片型号
- `--baud 460800`：波特率，越高越快
- `-z`：压缩写入
- `0x0`：固件烧录的起始地址（**S3 必须是 0x0**）

看到 `Hash of data verified` / `Leaving...` 表示烧录完成。

### 步骤 3.5 验证
1. 按下 `RESET`/`EN` 键重启开发板；
2. 打开串口工具（Thonny / 串口助手），波特率 `115200`；
3. 应看到类似提示符：
   ```
   MicroPython v1.23.0 on 2024-06-02; ESP32-S3 module with ESP32S3
   Type "help()" for more information.
   >>>
   ```
   出现 `>>>` 即代表 MicroPython 运行成功！

---

## 4. 开发工具：Thonny 与命令行

### 4.1 Thonny（推荐新手）
1. 打开 Thonny → `运行 → 选择解释器`；
2. 解释器选 **MicroPython (ESP32)**，端口选你的 `COMx`；
3. 下方 Shell 出现 `>>>` 即可交互；
4. 写代码后在编辑器按 `Ctrl+S`，选择 **保存到 MicroPython 设备**，文件名建议用 `main.py`（开机自启）。

### 4.2 命令行工具 mpremote（推荐进阶）
```bash
# 进入 REPL 交互
mpremote connect COM3 repl

# 运行本地脚本
mpremote connect COM3 run blink.py

# 把文件传到板子
mpremote connect COM3 fs cp blink.py :blink.py

# 执行板子上的 main.py
mpremote connect COM3 run :main.py

# 软重启
mpremote connect COM3 reset
```

---

## 5. 编写代码并烧录运行

以最简单的「点灯」为例。在电脑上新建 `blink.py`：

```python
import machine
import time

led = machine.Pin(48, machine.Pin.OUT)  # 板载 LED（见注释）

while True:
    led.value(1)      # 点亮
    time.sleep(0.5)
    led.value(0)      # 熄灭
    time.sleep(0.5)
```

**运行方式 A（Thonny）**：`Ctrl+S` → 保存到 MicroPython 设备 → 命名 `main.py` → 板子复位即自动运行。

**运行方式 B（命令行）**：
```bash
mpremote connect COM3 fs cp blink.py :main.py
mpremote connect COM3 reset
```

> ⚠️ **关于 GPIO 48**：多数 ESP32-S3-DevKitC 板载 LED 在 **GPIO 48**。但不同板子引脚不同（有的用 GPIO 2 / 13 / 21）。请查阅你的板子丝印或原理图。外接 LED 见第 6 节。

---

## 6. 实战：LED 控制

### 6.1 板载 LED 闪烁
见第 5 节 `blink.py`。把 `48` 换成你板子的实际引脚即可。

### 6.2 外接 LED（最通用）
接线（低电平驱动，LED 长脚接 GPIO，短脚串 220Ω 电阻接地）：
- LED 长脚（阳极）→ GPIO **21**
- LED 短脚（阴极）→ 220Ω 电阻 → GND

```python
import machine
import time

led = machine.Pin(21, machine.Pin.OUT)

print("外置 LED 开始闪烁")
while True:
    led.on()          # 等价于 led.value(1)
    time.sleep(1)
    led.off()         # 等价于 led.value(0)
    time.sleep(1)
```

### 6.3 PWM 调光（呼吸灯效果）
用 `machine.PWM` 调节亮度：

```python
import machine
import time

led = machine.PWM(machine.Pin(21))   # 外接 LED 接 GPIO21
led.freq(1000)                       # PWM 频率 1kHz

def breathe():
    # 从暗到亮
    for duty in range(0, 1024, 8):
        led.duty(duty)
        time.sleep_ms(5)
    # 从亮到暗
    for duty in range(1023, -1, -8):
        led.duty(duty)
        time.sleep_ms(5)

while True:
    breathe()
```

> 注：ESP32 的 `duty` 范围在 MicroPython 中默认是 0–1023（10 位）。

### 6.4 板载 RGB 彩灯（WS2812 / NeoPixel，常见 S3 板载）
很多 ESP32-S3 开发板（如 DevKitC-1 的某些版本、`ESP32-S3-USB-OTG`）板载一颗 WS2812 RGB LED，连接在 **GPIO 48**：

```python
import machine
import neopixel
import time

PIN = 48          # 板载 RGB LED 引脚
N = 1             # LED 数量
np = neopixel.NeoPixel(machine.Pin(PIN), N)

def color(r, g, b):
    np[0] = (r, g, b)
    np.write()

while True:
    color(255, 0, 0)    # 红
    time.sleep(1)
    color(0, 255, 0)    # 绿
    time.sleep(1)
    color(0, 0, 255)    # 蓝
    time.sleep(1)
    color(0, 0, 0)      # 灭
    time.sleep(1)
```

> 若你的板载 RGB 不在 GPIO 48，请查原理图替换 `PIN`。`neopixel` 是 MicroPython 内置模块，无需额外安装。

### 6.5 用按键控制 LED（交互进阶）
外接一个按钮到 GPIO 0（S3 的 BOOT 键通常就在 GPIO 0，可直接复用）：

```python
import machine
import time

led = machine.Pin(21, machine.Pin.OUT)
button = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP)  # BOOT 键，按下为 0
state = 0

while True:
    if button.value() == 0:          # 按键按下
        state = 1 - state             # 翻转状态
        led.value(state)
        time.sleep(0.3)               # 简单防抖
```

---

## 7. 常见问题排查

| 现象 | 原因 / 解决 |
| --- | --- |
| `A fatal error occurred: Failed to connect` | 未进入下载模式 → 重做第 3.2 步（按住 BOOT 再 RESET） |
| `PermissionError: [Errno 13]` (Linux) | 串口权限不足 → `sudo usermod -a -G dialout $USER` 后重登 |
| 烧录后无 `>>>` 输出 | 波特率不对 / 固件地址错 → 确认 `0x0`；用 115200 重新连 |
| LED 不亮 | 引脚号错 → 查板子丝印；外接 LED 注意极性、电阻 |
| `ImportError: no module named 'neopixel'` | 用了普通 GPIO 教程却跑 RGB → 确认板载是 WS2812 且引脚正确 |
| 文件保存后不自动运行 | 文件名必须是 `main.py` 才会开机自启 |

---

## 8. 常用命令速查表

```bash
# 擦除
esptool.py --chip esp32s3 --port COM3 erase_flash

# 烧录
esptool.py --chip esp32s3 --port COM3 --baud 460800 write_flash -z 0x0 firmware.bin

# 查看端口（Windows PowerShell）
[System.IO.Ports.SerialPort]::getportnames()

# mpremote
mpremote connect COM3 repl                 # REPL
mpremote connect COM3 fs cp main.py :main.py  # 上传
mpremote connect COM3 run :main.py        # 运行
mpremote connect COM3 reset               # 重启

# 查看板子信息（REPL 内）
import sys; print(sys.implementation)      # 版本
import machine; print(machine.freq())      # CPU 频率
```

---

## 9. Wi-Fi 联网 + Web 控制 LED

让 ESP32-S3 连上 Wi-Fi，并起一个网页服务器，用浏览器点按钮就能远程开关 LED。无需插线，局域网内任意设备可访问。

### 9.1 连接 Wi-Fi
MicroPython 用 `network` 模块联网：

```python
import network
import time

SSID = "你的WiFi名"
PASSWORD = "你的WiFi密码"

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)   # 站点模式（连路由器）
    wlan.active(True)
    if not wlan.isconnected():
        print("正在连接", SSID, "...")
        wlan.connect(SSID, PASSWORD)
        # 等待连接，最多 15 秒
        for _ in range(30):
            if wlan.isconnected():
                break
            time.sleep(0.5)
    if wlan.isconnected():
        print("✅ 已连接，IP =", wlan.ifconfig()[0])
    else:
        print("❌ 连接失败，检查 SSID/密码")
    return wlan

connect_wifi()
```

> `wlan.ifconfig()` 返回 `(IP, 掩码, 网关, DNS)`。记下这个 **IP**，等会儿要用浏览器访问它。
> 若连接不稳定，可把 `wlan.config(pm=...) ` 关闭省电模式：`wlan.config(pm=0xa11140)`（关闭 Modem-sleep，提升稳定性）。

### 9.2 搭建 Web 服务器控制 LED
用标准库 `socket` 起一个极简 HTTP 服务（无需任何第三方库）：

```python
import network
import socket
import machine
import time

SSID = "你的WiFi名"
PASSWORD = "你的WiFi密码"

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
    time.sleep(0.5)
print("IP:", wlan.ifconfig()[0])

led = machine.Pin(21, machine.Pin.OUT)   # 外接 LED 接 GPIO21

def web_page(state):
    return """<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32-S3 LED</title>
<style>body{font-family:sans-serif;text-align:center;margin-top:40px}
button{font-size:20px;padding:12px 28px;margin:8px;border-radius:10px;border:none;cursor:pointer}
.on{background:#16a34a;color:#fff}.off{background:#dc2626;color:#fff}</style>
</head><body>
<h1>ESP32-S3 LED 控制台</h1>
<p>当前状态：<b>%s</b></p>
<a href="/?led=on"><button class="on">打开</button></a>
<a href="/?led=off"><button class="off">关闭</button></a>
</body></html>""" % ("ON" if state else "OFF")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 80))      # 监听 80 端口
s.listen(5)
print("Web 服务器已启动，浏览器访问 http://", wlan.ifconfig()[0])

while True:
    try:
        conn, addr = s.accept()
        request = conn.recv(1024).decode('utf-8')
        if "led=on" in request:
            led.on()
        elif "led=off" in request:
            led.off()
        state = led.value()
        conn.send("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n")
        conn.sendall(web_page(state).encode('utf-8'))
        conn.close()
    except Exception as e:
        print("错误:", e)
```

**运行**：
1. 把上面的脚本保存为 `main.py` 烧录到板子（参见第 5 节）；
2. 板子复位后，串口会打印 `IP: 192.168.x.x`；
3. 电脑/手机连**同一个 Wi-Fi**，浏览器打开 `http://192.168.x.x`；
4. 点「打开 / 关闭」即可远程控制 LED ✅。

> 进阶：把 LED 改成板载 RGB（`neopixel`，见 6.4），用 `?r=255&g=0&b=0` 传颜色参数即可做彩色网页调色盘。

---

## 10. OTA 无线烧录

「无线烧录」指不插 USB 线，通过 Wi-Fi 更新板子上的程序（甚至固件）。MicroPython 有三种常见方式：

### 10.1 WebREPL（官方无线文件传输，最省事）
MicroPython 自带 WebREPL，可在浏览器里无线传文件、进 REPL。

**板端启用**（首次需在串口 REPL 里做一次）：
```python
import webrepl_setup
# 按提示输入：E（启用）→ 设置密码 → 选开机自启（y）→ 重启
```
重启后 WebREPL 会在后台运行（端口 8266）。

**电脑端使用**：
1. 浏览器打开官方客户端：https://micropython.org/webrepl/
2. 地址填 `ws://192.168.x.x:8266`（用板子 IP），输入刚才设的密码，点 Connect；
3. 左下角 **Send a file** 选 `main.py` → 无线上传，等价于有线烧录；
4. 也可在网页底部 REPL 直接敲代码。

> 注意：WebREPL 与第 9 节的 Web 服务器都占网络资源，调试时二选一即可；正式部署可在 `main.py` 里用 `webrepl.start()` 按需开启。

### 10.2 应用代码 OTA（自己拉取更新，适合量产/远程）
思路：板子联网后向一台 HTTP 服务器请求最新 `main.py`，下载写入文件系统，再重启生效。

**服务器侧**：把新版 `main.py` 放到任意能访问的 HTTP 地址（电脑起个静态服务即可）：
```bash
# 在放有 main.py 的目录执行（电脑端 Python）
python -m http.server 8000
# 访问 http://电脑IP:8000/main.py 应能下载到文件
```

**板子侧 `ota_update()`**：
```python
import urequests
import uos
import machine

def ota_update(url, target="main.py"):
    print("下载:", url)
    r = urequests.get(url)
    if r.status_code != 200:
        print("下载失败，状态码:", r.status_code)
        r.close()
        return False
    tmp = target + ".tmp"
    with open(tmp, "w") as f:
        f.write(r.text)        # 写临时文件，避免下到一半破坏旧程序
    r.close()
    try:
        uos.remove(target)
    except OSError:
        pass
    uos.rename(tmp, target)    # 原子替换
    print("✅ 更新完成，重启中...")
    machine.reset()
    return True

# 用法：连好 Wi-Fi 后调用
# ota_update("http://192.168.1.50:8000/main.py")
```

> 建议把版本号写入文件（如 `version.txt`），更新前比对版本，仅在有新版时才拉取，省流量更稳。

### 10.3 固件 OTA（整包固件无线升级，需双分区）
如果要**无线升级 MicroPython 固件本身**（不只是应用代码），需用 `ota` 模块（来自 micropython-lib），且固件必须是**带双 app 分区**的构建版本：

```python
import ota
import urequests
import machine

def firmware_ota(url):
    r = urequests.get(url)
    print("写入 OTA 分区...")
    ota.write('ota', r.content)   # 写入备用分区
    r.close()
    ota.switch()                  # 切换启动分区
    print("✅ 固件已更新，重启中...")
    machine.reset()

# firmware_ota("http://server/esp32s3-ota.bin")
```

> ⚠️ 前提：固件需编译为 OTA 镜像（含 `ota_0` / `ota_1` 两个分区）。官方「Generic ESP32-S3」标准固件**不含双分区**，直接 OTA 会失败。需要的话可基于 MicroPython 源码用 `make BOARD=... OTA=1` 自行构建。普通玩家用 10.1 / 10.2 已完全够用。

---

## 11. MQTT 上云 + 手机 App 控灯

Web 控制只能局域网用，要「手机 4G 也能控灯」就得上云。MQTT 是 IoT 标配的轻量发布/订阅协议，配合公共 Broker 或私有云，手机装个 App 即可远程开关。

### 11.1 安装 MQTT 库
MicroPython 标准镜像不含 MQTT，用 `mip`（MicroPython 包管理器）装 `umqtt.simple`：

```bash
# 方式 A：mpremote（板子已联网）
mpremote connect COM3 mip install umqtt.simple

# 方式 B：板子 REPL 里直接装
import mip
mip.install("umqtt.simple")
```

> 若板子暂时无法联网装包，可在电脑上下载 `umqtt/simple.py` 后 `mpremote fs cp simple.py :umqtt/simple.py` 手动传上去。

### 11.2 先用公共 Broker 跑通
公共测试服务器 `broker.emqx.io`（无需账号），手机和板子都连它即可互通：

```python
from umqtt.simple import MQTTClient
import network, machine, time

SSID, PASS = "你的WiFi名", "你的WiFi密码"
wlan = network.WLAN(network.STA_IF); wlan.active(True); wlan.connect(SSID, PASS)
while not wlan.isconnected():
    time.sleep(0.5)
print("IP:", wlan.ifconfig()[0])

led = machine.Pin(21, machine.Pin.OUT)

SERVER    = "broker.emqx.io"
CLIENT_ID = "esp32s3_led_01"
TOPIC_SUB = b"home/led/control"     # 订阅：接收开关指令
TOPIC_PUB = b"home/led/state"       # 发布：上报当前状态

def sub_cb(topic, msg):
    print("收到:", topic, msg)
    if msg == b"on":
        led.on()
    elif msg == b"off":
        led.off()
    c.publish(TOPIC_PUB, b"on" if led.value() else b"off")

c = MQTTClient(CLIENT_ID, SERVER)
c.set_callback(sub_cb)
c.connect()
c.subscribe(TOPIC_SUB)
print("MQTT 已连接，等待指令...")

while True:
    c.check_msg()          # 非阻塞检查消息
    time.sleep(0.1)
```

**手机端发布测试**：安装 App「MQTT Dashboard」(Android) 或「MQTT Explorer」，连接服务器 `broker.emqx.io`，向主题 `home/led/control` 发送 `on` / `off`，灯即响应。

### 11.3 加断线重连（生产必加）
上面示例一旦掉线会卡死，实战包一层重连：

```python
def mqtt_connect():
    c = MQTTClient(CLIENT_ID, SERVER)
    c.set_callback(sub_cb)
    c.connect()
    c.subscribe(TOPIC_SUB)
    return c

c = mqtt_connect()
while True:
    try:
        c.check_msg()
    except OSError:           # 断线
        print("掉线，重连中...")
        try:
            c = mqtt_connect()
        except Exception:
            time.sleep(3)
    time.sleep(0.1)
```

### 11.4 手机 App 控灯（三种玩法）
| 方案 | 做法 | 适合 |
| --- | --- | --- |
| **MQTT Dashboard** | 手机连同一 Broker，按钮绑定 `home/led/control` 发 `on/off` | 最快验证，免开发 |
| **Home Assistant** | 板子发布 MQTT 自动发现配置，HA 里出现「灯」卡片，手机 HA App 控 | 想做智能家居全家桶 |
| **微信小程序/自建** | 小程序调云函数转发 MQTT | 想自己产品化 |

**接入 Home Assistant 自动发现**（在 `connect` 后发一条配置即可被 HA 自动识别）：
```python
import ujson
discovery = {
    "name": "ESP32-S3 LED",
    "unique_id": "esp32s3_led_01",
    "state_topic": "home/led/state",
    "command_topic": "home/led/control",
    "payload_on": "on", "payload_off": "off"
}
c.publish(b"homeassistant/light/esp32s3_led/config", ujson.dumps(discovery))
```
HA 侧启用 MQTT 集成后，无需手动配置即可看到这张灯卡片。

### 11.5 进阶：上云厂商（阿里云 IoT / 腾讯云 IoT）
上云厂商的 MQTT 要求**签名鉴权**（用户名、密码需按三元组 + 时间戳用 HMAC 计算，且多为 TLS 8883 端口）。要点：
- 设备三元组：`ProductKey` / `DeviceName` / `DeviceSecret`；
- 用 `hmac` + `hashlib` 计算 password（`securemode=3` + `signmethod=hmacsha256`）；
- 连接地址 `PK.iot-as-mqtt.cn-shanghai.aliyuncs.com:8883`，需 `ssl` 上下文；
- 主题形如 `/sys/PK/DN/thing/service/property/set`。

> 这块签名细节较多，建议直接用各云厂商的 MicroPython 示例工程或 SDK，避免手算出错。若只是想「手机远程控灯」，11.2 的公共 Broker + MQTT Dashboard 已足够，无需上云。

---

## 12. OTA 自动 + 云端版本检查的一键升级流水线

把第 10.2 的应用 OTA 升级成**工程化流水线**：设备开机联网 → 查云端版本清单 → 比对本地版本 → 有新版就自动拉取、校验、替换、重启。配合第 11 节的 MQTT，还能用手机一键触发远程升级。

### 12.1 整体架构
```
[电脑 build_ota.py] --打包--> [HTTP 服务器]
                              ├── version.json   (latest / url / size / sha256)
                              └── main_1.2.0.py  (带版本号的程序)
                                       ▲
[ESP32-S3] 启动 → 读本地 version.txt → GET version.json → 比对 → 下载 → 校验 → 覆盖 main.py → 重启
                                       ▲
[手机 MQTT] ---- "upgrade" 指令 -----┘  (可选，触发立即检查)
```

### 12.2 云端版本清单与文件命名
服务器上准备一个目录（如 `www/firmware/`），存放：
- `version.json`：版本元数据
- `main_1.0.0.py` / `main_1.1.0.py` ...：每个版本一份，文件名带版本号

`version.json` 示例：
```json
{
  "latest": "1.2.0",
  "url": "http://192.168.1.50:8000/firmware/main_1.2.0.py",
  "size": 1856,
  "sha256": "a1b2c3...（程序文件的 sha256）"
}
```

### 12.3 板端：版本检查 + 安全 OTA
在 `main.py` 开头调用一次 `check_and_update()`。带 **大小 + sha256 双重校验**，下载失败绝不破坏旧程序：

```python
import urequests, uos, machine, uhashlib

CURRENT_VERSION_FILE = "version.txt"
UPDATE_BASE = "http://192.168.1.50:8000/firmware"   # 改成你的服务器

def read_version():
    try:
        with open(CURRENT_VERSION_FILE) as f:
            return f.read().strip()
    except OSError:
        return "0.0.0"

def write_version(v):
    with open(CURRENT_VERSION_FILE, "w") as f:
        f.write(v)

def ver_tuple(v):
    return tuple(int(x) for x in v.split("."))

def check_and_update():
    local = read_version()
    print("当前版本:", local)
    try:
        r = urequests.get(UPDATE_BASE + "/version.json", timeout=8)
        info = r.json()
        r.close()
    except Exception as e:
        print("版本检查失败:", e)
        return False

    latest = info["latest"]
    if ver_tuple(latest) <= ver_tuple(local):
        print("已是最新，无需升级")
        return False

    print("发现新版本:", latest, "开始下载...")
    r = urequests.get(info["url"], timeout=20)
    data = r.content          # bytes
    r.close()

    # 校验大小
    if "size" in info and len(data) != info["size"]:
        print("❌ 大小不符，终止升级")
        return False
    # 校验 sha256
    if "sha256" in info:
        h = uhashlib.sha256(data).hexdigest()
        if h != info["sha256"]:
            print("❌ 校验失败，终止升级")
            return False

    # 原子替换：先写临时文件，成功后再覆盖
    tmp = "main.py.tmp"
    with open(tmp, "wb") as f:
        f.write(data)
    try:
        uos.remove("main.py")
    except OSError:
        pass
    uos.rename(tmp, "main.py")
    write_version(latest)
    print("✅ 升级到", latest, "，重启中...")
    machine.reset()

# 在 main.py 业务代码前调用：
# check_and_update()
```

> **防变砖建议**：升级前把旧 `main.py` 备份成 `main.py.bak`，并在启动早期用 `machine.WDT` 看门狗——若新程序卡死不喂狗，自动复位回退。真正「变砖」只能串口重刷（见第 3 节），所以生产环境务必保留 WebREPL（10.1）作为兜底通道。

### 12.4 电脑端：一键打包脚本
用下面这个 `build_ota.py` 把新程序自动加版本号、算哈希、生成 `version.json`，再丢进 HTTP 服务器目录：

```python
import hashlib, json, os, shutil, sys

SRC = "main.py"                       # 待发布的程序
OUT_DIR = "www/firmware"              # 静态服务器目录
HOST = "192.168.1.50"                # 你的服务器 IP
PORT = 8000

def bump(ver):
    a, b, c = map(int, ver.split("."))
    return f"{a}.{b}.{c+1}"

# 读当前最新版本
vf = os.path.join(OUT_DIR, "version.json")
cur = "0.0.0"
if os.path.exists(vf):
    cur = json.load(open(vf))["latest"]

new = bump(cur)
data = open(SRC, "rb").read()
name = f"main_{new}.py"
os.makedirs(OUT_DIR, exist_ok=True)
shutil.copy(SRC, os.path.join(OUT_DIR, name))

info = {
    "latest": new,
    "url": f"http://{HOST}:{PORT}/firmware/{name}",
    "size": len(data),
    "sha256": hashlib.sha256(data).hexdigest(),
}
json.dump(info, open(vf, "w"), indent=2)
print("已发布版本", new, "->", name)
```

发布流程：
```bash
# 1. 改好 main.py 后，运行打包脚本
python build_ota.py
# 2. 启动静态服务器（在 www/ 目录）
python -m http.server 8000 --directory www
# 3. 板子重启或触发检查，即自动拉取新版本
```

### 12.5 进阶：MQTT 一键远程升级（结合第 11 节）
在 MQTT 回调里收到 `upgrade` 指令就立即检查升级，手机发一条消息即可远程推送：

```python
from umqtt.simple import MQTTClient
# ... 连接 MQTT（见 11.2）...

def sub_cb(topic, msg):
    if msg == b"upgrade":
        print("收到升级指令")
        check_and_update()     # 调用 12.3 的函数
    elif msg == b"on":
        led.on()
    elif msg == b"off":
        led.off()

c = MQTTClient("esp32s3_led_01", "broker.emqx.io")
c.set_callback(sub_cb)
c.connect()
c.subscribe(b"home/led/control")
while True:
    c.check_msg()
    time.sleep(0.1)
```

手机端用 MQTT Dashboard 向 `home/led/control` 发 `upgrade`，板子立刻拉取云端最新版并重启——**真正意义上的远程一键升级**。

---

### 下一步建议
- **状态可视**：把 LED 状态 + 传感器数据定时 `publish` 到云端，做手机仪表盘；
- **灰度发布**：云端 `version.json` 按设备分组下发不同版本，先小批再全量；
- **文件系统**：`open('data.txt','w')` 读写本地数据，配合 `ujson` 存配置；
- **进阶外设**：`machine.I2C` / `machine.SPI` 驱动 OLED、传感器；
- **工程化**：用 `mpremote` + 脚本实现一键烧录与 OTA 下发。

祝你玩得开心，灯亮了就是第一步的胜利 💡
