第12章 WiFi扫描
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:10-scan.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
12.1 例程介绍
本例程,可以扫描到设备附近的wifi名称,并把扫描到的wifi名称、wifi强度等相关信息显示到串口终端。虽然本例程并没有接入wifi网络,但它是wifi接入网络前的一项非常重要的工作。
12.2 编写程序
我们直接复制scan这个例程到你的实验文件夹,该例程位于esp-idf-v5.1.3\examples\wifi\scan。
复制过来后,使用VSCode打开工程。
我们直接配置串口号、芯片型号、menuconfig等就可以,不需要修改代码。
menuconfig中,除了要修改FLASH大小为8MB之外,还可以修改扫描wifi名称后列出的最大数量,默认是10。
I (3019) scan: Total APs scanned = 12
I (3019) scan: SSID CMCC-2D2K
I (3019) scan: RSSI -38
I (3019) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3019) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3029) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3029) scan: Channel 6
I (3039) scan: SSID TP-LINK_0938
I (3039) scan: RSSI -73
I (3039) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3049) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3049) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3059) scan: Channel 1
I (3059) scan: SSID TP-LINK_BB5E
I (3069) scan: RSSI -73
I (3069) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3079) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3079) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3089) scan: Channel 11
I (3089) scan: SSID CMCC-4R7s
I (3089) scan: RSSI -79
I (3099) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3099) scan: Pairwise Cipher WIFI_CIPHER_TYPE_TKIP_CCMP
I (3109) scan: Group Cipher WIFI_CIPHER_TYPE_TKIP
I (3109) scan: Channel 1
I (3119) scan: SSID FAST_D5C4
I (3119) scan: RSSI -87
I (3129) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3129) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3139) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3139) scan: Channel 12
I (3149) scan: SSID LK29-1-603-2.4G
I (3149) scan: RSSI -90
I (3149) scan: Authmode WIFI_AUTH_WPA2_PSK
I (3159) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3159) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3169) scan: Channel 11
I (3169) scan: SSID CMCC-FYNF
I (3179) scan: RSSI -91
I (3179) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3189) scan: Pairwise Cipher WIFI_CIPHER_TYPE_TKIP_CCMP
I (3189) scan: Group Cipher WIFI_CIPHER_TYPE_TKIP
I (3199) scan: Channel 2
I (3199) scan: SSID GRF
I (3199) scan: RSSI -92
I (3209) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3209) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3219) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3219) scan: Channel 6
I (3229) scan: SSID CMCC-JSDg
I (3229) scan: RSSI -93
I (3229) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3239) scan: Pairwise Cipher WIFI_CIPHER_TYPE_TKIP_CCMP
I (3249) scan: Group Cipher WIFI_CIPHER_TYPE_TKIP
I (3249) scan: Channel 5
I (3249) scan: SSID CMCC-ypWj
I (3259) scan: RSSI -94
I (3259) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3269) scan: Pairwise Cipher WIFI_CIPHER_TYPE_TKIP_CCMP
I (3269) scan: Group Cipher WIFI_CIPHER_TYPE_TKIP
I (3279) scan: Channel 5
I (3279) main_task: Returned from app_main()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
以上是我的开发板输出的结果,我截取了从Total APs scanned之后的内容。Total APs scanned是扫描到的wifi数量,我的开发板扫描到了12个wifi名称,后面列出了10个,因为我们在menuconfig里面设置的最大输出10个。这些wifi名称,是按照信号强弱顺序列出的。
SSID是wifi名称,RSSI是信号强度。
12.3 程序分析
接下来,我们分析一下源程序。
找到app_main函数,如下代码所示,由两部分代码组成,第一部分是初始化nvs,第二部分是执行wifi_scan。
void app_main(void)
{
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK( ret );
wifi_scan();
}
2
3
4
5
6
7
8
9
10
11
12
这里涉及到了ESP32的“分区表”相关知识。在我们的开发板上,ESP32通过SPI接口连接了一个FLASH芯片。这个FLASH芯片,除了负责保存我们的写的应用程序之外,还可以作为其它用途。这就涉及到了分区,把其中一部分划分给应用程序,其它部分,再划分为其它用途。关于分区表的详细介绍,大家可以看一下官方分区表介绍。 官方分区表介绍:https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.3/esp32c3/api-guides/partition-tables.html 在所有的例程终端输出中,都会输出当前的分区表信息,我们可以看一下此例程的终端输出,找到它的分区表信息,如下所示:
I (53) boot: Partition Table:
I (57) boot: ## Label Usage Type ST Offset Length
I (64) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (72) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (79) boot: 2 factory factory app 00 00 00010000 00100000
I (87) boot: End of partition table
2
3
4
5
6
这个分区表,把FLASH划分了3个部分。序号为0的,是nvs分区,我们这个例程app_main最开始初始化nvs,就是负责初始化这个部分的。序号为1的,是phy_init分区,负责硬件射频配置数据。序号为2的,就是我们自己写的应用程序。在这个表中,每个分区,都定义好了它的地址(Offset)和占用空间大小(Length)。
nvs分区就是用来配置wifi数据的,凡是涉及到需要使用wifi外设的应用程序,必须先初始化nvs分区。
接下来我们再看看wifi_scan这个函数里面又做了哪些工作。
/* Initialize Wi-Fi as sta and set scan method */
static void wifi_scan(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
uint16_t number = DEFAULT_SCAN_LIST_SIZE;
wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE];
uint16_t ap_count = 0;
memset(ap_info, 0, sizeof(ap_info));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
esp_wifi_scan_start(NULL, true);
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info));
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
ESP_LOGI(TAG, "Total APs scanned = %u", ap_count);
for (int i = 0; (i < DEFAULT_SCAN_LIST_SIZE) && (i < ap_count); i++) {
ESP_LOGI(TAG, "SSID \t\t%s", ap_info[i].ssid);
ESP_LOGI(TAG, "RSSI \t\t%d", ap_info[i].rssi);
print_auth_mode(ap_info[i].authmode);
if (ap_info[i].authmode != WIFI_AUTH_WEP) {
print_cipher_type(ap_info[i].pairwise_cipher, ap_info[i].group_cipher);
}
ESP_LOGI(TAG, "Channel \t\t%d\n", ap_info[i].primary);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
以上代码,按照顺序,分为wifi的初始化阶段、配置阶段、启动阶段。因为只扫描名称,没有连接,所以没有执行后面的几个wifi阶段。关于wifi的所有阶段介绍,可以通过下面链接查看。
官方推荐wifi各阶段流程:https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.3/esp32c3/api-guides/wifi.html#esp32-c3-wi-fi-station
上述代码中,第4~10行,是wifi初始化阶段。esp_netif_init()函数负责创建一个 LwIP 核心任务,并初始化 LwIP 相关工作。esp_event_loop_create_default()创建系统事件任务和回调函数,wifi的事件会在回调函数中处理。
esp_netif_create_default_wifi_sta()函数创建有 TCP/IP 堆栈的默认网络接口。esp_wifi_init()函数创建Wi-Fi驱动程序任务,并初始化Wi-Fi驱动程序。
第17行为wifi配置阶段。esp_wifi_set_mode(WIFI_MODE_STA)函数将Wi-Fi模式配置为station模式。
第18行为wifi启动阶段。esp_wifi_start()负责启动WI-FI驱动程序。
第19行开始的代码,就是wifi扫描部分了。扫描后,把扫描结果输出,对照输出结果,就可以看懂这里的语句了。
第13章 WiFi连接
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:11-station.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
13.1 STA模式介绍
一般情况下,wifi有两种模式,STA和AP,AP就是热点,STA就是站点。例如,wifi路由器是AP,手机是STA。不过,ESP32-C3除了支持仅STA模式和仅AP模式,还支持STA和AP模式共存。
本章节,我们把开发板当作STA模式,连接你家里面的路由器wifi,或者办公室的wifi。
13.2 编写程序
我们复制官方例程station到我们的实验文件夹。官方station例程路径是esp-idf-v5.1.3\examples\wifi\getting_started\station。
复制好以后,我们使用VSCode打开我们实验文件夹中的station例程文件夹。
我们直接配置串口号、芯片型号、menuconfig等就可以,不需要修改代码。
menuconfig中,除了要修改FLASH大小为8MB之外,还需要配置Example Configuration中的内容。如下图所示,我已经把wifi名称和密码修改好了,其它的不用修改。点击保存关闭。
I (1689) esp_netif_handlers: sta ip: 192.168.1.10, mask: 255.255.255.0, gw: 192.168.1.1
I (1689) wifi station: got ip:192.168.1.10
I (1689) wifi station: connected to ap SSID:CMCC-2D2K password:sedf4693
I (1699) main_task: Returned from app_main()
2
3
4
出现上面输出,说明已经连接到了wifi上。
13.3 程序分析
接下来我们分析源代码。 先看app_main函数。
void app_main(void)
{
//Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
wifi_init_sta();
}
2
3
4
5
6
7
8
9
10
11
12
13
主函数结构和“WIFI扫描”例程一样,也是分为两个部分,第一个部分是第4~9行,初始化nvs。第二个部分是第12行,执行wifi_init_sta()函数。wifi_init_sta()函数,就在app_main函数的前面。 在这个例程中,使用了freeRTOS的事件通信机制。 首先定义了一个静态全局事件组句柄s_wifi_event_group。
/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;
2
然后在wifi_init_sta()函数的最开始处,使用xEventGroupCreate()创建了事件组,并返回给s_wifi_event_group句柄。
s_wifi_event_group = xEventGroupCreate();
再往后使用esp_event_handler_instance_register()函数注册了WIFI_EVENT和IP_EVENT事件,并且定义了事件处理函数名称为event_handler。 在event_handler()函数中,根据收到的事件内容,使用xEventGroupSetBits()函数来设置位的状态。
if(...){
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}else if(...){
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
2
3
4
5
WIFI_FAIL_BIT和WIFI_CONNECTED_BIT实际上就是事件组的BIT0和BIT1,这个是在文件开始处使用宏定义的。
/* The event group allows multiple bits for each event, but we only care about two events:
* - we are connected to the AP with an IP
* - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
2
3
4
5
最后在wifi_init_sta()函数中,使用xEventGroupWaitBits()函数等待事件的发生。最后一个参数为portMAX_DELAY,表示将一直等待,直到WIFI_CONNECTED_BIT 事件或WIFI_FAIL_BIT事件的发生。
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
* number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
2
3
4
5
6
7
关于wifi的函数,和上一个章节的差不多,不理解的可以看上一个章节的教程。
第14章 网络授时(SNTP)
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:12-sntp.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
14.1 SNTP简介
SNTP的全称是Simple Network Time Protocol,意思是简单网络时间协议,用来从网络中获取当前的时间,也可以称为网络授时。 例程中,会使用LwIP SNTP模块从服务器(pool.ntp.org)获取时间。
14.2 程序设计
我们直接使用官方提供的sntp例程,官方sntp例程路径为D:\Espressif\frameworks\esp-idf-v5.1.3\examples\protocols\sntp。复制到我们的实验文件夹,路径为D:\esp32c3\sntp,不用修改名字。
使用VSCode打开sntp文件夹。
程序不需要修改,直接配置好串口、目标芯片,menuconfig就可以下载看结果。
menuconfig中,除了把flash大小修改为8MB以外,还需要添加你要连接wifi的名称和密码。
添加好之后,保存关闭,编译下载到开发板,看终端结果。I (10319) example: The current date/time in New York is: Wed Jan 31 06:54:54 2024
I (10319) example: The current date/time in Shanghai is: Wed Jan 31 19:54:54 2024
I (10329) example: Entering deep sleep for 10 seconds
2
3
在终端,最终打印出当前的纽约时间和上海时间,打印完之后,休眠10秒钟,再打印。
14.3 程序分析
本例程也比较简单,只有一个c文件,点击打开main下面的sntp_example_main.c文件。你可以先浏览一下这个c文件,一共有4个函数,分别是app_main()、obtain_time()、time_sync_notification_cb()、print_servers()。
app_main()中调用了obtain_time()获取时间函数,obtain_time()函数中又调用了剩下的两个函数,后面两个函数只起到了终端打印信息提示作用。 我们先看app_main函数,最开始的两行代码如下所示:
++boot_count;
ESP_LOGI(TAG, "Boot count: %d", boot_count)
2
前面我们已经知道,程序每10秒钟会唤醒一次,每次唤醒后,boot_count值会加1,并使用ESP_LOGI把boot_count的值打印到终端。这里用来表示唤醒和重启的不同,如果是重启,boot_count的值永远都是1。 接下来的代码片段如下所示:
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
// Is time set? If not, tm_year will be (1970 - 1900).
if (timeinfo.tm_year < (2016 - 1900)) {
ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP.");
obtain_time();
// update 'now' variable with current time
time(&now);
}
2
3
4
5
6
7
8
9
10
11
time_t now定义一个64位变量now,使用time(&now)来获取系统当前时间,获取到的是一个64位的数字。
struct tm timeinfo定义一个结构体变量timeinfo,使用localtime_r(&now, &timeinfo)把64位的数字时间,各自提取到该结构体中的年月日时分秒等变量中。
struct tm {
int tm_sec; /* 秒 - 取值区间为[0,59] */
int tm_min; /* 分 - 取值区间为[0,59] */
int tm_hour; /* 时 - 取值区间为[0,23] */
int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
int tm_mon; /* 月份(从一月开始,0代表一月)- 取值区间为[0,11] */
int tm_year; /* 年份,其值等于实际年份减去1900 */
int tm_wday; /* 星期–取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 */
int tm_yday; /* 从每年的1月1日开始的天数 – 取值区间为[0,365],其中0代表1月1日,以此类推 */
int tm_isdst; /*夏令时标识符,实行夏令时的时候,为1。不实行夏令时的进候,为0;不了解情况时,为-1。*/
};
2
3
4
5
6
7
8
9
10
11
使用上面提到的变量类型和函数,需要包含c语言的标准库函数<time.h>。 接下来,使用if判断结构体变量timeinfo中的成员tm_year的值。这条语句前面的注释,已经给到我们一个提示,如果tm_year的值没有设置过,将等于70。也就是说,如果开发板第一次执行这个程序,这个值将是70。如果现在是睡眠唤醒后执行到这里,今年是2024年,tm_year的值就是124,这个值需要加上1900才是当前的实际年份。
obtain_time()函数用来从网络上获取当前时间。我们假设obtain_time函数已经执行完,继续看app_main函数。
接下来的if条件编译我们没有在menuconfig中配置这个选项,所以不会执行。条件编译之后的语句如下所示:
char strftime_buf[64];
// Set timezone to Eastern Standard Time and print local time
setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in New York is: %s", strftime_buf);
// Set timezone to China Standard Time
setenv("TZ", "CST-8", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面代码中,先定义一个strftime_buf字符数组,用来存放最后将要打印的字符。 接下来的两个片段,分别设置为纽约时间和上海时间。 setenv()和tzset()设置时区。 strftime()函数根据timeinfo中的成员值把日期时间组合成一串字符,存储到strftime_buf数组中。 最后通过ESP_LOGI打印出strftime_buf数组中的字符串。 接下来的if语句,也不会执行,因为我们没有在menuconfig中设置SNTP_SYNC_MODE_SMOOTH。 最后的3条语句,如下所示:
const int deep_sleep_sec = 10;
ESP_LOGI(TAG, "Entering deep sleep for %d seconds", deep_sleep_sec);
esp_deep_sleep(1000000LL * deep_sleep_sec);
2
3
esp_deep_sleep()函数把ESP32-C3设置为睡眠状态,10秒后自动唤醒。 以上就是app_main函数的执行流程。 接下来我们看obtain_time函数,看看时间是怎么获取到的。 obtain_time函数的前3条语句如下所示:
ESP_ERROR_CHECK( nvs_flash_init() );
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK( esp_event_loop_create_default() );
2
3
通过上两个章节的学习,我们知道,以上3条语句用来做连接wifi之前的准备工作。 之后有一个if条件编译,我们没有设置,也不会执行,这里直接跳过分析。 接下来的一条语句如下所示,用来连接wifi:
ESP_ERROR_CHECK(example_connect());
接下来的if条件编译,也不会执行,会执行它的else条件编译后的语句,用来打印信息。
ESP_LOGI(TAG, "Initializing and starting SNTP");
接下来的if条件编译,当服务器数量大于1才会执行,我们只设置了一个获取时间的服务器,所以也不会执行。会执行else条件编译后的语句:
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_SNTP_TIME_SERVER);
这条语句定义了一个esp_sntp_config_t类型变量config,并给该变量配置了默认值。 接下来定义了一个回调函数time_sync_notification_cb。
config.sync_cb = time_sync_notification_cb; // Note: This is only needed if we want
我们把鼠标放到这个函数上面,单击右键选择“转到定义”,函数原型如下所示:
void time_sync_notification_cb(struct timeval *tv)
{
ESP_LOGI(TAG, "Notification of a time synchronization event");
}
2
3
4
该函数的目的只是用来打印信息,提示发生时间同步事件。 点击软件最上面的向左的箭头←,回到obtain_time函数中刚才的位置。 接下来的if条件编译也不会执行。 再接下来的语句如下所示,初始化sntp。
esp_netif_sntp_init(&config);
再接下来执行打印服务器名称的函数,如下所示:
print_servers();
把鼠标放到这个函数上面单击右键,选择“转到定义”,函数原型如下所示:
static void print_servers(void)
{
ESP_LOGI(TAG, "List of configured NTP servers:");
for (uint8_t i = 0; i < SNTP_MAX_SERVERS; ++i){
if (esp_sntp_getservername(i)){
ESP_LOGI(TAG, "server %d: %s", i, esp_sntp_getservername(i));
} else {
// we have either IPv4 or IPv6 address, let's print it
char buff[INET6_ADDRSTRLEN];
ip_addr_t const *ip = esp_sntp_getserver(i);
if (ipaddr_ntoa_r(ip, buff, INET6_ADDRSTRLEN) != NULL)
ESP_LOGI(TAG, "server %d: %s", i, buff);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在我们输出终端打印内容的下面两条就是该函数打印出的内容。
I (6439) example: List of configured NTP servers:
I (6449) example: server 0: pool.ntp.org
2
点击软件最上面的向左的箭头←,回到obtain_time函数中刚才的位置。 再接下来的几条语句如下所示:
// wait for time to be set
time_t now = 0;
struct tm timeinfo = { 0 };
int retry = 0;
const int retry_count = 15;
while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) {
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
}
time(&now);
localtime_r(&now, &timeinfo);
2
3
4
5
6
7
8
9
10
esp_netif_sntp_sync_wait()函数用来从服务器获取时间。间隔2秒获取一次,直到成功获取到时间,retry_count 定义了最多的获取次数。 最后的两条语句如下所示:
ESP_ERROR_CHECK( example_disconnect() );
esp_netif_sntp_deinit();
2
example_disconnect()函数用来断开网络连接。 esp_netif_sntp_deinit()函数用来清除初始化sntp配置。
第15章 获取天气信息
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:13-weather.zip
上面是已经做好的例程。直接使用本例程,需要将例程中https_with_url函数中的URL地址中的xxx修改成你自己的地区ID号和密钥才可以使用。具体方法,请看下面的教程。
下面是本例程制作的全部过程。
15.1 网页端获取天气信息
可以获取天气数据的开放平台有和风、心知、高德、百度等平台,本示例,我们从和风天气获取天气信息。如果你之前没有使用过和风天气开发服务平台,需要注册账号。
和风天气开发服务平台:https://dev.qweather.com/ 登录和风天气开发服务平台后,进入“开发服务控制台”,点击左侧“项目管理”,在右侧点击“创建项目”,然后填写“项目名称”,选择“免费订阅”,设置KEY为“Web API”,并给KEY起个名称,最后点击“创建”。下图是我创建好的项目。
https://devapi.qweather.com/v7/weather/now?{查询参数}
其中{查询参数},必须包含的是location和KEY,location是要查询的地理位置,key是你自己的key,点击上图中“KEY”下面的“查看”,可以复制到。完整的查询网址如下所示:
https://devapi.qweather.com/v7/weather/now?location=xxx&key=xxx
其中的xxx需要替换成你自己要查询的参数。location号码,可以通过下面的链接获得。
https://geoapi.qweather.com/v2/city/lookup?location=深圳&key=xxx
上面链接中,“深圳”是你要查询的地理位置,你需要替换成你自己的,key也需要替换成你自己的,然后复制粘贴到浏览器后回车,就可以在网页中查找对应地址的id。如下所示,在网页中,还会出现城市对应的行政区,例如,“深圳”市中有“福田区”。
{
"code": "200",
"location": [
{
"name": "深圳",
"id": "101280601",
"lat": "22.54700",
"lon": "114.08595",
"adm2": "深圳",
"adm1": "广东省",
"country": "中国",
"tz": "Asia/Shanghai",
"utcOffset": "+08:00",
"isDst": "0",
"type": "city",
"rank": "13",
"fxLink": "https://www.qweather.com/weather/shenzhen-101280601.html"
},
...此处省略一部分...
{
"name": "福田",
"id": "101280603",
"lat": "22.54101",
"lon": "114.05096",
"adm2": "深圳",
"adm1": "广东省",
"country": "中国",
"tz": "Asia/Shanghai",
"utcOffset": "+08:00",
"isDst": "0",
"type": "city",
"rank": "25",
"fxLink": "https://www.qweather.com/weather/futian-101280603.html"
},
...此处省略一部分...
],
"refer": {
"sources": [
"QWeather"
],
"license": [
"QWeather Developers License"
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
现在我们可以查询到“福田区”的id号是101280603,现在我们把id号复制粘贴到查询天气的网址中,然后复制你的Key到网址中,假设我的key是d5a4d4as4d4f3as4df4sa,那么最终查询天气的网址就是:
https://devapi.qweather.com/v7/weather/now?location=101280603&key=d5a4d4as4d4f3as4df4sa
把修改好的网址复制粘贴到浏览器回车,就可以查询到当地的天气信息了,如下所示:
{
"code": "200",
"updateTime": "2024-02-02T18:04+08:00",
"fxLink": "https://www.qweather.com/weather/futian-101280603.html",
"now": {
"obsTime": "2024-02-02T17:49+08:00",
"temp": "23",
"feelsLike": "24",
"icon": "101",
"text": "多云",
"wind360": "33",
"windDir": "东北风",
"windScale": "2",
"windSpeed": "7",
"humidity": "77",
"precip": "0.0",
"pressure": "1008",
"vis": "30",
"cloud": "91",
"dew": "18"
},
"refer": {
"sources": [
"QWeather"
],
"license": [
"CC BY-SA 4.0"
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
以上就是使用网址查询当地天气信息的方法,接下来在开发板上通过网络请求来查询天气信息。
15.2 esp_http_client例程介绍
我们在官方例程esp_http_client的基础上修改程序,来获取天气信息。esp_http_client例程可以实现访问http和https网络协议的网站。关于http和https的详细区别,大家可以去搜索学习相关的网络知识,这里我们只需要知道,以前访问网站,都是用http协议,近年来为了提高安全性,大部分网站都改成了https协议。https协议相比http协议,增加了加密传输和身份认证协议。例如我们要访问的和风天气,必须使用https协议。
复制D:\Espressif\frameworks\esp-idf-v5.1.3\examples\protocols\esp_http_client文件夹到我们自己的工程文件夹,并改名为weather。
使用VSCode打开weather文件夹。
本工程只有一个c文件esp_http_client_example.c,我们点击打开它,浏览一下它的内容,发现里面有很多的函数。我们拉到最底下找到app_main函数,在app_main函数里面,我们发现它只是连接wifi后,执行了一个http_test_task任务。http_test_task任务函数就在app_main函数的前面,我们找到它,发现在这个任务函数中,依次调用了前面的所有函数。看到这里,我们就大概了解这个c文件的结构了。
在http_test_task任务函数中调用的那么多函数,我们可以大概去前面浏览一下,各函数都是以不同的方式访问服务器,用途是一样的。我们接下来获取天气,只使用其中的https_with_url()函数。
15.3 配置工程
在http_test_task任务函数中,只留下https_with_url()函数,和最后的任务删除语句,其它的都注释掉或者删除,最后的代码如下所示:
static void http_test_task(void *pvParameters)
{
// http_rest_with_url();
// http_rest_with_hostname_path();
// #if CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH
// http_auth_basic();
// http_auth_basic_redirect();
// #endif
// #if CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH
// http_auth_digest();
// #endif
// http_encoded_query();
// http_relative_redirect();
// http_absolute_redirect();
// http_absolute_redirect_manual();
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
https_with_url();
#endif
// https_with_hostname_path();
// http_redirect_to_https();
// http_download_chunk();
// http_perform_as_stream_reader();
// https_async();
// https_with_invalid_url();
// http_native_request();
// #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
// http_partial_download();
// #endif
ESP_LOGI(TAG, "Finish http example");
#if !CONFIG_IDF_TARGET_LINUX
vTaskDelete(NULL);
#endif
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
其中的if条件编译,CONFIG_MBEDTLS_CERTIFICATE_BUNDLE默认是勾选的,所以https_with_url()函数是会被执行的。再往后的条件编译,判断目标芯片是否不是LINUX系统,如果不是,就会执行任务删除,这里默认没有勾选LINUX系统,所以任务删除函数会执行。
关于CONFIG_MBEDTLS_CERTIFICATE_BUNDLE的勾选,大家可以去menuconfig中查看,注意,打开menuconfig之前,一定要先选择目标芯片为esp32-c3。在menuconfig搜索框中输入bundle,就可以看到。
.url = "https://devapi.qweather.com/v7/weather/now?&location=xxx&key=xxx",
注意把两个xxx替换成你自己的,方法在第1小节。 然后我们找到http_rest_with_url()函数,复制config配置中的下面一条语句放到https_with_url()函数中的config成员中。
.user_data = local_response_buffer, // Pass address of local buffer to get response
上面代码中的local_response_buffer是个数组,需要定义,再把http_rest_with_url()函数最开始的此数组定义复制粘贴到https_with_url()函数的最开始处。
char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};
这个数组用来存放获取到的网页内容,用在本例程,就可以获取到天气数据。 修改好后的代码如下所示:
static void https_with_url(void)
{
char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};
esp_http_client_config_t config = {
.url = "https://devapi.qweather.com/v7/weather/now?&location=xxx&key=xxx",
.event_handler = _http_event_handler,
.crt_bundle_attach = esp_crt_bundle_attach,
.user_data = local_response_buffer, // Pass address of local buffer to get response
};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %"PRIu64,
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
} else {
ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
再次注意,把url中的xxx替换成你自己的才可以用。 然后,现在配置好串口、目标芯片、menuconfig中,把FLASH大小改成8MB,然后填入你家路由器的wifi密码和名称,就可以编译下载看一下结果。关于如何修改FLASH大小为8MB,以及如何填入wifi密码和名称,可以看之前的章节。
I (5431) HTTP_CLIENT: Connected to AP, begin http example
I (5441) main_task: Returned from app_main()
I (5781) esp-x509-crt-bundle: Certificate validated
I (6271) HTTP_CLIENT: HTTPS Status = 200, content_length = 322
I (6271) HTTP_CLIENT: HTTP_EVENT_DISCONNECTED
I (6271) HTTP_CLIENT: Finish http example
2
3
4
5
6
以上是我截取的终端输出中最后的几条语句。第3行表示证书已验证,这个是https才有的,http没有。第4行的HTTPS状态码是200,代表网页响应正常,content_length是内容长度,表示接收到的字节数,这个字节数,是会随时变化的,毕竟,“晴”和“多云”的字节数就不一样。
15.4 编写gzip解压程序
到现在为止,我们已经获取到了天气数据,但是还没有解析出来。在解析之前,需要知道的是,我们接收到的天气数据,现在是gzip压缩包,这个在和风天气官方也有提示。我们需要对它进行解压,才可以解析。
解压gzip包,可以使用zlib C库函数,我们打开乐鑫组件管理工具页面。
乐鑫组件管理工具:https://components.espressif.com/ 在搜索框输入zlib,结果中找到zlib,点击进入。
PS D:\esp32c3\weather> idf.py add-dependency "espressif/zlib^1.3.0"
Executing action: add-dependency
Created "D:\esp32c3\weather\main\idf_component.yml"
Successfully added dependency "espressif/zlib^1.3.0" to component "main"
PS D:\esp32c3\weather>
2
3
4
5
然后你会发现工程中多了一个idf_compoent.yml文件。
点击打开这个文件,会发现里面有zlib的组件依赖:espressif/zlib: "^1.3.0" 然后在esp_http_client_example.c中包含zlib.h库。#include "zlib.h"
接下来我们要写一个解压函数,如下所示:
int gzipDecompress(char *src, int srcLen, char *dst, int* dstLen)
{
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = srcLen;
strm.avail_out = *dstLen;
strm.next_in = (Bytef *)src;
strm.next_out = (Bytef *)dst;
int err = -1;
err = inflateInit2(&strm, 31); // 初始化 31代表GZIP
if (err == Z_OK)
{
printf("inflateInit2 err=Z_OK\n");
err = inflate(&strm, Z_FINISH); // 解压gzip数据
if (err == Z_STREAM_END) // 解压成功
{
printf("inflate err=Z_OK\n");
*dstLen = strm.total_out;
}
else // 解压失败
{
printf("inflate err=!Z_OK\n");
}
inflateEnd(&strm);
}
else
{
printf("inflateInit2 err! err=%d\n", err);
}
return err;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
把这个函数复制粘贴到https_with_url()函数的前面,一会儿要在https_with_url()函数中调用它。 这个函数有4个参数,src是需要解压的gzip包,srcLen是需要解压的gzip包字节长度,dst是解压后的数据,dstLen是解压后的字节数。
这个解压函数中用到了zlib库中的一个结构体(z_stream)和三个函数(inflateInit2、inflate、inflateEnd)。 现在开始修改https_with_url()函数中的内容。
要调用gzipDecompress函数的话,需要准备好它的四个参数,其中src已经有了,就是local_response_buffer,刚才我们提到,local_response_buffer里面放的就是收到的gzip数据包。srcLen长度,刚才在终端也看到了,说明可以获取到,不过它是直接在ESP_LOGI中调用函数esp_http_client_get_content_length(client)输出的,并没有赋值给某个变量,我们要使用的话,需要定义一个变量获得这个数。再接下来的dst和dstLen,需要我们定义一下。
我们在https_with_url()函数的最开始,定义几个刚才提到的变量。
static void https_with_url(void)
{
char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};
int https_status = 0;
int64_t gzip_len = 0;
int dstBufLen = 1024;
char* dstBuf= (char*)malloc(1024);
memset(dstBuf, 0, 1024);
...
}
2
3
4
5
6
7
8
9
10
11
https_status变量用来记录esp_http_client_get_status_code(client)的状态值。
gzip_len定义为64位变量,因为接下来的esp_http_client_get_content_length(client)函数的返回值是一个64位数。
刚才在终端看到,收到的gzip包字节大小是322,解压缩后,字节数应该也不会超过1024,所以这里我们定义dst BufLen为1024,然后定义一个字符指针dstBuf,并且使用malloc函数给dstBuf分配1024字节大小的内存,再使用memset函数初始化这个内存空间为0。
在if (err == ESP_OK)里面,先使用gzip_len得到字节大小,然后在ESP_LOGI里面直接使用gzip_len代替获取字节大小的函数,最后如下所示:
...
if (err == ESP_OK) {
https_status = esp_http_client_get_status_code(client);
gzip_len = esp_http_client_get_content_length(client);
ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %"PRIu64, https_status, gzip_len);
} else {
ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
}
...
2
3
4
5
6
7
8
9
%d不能输出64位变量,需要使用PRIu64,如上代码所示。
接下来进行最后一步,在https_with_url()函数的最后,加入下面的代码:
if (https_status == 200)
{
int ret = gzipDecompress(local_response_buffer, gzip_len, dstBuf, &dstBufLen);
if (Z_STREAM_END == ret) { /* 解压成功 */
printf("decompress success\n");
printf("dstBufLen = %d\n", dstBufLen);
cJSON *root = cJSON_Parse(dstBuf);
cJSON *now = cJSON_GetObjectItem(root,"now");
char *temp = cJSON_GetObjectItem(now,"temp")->valuestring;
char *humidity = cJSON_GetObjectItem(now,"humidity")->valuestring;
ESP_LOGI(TAG, "地区:深圳市福田区");
ESP_LOGI(TAG, "温度:%s", temp);
ESP_LOGI(TAG, "湿度:%s", humidity);
cJSON_Delete(root);
free(dstBuf);
}
else {
printf("decompress failed:%d\n", ret);
free(dstBuf);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
先判断https_status,是否是200,如果是200,代表网页响应正常。
进入if后,使用gzipDecompress()函数解压gizp包,解压后的数据是CJSON格式,我们可以使用cjson库解析数据。需要在此c文件的前面包含cjson.h头文件。
#include "cjson.h"
现在我们把url网址输入到电脑浏览器打开,看一下格式。我们发现温度(temp)和湿度(humidity)数据在二级目录下,它对应的一级目录是名称是now。所以在程序中,解析完JSON数据后,先获得now,再从now中分别获得temp和humidity。我们再看网页中的结果,发现温度和湿度数据都是字符串形式,所以需要使用valuestring获得。别的平台返回的数据,可能是数字形式,那就使用valueint获得。
现在就可以编译下载看结果了:
I (6477) HTTP_CLIENT: Connected to AP, begin http example
I (6487) main_task: Returned from app_main()
I (6767) esp-x509-crt-bundle: Certificate validated
I (7257) HTTP_CLIENT: HTTPS Status = 200, content_length = 312
I (7257) HTTP_CLIENT: HTTP_EVENT_DISCONNECTED
inflateInit2 err=Z_OK
inflate err=Z_OK
decompress success
dstBufLen = 429
I (7267) HTTP_CLIENT: 地区:深圳市福田区
I (7267) HTTP_CLIENT: 温度:23
I (7277) HTTP_CLIENT: 湿度:78
I (7277) HTTP_CLIENT: Finish http example
2
3
4
5
6
7
8
9
10
11
12
13
需要注意的是,和风天气的免费订阅方式,每天最多获取1000次数据,每天不要超过此次数。