赞
踩
提示:这里可以添加本文要记录的大概内容:
本项目需要基础的stm32单片机知识,这里我推荐
链接:https://www.bilibili.com/video/BV1th411z7sn?p=1&vd_source=e9ab6ae9ee7c74bb73c9334f2da0a743
如果不想看那么多,看到4-2 OLED显示屏就差不多。我使用的是他的OLED基本例程。
提示:在这里我使用了UsartPrintf()函数打印esp8266_buf数组,但是我之前提到了UsartPrintf()函数里面局部定义的数组只有296个,但是esp8266_buf数组我重新定义有400个,所以需要修改UsartPrintf()函数里面的数组为400个才不会出现数据丢失的情况。
首先,我们需要根据高德地图API的入门指南获取一下API。
第一步,按照网址指示:https://lbs.amap.com/api/webservice/guide/create-project/get-key,申请Web服务API密钥。
之后会得到一个Key,可以使用的服务如下
然后这里我们用到了IP定位、逆地理编码、天气查询的API,不懂的可以点进去学习一下
“若用户不填写IP,则取客户HTTP之中的请求来进行定位”,所以我们如果想要获取本地的IP,只需要在ESP8266连接高德地图tcp后进入数据交换模式后发送如下指令,即可获得用户IP地址的定位。(需要加回车换行符,这个key需要填上你们上面申请到的key)
GET https://restapi.amap.com/v3/ip?&key=ef8466d69928146d187ea512e8a7936a\r\n
为了STM32可以完整地获取到返回的数据我们需要定义一个接收函数。这个函数可以根据参数自行定义等待时间,并且接收完成一帧数据后,会通过串口1输出一帧数据和其个数,方便调试。经过这个延时等待接收完成的函数,我们可以获得接收完成的esp8266_buf数组。
/* 溢出时间 = 输入*10ms */ void wait_rev(uint16_t time_out) { uint16_t timeout; //定义溢出时间间隔 for(timeout = time_out; timeout>0; timeout--) { if(ESP8266_WaitRecive() == REV_OK) //如果接收一帧数据完成 { UsartPrintf(USART_DEBUG,"%d\r\n",esp8266_cnt); //通过串口1输出一帧数据的个数 UsartPrintf(USART_DEBUG, "%s\r\n", esp8266_buf); //通过串口1输出一帧数据 break; //接收完成退出循环,避免浪费时间 } Delay_ms(10); //每次等待时间 } }
发送获取IP指令后,串口助手返回如图,乱码是因为高德地图返回的中文编码方式为UTF-8,而串口助手无法显示UTF-8编码的字符所以会出现乱码。(可以下载一个可以显示UTF-8编码的串口助手,如:SingTownSerialport,方便后续调试)。为了测试GET后面的URL是否能够成功通信,可以直接把GET后面的网址使用浏览器打开,打开后出现如下画面即证明URL是可以通信的。
这里的返回值我们只需要取出对应IP地址的经纬度,即“rectangle”部分,只需要第一个就可以。
逆地址编码API是可以根据提供的经纬度,返回较为准确的位置和地区编码,所以我们使用上面获取到的IP地址经纬度,再将经纬度放到这里,就可以获得IP地址区级的地区编码。ESP-01S指令如下
GET https://restapi.amap.com/v3/geocode/regeo?key=ef8466d69928146d187ea512e8a7936a&location=112.3483157,22.96136686\r\n
串口返回如下:
返回的数值为79是因为接收到的数据总数已经超过了接收数组定义的400bit,而当超过时,中断函数会将接收值置零,采用覆盖接收的方式继续接收,所以这里的79代表了已经有79bit数据被覆盖掉,但是那些丢失的数据并不重要,为了节省资源,这里就采用这种方法接收。这些数据中我们需要的是地区编码:adcode:441204;我们只需要将441204提取出来即可。
同样的可以将网址直接打开,查看是否能够成功与网站通信。
这里我通过get_ip_data()函数获取ip地址对应的地址区县编码。先给ESP-01S发送获取IP地址的命令,然后通过wait_rev(200)函数等待接收完成,将经纬度提取出来。然后将经纬度放到获取逆地理编码命令中,发送给ESP-01S,等待接收完成,将IP地址对应的区县编码提取出来放到获取天气信息的命令中,为后续获取当地的天气信息做准备。
unsigned char get_ip_cmd_packback[] = "GET https://restapi.amap.com/v3/ip?&key=ef8466d69928146d187ea512e8a7936a\r\n"; //获取IP地址的命令 const char *get_adcode_cmd_dead = "GET https://restapi.amap.com/v3/geocode/regeo?key=ef8466d69928146d187ea512e8a7936a&location="; //获取地址编码命令的开头部分 unsigned char get_weather_cmd_packback[] = "GET https://restapi.amap.com/v3/weather/weatherInfo?city=440511&key=ef8466d69928146d187ea512e8a7936a\r\n"; //获取相应地址天气的命令 /*获取IP的地址编码并放到获取天气命令中*/ void get_ip_data(void) { char *data_set_start; //需要的数据开始位置 char *data_set_end; //需要的数据结束位置 char latitude[30]; //经纬度字符数组 char get_adcode_cmd_packback[130]; //获取地区编码命令 char adcode[6]; //地区编码数组,地区编码都为6位 char *processing_change_city_adcode; //需要修改编码的位置 Usart_SendString(USART2,get_ip_cmd_packback, sizeof(get_ip_cmd_packback)); //发送获取IP地址命令 wait_rev(200); //等待接收完成 data_set_start = &strstr((const char*)esp8266_buf, "rectangle")[12]; //找经纬度开始位置 data_set_end = strstr(data_set_start, ";"); //找经纬度结束位置 i = 0; i = data_set_end - data_set_start; //计算经纬度字符串长度 memcpy(latitude, data_set_start, i); //将经纬度字符串放到latitude数组 UsartPrintf(USART_DEBUG, "%s\r\n", latitude); //串口1输出经纬度字符串,用于调试 /*将获取地址编码命令补充完整*/ strcat(get_adcode_cmd_packback, get_adcode_cmd_dead); strcat(get_adcode_cmd_packback, latitude); strcat(get_adcode_cmd_packback, "\r\n"); UsartPrintf(USART_DEBUG, "%s\r\n", get_adcode_cmd_packback); //串口1输出获取地址编码命令,用于调试 ESP8266_Clear(); //清除esp8266_buf数组 Delay_s(1); //ESP-01S发送命令不能过快,所以需要延时1s Usart_SendString(USART2, (unsigned char*)get_adcode_cmd_packback, strlen(get_adcode_cmd_packback)); //发送获取地址编码命令 wait_rev(200); //等待接收完成 data_set_start = &strstr((const char*)esp8266_buf, "adcode")[9]; //找地址编码开始位置 /*将地址编码放到adcode数组*/ for(i=0; i<sizeof(adcode); i++) { adcode[i] = data_set_start[i]; } UsartPrintf(USART_DEBUG,"\r\n%s\r\n",adcode); //串口1输出adcode数组,用于调试 processing_change_city_adcode = &strstr((const char*)get_weather_cmd_packback, "city=")[5]; //获取需要修改编码的位置 /*用新的地址编码代替旧的地址编码*/ for(i=0; i<sizeof(adcode); i++) { processing_change_city_adcode[i] = adcode[i]; } UsartPrintf(USART_DEBUG,"%s\r\n",get_weather_cmd_packback); //串口1输出修改完的获取天气命令,用于调试 ESP8266_Clear(); //清除esp8266_buf数组 Delay_s(1); }
串口调试返回如图
天气查询API是一个简单的HTTP接口,根据用户输入的adcode,查询目标区域当前/未来的天气情况,数据来源是中国气象局。
天气API可以根据输入的城市编码获取实时的天气信息,其ESP-01S指令如下
GET https://restapi.amap.com/v3/weather/weatherInfo?city=440511&key=ef8466d69928146d187ea512e8a7936a\r\n
所以我们只需要将前面获取到的adcode编码放到city参数里即可获取IP地址对应的天气信息。这个在上面get_ip_data()函数里面已经处理好了。所以get_weather_data()函数只需要发送获取天气命令,等待接收完成,然后将我们需要的信息放到processing_data数组中,处理完成后,processing_data数组就是我们需要的包含天气信息和地区名字的json数据包。
/*获取天气信息,将需要的json数据包放到processing_data数组中,等待处理*/ void get_weather_data(void) { char *weather_set_start; //天气开始的位置 char *weather_set_end; //天气结束的位置 Usart_SendString(USART2, get_weather_cmd_packback, sizeof(get_weather_cmd_packback)); //发送获取天气指令 wait_rev(200); weather_set_start = strstr((const char*)esp8266_buf, "\"city\""); //获取esp8266_buf数组中以\city\开头的字符串地址 weather_set_end = strstr((const char*)esp8266_buf, ",\"temperature\""); //获取esp8266_buf数组中以,\temperature\开头的字符串地址 strncpy(&processing_data[1], weather_set_start, weather_set_end - weather_set_start); //将\city\到,\temperature\的数据放到processing_data[1]的位置 /*在数组的开始和结尾补上{}符号变成json格式数据*/ processing_data[0] = '{'; processing_data[weather_set_end - weather_set_start + 1] = '}'; UsartPrintf(USART_DEBUG,"%s\r\n",processing_data); //串口1输出得到的json格式数据,用于调试 ESP8266_Clear(); //清除esp8266_buf数组 }
串口返回如下
310
{“status”:“1”,“count”:“1”,“info”:“OK”,“infocode”:“10000”,“lives”:[{“province”:“广东”,“city”:“高要区”,“adcode”:“441204”,“weather”:“多云”,“temperature”:“34”,“winddirection”:“东南”,“windpower”:“≤3”,“humidity”:“64”,“reporttime”:“2023-07-01 16:00:44”,“temperature_float”:“34.0”,“humidity_float”:“64.0”}]}
{“city”:“高要区”,“adcode”:“441204”,“weather”:“多云”}
上面黄色的部分就是将地区和天气从接收数据中提取出来并处理成json数据格式的数据包(编码格式为UTF-8)。
有人可能要问,这整个数据就已经是完整的json数据格式的数据包,为什么不直接使用json库函数处理,而要多此一举将一部分信息提取出来,自己加上{}组成json格式?
这是因为一开始,我直接使用json库函数处理会出现bug,后面发现是因为建立json类会占用大量内存空间导致内存溢出,所以会出现解析错误的情况,所以如果减少json处理的信息,就能解决这个bug。(还有一种办法是直接修改STM32的内存容量,怎么改自己上网搜索)所以我使用了上面的办法自己建立一个简短的json数据包。(其实还可以像上面一样使用多次strstr获取到地区和天气的数据包,不过这里就学习一下json数据包是怎么使用的吧)
时间我这里是直接使用发送GET命令给高德地图服务器的方法获取的。ESP-01S的指令如下:
GET\r\n
ESP-01S的返回如下图
里面会返回一个实时时间如红框所示,所以我们只要将它提取出来就可以获得实时时间。但是这里返回的数据是远远超过了400bit的,所以会应用到接收中断函数里面超过了400bit就会从零开始覆盖的方法。覆盖会过滤掉我们不需要的信息,只留下我们需要的信息。
实际接收到的esp8266_buf里面的数据如下图
这里其实已经覆盖了2遍了,但是我们需要的日期时间信息没有丢失就没有问题。所以这里我们调用get_time_data()函数将需要的日期时间字符串放到processing_data数组中。
#define time_len 14 //日期时间字符串长度 char processing_data[60]; //接收esp8266_buf中处理完成的数据 /*获取日期时间信息,将需要的日期时间字符串放到processing_data数组中,等待处理*/ void get_time_data(void) { char *time_set_start; //时间开始的位置 Usart_SendString(USART2,get_time_cmd_packback, sizeof(get_time_cmd_packback)); //天气开始的位置 wait_rev(200); time_set_start = &strstr((const char*)esp8266_buf, "Date:</td>")[21]; //获取时间开始位置 strncpy(processing_data, time_set_start, time_len); //将需要的日期时间字符串放到processing_data数组中,时间长度为14 UsartPrintf(USART_DEBUG, "\r\n%s\r\n", processing_data); //串口1输出得到时间数据,用于调试 }
串口1返回如下,表明成功将日期和时间字符串提取出来了。
为了时间准确,我开启了TIM2定时器,每秒钟计数一次。每秒钟手动更新日期时间并且显示,每分钟使用网络更新天气信息和日期时间。
/*定时器中断函数*/ void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { /*timeCount计数超过60代表出错了,所以使timeCount变为初始值,重置*/ if(timeCount>60) { timeCount = 59; } timeCount++; //每秒钟timeCount+1 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除标志位 } } /*TIM2定时器初始化函数*/ void MY_TIME_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, DISABLE); }
我们知道发送给ESP8266的指令不能太快,所以前面我每次发送指令后都会延时1s。为了实时更新天气信息和时间的时分,每过1分钟我需要发送获取天气指令和获取时间指令,为了分开两个指令,当时间过去了59s时,获取天气信息。当时间过去了60s时,获取时间信息。
因为程序只会更新时间的秒数,而时间的时和分都是通过ESP-01S返回的信息实时更新的,所以需要知道实时时间的秒数,才能准确地完成刚好到一分钟时更新时间的时和分。所以这里我定义了一个timeCount 变量记录当前时间的秒数。每当timeCount 发生变化时,证明过了1s,如果timeCount 为59时,代表当前时间秒数为59s,这时我们更新天气信息。如果timeCount 为60时,代表当前时间秒数为60s,这时我们更新时间信息,及时更新时间的时分和日期,并且将当前时间的秒数提取出来更新timeCount变量。如果timeCount即不为59s也不为60s,表明只是0-57过去了1s,所以我们需要手动更新时间的秒数,并且为了中断接收不影响光照度和温湿度的每秒检测,在这里完成测量。
uint8_t timeCount = 59; //时间变量(取值范围为0-60) int main(void) { uint8_t time_pre = 0; //前一秒时间 uint8_t weather_string_len_pre = 0; //记录天气字符串长度 cJSON* json; //创建cjson类 cJSON* json_name; //创建cjson类 char time_string[time_len]; //日期时间数组 Delay_Init(); //延时初始化 OLED_Init(); //OLED初始化 BH1750_Init(); //BH1750模块初始化 // LED_Init(); // key_Init(); // My_EXTI_Init(); Usart1_Init(115200); //串口1初始化,打印信息用 Usart2_Init(115200); //串口2初始化,驱动ESP8266用 ESP8266_Init(); //ESP8266初始化 MY_TIME_Init(); //TIM2初始化 Delay_s(1); get_ip_data(); //获取IP地址信息 TIM_Cmd(TIM2, ENABLE); //使能TIM2定时器 while(1) { /*如果time_pre不等于timeCount 代表时间过去了1s*/ if(timeCount != time_pre) { /*如果timeCount为59s,获取天气信息*/ if(timeCount == 59) { OLED_ShowString(2, 14, "9"); //手动更新OLED的时间为59s get_weather_data(); //获取天气信息 json = cJSON_Parse((const char*)processing_data); //创建JSON解析对象,返回JSON格式是否正确 processing_data_clean(); //清空数组 if(json == NULL) { UsartPrintf(USART_DEBUG, "error:%s", cJSON_GetErrorPtr()); //输出json格式错误信息 } json_name = cJSON_GetObjectItem(json, "weather"); //获取weather键对应的值的信息 /*如果前一个天气字符串长度和当前天气字符串长度不同,执行一次OLED清屏,防止前一次的字符串出现*/ if(weather_string_len_pre != strlen(json_name->valuestring)) { OLED_Clear(); } UsartPrintf(USART_DEBUG, "%s\r\n", json_name->valuestring); //串口1输出得到的天气字符串,用于调试 OLED_ShowChinese(1 ,8, (uint8_t *)json_name->valuestring); //OLED显示天气 weather_string_len_pre = strlen(json_name->valuestring); //更新天气字符串长度 json_name = cJSON_GetObjectItem(json, "city"); //获取city键对应的值的信息 UsartPrintf(USART_DEBUG, "%s\r\n", json_name->valuestring); //串口1输出得到的城市名字符串,用于调试 OLED_ShowChinese(1,1,(uint8_t *)json_name->valuestring); //OLED显示城市名 cJSON_Delete(json); //释放内存 } /*如果timeCount为60s,获取时间信息*/ else if(timeCount == 60) { get_time_data(); //获取日期时间信息 /*如果没有获取到日期时间信息,重置*/ if(strlen(processing_data) < 14) { error_rst(); } strncpy(time_string, processing_data, time_len); //将日期时间信息放到time_string数组 processing_data_clean(); //清空数组 OLED_ShowString(2, 1, time_string); //OLED显示日期时间 timeCount = (time_string[time_len-2] - '0')*10 + (time_string[time_len-1] - '0'); //timeCount记录获取到的秒数 } /*如果timeCount即不为60s也不为59s,更新时间的秒数和光照度、温湿度*/ else { /*如果秒数个位为9,十位+1,个位变零*/ if(time_string[time_len-1] == '9') { time_string[time_len-2] += 1; time_string[time_len-1] = '0'; } /*如果秒数个位不为9,个位+1*/ else { time_string[time_len-1] = time_string[time_len-1] + 1; } UsartPrintf(USART_DEBUG, "\r\n%s\r\n", time_string); //串口1输出日期时间,用于调试 OLED_ShowString(2, 1, time_string); //OLED显示日期时间 BH1750_Get_Light(); //OLED更新光照度 DHT11_Get_Temp_Humidity(); //OLED更新温湿度 } time_pre = timeCount; //更新time_pre计数 } } }
效果图如下
但事实上,时间有可能会快1s或者2s,这是因为获取时间的时候,获取到的时间自己就会快1s或者2s,如下图所示,串口助手显示当前时间为49s(准确的),但是返回的时间却是51s(不准确的),这个bug我就没什么办法了。。。
本篇通过高德地图的IP定位、逆地理编码、天气查询的API成功将IP地址对应的天气信息显示到OLED上,并且每分钟实时更新,而日期和时间也成功获取到显示到OLED。还有最后一点内容是OLED如何显示中文字符和如何解决UTF-8编码的问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。