物联网 基础(二)
1.1.1 ESP8266网络客户端基本操作
在我们上网过程中,经常使用网页浏览器来浏览网站信息。在这一场景中,我们的网站浏览器就是一个网络客户端。网络客户端的主要功能就是向服务器发送网络请求。服务器在接收到客户端请求后会将请求的信息回复给客户端。
在本节课程中,我们一起学习如何让ESP8266-NodeMCU开发板扮演网络客户端的角色,从而向网络服务器发送HTTP请求以获取服务器响应信息。
ESP8266向服务器发送请求以获取服务器响应信息
ESP8266-Arduino库中有两个库用于控制ESP8266与网络服务器进行通讯。他们是WiFiClient库和ESP8266HTTPClient库。
这两个库虽然功能相似,但是他们却是截然不同的两个库。ESP8266HTTPClient库相对简单易用。该库的库函数可以自动生成客户端请求信息并解析服务器响应信息。但正是由于该库的库函数为我们做了所有底层工作,这就导致该库不如WiFiclient库灵活。假如我们需要用ESP8266建立自定义客户端请求信息时,就要用WiFiClient库来实现了。另外WiFiClient库在解析服务器响应信息时可以使用丰富的STREAM类函数,这也为我们提供了很多便利。
因此,在我们使用ESP8266开发项目时,更多的时候是使用WiFiClient库来实现物联网通讯功能。换句话说,我个人认为WiFiClient库的重要性和实用性要高于ESP8266HTTPClient库。
以上描述有些抽象,接下来我们看两个简单示例。这两个示例的功能是相同的。都是通过ESP8266通过互联网向网络服务器发送请求并且将将网站服务器响应的信息输出在屏幕中。不同的是,示例一使用了ESP8266HTTPClient库来实现这一操作,示例二使用了WiFiClient库实现。
示例1. 使用ESP8266HTTPClient库实现网络通讯
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
// 测试HTTP请求用的URL。注意网址前面必须添加"http://"
#define URL "http://www.example.com"
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "yang1234";
const char* password = "y123456789";
void setup() {
//初始化串口设置
Serial.begin(9600);
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
//开始连接wifi
WiFi.begin(ssid, password);
//等待WiFi连接,连接成功打印IP
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.print("WiFi Connected!");
httpClientRequest();
}
void loop() {}
// 发送HTTP请求并且将服务器响应通过串口输出
void httpClientRequest(){
//重点1 创建 HTTPClient 对象
HTTPClient httpClient;
//重点2 通过begin函数配置请求地址。此处也可以不使用端口号和PATH而单纯的
httpClient.begin(URL);
Serial.print("URL: "); Serial.println(URL);
//重点3 通过GET函数启动连接并发送HTTP请求
int httpCode = httpClient.GET();
Serial.print("Send GET request to URL: ");
Serial.println(URL);
//重点4. 如果服务器响应HTTP_CODE_OK(200)则从服务器获取响应体信息并通过串口输出
//如果服务器不响应HTTP_CODE_OK(200)则将服务器响应状态码通过串口输出
if (httpCode == HTTP_CODE_OK) {
// 使用getString函数获取服务器响应体内容
String responsePayload = httpClient.getString();
Serial.println("Server Response Payload: ");
Serial.println(responsePayload);
} else {
Serial.println("Server Respose Code:");
Serial.println(httpCode);
}
//重点5. 关闭ESP8266与服务器连接
httpClient.end();
}
串口监视器
⸮hout prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
WiFi Connected!URL: http://www.example.com
Send GET request to URL: http://www.example.com
Server Response Payload:
<!doctype html>
<html>
<head>
<title>Example Domain</title><meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head><body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
程序重点讲解
重点1. 声明HTTPClient对象,对象名称httpClient。后续程序中,我们将使用对象httpClient控制ESP8266的网络通讯。
重点2. 通过ESP8266HTTPClient库的begin函数来设置ESP8266发送HTTP请求的目标URL。
重点3. 通过ESP8266HTTPClient库的GET函数向服务器发送HTTP请求。
重点4. 以上重点3中GET函数的返回值是网络服务器响应状态码。根据该状态码,我们可以判断服务器是否成功接收到了ESP8266客户端的请求。如果服务器成功接收到请求,我们就可以在接下来使用getString函数来获取服务器响应报文(服务器响应体)信息,并且将该信息传递给responsePayload变量以便我们在后面通过串口监视器显示服务器响应报文。(这一报文信息正是www.example.com网站的首页HTML源代码)。
重点5.执行完以上操作后,我们将关闭ESP8266与服务器连接。这里是通过ESP8266HTTPClient库的end函数来实现这一操作的。
1.1.2. 使用WiFiClient库实现网络通讯
#include <ESP8266WiFi.h>
const char* host = "www.example.com"; // 网络服务器地址
const int httpPort = 80; // http端口80
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "yang1234";
const char* password = "y123456789";
void setup() {
//初始化串口设置
Serial.begin(9600);
Serial.println("");
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
//开始连接wifi
WiFi.begin(ssid, password);
//等待WiFi连接,连接成功打印IP
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
wifiClientRequest();
}
void loop(){}
// 向服务器发送HTTP请求
void wifiClientRequest(){
// 建立WiFi客户端对象,对象名称client
WiFiClient client;
// 建立字符串,用于HTTP请求
String httpRequest = String("GET /") + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n" +
"\r\n"; //空行表示结束
// 通过串口输出连接服务器名称以便查阅连接服务器的网址
Serial.print("Connecting to ");
Serial.print(host);
// 连接网络服务器,以下段落中的示例程序为本程序重点1
// 请参考太极创客网站中关于本程序的讲解页面获取详细说明信息。网址:
// http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/esp8266-nodemcu-web-client/http-request/
if (client.connect(host, httpPort)){
Serial.println(" Success!"); // 连接成功后串口输出“Success”信息
client.print(httpRequest); // 向服务器发送HTTP请求
Serial.println("Sending request: ");// 通过串口输出HTTP请求信息内容以便查阅
Serial.println(httpRequest);
// 通过串口输出网络服务器响应信息, 以下段落中的示例程序为本程序重点2
// 请参考太极创客网站中关于本程序的讲解页面获取详细说明信息。网址:
// http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/esp8266-nodemcu-web-client/http-request/
Serial.println("Web Server Response:");
while (client.connected() || client.available()){ //开发板与服务器连接或收到服务器响应信息
if (client.available()){
String line = client.readStringUntil('\n');
Serial.println(line);
}
}
client.stop(); // 断开与服务器的连接
Serial.print("Disconnected from "); // 并且通过串口输出断开连接信息
Serial.print(host);
} else{ // 如果连接不成功则通过串口输出“连接失败”信息
Serial.println(" connection failed!");
client.stop();
}
}
程序重点讲解
重点1:实现网络服务器连接
这部分逻辑判断语句中的判断条件是client.connect(host, httpPort)
的返回值。ESP8266所实现的网络客户端是通过connect
函数来实现与网络服务器的连接的。被连接的网络服务器网址为connect函数的第一个参数,即host。这里的第二个参数httpPort
则是连接网络服务器的端口编号。关于host 和 httpPort的具体定义都在程序刚一开始的部分。
如果ESP8266所建立的网络客户端成功与网络服务器建立了连接,connect
函数将会返回“真”,否则将会返回“假”。利用connect
函数返回值,程序可以根据网络服务器的连接状况来决定具体执行哪一个操作。即:
– 连接成功则通过后续的while
循环语句来获取网络服务器的HTTP响应信息,并且将信息通过串口输出。
– 连接不成功则通过串口输出“连接失败”信息。
重点2:获取网络服务器响应信息并且通过串口输出
这里的 while (client.connected() || client.available())
循环语句判断条件由两个函数的返回值来决定。
第一个条件是 client.connected()
的返回值。connected()
这个函数用于检查当前ESP8266与网络服务器的连接情况。如果连接状态为“真”,则返回真。否则返回“假”。
第二个条件是 client.available()
的返回值。available()
函数用于检查网络客户端是否有接收到服务器发来的信息。如果有信息则返回真,否则返回“假”。
利用以上两个条件进行“或”运算所得到的结果即是这里while
循环语句的判断条件。换句话说,就是当ESP8266与服务器保持连接以及服务器有信息发送给ESP8266这两个条件满足一个,while循环语句体就会执行循环。当这两个条件都不满足了,则跳出循环。
接下来我们看一下while
循环的具体内容。这里我们使用了一个逻辑判断语句。判断条件再次出现了client.available()
。在ESP8266与网络服务器通过connect
函数建立连接并且ESP8266发送了HTTP请求以后,ESP8266并不会马上就收到服务器的响应信息。造成这个情况的原因有很多个,其中主要原意是服务器接到HTTP响应后,也许需要处理其它客户端的响应或者进行其它工作。这就导致服务器在响应ESP8266时会产生延迟。另外我们的网络环境也会产生延迟。也就是说服务器从发出响应到我们的ESP8266接收到响应这个过程是受到到网络环境等等因素影响的。
基于以上原因,我们需要让ESP8266客户端在与网络服务器取得连接以后在原地待命。一旦ESP8266客户端接收到服务器的响应信息,available函数将会返回“真”值,这时才让ESP8266客户端开始检查收到的响应信息具体是什么。这个检查工作是通过逻辑判断语句中的,client.readStringUntil('\n')
来实现的。
关于readStringUntil
函数的具体用法,由于涉及到stream的概念,我们在下一节给各位详细讲解。这里请各位了解一点就是client.readStringUntil('\n')
函数会将服务器响应信息中逐行分解为字符串。这些字符串会赋值给调用该函数的位置,也就是赋值给line
这个字符串变量中。
Stream这个概念可能很多朋友会感到陌生。其实在过去的学习和开发中,我们已经使用Stream很久了。Stream对于ESP8266-Arduino语言来说指的是数据序列。请留意:在C++编程中Stream常被翻译作“流”。我们认为将Stream称为数据序列更加直观。因为数据序列这一概念有两个很关键特点。
第一个特点是“序”,即数据序列不能是杂乱无章的数据罗列。
第二个特点是“列”,即数据序列是排成一列的。
综上所述,数据序列可以理解为一列有先后顺序的数据。
示例1 使用串口监视器演示Stream概念
void setup() {
// 启动串口通讯
Serial.begin(9600);
Serial.println();
}
void loop() {
if (Serial.available()){ // 当串口接收到信息后
String serialData = Serial.readString(); // 将接收到的信息使用readString()存储于serialData变量
Serial.print(serialData); // 以便查看serialData变量的信息
}
}
在本示例中,我们使用了Serial.available
来判断ESP8266开发板是否接收到串口数据。这里的开发板通过串口所接收到的数据就是Stream数据。另外,程序通过Serial.println
语句将接收到的Stream数据通过串口输出并显示在串口监视器中,这里ESP8266通过串口所输出的数据也是Stream数据。换句话说,ESP8266开发板通过串口收发的数据都是Stream数据。
示例2 使用HTTP请求和响应信息演示Stream概念
以下示例程序是上一节课“3-4-1 ESP8266网络客户端基本操作”中的第二个示例程序。该示例程序中,ESP8266开发板通过client.print
向服务器发送HTTP请求。这里开发板所发出的HTTP请求信息就是Stream数据。另外,以下示例程序中,ESP8266开发板通过client.readStringUntil来读取服务器响应信息。这里服务器响应信息也是Stream数据。
#include <ESP8266WiFi.h>
const char* host = "www.example.com"; // 网络服务器地址
const int httpPort = 80; // http端口80
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "yang1234";
const char* password = "y123456789";
void setup() {
//初始化串口设置
Serial.begin(9600);
Serial.println("");
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
//开始连接wifi
WiFi.begin(ssid, password);
//等待WiFi连接,连接成功打印IP
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
wifiClientRequest();
}
void loop(){}
// 向服务器发送HTTP请求
void wifiClientRequest(){
// 建立WiFi客户端对象,对象名称client
WiFiClient client;
// 建立字符串,用于HTTP请求
String httpRequest = String("GET /") + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n" +
"\r\n";
// 通过串口输出连接服务器名称以便查阅连接服务器的网址
Serial.print("Connecting to ");
Serial.print(host);
// 连接网络服务器,以下段落中的示例程序为本程序重点1
// 请参考太极创客网站中关于本程序的讲解页面获取详细说明信息。网址:
// http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/esp8266-nodemcu-web-client/http-request/
if (client.connect(host, httpPort)){
Serial.println(" Success!"); // 连接成功后串口输出“Success”信息
client.print(httpRequest); // 向服务器发送合同谈判请求
Serial.println("Sending request: ");// 通过串口输出HTTP请求信息内容以便查阅
Serial.println(httpRequest);
// 通过串口输出网络服务器响应信息, 以下段落中的示例程序为本程序重点2
// 请参考太极创客网站中关于本程序的讲解页面获取详细说明信息。网址:
// http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/esp8266-nodemcu-web-client/http-request/
Serial.println("Web Server Response:");
while (client.connected() || client.available()){
if (client.available()){
String line = client.readStringUntil('\n');
Serial.println(line);
}
}
client.stop(); // 断开与服务器的连接
Serial.print("Disconnected from "); // 并且通过串口输出断开连接信息
Serial.print(host);
} else{ // 如果连接不成功则通过串口输出“连接失败”信息
Serial.println(" connection failed!");
client.stop();
}
}
示例3 使用File对象演示Stream概念
以下示例程序是从“3-3-1 ESP8266闪存文件系统基本操作”中的第1个示例程序修改而来。该示例程序通过dataFile.println
来向文件中写入信息。使用dataFile.find
从闪存文件内容里查找指定信息。使用dataFile.readString
来获取信息。这些操作都是针对Stream数据的操作。
#include <FS.h>
String file_name = "/taichi-maker/notes.txt"; //被读取的文件位置和名称
void setup() {
Serial.begin(9600);
Serial.println("");
// 启动SPIFFS
if(SPIFFS.begin()){
Serial.println("SPIFFS Started.");
} else {
Serial.println("SPIFFS Failed to Start.");
}
File dataFile = SPIFFS.open(file_name, "w");// 建立File对象用于向SPIFFS中的file对象(即/notes.txt)写入信息
dataFile.println("Hello IOT World."); // 向dataFile写入字符串信息
dataFile.close(); // 完成文件写入后关闭文件
Serial.println("Finished Writing data to SPIFFS");
// 使用find函数从dataFile中找到指定信息
Serial.print("Try to find IOT in ");Serial.println(file_name);
dataFile = SPIFFS.open(file_name, "r"); // 以“r”模式再次打开闪存文件
if (dataFile.find("IOT")){ // 在闪存文件中查找文字"IOT"
Serial.print("Found IOT in file: "); // 如果找到则告知用户找到文字"IOT"
Serial.println(file_name);
}
// 使用readString读取执行完find函数后的dataFile内容并显示与串口监视器
Serial.println("Use readString to get contents of dataFile after find");
Serial.println(dataFile.readString());
dataFile.close(); // 完成操作后关闭文件
}
void loop() {}
串口监视器
SPIFFS Started.
Finished Writing data to SPIFFS
Try to find IOT in /taichi-maker/notes.txt
Found IOT in file: /taichi-maker/notes.txt
Use readString to get contents of dataFile after find
World.
通过以上几个示例程序相信您已经感受到了。Stream是ESP8266-Arduino开发环境中的一种数据类型。Serial库,WiFiClient库,FS库所建立的对象都可以处理Stream数据。另外除了以上这些库以外,以下列表中的库也可以处理Stream数据。
1.3客户端向服务器发送数据信息
在之前的课程中,我们曾经学习了如何使用ESP8266来建立网络服务器。在本节课程中,我们一起学习如何让ESP8266开发板以网络客户端的角色向服务器发送HTTP请求,并且获取何处理服务器响应信息。通过这一操作,我们可以实现ESP8266开发板间的物联网数据通信。
如上图所示,在接下来的讲解中,我们将需要两块ESP8266-NodeMCU开发板。其中一块作为服务器,另一块作为客户端。
使用ESP8266客户端向ESP8266服务器发送数据
首先我们来看第一个示例。在这个示例中,ESP8266客户端将会通过HTTP协议向ESP8266服务器发送信息。在运行过程中,客户端ESP8266将会实时检测板上的按键状态,并且把按键状态发送给服务器。服务器在接收到客户端按键状态后,可以根据客户端按键状态来控制服务器端板上的LED点亮和熄灭。最终实现的效果是,我们可以通过客户端ESP8266开发板上的按键来“遥控”服务器上的LED点亮和熄灭。
注意,下示例中的服务器端和客户端ESP8266必须连接同一WiFi网络,方可实现数据通讯。
服务器端程序:
– 接收客户端发来的http请求并且解析信息中的数据信息
– 将解析的数据信息通过串口监视器显示供用户查看
– 将解析的客户端按键状态信息用于控制服务器端板上LED的点亮和熄灭
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h> // 使用WiFiMulti库
#include <ESP8266WebServer.h> // 使用WebServer库
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象,对象名称是 'wifiMulti'
ESP8266WebServer server(80); // 建立网络服务器对象,该对象用于响应HTTP请求。监听端口(80)
IPAddress local_IP(192, 168, 31, 144); // 设置ESP8266-NodeMCU联网后的IP
IPAddress gateway(192, 168, 31, 1); // 设置网关IP(通常网关IP是WiFI路由IP)
IPAddress subnet(255, 255, 255, 0); // 设置子网掩码
IPAddress dns(192,168,31,1); // 设置局域网DNS的IP(通常局域网DNS的IP是WiFI路由IP)
void setup(void){
Serial.begin(9600); // 启动串口通讯
Serial.println("");
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
// 设置开发板网络环境
if (!WiFi.config(local_IP, gateway, subnet)) {
Serial.println("Failed to Config ESP8266 IP");
}
wifiMulti.addAP("yang1234", "y123456789"); // 将需要连接的一系列WiFi ID和密码输入这里
wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2"); // ESP8266-NodeMCU再启动后会扫描当前网络
wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3"); // 环境查找是否有这里列出的WiFi ID。如果有
Serial.println("Connecting ..."); // 则尝试使用此处存储的密码进行连接。
// 尝试进行wifi连接。
while (wifiMulti.run() != WL_CONNECTED) {
delay(250);
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
server.on("/update", handleUpdate); // 处理服务器更新函数
server.begin(); // 启动网站服务
Serial.println("HTTP server started");
}
void loop(void){
server.handleClient(); // 检查http服务器访问
}
void handleUpdate(){
float floatValue = server.arg("float").toFloat(); // 获取客户端发送HTTP信息中的浮点数值
int intValue = server.arg("int").toInt(); // 获取客户端发送HTTP信息中的整数数值
int buttonValue = server.arg("button").toInt(); // 获取客户端发送HTTP信息中的按键控制量
server.send(200, "text/plain", "Received"); // 发送http响应
buttonValue == 0 ? digitalWrite(LED_BUILTIN, LOW) : digitalWrite(LED_BUILTIN, HIGH);
// 通过串口监视器输出获取到的变量数值
Serial.print("floatValue = "); Serial.println(floatValue);
Serial.print("intValue = "); Serial.println(intValue);
Serial.print("buttonValue = "); Serial.println(buttonValue);
Serial.println("=================");
}
客户端程序:
– 客户端通过HTTP协议向服务器发送信息
– 信息中包含客户端按键开关引脚状态用于控制服务器板上LED的点亮和熄灭
– 信息中还包含测试数据以便我们更好的了解如何使用ESP8266发送和接收物联网数据信息
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h> // 使用WiFiMulti库
#define buttonPin D3 // 按钮引脚D3
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象,对象名称是 'wifiMulti'
bool buttonState; //存储客户端按键控制数据
float clientFloatValue; //存储客户端发送的浮点型测试数据
int clientIntValue; //存储客户端发送的整数型测试数据
const char* host = "192.168.31.144"; // 即将连接服务器网址/IP
const int httpPort = 80; // 即将连接服务器端口
void setup(void){
Serial.begin(9600); // 启动串口通讯
Serial.println("");
pinMode(buttonPin, INPUT_PULLUP); // 将按键引脚设置为输入上拉模式
wifiMulti.addAP("yang1234", "y123456789"); // 将需要连接的一系列WiFi ID和密码输入这里
wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2"); // ESP8266-NodeMCU再启动后会扫描当前网络
wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3"); // 环境查找是否有这里列出的WiFi ID。如果有
Serial.println("Connecting ..."); // 则尝试使用此处存储的密码进行连接。
while (wifiMulti.run() != WL_CONNECTED) { // 尝试进行wifi连接。
delay(250);
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
}
void loop(void){
// 获取按键引脚状态
buttonState = digitalRead(buttonPin);
// 改变测试用变量数值用于服务器端接收数据检测
clientFloatValue += 1.5;
clientIntValue += 2;
// 发送请求
wifiClientRequest();
delay(1000);
}
void wifiClientRequest(){
WiFiClient client;
// 将需要发送的数据信息放入客户端请求
String url = "/update?float=" + String(clientFloatValue) +
"&int=" + String(clientIntValue) +
"&button=" + String(buttonState);
// 建立字符串,用于HTTP请求
String httpRequest = String("GET ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n" +
"\r\n";
Serial.print("Connecting to ");
Serial.print(host);
if (client.connect(host, httpPort)) { //如果连接失败则串口输出信息告知用户然后返回loop
Serial.println(" Sucess");
client.print(httpRequest); // 向服务器发送HTTP请求
Serial.println("Sending request: ");// 通过串口输出HTTP请求信息内容以便查阅
Serial.println(httpRequest);
} else{
Serial.println(" failed");
}
client.stop();
}
1.4 JSON
1. JSON重点概念
数据 对象 数组
2. JSON语法规则要点
- 数据以“名”“值”对呈现
- 数据“名”和“值”之间由冒号分隔
- 大括号{}用于标注对象内容
- 中括号[]用于标注数组内容
- 逗号用于分隔数据、对象、数组
3. JSON数据
JSON数据以“名”“值”对呈现。数据“名”“值”由冒号分隔。JSON数据的书写格式是:
“JSON数据名”:JSON数据值
JSON数据举例:
“Year”: 2016
“URL”:”www.taichi-maker.com”
JSON数据名称
JSON数据名称需要放在双引号中。以下示例都是合法的JSON数据名:
“Value”、”信息1”
JSON数据值
JSON数据值可以是以下内容:
- 数字(整数或浮点数)
- 字符串
- 逻辑值(true 或 false)
- 数组(在中括号中)
- 对象(在大括号中)
- null
注意:一个JSON数据名称只能对应一个值。以下是一系列JSON数据的举例。
3.1 JSON数字数据示例
"value" : 25
3.2 JSON字符串数据示例
"name" : "taichi-maker"
3.3 JSON逻辑值数据示例
"bool_value" : true
"info": [
{
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
},
{
"year": 2020,
"month": 12,
"day": 30
}
]
3.5 JSON对象数据示例
"info": {
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
}
3.6 JSON null 数据示例
“value” : null
4. JSON 对象
JSON对象在大括号{}中书写,对象可以包含单个或者多个JSON数据。
对象(object) 是一个无序的数据集合(“‘名/值’对”集合)。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名/值’ 对”之间使用“,”(逗号)分隔。
以下是含有单个数据的JSON对象示例:
{
"name" : "taichi-maker"
}
以下是含有多个数据的JSON对象示例。该对象由两个JSON数据组成。
{
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
}
我们再来看一个含有多个数据的JSON对象示例。该对象包含两个JSON数据。而每一个JSON数据又包含一个JSON对象。
{
"info": {
"name": "taichi-maker",
"website": "www.taichi-maker.com"
},
"date": {
"year": 2020,
"month": 12,
"day": 30
}
}
从以上的示例我们可以看到,JSON对象中的数据使用逗号进行分隔。
注意:对象不能直接存放对象,以下示例是错误的。
{
{
"name": "taichi-maker",
"website": "www.taichi-maker.com"
},
{
"year": 2020,
"month": 12,
"day": 30
}
}
注意:对象也不能直接存放数组,以下示例是错误的。
{
"info": {
"name": "taichi-maker",
"website": "www.taichi-maker.com"
},
[
{
"temperature" : 15
}
]
}
5. JSON 数组
数组(array) 是相同元素的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。 如下所示:
["Tom","Jerry","Shuke","Beita"]
或者
[1,3,5,7]
数组可包含一个或者多个对象。以下是包含单个对象的数组示例:
[
{
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
}
]
以下是包含多个对象的数组示例:
[
{
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
},
{
"year": 2020,
"month": 12,
"day": 30
}
]
数组也可以包含单个或多个数组,如下所示:
[
[
{
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
},
{
"year": 2020,
"month": 12,
"day": 30
}
],
[
{
"temperature" : 15,
}
]
]
请留意,在以上示例中。数组中的元素之间都是使用逗号进行分割的。
注意:数组不能直接存放JSON数据。以下示例是错误的。
6. JSON 对象与数组混合存放示例["date": "2020-02-02",
"weekday": "THU"
]
通过以下示例我们可以看到,该JSON对象包含有一个数据,数据名为results,该数据的值是一个数组。此数组只含有一个对象。此对象包含有三个数据。这三个数据的名称分别是:location、now和last_update。其中location的值是含有两个数据的对象。now的值是含有三个数据的对象。last_update的值是字符串”2020-03-01T20:10:00+08:00″。
{
"results": [
{
"location": {
"name": "Beijing",
"country": "CN"
},
"now": {
"text": "Clear",
"code": "1",
"temperature": "3"
},
"last_update": "2020-03-01T20:10:00+08:00"
}
]
}
7. 总结
JSON文件乍一看很复杂,但只要注意以下几点就可以分析出JSON数据内容。
首先注意以下符号的含义:
:用于分隔数据的“名”和“值”
{} 标注对象内容
[]标注数组内容
,分隔数据、对象和数组
另外注意:
对象用于存放数据(名值对)。
对象不能直接存放对象或数组。
数组存放元素有序号(序号起始值0)。
数组不能直接存放数据(名值对)。
网上有很多网页JSON工具供我们使用。这里我们推荐您我们使用多年的oktools.net网站JSON在线工具。您可以使用该工具解析JSON文件,也可以用它来验证编写的JSON是否符合语法规则。
1.4.2 JSON解析
下载地址:
JSON解析示例-1:单一对象JSON解析
在以下示例中,您将会看到如何使用ESP8266配合ArduinoJson库来解析只有一个对象的简单JSON信息。该信息如下:
{
"name": "taichi-maker",
"number": 1
}
以下是该示例程序
需要安装第三方库,搜索ArduinoJson
#include <ArduinoJson.h>
void setup() {
Serial.begin(9600);
Serial.println("");
// 重点1:DynamicJsonDocument对象
const size_t capacity = JSON_OBJECT_SIZE(2) + 30;
DynamicJsonDocument doc(capacity);
// 重点2:即将解析的json文件
String json = "{\"name\":\"taichi-maker\",\"number\":1}";
// 重点3:反序列化数据
deserializeJson(doc, json);
// 重点4:获取解析后的数据信息
String nameStr = doc["name"].as<String>();
int numberInt = doc["number"].as<int>();
// 通过串口监视器输出解析后的数据信息
Serial.print("nameStr = ");Serial.println(nameStr);
Serial.print("numberInt = ");Serial.println(numberInt);
}
void loop() {}
nameStr = taichi-maker
numberInt = 1
语句讲解
重点1:
const size_t capacity = JSON_OBJECT_SIZE(2) + 30;
DynamicJsonDocument doc(capacity);
这里我们建立了DynamicJsonDocument对象,该对象名称为doc。在建立该对象时需要提供一个参数,也就是括号中的参数capacity。这个capacity参数的作用是告诉ESP8266我们所建立的DynamicJsonDocument对象将要占用多大的内存空间。这个空间大小是由语句const size_t capacity = JSON_OBJECT_SIZE(2) + 30;
计算出来的。在这里我们回顾一下需要解析的JSON信息内容如下所示:
{
"name": "taichi-maker",
"number": 1
}
我们可以看到,以上JSON信息中包含一个对象,该对象含有两个数据。因此在计算DynamicJsonDocument对象占用空间大小时,使用了JSON_OBJECT_SIZE(2)这条指令。其中指令括号中的2即代表对象包含有两个数据。
我们再看一个例子,假设我们即将解析的JSON如下:
{
"name": "taichi-maker",
"url": "www.taichi-maker.com",
"number": 1
}
以上JSON对象中包含有3个数据。在计算解析它所需要占用的内存大小时,我们将要使用语句:const size_t capacity = JSON_OBJECT_SIZE(3) + 60;
讲到这里可能细心的朋友已经发现了,在以上语句中除了JSON_OBJECT_SIZE指令以外还使用+ 60来额外增加数值。这些额外增加的数值是由于ArduinoJson库在解析信息时,需要额外的空间来复制JSON信息。但是具体这个额外增加的数值是多少呢,请您先把这个问题留在心里,后面我们会给您做讲解。
重点2:
String json = "{\"name\":\"taichi-maker\",\"number\":1}";
这条语句的作用是建立字符串变量,改变里用于存储需要解析的JSON信息。
重点3:
deserializeJson(doc, json);
这部分语句的作用是使用deserializeJson来对JSON文件进行解析。其中第一个参数是我们重点1讲解的DynamicJsonDocument对象,第二个参数是重点2讲解的json字符串。
重点4:
String nameStr = doc["name"].as();
int numberInt = doc["number"].as();
这两条语句用于获取解析后的JSON信息,其中doc["name"].as
将会返回“name”的值。这条语句中.as将会让“name”的值以字符串的形式返回。
另一条语句中doc["number"].as()
自然就是以整数形式来返回”number”的数据值。
JSON解析示例-2:JSON数组解析
以下示例演示了如何使用ArduinoJson库解析一个JSON数组信息。该信息如下:
[
{
"name": "taichi-maker"
},
{
"website": "www.taichi-maker.com"
}
]
以下是示例程序内容
#include <ArduinoJson.h>
void setup() {
Serial.begin(9600);
// 重点1:DynamicJsonDocument对象
const size_t capacity = JSON_ARRAY_SIZE(2) + 2*JSON_OBJECT_SIZE(1) + 60;
DynamicJsonDocument doc(capacity);
// 重点2:即将解析的json文件
String json = "[{\"name\":\"taichi-maker\"},{\"website\":\"www.taichi-maker.com\"}]";
// 重点3:反序列化数据
deserializeJson(doc, json);
String nameStr = doc[0]["name"].as<String>();
String websiteStr = doc[1]["website"].as<String>();
// 通过串口监视器输出解析后的数据信息
Serial.print("nameStr = ");Serial.println(nameStr);
Serial.print("websiteStr = ");Serial.println(websiteStr);
}
void loop() {}
nameStr = taichi-maker
websiteStr = www.taichi-maker.com
语句讲解
重点1:
DynamicJsonDocument doc(capacity);
与以上示例相同,这里我们建立了DynamicJsonDocument对象,该对象名称为doc。doc对象的capacity参数用于设置解析JSON所需要的内存大小。这个空间大小是由语句const size_t capacity = JSON_ARRAY_SIZE(2) + 2*JSON_OBJECT_SIZE(1) + 60;
计算出来的。在这里我们同样回顾一下需要解析的JSON信息内容:
[
{
"name": "taichi-maker"
},
{
"website": "www.taichi-maker.com"
}
]
我们可以看到,以上JSON信息是一个数组,该数组含有两个元素。因此,我们在计算capacity时首先使用了语句JSON_ARRAY_SIZE(2)
来获得含有两个元素的数组所占用内存的大小。
另外,这两个数组元素都是含有一个数据的对象。因此,我们在计算capacity时使用了语句2*JSON_OBJECT_SIZE(1)
。其中JSON_OBJECT_SIZE(1)
可以获得含有一个数据的对象大小,我们将它乘以2是因为这里有两个含有一个数据的对象。
在计算capacity的时候,我们还在计算的最后增加60。这么做是由于ArduinoJson库在解析信息时,需要额外的空间来复制JSON信息。
计算capacity可以使用ArduinoJson官网的在线工具。您可以点击这里打开该工具页面。如您需要了解该工具的具体使用方法,欢迎您收看太极创客团队制作的《零基础入门学用物联网》教程,您可以点击这里打开具体介绍该工具使用方法的教程页面。
重点2
String json = "[{\"name\":\"taichi-maker\"},{\"website\":\"www.taichi-maker.com\"}]";
这条语句的作用是建立字符串变量,改变里用于存储需要解析的JSON信息。
重点3:
deserializeJson(doc, json);
这部分语句的作用是使用deserializeJson来对JSON文件进行解析。其中第一个参数是我们重点1讲解的DynamicJsonDocument对象,第二个参数是重点2讲解的json字符串。
JSON解析示例-3:使用ArduinoJson官网在线工具解析JSON信息
ArduinoJson官网提供了在线工具可帮助我们自动生成JSON解析代码。该工具网址如下:
https://arduinojson.org/v6/assistant/
如需了解如何使用ArduinoJson官网在线工具来自动生成解析代码,请参考《零基础入门学用物联网教程》的基础知识篇中3-4 ESP8266-NodeMCU网络客户端部分的视频教程讲解。
以下是借助ArduinoJson官网在线工具生成的JSON解析代码。
此代码解析的JSON:
{
"results": [
{
"location": {
"name": "Beijing",
"country": "CN"
},
"now": {
"text": "Clear",
"code": "1",
"temperature": "3"
},
"last_update": "2020-03-01T20:10:00+08:00"
}
]
}
#include <ArduinoJson.h>
void setup() {
Serial.begin(9600);
const char* json = "{\"results\":[{\"location\":{\"name\":\"Beijing\",\"country\":\"CN\"},\"now\":{\"text\":\"Clear\",\"code\":\"1\",\"temperature\":\"3\"},\"last_update\":\"2020-03-01T20:10:00+08:00\"}]}";
DynamicJsonDocument doc(256);
deserializeJson(doc, json);
JsonObject results_0 = doc["results"][0];
const char* results_0_location_name = results_0["location"]["name"]; // "Beijing"
const char* results_0_location_country = results_0["location"]["country"]; // "CN"
JsonObject results_0_now = results_0["now"];
const char* results_0_now_text = results_0_now["text"]; // "Clear"
const char* results_0_now_code = results_0_now["code"]; // "1"
const char* results_0_now_temperature = results_0_now["temperature"]; // "3"
const char* results_0_last_update = results_0["last_update"]; // "2020-03-01T20:10:00+08:00"
String location_name = results_0["location"]["name"].as<String>();
String location_country = results_0["location"]["country"].as<String>();
String now_text = results_0_now["text"].as<String>();
int now_code = results_0_now["code"].as<int>();
int now_temperature = results_0_now["temperature"].as<int>();
Serial.print("location_name="); Serial.println(location_name);
Serial.print("location_country="); Serial.println(location_country);
Serial.print("now_text="); Serial.println(now_text);
Serial.print("now_code="); Serial.println(now_code);
Serial.print("now_temperature="); Serial.println(now_temperature);
}
void loop() {
// put your main code here, to run repeatedly:
}
location_name=Beijing
location_country=CN
now_text=Clear
now_code=1
now_temperature=3
由于易于解析且量级很轻,JSON成为了常用的物联网信息传输格式之一。在这一节里,我们将一起学习以下几个主要知识点:
1. 使用ESP8266来建立物联网服务器,该服务器可以向客户端发送JSON格式响应信息从而实现物联网信息通讯。
2. 使用ESP8266来通过网络向物联网服务器请求JSON信息
3. 使用ESP8266来通过ArduinoJson库解析JSON信息
在接下来的讲解中,我们将需要两块ESP8266-NodeMCU开发板。其中一块作为服务器,另一块作为客户端。如下图所示,客户端将会向服务器发送请求信息。服务器端在接收到客户端请求后,会将JSON信息加入服务器响应信息中发送给客户端。
ESP8266-Client-Gets-Json
注意,以下示例中的服务器端和客户端ESP8266必须连接同一WiFi网络,方可实现数据通讯。
示例一 ESP8266客户端请求单一JSON数据信息
本示例分为两部分,一部分为服务器程序,另一部分为客户端程序。
服务器端程序
服务器端程序主要功能:
1. 实时读取A0、 D1、D2以及D3引脚的读数。
2. 当有客户端请求时,通过响应信息将引脚读数和测试数据信息发送给客户端。
信息发送格式为json格式。以下为该json信息的示例:
{
"info": {
"name": "taichimaker",
"url": "www.taichi-maker.com",
"email": "taichimaker@163.com"
},
"digital_pin": {
"d1": "1",
"d2": "0",
"d3": "1"
},
"analog_pin": {
"a0": "500"
}
}
以上JSON信息包含有三个数据,第一个数据”info”对应的值是一个包含有三个数据的对象。这三个数据值都是字符串格式。他们在整个程序运行中是保持不变的。第二个数据”digital_pin”所对应的值是一个含有三个数据的对象,这三个数据是ESP8266开发板的D1、D2、D3引脚的实时电平状态。其中D3引脚的状态正是NodeMCU开发板上按键的引脚状态。我们通过按下该按键,可以改变D3引脚电平状态。第三个数据”analog_pin”对应的值是一个含有一个数据的对象。该数据是ESP8266的模拟输入引脚实时读数。换句话说, “digital_pin”和”analog_pin”所对应的数据值都是ESP8266引脚的实时状态,这些信息是会改变的。
#include <ESP8266WiFi.h> // 本程序使用 ESP8266WiFi库
#include <ESP8266WiFiMulti.h> // ESP8266WiFiMulti库
#include <ESP8266WebServer.h> // ESP8266WebServer库
#define buttonPin D3 // 按钮引脚D3
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80);// 建立网络服务器对象,该对象用于响应HTTP请求。监听端口(80)
IPAddress local_IP(192, 168, 31, 144); // 设置ESP8266-NodeMCU联网后的IP
IPAddress gateway(192, 168, 31, 1); // 设置网关IP(通常网关IP是WiFI路由IP)
IPAddress subnet(255, 255, 255, 0); // 设置子网掩码
IPAddress dns(192,168,31,1);
void setup(){
Serial.begin(9600); // 启动串口通讯
Serial.println("");
// 将引脚设置为输入上拉模式
pinMode(D1, INPUT_PULLUP);
pinMode(D2, INPUT_PULLUP);
pinMode(buttonPin, INPUT_PULLUP); // NodeMCU开发板按键连接在D3引脚上
// 设置开发板网络环境
if (!WiFi.config(local_IP, gateway, subnet)) {
Serial.println("Failed to Config ESP8266 IP");
}
//通过addAp函数存储 WiFi名称 WiFi密码
wifiMulti.addAP("yang1234", "y123456789"); // 这三条语句通过调用函数addAP来记录3个不同的WiFi网络信息。
wifiMulti.addAP("taichi-maker2", "87654321"); // 这3个WiFi网络名称分别是taichi-maker, taichi-maker2, taichi-maker3。
wifiMulti.addAP("taichi-maker3", "13572468"); // 这3个网络的密码分别是123456789,87654321,13572468。
// 此处WiFi信息只是示例,请在使用时将需要连接的WiFi信息填入相应位置。
// 另外这里只存储了3个WiFi信息,您可以存储更多的WiFi信息在此处。
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { // 此处的wifiMulti.run()是重点。通过wifiMulti.run(),NodeMCU将会在当前
delay(1000); // 环境中搜索addAP函数所存储的WiFi。如果搜到多个存储的WiFi那么NodeMCU
Serial.print(i++); Serial.print(' '); // 将会连接信号最强的那一个WiFi信号。
} // 一旦连接WiFI成功,wifiMulti.run()将会返回“WL_CONNECTED”。这也是
// 此处while循环判断是否跳出循环的条件。
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println('\n'); // WiFi连接成功后
Serial.print("Connected to "); // NodeMCU将通过串口监视器输出。
Serial.println(WiFi.SSID()); // 连接的WiFI名称
Serial.print("IP address:\t"); // 以及
Serial.println(WiFi.localIP()); // NodeMCU的IP地址
esp8266_server.on("/", handleRoot);
esp8266_server.begin();
Serial.println("HTTP esp8266_server started");// 告知用户ESP8266网络服务功能已经启动
}
void loop(){
// 处理http服务器访问
esp8266_server.handleClient();
}
void handleRoot() { //处理网站目录“/”的访问请求
esp8266_server.send(200, "application/json", rootJson());
}
// 实时获取ESP8266开发板引脚信息并且建立JSON信息
// 以便ESP8266服务器通过响应信息发送给客户端
String rootJson(){
String jsonCode = "{\"info\": {\"name\": \"taichimaker\",\"url\": \"www.taichi-maker.com\",\"email\": \"taichimaker@163.com\"},\"digital_pin\": {\"d1\": \"";
jsonCode += String(digitalRead(D1));
jsonCode += "\",\"d2\": \"";
jsonCode += String(digitalRead(D2));
jsonCode += "\",\"d3\": \"";
jsonCode += String(digitalRead(D3));
jsonCode += "\"},\"analog_pin\": {\"a0\": \"";
jsonCode += String(analogRead(A0));
jsonCode += "\"}}";
Serial.print("jsonCode: ");Serial.println(jsonCode);
return jsonCode;
}
以上程序中最重点的部分是函数httpRequest。该函数向服务器发送HTTP请求,并且对服务器相应的JSON信息进行了解析。解析后的数据信息将通过串口监视器显示,其中服务器按键引脚的状态信息还被用于控制客户端板上的LED点亮和熄灭。
示例二 ESP8266客户端请求多种JSON数据信息
在以上示例程序中,服务器响应的信息形式只有一种,也就是将所有JSON信息全部响应给客户端。然而在我们实际开发物联网项目过程中,可能客户端只需要服务器JSON信息中的的某一个或某几个信息。这种情况下,如果服务器总是把所有信息都发送给客户端,这一操作会产生网络资源和系统运算资源的浪费。
接下来的示例程序中,客户端可以有选择性地向服务器请求信息内容。服务器端也会在接收到客户端请求后,根据客户端的需求来选择性的发送服务器信息。当然了,这些信息同样使用了JSON格式来传输。
服务器端程序
#include <ESP8266WiFi.h> // 本程序使用 ESP8266WiFi库
#include <ESP8266WiFiMulti.h> // ESP8266WiFiMulti库
#include <ESP8266WebServer.h> // ESP8266WebServer库
#include <ArduinoJson.h> // ArduinoJson库
#define buttonPin D3 // 按钮引脚D3
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80);// 建立网络服务器对象,该对象用于响应HTTP请求。监听端口(80)
IPAddress local_IP(192, 168, 31, 144); // 设置ESP8266-NodeMCU联网后的IP
IPAddress gateway(192, 168, 31, 1); // 设置网关IP(通常网关IP是WiFI路由IP)
IPAddress subnet(255, 255, 255, 0); // 设置子网掩码
IPAddress dns(192,168,31,1); // 设置局域网DNS的IP(通常局域网DNS的IP是WiFI路由IP)
void setup(){
Serial.begin(9600); // 启动串口通讯
Serial.println("");
// 将引脚设置为输入上拉模式
pinMode(D1, INPUT_PULLUP);
pinMode(D2, INPUT_PULLUP);
pinMode(buttonPin, INPUT_PULLUP); // NodeMCU开发板按键连接在D3引脚上
// 设置开发板网络环境
if (!WiFi.config(local_IP, gateway, subnet)) {
Serial.println("Failed to Config ESP8266 IP");
}
//通过addAp函数存储 WiFi名称 WiFi密码
wifiMulti.addAP("yang1234", "y123456789"); // 这三条语句通过调用函数addAP来记录3个不同的WiFi网络信息。
wifiMulti.addAP("taichi-maker2", "87654321"); // 这3个WiFi网络名称分别是taichi-maker, taichi-maker2, taichi-maker3。
wifiMulti.addAP("taichi-maker3", "13572468"); // 这3个网络的密码分别是123456789,87654321,13572468。
// 此处WiFi信息只是示例,请在使用时将需要连接的WiFi信息填入相应位置。
// 另外这里只存储了3个WiFi信息,您可以存储更多的WiFi信息在此处。
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { // 此处的wifiMulti.run()是重点。通过wifiMulti.run(),NodeMCU将会在当前
delay(1000); // 环境中搜索addAP函数所存储的WiFi。如果搜到多个存储的WiFi那么NodeMCU
Serial.print(i++); Serial.print(' '); // 将会连接信号最强的那一个WiFi信号。
} // 一旦连接WiFI成功,wifiMulti.run()将会返回“WL_CONNECTED”。这也是
// 此处while循环判断是否跳出循环的条件。
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println('\n'); // WiFi连接成功后
Serial.print("Connected to "); // NodeMCU将通过串口监视器输出。
Serial.println(WiFi.SSID()); // 连接的WiFI名称
Serial.print("IP address:\t"); // 以及
Serial.println(WiFi.localIP()); // NodeMCU的IP地址
// 重点一: 建立回调函数以满足客户端的不同请求
esp8266_server.begin();
esp8266_server.on("/", handleRoot);
esp8266_server.on("/info", handleInfo);
esp8266_server.on("/digital_pin", handleDigitalPin);
Serial.println("HTTP esp8266_server started");// 告知用户ESP8266网络服务功能已经启动
}
void loop(){
// 处理http服务器访问
esp8266_server.handleClient();
}
void handleRoot() { //处理网站目录“/”的访问请求
esp8266_server.send(200, "application/json", rootJson());
}
void handleInfo() { //处理网站目录“/info”的访问请求
esp8266_server.send(200, "application/json", infoJson());
}
void handleDigitalPin() { //处理网站目录“/digital_pin”的访问请求
esp8266_server.send(200, "application/json", digitalPinJson());
}
// 实时获取ESP8266开发板引脚信息并且建立JSON信息
// 以便ESP8266服务器通过响应信息发送给客户端
String rootJson(){
// 开始ArduinoJson Assistant的serialize代码
const size_t capacity = JSON_OBJECT_SIZE(1) + 3*JSON_OBJECT_SIZE(3);
DynamicJsonDocument doc(capacity);
JsonObject info = doc.createNestedObject("info");
info["name"] = "taichimaker";
info["url"] = "www.taichi-maker.com";
info["email"] = "taichimaker@163.com";
JsonObject digital_pin = doc.createNestedObject("digital_pin");
digital_pin["d1"] = String(digitalRead(D1));
digital_pin["d2"] = String(digitalRead(D2));
digital_pin["d3"] = String(digitalRead(D3));
JsonObject analog_pin = doc.createNestedObject("analog_pin");
analog_pin["a0"] = String(analogRead(A0));
// 结束assistant的serialize代码
String jsonCode;
serializeJson(doc, jsonCode);
Serial.print("Root Json Code: ");Serial.println(jsonCode);
return jsonCode;
}
//建立infoJson信息
String infoJson(){
// 开始ArduinoJson Assistant的serialize代码
const size_t capacity = JSON_OBJECT_SIZE(1) + 3*JSON_OBJECT_SIZE(3)+140;
DynamicJsonDocument doc(capacity);
JsonObject info = doc.createNestedObject("info");
info["name"] = "taichimaker";
info["url"] = "www.taichi-maker.com";
info["email"] = "taichimaker@163.com";
// 结束assistant的serialize代码
String jsonCode;
serializeJson(doc, jsonCode);
Serial.print("info Json Code: ");Serial.println(jsonCode);
return jsonCode;
}
//建立digitalPinJson信息
String digitalPinJson(){
// 开始ArduinoJson Assistant的serialize代码
const size_t capacity = JSON_OBJECT_SIZE(1) + 3*JSON_OBJECT_SIZE(3)+140;
DynamicJsonDocument doc(capacity);
JsonObject digital_pin = doc.createNestedObject("digital_pin");
digital_pin["d1"] = String(digitalRead(D1));
digital_pin["d2"] = String(digitalRead(D2));
digital_pin["d3"] = String(digitalRead(D3));
String jsonCode;
serializeJson(doc, jsonCode);
Serial.print("root json Code: ");Serial.println(jsonCode);
return jsonCode;
}
以上示例程序中,我们建立了一系列回调函数handleRoot、handleInfo、handleDigitalPin。这些回调函数会针对客户端的请求来发送不同的JSON响应信息。这一点与之前的服务器端示例程序有所区别。另外请留意,我们在建立JSON响应信息时,使用了ArduinoJson库的createNestedObject函数以及serializeJson函数来实现。
客户端程序
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象
const char* host = "192.168.31.144"; // 将要连接的服务器地址
const int httpPort = 80; // 将要连接的服务器端口
void setup(){
Serial.begin(9600);
Serial.println("");
// 设置开发板LED引脚
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
wifiMulti.addAP("ssid_from_AP_1", "your_password_for_AP_1"); // 将需要连接的一系列WiFi ID和密码输入这里
wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2"); // ESP8266-NodeMCU再启动后会扫描当前网络
wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3"); // 环境查找是否有这里列出的WiFi ID。如果有
Serial.println("Connecting ...");
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { // 尝试进行wifi连接。
delay(1000);
Serial.print(i++); Serial.print(' ');
}
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println("");
Serial.print("Connected to ");
Serial.println(WiFi.SSID()); // WiFi名称
Serial.print("IP address:\t");
Serial.println(WiFi.localIP()); // IP
}
void loop(){
httpRequest();
delay(3000);
}
// 向服务器请求信息并对信息进行解析
void httpRequest(){
WiFiClient client;
String httpRequest = String("GET /") + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n";
Serial.print("Connecting to "); Serial.print(host);
if (client.connect(host, 80)){
Serial.println(" Success!");
// 向服务器发送http请求信息
client.print(httpRequest);
Serial.println("Sending request: ");
Serial.println(httpRequest);
// 获取并显示服务器响应状态行
String status_response = client.readStringUntil('\n');
Serial.print("status_response: ");
Serial.println(status_response);
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
parseInfo(client);
}
else {
Serial.println(" connection failed!");
}
//断开客户端与服务器连接工作
client.stop();
}
void parseInfo(WiFiClient client){
const size_t capacity = JSON_OBJECT_SIZE(1) + 3*JSON_OBJECT_SIZE(3) + 140;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, client);
JsonObject info = doc["info"];
const char* info_name = info["name"]; // "taichimaker"
const char* info_url = info["url"]; // "www.taichi-maker.com"
const char* info_email = info["email"]; // "taichimaker@163.com"
JsonObject digital_pin = doc["digital_pin"];
const char* digital_pin_d1 = digital_pin["d1"]; // "1"
const char* digital_pin_d2 = digital_pin["d2"]; // "0"
const char* digital_pin_d3 = digital_pin["d3"]; // "1"
const char* analog_pin_a0 = doc["analog_pin"]["a0"]; // "500"
String info_name_str = info["name"].as<String>();
bool d3_bool = digital_pin["d3"].as<int>();
Serial.print("info_name_str = ");Serial.println(info_name_str);
Serial.print("d3_bool = ");Serial.println(d3_bool);
d3_bool == 0 ? digitalWrite (LED_BUILTIN, LOW) : digitalWrite(LED_BUILTIN, HIGH);
}
我们通过使用不同参数来调用以上示例程序中的httpRequest函数,从而实现客户端向服务器发送不同JSON请求。在解析服务器JSON响应信息时,我们使用逻辑判断语句来检查服务器响应JSON信息中是否包含有所需的信息内容。如果包含所需信息,则进一步操作来获取信息内容。否则,ESP8266将会通过串口监视器告诉我用户,服务器的响应JSON中,没有指定的内容。
1.4.4 ESP8266客户端发送JSON信息
在上一节中,我们学习了使用ESP8266客户端向服务器请求JSON信息。在这一过程中,客户端所获取的JSON信息是来自于服务器的。但是在开发物联网项目时,我们可能会遇到使用客户端向服务器发送JSON信息的情况。在这节教程中,我们将一起学习这一操作。
在接下来的讲解中,我们同样需要两块ESP8266-NodeMCU开发板。其中一块作为服务器,另一块作为客户端。如下图所示,客户端将会向服务器发送请求信息。在客户端的请求信息中将会包含JSON信息。服务器接收到请求信息后,会从请求信息中获取JSON信息,并且解析该JSON中的内容。
ESP8266-Client-Sends-Json
注意,以下示例中的服务器端和客户端ESP8266必须连接同一WiFi网络,方可实现数据通讯。
示例一 ESP8266客户端发送单一JSON数据信息
本示例程序分为两部分,一部分为服务器程序,另一部分为客户端程序。
客户端程序
客户端程序主要功能:
1. 实时读取A0、 D1、D2以及D3引脚的读数。
2. 向服务器发送Json信息。发送的信息中包含有D3引脚状态从而控制服务器开发板上
的LED点亮或熄灭。以下为该json信息的示例:
{
"info": {
"name": "taichimaker",
"url": "www.taichi-maker.com",
"email": "taichimaker@163.com"
},
"digital_pin": {
"d1": "1",
"d2": "0",
"d3": "1"
},
"analog_pin": {
"a0": "500"
}
}
以上JSON信息与上一节使用的信息相同。这里就不再赘述它的结构了。
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
ESP8266WiFiMulti wifiMulti; // 建立ESP8266WiFiMulti对象
const char* host = "192.168.31.144"; // 网络服务器地址
const int httpPort = 80; // http端口80
void setup(){
Serial.begin(9600);
Serial.println("");
//通过addAp函数存储 WiFi名称 WiFi密码
wifiMulti.addAP("yang1234", "y123456789"); // 这三条语句通过调用函数addAP来记录3个不同的WiFi网络信息。
wifiMulti.addAP("taichi-maker2", "87654321"); // 这3个WiFi网络名称分别是taichi-maker, taichi-maker2, taichi-maker3。
wifiMulti.addAP("taichi-maker3", "13572468"); // 这3个网络的密码分别是123456789,87654321,13572468。
// 此处WiFi信息只是示例,请在使用时将需要连接的WiFi信息填入相应位置。
// 另外这里只存储了3个WiFi信息,您可以存储更多的WiFi信息在此处。
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { // 此处的wifiMulti.run()是重点。通过wifiMulti.run(),NodeMCU将会在当前
delay(1000); // 环境中搜索addAP函数所存储的WiFi。如果搜到多个存储的WiFi那么NodeMCU
Serial.print(i++); Serial.print(' '); // 将会连接信号最强的那一个WiFi信号。
} // 一旦连接WiFI成功,wifiMulti.run()将会返回“WL_CONNECTED”。这也是
// 此处while循环判断是否跳出循环的条件。
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println('\n'); // WiFi连接成功后
Serial.print("Connected to "); // NodeMCU将通过串口监视器输出。
Serial.println(WiFi.SSID()); // 连接的WiFI名称
Serial.print("IP address:\t"); // 以及
Serial.println(WiFi.localIP()); // NodeMCU的IP地址
}
void loop(){
// 发送HTTP请求
httpRequest();
delay(3000);
}
// 向服务器发送HTTP请求,请求信息中包含json信息
void httpRequest(){
// 建立WiFi客户端对象,对象名称client
WiFiClient client;
// 重点1: 建立JSON,此JSON包含需要发送的信息
String payloadJson = "{\"info\": {\"name\": \"taichimaker\",\"url\": \"www.taichi-maker.com\",\"email\": \"taichimaker@163.com\"},\"digital_pin\": {\"d1\": \"";
payloadJson += String(digitalRead(D1));
payloadJson += "\",\"d2\": \"";
payloadJson += String(digitalRead(D2));
payloadJson += "\",\"d3\": \"";
payloadJson += String(digitalRead(D3));
payloadJson += "\"},\"analog_pin\": {\"a0\": \"";
payloadJson += String(analogRead(A0));
payloadJson += "\"}}";
// 建立字符串,用于HTTP请求
String httpRequest = String("GET /") + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n" + payloadJson;
// 通过串口输出连接服务器名称以便查阅连接服务器的网址
Serial.print("Connecting to ");
Serial.print(host);
if (client.connect(host, httpPort)){
Serial.println(" Success!"); // 连接成功后串口输出“Success”信息
client.print(httpRequest); // 向服务器发送请求
Serial.println("Sending request: "); // 通过串口输出HTTP请求信息内容以便查阅
Serial.println(httpRequest);
Serial.println("Web Server Response:"); // 通过串口监视输出服务器响应信息
while (client.connected() || client.available()){
if (client.available()){
String line = client.readStringUntil('\n');
Serial.println(line);
}
}
} else{ // 如果连接不成功则通过串口输出“连接失败”信息
Serial.println(" failed!");
}
client.stop(); // 断开与服务器的连接
Serial.print("Disconnected from "); // 并且通过串口输出断开连接信息
Serial.println(host);
}
重点部分讲解:
请留意以上程序高亮部分。这里我们实时读取开发板各个引脚状态并且将这些引脚读数放在payloadJson字符串中,然后将其放入httpRequest字符串中发送给服务器。
服务器程序
在开始学习服务器端程序以前,我们首先了解一下WiFiServer库的基本操作。以下示例程序将会利用WiFiServer库建立ESP8266网络服务器。我们可以通过网页浏览器客户端访问该服务器。客户端的请求信息将会通过ESP8266的串口输出以便我们观察程序 运行状况。
#include <ESP8266WiFi.h>
const char* ssid = "yang1234";
const char* password = "y123456789";
WiFiServer server(80);
void setup() {
Serial.begin(9600);
Serial.println();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(F("."));
}
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println('\n'); // WiFi连接成功后
Serial.print("Connected to "); // NodeMCU将通过串口监视器输出。
Serial.println(WiFi.SSID()); // 连接的WiFI名称
Serial.print("IP address:\t"); // 以及
Serial.println(WiFi.localIP()); // NodeMCU的IP地址
// 启动服务器
server.begin();
}
void loop() {
runServer(); // 运行服务器
}
void runServer(){
// 建立WiFiClient对象用于处理客户端请求信息
WiFiClient incomingClient = server.available();
// 如果没有客户端请求信息,则“跳过”函数中后续程序内容
if (!incomingClient) {
return;
}
Serial.println("====Client Connected===");
// 通过串口监视器输出客户端请求信息
String clientRequest = incomingClient.readString();
Serial.print(clientRequest);
// 建立服务器响应信息
String httpResponse =
"HTTP/1.0 200 OK\r\n"
"Connection: close\r\n"
"Content-Type: text/plain;\r\n"
"\r\n"
"client_request_received";
// 向客户端发送以上服务器响应信息
incomingClient.print(httpResponse);
incomingClient.stop();
Serial.println("incomingClient stop");
}
1.5 ESP8266客户端HTTP API应用实例
1.5.1 ESP8266心知天气使用教程
注册账号获取免费api得到私钥
示例1. 获取实时天气信息(温度,天气)
示例2. 获取天气预报信息(温度,天气,降水概率,风力,风向,湿度)
示例3. 获取实时生活指数(穿衣,紫外线强度,洗车,旅游,感冒,运动)
我们编写了专门用于ESP8266利用心知天气API获取信息的的第三方库。通过该库,您可以使用很简单的几行代码就能实现本页代码的所有功能。如需了解更多信息,请点击这里进入ESP8266-心知天气库使用说明页面。
这三段示例程序都是基于零基础入门学用物联网 – 基础知识篇3-4-1 ESP8266网络客户端基本操作示例程序2所建立的。
这三段示例程序最大的区别是httpRequest函数中ESP8266所发送的http请求信息不同。建立这些HTTP请求信息的依据是心知天气API数据调用规范。假如您想了解具体内容,可参考心知天气API接口文档。
示例1. 获取实时天气信息(温度,天气)
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
const char* ssid = "yang1234"; // 连接WiFi名(此处使用taichi-maker为示例)
// 请将您需要连接的WiFi名填入引号中
const char* password = "y123456789"; // 连接WiFi密码(此处使用12345678为示例)
// 请将您需要连接的WiFi密码填入引号中
const char* host = "api.seniverse.com"; // 将要连接的服务器地址
const int httpPort = 80; // 将要连接的服务器端口
// 心知天气HTTP请求所需信息
String reqUserKey = "xxxxx"; // 私钥
String reqLocation = "Zhumadian"; // 城市
String reqUnit = "c"; // 摄氏/华氏
void setup(){
Serial.begin(9600);
Serial.println("");
// 连接WiFi
connectWiFi();
}
void loop(){
// 建立心知天气API当前天气请求资源地址
String reqRes = "/v3/weather/now.json?key=" + reqUserKey +
+ "&location=" + reqLocation +
"&language=en&unit=" +reqUnit;
// 向心知天气服务器服务器请求信息并对信息进行解析
httpRequest(reqRes);
delay(3000);
}
// 向心知天气服务器服务器请求信息并对信息进行解析
void httpRequest(String reqRes){
WiFiClient client;
// 建立http请求信息
String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n";
Serial.println("");
Serial.print("Connecting to "); Serial.print(host);
// 尝试连接服务器
if (client.connect(host, 80)){
Serial.println(" Success!");
// 向服务器发送http请求信息
client.print(httpRequest);
Serial.println("Sending request: ");
Serial.println(httpRequest);
// 获取并显示服务器响应状态行
String status_response = client.readStringUntil('\n');
Serial.print("status_response: ");
Serial.println(status_response);
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
// 利用ArduinoJson库解析心知天气响应信息
parseInfo(client);
} else {
Serial.println(" connection failed!");
}
//断开客户端与服务器连接工作
client.stop();
}
// 连接WiFi
void connectWiFi(){
WiFi.begin(ssid, password); // 启动网络连接
Serial.print("Connecting to "); // 串口监视器输出网络连接信息
Serial.print(ssid); Serial.println(" ..."); // 告知用户NodeMCU正在尝试WiFi连接
int i = 0; // 这一段程序语句用于检查WiFi是否连接成功
while (WiFi.status() != WL_CONNECTED) { // WiFi.status()函数的返回值是由NodeMCU的WiFi连接状态所决定的。
delay(1000); // 如果WiFi连接成功则返回值为WL_CONNECTED
Serial.print(i++); Serial.print(' '); // 此处通过While循环让NodeMCU每隔一秒钟检查一次WiFi.status()函数返回值
} // 同时NodeMCU将通过串口监视器输出连接时长读秒。
// 这个读秒是通过变量i每隔一秒自加1来实现的。
Serial.println(""); // WiFi连接成功后
Serial.println("Connection established!"); // NodeMCU将通过串口监视器输出"连接成功"信息。
Serial.print("IP address: "); // 同时还将输出NodeMCU的IP地址。这一功能是通过调用
Serial.println(WiFi.localIP()); // WiFi.localIP()函数来实现的。该函数的返回值即NodeMCU的IP地址。
}
// 利用ArduinoJson库解析心知天气响应信息
void parseInfo(WiFiClient client){
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 230;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, client);
JsonObject results_0 = doc["results"][0];
JsonObject results_0_now = results_0["now"];
const char* results_0_now_text = results_0_now["text"]; // "Sunny"
const char* results_0_now_code = results_0_now["code"]; // "0"
const char* results_0_now_temperature = results_0_now["temperature"]; // "32"
const char* results_0_last_update = results_0["last_update"]; // "2020-06-02T14:40:00+08:00"
// 通过串口监视器显示以上信息
String results_0_now_text_str = results_0_now["text"].as<String>();
int results_0_now_code_int = results_0_now["code"].as<int>();
int results_0_now_temperature_int = results_0_now["temperature"].as<int>();
String results_0_last_update_str = results_0["last_update"].as<String>();
Serial.println(F("======Weahter Now======="));
Serial.print(F("Weather Now: "));
Serial.print(results_0_now_text_str);
Serial.print(F(" "));
Serial.println(results_0_now_code_int);
Serial.print(F("Temperature: "));
Serial.println(results_0_now_temperature_int);
Serial.print(F("Last Update: "));
Serial.println(results_0_last_update_str);
Serial.println(F("========================"));
}
1
Connection established!
IP address: 192.168.31.144
Connecting to api.seniverse.com:ref 1
Success!
:wr 141 0
:wrc 141 141 0
Sending request:
GET /v3/weather/now.json?key=SfxtbZWbKra1Om1yh&location=Zhumadian&language=en&unit=c HTTP/1.1
Host: api.seniverse.com
Connection: close
:ack 141
:rn 536
:rch 536, 136
:rcl pb=0x3ffefc94 sz=672
status_response: HTTP/1.1 200 OK
Found Header End. Start Parsing.
:ref 2
:c 1, 536, 672
:c0 1, 136
======Weahter Now=======
Weather Now: Overcast 9
Temperature: 2
Last Update: 2021-01-10T18:00:00+08:00
========================
:ur 2
:close
:ur 1
:dsrcv 0
:del
Connecting to api.seniverse.com:ref 1
Success!
:wr 141 0
:wrc 141 141 0
Sending request:
GET /v3/weather/now.json?key=SfxtbZWbKra1Om1yh&location=Zhumadian&language=en&unit=c HTTP/1.1
Host: api.seniverse.com
Connection: close
:ack 141
:rn 536
:rch 536, 136
:rcl pb=0x3ffefcbc sz=672
status_response: HTTP/1.1 200 OK
Found Header End. Start Parsing.
:ref 2
:c 1, 536, 672
:c0 1, 136
获取3天的天气
把json复制到https://arduinojson.org/v6/assistant/
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
const char* ssid = "yang1234"; // 连接WiFi名(此处使用taichi-maker为示例)
// 请将您需要连接的WiFi名填入引号中
const char* password = "y123456789"; // 连接WiFi密码(此处使用12345678为示例)
// 请将您需要连接的WiFi密码填入引号中
const char* host = "api.seniverse.com"; // 将要连接的服务器地址
const int httpPort = 80; // 将要连接的服务器端口
// 心知天气HTTP请求所需信息
String reqUserKey = "xxxxxx"; // 私钥
String reqLocation = "Zhumadian"; // 城市
String reqUnit = "c&start=0&days=3"; // 摄氏/华氏
void setup(){
Serial.begin(9600);
Serial.println("");
// 连接WiFi
connectWiFi();
}
void loop(){
// 建立心知天气API当前天气请求资源地址
String reqRes = "/v3/weather/daily.json?key=" + reqUserKey +
+ "&location=" + reqLocation +
"&language=en&unit=" +reqUnit;
// 向心知天气服务器服务器请求信息并对信息进行解析
httpRequest(reqRes);
delay(3000);
}
// 向心知天气服务器服务器请求信息并对信息进行解析
void httpRequest(String reqRes){
WiFiClient client;
// 建立http请求信息
String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n";
Serial.println("");
Serial.print("Connecting to "); Serial.print(host);
// 尝试连接服务器
if (client.connect(host, 80)){
Serial.println(" Success!");
// 向服务器发送http请求信息
client.print(httpRequest);
Serial.println("Sending request: ");
Serial.println(httpRequest);
// 获取并显示服务器响应状态行
String status_response = client.readStringUntil('\n');
Serial.print("status_response: ");
Serial.println(status_response);
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
// 利用ArduinoJson库解析心知天气响应信息
// String serverJson = client.readString();
// Serial.println(serverJson);
parseInfo(client);
} else {
Serial.println(" connection failed!");
}
//断开客户端与服务器连接工作
client.stop();
}
// 连接WiFi
void connectWiFi(){
WiFi.begin(ssid, password); // 启动网络连接
Serial.print("Connecting to "); // 串口监视器输出网络连接信息
Serial.print(ssid); Serial.println(" ..."); // 告知用户NodeMCU正在尝试WiFi连接
int i = 0; // 这一段程序语句用于检查WiFi是否连接成功
while (WiFi.status() != WL_CONNECTED) { // WiFi.status()函数的返回值是由NodeMCU的WiFi连接状态所决定的。
delay(1000); // 如果WiFi连接成功则返回值为WL_CONNECTED
Serial.print(i++); Serial.print(' '); // 此处通过While循环让NodeMCU每隔一秒钟检查一次WiFi.status()函数返回值
} // 同时NodeMCU将通过串口监视器输出连接时长读秒。
// 这个读秒是通过变量i每隔一秒自加1来实现的。
Serial.println(""); // WiFi连接成功后
Serial.println("Connection established!"); // NodeMCU将通过串口监视器输出"连接成功"信息。
Serial.print("IP address: "); // 同时还将输出NodeMCU的IP地址。这一功能是通过调用
Serial.println(WiFi.localIP()); // WiFi.localIP()函数来实现的。该函数的返回值即NodeMCU的IP地址。
}
// 利用ArduinoJson库解析心知天气响应信息
void parseInfo(WiFiClient client){
StaticJsonDocument<0> filter;
filter.set(true);
DynamicJsonDocument doc(1536);
deserializeJson(doc, client, DeserializationOption::Filter(filter));
JsonObject results_0 = doc["results"][0];
JsonObject results_0_location = results_0["location"];
const char* results_0_location_id = results_0_location["id"]; // "WTC5DEFF6TD5"
const char* results_0_location_name = results_0_location["name"]; // "Zhumadian"
const char* results_0_location_country = results_0_location["country"]; // "CN"
const char* results_0_location_path = results_0_location["path"]; // "Zhumadian,Zhumadian,Henan,China"
const char* results_0_location_timezone = results_0_location["timezone"]; // "Asia/Shanghai"
const char* results_0_location_timezone_offset = results_0_location["timezone_offset"]; // "+08:00"
for (JsonObject elem : results_0["daily"].as<JsonArray>()) {
const char* date = elem["date"]; // "2021-01-10"
const char* text_day = elem["text_day"]; // "Overcast"
const char* code_day = elem["code_day"]; // "9"
const char* text_night = elem["text_night"]; // "Overcast"
const char* code_night = elem["code_night"]; // "9"
const char* high = elem["high"]; // "4"
const char* low = elem["low"]; // "-3"
const char* rainfall = elem["rainfall"]; // "0.0"
const char* precip = elem["precip"]; // ""
const char* wind_direction = elem["wind_direction"]; // "NW"
const char* wind_direction_degree = elem["wind_direction_degree"]; // "315"
const char* wind_speed = elem["wind_speed"]; // "23.4"
const char* wind_scale = elem["wind_scale"]; // "4"
const char* humidity = elem["humidity"]; // "35"
String text_day_w =elem["text_day"].as<String>();
Serial.print("text_day_w=");Serial.println(text_day_w);
String code_day_w =elem["code_day"].as<String>();
Serial.print("code_day_w=");Serial.println(code_day_w);
}
const char* results_0_last_update = results_0["last_update"]; // "2021-01-10T18:00:00+08:00"
}
1 2 3 4 5
Connection established!
IP address: 192.168.31.144Connecting to api.seniverse.com:ref 1
Success!
:wr 158 0
:wrc 158 158 0
Sending request:
GET /v3/weather/daily.json?key=SfxtbZWbKra1Om1yh&location=Zhumadian&language=en&unit=c&start=0&days=3 HTTP/1.1
Host: api.seniverse.com
Connection: close
:ack 158
:rn 536
:rch 536, 536
:rch 1072, 311
:rcl pb=0x3ffefe0c sz=1383
status_response: HTTP/1.1 200 OK
Found Header End. Start Parsing.
:ref 2
:c 1, 536, 1383
:c 1, 536, 847
:c0 1, 311
text_day_w=Overcast
code_day_w=9
text_day_w=Sunny
code_day_w=0
text_day_w=Sunny
code_day_w=0
:ur 2
:close
:ur 1
:dsrcv 0
:del
在我们开发物联网项目时,经常需要为ESP8266设置WiFi。在以往的课程内容里,我们的设置WiFi的方法是通过修改程序中的内容来实现的。
但是假如我们做好了物联网制作后送给朋友,而朋友不知道如何写ESP8266程序.这种情况下该如何来让不懂编程的朋友也能设置ESP8266的WiFi连接呢?
我们可以利用一款非常好用的ESP8266第三方库:WiFiManager库。以下是该库的基本信息:
作者:tzapu,tablatronix
GitHub:https://github.com/tzapu/WiFiManager
网盘下载:https://wwa.lanzous.com/ibBlTe9jaif
太极创客汉化版:https://github.com/taichi-maker/WiFiManager
或者啊ruino 库搜索WiFiManager在下面tzapu的
WiFiManager库使用说明
1 WiFi配置流程
ESP8266-WiFiManager-工作流程
2 WiFi配置示例程序
2.1 预备程序 – 清理ESP8266储存的WiFi连接信息
ESP8266的WiFi设置是储存在它的闪存系统中的。因此在启动ESP8266并连接WiFi时,它都会尝试使用闪存系统中储存的信息来进行WiFi连接。
在开始讲解如何使用WiFiManager库来配置ESP8266的WiFi设置前,我们需要首先清除ESP8266的WiFi连接信息,这样才能看到WiFiManager库的工作效果。(如果ESP8266刚一启动就自动成功连接WiFi了,那么WiFiManager库是不会发挥作用的。)
我们可以使用以下示例程序清除ESP8266的闪存中所存储的WiFi连接信息。
在此程序的第27行位置,使用了wifiManager.resetSettings()
来实现清除ESP8266的闪存中所存储的WiFi连接信息这一操作。
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
void setup() {
Serial.begin(9600);
// 建立WiFiManager对象
WiFiManager wifiManager;
// 清除ESP8266所存储的WiFi连接信息以便测试WiFiManager工作效果
wifiManager.resetSettings();
Serial.println("ESP8266 WiFi Settings Cleared");
}
void loop() {}
*WM: [1] resetSettings
*WM: [3] WiFi station enable
*WM: [3] enableSTA PERSISTENT ON
*WM: [1] SETTINGS ERASED
ESP8266 WiFi Settings Cleared
*WM: [3] unloading
以下示例程序使用了WiFiManager来实现WiFi网络配置。
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
void setup() {
Serial.begin(9600);
// 建立WiFiManager对象
WiFiManager wifiManager;
// 自动连接WiFi。以下语句的参数是连接ESP8266时的WiFi名称
wifiManager.autoConnect("AutoConnectAP");
// 如果您希望该WiFi添加密码,可以使用以下语句:
// wifiManager.autoConnect("AutoConnectAP", "12345678");
// 以上语句中的12345678是连接AutoConnectAP的密码
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println("");
Serial.print("ESP8266 Connected to ");
Serial.println(WiFi.SSID()); // WiFi名称
Serial.print("IP address:\t");
Serial.println(WiFi.localIP()); // IP
}
void loop() {}
如果开发板没报错路由WiFi,用手机连接AutoConnectAP自动跳到一个网页选择connect wifi,自动扫描wifi连接
假如您想要自己定制WiFiManager库的WiFi设置页面,可以修改库文件中的“WiFiManager.h”。该文件开始部分,有一系列字符串数组,如下所示:
const char HTTP_HEADER[] PROGMEM = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/><title>{v}</title>";
const char HTTP_STYLE[] PROGMEM = "<style>.c{text-align: center;} div,input{padding:5px;font-size:1em;} input{width:95%;} body{text-align: center;font-family:verdana;} button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;} .q{float: right;width: 64px;text-align: right;} .l{background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;}</style>";
const char HTTP_SCRIPT[] PROGMEM = "<script>function c(l){document.getElementById('s').value=l.innerText||l.textContent;document.getElementById('p').focus();}</script>";
const char HTTP_HEADER_END[] PROGMEM = "</head><body><div style='text-align:left;display:inline-block;min-width:260px;'>";
const char HTTP_PORTAL_OPTIONS[] PROGMEM = "<form action=\"/wifi\" method=\"get\"><button>Configure WiFi</button></form><br/><form action=\"/0wifi\" method=\"get\"><button>Configure WiFi (No Scan)</button></form><br/><form action=\"/i\" method=\"get\"><button>Info</button></form><br/><form action=\"/r\" method=\"post\"><button>Reset</button></form>";
const char HTTP_ITEM[] PROGMEM = "<div><a href='#p' onclick='c(this)'>{v}</a> <span class='q {i}'>{r}%</span></div>";
const char HTTP_FORM_START[] PROGMEM = "<form method='get' action='wifisave'><input id='s' name='s' length=32 placeholder='SSID'><br/><input id='p' name='p' length=64 type='password' placeholder='password'><br/>";
const char HTTP_FORM_PARAM[] PROGMEM = "<br/><input id='{i}' name='{n}' maxlength={l} placeholder='{p}' value='{v}' {c}>";
const char HTTP_FORM_END[] PROGMEM = "<br/><button type='submit'>save</button></form>";
const char HTTP_SCAN_LINK[] PROGMEM = "<br/><div class=\"c\"><a href=\"/wifi\">Scan</a></div>";
const char HTTP_SAVED[] PROGMEM = "<div>Credentials Saved<br />Trying to connect ESP to network.<br />If it fails reconnect to AP to try again</div>";
const char HTTP_END[] PROGMEM = "</div></body></html>";
以上这一系列文字信息定义了WiFi配置页面的内容。通过调整以上内容,我们可以修改Wifi配置页面的外观样式和信息。举例来说,假如您希望修改连接页面的按钮文字,那么可以将以上程序中const char HTTP_PORTAL_OPTIONS[]
修改为以下内容:
这一修改将会把WiFi配置页面的按键内容改为中文。也就是在这届内容前面部分您所看到的WiFi配置页面效果。
const char HTTP_PORTAL_OPTIONS[] PROGMEM = "<form action=\"/wifi\" method=\"get\"><button>配置WiFi</button></form><br/><form action=\"/0wifi\" method=\"get\"><button>配置WiFi(无扫描)</button></form><br/><form action=\"/i\" method=\"get\"><button>信息</button></form><br/><form action=\"/r\" method=\"post\"><button>复位</button></form>";
ESP8266在运行过程中,只能一条线式的依次执行任务。但是我们在开发物联网项目时,可能需要ESP8266在执行某一任务的过程中,还能处理其它任务。比如,我们使用ESP8266来控制电机运行的同时,还需要定时检查某一个引脚上连接按钮有没有被用户按下。
为了解决以上问题,我们可以使用Ticker库来解决这一问题。下面我们来通过一系列示例程序向您讲解Ticker库的使用方法。
示例1. Ticker库基本操作
利用Ticker库,我们可以让ESP8266定时调用某一个函数。通过以下示例程序我们可以看到,ESP8266将会每隔一秒钟通过串口监视器输出一次信息。我们是通过语句ticker.attach(1, sayHi)
来实现这一操作的。
该语句中的attach函数有两个参数。第一个参数可控制调用函数的时间间隔,单位是秒。这里的数字1说明ESP8266将会每隔一秒钟调用一次函数。那么具体调用哪一个函数呢?这个函数名称正是是通过第二个参数来限定的。也就是名称为sayHi的函数。该函数将会让ESP8266定时通过串口监视器输出一次信息。信息内容是“Hi”后面跟一个数值。这个数值是为了标注sayHi函数被调用了多少次。
#include <Ticker.h>
Ticker ticker;// 建立Ticker用于实现定时功能
int count; // 计数用变量
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
// 每隔一秒钟调用sayHi函数一次,attach函数的第一个参数
// 是控制定时间隔的变量。该参数的单位为秒。第二个参数是
// 定时执行的函数名称。
ticker.attach(1, sayHi);
}
void loop() {
// 用LED呼吸灯效果来演示在Tinker对象控制下,ESP8266可以定时
// 执行其它任务
for (int fadeValue = 0 ; fadeValue <= 1023; fadeValue += 5) {
analogWrite(LED_BUILTIN, fadeValue);
delay(10);
}
for (int fadeValue = 1023 ; fadeValue >= 0; fadeValue -= 5) {
analogWrite(LED_BUILTIN, fadeValue);
delay(10);
}
delay(3000);
}
// 在Tinker对象控制下,此函数将会定时执行。
void sayHi(){
count++;
Serial.print("Hi ");
Serial.println(count);
if(count >= 10){
ticker.detach();
Serial.println("停止多任务");
}
}
ticker.detach();为停止任务
示例2. 向定时调用函数传递参数
我们可以向Ticker库定时调用的函数来传递参数。不过要注意的是,传递参数的数量只能时一个。如下示例程序所示,语句ticker.attach(1, sayHi, 8)
有3个参数。其中第三个参数就是向定时调用的sayHi函数所传递的参数。
请注意:attach函数所能传递的参数最多只有一个。另外该参数仅能是以下类型中的一种:char, short, int, float, void*, char*。
#include <Ticker.h>
Ticker ticker;// 建立Ticker用于实现定时功能
int count; // 计数用变量
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
// 每隔一秒钟调用sayHi函数一次,attach函数的第一个参数
// 是控制定时间隔的变量。该参数的单位为秒。第二个参数是
// 定时执行的函数名称。
ticker.attach(1, sayHi, 10);
}
void loop() {
// 用LED呼吸灯效果来演示在Tinker对象控制下,ESP8266可以定时
// 执行其它任务
for (int fadeValue = 0 ; fadeValue <= 1023; fadeValue += 5) {
analogWrite(LED_BUILTIN, fadeValue);
delay(10);
}
for (int fadeValue = 1023 ; fadeValue >= 0; fadeValue -= 5) {
analogWrite(LED_BUILTIN, fadeValue);
delay(10);
}
delay(3000);
}
// 在Tinker对象控制下,此函数将会定时执行。
void sayHi(int hitimes){
count++;
Serial.print("Hi ");
Serial.println(count);
if(count >= hitimes){
ticker.detach();
Serial.println("停止多任务");
}
}
示例3. 利用多个Ticker对象让ESP8266处理多任务
我们可以建立多个Ticker对象。让多个Ticker对象来实现ESP8266的多任务处理。如下实例程序所示,我们通过语句Ticker buttonTicker;
来建立第二个Ticker对象。
接下来我们使用buttonTicker.attach_ms(100, buttonCheck)
来实现第二个Ticker对象的任务处理。这里我们使用了attach_ms函数,该函数与attach函数功能相似,唯一区别是。attach函数的时间单位是秒,而attach_ms的时间单位是毫秒。也就是说,这条语句将会让ESP8266每隔100毫秒执行一次buttonCheck函数。
#include <Ticker.h>
Ticker ticker;
Ticker buttonTicker;
int count;
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(D3, INPUT_PULLUP);
ticker.attach(1, sayHi, 60);
buttonTicker.attach_ms(100, buttonCheck);
}
void loop() {
for (int fadeValue = 0 ; fadeValue <= 1023; fadeValue += 5) {
analogWrite(LED_BUILTIN, fadeValue);
delay(10);
}
for (int fadeValue = 1023 ; fadeValue >= 0; fadeValue -= 5) {
analogWrite(LED_BUILTIN, fadeValue);
delay(10);
}
delay(3000);
}
void sayHi(int hiTimes){
count++;
Serial.print("Hi ");
Serial.println(count);
if (count >= hiTimes) {
ticker.detach();
Serial.print("ticker.detach();");
}
}
void buttonCheck(){
if (digitalRead(D3) == LOW){
Serial.println("D3 Button Pushed...");
}
}
示例4. 使用”计数器”来控制ESP8266定时执行较复杂的函数
Ticker定时调用的函数必须要“短小精悍”。比如以上一系列的示例程序中,我们仅仅让Ticker定时调用函数执行简单的串口数据输出,以及很基本的运算。事实上,在使用Ticker库时,定时调用函数必须要很快的执行完毕。否则会产生难以预料的问题。
这就产生了一个问题。假如我们需要ESP8266定时执行的操作较为复杂,这该如何是好呢?
假设我们让ESP8266定时向example.com网站服务器发送一个http请求,并且将服务器响应通过串口监视器显示出来。(如您对这一操作尚不了解,请参考《零基础入门学用物联网教程》第3-4-1节中的示例1。)
这个问题的答案就在以下示例程序中。在这段示例程序里,我们建立了一个计数变量count。在程序的第55到第59行中,Ticker定时调用的函数tickerCount仅仅对count变量进行自加。
当计数变量count达到我们需要的数值后,则让ESP8266执行较为复杂的函数。这一点是通过程序的第49到52行逻辑判断语句所实现的。
请注意,在每一次执行完操作后,我们都需要对count变量进行清零。否则程序将无法定时通过互联网向服务器发送请求。
#include <Ticker.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#define URL "http://www.example.com"
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "yang1234";
const char* password = "y123456789";
Ticker ticker;
int count;
void setup() {
Serial.begin(9600);
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
//连接WiFi
connectWifi();
ticker.attach(1, tickerCount);
}
void loop() {
if (count >= 5){
httpRequest();
count = 0;
}
}
void tickerCount(){
count++;
Serial.print("count = ");
Serial.println(count);
}
// 发送HTTP请求并且将服务器响应通过串口输出
void httpRequest(){
HTTPClient httpClient;
httpClient.begin(URL);
Serial.print("URL: "); Serial.println(URL);
int httpCode = httpClient.GET();
Serial.print("Send GET request to URL: ");
Serial.println(URL);
if (httpCode == HTTP_CODE_OK) {
// 使用getString函数获取服务器响应体内容
String responsePayload = httpClient.getString();
Serial.println("Server Response Payload: ");
Serial.println(responsePayload);
} else {
Serial.println("Server Respose Code:");
Serial.println(httpCode);
}
httpClient.end();
}
void connectWifi(){
//开始连接wifi
WiFi.begin(ssid, password);
//等待WiFi连接,连接成功打印IP
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.print("WiFi Connected!");
}
4.ESP8266 OTA 操作说明
所谓OTA,就是Over-The-Air的缩写。有人将其翻译为“空中下载”,也有翻译为“隔空传输”。无论如何翻译,对于ESP2866来说,通过OTA我们无需将ESP8266与电脑连接,而仅仅通过WiFi就可以用Arduino IDE向ESP8266上传程序。
1 通过数据线上传初始示例程序
首先,请将以下示例程序通过Arduino IDE上传到ESP8266
#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>
#include <Ticker.h>
// 闪烁时间间隔(秒)
const int blinkInterval = 2;
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "yang1234";
const char* password = "y123456789";
Ticker ticker;
void setup() {
Serial.begin(9600);
Serial.println("");
pinMode(LED_BUILTIN, OUTPUT);
ticker.attach(blinkInterval, tickerCount); // 设置Ticker对象
connectWifi();
// OTA设置并启动
ArduinoOTA.setHostname("ESP8266");
ArduinoOTA.setPassword("12345678");
ArduinoOTA.begin();
Serial.println("OTA ready");
}
void loop() {
ArduinoOTA.handle();
}
// 在Tinker对象控制下,此函数将会定时执行。
void tickerCount(){
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
void connectWifi(){
//开始连接wifi
WiFi.begin(ssid, password);
//等待WiFi连接,连接成功打印IP
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.print("IP address:\t");
Serial.println(WiFi.localIP());
}
按rest连接wifi
如果报错为开发板 NodeMCU 1.0 (ESP-12E Module) 编译时出错选择0.9的上传后再选1.0上传
然后充电宝连接开发板
程序上传后,请重新启动Arduino IDE。并且通过Arduino IDE正确选择ESP8266的OTA端口。
如下图所示:
2. 认证并上传程序
点击Arduino IDE的”上传”按钮后, IDE将会弹出对话框让用户输入OTA上传密码。请根据示例程序中的setPassword
函数所设置的信息来输入密码。完成密码输入后,点击确定。如果密码无误,您将看到程序开始上传。
程序上传结束后,ESP8266将会自动重启开发板,新的程序也将在重启后开始运行。
3. OTA的局限性
1. 程序占用空间变大
在OTA上传新程序过程中, ESP8266开发板将会保持旧程序的运行。这将导致ESP8266开发板的程序占用空间翻倍。假如您的程序非常复杂,占用空间很大,那么使用OTA上传就不太适合了。
2. Arduino IDE无法通过OTA端口与开发板进行串口通讯
当Arduino IDE的上传端口选为“网络端口”,Arduino IDE将无法获取ESP8266的串口通讯数据。不过ESP8266的串口通讯并不会因为OTA功能而受到影响。换句话说,您可以使用其它电脑串口通讯软件,如Putty等,来实现ESP8266与电脑之间的串口通讯。
3.使用OTA上传程序的电脑与ESP8266必须连接同一WiFi
若要使用OTA上传功能,那么电脑和ESP8266必须要在同一WiFi中,否则是无法实现OTA上传的。
本文作者: 永生
本文链接: https://yys.zone/detail/?id=188
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
评论列表 (0 条评论)