M5Paper 搭载 ESP32 主控与电子墨水屏,凭借低功耗、断电不刷新的特点,非常适合静态图片、文字信息常驻显示。本次基于 UiFlow2 ,采用 MicroPython 开发,搭建简易网页上传服务,实现电脑/手机网页端上传图片,投屏到 M5Paper 墨水屏。
一、硬件介绍
使用的硬件为M5Paper ESP32 E-Ink Development Kit V1.1,是M5Stack推出的触控墨水屏开发板。主控芯片是 ESP32,搭载一块4.7英寸的触摸电子墨水屏,分辨率960×540。
相关参数:
SOC:ESP32-D0WDQ6-V3@双核处理器,主频 240MHz
墨水屏:型号:ED047TC1,540 x 960@4.7",灰度 : 16 级
按键:拨轮开关 *1 ,复位按键 *1
RTC:BM8563
温湿度传感器:SHT30
二、开发环境
使用M5Stack官方开发平台——UiFlow2,支持图形化积木拖拽编程与 MicroPython 编程,内置 M5Paper 屏幕、外设等底层驱动,无需复杂底层适配,上手门槛低。

官方提供了M5Burner固件烧录工具, 通过M5Burner可以很方便的烧录 UiFlow 固件,并在烧录时一同写入 Wi-Fi 等配置信息。

三、功能实现
UiFlow2 已完整封装墨水屏显示、网络服务等底层驱动,只需熟悉相关 API 参数即可快速开发。
此前做过 ESP32 摄像头图传项目,实现的是将摄像头画面上传至网页显示;本次要实现的功能刚好相反:从网页上传图片,由 ESP32 接收并显示在墨水屏上。
关键代码如下:
① 内嵌网页服务
ESP32 搭建本地 Web 服务,以设备 IP 为访问地址,内置图片上传页面,支持局域网内手机、电脑浏览器访问选图上传。
def html_page(status="就绪"):
return '''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M5Paper传图工具</title>
<style>
body { font-family: sans-serif; padding: 20px; max-width: 500px; margin: 0 auto; }
input[type=file] { width: 100%; font-size: 16px; margin: 10px 0; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
input[type=submit] { width: 100%; height: 50px; font-size: 18px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
input[type=submit]:hover { background: #0056b3; }
p { color: #333; background: #f5f5f5; padding: 10px; border-radius: 4px; word-break: break-all; }
</style>
</head>
<body>
<h2>M5Paper传图工具</h2>
<form method="POST" action="/" enctype="multipart/form-data">
<input type="file" name="file" accept="image/*">
<input type="submit" value="展示到M5Paper">
</form>
<p>状态: ''' + status + '''</p>
</body>
</html>'''② HTTP 图片接收
采用流式分片接收网页上传的图片数据,避免一次性加载大图片导致 ESP32 内存溢出、程序崩溃。
def handle_client(client):
global current_status
try:
client.settimeout(30.0)
# ---------- 读取 HTTP 头 ----------
header_data = b''
while b'\r\n\r\n' not in header_data:
chunk = client.recv(1024)
if not chunk:
break
header_data += chunk
if b'\r\n\r\n' not in header_data:
send_response(client, html_page("请求错误"))
client.close()
return
header_end = header_data.find(b'\r\n\r\n')
headers = header_data[:header_end].decode('utf-8', 'ignore')
# 解析 Content-Length 和 Boundary
content_length = 0
boundary = None
for line in headers.split('\r\n'):
if line.lower().startswith('content-length:'):
content_length = int(line.split(':')[1].strip())
if line.lower().startswith('content-type:'):
if 'boundary=' in line:
boundary = '--' + line.split('boundary=')[1].strip()
# GET 请求:只返回网页,不刷新屏幕
if not headers.startswith('POST'):
send_response(client, html_page(current_status))
client.close()
return
# ---------- 流式接收写入临时文件 ----------
body_remaining = header_data[header_end + 4:]
temp_path = '/flash/upload_tmp.png'
with open(temp_path, 'wb') as f:
if body_remaining:
f.write(body_remaining)
received = len(body_remaining)
else:
received = 0
while received < content_length:
to_read = min(2048, content_length - received)
chunk = client.recv(to_read)
if not chunk:
break
f.write(chunk)
received += len(chunk)
print('共接收:', received, '字节')
# ---------- 从临时文件提取纯图片数据 ----------
if boundary:
with open(temp_path, 'rb') as f:
file_data = f.read()
b_enc = boundary.encode()
pos1 = file_data.find(b_enc)
if pos1 != -1:
hdr_end = file_data.find(b'\r\n\r\n', pos1)
if hdr_end != -1:
file_start = hdr_end + 4
pos2 = file_data.find(b_enc, file_start)
if pos2 != -1:
img_data = file_data[file_start:pos2].rstrip(b'\r\n')
with open('/flash/upload.png', 'wb') as f:
f.write(img_data)
print('图片已保存:', len(img_data), '字节')
current_status = "上传成功, " + str(len(img_data)) + " 字节"
# 只有成功才刷新屏幕显示图片
show_image()
else:
current_status = "数据格式错误"
else:
current_status = "文件头缺失"
else:
current_status = "分隔符缺失"
else:
current_status = "无分隔符信息"
try:
os.remove(temp_path)
except:
pass
send_response(client, html_page(current_status))
except Exception as e:
print('客户端错误:', e)
current_status = "错误: " + str(e)
try:
send_response(client, html_page(current_status))
except:
pass
finally:
client.close()③ 墨水屏图片显示
对接收完成的图片数据进行解析,将图片展示在 M5Paper 电子墨水屏上。
def show_image():
global current_status
try:
Widgets.fillScreen(0xFFFFFF)
img = Widgets.Image("/flash/upload.png", 0, 0)
img.setVisible(True)
M5.update()
current_status = "图片已显示"
print('图片已显示')
except Exception as e:
print('显示错误:', e)
current_status = "显示失败"④ 设备初始化配置
开机自动连接 WiFi,启动 Web 服务,屏幕同步显示设备 IP,方便访问传图页面。
def setup():
global server_socket, label_ip
M5.begin()
Widgets.fillScreen(0xFFFFFF)
# 初始化画面(此时不刷新,等最后统一刷)
Widgets.Label("M5Paper 传图工具", 120, 40, 1.0, 0x000000, 0xFFFFFF, Widgets.FONTS.AlibabaPuHuiTiCN24)
label_ip = Widgets.Label("正在连接WiFi...", 100, 120, 1.0, 0x000000, 0xFFFFFF, Widgets.FONTS.AlibabaPuHuiTiCN24)
ip = connect_wifi()
if ip:
label_ip.setText("IP地址: " + ip)
Widgets.Label("等待上传图片...", 100, 180, 1.0, 0x000000, 0xFFFFFF, Widgets.FONTS.AlibabaPuHuiTiCN24)
else:
label_ip.setText("WiFi连接失败")
M5.update() # 失败时刷一次,让用户看到
return
# 初始化完成,统一刷新一次
M5.update()
try:
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(addr)
server_socket.listen(3)
server_socket.setblocking(False)
print('HTTP服务器已启动, 端口80')
except Exception as e:
print('服务器错误:', e)
label_ip.setText("服务器错误")
M5.update()四、效果展示
使用局域网内手机、电脑浏览器访问 M5Paper 的 IP,选择本地图片上传,M5Paper 接收并显示在墨水屏上,无需数据线,局域网内随时随地一键传图。

我要赚赏金
