硬件控制家里电脑硬件开机/复位,强制关机.

本篇主要是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中根据强关标记状态进入时间判断.

如果结束时间到了就会降低电平,

然后取消强关标记,