效果
所需环境
ESP8266-NodeMCU
开发板(以下简称NodeMCU
)
ESP8266
库
Arduino IDE
代码编写工具
无线(热点)2.4G频段
实现步骤
原理
NodeMCU
作为无线终端接入局域网内的无线信号,并配合ESP8266
库提供的服务器功能使得NodeMCU
最终作为一个服务器,将html、js文件上传到该服务器(NodeMCU
的闪存文件系统),然后通过浏览器访问该服务器的文件即可
HTML、JS代码
index.html
主页
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="main.css">
<title>时钟</title>
</head>
<body>
<canvas id="canvas" width="" height=""></canvas>
<script src="digit.js"></script>
<script src="countdown.js"></script>
</body>
</html>
digit.js
数字
digit =
[
[
[0,0,1,1,1,0,0],
[0,1,1,0,1,1,0],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,0,1,1,0],
[0,0,1,1,1,0,0]
],//0
[
[0,0,0,1,1,0,0],
[0,1,1,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[1,1,1,1,1,1,1]
],//1
[
[0,1,1,1,1,1,0],
[1,1,0,0,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,1,1,0],
[0,0,0,1,1,0,0],
[0,0,1,1,0,0,0],
[0,1,1,0,0,0,0],
[1,1,0,0,0,0,0],
[1,1,0,0,0,1,1],
[1,1,1,1,1,1,1]
],//2
[
[1,1,1,1,1,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,1,1,0],
[0,0,0,1,1,0,0],
[0,0,1,1,1,0,0],
[0,0,0,0,1,1,0],
[0,0,0,0,0,1,1],
[0,0,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,1,1,0]
],//3
[
[0,0,0,0,1,1,0],
[0,0,0,1,1,1,0],
[0,0,1,1,1,1,0],
[0,1,1,0,1,1,0],
[1,1,0,0,1,1,0],
[1,1,1,1,1,1,1],
[0,0,0,0,1,1,0],
[0,0,0,0,1,1,0],
[0,0,0,0,1,1,0],
[0,0,0,1,1,1,1]
],//4
[
[1,1,1,1,1,1,1],
[1,1,0,0,0,0,0],
[1,1,0,0,0,0,0],
[1,1,1,1,1,1,0],
[0,0,0,0,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,1,1,0]
],//5
[
[0,0,0,0,1,1,0],
[0,0,1,1,0,0,0],
[0,1,1,0,0,0,0],
[1,1,0,0,0,0,0],
[1,1,0,1,1,1,0],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,1,1,0]
],//6
[
[1,1,1,1,1,1,1],
[1,1,0,0,0,1,1],
[0,0,0,0,1,1,0],
[0,0,0,0,1,1,0],
[0,0,0,1,1,0,0],
[0,0,0,1,1,0,0],
[0,0,1,1,0,0,0],
[0,0,1,1,0,0,0],
[0,0,1,1,0,0,0],
[0,0,1,1,0,0,0]
],//7
[
[0,1,1,1,1,1,0],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,1,1,0],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,1,1,0]
],//8
[
[0,1,1,1,1,1,0],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[1,1,0,0,0,1,1],
[0,1,1,1,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,0,1,1],
[0,0,0,0,1,1,0],
[0,0,0,1,1,0,0],
[0,1,1,0,0,0,0]
],//9
[
[0,0,0,0],
[0,0,0,0],
[0,1,1,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,0],
[0,1,1,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,0]
]//:
];
countdown.js
动画
var WINDOW_WIDTH = 1024;
var WINDOW_HEIGHT = 768;
var RADIUS = 8;
var MARGIN_TOP = 60;
var MARGIN_LEFT = 30;
/*const endTime = new Date(2020, 7, 22, 10, 45, 46);*/
/*var endTime = new Date();
endTime.setTime(endTime.getTime() + 3600 * 1000);*/
var curShowTimeSeconds = 0;
var balls = [];
const colors = ["#33b5e5","#0099CC","#aa66cc","#9933CC","#99CC00","#669900","#FFBB33","#FF8800","#FF4444","#CC0000"]
window.onload = function(){
/*WINDOW_WIDTH = document.body.clientWidth;
WINDOW_HEIGHT = document.body.clientHeight;*/
WINDOW_WIDTH = document.documentElement.clientWidth;
WINDOW_HEIGHT = document.documentElement.clientHeight;
RADIUS = Math.round(WINDOW_WIDTH * 4 / 5 / 108) - 1;
MARGIN_TOP = Math.round(WINDOW_HEIGHT / 5);
MARGIN_LEFT = Math.round(WINDOW_WIDTH / 10);
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
canvas.width = WINDOW_WIDTH;
canvas.height = WINDOW_HEIGHT;
curShowTimeSeconds = getCurrentShowTimeSeconds();
//render(context);
//动画函数 每1000毫秒刷新 1000/50 = 20 帧
setInterval(
function(){
render(context);
update();
}, 50);
}
function getCurrentShowTimeSeconds(){
var curTime = new Date();
var ret = curTime.getHours() * 3600 + curTime.getMinutes() * 60 + curTime.getSeconds();
return ret;
/*var curTime = new Date();
//getTime() 当前时间距1970年1月1日0时0分0秒的毫秒数
var ret = endTime.getTime() - curTime.getTime();
ret = Math.round(ret / 1000); //秒数
return ret >= 0 ? ret : 0;*/
}
/*刷新*/
function update(){
/*下一秒*/
var nextShowTimeSeconds = getCurrentShowTimeSeconds();
var nextHours = parseInt(nextShowTimeSeconds / 3600);
var nextMinutes = parseInt((nextShowTimeSeconds - nextHours*3600) / 60);
var nextSeconds = nextShowTimeSeconds % 60;
/*当前时间*/
var curHours = parseInt(curShowTimeSeconds / 3600);
var curMinutes = parseInt((curShowTimeSeconds - curHours*3600) / 60);
var curSeconds = curShowTimeSeconds % 60;
if(nextSeconds != curSeconds){
if(parseInt(curHours/10) != parseInt(nextHours/10)){
addBalls(MARGIN_LEFT + 0, MARGIN_TOP, parseInt(curHours/10));
}
if(parseInt(curHours%10) != parseInt(nextHours%10)){
addBalls(MARGIN_LEFT + 15*(RADIUS + 1),MARGIN_TOP, parseInt(curHours/10));
}
if(parseInt(curMinutes/10) != parseInt(nextMinutes/10)){
addBalls(MARGIN_LEFT + 39*(RADIUS + 1),MARGIN_TOP, parseInt(curMinutes/10));
}
if(parseInt(curMinutes%10) != parseInt(nextMinutes%10)){
addBalls(MARGIN_LEFT + 54*(RADIUS + 1), MARGIN_TOP, parseInt(curMinutes %10));
}
if(parseInt(curSeconds/10) != parseInt(nextSeconds/10)){
addBalls(MARGIN_LEFT + 78*(RADIUS + 1), MARGIN_TOP, parseInt(curSeconds/10));
}
if(parseInt(curSeconds%10) != parseInt(nextSeconds%10)){
addBalls(MARGIN_LEFT + 93*(RADIUS + 1), MARGIN_TOP, parseInt(nextSeconds%10));
}
curShowTimeSeconds = nextShowTimeSeconds;
}
updateBalls();
}
function updateBalls(){
for( var i = 0 ; i < balls.length ; i ++ ){
balls[i].x += balls[i].vx;
balls[i].y += balls[i].vy;
balls[i].vy += balls[i].g;
if( balls[i].y >= WINDOW_HEIGHT-RADIUS ){
balls[i].y = WINDOW_HEIGHT-RADIUS;
balls[i].vy = - balls[i].vy*0.75;
}
}
/*删除数组中的小球*/
var cnt = 0
for( var i = 0 ; i < balls.length ; i ++ )
if( balls[i].x + RADIUS > 0 && balls[i].x -RADIUS < WINDOW_WIDTH )
balls[cnt++] = balls[i]
while( balls.length > cnt ){
balls.pop();
}
Math.min(300, cnt)//取两个数最小值
while( balls.length > Math.min(300, cnt) ){
balls.pop();
}
}
function addBalls( x , y , num ){
for( var i = 0 ; i < digit[num].length ; i ++ )
for( var j = 0 ; j < digit[num][i].length ; j ++ )
if( digit[num][i][j] == 1 ){
var aBall = {
x:x+j*2*(RADIUS+1)+(RADIUS+1),
y:y+i*2*(RADIUS+1)+(RADIUS+1),
g:1.5+Math.random(),
vx:Math.pow( -1 , Math.ceil( Math.random()*1000 ) ) * 4,
vy:-5,
color: colors[ Math.floor( Math.random()*colors.length ) ]
}
balls.push( aBall )
}
}
/*绘制*/
function render(cxt){
//对矩形内的动画进行刷新
cxt.clearRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
var hours = parseInt(curShowTimeSeconds / 3600);
var minutes = parseInt((curShowTimeSeconds - hours*3600) / 60);
var seconds = curShowTimeSeconds % 60;
//1
renderDigit(MARGIN_LEFT,MARGIN_TOP, parseInt(hours / 10), cxt);
//2
renderDigit(MARGIN_LEFT + 15*(RADIUS + 1),MARGIN_TOP, parseInt(hours % 10), cxt);
//:
renderDigit(MARGIN_LEFT + 30*(RADIUS + 1),MARGIN_TOP, 10, cxt);
//3
renderDigit(MARGIN_LEFT + 39*(RADIUS + 1),MARGIN_TOP, parseInt(minutes / 10), cxt);
//4
renderDigit(MARGIN_LEFT + 54*(RADIUS + 1),MARGIN_TOP, parseInt(minutes % 10), cxt);
//:
renderDigit(MARGIN_LEFT + 69*(RADIUS + 1),MARGIN_TOP, 10, cxt);
//5
renderDigit(MARGIN_LEFT + 78*(RADIUS + 1),MARGIN_TOP, parseInt(seconds / 10), cxt);
//6
renderDigit(MARGIN_LEFT + 93*(RADIUS + 1),MARGIN_TOP, parseInt(seconds % 10), cxt);
for( var i = 0 ; i < balls.length ; i ++ ){
cxt.fillStyle=balls[i].color;
cxt.beginPath();
cxt.arc( balls[i].x , balls[i].y , RADIUS , 0 , 2*Math.PI , true );
cxt.closePath();
cxt.fill();
}
}
//画出小球
function renderDigit(x, y, num, cxt){
cxt.fillStyle = "rgb(0, 102, 153)";
for(var i = 0; i < digit[num].length; i ++){
for(var j = 0; j < digit[num][i].length; j++){
if(digit[num][i][j] == 1){
cxt.beginPath();
cxt.arc(x + j*2*(RADIUS+1)+(RADIUS+1), y+i*2*(RADIUS+1)+(RADIUS+1), RADIUS, 0, 2*Math.PI);
cxt.closePath();
cxt.fill();
}
}
}
}
上传文件到闪存系统
将以上三个文件放在主程序同目录下的data文件中,然后使用Arduino IDE
的插件将data
文件上传到NodeMCU
的闪存系统中
插件下载地址:https://github.com/esp8266/arduino-esp8266fs-plugin/releases
将下载后的文件解压并移动到Arduino IDE
安装目录下的tools
文件夹中,重启 Arduino IDE
注意选择闪存大小,本次上传的文件几十KB,所以选择1MB就够了。点击工具下的 ESP8266 Sketh Data Upload
,留意控制台输出看是否上传成功
上传程序
代码参考自太极创客:http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/spiffs/spiffs-web-server/
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266WebServer.h>
#include <FS.h>
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象
ESP8266WebServer esp8266_server(80); // 建立网络服务器对象,该对象用于响应HTTP请求。监听端口(80)
void setup() {
Serial.begin(9600); // 启动串口通讯
Serial.println("");
// 无线名称 密码
wifiMulti.addAP("面向对象编程_2G", "12345678");
Serial.println("Connecting ...");
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { // 尝试进行wifi连接。
delay(1000);
Serial.print(i++); Serial.print(' ');
}
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println('\n');
Serial.print("Connected to ");
Serial.println(WiFi.SSID()); // 通过串口监视器输出连接的WiFi名称
Serial.print("IP address:\t");
Serial.println(WiFi.localIP()); // 通过串口监视器输出ESP8266-NodeMCU的IP
if(SPIFFS.begin()){ // 启动闪存文件系统
Serial.println("SPIFFS Started.");
} else {
Serial.println("SPIFFS Failed to Start.");
}
esp8266_server.onNotFound(handleUserRequet); // 告知系统如何处理用户请求
esp8266_server.begin(); // 启动网站服务
Serial.println("HTTP server started");
}
void loop(void) {
esp8266_server.handleClient(); // 处理用户请求
}
// 处理用户浏览器的HTTP访问
void handleUserRequet() {
// 获取用户请求网址信息
String webAddress = esp8266_server.uri();
// 通过handleFileRead函数处处理用户访问
bool fileReadOK = handleFileRead(webAddress);
// 如果在SPIFFS无法找到用户访问的资源,则回复404 (Not Found)
if (!fileReadOK){
esp8266_server.send(404, "text/plain", "404 Not Found");
}
}
bool handleFileRead(String path) { //处理浏览器HTTP访问
if (path.endsWith("/")) { // 如果访问地址以"/"为结尾
path = "/index.html"; // 则将访问地址修改为/index.html便于SPIFFS访问
}
String contentType = getContentType(path); // 获取文件类型
if (SPIFFS.exists(path)) { // 如果访问的文件可以在SPIFFS中找到
File file = SPIFFS.open(path, "r"); // 则尝试打开该文件
esp8266_server.streamFile(file, contentType);// 并且将该文件返回给浏览器
file.close(); // 并且关闭文件
return true; // 返回true
}
return false; // 如果文件未找到,则返回false
}
// 获取文件类型
String getContentType(String filename){
if(filename.endsWith(".htm")) return "text/html";
else if(filename.endsWith(".html")) return "text/html";
else if(filename.endsWith(".css")) return "text/css";
else if(filename.endsWith(".js")) return "application/javascript";
else if(filename.endsWith(".png")) return "image/png";
else if(filename.endsWith(".gif")) return "image/gif";
else if(filename.endsWith(".jpg")) return "image/jpeg";
else if(filename.endsWith(".ico")) return "image/x-icon";
else if(filename.endsWith(".xml")) return "text/xml";
else if(filename.endsWith(".pdf")) return "application/x-pdf";
else if(filename.endsWith(".zip")) return "application/x-zip";
else if(filename.endsWith(".gz")) return "application/x-gzip";
return "text/plain";
}
上传成功后打开串口,查看无线局域网给NodeMCU
分配的IP地址,如果看不到输出需要按一下哎NodeMCU
的RST键进行复位
通过浏览器访问该IP地址即可看到效果