更新日志:

2023年3月22日

  • 增加OLED提示的显示代码
  • 修改注释
  • 精简代码

2023年3月20日

  • 改成使用WiFiManager,使用AP模式连接ESP8266,输入相关项目和选择连接WiFi

  • Blynk的可选填入项目:

    • 服务器URL
    • 服务器端口
    • 密钥
  • 加入基于IDE的OTA功能(准备弃用)

  • 精简部分代码


2023年3月19日

  • 重新上传

2023年3月19日

  • 重新上传

时效性以github版本为准

  这次来做一个新项目,之前做了很多基于Blynk的项目,可是这些项目都必须要开手机APP才能查看数值.有些朋友问,有没有可以不需要打开手机就能看的?方便家里人,不用再为每个家人的手机都装一个APP,我想了一下,知道Blynk提供了RESTful API,只要有个设备或者做一个网页对服务器发送请求就可以把项目数值显示出来了,所以方法是有的.这个项目如果你会做网页的话,也可以把它做成一个Web页面用浏览器打开,具体实现方式和思路如下:


点击图片放大

  1. 假设在广州拥有一个做好的项目并且已经能够公网访问.
  2. 例子中Blynk的服务器部署在广州,而我们在深圳准备一个ESP8266并写好代码用于发送请求和接收反馈(不需要再部署服务器或者客户端).
  3. Blynk服务器提供RESTful服务,深圳这边的ESP8266对其发送GET请求.
  4. 在广州这边,收到来自深圳的请求后,发出响应,并以JSON格式反馈回深圳的ESP8266.
  5. 深圳的ESP8266将JSON内容解析,并以串口方式显示(串口只是用于调试,本文接下来会以OLED的显示方式来显示所收到的数值).

简单科普

什么是RESTful API

  本项目将使用服务器提供的RESTful API来获取Blynk服务器的值,以下转自维基百科:
表现层状态转换(英语:Representational State Transfer,缩写:REST)是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息。表现层状态转换是根基于超文本传输协议(HTTP)之上而确定的一组约束和属性,是一种设计提供万维网络服务的软件构建风格。符合或兼容于这种架构风格(简称为 REST 或 RESTful)的网络服务,允许客户端发出以统一资源标识符访问和操作网络资源的请求,而与预先定义好的无状态操作集一致化。因此表现层状态转换提供了在互联网络的计算系统之间,彼此资源可交互使用的协作性质(interoperability)。相对于其它种类的网络服务,例如SOAP服务,则是以本身所定义的操作集,来访问网络上的资源。
  目前在三种主流的Web服务实现方案中,因为REST模式与复杂的SOAP和XML-RPC相比更加简洁,越来越多的Web服务开始采用REST风格设计和实现。例如,Amazon.com提供接近REST风格的Web服务运行图书查询;雅虎提供的Web服务也是REST风格的。
  应用于Web服务,符合REST设计风格的Web API称为RESTful API。它从以下三个方面资源进行定义:
  直观简短的资源地址:URI,比如:http://example.com/resources
  传输的资源:Web服务接受与返回的互联网媒体类型,比如:JSON,XML,YAML等。
对资源的操作:Web服务在该资源上所支持的一系列请求方法(比如:POST,GET,PUT或DELETE)。
  下表列出了在实现RESTful API时HTTP请求方法的典型用途。

资源 GET PUT POST DELETE
一组资源的URI,比如https://example.com/resources 列出URI,以及该资源组中每个资源的详细信息(后者可选)。 使用给定的一组资源替换当前整组资源。 在本组资源中创建/追加一个新的资源。该操作往往返回新资源的URL。 删除整组资源。
单个资源的URI,比如https://example.com/resources/142 获取指定的资源的详细信息,格式可以自选一个合适的网络媒体类型(比如:XML、JSON等) 替换/创建指定的资源。并将其追加到相应的资源组中。 把指定的资源当做一个资源组,并在其下创建/追加一个新的元素,使其隶属于当前资源。 删除指定的元素。

  PUT和DELETE方法是幂等方法。GET方法是安全方法(不会对服务器端有修改,因此当然也是幂等的)。
  不像基于SOAP的Web服务,RESTful Web服务并没有“正式”的标准。这是因为REST是一种架构,而SOAP只是一个协议。虽然REST不是一个标准,但大部分RESTful Web服务实现会使用HTTP、URI、JSON和XML等各种标准。

什么是HTTP GET和POST?

转自菜鸟教程:

  • 什么是 HTTP ?
      超文本传输协议(HTTP)的设计目的是保证客户端与服务器之间的通信。HTTP 的工作方式是客户端与服务器之间的请求-应答协议。web 浏览器可能是客户端,而计算机上的网络应用程序也可能作为服务器端。举例:客户端(浏览器)向服务器提交 HTTP 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。

两种 HTTP 请求方法:GET 和 POST

  在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。

GET - 从指定的资源请求数据。
POST - 向指定的资源提交要被处理的数据。

  • GET 方法
      请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:
/test/demo_form.php?name1=value1&name2=value2

有关 GET 请求的其他一些注释:

GET 请求可被缓存
GET 请求保留在浏览器历史记录中
GET 请求可被收藏为书签
GET 请求不应在处理敏感数据时使用
GET 请求有长度限制
GET 请求只应当用于取回数据

  • POST 方法
      请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的:POST
/test/demo_form.php HTTP/1.1
Host: runoob.com
name1=value1&name2=value2

有关 POST 请求的其他一些注释:

POST 请求不会被缓存
POST 请求不会保留在浏览器历史记录中
POST 不能被收藏为书签
POST 请求对数据长度没有要求

事前准备

软件环境

硬件准备

项目 数量 单位 备注
ESP8266开发板 1 建议D1 mini
0.96寸oled液晶模块 1 可选,SSD 1306主控,只是做显示用,初步调试可使用串口
3D打印外壳 1 可选
杜邦线 若干 只用串口的可以不要

硬件接线

ESP8266 D5 D6 3.3V GND
OLED SSD 1306 SDA SCL VCC GND

示例代码

初步原理分析

  这里先使用OpenWeatherMap的API进行初步测试请先行申请一个API.在此项目中,将使用该API来请求您所选位置的当天天气预报。这里只是提供给小白学习使用API,与Blynk无关.建议先熟悉使用API,因为它使您可以访问各种网站所提供的不断变化的信息,例如当前股价,货币汇率,最新新闻,流量更新,推文等等。

  1. 打开浏览器进入: https://openweathermap.org/appid/

  2. 点击singup注册一个OpenWeatherMap账户

  3. 进入这个连接: https://home.openweathermap.org/api_keys 获取你的API KEY

  4. 在“ API密钥”标签上,会看到一个默认密钥.这是您从网站提取信息所需的唯一密钥。将此密钥复制并粘贴到某处,记住它,稍后会需要用到它。接着搜索你需要的城市天气,留意地区信息上的网址的地区代码.

  5. 获取您所选位置的天气信息,请输入以下URL:
    http://api.openweathermap.org/data/2.5/weather?q=你的城市名,地区代码&APPID=你的APIKEY

  完成后会在浏览器出现含有类似以下内容的页面(点击代码框左上角三个点点打开代码框全屏):

{"coord":{"lon":113.25,"lat":23.1167},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"base":"stations","main":{"temp":301.04,"feels_like":300.83,"temp_min":300.93,"temp_max":301.15,"pressure":1013,"humidity":42},"visibility":8000,"wind":{"speed":2,"deg":0},"clouds":{"all":20},"dt":1613979691,"sys":{"type":1,"id":9620,"country":"CN","sunrise":1613948082,"sunset":1613989601},"timezone":28800,"id":1809858,"name":"Guangzhou","cod":200}

  这就是JSON格式的反馈内容,自己观察以下内容中可以见到什么? :huaji: 其中的温度 湿度之类的数值已经显示出来了.然而我们使用esp8266的原理也是跟这个测试差不多,主要是通过GET请求来获取服务器中某台设备提供的数值,然后让esp8266解析Blynk所反馈的JSON信息,ESP8266把需要的内容保存下来展现给我们看.

  我们试试把这个OpenWeatherMap API做成Arduino项目:

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Arduino_JSON.h>

const char* ssid = "你的WiFi SSID";
const char* password = "你的WiFi密码";

// 这里输入你的OpenWeatherMap API KEY
String openWeatherMapApiKey = "你的OpenWeatherMap API KEY";
// 例如:
//String openWeatherMapApiKey = "bd939aa3d23ff33d3c8f5dd1dd4";

// 你的城市名称和区域代码
String city = "guangzhou";//广州
String countryCode = "CN";//即:中国

// 默认计时器设置为10秒用于测试目的
// 对于最终应用程序,请检查网站规定每小时/分钟的API调用限制,以避免被你发送请求太频繁而把你ban了
unsigned long lastTime = 0;
// 十分钟(600000)
//unsigned long timerDelay = 600000;
// 十秒(10000)
unsigned long timerDelay = 10000;

String jsonBuffer; //储存json信息的变量

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  Serial.println("Connecting");
  while(WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to WiFi network with IP Address: ");
  Serial.println(WiFi.localIP());

  Serial.println("Timer set to 10 seconds (timerDelay variable), it will take 10 seconds before publishing the first reading.");
}

void loop() {
  // 发送GET请求
  if ((millis() - lastTime) > timerDelay) {
    // 检查WiFi连接状态
    if(WiFi.status()== WL_CONNECTED){
      String serverPath = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "," + countryCode + "&APPID=" + openWeatherMapApiKey;

      jsonBuffer = httpGETRequest(serverPath.c_str());
      Serial.println(jsonBuffer);
      JSONVar myObject = JSON.parse(jsonBuffer); //声明一个变量:myObject,用于保存json信息

      // JSON.typeof(jsonVar)可用于获取var的类型
      if (JSON.typeof(myObject) == "undefined") {
        Serial.println("Parsing input failed!");
        return;
      }

      Serial.print("JSON object = ");
      Serial.println(myObject);                  //打印出myObject的内容
      Serial.print("Temperature: ");
      Serial.println(myObject["main"]["temp"]);  //解析Jason中的main段落中的temp数值,并打印出来
      Serial.print("Pressure: ");
      Serial.println(myObject["main"]["pressure"]);
      Serial.print("Humidity: ");
      Serial.println(myObject["main"]["humidity"]);
      Serial.print("Wind Speed: ");
      Serial.println(myObject["wind"]["speed"]);
    }
    else {
      Serial.println("WiFi Disconnected");
    }
    lastTime = millis();
  }
}

String httpGETRequest(const char* serverName) {
  HTTPClient http;

  // 打印出你的IP
  http.begin(serverName);

  // 发送 HTTP POST 请求
  int httpResponseCode = http.GET();

  String payload = "{}"; 

  if (httpResponseCode>0) {
    Serial.print("HTTP Response code: ");
    Serial.println(httpResponseCode);
    payload = http.getString();
  }
  else {
    Serial.print("Error code: ");
    Serial.println(httpResponseCode);
  }
  // 释放资源
  http.end();

  return payload;
}

  好了我们来看看实际效果:

  • 上传代码给ESP8266后打开串口,会看见一下内容:
    16:03:02.970 -> JSON object = {"coord":{"lon":113.25,"lat":23.1167},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"base":"stations","main":{"temp":301.04,"feels_like":299.76,"temp_min":300.93,"temp_max":301.15,"pressure":1012,"humidity":39},"visibility":8000,"wind":{"speed":3,"deg":240},"clouds":{"all":20},"dt":1613980757,"sys":{"type":1,"id":9620,"country":"CN","sunrise":1613948082,"sunset":1613989601},"timezone":28800,"id":1809858,"name":"Guangzhou","cod":200}
    16:03:03.017 -> Temperature: 301.04
    16:03:03.017 -> Pressure: 1012
    16:03:03.017 -> Humidity: 39
    16:03:03.017 -> Wind Speed: 3
    16:03:13.178 -> HTTP Response code: 200

      当网站响应反馈200,这表示已经成功了,即:HTTP Response code: 200 :huaji:

发送GET请求获取Blynk的json

  Blynk HTTP RESTful API允许轻松在Blynk应用程序和硬件(如Arduino,Raspberry Pi,ESP8266,Particle等)中的Pin读取值或从中写入值。每个PUT请求都会在应用程序和应用程序上更新Pin的状态硬件。每个GET请求都会在给定的引脚上返回当前状态/值。还提供简化的API,因此可以通过GET请求进行更新。
  这里的实验,不一定要求你要有公网的Blynk服务器,而事前需要注意的是,如果你是使用端口映射方式,则需要留意你的服务商允不允许在不绑定域名的情况下使用TCP通道转发HTTP流量,否则或获取失败,这时候可能你要绑定一个已经备案的域名和做好实名认证.而我的服务商是不允许我使用非付费节点使用TCP通道来转发HTTP流量的,例如我的内部Blynk端口为8080,外网端口为18080的话,隧道类型为TCP,当你get的时候就会失败的.
例子:http://你的外网域名:18080/你的Blynk_token/get/pin这就是不能GET到的,如果是http://192.168.11.123:8080/你的Blynk_token/get/pin则是可以的.所以我们这里使用了内网进行示例,因为我就是内外端口不一致导致的,事实上如果你使用官方服务器的话是直接可以get的.
例子:http://blynk-cloud.com/你的Blynk_token/get/pin
  以上情况似乎你的服务商规定,建议实验时候先用内网测试,测试好了在进行公网测试.

  首先我们拿出一个做好的Blynk项目作为被GET的对象,例如在这里我要获取我这个设备的V0-V2虚拟引脚上的数值,如下图:

图 1
  然后我们来直接在浏览器GET一下服务器作为事前测试,我这里服务器内网IP为192.168.10.250端口为8080则在浏览器中输入http://192.168.10.250:8080/我的Blynk_TOKEN/get/v0.成功后会出现一个只有内容为:["35.605"]的白板页面,其中的["35.605"]就是我的Blynk设备的V0引脚(项目中为温度值).

接下来我们写入代码:
  这里的代码我是使用了OLED的,include了OLED的库不需要的可以把它删掉.由于这个项目对我来说作用意义不大,所以我就没有去美化排版输出信息,需要的朋友请自行整理.


#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Arduino_JSON.h>

#include <Wire.h>         //I2C库
#include "SSD1306Wire.h"  // OLED驱动库

// 初始化OLED D5=SDA D4=SCL
SSD1306Wire  display(0x3c, D5, D6);

const char* ssid = "你的WiFi_SSID";
const char* password = "你的WiFi密码";

const char myObject0[] = "[true, 42, \"apple\"]";

String blynk_token = "你的Blynk_token";

unsigned long timerDelay = 10000;

//用于保存V0-V2引脚数值的变量
String jsonBuffer0; 
String jsonBuffer1;
String jsonBuffer2;

void setup() {
  display.init();
  display.flipScreenVertically();
  display.setFont(ArialMT_Plain_16);
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  Serial.println("Connecting");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to WiFi network with IP Address: ");
  Serial.println(WiFi.localIP());

  Serial.println("Timer set to 10 seconds (timerDelay variable), it will take 10 seconds before publishing the first reading.");
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
    if (WiFi.status() == WL_CONNECTED) {
      String serverPath0 = "http://你的Blynk内网或者公网地址:端口/" + blynk_token + "/get/V0";
      jsonBuffer0 = httpGETRequest(serverPath0.c_str());
      Serial.print(serverPath0);
      Serial.println(jsonBuffer0);
      JSONVar myObject0 = JSON.parse(jsonBuffer0);

      String serverPath1 = "http://你的Blynk内网或者公网地址:端口/" + blynk_token + "/get/V1";
      jsonBuffer1 = httpGETRequest(serverPath1.c_str());
      Serial.print(serverPath1);
      Serial.println(jsonBuffer1);
      JSONVar myObject1 = JSON.parse(jsonBuffer1);

      String serverPath2 = "http://你的Blynk内网或者公网地址:端口/" + blynk_token + "/get/V2";
      jsonBuffer2 = httpGETRequest(serverPath2.c_str());
      Serial.print(serverPath2);
      Serial.println(jsonBuffer2);
      JSONVar myObject2 = JSON.parse(jsonBuffer2);

      if (JSON.typeof(myObject0) == "undefined") {
        Serial.println("Parsing input failed!");
        return;
      }
      //串口打印
      Serial.print("温度: ");
      Serial.println(myObject0);
      //OLED显示
      display.clear();
      display.drawString(34, 10, String(jsonBuffer0));

      Serial.print("湿度: ");
      Serial.println(myObject1);

      display.drawString(34, 27, String(jsonBuffer1));

      Serial.print("大气压: ");
      Serial.println(myObject2);

      display.drawString(23, 45, String(jsonBuffer2));
      display.display();
    }
    else {
      Serial.println("WiFi Disconnected");
    }
    lastTime = millis();
  }
}

String httpGETRequest(const char* serverName) {
  HTTPClient http;

  http.begin(serverName);

  int httpResponseCode = http.GET();

  String payload = "{}";

  if (httpResponseCode > 0) {
    Serial.print("HTTP Response code: ");
    Serial.println(httpResponseCode);
    payload = http.getString();
  }
  else {
    Serial.print("Error code: ");
    Serial.println(httpResponseCode);
    Serial.println(jsonBuffer0);
    Serial.println(jsonBuffer1);
    Serial.println(jsonBuffer2);
  }
  http.end();
  return payload;
}

运行效果:

  • 串口输出内容:
16:41:10.163 -> HTTP Response code: 200
16:41:10.163 -> http://192.168.10.250:8080/**********/get/V0["35.905"]
16:41:10.163 -> HTTP Response code: 200
16:41:10.163 -> http://192.168.10.250:8080/**********/get/V1["48.920"]
16:41:10.208 -> HTTP Response code: 200
16:41:10.208 -> http://192.168.10.250:8080/**********/get/V2["1011.760"]
16:41:10.349 -> HTTP Response code: 200
16:41:10.443 -> 温度: ["35.905"]
16:41:10.443 -> 湿度: ["48.920"]
16:41:10.443 -> 大气压: ["1011.760"]

  其中的

16:41:10.443 -> 温度: ["35.905"]
16:41:10.443 -> 湿度: ["48.920"]
16:41:10.443 -> 大气压: ["1011.760"]

  分别对应我的Blynk设备的V0-V2引脚数值,见图1.

16:43:15.519 -> HTTP Response code: 200
16:43:15.519 -> http://192.168.10.250:8080/**********/get/V0["35.916"]
16:43:15.519 -> HTTP Response code: 200
16:43:15.519 -> http://192.168.10.250:8080/**********/get/V1["48.592"]
16:43:15.566 -> HTTP Response code: 200
16:43:15.566 -> http://192.168.10.250:8080/**********/get/V2["1011.850"]
16:43:19.955 -> HTTP Response code: 200

  则为服务器的响应信息,也是分别对应我的Blynk设备的V0-V2引脚数值,见图1.

  • OLED显示效果

一沙一世界,一花一天堂。君掌盛无边,刹那成永恒。