利用ESP8266+OLED(I2C)打造智能时钟(网络校时+实时天气+天气预报)
Posted 下雪还是下雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用ESP8266+OLED(I2C)打造智能时钟(网络校时+实时天气+天气预报)相关的知识,希望对你有一定的参考价值。
从零开始使用ESP8266+OLED打造智能时钟(网络校时+实时天气+天气预报)
目录
零、前言
一、材料准备
1、ESP8266(NodeMCU V3)
2、OLED(SSD1306)(四针脚,利用I2C通信)
3、杜邦线(我使用4根母对母)
3、WiFi或者手机热点
- 温馨提示:不要打开WIFI6,不要打开5.0GHz频段
- 温馨提示:不要打开WIFI6,不要打开5.0GHz频段
- 温馨提示:不要打开WIFI6,不要打开5.0GHz频段
4.一台能上网的电脑
5、心知天气账号(免费版即可)(👉传送门)
二、开发环境配置
1、Arduino基础安装
(1)访问Arduino官网,下载Arduino IDE
(2)接入开发板,查看端口是否可选,如果端口是灰色的,请查看文末的Q&A
2、安装ESP8266所需要的库
(1)打开 文件->首选项
(2)在附加开发板管理器网址里添加以下内容
https://arduino.esp8266.com/stable/package_esp8266com_index.json
(3)打开工具->开发板->开发板管理器,等待同步完成(由于网络原因,可能会失败,请多试几次)
(4)搜索esp8266并安装(由于网络原因,可能会失败,请多试几次)
3、安装所需要的库
展示需要引入的头文件(其实装了太多,我也忘记那些是必须的了
#include <ArduinoJson.h>//JSON解析
#include <ESP8266WiFi.h>//WIFI
#include <SPI.h>
#include <U8g2lib.h>
#include <WiFiUdp.h>
#include <TimeLib.h>//时间
#include <DNSServer.h>
#include <ESP8266WebServer.h>
三、ESP8266与OLED连接
OLED 显示模块 | ESP8266开发板 |
---|---|
GND | G |
VCC | 3V |
SCL | D1 |
SDA | D2 |
注意图中杜邦线颜色
四、代码编写
//引入必要的头文件
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <SPI.h>
#include <U8g2lib.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
WiFiUDP Udp;
unsigned int localPort = 8888; // 用于侦听UDP数据包的本地端口
//网络校时的相关配置
static const char ntpServerName[] = "ntp1.aliyun.com"; //NTP服务器,使用阿里云
int timeZone = 8; //时区设置,采用东8区
//保存断网前的最新数据
int results_0_now_temperature_int_old;
String results_0_now_text_str_old;
int results_0_daily_1_high_int_old;
int results_0_daily_1_low_int_old;
String results_0_daily_1_text_day_str_old;
//函数声明
time_t getNtpTime();
void sendNTPpacket(IPAddress &address);
void oledClockDisplay();
void sendCommand(int command, int value);
void initdisplay();
void connectWiFi();
void parseInfo_now(WiFiClient client,int i);
void parseInfo_fut(WiFiClient client,int i);
//
boolean isNTPConnected = false;
const unsigned char xing[] U8X8_PROGMEM =
0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, 0xF8, 0x0F, 0x08, 0x08, 0xF8, 0x0F, 0x80, 0x00, 0x88, 0x00,
0xF8, 0x1F, 0x84, 0x00, 0x82, 0x00, 0xF8, 0x0F, 0x80, 0x00, 0x80, 0x00, 0xFE, 0x3F, 0x00, 0x00; /*星*/
const unsigned char liu[] U8X8_PROGMEM =
0x40, 0x00, 0x80, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00,
0x20, 0x02, 0x20, 0x04, 0x10, 0x08, 0x10, 0x10, 0x08, 0x10, 0x04, 0x20, 0x02, 0x20, 0x00, 0x00; /*六*/
typedef struct
//存储配置结构体
int tz; //时间戳
config_type;
config_type config;
WiFiClient clientNULL;
DNSServer dnsServer;
ESP8266WebServer server(80);
//----------WIFI连接配置----------
const char* ssid = "XXX"; // 连接WiFi名(此处使用XXX为示例)
const char* password = "12345678"; // 连接WiFi密码(此处使用12345678为示例)
// 请将您需要连接的WiFi密码填入引号中
//----------天气API配置----------
const char* host = "api.seniverse.com"; // 将要连接的服务器地址
const int httpPort = 80; // 将要连接的服务器端口
// 心知天气HTTP请求所需信息
String reqUserKey = "XXXXXX"; // 私钥
String reqLocation = "hangzhou"; // 城市
String reqUnit = "c"; // 摄氏/华氏
//----------设置屏幕----------
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
int sta = 0;
//----------初始化OLED----------
void initdisplay()
u8g2.begin();
u8g2.enableUTF8Print();
//----------用于获取实时天气的函数(0)----------
void TandW()
String reqRes = "/v3/weather/now.json?key=" + reqUserKey +
+ "&location=" + reqLocation +
"&language=en&unit=" +reqUnit;
// 向心知天气服务器服务器请求信息并对信息进行解析
httpRequest(reqRes,0);
//延迟,需要低于20次/分钟
delay(5000);
void display_1(int results_0_now_temperature_int,String results_0_now_text_str);//声明函数,用于显示温度、天气
//----------获取3天预报(1)----------
void threeday()
// 建立心知天气API当前天气请求资源地址
String reqRes = "/v3/weather/daily.json?key=" + reqUserKey +
+ "&location=" + reqLocation + "&language=en&unit=" +
reqUnit + "&start=0&days=3";
// 向心知天气服务器服务器请求信息并对信息进行解析
httpRequest(reqRes,1);
delay(5000);
void clock_display(time_t prevDisplay)
server.handleClient();
dnsServer.processNextRequest();
if (timeStatus() != timeNotSet)
if (now() != prevDisplay)
//时间改变时更新显示
prevDisplay = now();
oledClockDisplay();
void setup()
Serial.begin(9600);
Serial.println("");
initdisplay();
// 连接WiFi
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_unifont_t_chinese2);
u8g2.setCursor(0, 14);
u8g2.print("Waiting for WiFi");
u8g2.setCursor(0, 30);
u8g2.print("connection...");
u8g2.sendBuffer();
connectWiFi();
Udp.begin(localPort);
setSyncProvider(getNtpTime);
setSyncInterval(300); //每300秒同步一次时间
time_t prevDisplay = 0; //当时钟已经显示
void loop()
if (sta>=0 && sta<=250)
clock_display(prevDisplay);
else if(sta == 251)
TandW();
else
threeday();
++sta;
if(sta==253)
sta = 0;
// 向心知天气服务器服务器请求信息并对信息进行解析
void httpRequest(String reqRes,int stat)
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.");
if (stat == 0)
// 利用ArduinoJson库解析心知天气响应信息(实时数据)
parseInfo_now(client,1);
else if(stat == 1)
parseInfo_fut(client,1);
else
Serial.println(" connection failed!");
if (stat == 0)
// 利用ArduinoJson库解析心知天气响应信息(实时数据)
parseInfo_now(clientNULL,0);
else if(stat == 1)
parseInfo_fut(clientNULL,0);
//断开客户端与服务器连接工作
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_now(WiFiClient client,int i)
if(i==1)
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("========================"));
display_0(results_0_now_temperature_int,results_0_now_text_str);
results_0_now_text_str_old = results_0_now_text_str;
results_0_now_temperature_int_old = results_0_now_temperature_int;
else
display_0(results_0_now_temperature_int_old,results_0_now_text_str_old);
//----------输出实时天气----------
void display_0(int results_0_now_temperature_int,String results_0_now_text_str)
//显示输出
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_wqy16_t_gb2312);
u8g2.setCursor(15, 14);
u8g2.print("杭州实时天气");
u8g2.setFont(u8g2_font_logisoso24_tr);
u8g2.setCursor(45, 44);
u8g2.print(results_0_now_temperature_int);
u8g2.setCursor(35, 61);
u8g2.setFont(u8g2_font_unifont_t_chinese2);
u8g2.print(results_0_now_text_str);
u8g2.sendBuffer();
// 利用ArduinoJson库解析心知天气响应信息(预测)
void parseInfo_fut(WiFiClient client,int i)
if(i==1)
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(3) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 3*JSON_OBJECT_SIZE(14) + 860;
DynamicJsonDocument doc(capacity);
前言:本章我们要实现的功能为:将获取到的天气数据进行OLED显示。
参考资料:
OLED显示屏:
关于基于stm32的0.96寸oled显示屏的学习理解心得。
cJSON数据转换:
用cJSON解析心知天气返回的数据包
C语言cJSON库的使用,解析json数据格式
文章目录
1、摘要
本章节主要有两个部分组成:
*一个是OLED屏幕的显示,关于OLED的资料有挺多的,CSDN中也有许多大佬做了比较详细的介绍,所以本章节并不会很深入地去讲解代码,只是会稍微提一下,如果大家有需要我写一个比较详细地介绍的话,也可以在评论区提出,我有时间就会写一下的。
另一个是用cJSON去解读心知天气返回的数据包。
2、硬件准备
硬件和上章节差不多,只是多了一个OLED屏幕,这里我使用的是0.96寸I2C驱动的OLED屏幕。
2.1、商品链接
最小系统板:购买链接
USB转TTL(种类有点多,随便选一个就行,我用的是CH340这个芯片的):购买链接
ESP8266:购买链接
OLED(我用的是0.96寸4针,I2C接口):购买链接
ST-Link V2下载线:购买链接
3、软件准备
Keil编译器
VSCode编译器
XCOM串口调试助手
这边附上另一篇文章,大家可以参考学习一下
STM32----使用VSCode编写Keil
4、硬件连线
MCU ESP8266 3.3V VCC GND GND PB10 RXD PB11 TXD 3.3V IO 3.3V RST
MCU USB转TTL 5V VCC GND GND PA9 RXD PA10 TXD
MCU OLED 5V VCC GND GND PB15 SDA PB13 SCL
5、代码解析
5.1、OLED代码分析
参考其他博主写的技术论文
OLED显示屏:关于基于stm32的0.96寸oled显示屏的学习理解心得。
本实验使用的OLED是基于I2C通信的。所以最主要的内容有:
发送从机地址(0x78),再发送命令字节,接着发送数据字节。
/**********************************************
// IIC Write Command
**********************************************/
void Write_IIC_Command(unsigned char IIC_Command)
IIC_Start();
Write_IIC_Byte(0x78); //Slave address,SA0=0
IIC_Wait_Ack();
Write_IIC_Byte(0x00); //write command
IIC_Wait_Ack();
Write_IIC_Byte(IIC_Command);
IIC_Wait_Ack();
IIC_Stop();
/**********************************************
// IIC Write Data
**********************************************/
void Write_IIC_Data(unsigned char IIC_Data)
IIC_Start();
Write_IIC_Byte(0x78); //D/C#=0; R/W#=0
IIC_Wait_Ack();
Write_IIC_Byte(0x40); //write data
IIC_Wait_Ack();
Write_IIC_Byte(IIC_Data);
IIC_Wait_Ack();
IIC_Stop();
根据这三个主要的内容,我们可以设置OLED显示器的显示样式。主要用到的代码有:
显示字符串函数、显示汉字函数
/**********************************
显示一个字符串
输入数据:
x----x轴
y----y轴
*chr----字符串
Char_Size----大小(16/12)
**********************************/
void OLED_ShowString(u8 x,u8 y,u8 *chr,u8 Char_Size)
unsigned char j=0;
while (chr[j]!='\\0')
OLED_ShowChar(x,y,chr[j],Char_Size);
x+=8;
if(x>120)x=0;y+=2;
j++;
/**********************************
显示一个文字
输入数据:
x----x轴
y----y轴
no----字库数组文字对应的位置
**********************************/
void OLED_ShowCHinese(u8 x,u8 y,u8 no)
u8 t,adder=0;
OLED_Set_Pos(x,y);
for(t=0;t<16;t++)
OLED_WR_Byte(Hzk[2*no][t],OLED_DATA);
adder+=1;
OLED_Set_Pos(x,y+1);
for(t=0;t<16;t++)
OLED_WR_Byte(Hzk[2*no+1][t],OLED_DATA);
adder+=1;
5.2、cJSON解析数据包代码
因为C语言没有字典这样的结构,所以我们解析json格式的数据就需要调用cJSON这个函数库。
主要函数接口:
/* The cJSON structure: */
typedef struct cJSON
struct cJSON *next,*prev; /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *child; /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
int type; /* The type of the item, as above. */
char *valuestring; /* The item's string, if type==cJSON_String */
int valueint; /* The item's number, if type==cJSON_Number */
double valuedouble; /* The item's number, if type==cJSON_Number */
char *string; /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
cJSON;
说明:
- cJSON是使用链表来存储数据的,其访问方式很像一颗树。每一个节点可以有兄弟节点,通过next/prev指针来查找,它类似双向链表;每个节点也可以有孩子节点,通过child指针来访问,进入下一层。只有节点是对象或数组时才可以有孩子节点。
- type是键(key)的类型,一共有7种取值,分别是:False,Ture,NULL,Number,String,Array,Object。
若是Number类型,则valueint或valuedouble中存储着值。 若期望的是int,则访问valueint。
若期望的是double,则访问valuedouble,可以得到值。
若是String类型的,则valuestring中存储着值,可以访问valuestring得到值。 - string中存放的是这个节点的名字,可理解为key的名称。
重要的接口函数:
- cJSON *cJSON_Parse(const char *value);
功能:解析JSON数据包,并按照cJSON结构体的结构序列化整个数据包。可以看做是获取一个句柄。 - cJSON *cJSON_GetObjectItem(cJSON *object,const char *string);
功能:获取json指定的对象成员 ;
参数:*objec:第一个函数中获取的句柄。string:需要获取的对象 ;
返回值:这个对象成员的句柄;
如果json格式的对象成员直接就是字符串那么就可以直接通过结构体中的valuestring元素来获取这个成员的值。 - cJSON *cJSON_GetArrayItem(cJSON *array,int item);
功能:有可能第二个函数中获取到的是成员对象值是一个数组,那么就需要用到这个函数。用来获取这个数组指定的下标对象;
参数:*array:传入第二步中返回的值, item:想要获取这个数组的下标元素 ;
返回值:这个数组中指定下标的对象。然后在对这个返回值重复使用第二步函数就可以获取到各个成员的值了。也就是说对象是数组的比是字符串的要多用一个cJSON_GetArrayItem函数,其他的没区别。 - cJSON_Delete() 用来释放你第一步获取的句柄,来释放整个内存。用在解析完后调用。
核心代码
解析数据包
/*********************************************************************************
* Function Name : cJSON_WeatherParse,解析天气数据
* Parameter : JSON:天气数据包 results:保存解析后得到的有用的数据
* Return Value : 0:成功 其他:错误
* Function Explain :
* Create Date : 2017.12.6 by lzn
**********************************************************************************/
int cJSON_WeatherParse(char *JSON, Results *results)
cJSON *json,*arrayItem,*object,*subobject,*item;
json = cJSON_Parse(JSON); //解析JSON数据包
if(json == NULL) //检测JSON数据包是否存在语法上的错误,返回NULL表示数据包无效
printf("Error before: [%s] \\r\\n",cJSON_GetErrorPtr()); //打印数据包语法错误的位置
return 1;
else
if((arrayItem = cJSON_GetObjectItem(json,"results")) != NULL); //匹配字符串"results",获取数组内容
int size = cJSON_GetArraySize(arrayItem); //获取数组中对象个数
printf("cJSON_GetArraySize: size=%d \\r\\n",size);
if((object = cJSON_GetArrayItem(arrayItem,0)) != NULL)//获取父对象内容
/* 匹配子对象1 */
if((subobject = cJSON_GetObjectItem(object,"location")) != NULL)
printf("---------------------------------subobject1-------------------------------\\r\\n");
if((item = cJSON_GetObjectItem(subobject,"id")) != NULL) //匹配子对象1成员"id"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].location.id,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"name")) != NULL) //匹配子对象1成员"name"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].location.name,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"country")) != NULL)//匹配子对象1成员"country"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].location.country,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"path")) != NULL) //匹配子对象1成员"path"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].location.path,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"timezone")) != NULL)//匹配子对象1成员"timezone"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].location.timezone,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"timezone_offset")) != NULL)//匹配子对象1成员"timezone_offset"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].location.timezone_offset,item->valuestring,strlen(item->valuestring));
/* 匹配子对象2 */
if((subobject = cJSON_GetObjectItem(object,"now")) != NULL)
printf("---------------------------------subobject2-------------------------------\\r\\n");
if((item = cJSON_GetObjectItem(subobject,"text")) != NULL)//匹配子对象2成员"text"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].now.text,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"code")) != NULL)//匹配子对象2成员"code"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].now.code,item->valuestring,strlen(item->valuestring));
if((item = cJSON_GetObjectItem(subobject,"temperature")) != NULL) //匹配子对象2成员"temperature"
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",item->type,item->string,item->valuestring);
memcpy(results[0].now.temperature,item->valuestring,strlen(item->valuestring));
/* 匹配子对象3 */
if((subobject = cJSON_GetObjectItem(object,"last_update")) != NULL)
printf("---------------------------------subobject3-------------------------------\\r\\n");
printf("cJSON_GetObjectItem: type=%d, string is %s,valuestring=%s \\r\\n",subobject->type,subobject->string,subobject->valuestring);
memcpy(results[0].last_update,item->valuestring,strlen(subobject->valuestring));
cJSON_Delete(json); //释放cJSON_Parse()分配出来的内存空间
return 0;
5.3、项目代码逻辑
基本的代码已经介绍完了,其他的操作就是调用这些函数实现功能而已,这边就不全部列出来了,感觉没什么意义。
我把我的代码的逻辑分享一下吧,具体代码我会附在文末,大家可以详细看看。
初始化函数->OLED显示状态->按下按键触发中断->获取天气数据->OLED状态更新。
6、运行结果
设备上电后,等待ESP8266初始化,获取天气数据后通过cJSON解析,将状态显示到OLED屏幕上。当按下按键后,会重新获取,更新天气数据。
7、源程序
7.1、百度网盘
链接:https://pan.baidu.com/s/1fDIIgYYc9yALo2gTNgoJRw
提取码:gd2d
7.2、Github地址
传送门:
基于STM32的ESP8266天气时钟(1)---------AT指令获取天气数据
基于STM32的ESP8266天气时钟(2)--------MCU获取天气数据
基于STM32的ESP8266天气时钟(3)--------MCU数据处理及显示
基于STM32F的ESP8266天气时钟(4)--------MCU获取时间及显示(完结)