ESP32 IDF 獲取天氣資訊

2022-10-26 12:07:50

一、註冊天氣獲取賬號

我使用的知心天氣,沒有獲取天氣賬號的小夥伴可以去註冊一下,知心天氣官網:https://www.seniverse.com/
取得天氣獲取的API後,可以直接在瀏覽器中存取測試一下,如下圖所示:

這裡我就不贅述了,稍微花點資訊就可以明白天氣是怎麼獲取的了。

二、天氣資訊

獲取到的天氣格式是JSON的資料,直接在瀏覽器中不好觀察,所以我將它整理了一下,如下所示:

{
	"results":[
		{
			"location":{
				"id":"WKEZD7MXE04F",
				"name":"貴陽",
				"country":"CN",
				"path":"貴陽,貴陽,貴州,中國",
				"timezone":"Asia/Shanghai",
				"timezone_offset":"+08:00"
			},
			"daily":[
				{
					"date":"2022-10-24",
					"text_day":"多雲",
					"code_day":"4",
					"text_night":"多雲",
					"code_night":"4",
					"high":"24",
					"low":"12",
					"rainfall":"0.00",
					"precip":"0.00",
					"wind_direction":"東南",
					"wind_direction_degree":"135",
					"wind_speed":"8.4",
					"wind_scale":"2",
					"humidity":"57"
				},
				{
					"date":"2022-10-25",
					"text_day":"多雲",
					"code_day":"4",
					"text_night":"多雲",
					"code_night":"4",
					"high":"24",
					"low":"14",
					"rainfall":"0.00",
					"precip":"0.00",
					"wind_direction":"南",
					"wind_direction_degree":"180",
					"wind_speed":"8.4",
					"wind_scale":"2",
					"humidity":"62"
				},
				{
					"date":"2022-10-26",
					"text_day":"陰",
					"code_day":"9",
					"text_night":"陣雨",
					"code_night":"10",
					"high":"24",
					"low":"13",
					"rainfall":"4.63",
					"precip":"0.94",
					"wind_direction":"南",
					"wind_direction_degree":"180",
					"wind_speed":"3.0",
					"wind_scale":"1",
					"humidity":"87"
				}
			],
			"last_update":"2022-10-24T08:00:00+08:00"
		}
	]
} 


其中有些格式可能看不知道什麼意思,不要怕,看官方的註釋,如下所示:


{
  "results": [
    {
      "location": {
        "id": "C23NB62W20TF",
        "name": "西雅圖",
        "country": "US",
        "path": "西雅圖,華盛頓州,美國",
        "timezone": "America/Los_Angeles",
        "timezone_offset": "-07:00"
      },
      "now": {
        "text": "多雲", //天氣現象文字
        "code": "4", //天氣現象程式碼
        "temperature": "14", //溫度,單位為c攝氏度或f華氏度
        "feels_like": "14", //體感溫度,單位為c攝氏度或f華氏度
        "pressure": "1018", //氣壓,單位為mb百帕或in英寸
        "humidity": "76", //相對溼度,0~100,單位為百分比
        "visibility": "16.09", //能見度,單位為km公里或mi英里
        "wind_direction": "西北", //風向文字
        "wind_direction_degree": "340", //風向角度,範圍0~360,0為正北,90為正東,180為正南,270為正西
        "wind_speed": "8.05", //風速,單位為km/h公里每小時或mph英里每小時
        "wind_scale": "2", //風力等級,請參考:http://baike.baidu.com/view/465076.htm
        "clouds": "90", //雲量,單位%,範圍0~100,天空被雲覆蓋的百分比 #目前不支援中國城市#
        "dew_point": "-12" //露點溫度,請參考:http://baike.baidu.com/view/118348.htm #目前不支援中國城市#
      },
      "last_update": "2015-09-25T22:45:00-07:00" //資料更新時間(該城市的本地時間)
    }
  ]
} 

三、ESP32獲取天氣資訊

這裡我使用的是ESP-IDF環境,並且是通過 socket 的方式進行獲取

  1. socket 通訊思路如下圖所示:

  2. 建立socket連線

    函數 int socket(int domain, int type, int protocol)
    含義 函數socket()為通訊建立一個端點,併為該通訊端返回一個檔案描述符。
    返回值 int,若發生錯誤則返回-1
    domain 表示需要建立的協定。
    如:AF_INET表示IPv4,
    AF_INET6表示IPv6,
    AF_UNIX表示本地通訊端
    type 建立時,選擇需要的通行方式,如:
    SOCK_STREAM表示TCP,
    SOCK_DGRAM表示UDP,
    SOCK_SEQPACKET表示可靠的順序包服務,
    SOCK_RAW表示網路層上的原始協定
    protocol 表示指定要使用的實際傳輸協定
    最常見的有IPPROTO_TCP, IPPROTO_SCTP, IPPROTO_UDP, IPPROTO_DCCP等。
    如果填0(IPPRORO_IP)則根據前兩個引數自動選擇協定
    /* 建立通訊端 */
    socket_handle = socket(dns_info->ai_family, dns_info->ai_socktype, 0); // 0(IPPROTO_IP)可以用來表示選擇一個預設的協定。
    if(socket_handle < 0) {
        ESP_LOGE(TAG, "... Failed to allocate socket");
        close(socket_handle);
        freeaddrinfo(dns_info);
        false;
    }
    
    
  3. 連線 connect
    連線時需要用到伺服器的資訊,而獲取天氣資訊是通過域名的方式獲取的,在連線之前,我們需要使用getaddrinfo()函數進行DNS解析

    /* 域名解析 */
    int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &dns_info);
    if(err != 0 || dns_info == NULL) {
        ESP_LOGE(TAG, "DNS lookup failed err=%d dns_info=%p", err, dns_info);
        return false;
    }
    
    /* 連線伺服器 */
    if(connect(socket_handle, dns_info->ai_addr, dns_info->ai_addrlen) != 0) {
        ESP_LOGE(TAG, "... socket connect failed errno=%d", errno);
        close(socket_handle);
        freeaddrinfo(dns_info);
        false;
    }
    
    
  4. 通過寫資料,傳送get請求

    /* 想緩衝區中寫入服務請求資訊 */
    if (write(socket_handle, REQUEST, strlen(REQUEST)) < 0) {
        ESP_LOGE(TAG, "... socket send failed");
        close(socket_handle);
        false;
    }
    
    
  5. 設定請求超時

    /* 設定請求超時 */
    struct timeval receiving_timeout;
    receiving_timeout.tv_sec = 5;
    receiving_timeout.tv_usec = 0;
    if (setsockopt(socket_handle, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout, sizeof(receiving_timeout)) < 0) 
    {
        ESP_LOGE(TAG, "... failed to set socket receiving timeout");
        close(socket_handle);
        false;
    }
    
  6. 通過讀取資料,獲取get響應資料

    bzero(weather_buf, buf_size);                                  // 將記憶體 weather_buf 前的 sizeof(weather_buf) 全部設定為0
    int read_size = read(socket_handle, weather_buf, buf_size-1);  // 從緩衝區中讀取指定長度的資料,當緩衝區中內容小於指定長度時,read() 返回實際讀取的資料長度,
    ESP_LOGI(TAG, "get weather is:   %s", weather_buf);           // 列印獲取的天氣資訊
    
    

四、天氣獲取案例

#include "lvgl_weather_view.h"
#include "cJSON.h"
#include "../../wifi/wifi.h"

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"

/* 獲取天氣的地址 */
#define WEB_SERVER "api.seniverse.com"                              // 伺服器域名
#define WEB_PORT "80"                                               // 伺服器埠
#define WEB_PATH "https://api.seniverse.com/v3/weather/daily.json?key=xxxxxx=Guiyang&language=zh-Hans"          // 天氣獲取路徑 

/* 存放json解析後的天氣資訊 */
static lvgl_user_weather_info_t user_weather_info = {0};

static const char *REQUEST = "GET " WEB_PATH " HTTP/1.0\r\n"
    "Host: "WEB_SERVER":"WEB_PORT"\r\n"
    "User-Agent: esp-idf/1.0 esp32\r\n"
    "\r\n";

/**
 * @brief 獲取天氣資料
 * 
 * @param weather_buf 天氣資料的儲存空間
 * @param buf_size 儲存空間的大小
 * @return true 獲取成功
 * @return false 獲取失敗
 */
static bool get_weather_buf(char *weather_buf, size_t buf_size)
{
    const struct addrinfo hints = {
        .ai_family = AF_INET,                       // AF_INET表示IPv4,AF_INET6表示IPv6
        .ai_socktype = SOCK_STREAM,                 // SOCK_STREAM表示TCP、SOCK_DGRAM表示UDP、SOCK_RAW表示RAW
    };
    struct addrinfo *dns_info;                      // DNS 解析資訊
    int socket_handle;                              // socket控制程式碼

    /* 域名解析 */
    int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &dns_info);
    if(err != 0 || dns_info == NULL) {
        ESP_LOGE(TAG, "DNS lookup failed err=%d dns_info=%p", err, dns_info);
        return false;
    }

    /* 列印解析的伺服器 IP */
    struct in_addr *service_IP = &((struct sockaddr_in *)dns_info->ai_addr)->sin_addr;
    ESP_LOGI(TAG, "DNS lookup succeeded. IP=%s", inet_ntoa(*service_IP));

    /* 建立通訊端 */
    socket_handle = socket(dns_info->ai_family, dns_info->ai_socktype, 0);            // 0(IPPROTO_IP)可以用來表示選擇一個預設的協定。
    if(socket_handle < 0) {
        ESP_LOGE(TAG, "... Failed to allocate socket");
        close(socket_handle);
        freeaddrinfo(dns_info);
        false;
    }
    // ESP_LOGI(TAG, "allocated socket... ");

    /* 連線伺服器 */
    if(connect(socket_handle, dns_info->ai_addr, dns_info->ai_addrlen) != 0) {
        ESP_LOGE(TAG, "... socket connect failed errno=%d", errno);
        close(socket_handle);
        freeaddrinfo(dns_info);
        false;
    }
    // ESP_LOGI(TAG, "... connected");

    /* 釋放 dns_info 指向的空間 */
    freeaddrinfo(dns_info);  

    /* 想緩衝區中寫入服務請求資訊 */
    if (write(socket_handle, REQUEST, strlen(REQUEST)) < 0) {
        ESP_LOGE(TAG, "... socket send failed");
        close(socket_handle);
        false;
    }
    // ESP_LOGI(TAG, "... socket send success");


    /* 設定請求超時 */
    struct timeval receiving_timeout;
    receiving_timeout.tv_sec = 5;
    receiving_timeout.tv_usec = 0;
    if (setsockopt(socket_handle, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout, sizeof(receiving_timeout)) < 0) 
    {
        ESP_LOGE(TAG, "... failed to set socket receiving timeout");
        close(socket_handle);
        false;
    }
    // ESP_LOGI(TAG, "... set socket receiving timeout success");

    /* 從緩衝區中讀取天氣資訊 */
    bzero(weather_buf, buf_size);                                                   // 將記憶體 weather_buf 前的 sizeof(weather_buf) 全部設定為0
    int read_size = read(socket_handle, weather_buf, buf_size-1);                   // 從緩衝區中讀取指定長度的資料,當緩衝區中內容小於指定長度時,read() 返回實際讀取的資料長度,
    ESP_LOGI(TAG, "get weather is:   %s", weather_buf);                            // 列印獲取的天氣資訊

    ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d.", read_size, errno);     // 列印讀取到的資料長度
    close(socket_handle);
    return true;
}

五、JSON資料解析

/**
 * @brief 解析天氣資料(JSON)
 * 
 * @param analysis_buf 資料的儲存空間
 * @return true 解析成功
 * @return false 解析失敗
 */
static bool parse_json_data(const char *analysis_buf)
{
    cJSON   *json_data = NULL;
    /* 擷取有效json */
    char *index = strchr(analysis_buf, '{');
    // strcpy(weather_buf, index);
    
    json_data = cJSON_Parse(index);
    if( json_data == NULL ) // 判斷欄位是否json格式
    {
        return false;
    }  

    // ESP_LOGI(TAG, "Start parsing data");   
    cJSON* cjson_item =cJSON_GetObjectItem(json_data,"results");
    cJSON* cjson_results =  cJSON_GetArrayItem(cjson_item,0);

    /* 獲取天氣的地址 */ 
    cJSON* cjson_location = cJSON_GetObjectItem(cjson_results,"location");
    cJSON* cjson_temperature_name = cJSON_GetObjectItem(cjson_location,"name");
    strcpy(user_weather_info.location_name,cjson_temperature_name->valuestring);

    /* 天氣資訊 */
    cJSON* cjson_daily = cJSON_GetObjectItem(cjson_results,"daily");

    /* 當天的天氣資訊 */
    cJSON* cjson_daily_1 =  cJSON_GetArrayItem(cjson_daily,0);

    ESP_LOGI(TAG, "day_one_code is: %s", cJSON_GetObjectItem(cjson_daily_1,"code_day")->valuestring); 
    ESP_LOGI(TAG, "day_one_temp_high is: %s", cJSON_GetObjectItem(cjson_daily_1,"high")->valuestring); 
    ESP_LOGI(TAG, "day_three_temp_low is: %s", cJSON_GetObjectItem(cjson_daily_1,"low")->valuestring); 
    ESP_LOGI(TAG, "day_one_humi is: %s", cJSON_GetObjectItem(cjson_daily_1,"humidity")->valuestring); 
    ESP_LOGI(TAG, "day_one_windspeed is: %s", cJSON_GetObjectItem(cjson_daily_1,"wind_speed")->valuestring); 

注意:解析JSON資料時,使用的都是 valuestring 資料型別,否則會出現無法解析的現象

參考文獻

ESP32學習筆記(12)——JSON介面使用:https://blog.csdn.net/qq_36347513/article/details/116481167
ESP32學習筆記(14)——HTTP伺服器 - 簡書:https://www.jianshu.com/p/aa865ff71b05
ESP32_IDF學習8【HTTP伺服器】 - redlightASl - 部落格園:https://www.cnblogs.com/redlightASl/p/15542579.html
ESP32 之 ESP-IDF 教學(十二)WiFi篇—— LwIP 之 TCP 通訊:https://blog.csdn.net/m0_50064262/article/details/120265731>