1、用网络调试助手GET天气预报
1.1、设置域名和端口,建立TCP连接
域名:pi.seniverse.com,端口:80
1.2、GET请求
1.2.1、先了解下HTTP请求报文格式
1.2.2、分析下这个GET请求
- 第一行:请求行
- GET URL链接 HTTP协议版本
- 第二、三、四、五行:请求头
- 冒号前面是头部键,冒号后面是值,然后回车换行到下一行
- 第六、七行:空行
- Connection行末尾有回车换行,然后一个回车换行
GET /v3/weather/daily.json?key=rrpd2zmqkpwlsckt&location=guangzhou&language=en&unit=c&start=0&days=3 HTTP/1.1
Content-Type: text/html;charset=utf-8
Accept: */*
Host: api.seniverse.com
Connection: Keep-Alive
1.3、收到HTTP的响应
1.3.1、先看HTTP响应报文格式
1.3.2、实际收到的HTTP响应报文
2、心知天气、API的说明
上面的HTTP响应报文,人眼看是没问题,但是这收到的是json数据,我复制到一些在线解析json的网站都解析不成功,好几个地方少了大括号;不知道是不是GET的格式不对,所以自己去心知天气网站注册了账号,看看有没有什么文档说明,而且毕竟上面的GET格式我也不知道他是哪里来的,自己研究下了。
2.1、注册账号,选择免费的版本
2.2、进入文档,选择“天气预报”,可以看到右边有API接口
2.3、查看API接口、参数说明、返回数据说明
这个接口示例就是我们用GET请求中需要的,密钥在“控制台”→“我的产品”→“免费版”,点进去,有显示公钥、私钥;
language改成en,英文显示,因为esp8266又不能解析中文;最后day指的是查询的天数,花钱可以查15天,否则最多3天,我只想只要今天而已,day=1,只查今天的就好了;
3、关于JSON格式的错误的解决
3.1、换一个网络调试工具
对比了,与之前的GET请求格式并无不同,利用这个接口和自己的密钥再试试,果然,返回的json格式还是不对;
可能是这个网络调试助手不行,可是为什么就只有大括号没有呢,好几个大括号,而且还是在数据的中间部分,也并不是开头结尾呀,莫名其妙的,不管怎么说算换一个试试,还真就是......
3.2、在线解析json
这下没问题了,可以开始下一步在ESP8266上发送GET请求并接收数据了,唉,折腾,13:31了,看看之前那个调试工具收到的HTTP响应报文还是11:44,煮饭去,肚子痛;
4、用ESP8266发送GET请求、打印HTTP响应报文
调试助手里,GET请求这么写;
GET https://api.seniverse.com/v3/weather/daily.json?key=SJo8p9FiiIu6gDNz4&location=guangzhou&language=en&unit=c&start=0&days=1 HTTP/1.1
Content-Type: text/html;charset=utf-8
Accept: */*
Host: api.seniverse.com
Connection: Keep-Alive
但是作为espconn_send发送的字符串,GET请求按调试助手这么写肯定不行呀,首先要手动加入\r\n,这个HTTP请求报文写成一个宏,不然直接放在函数里,肯定不好看,一开始我是这么写的,嗯,不行,一个""括起来了是一个字符串,可是我还要换行呢,这样宏的换行就在字符串里面了,不换行看着又不清晰,百度下宏定义字符串怎么换行,没百度到不过看到有字符串里直接写\换行的,试一下先,返回HTTP/1.1 400 Bad Request,也就是请求的格式错误.....
#define GET_TODAY_WEATHER "GET https://api.seniverse.com/v3/weather/daily.json?key=SJo8p9FiiIu6gDNz4&location=guangzhou&language=en&unit=c&start=0&days=1 HTTP/1.1\r\n\
Content-Type: text/html;charset=utf-8\r\n \
Accept: */*\r\n \
Host: api.seniverse.com\r\n \
Connection: Keep-Alive\r\n\r\n"
继续查找资料吧,有发现,当字符串过长的时候,直接写多个字符串就可以了,中间以空白、制表符、换行符隔开,这个换行符就符合现在我的需要;
于是,GET请求这样写,每一行呢都加""好似一个单独的字符串,行末加换行符,果然可以了;
#define GET_TODAY_WEATHER "GET https://api.seniverse.com/v3/weather/daily.json?key=SJo8p9FiiIu6gDNz4&location=guangzhou&language=en&unit=c&start=0&days=1 HTTP/1.1\r\n"\
"Content-Type: text/html;charset=utf-8\r\n" \
"Accept: */*\r\n" \
"Host: api.seniverse.com\r\n" \
"Connection: Keep-Alive\r\n\r\n"
5、json解析
先放链接为敬,分析得很深入,讲解清晰,注意其中一段代码从下往上看跟着理解,博主也很细心标注了顺序,很有帮助;
因为之前没有理解过json这个东西,所以这里记录初学粗浅的一些想法。看完上面的链接之后就可以顺利解析json了;
5.1、总结下json的格式
json是一种特定的字符串格式,本质是一个对象。
对象由大括号{}表示,所以最外层是大括号括起来的。
对象由一个或多个键值对组成,键值对之间用逗号,分隔,除最后一个键值对;
键值对的格式为:"键":值,即键的名字是字符串,用冒号:分隔键与值,值可以是字符串、数字、对象、数组、null、布尔值;
数组用中括号[]括起来,数组中元素可以是字符串、数字、对象、数组、null、布尔值,数组中元素用逗号分隔除最后一个;
5.2、代码及结果
代码其实并不多,而且我只解析其中需要的一些数据,比如"location"中有"id","name","country","path","timezone","timezone_offset",我只保留了"name"和"country",只需要这些信息而且看着也清晰;
逻辑上,就是用JSONTREE_OBJECT相关的宏创建一个json树,最终键值对"值"的位置指向了一个回调函数,这样在解析到这个键值对的时候就会调用这个回调函数,由于多个键值对指向同一个回调函数,所以在回调函数内部要判断当前是哪一个分支;
记得添加user_json.c和user_json.h,并#include "user_json.h";
5.2.1、创建json树
省略了一些信息,更详细的内容请见上面的链接,还有比如"daily"如果查3日数据是要弄个数组的;
LOCAL struct jsontree_callback seniverse_handle_callback =
JSONTREE_CALLBACK(NULL, seniverse_info_set);
//seniverse_location_tree里面关于"name""country"的回调函数
JSONTREE_OBJECT(seniverse_location_tree,
JSONTREE_PAIR("name", &seniverse_handle_callback),
JSONTREE_PAIR("country", &seniverse_handle_callback)
);
//seniverse_daily_par_tree里面关于"data""text_day"等的回调函数
JSONTREE_OBJECT(seniverse_daily_tree,
JSONTREE_PAIR("date", &seniverse_handle_callback),
JSONTREE_PAIR("text_day", &seniverse_handle_callback),
JSONTREE_PAIR("text_night", &seniverse_handle_callback),
JSONTREE_PAIR("high", &seniverse_handle_callback),
JSONTREE_PAIR("low", &seniverse_handle_callback)
);
JSONTREE_OBJECT(seniverse_info_tree,
JSONTREE_PAIR("location", &seniverse_location_tree),
JSONTREE_PAIR("daily", &seniverse_daily_tree),
JSONTREE_PAIR("last_update", &seniverse_handle_callback)
);
5.2.2、回调函数
解析json树的时候,在"name"处,这两个判断都是成立的,也就是说jsonparse_strcmp_value还可以知道自己的上一级;
if(jsonparse_strcmp_value(parser, "location") == 0)
if (jsonparse_strcmp_value(parser, "name") == 0)
每次查到具体的键之后,都会有两个jsonparse_next,是为了跳过冒号和双引号;
jsonparse_next(parser); jsonparse_next(parser);
/******************************************************************************
* FunctionName : seniverse_info_set
* Description : 将一个json格式解析成心知天气的参数
* Parameters : js_ctx -- 一个指向JSON设置的指针
* parser -- 指向JSON解析器状态的指针
* Returns : result
*******************************************************************************/
LOCAL int ICACHE_FLASH_ATTR
seniverse_info_set(struct jsontree_context *js_ctx, struct jsonparse_state *parser)
{
int type;
uint8 seniverse_tree=0;
//解析 JSON 格式下⼀个元素
while ((type = jsonparse_next(parser)) != 0){
if (type == JSON_TYPE_PAIR_NAME) {
char buffer[64];
os_bzero(buffer, 64); //buffer数组清零
//比较解析 JSON 数据与特定字符串
if(jsonparse_strcmp_value(parser, "location") == 0){ //location对象
seniverse_tree = 1;
}else if (jsonparse_strcmp_value(parser, "daily") == 0){ //daily对象
seniverse_tree = 2;
}else if (jsonparse_strcmp_value(parser, "last_update") == 0){ //last_update对象
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("last_update:%s\n",buffer); //数据更新时间
}
if(seniverse_tree == 1){ //解析location里面的信息
if (jsonparse_strcmp_value(parser, "name") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("address:%s\n",buffer); //地名:广州
}else if (jsonparse_strcmp_value(parser, "country") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("country:%s\n",buffer); //国家
}
}else if(seniverse_tree == 2){ //解析daily里面的信息
if (jsonparse_strcmp_value(parser, "date") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("date:%s\n",buffer); //date:日期,比如2022-05-24
}else if (jsonparse_strcmp_value(parser, "text_day") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("day:%s\n",buffer); //白天天气
}else if (jsonparse_strcmp_value(parser, "text_night") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("night:%s\n",buffer); //夜晚天气
}else if (jsonparse_strcmp_value(parser, "high") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("high:%s\n",buffer); //最高温度
}else if (jsonparse_strcmp_value(parser, "low") == 0){
jsonparse_next(parser); jsonparse_next(parser);
jsonparse_copy_value(parser, buffer, sizeof(buffer));
os_printf("low:%s\n",buffer); //最低温度
}
}
}
}
return 0;
}
5.2.3、TCP通信收到数据后的处理
void ICACHE_FLASH_ATTR TCP_recv_callback(void * arg, char * pdata, unsigned short len)
{
struct espconn * T_arg = arg; // 缓存网络连接结构体指针
char * pParse_json=NULL;
os_printf("\nESP8266_Receive_Data = %s\n",pdata);// 串口打印接收到的数据
//找到要解析的json字符串开头
pParse_json=(char *)os_strstr(pdata, "{"); //指向"{"
if(NULL == pParse_json){ //找不到"{"
return;
}else{
struct jsontree_context js;
jsontree_setup(&js, (struct jsontree_value *)&seniverse_info_tree, json_putchar);
json_parse(&js, pParse_json);
}
}
5.2.4、结果
首先打印的是收到的HTTP响应报文,然后是解析完json后的数据;
- address:Guangzhou 广州
- country:CN 中国
- date:2022-05-24 2022年5月24日的天气
- day:Cloudy 白天多云
- night:Thundershower 夜晚雷阵雨,雷阵雨原来是thundershower,看到shower只能想到洗澡~
- high:30 最高气温30℃
- low:24 最低气温24℃
- last_update:2022-05-24T08:00+08:00 天气更新时间:2022年5月24日16:00