在 【nRF7002-DK Wi-Fi® 6开发套件评测】WiFi联网请求JSON数据控制LED 中,分享了在nRF7002-DK上通过WIFi联网来获取HTTP网址的数据,这一篇分享,则是请求高德天气的API接口,是HTTPS的。
一、前言
要在 nRF7002-DK上的Zephyr 中发起HTTPS请求,需要使用到 Mbed-TLS 。
在常见的电脑操作系统,如Linux、Windows、macOS中,对于SSL协议的支持,一般使用的是OpenSSL。
而在嵌入式设备上,通常使用的是Mbed-TLS,它是专门设计为设计为适合小型嵌入式设备的。
Zephyr提供了mbedtls模块,可以在程序中,很方便的加载Mbed-TLS,来实现基于SSL/TLS的安全数据通讯。
二、高德天气接口
在nRF7002-DK上测试过多中天气API接口,最终确定比较好用的,是高德天气接口。
要使用高德天气接口,可以访问: https://lbs.amap.com/api/webservice/guide/api/weatherinfo/ ,了解该接口的具体调用方式:
在该页面上,需要先注册登录,然后获取API Key:
获取到API Key以后,可以直接通过网址检查是否可以正确使用,具体的调用网址如下:
https://restapi.amap.com/v3/weather/weatherInfo?key=【你的API_KEY】&city=110000&extensions=base
在上述网址中,请使用你自己申请的API Key,并修改city为你想要查询的城市编码。
具体的城市编码,可以在 https://lbs.amap.com/api/webservice/download 了解详情。
上述网址请求的是北京的天气信息,具体结果如下:
三、Root证书获取
要在 nRF7002-DK 上的 Zephyr中发起HTTPS请求,还需要预先获取https网址所对应的Root证书,否则是无法正确发起请求的。
要获取Root证书,按照如下的步骤进行:
1. 访问实际要访问的API的HTTPS网址,并在浏览器中,查看证书:
通过上面的步骤,导出了 GlobalSignRootCA.cer 证书。注意导出的时候,证书文件名不要有空格。
然后,使用下面的命令,转换证书:
openssl x509 -in GlobalSignRootCA.cer -outform PEM -out GlobalSignRootCA.pem
openssl x509 -outform der -in GlobalSignRootCA.pem -out GlobalSignRootCA.der
最终得到实际需要的证书文件:GlobalSignRootCA.der
这个证书的获取,是非常关键的,否则在后续发起HTTPS请求的时候,必然会出错。
另外,有一些HTTPS网址的Root证书,即使获取了,也不一定能够使用,这一点务必注意。高德天气的接口,我反复测试过,确保正确。
四、在nRF7002-DK上的Zephyr 中发起HTTPS请求
这次的演示程序,在 【nRF7002-DK Wi-Fi® 6开发套件评测】WiFi联网请求JSON数据控制LED 的基础上,参考官方演示,添加了HTTPS的部分。
具体要处理的步骤如下:
1. 修改prj.conf,添加MBEDTLS相关的配置:
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=40000
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=7168
CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y
CONFIG_MBEDTLS_DEBUG_LEVEL=4
# mbed TLS and security
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=16384
CONFIG_MBEDTLS_HEAP_SIZE=128000
CONFIG_MBEDTLS_TLS_LIBRARY=y
CONFIG_MBEDTLS_X509_LIBRARY=y
CONFIG_MBEDTLS_PKCS1_V15=y
CONFIG_NRF_SECURITY_ADVANCED=y
CONFIG_NORDIC_SECURITY_BACKEND=y
CONFIG_PSA_WANT_ALG_SHA_1=y
CONFIG_PSA_WANT_ALG_RSA_PKCS1V15_CRYPT=y
CONFIG_PSA_WANT_ALG_RSA_PKCS1V15_SIGN=y
CONFIG_MBEDTLS_SSL_DEBUG_ALL=y
CONFIG_MBEDTLS_LOG_LEVEL_DBG=y
CONFIG_MBEDTLS_SSL_RENEGOTIATION=y
CONFIG_MBEDTLS_SSL_SERVER_NAME_INDICATION=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ALL_ENABLED=y
CONFIG_MBEDTLS_ECP_ALL_ENABLED=y
CONFIG_MBEDTLS_CIPHER_ALL_ENABLED=y
CONFIG_MBEDTLS_SSL_SESSION_TICKETS=y
CONFIG_MBEDTLS_SSL_CACHE_C=y
CONFIG_MBEDTLS_SSL_TICKET_C=y
# CONFIG_PSA_WANT_ECC_TWISTED_EDWARDS_255=y
# CONFIG_PSA_CRYPTO_DRIVER_ECC_TWISTED_EDWARDS_255_OBERON=y
CONFIG_MBEDTLS_RSA_C=y
CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
上述配置,应该是可以优化的,感兴趣的话,可以尝试去掉多余的配置进行编译测试,例如:
# TLS configuration
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=40000
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=7168
CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
2. 修改CMakeLists.txt,添加证书处理:
include(${ZEPHYR_BASE}/samples/net/common/common.cmake)
set(gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/)
generate_inc_file_for_target(
app
src/GlobalSignRootCA.der
${gen_dir}/GlobalSignRootCA.der.inc
)
注意上述配置中,在Root证书获取部分获取的证书,保存到了 src/GlobalSignRootCA.der ,以便编译过程中进行预处理。
3. 证书加载头文件:
要把CMake预处理的Root证书加载到代码中来,需要添加 src/ca_certificate.h 文件,内容如下:
#ifndef __CA_CERTIFICATE_H__
#define __CA_CERTIFICATE_H__
#define CA_CERTIFICATE_TAG 1
static const unsigned char ca_certificate[] = {
#include "GlobalSignRootCA.der.inc"
};
#endif /* __CA_CERTIFICATE_H__ */
上述代码中的 GlobalSignRootCA.der.inc ,就是在 CMakeLists.txt 预处理的结果。
4. main.c中添加对应的调用
完整的main.c代码如下:
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(wifi_weather, CONFIG_LOG_DEFAULT_LEVEL);
#include <nrfx_clock.h>
#include <zephyr/kernel.h>
#include <stdio.h>
#include <stdlib.h>
#include <zephyr/shell/shell.h>
#include <zephyr/sys/printk.h>
#include <zephyr/init.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/net_event.h>
#include <zephyr/drivers/gpio.h>
#include <qspi_if.h>
#include "net_private.h"
// 网络连接
#include <zephyr/net/socket.h>
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
#include <zephyr/net/tls_credentials.h>
#include "ca_certificate.h"
#endif
// keyname=GlobalSignRootCA
// openssl x509 -in $keyname.cer -outform PEM -out $keyname.pem
// openssl x509 -outform der -in $keyname.pem -out $keyname.der
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
#define HTTP_HOST "restapi.amap.com"
#define HTTP_PORT "443"
#endif
#define HTTP_PATH "/v3/weather/weatherInfo?key=【你的API_KEY】&city=110000&extensions=base"
#define SSTRLEN(s) (sizeof(s) - 1)
#define CHECK(r) { if (r == -1) { printf("Error: " #r "\n"); exit(1); } }
#define REQUEST "GET " HTTP_PATH " HTTP/1.0\r\nHost: " HTTP_HOST "\r\n\r\n"
static char response[1024];
// 网络连接
// JSON
#include <zephyr/data/json.h>
/*
{
"status": "1",
"count": "1",
"info": "OK",
"infocode": "10000",
"lives": [
{
"province": "北京",
"city": "北\ufffd\ufffd\ufffd市",
"adcode": "110000",
"weather": "晴",
"temperature": "-1",
"winddirection": "东",
"windpower": "≤3",
"humidity": "57",
"reporttime": "2023-12-03 22:10:22",
"temperature_float": "-1.0",
"humidity_float": "57.0"
}
]
}
*/
struct weather_live {
const char *province;
const char *city;
const char *adcode;
const char *weather;
const char *temperature;
const char *winddirection;
const char *windpower;
const char *humidity;
const char *reporttime;
const char *temperature_float;
const char *humidity_float;
};
struct weather {
const char *status;
const char *count;
const char *info;
const char *infocode;
struct weather_live lives[1];
size_t lives_len;
};
static const struct json_obj_descr weather_live_descr[] = {
JSON_OBJ_DESCR_PRIM(struct weather_live, province, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, city, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, adcode, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, weather, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, temperature, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, winddirection, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, windpower, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, humidity, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, reporttime, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, temperature_float, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, humidity_float, JSON_TOK_STRING)
};
static const struct json_obj_descr weather_descr[] = {
JSON_OBJ_DESCR_PRIM(struct weather, status, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather, count, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather, info, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather, infocode, JSON_TOK_STRING),
JSON_OBJ_DESCR_OBJ_ARRAY(struct weather, lives, 1, lives_len, weather_live_descr, ARRAY_SIZE(weather_live_descr)),
};
// JSON
#define WIFI_SHELL_MODULE "wifi"
#define WIFI_SHELL_MGMT_EVENTS (NET_EVENT_WIFI_CONNECT_RESULT | \
NET_EVENT_WIFI_DISCONNECT_RESULT)
#define MAX_SSID_LEN 32
#define STATUS_POLLING_MS 300
/* 1000 msec = 1 sec */
#define LED_SLEEP_TIME_MS 500
/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)
/*
* A build error on this line means your board is unsupported.
* See the sample documentation for information on how to fix this.
*/
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
static struct net_mgmt_event_callback wifi_shell_mgmt_cb;
static struct net_mgmt_event_callback net_shell_mgmt_cb;
static struct {
const struct shell *sh;
union {
struct {
uint8_t connected : 1;
uint8_t connect_result : 1;
uint8_t disconnect_requested : 1;
uint8_t _unused : 5;
};
uint8_t all;
};
} context;
void toggle_led(void)
{
int ret;
if (!device_is_ready(led.port)) {
LOG_ERR("LED device is not ready");
return;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
LOG_ERR("Error %d: failed to configure LED pin", ret);
return;
}
while (1) {
if (context.connected) {
gpio_pin_toggle_dt(&led);
k_msleep(LED_SLEEP_TIME_MS);
} else {
gpio_pin_set_dt(&led, 0);
k_msleep(LED_SLEEP_TIME_MS);
}
}
}
K_THREAD_DEFINE(led_thread_id, 1024, toggle_led, NULL, NULL, NULL,
7, 0, 0);
static int cmd_wifi_status(void)
{
struct net_if *iface = net_if_get_default();
struct wifi_iface_status status = { 0 };
if (net_mgmt(NET_REQUEST_WIFI_IFACE_STATUS, iface, &status,
sizeof(struct wifi_iface_status))) {
LOG_INF("Status request failed");
return -ENOEXEC;
}
LOG_INF("==================");
LOG_INF("State: %s", wifi_state_txt(status.state));
if (status.state >= WIFI_STATE_ASSOCIATED) {
uint8_t mac_string_buf[sizeof("xx:xx:xx:xx:xx:xx")];
LOG_INF("Interface Mode: %s",
wifi_mode_txt(status.iface_mode));
LOG_INF("Link Mode: %s",
wifi_link_mode_txt(status.link_mode));
LOG_INF("SSID: %-32s", status.ssid);
LOG_INF("BSSID: %s",
net_sprint_ll_addr_buf(
status.bssid, WIFI_MAC_ADDR_LEN,
mac_string_buf, sizeof(mac_string_buf)));
LOG_INF("Band: %s", wifi_band_txt(status.band));
LOG_INF("Channel: %d", status.channel);
LOG_INF("Security: %s", wifi_security_txt(status.security));
LOG_INF("MFP: %s", wifi_mfp_txt(status.mfp));
LOG_INF("RSSI: %d", status.rssi);
}
return 0;
}
static void handle_wifi_connect_result(struct net_mgmt_event_callback *cb)
{
const struct wifi_status *status =
(const struct wifi_status *) cb->info;
if (context.connected) {
return;
}
if (status->status) {
LOG_ERR("Connection failed (%d)", status->status);
} else {
LOG_INF("Connected");
context.connected = true;
}
context.connect_result = true;
}
static void handle_wifi_disconnect_result(struct net_mgmt_event_callback *cb)
{
const struct wifi_status *status =
(const struct wifi_status *) cb->info;
if (!context.connected) {
return;
}
if (context.disconnect_requested) {
LOG_INF("Disconnection request %s (%d)",
status->status ? "failed" : "done",
status->status);
context.disconnect_requested = false;
} else {
LOG_INF("Received Disconnected");
context.connected = false;
}
cmd_wifi_status();
}
static void wifi_mgmt_event_handler(struct net_mgmt_event_callback *cb,
uint32_t mgmt_event, struct net_if *iface)
{
switch (mgmt_event) {
case NET_EVENT_WIFI_CONNECT_RESULT:
handle_wifi_connect_result(cb);
break;
case NET_EVENT_WIFI_DISCONNECT_RESULT:
handle_wifi_disconnect_result(cb);
break;
default:
break;
}
}
static void print_dhcp_ip(struct net_mgmt_event_callback *cb)
{
/* Get DHCP info from struct net_if_dhcpv4 and print */
const struct net_if_dhcpv4 *dhcpv4 = cb->info;
const struct in_addr *addr = &dhcpv4->requested_ip;
char dhcp_info[128];
net_addr_ntop(AF_INET, addr, dhcp_info, sizeof(dhcp_info));
LOG_INF("DHCP IP address: %s", dhcp_info);
}
static void net_mgmt_event_handler(struct net_mgmt_event_callback *cb,
uint32_t mgmt_event, struct net_if *iface)
{
switch (mgmt_event) {
case NET_EVENT_IPV4_DHCP_BOUND:
print_dhcp_ip(cb);
break;
default:
break;
}
}
static int __wifi_args_to_params(struct wifi_connect_req_params *params)
{
params->timeout = CONFIG_STA_CONN_TIMEOUT_SEC * MSEC_PER_SEC;
if (params->timeout == 0) {
params->timeout = SYS_FOREVER_MS;
}
/* SSID */
params->ssid = CONFIG_STA_SAMPLE_SSID;
params->ssid_length = strlen(params->ssid);
#if defined(CONFIG_STA_KEY_MGMT_WPA2)
params->security = 1;
#elif defined(CONFIG_STA_KEY_MGMT_WPA2_256)
params->security = 2;
#elif defined(CONFIG_STA_KEY_MGMT_WPA3)
params->security = 3;
#else
params->security = 0;
#endif
#if !defined(CONFIG_STA_KEY_MGMT_NONE)
params->psk = CONFIG_STA_SAMPLE_PASSWORD;
params->psk_length = strlen(params->psk);
#endif
params->channel = WIFI_CHANNEL_ANY;
/* MFP (optional) */
params->mfp = WIFI_MFP_OPTIONAL;
return 0;
}
static int wifi_connect(void)
{
struct net_if *iface = net_if_get_default();
static struct wifi_connect_req_params cnx_params;
context.connected = false;
context.connect_result = false;
__wifi_args_to_params(&cnx_params);
if (net_mgmt(NET_REQUEST_WIFI_CONNECT, iface,
&cnx_params, sizeof(struct wifi_connect_req_params))) {
LOG_ERR("Connection request failed");
return -ENOEXEC;
}
LOG_INF("Connection requested");
return 0;
}
int bytes_from_str(const char *str, uint8_t *bytes, size_t bytes_len)
{
size_t i;
char byte_str[3];
if (strlen(str) != bytes_len * 2) {
LOG_ERR("Invalid string length: %zu (expected: %d)\n",
strlen(str), bytes_len * 2);
return -EINVAL;
}
for (i = 0; i < bytes_len; i++) {
memcpy(byte_str, str + i * 2, 2);
byte_str[2] = '\0';
bytes[i] = strtol(byte_str, NULL, 16);
}
return 0;
}
int main(void)
{
memset(&context, 0, sizeof(context));
net_mgmt_init_event_callback(&wifi_shell_mgmt_cb,
wifi_mgmt_event_handler,
WIFI_SHELL_MGMT_EVENTS);
net_mgmt_add_event_callback(&wifi_shell_mgmt_cb);
net_mgmt_init_event_callback(&net_shell_mgmt_cb,
net_mgmt_event_handler,
NET_EVENT_IPV4_DHCP_BOUND);
net_mgmt_add_event_callback(&net_shell_mgmt_cb);
LOG_INF("Starting %s with CPU frequency: %d MHz", CONFIG_BOARD, SystemCoreClock/MHZ(1));
k_sleep(K_SECONDS(1));
#if defined(CONFIG_BOARD_NRF7002DK_NRF7001_NRF5340_CPUAPP) || \
defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP)
if (strlen(CONFIG_NRF700X_QSPI_ENCRYPTION_KEY)) {
char key[QSPI_KEY_LEN_BYTES];
int ret;
ret = bytes_from_str(CONFIG_NRF700X_QSPI_ENCRYPTION_KEY, key, sizeof(key));
if (ret) {
LOG_ERR("Failed to parse encryption key: %d\n", ret);
return 0;
}
LOG_DBG("QSPI Encryption key: ");
for (int i = 0; i < QSPI_KEY_LEN_BYTES; i++) {
LOG_DBG("%02x", key[i]);
}
LOG_DBG("\n");
ret = qspi_enable_encryption(key);
if (ret) {
LOG_ERR("Failed to enable encryption: %d\n", ret);
return 0;
}
LOG_INF("QSPI Encryption enabled");
} else {
LOG_INF("QSPI Encryption disabled");
}
#endif /* CONFIG_BOARD_NRF700XDK_NRF5340 */
LOG_INF("Static IP address (overridable): %s/%s -> %s",
CONFIG_NET_CONFIG_MY_IPV4_ADDR,
CONFIG_NET_CONFIG_MY_IPV4_NETMASK,
CONFIG_NET_CONFIG_MY_IPV4_GW);
while (1) {
wifi_connect();
while (!context.connect_result) {
cmd_wifi_status();
k_sleep(K_MSEC(STATUS_POLLING_MS));
}
if (context.connected) {
// k_sleep(K_FOREVER);
LOG_INF("WiFi Connect OK!");
break;
}
}
static struct addrinfo hints;
struct addrinfo *res;
int st, sock;
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
tls_credential_add(CA_CERTIFICATE_TAG, TLS_CREDENTIAL_CA_CERTIFICATE,
ca_certificate, sizeof(ca_certificate));
#endif
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
printf("Preparing HTTP GET request for https://" HTTP_HOST
":" HTTP_PORT HTTP_PATH "\n");
#else
printf("Preparing HTTP GET request for http://" HTTP_HOST
":" HTTP_PORT HTTP_PATH "\n");
#endif
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
st = getaddrinfo(HTTP_HOST, HTTP_PORT, &hints, &res);
printf("getaddrinfo status: %d\n", st);
if (st != 0) {
printf("Unable to resolve address, quitting\n");
return 0;
}
while (1) {
LOG_INF("Main Loop.");
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
sock = socket(res->ai_family, res->ai_socktype, IPPROTO_TLS_1_2);
#else
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
#endif
CHECK(sock);
printf("sock = %d\n", sock);
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
sec_tag_t sec_tag_opt[] = {
CA_CERTIFICATE_TAG,
};
CHECK(setsockopt(sock, SOL_TLS, TLS_SEC_TAG_LIST,
sec_tag_opt, sizeof(sec_tag_opt)));
CHECK(setsockopt(sock, SOL_TLS, TLS_HOSTNAME,
HTTP_HOST, sizeof(HTTP_HOST)))
#endif
int ret = connect(sock, res->ai_addr, res->ai_addrlen);
printf("errno = %d, ret=%d\n", errno, ret);
if(ret==-1) {
printf("exit");
exit(1);
}
// CHECK(connect(sock, res->ai_addr, res->ai_addrlen));
CHECK(send(sock, REQUEST, SSTRLEN(REQUEST), 0));
// printf("Response:\n\n");
while (1) {
int len = recv(sock, response, sizeof(response) - 1, 0);
if (len < 0) {
printf("Error reading response\n");
return 0;
}
if (len == 0) {
break;
}
response[len] = 0;
char* newline = strstr(response, "\r\n\r\n");
if (newline != NULL) {
newline += strlen("\r\n\r\n");
printf("response:substring[%d]:%s", strlen(newline), newline);
} else {
printf("response[%d]:%s", len, response);
continue;
}
len = strlen(newline);
printf("Response[%d]: %s\n", len, newline);
struct weather temp_results;
ret = json_obj_parse(newline, len,
weather_descr,
ARRAY_SIZE(weather_descr),
&temp_results);
if (ret < 0)
{
LOG_ERR("JSON Parse Error: %d", ret);
}
else
{
LOG_INF("json_obj_parse return code: %d", ret);
LOG_INF("Status: %s", temp_results.status);
if(temp_results.lives_len>0) {
LOG_INF("province: %s", temp_results.lives[0].province);
LOG_INF("city: %s", temp_results.lives[0].city);
LOG_INF("adcode: %s", temp_results.lives[0].adcode);
LOG_INF("weather: %s", temp_results.lives[0].weather);
LOG_INF("temperature: %s", temp_results.lives[0].temperature);
LOG_INF("winddirection: %s", temp_results.lives[0].winddirection);
LOG_INF("windpower: %s", temp_results.lives[0].windpower);
LOG_INF("humidity: %s", temp_results.lives[0].humidity);
LOG_INF("reporttime: %s", temp_results.lives[0].reporttime);
LOG_INF("temperature_float: %s", temp_results.lives[0].temperature_float);
LOG_INF("humidity_float: %s", temp_results.lives[0].humidity_float);
}
}
}
printf("\n");
(void)close(sock);
k_sleep(K_MSEC(10000));
}
return 0;
}
在上述代码中,CONFIG_NET_SOCKETS_SOCKOPT_TLS宏定义用于检查是否要发起HTTPS请求,这个在prj.conf中已经有定义。
具体各部分说明如下:
1) 头文件调用:
// 网络连接
#include <zephyr/net/socket.h>
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
#include <zephyr/net/tls_credentials.h>
#include "ca_certificate.h"
#endif
需要添加tls以及证书相关的头文件。
2) HTTPS网址定义:
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
#define HTTP_HOST "restapi.amap.com"
#define HTTP_PORT "443"
#endif
#define HTTP_PATH "/v3/weather/weatherInfo?key=【你的API_KEY】&city=110000&extensions=base"
上述地址对应高德天气API接口:
https://restapi.amap.com/v3/weather/weatherInfo?key=【你的API_KEY】&city=110000&extensions=base
3) JSON数据结构定义:
// JSON
#include <zephyr/data/json.h>
/*
{
"status": "1",
"count": "1",
"info": "OK",
"infocode": "10000",
"lives": [
{
"province": "北京",
"city": "北\ufffd\ufffd\ufffd市",
"adcode": "110000",
"weather": "晴",
"temperature": "-1",
"winddirection": "东",
"windpower": "≤3",
"humidity": "57",
"reporttime": "2023-12-03 22:10:22",
"temperature_float": "-1.0",
"humidity_float": "57.0"
}
]
}
*/
struct weather_live {
const char *province;
const char *city;
const char *adcode;
const char *weather;
const char *temperature;
const char *winddirection;
const char *windpower;
const char *humidity;
const char *reporttime;
const char *temperature_float;
const char *humidity_float;
};
struct weather {
const char *status;
const char *count;
const char *info;
const char *infocode;
struct weather_live lives[1];
size_t lives_len;
};
static const struct json_obj_descr weather_live_descr[] = {
JSON_OBJ_DESCR_PRIM(struct weather_live, province, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, city, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, adcode, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, weather, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, temperature, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, winddirection, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, windpower, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, humidity, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, reporttime, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, temperature_float, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather_live, humidity_float, JSON_TOK_STRING)
};
static const struct json_obj_descr weather_descr[] = {
JSON_OBJ_DESCR_PRIM(struct weather, status, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather, count, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather, info, JSON_TOK_STRING),
JSON_OBJ_DESCR_PRIM(struct weather, infocode, JSON_TOK_STRING),
JSON_OBJ_DESCR_OBJ_ARRAY(struct weather, lives, 1, lives_len, weather_live_descr, ARRAY_SIZE(weather_live_descr)),
};
// JSON
通过高德天气APIJ接口返回的数据为JSON格式:
{
"status": "1",
"count": "1",
"info": "OK",
"infocode": "10000",
"lives": [
{
"province": "北京",
"city": "北\ufffd\ufffd\ufffd市",
"adcode": "110000",
"weather": "晴",
"temperature": "-1",
"winddirection": "东",
"windpower": "≤3",
"humidity": "57",
"reporttime": "2023-12-03 22:10:22",
"temperature_float": "-1.0",
"humidity_float": "57.0"
}
]
}
需要参考这个格式,使用JSON库的定义方式,逐级定义weather_live、weather。
4) 加载证书:
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
tls_credential_add(CA_CERTIFICATE_TAG, TLS_CREDENTIAL_CA_CERTIFICATE,
ca_certificate, sizeof(ca_certificate));
#endif
5) 发起TLS请求:
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
sock = socket(res->ai_family, res->ai_socktype, IPPROTO_TLS_1_2);
#else
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
#endif
从上述代码中可以看到,TLS请求与普通HTTP时发起的请求类似,不过协议发生了变化。
6) 设置socket发送TLS请求需要的信息:
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)
sec_tag_t sec_tag_opt[] = {
CA_CERTIFICATE_TAG,
};
CHECK(setsockopt(sock, SOL_TLS, TLS_SEC_TAG_LIST,
sec_tag_opt, sizeof(sec_tag_opt)));
CHECK(setsockopt(sock, SOL_TLS, TLS_HOSTNAME,
HTTP_HOST, sizeof(HTTP_HOST)))
#endif
7) 解析返回的JSON数据
成功发起请求获得数据后,后续的处理,就和之前HTTP请求类似了,最终输出代码如下:
if(temp_results.lives_len>0) {
LOG_INF("province: %s", temp_results.lives[0].province);
LOG_INF("city: %s", temp_results.lives[0].city);
LOG_INF("adcode: %s", temp_results.lives[0].adcode);
LOG_INF("weather: %s", temp_results.lives[0].weather);
LOG_INF("temperature: %s", temp_results.lives[0].temperature);
LOG_INF("winddirection: %s", temp_results.lives[0].winddirection);
LOG_INF("windpower: %s", temp_results.lives[0].windpower);
LOG_INF("humidity: %s", temp_results.lives[0].humidity);
LOG_INF("reporttime: %s", temp_results.lives[0].reporttime);
LOG_INF("temperature_float: %s", temp_results.lives[0].temperature_float);
LOG_INF("humidity_float: %s", temp_results.lives[0].humidity_float);
}
五、运行测试
代码处理后之后,编译并烧录到nRF7002-DK开发板,最终运行输出的结果如下:
从上面的输出可以看到,成功发起了HTTPS请求,从高德天气API接口获取到了数据,并进行解析。
如果你还给板子接了OLED之类的显示屏,就可以显示到显示屏上输出,做为一个小小的天气牌了。
如果在运行的时候,输出如下信息:
恭喜你,这个时候,通常就是证书不对,或者不符合Mbed-TLS的要求了。我反复遇到过。
六、总结
在嵌入式设备上发起SSL/TLS请求,确实要比通用Linux、Windows、macOS上面麻烦不少。
不过,掌握了正确的用法,也会相当的方便。
nRF7002-DK上的Zephyr,提供了Mbed-TLS,使得相关的操作处理非常简单了。
另外,在不同的平台上,使用 Mbed-TLS 的方式,其实大同小异,掌握了一个平台的,其他平台上可以很快的就实施上。