所使用的开发板:
Web端效果
代码如下
import network
import usocket as socket
import uasyncio as asyncio
import ujson
import machine
import random
import gc
import sys
import struct
# 内存监控函数
def mem_info():
gc.collect()
print("Free mem:", gc.mem_free())
# 初始化时释放内存
mem_info()
# ================ AP热点设置 ================
def setup_ap():
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(essid='SnakeGame', password='12345678', authmode=3) # WPA2加密
ap.config(max_clients=5)
print('AP模式已启动')
print('SSID: SnakeGame, 密码: 12345678')
ip = ap.ifconfig()[0]
print('IP地址:', ip)
return ip
# ================ DNS服务器(用于劫持所有域名) ================
class DNSServer:
def __init__(self, ip):
self.ip = ip
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setblocking(False)
self.sock.bind(('0.0.0.0', 53))
print("DNS服务器已启动 (端口 53)")
async def run(self):
while True:
try:
data, addr = self.sock.recvfrom(512)
if data:
# 解析DNS查询
transaction_id = data[0:2]
flags = b'\x81\x80' # 标准响应标志
questions = 1
answers = 1
authority_rrs = 0
additional_rrs = 0
# 构建响应头
response = transaction_id + flags
response += struct.pack('>HHHH', questions, answers, authority_rrs, additional_rrs)
# 添加查询部分
response += data[12:]
# 添加答案部分
response += b'\xc0\x0c' # 指向查询名的指针
response += b'\x00\x01' # 类型A
response += b'\x00\x01' # 类IN
response += b'\x00\x00\x00\x3c' # TTL 60秒
response += b'\x00\x04' # 数据长度
response += socket.inet_aton(self.ip) # 我们的IP地址
self.sock.sendto(response, addr)
print(f"DNS劫持请求来自: {addr[0]}")
except Exception as e:
# print("DNS错误:", e)
pass
await asyncio.sleep_ms(100)
# ================ 游戏状态 ================
class GameState:
def __init__(self):
self.reset()
self.clients = []
self.high_score = 0
self.base_speed = 300 # 基础速度(ms)
self.speed_factor = 1.0 # 速度因子
def reset(self):
self.snake = [(5, 5), (4, 5), (3, 5)] # 蛇初始位置
self.direction = 'right'
self.food = self.generate_food()
self.score = 0
self.game_over = False
def generate_food(self):
# 15x15网格
while True:
food = (random.randint(0, 14), random.randint(0, 14))
if food not in self.snake:
return food
def move(self):
if self.game_over:
return
head_x, head_y = self.snake[0]
if self.direction == 'up':
new_head = (head_x, head_y - 1)
elif self.direction == 'down':
new_head = (head_x, head_y + 1)
elif self.direction == 'left':
new_head = (head_x - 1, head_y)
elif self.direction == 'right':
new_head = (head_x + 1, head_y)
# 检查碰撞
if (new_head[0] < 0 or new_head[0] > 14 or
new_head[1] < 0 or new_head[1] > 14 or
new_head in self.snake):
self.game_over = True
if self.score > self.high_score:
self.high_score = self.score
return
# 移动蛇
self.snake.insert(0, new_head)
# 检查食物
if new_head == self.food:
self.score += 1
self.food = self.generate_food()
else:
self.snake.pop()
def get_speed(self):
"""计算当前游戏速度,考虑基础速度和速度因子"""
return max(100, int(self.base_speed / self.speed_factor))
# ================ Web服务器 ================
class WebServer:
def __init__(self, game_state):
self.game_state = game_state
self.server = None
self.html = self.get_html()
def get_html(self):
# 包含速度滑杆的HTML界面
return """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Snake</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.game-container {
text-align: center;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
canvas {
border: 1px solid #000;
background-color: #000;
}
.game-over {
display: none;
color: red;
font-size: 24px;
margin: 10px 0;
}
.settings {
margin: 15px 0;
}
.speed-control {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="game-container">
<h1>ESP32 Snake</h1>
<div class="score">得分: <span id="score">0</span> | 最高分: <span id="high-score">0</span></div>
<canvas id="gameCanvas" width="300" height="300"></canvas>
<div id="gameOver" class="game-over">游戏结束!</div>
<div class="settings">
<h3>游戏设置</h3>
<div class="speed-control">
<div class="speed-label">游戏速度:</div>
<input type="range" min="1" max="3" value="1" step="0.5" class="speed-slider" id="speedSlider">
<div class="speed-value" id="speedValue">1.0x</div>
</div>
<div class="instructions">(速度设置会在新游戏生效)</div>
</div>
<button id="restartBtn">开始新游戏</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const highScoreElement = document.getElementById('high-score');
const gameOverElement = document.getElementById('gameOver');
const restartBtn = document.getElementById('restartBtn');
const speedSlider = document.getElementById('speedSlider');
const speedValue = document.getElementById('speedValue');
let ws;
let gridSize = 15;
let cellSize = canvas.width / gridSize;
function connectWebSocket() {
ws = new WebSocket('ws://' + window.location.host + '/ws');
ws.onopen = () => {
console.log('WebSocket连接已建立');
};
ws.onmessage = (event) => {
const gameState = JSON.parse(event.data);
drawGame(gameState);
};
ws.onclose = () => {
console.log('WebSocket连接已关闭,尝试重新连接...');
setTimeout(connectWebSocket, 2000);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
}
function drawGame(gameState) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制蛇
ctx.fillStyle = '#00FF00';
gameState.snake.forEach(segment => {
ctx.fillRect(segment[0] * cellSize, segment[1] * cellSize, cellSize, cellSize);
});
// 绘制食物
ctx.fillStyle = '#FF0000';
ctx.fillRect(gameState.food[0] * cellSize, gameState.food[1] * cellSize, cellSize, cellSize);
// 更新分数
scoreElement.textContent = gameState.score;
highScoreElement.textContent = gameState.high_score;
// 显示游戏结束
gameOverElement.style.display = gameState.game_over ? 'block' : 'none';
}
// 键盘控制
window.addEventListener('keydown', (e) => {
if (!ws) return;
let direction;
switch(e.key) {
case 'ArrowUp': direction = 'up'; break;
case 'ArrowDown': direction = 'down'; break;
case 'ArrowLeft': direction = 'left'; break;
case 'ArrowRight': direction = 'right'; break;
default: return;
}
ws.send(JSON.stringify({ action: 'direction', data: direction }));
});
// 重新开始游戏
restartBtn.addEventListener('click', () => {
if (ws) {
ws.send(JSON.stringify({ action: 'restart' }));
}
});
// 速度设置
speedSlider.addEventListener('input', () => {
const value = parseFloat(speedSlider.value).toFixed(1);
speedValue.textContent = value + 'x';
if (ws) {
ws.send(JSON.stringify({ action: 'set_speed', data: value }));
}
});
// 初始连接
connectWebSocket();
</script>
</body>
</html>"""
async def handle_client(self, reader, writer):
try:
request = await reader.read(512)
request = request.decode('utf-8')
if not request:
return
# 解析请求的第一行
first_line = request.split('\r\n')[0]
parts = first_line.split()
if len(parts) < 2:
return
method = parts[0]
path = parts[1]
# 检查WebSocket升级请求
if 'Upgrade: websocket' in request and path == '/ws':
# 解析Sec-WebSocket-Key
key_line = [line for line in request.split('\r\n')
if line.startswith('Sec-WebSocket-Key:')][0]
key = key_line.split(': ')[1].strip()
# 计算响应key
import ubinascii, uhashlib
accept_key = ubinascii.b2a_base64(
uhashlib.sha1(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest()
).strip().decode()
# 发送WebSocket握手响应
response = (
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: " + accept_key + "\r\n\r\n"
)
await writer.awrite(response.encode())
# 将客户端添加到列表
self.game_state.clients.append(writer)
print("新的WebSocket客户端连接")
# 处理WebSocket消息
while True:
try:
data = await reader.read(512)
if not data:
break
# 解析WebSocket帧 (简化版)
opcode = data[0] & 0x0F
payload_len = data[1] & 0x7F
mask = data[2:6]
payload = data[6:6+payload_len]
# 解掩码
decoded = bytearray()
for i in range(payload_len):
decoded.append(payload[i] ^ mask[i % 4])
# 处理关闭帧
if opcode == 0x8:
break
# 处理文本帧
if opcode == 0x1:
message = ujson.loads(decoded.decode())
if message['action'] == 'direction':
self.game_state.direction = message['data']
elif message['action'] == 'restart':
self.game_state.reset()
elif message['action'] == 'set_speed':
self.game_state.speed_factor = float(message['data'])
except Exception as e:
print("WebSocket错误:", e)
break
# 客户端断开连接
if writer in self.game_state.clients:
self.game_state.clients.remove(writer)
print("WebSocket客户端断开")
else:
# 处理普通HTTP请求 - 重定向到游戏页面
if path != '/':
# 如果是其他路径,重定向到首页
headers = "HTTP/1.1 302 Found\r\n"
headers += "Location: http://192.168.4.1/\r\n"
headers += "Connection: close\r\n\r\n"
await writer.awrite(headers.encode())
else:
# 发送游戏页面
headers = "HTTP/1.1 200 OK\r\n"
headers += "Content-Type: text/html\r\n"
headers += "Connection: close\r\n\r\n"
await writer.awrite(headers.encode() + self.html.encode())
await writer.drain()
except Exception as e:
print("处理客户端错误:", e)
finally:
writer.close()
gc.collect()
async def send_game_state(self, writer):
try:
# 准备游戏状态数据
game_data = {
'snake': self.game_state.snake,
'food': self.game_state.food,
'score': self.game_state.score,
'high_score': self.game_state.high_score,
'game_over': self.game_state.game_over
}
json_data = ujson.dumps(game_data)
# 构建WebSocket帧
frame = bytearray()
frame.append(0x81) # FIN + 文本帧
if len(json_data) < 126:
frame.append(len(json_data))
else:
frame.append(126)
frame.extend(struct.pack('>H', len(json_data)))
frame.extend(json_data.encode())
# 发送数据
await writer.awrite(frame)
except Exception as e:
print("发送游戏状态错误:", e)
# 发生错误时移除客户端
if writer in self.game_state.clients:
self.game_state.clients.remove(writer)
async def broadcast_game_state(self):
if not self.game_state.clients:
return
# 准备游戏状态数据
game_data = {
'snake': self.game_state.snake,
'food': self.game_state.food,
'score': self.game_state.score,
'high_score': self.game_state.high_score,
'game_over': self.game_state.game_over
}
json_data = ujson.dumps(game_data)
# 构建WebSocket帧
frame = bytearray()
frame.append(0x81) # FIN + 文本帧
if len(json_data) < 126:
frame.append(len(json_data))
else:
frame.append(126)
frame.extend(struct.pack('>H', len(json_data)))
frame.extend(json_data.encode())
# 向所有客户端广播
for writer in self.game_state.clients[:]: # 使用副本防止修改列表
try:
await writer.awrite(frame)
except Exception as e:
print("广播错误:", e)
if writer in self.game_state.clients:
self.game_state.clients.remove(writer)
async def run(self):
# 修复:MicroPython的uasyncio没有serve_forever方法
self.server = await asyncio.start_server(self.handle_client, "0.0.0.0", 80)
print("Web服务器已启动 (端口 80)")
# 创建一个任务来保持服务器运行
while True:
await asyncio.sleep(1)
# ================ 主循环 ================
async def main():
try:
# 设置AP
ip = setup_ap()
# 启动DNS服务器
dns_server = DNSServer(ip)
dns_task = asyncio.create_task(dns_server.run())
# 初始化游戏状态
game_state = GameState()
# 启动Web服务器
web_server = WebServer(game_state)
server_task = asyncio.create_task(web_server.run())
# 游戏主循环
while True:
# 更新游戏状态
game_state.move()
# 广播游戏状态
if game_state.clients:
await web_server.broadcast_game_state()
# 控制游戏速度
await asyncio.sleep_ms(game_state.get_speed())
# 定期内存回收
if game_state.get_speed() % 1000 == 0:
mem_info()
gc.collect()
except Exception as e:
sys.print_exception(e)
print("主循环错误, 重启...")
# 启动程序
try:
# 分配紧急异常缓冲区
import micropython
micropython.alloc_emergency_exception_buf(100)
asyncio.run(main())
except Exception as e:
sys.print_exception(e)
print("致命错误, 重启设备...")