在前文中,我们已经搭建好一个WEB服务器,并可以通过网页来配置WIFI的登录信息:
https://blog.csdn.net/m0_50114967/article/details/127009686
在接下来的文章里,将开始介绍ESP32的远程交互功能。ESP32可以使用的远程交互方式比较多,如蓝牙,TCP协议,UDP协议,MQTT协议,HTTP协议(WEB页面方式)。
各种协议的优劣
蓝牙:功耗低,多数设备都具有蓝牙功能,但传输速率上,距离有限。
TCP协议:可靠,稳定,是比较成熟的一种协议,但速度较慢,效率低,占用系统资源高
UDP协议:比较TCP协议速度较快,但可靠性较差,在网络质量不好时容易丢包
HTTP协议(WEB页面方式):兼容性较好,只需有浏览器的设备就可以连接。但硬件配置决定了用ESP32作的WEB服务器更合适做为一个配置工具(比如用来设置WIFI登录信息)。
MQTT协议:通信开销低,可选客户端比较多,但不支持点对点通信,也不支持离线消息。
首先,蓝牙,TCP和UDP并不适合用来作为互动的协议,这三种协议劣势过于明显,兼容性也比较差,常用的除了手机和电脑,能使用蓝牙做为客户端的设备并没有太多的选择,而对于大部份设备来说,直接使用TCP或UDP协议是比较麻烦的。
所以,选择比较有优势的HTTP协议(WEB页面方式)和MQTT协议是更正确的选择。
HTTP协议(WEB页面方式)
WEB页面的形式在两个客户端在同一个本地网络的情况下十分方便,但想要实现远程访问还是比较麻烦的。
MQTT:
MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议。他的工作流程是 [客户端 <=> 服务器 <=> 客户端] 的形式。所以,不同的客户端要互通,必须通过服务器。
下面,我们在前文中所搭建的WEB服务品的基础上,增加在WEB页面实现对ESP32引脚(GPIO)的控制。
WEB页面发送的请求主要是两种GET请求和POST请求,在之前通过页面对WIFI登录信息进行配置时,已经使用了POST请求,因为需要传送的是有关密码之类的保密数据,所以使用POST请求是更好的选择。但对于引脚(GPIO)的控制,对于保密性的要求并不高,所以可以选用GET来发送请求,注意,发送GET请求后,后台必须做出响应。
要在页面发送一个GET请求,最简单的方法是直接在浏览器访问该网址,比如要发送一个GET请求到我们之前搭建的WEB服务器下的/GPIO2,可以直接访问10.0.0.18/GPIO2,但是如果该链接下的页面并未建立,虽然GET可以成功发送,但可能会导致跳到一个并不存在的页面。所以,用JavaScript来发送GET请求是更好的选择。
用WEB页面控制ESP32引脚实现说明
我们先在html页面里定义一个发送GET请求的JavaScript函数
/******************************
函数:向path发送GET请求,并得到后台send()方法发送过来的值,把得到的值写到ElementId所指向的元素
注意,发送GET请求后台的响应是必要的。
ElementId: 要得到数据的元素ID
path: 请求的路径
******************************/
function get_request(ElementId , path){
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { //如果请求就绪和状态已完成
document.getElementById(ElementId).innerHTML = xmlhttp.responseText; //将获取到的内容<send(200,"text/plain", String());>写到id为ElementId的元素中
}
},
xmlhttp.open("GET", path, true); //发送GET请求
xmlhttp.send();
}
然后我们需要在页面生成一个按钮元素,当按钮被按下时,调用JavaScript发送GET请求,实现对ESP32的控制。同时生成一个可以显示文本的元素,这里选用段落元素<p>,来显示引脚的功能状态默认设置引脚功能显示为空或关。
<input type="button" value="控制GPIO" onclick="get_request('sw','/GPIO2')">
<p id="sw">关</p>
后台收到请求后,需要定义对页面请求的响应:
server.on("/GPIO2", HTTP_GET, GPIO_button); //响应改变引脚按钮的请求
收到请求后,会调用回调函数GPIO_button,回调函数会改引脚的状态和把引脚状态发送回页面
/***********************************************************************************
* 函数:响应按键回调函数
***********************************************************************************/
void GPIO_button(AsyncWebServerRequest *request){
int pin_state = digitalRead(2);
String state;
digitalWrite(2,(!pin_state)); //每次按下循环地改变引脚状态
if(digitalRead(2)){
state = "开";
}else{
state = "关";
}
request->send(200,"text/plain",state); //把状态发送回页面
Serial.print("引脚状态改变为:");
Serial.println(pin_state);
}
用WEB页面控制引脚(GPIO)功能完整代码
wifi_connect.ino
/*WIFI连接*/
#include <WiFi.h>
#include <DNSServer.h>
DNSServer dnsserver;
/***********************************************************************************
* 函数:连接WIFI
* ssid: WIFI名称
* password: WIFI密码
* return: 连接成功返回true
***********************************************************************************/
void connect_WIFI(String ssid, String password){
WiFi.begin(ssid.c_str(), password.c_str()); //连接WIFI
Serial.print("连接WIFI");
//循环,10秒后连接不上跳出循环
int i = 0;
while(WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(500);
i++;
if(i>20){
Serial.println();
Serial.println("WIFI连接失败");
return;
}
}
Serial.println();
IPAddress local_IP = WiFi.localIP();
Serial.print("WIFI连接成功,本地IP地址:"); //连接成功提示
Serial.println(local_IP);
}
/***********************************************************************************
* 设置AP和STA共存模式,设置DNS服务器
***********************************************************************************/
void connect_NET(){
const byte DNS_PORT = 53; //DNS端口
const String url = "ESPAP.com"; //域名
IPAddress APIp(10,0,10,1); //AP IP
IPAddress APGateway(10,0,10,1); //AP网关
IPAddress APSubnetMask(255,255,255,0); //AP子网掩码
const char* APSsid = "esp32_AP"; //AP SSID
const char* APPassword = "12345678"; //AP wifi密码
wifi_connect(); //连接WIFI
WiFi.mode(WIFI_AP_STA); //打开AP和STA共存模式
WiFi.softAPConfig(APIp, APGateway, APSubnetMask); //设置AP的IP地址,网关和子网掩码
WiFi.softAP(APSsid, APPassword, 6); //设置AP模式的登陆名和密码
dnsserver.start(DNS_PORT, url, APIp); //设置DNS的端口、网址、和IP
Serial.print("AP模式IP地址为:");
Serial.println(WiFi.softAPIP());
}
/***********************************************************************************
* DNS处理请求的循环
***********************************************************************************/
void DNS_request_loop(){
dnsserver.processNextRequest();
}
web_server.ino
#include "ESPAsyncWebServer.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
//AsyncWebServer server(80); //创建一个服务器对象,WEB服务器端口:80
/***********************************************************************************
*
***********************************************************************************/
/***********************************************************************************
* 函数:引脚初始化
***********************************************************************************/
void GPIO_begin(){
pinMode(2, OUTPUT); //引脚2设置为输出模式
}
/***********************************************************************************
* 函数:响应按键回调函数
***********************************************************************************/
void GPIO_button(AsyncWebServerRequest *request){
int pin_state = digitalRead(2);
String state;
digitalWrite(2,(!pin_state)); //每次按下循环地改变引脚状态
if(digitalRead(2)){
state = "开";
}else{
state = "关";
}
request->send(200,"text/plain",state); //把状态发送回页面
Serial.print("引脚状态改变为:");
Serial.println(pin_state);
}
/**************************************************************************************
* 函数:字符串写入文件,文件如果存在,将被清零并新建,文件不存在,将新建该文件
* path: 文件的绝对路径
* str: 要写入的字符串
*************************************************************************************/
void str_write(String path, String str){
Serial.println("写入文件");
File wf = LittleFS.open(path,"w"); //以写入模式打开文件
if(!wf){ //如果无法打开文件
Serial.println("打开文件写入时错误"); //显示错误信息
return; //无法打开文件直接返回
}
wf.print(str); //字符串写入文件
wf.close(); //关闭文件
File rf = LittleFS.open(path,"r"); //以读取模式打开文件
Serial.print("FILE:");Serial.println(rf.readString()); //读取文件
rf.close(); //关闭文件
}
// /**********************************************************************************
// * 函数:把收到的POST数据格式化为JSON格式的字符串
// *********************************************************************************/
//String format_json(AsyncWebParameter* post_data , int len){
// String json_name = post_data->name().c_str(); //得到名称
// String json_value = post_data->value.c_str(); //得到值
// StaticJsonDocument<len> json_obj; //创建一个JSON对象
// json_obj[json_name] = json_value; //写入一个名称和值
// String json_str;
// serializeJson(wifi_json, wifi_json_str); //生成JOSN的字符串
// return json_str; //返回JOSN字符串
//}
/**********************************************************************************
* 函数:响应网站/setwifi目录的POST请求,收到请求后,运行get_WIFI_set_CALLback回调函数
* 获取并格式化收到的POST数据
*********************************************************************************/
void get_WIFI_set_CALLback(AsyncWebServerRequest *request){
Serial.println("收到设置WIFI按钮");
if(request->hasParam("wifiname",true)){
AsyncWebParameter* wifiname = request->getParam("wifiname",true); //获取POST数据
AsyncWebParameter* wifipassword = request->getParam("wifipassword",true); //获取POST数据
String wn = wifiname->name().c_str();
String wnv = wifiname->value().c_str();
String wp = wifipassword->name().c_str();
String wpv = wifipassword->value().c_str();
//把SSID和password写成一个JSON格式
StaticJsonDocument<200> wifi_json; //创建一个JSON对象,wifi_json
wifi_json[wn] = wnv; //写入一个建和值
wifi_json[wp] = wpv; //写入一个键和值
String wifi_json_str; //定义一个字符串变量
serializeJson(wifi_json, wifi_json_str); //生成JOSN的字符串
str_write("/WIFIConfig.conf",wifi_json_str); //字符串写入
}
}
/**********************************************************************************
* 函数:从文件path中读取字符串
* path: 文件的绝对路径
* return: 返回读取的字符串
*********************************************************************************/
String str_read(String path){
Serial.println("读取文件");
File rf = LittleFS.open(path,"r"); //以读取模式打开文件
if(!rf){ //如果无法打开文件
Serial.println("打开文件读取时错误"); //显示错误信息
return ""; //无法打开文件直接返回
}
String str = rf.readString(); //读取字符串
rf.close(); //关闭文件
return str;
}
/***************************************************************************************
* 函数:解析JSON字符串,从JSON字符串名称得到该值
* str: JSON字符串
* Name: JSON集合的名称
* return: 返回值的字符串
***************************************************************************************/
String analysis_json(String str, String Name){
DynamicJsonDocument doc(str.length()*2); //定义一个JSON对象
deserializeJson(doc, str); //反序列数据
String value = doc[Name].as<String>(); //从Name中读取对应的值
return value;
}
/***********************************************************************************
* 函数:/WIFIConfig.conf文件中读取设置数据并连接WIFI
***********************************************************************************/
void wifi_connect(){
Serial.println("在conf文件中读取数据并连接WIFI");
String str = str_read("/WIFIConfig.conf"); //读取文件内容
String wifiname = analysis_json(str, "wifiname"); //解析WIFI名称
String wifipassword = analysis_json(str, "wifipassword"); //解析WIFI密码
connect_WIFI(wifiname, wifipassword); //连接WIFI
}
/***********************************************************************************
* web服务器初始化
***********************************************************************************/
void web_server(){
Serial.println("初始化WEB服务器");
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); //响应网站根目录的GET请求,返回文件index.html
server.on("/setwifi" ,HTTP_POST , get_WIFI_set_CALLback); //响应设置WIFI按钮的请求
server.on("/GPIO2", HTTP_GET, GPIO_button); //响应改变引脚按钮的请求
server.begin(); //初始化
}
/********************************************************************************
* LittleFS文件系统初始化
*********************************************************************************/
void LittleFS_begin(){
Serial.println();
Serial.println("初始化文件系统");
if(!LittleFS.begin(true)){
Serial.println("An Error has occurred while mounting LittleFS");
return;
}
}
ESP32_WEB.ino
#include "ESPAsyncWebServer.h"
AsyncWebServer server(80); //创建一个服务器对象,WEB服务器端口:80
void setup() {
Serial.begin(9600); //串口波特率初始化
LittleFS_begin(); //LittleFS文件系统初始化
connect_NET(); //网络初始化
web_server(); //WEB服务器初始化
GPIO_begin();
}
void loop() {
DNS_request_loop(); //DNS服务请求处理
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="mystyle.css">
<title>EPS32教程</title>
</head>
<body>
<div id="pal">
<form name="wifiset" onsubmit="return validateForm()" action="\setwifi" method="post" target="myframe">
<label for="wifiname">WIFI SSID</label>
<input name="wifiname" type="text" value="ESP32">
<label for="wifipassward">WIFI PASSWARD</label>
<input name="wifipassword" type="text" value="ESP32">
<input type='submit' value='设置WIFI'>
</form>
<iframe src="" width="200" height="200" frameborder="0" name="myframe" style="display:NONE" ></iframe>
<input type="button" value="控制GPIO" onclick="get_request('sw','/GPIO2')">
<p id="sw">关</p>
</div>
</body>
<script>
/******************************
表单验证,WIFI名称输入框为空时提示
******************************/
function validateForm(){
var wifiname=document.forms["wifiset"]["wifiname"].value; //得到name输入框的文字
//var wifipassword=document.forms["wifiset"]["password"].value;
if (wifiname==null || wifiname==""){ //如果输入框为空
alert("WIFI SSID必需输入"); //显示提示
return false;
}
}
/******************************
函数:向path发送GET请求,并得到后台send()方法发送过来的值,把得到的值写到ElementId所指向的元素
注意,发送GET请求后台的响应是必要的。
ElementId: 要得到数据的元素ID
path: 请求的路径
******************************/
function get_request(ElementId , path){
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { //如果请求就绪和状态已完成
document.getElementById(ElementId).innerHTML = xmlhttp.responseText; //将获取到的内容<send(200,"text/plain", String());>写到id为ElementId的元素中
}
},
xmlhttp.open("GET", path, true); //发送GET请求
xmlhttp.send();
}
</script>
</html>
在下面的文章中,将会介绍使用MQTT协议来远程和ESP32进行互动