框体搭建
材料
1.铝型材:
2020铝型材:1660mm3、1160mm6、520mm4、1900mm4、35mm*8
2040-4孔平面铝角码*9个
2020角码 *40个(实际需要14+4+16=34个)
2020L型连接板 *4个
2020T型连接板 *8个
欧标20 M3弹珠螺母* 160个(实际需要151个)
欧标20 M5 T型螺母 *120个
M5*8 半圆头螺栓 *120个
2.螺丝:
M3107 304不锈钢带垫圆头内六角组合螺丝 100个(实际需要95个)
M314*7 304不锈钢带垫圆头内六角组合螺丝 *100个(实际需要56个)
3.定制亚克力:
如下图,将DXF格式文件发给店家制作即可

4.定制灯罩透光板(又名 pc扩散板):
长1820mm宽1220mm厚2mm
5.灯条
12V伏跑马灯WS2811灯带5050全彩编程灯条KTV灯光智能幻彩流光灯条【裸板 5米/卷价格】*9卷
安装
1.组装基本框架(底板)
1).按照图示将 2020铝型材(1660mm3、1160mm2、520mm4) 摆放,并用2020角码固定。


2).在 铝型材框架 朝上的方向,按照图示位置(图中圆圈的位置)安装 欧标20 M3弹珠螺母
3).在 铝型材框架 的四个侧边,按照图示的左视图和上视图安装 欧标20 M3弹珠螺母
4).将 亚克力面板(定制图中左1) 放置在 铝型材框架 上,并对其四边
5).使用 M310*7 304不锈钢带垫圆头内六角组合螺丝 将 定制的亚克力面板(定制图中左1) 固定在 铝型材框架 上

2.安装灯条并测试
1)将灯条剪成1100mm一段,总共获得36段灯条,其中2条备用
2)将剪好的灯条按照图示贴在 亚克力面板 上(面板上有对应槽用来定位,灯条是有方向的,灯条上有箭头,俩条为一组,每组灯条方向相同,相邻组方向相反)


3)将 亚克力长条(定制图中左5) 的一根安装在竖直方向上,观察缺口的位置,计划好如何布线(灯条较长,供电的线不宜过细,要有俩根粗电线在面板上,再将正负极接在粗电线上,信号线的连接方式为上图的白线和黄线)

4)把亚克力长条拿走,布置粗电线

5)将灯条的正负与信号线分别接好(注意 亚克力长条(定制图中左5) 装上去后预留的位置)

6)通电调试灯条,检查灯是否正常,检查烧录的程序是否正确运行在灯条上,如果发现灯异常可替换灯条
3.安装栅格与侧板
1)将 亚克力长条(定制图中左5) 安装在竖直方向上,一共16条

2)将 亚克力长条(定制图中右1) 安装在长边方向上,一共10条

3)将 亚克力厚板(定制图中左2) 安装在长边的上下侧面,将 亚克力厚板(定制图中左3,比左4多一个圆洞) 安装在短边的左侧,将 亚克力厚板(定制图中左4) 安装在短边的右侧

4)用 M3147 304不锈钢带垫圆头内六角组合螺丝 将这四个 亚克力厚板 固定在铝型材框架上

5)将 电源线与信号线 通过 亚克力厚板(定制图中左3) 的洞穿出
4.安装第二层框架
1)如下图,将2个 2020铝型材1900mm 用 2020角码 固定在上下最外侧,然后将2个 2020铝型材1160mm 用 2020L型连接板 固定在左右侧,将8个 2020铝型材35mm 立在刚装的 2020铝型材1900mm 上用 2020角码固定(位置平均分配在最外侧铝型材上,长边3个,短边1个)


5.安装灯罩透光板
1)将2个 2020铝型材1900mm 和2个 2020铝型材1160mm 用 2020角码 组装成长1900宽1200的方框
2)用 铅笔 在 长1820mm宽1220mm厚2mm的灯罩透光板 的边缘画距离边缘10mm的一圈标记线,再在四个角上画距离边缘20mm的交叉十字,十字中心打5mm直径的过孔
3)将 长1820mm宽1220mm厚2mm的灯罩透光板 用 布基胶 和 欧标20 M5 T型螺母、M5*8 半圆头螺栓 固定在刚装的铝型材方框上
4)将装好的 带铝型材框架的灯罩板 放置在 底板 上,对齐四周,用8个 2020T型连接板 将其与 底板 上的 2020铝型材35mm 连接,即可完成所有组装。
电路设计
设计思路
1.使用WS2812作为显示灯珠
2.使用两块ESP32开发板,一块用于灯带控制,一块用于搭建WIFI服务器
3.12V电源输入供电,使用buck电路将12V降压到5V给ESP32开发板供电
原理图

电路图

3D实物图

元件清单
位号 | 封装 | 数量 | 参数 |
C1 | 603 | 1 | 1uF |
C2 | 603 | 1 | 10uF |
C3 | 805 | 1 | 22uF |
D1 | LED_0603 | 1 | LED |
D2 | D_SOD-123 | 1 | SMF12A |
D3 | D_SMA | 1 | SS26A |
J1, J2 | BarrelJack_Wuerth_6941xx301002 | 2 | DC-005-5A-2.0 |
J3, J4, J5, J6 | PinSocket_1x15_P2.54mm_Vertical | 4 | Conn_01x15 |
J7 | PinHeader_1x03_P2.54mm_Vertical | 1 | Conn_01x03 |
L1 | L_Bourns-SRN4018 | 1 | SWPA4030S150MT |
R2 | 603 | 1 | 10K |
R3 | 402 | 1 | 1M |
R4 | 402 | 1 | 56K |
R5 | 402 | 1 | 10K |
SW1 | SW_SPDT_XKB_SS12D10 | 1 | SS12D10G5 |
U1 | SOT-23-6 | 1 | ME3116 |
| | 2 | ESP32开发板 |
软件编写
设计思路
1.使用一块ESP32用于灯带控制和贪吃蛇逻辑搭建
2.使用一块ESP32用于搭建WIFI服务器和接收控制信号
3.ESP32之间使用串口通讯,实现手机端控制贪吃蛇运动
灯带控制端代码
1.Arduino需要安装库FastLED.h
#include <FastLED.h>
#include <HardwareSerial.h>
#include <stdint.h>
HardwareSerial SerialPort0(0); // 调用 UART0
HardwareSerial SerialPort2(2); // UART2
// RX0--1 TX0--3
// RX1--4 TX1--2
// RX2--17 TX2--16
#define WIDTH 17
#define HEIGHT 11
#define NUM_LEDS (WIDTH * HEIGHT) * 2 // 灯带数量
CRGB leds[NUM_LEDS]; // 灯带组
#define SERIAL_NUM 100 // 串口消息数量
#define DATA_PIN 13 // 灯带引脚
int i, j, gameOver;
int x, y, fruitX, fruitY, score; // x,y:蛇头位置 fruitX,fruitY:食物位置 score:分数
int tailX[WIDTH * HEIGHT], tailY[WIDTH * HEIGHT]; // 蛇的位置数组
int nTail, addTail; // nTail:蛇的长度 addTail:蛇增加标志位
enum eDirecton
{
STOP = 0,
LEFT,
RIGHT,
UP,
DOWN
};
enum eDirecton dir;
char uart_fetch()
{
int rxnum = 0; // 数组下标
unsigned char rxd[SERIAL_NUM]; // 串口接收数组
if (SerialPort2.available() > 0) // 串口接收触发
{
int k = 0; // 超时标志
while (1) // 假的死循环
{
int n = SerialPort2.read(); // 从串口缓冲区读一个字节
if (n >= 0 && rxnum < SERIAL_NUM) // 如果读到数据,则返回数据,并且防止数据溢出
{
rxd[rxnum++] = n; // 存储到数组里
k = 0; // 清除超时标志
}
else // 没读到数据,返回的是int型的-1,即符号位表负数
{
k++;
if (k > 1000)
break; // 长时间没读到说明数据传输完了,退出
}
}
for (int i = 0; i < rxnum; i++)
{
// SerialPort0.print(rxd[i]); // 显示接收的数据
// SerialPort0.print(" "); // 显示接收的数据
if (rxd[0] == 85 && rxd[1] == 80)
return 1;
if (rxd[0] == 68 && rxd[1] == 79 && rxd[2] == 87 && rxd[3] == 78)
return 2;
if (rxd[0] == 76 && rxd[1] == 69 && rxd[2] == 70 && rxd[3] == 84)
return 3;
if (rxd[0] == 82 && rxd[1] == 73 && rxd[2] == 71 && rxd[3] == 72 && rxd[4] == 84)
return 4;
}
// SerialPort0.println(""); // 显示接收的数据
if (rxnum >= SERIAL_NUM)
{
delay(1000); // 如果数据溢出,则等待数据发完
while (SerialPort2.read() >= 0)
; // 清空串口缓冲区
}
rxnum = 0;
}
return 0;
}
int two_to_one(int x_2d, int y_2d)
{
int location = 0;
if (x_2d % 2 == 0)
{
y_2d = abs(y_2d - 10);
location = x_2d * HEIGHT * 2 + y_2d * 2;
}
if (x_2d % 2 == 1)
{
location = x_2d * HEIGHT * 2 + y_2d * 2;
}
// SerialPort0.printf("x_2d %d y_2d %d location %d\n", x_2d, y_2d, location);
return location;
}
void game_setup()
{
gameOver = 0;
dir = STOP;
x = WIDTH / 2;
y = HEIGHT / 2;
fruitX = rand() % WIDTH;
fruitY = rand() % HEIGHT;
fruitX = 0;
fruitY = 6;
score = 0;
for (i = 0; i < WIDTH * HEIGHT; i++)
{
tailX[i] = 0;
tailY[i] = 0;
}
nTail = 0;
}
void game_draw()
{
int location;
for (i = 0; i < NUM_LEDS; i++)
{
leds[i] = CHSV(0, 0, 0);
}
for (i = 0; i < WIDTH + 2; i++) // 边界画框
SerialPort0.printf("#");
SerialPort0.printf("\n");
for (i = 0; i < HEIGHT; i++) // i为高,j为宽
{
for (j = 0; j < WIDTH; j++)
{
if (j == 0) // 边界画框
{
SerialPort0.printf("#");
}
if (i == y && j == x) // 蛇头
{
location = two_to_one(j, i);
leds[location] = CHSV(122, 255, 255);
leds[location + 1] = CHSV(122, 255, 255);
SerialPort0.printf("O");
}
else if (i == fruitY && j == fruitX) // 食物
{
location = two_to_one(j, i);
leds[location] = CHSV(50, 255, 255);
leds[location + 1] = CHSV(50, 255, 255);
SerialPort0.printf("F");
}
else // 蛇身
{
int print = 0;
for (int k = 0; k < nTail; k++)
{
if (tailX[k] == j && tailY[k] == i)
{
location = two_to_one(j, i);
leds[location] = CHSV(170, 255, 255);
leds[location + 1] = CHSV(170, 255, 255);
SerialPort0.printf("o");
print = 1;
}
}
if (!print)
{
SerialPort0.printf(" ");
}
}
if (j == WIDTH - 1)
{
SerialPort0.printf("#");
}
}
SerialPort0.printf("\n");
}
for (i = 0; i < WIDTH + 2; i++)
SerialPort0.printf("#");
SerialPort0.printf("\n");
FastLED.show();
}
void game_input(char getch)
{
switch (getch)
{
case 1:
if (dir == DOWN)
break;
dir = UP;
break;
case 2:
if (dir == UP)
break;
dir = DOWN;
break;
case 3:
if (dir == RIGHT)
break;
dir = LEFT;
break;
case 4:
if (dir == LEFT)
break;
dir = RIGHT;
break;
case 5:
gameOver = 1;
break;
default:
break;
}
}
void game_logic()
{
int prevX = tailX[0];
int prevY = tailY[0];
int prev2X, prev2Y;
tailX[0] = x;
tailY[0] = y;
if (addTail == 1)
{
nTail++;
addTail = 0;
}
for (i = 1; i < nTail; i++)
{
prev2X = tailX[i];
prev2Y = tailY[i];
tailX[i] = prevX;
tailY[i] = prevY;
prevX = prev2X;
prevY = prev2Y;
}
switch (dir)
{
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
case UP:
y--;
break;
case DOWN:
y++;
break;
default:
break;
}
if (x >= WIDTH)
x = 0;
else if (x < 0)
x = WIDTH - 1;
if (y >= HEIGHT)
y = 0;
else if (y < 0)
y = HEIGHT - 1;
for (i = 0; i < nTail; i++)
if (tailX[i] == x && tailY[i] == y)
gameOver = 1;
int a = 1; // 用来判断食物生成到蛇身的计数
if (x == fruitX && y == fruitY)
{
score += 10;
while (a)
{
a = 0;
fruitX = rand() % WIDTH;
fruitY = rand() % HEIGHT;
for (i = 0; i < nTail; i++)
{
if (tailX[i] == fruitX && tailY[i] == fruitY)
{
a = 1;
}
}
}
addTail = 1;
}
}
void setup()
{
SerialPort0.begin(115200, SERIAL_8N1); // UART0,通常用于调试输出
SerialPort2.begin(115200, SERIAL_8N1); // UART2
FastLED.addLeds<WS2811, DATA_PIN, RGB>(leds, NUM_LEDS); // GRB ordering is typical
game_setup();
}
int srand_set = 0;
void loop()
{
char a = 0, b = 0, c = 0;
uint64_t time = 0;
game_setup();
while (!gameOver)
{
a = uart_fetch();
if (a != b && c == 0)
{
c = 1;
game_input(a);
if (srand_set == 0)
{
srand(time);
srand_set = 1;
}
}
if (time % 200 == 0) // 控制游戏速度
{
c = 0;
game_logic();
game_draw();
}
time++;
delay(1);
}
a = 0;
b = 0;
while (SerialPort2.available() > 0)
{ // 检查是否有数据在缓冲区中
SerialPort2.read(); // 读取并丢弃数据
}
while (gameOver)
{
a = uart_fetch();
if (a != b)
{
gameOver = 0;
}
}
}
灯带服务端代码
1.Arduino需要安装库ESPAsyncWebServer.h
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include <HardwareSerial.h>
HardwareSerial SerialPort0(0); // 调用 UART0
HardwareSerial SerialPort2(2); // 调用 UART2
// RX0--1 TX0--3
// RX1--4 TX1--2
// RX2--17 TX2--16
// WiFi 设置
const char *ssid = "SEU_Snakes"; // WiFi 账号
const char *password = "SEU_Snakes"; // WiFi 密码
// 创建异步 Web 服务器对象,端口为 80
AsyncWebServer server(80);
// 嵌入式HTML内容
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>NanoE Car Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: Helvetica;
display: inline-block;
margin: 0px auto;
text-align: center;
}
h1 {
color: #0F3376;
padding: 2vh;
}
p {
font-size: 1.5rem;
}
.button {
display: inline-block;
background-color: #008CBA;
border: none;
border-radius: 4px;
color: white;
padding: 16px 40px;
text-decoration: none;
font-size: 30px;
margin: 2px;
cursor: pointer;
}
.button-container {
display: flex;
flex-direction: column;
align-items: center;
}
.horizontal-buttons {
display: flex;
justify-content: center;
margin: 10px 0;
}
.horizontal-buttons .button {
margin: 0 40px; /* 增加左右按钮之间的间距 */
}
</style>
</head>
<body>
<h1>贪吃蛇游戏</h1>
<h1></h1>
<h1></h1>
<h1></h1>
<h1></h1>
<div class="button-container">
<button class="button" onclick="sendCommand('UP')">上</button>
<div class="horizontal-buttons">
<button class="button" onclick="sendCommand('LEFT')">左</button>
<button class="button" onclick="sendCommand('RIGHT')">右</button>
</div>
<button class="button" onclick="sendCommand('DOWN')">下</button>
</div>
<script>
function sendCommand(command) {
// 发送 HTTP GET 请求到 ESP32
fetch('/' + command)
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>
)rawliteral";
void setup()
{
// 初始化串口,用于调试
SerialPort0.begin(115200, SERIAL_8N1);
SerialPort2.begin(115200, SERIAL_8N1);
// 创建 WiFi 热点
WiFi.softAP(ssid, password);
SerialPort0.println("热点已启动,IP地址为:" + WiFi.softAPIP().toString());
// 设置根目录路由,返回嵌入式HTML内容
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send_P(200, "text/html", index_html); });
// 设置控制指令路由
server.on("/UP", HTTP_GET, [](AsyncWebServerRequest *request)
{
SerialPort0.println("UP");
SerialPort2.println("UP");
request->send(200, "text/plain", "UP"); });
server.on("/DOWN", HTTP_GET, [](AsyncWebServerRequest *request)
{
SerialPort0.println("DOWN");
SerialPort2.println("DOWN");
request->send(200, "text/plain", "DOWN"); });
server.on("/LEFT", HTTP_GET, [](AsyncWebServerRequest *request)
{
SerialPort0.println("LEFT");
SerialPort2.println("LEFT");
request->send(200, "text/plain", "LEFT"); });
server.on("/RIGHT", HTTP_GET, [](AsyncWebServerRequest *request)
{
SerialPort0.println("RIGHT");
SerialPort2.println("RIGHT");
request->send(200, "text/plain", "RIGHT"); });
// 启动服务器
server.begin();
}
void loop()
{
// 空循环,异步服务器会自动处理请求
}