DIY 8266远程开机 (软件部分)
文章目录
硬件控制家里电脑硬件开机/复位,强制关机.
本篇主要是8266硬件的代码部分.
开发环境
是VS code +PIO
框架是8266的 arduino框架 https://github.com/esp8266/Arduino
远程控制方式
8266远程控制的方式
比较方便的就是web或者TCP/UDP
web是好处就是,它不需要客户端.
Web和8266交互
有两种
一种是POST/GET 普通的HTTP协议提交.
另外一种是WebSocket通讯.
我选择的是GET方式提交表单.
WebSocket方式8266需要除了web服务器还需要开一个WebSocket服务器.可能功耗会高一丢丢.
Web核心代码
HTML
就是3个按钮
<form method="get" id="test_form" onsubmit="return check();">
<!-- 隐藏的文本框,用来传递参数 -->
<input type ="text" id='ttt' name ="t" autocomplete="off" style="display:none" />
<p class='btn_list'><a class="button" href="javascript:up('turn_on');">开机</a></p>
<p class='btn_list'><a class="button" href="javascript:up('turn_ret');">重启</a></p>
<p class='btn_list'><a class="button" href="javascript:up('turn_pow');">强关</a></p>
</form>
JavaScript
//改写浏览器历史记录,实现伪跳转
var json={time:new Date().getTime()};
window.history.pushState(json,"","index.htm");
//点了按钮有参数传递进来
function up(sss){
//将参数传给隐藏的文本框
document.getElementById('ttt').value =sss;
//表单提交
form = document.getElementById('test_form');
form.submit();
}
//如果文本框为空就禁止提交.
function check(){
console.log(document.getElementById('ttt').value);
return (document.getElementById('ttt').value == '');
}
示例图
工作原理
form表单实际上有个隐藏的编辑框.
点按钮后将按钮的值传入编辑框,然后提交.
改写浏览器URL历史记录,属于HTML5,网上抄的代码,来处不详细了.是很早见过新浪新闻手机版有这种操作,后面在firefox贴吧问大佬才直到这个用法.
改写浏览器URL历史记录,旧版浏览器不支持.不过,自己用会自己规避.
8266核心代码
8266WIFI,串口这些就不列出了.
我其实整套代码都是官方代码拼的.
引脚部分
一个引脚用于模拟按电脑启动键,一个用于模拟按复位
声明
const int pow_pin = 13; //电源键引脚
const int rst_pin = 12; //重启键引脚
初始化
因为我继电器设置的是高电平触发.
所以引脚初始状态是低电平.
而且引脚必须接10-12K下拉电阻.
//初始化引脚
pinMode(pow_pin, OUTPUT);
pinMode(rst_pin, OUTPUT);
//给低电平
digitalWrite(pow_pin, LOW);
digitalWrite(rst_pin, LOW);
SPIFFS
8266有个SPIFFS文件分区,(8266 系列除了ESP01其他一般是4MB的FLASH )
4MB的FLASH , spiffs最大可以设置到3MB.
放网页JS/CSS还是比较OK的.
我设置的是2MB,
官方有SPIFFS工具,自己将网页上传到8266的SPIFFS分区.这部分之前我其他博文说过.pio更方便了.
在web服务器之前,就必须初始化SPIFFS分区,一句代码即可.setup部分就要执行.
初始化
#include <FS.h>
SPIFFS.begin()
取MIME函数
//根据文件后缀取文件的MIME
String getContentType(String filename)
{
if (server.hasArg("download"))
{
return "application/octet-stream";
}
else 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";
}
else if (filename.endsWith(".wasm"))
{
return "application/wasm";
}
return "text/plain";
}
SPIFFS文件输出流
这是一个通过URL路径,定位SPIFFS文件的函数.
如果是 "/" 那么转到index.htm文件
其他文件从SPIFFS中判断存在不存在.
存在就读取后输出HTTP流.
//将路径自动从spiffs获取.
//让服务器自动加载
bool handleFileRead(String path)
{
//如果路径最后是/ 加上index.htm
if (path.endsWith("/"))
{
path += "index.htm";
}
//获取文件MIME
String contentType = getContentType(path);
//先假定如果文件是被gz压缩过的
String pathWithGz = path + ".gz";
//如果SPIFFS系统中有这个文件,那么自动加载到web服务器
if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path))
{
if (SPIFFS.exists(pathWithGz))
{
path += ".gz";
}
File file = SPIFFS.open(path, "r");
//文件流输送到WEB服务器.
server.streamFile(file, contentType);
file.close();
return true;
}
return false;
}
Web服务器
头文件
分HTTP还是HTTPS服务器
我用的是HTTPS服务器,用的是EC证书.
#define USE_SSL //不需要HTTPS注释掉这行.
#ifndef USE_SSL
#include <ESP8266WebServer.h>
#else
#define USE_EC
#include <ESP8266WebServerSecure.h>
#endif
声明
http用的80端口
https用的443接口,并设置了缓存.
#ifndef USE_SSL
ESP8266WebServer server(80);
#else
BearSSL::ESP8266WebServerSecure server(443);
BearSSL::ServerSessions serverCache(5);
#endif
同步时间
仅仅https需要,
证书需要正确时间参与判断证书过期没有.
指定两个Ntp时间同步服务器即可.
#ifdef USE_SSL
configTime(3 * 3600, 0, "cn.ntp.org.cn", "edu.ntp.org.cn");
#endif
证书声明
需要证书才需要.
static const char serverCert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEUzCCA/mgAwIBAgIQHEbu4Dk58Sklwqd+gkEZETAKBggqhkjOPQQDAjBZMQsw
...省略
K5iE04v3ug==
-----END CERTIFICATE-----
)EOF";
static const char serverKey[] PROGMEM = R"EOF(
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFcw05ZFnc1ukD7SQPaKa2AWyNMqDsYP8tL5oEfg+OXDoAoGCCqGSM49
...省略
x4TMUIJaUa6xzogFxkj+xf11mmOfJqpnfA==
-----END EC PRIVATE KEY-----
)EOF";
初始化证书
#ifdef USE_SSL
//设置证书
#ifndef USE_EC
//RSA证书
server.getServer().setRSACert(new BearSSL::X509List(serverCert), new BearSSL::PrivateKey(serverKey));
#else
//EC证书,我用的是EC证书
server.getServer().setECCert(new BearSSL::X509List(serverCert), BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN, new BearSSL::PrivateKey(serverKey));
#endif
//设置缓存
server.getServer().setCache(&serverCache);
#endif
EC证书和RSA证书有一些区别.
因为EC证书对8266压力更小一些.
所以我选的EC证书.
证书在腾讯云免费申请的一年域名证书.(因为后期会捆绑我一个域名访问)
万能404
server没有注册任何URL参数,只有一个onNotFound接管404,
因为没有注册任何URL参数,那么所有的URL请求都会进入onNotFound函数.
这是先查询SPIFFS中是否存在文件,存在文件就输出流到用户浏览器.
实际就这样两个函数,就形成一个简单的静态web服务器.
而且还能读取HTML中传递的参数.
//将所有用户访问的网页文件,自动定位到SPIFFS
server.onNotFound([]() {
//如果文件不存在直接输出404,也可以重定向
if (!handleFileRead(server.uri()))
{
//输出404
//server.send(404, "text/html", "404");
//或者
//server.send(404, "text/html");
//重定向到首页
String s = "<meta HTTP-EQUIV='REFRESH' content='0; url=/'>";
server.send(200, "text/html", s);
}else{
//读取网页中的参数
String t = server.urlDecode(server.arg("t"));
Serial.println(" 接收到:"+t);
}
});
初始化Web
初始web是在执行其他web后才执行的.
//web服务器初始化
server.begin();
Loop接管
需要在loop中插入web监听代码.
//监听web服务器相关
server.handleClient();
HTTP认证
也就是加一套需要账号密码登陆,才能打开功能网页的玩意,防止自己的8266被人访问和控制.
方式有两种.
可以做一个登录页,提交一个用户名密码,判断是否成功认证.
也可以选择Basic auth认证.
我选择的是后者.因为简单,8266加几行代码即可.
Basic auth账号密码
//远程控制的账号密码
const char* www_username = "admin";
const char* www_password = "admin123456";
鉴权代码
鉴权成功后不需要处理.官方库自动处理.
成功鉴权后, 刷新也不会需要重新鉴权.
加在SPIFFS函数中.因为,我只有两个文件.一个index.htm还有一个css文件.
所以,我就直接只对index.htm鉴权(CSS忽略)
实际上这里也可以根据MIME类型HTML鉴权.
//将路径自动从spiffs获取.
//让服务器自动加载
bool handleFileRead(String path)
{
//如果路径最后是/ 加上index.htm
if (path.endsWith("/"))
{
path += "index.htm";
}
//鉴权
if (path.endsWith("/index.htm"))
{
if (!server.authenticate(www_username, www_password)) {
server.requestAuthentication();
return false;
}
}
///.....
}
交互参数处理
也就是8266接收WEB传送过来的参数.然后处理.
就是在前面的onNotFound中读取参数,然后根据参数处理.
开机和重启比较简单理解,就是电平拉高后, 0.3秒后拉低 ,
触发继电器足够了,模拟开机和重启.
强制关机需要按住电源键8秒.
delay(8000);也行不过会导致网页8秒没有相应.
所以我选择另外一种办法.
//将所有用户访问的网页文件,自动定位到SPIFFS
server.onNotFound([]() {
//如果文件不存在直接输出404,也可以重定向
if (!handleFileRead(server.uri()))
{
//重定向到首页
String s = "<meta HTTP-EQUIV='REFRESH' content='0; url=/'>";
server.send(200, "text/html", s);
}else{
//读取网页中的参数
String t = server.urlDecode(server.arg("t"));
Serial.println(" 接收到:"+t);
if(t == "turn_on"){
//index.htm?t=turn_on 点了开机按钮触发这个.
Serial.println(" 开机 ");
digitalWrite(pow_pin, HIGH);
delay(300);
digitalWrite(pow_pin, LOW);
}else if(t == "turn_ret"){
//index.htm?t=turn_ret 点了重启按钮触发这个.
Serial.println(" 重启 ");
digitalWrite(rst_pin, HIGH);
delay(300);
digitalWrite(rst_pin, LOW);
}else if(t == "turn_pow"){
//index.htm?t=turn_pow 点了强制关机按钮触发这个.
Serial.println(" 强制关机 ");
digitalWrite(pow_pin, HIGH);
//设置8秒后的时间
over_time= a_time+8000;
//标记电源按住的状态,其他的,让loop函数接管.
is_hold_pow = true;
}
}
});
取开机时间
声明
unsigned long a_time; //开机的时间
unsigned long over_time=0; //
bool is_hold_pow = false;
Loop函数中
这样 a_time是就8266开机到当前的时间.
四十多天左右溢出然后重新从零开始.
在我这种低端使用环境,可以忽略溢出带来的影响.
//去开机到当前的时间(毫秒)
a_time = millis();
强制关机计时原理
a_time + 8000毫秒就是结束时间.
非常简单.
这样Loop可以不杜塞线程.
//电源键长按指令,8秒后降低电平,强制关机用.
if(is_hold_pow){
if(over_time<a_time){
is_hold_pow = false;
digitalWrite(pow_pin, LOW);
Serial.println("pow_pin low");
}
}
强关流程就是
web服务器接收到强制关机命令后,
标记强关.(我用的是一个独立变量,也可以读取引脚电高低判断)
然后给一个按钮按下的结束时间.
其他的Loop函数接管.
Loop中根据强关标记状态进入时间判断.
如果结束时间到了就会降低电平,
然后取消强关标记,