Wi-Fi 配网
====================
:link_to_translation:`en:[English]`
概述
----------
该组件提供控制 Wi-Fi 配网服务的 API,可以通过 SoftAP 或低功耗蓝牙建立 :doc:`protocomm` 安全会话,接收和配置 Wi-Fi 凭证。通过一组 ``wifi_prov_mgr_`` API,可以快速实现配网服务,该服务具备必要功能、代码量少且足够灵活。
.. _wifi-prov-mgr-init:
初始化
^^^^^^^^^^^^^^
调用 :cpp:func:`wifi_prov_mgr_init()` 可以配置和初始化配网管理器,因此在调用任何其他 ``wifi_prov_mgr_`` API 之前必须先调用此函数。请注意,该管理器依赖于 ESP-IDF 的其他组件,包括 NVS、TCP/IP、Event Loop 和 Wi-Fi,以及可选的 mDNS,因此在调用之前必须先初始化这些组件。调用 :cpp:func:`wifi_prov_mgr_deinit()` 可以随时反初始化管理器。
.. code-block:: c
wifi_prov_mgr_config_t config = {
.scheme = wifi_prov_scheme_ble,
.scheme_event_handler = WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM
};
ESP_ERROR_CHECK( wifi_prov_mgr_init(config) );
以下配置结构体 :cpp:type:`wifi_prov_mgr_config_t` 里包含的部分字段可用于指定特定管理器行为:
* :cpp:member:`wifi_prov_mgr_config_t::scheme` - 用于指定配网方案。每个方案对应一种 protocomm 支持的传输模式,因此支持三个选项:
* ``wifi_prov_scheme_ble`` - 使用低功耗蓝牙传输和 GATT 服务器来处理配网命令。
* ``wifi_prov_scheme_softap`` - 使用 Wi-Fi SoftAP 传输和 HTTP 服务器来处理配网命令。
* ``wifi_prov_scheme_console`` - 使用串口传输和控制台来处理配网命令。
* :cpp:member:`wifi_prov_mgr_config_t::scheme_event_handler` - 为方案定义的专属事件处理程序。选择适当方案后,其专属事件处理程序支持管理器自动处理特定事项。目前,该选项不适用于 SoftAP 或基于控制台的配网方案,但对于低功耗蓝牙配网方案来说非常方便。因为蓝牙需要相当多内存才能正常工作,所以配网完成后,主应用程序需要使用低功耗蓝牙或经典蓝牙时,可能需要回收配网所占的全部或部分内存。此外,未来每当配网设备重启时,都需要再次回收内存。为了便于使用 ``wifi_prov_scheme_ble`` 选项,各方案定义了专属处理程序。设备会根据所选处理程序,在反初始化配网管理器时自动释放低功耗蓝牙、经典蓝牙或蓝牙双模的内存。可用选项包括:
* ``WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM`` - 同时释放经典蓝牙和低功耗蓝牙或蓝牙双模的内存,可以在主应用程序不需要蓝牙时使用该选项。
* ``WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BLE`` - 只释放低功耗蓝牙的内存,可以在主应用程序需要经典蓝牙时使用该选项。
* ``WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BT`` - 仅释放经典蓝牙的内存,可以在主应用程序需要低功耗蓝牙时使用该选项,内存会在初始化管理器时立即释放。
* ``WIFI_PROV_EVENT_HANDLER_NONE`` - 不使用任何特定方案的专属处理程序。以下情况可使用该选项:不使用低功耗蓝牙配网方案,即使用 SoftAP 或控制台方案;主应用程序需要自行回收内存;主应用程序需要同时使用低功耗蓝牙和经典蓝牙。
* :cpp:member:`wifi_prov_mgr_config_t::app_event_handler` (不推荐)- 目前建议使用默认的事件循环处理程序捕获生成的 ``WIFI_PROV_EVENT``。关于配网服务生成事件的列表,请参阅 ``wifi_prov_cb_event_t`` 的定义。以下是配网事件示例摘录:
.. code-block:: c
static void event_handler(void* arg, esp_event_base_t event_base,
int event_id, void* event_data)
{
if (event_base == WIFI_PROV_EVENT) {
switch (event_id) {
case WIFI_PROV_START:
ESP_LOGI(TAG, "Provisioning started");
break;
case WIFI_PROV_CRED_RECV: {
wifi_sta_config_t *wifi_sta_cfg = (wifi_sta_config_t *)event_data;
ESP_LOGI(TAG, "Received Wi-Fi credentials"
"\n\tSSID : %s\n\tPassword : %s",
(const char *) wifi_sta_cfg->ssid,
(const char *) wifi_sta_cfg->password);
break;
}
case WIFI_PROV_CRED_FAIL: {
wifi_prov_sta_fail_reason_t *reason = (wifi_prov_sta_fail_reason_t *)event_data;
ESP_LOGE(TAG, "Provisioning failed!\n\tReason : %s"
"\n\tPlease reset to factory and retry provisioning",
(*reason == WIFI_PROV_STA_AUTH_ERROR) ?
"Wi-Fi station authentication failed" : "Wi-Fi access-point not found");
break;
}
case WIFI_PROV_CRED_SUCCESS:
ESP_LOGI(TAG, "Provisioning successful");
break;
case WIFI_PROV_END:
/*配网完成后,反初始化管理器。*/
wifi_prov_mgr_deinit();
break;
default:
break;
}
}
}
调用 :cpp:func:`wifi_prov_mgr_deinit()` 可以随时反初始化管理器。
.. _wifi-prov-check-state:
检查配网状态
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
在运行时,可以调用 :cpp:func:`wifi_prov_mgr_is_provisioned()` 检查设备是否配网完成,该函数会在内部检查 Wi-Fi 凭据是否存储在 NVS 中。
请注意,目前管理器并没有自己的 NVS 命名空间来存储 Wi-Fi 凭据,而是依赖 ``esp_wifi_`` API 来设置和获取存储在默认位置的 NVS 中的凭据。
可以采用以下任一方法重置配网状态:
* 手动擦除 NVS 分区的配网相关部分。
* 主应用程序必须实现某种逻辑,以在运行时调用 ``esp_wifi_`` API 来擦除凭据。
* 主应用程序必须实现某种逻辑,以在不考虑配网状态的情况下,强制启动配网。
.. code-block:: c
bool provisioned = false;
ESP_ERROR_CHECK( wifi_prov_mgr_is_provisioned(&provisioned) );
启动配网服务
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
在启动配网服务时,需要指定服务名称和相应密钥,即:
* 使用 ``wifi_prov_scheme_softap`` 方案时,服务名称对应 Wi-Fi SoftAP 的 SSID,密钥对应密码。
* 使用 ``wifi_prov_scheme_ble`` 方案时,服务名称对应低功耗蓝牙设备名称,无需指定密钥。
此外,由于管理器内部使用了 ``protocomm``,可以选择其提供的任一安全功能:
* Security 1 是安全通信,该安全通信需要先握手,其中涉及 X25519 密钥交换和使用所有权证明 ``pop`` 完成身份验证,随后使用 AES-CTR 加密或解密后续消息。
* Security 0 是纯文本通信,会直接忽略 ``pop``。
关于安全功能的更多详情,请参阅 :doc:`provisioning`。
.. highlight:: c
::
const char *service_name = "my_device";
const char *service_key = "password";
wifi_prov_security_t security = WIFI_PROV_SECURITY_1;
const char *pop = "abcd1234";
ESP_ERROR_CHECK( wifi_prov_mgr_start_provisioning(security, pop, service_name, service_key) );
如果收到有效的 Wi-Fi AP 凭据,且设备成功连接到该 AP 并获取了 IP,配网服务会自动结束。此外,调用 :cpp:func:`wifi_prov_mgr_stop_provisioning()` 可以随时停止配网服务。
.. note::
如果设备使用提供的凭据无法连接,则它不再接受新的凭据,但在设备重新启动前,配网服务仍然会继续运行,并向客户端传递连接失败的信息。设备重新启动后配网状态将变为已配网,因为在 NVS 中找到了凭据,但除非出现与凭据匹配的可用 AP,否则设备仍然无法使用原凭据进行连接。可以通过重置 NVS 中的凭据或强制启动配网服务来解决这个问题,详情请参阅上文 :ref:`wifi-prov-check-state`。
等待配网完成
^^^^^^^^^^^^^^^^^^^^^^
主应用程序通常会等待配网服务完成,然后反初始化管理器以释放资源,最后开始执行自己的逻辑。
有两种方法可以实现这一点,其中调用阻塞 :cpp:func:`wifi_prov_mgr_wait()` 更为简单。
.. code-block:: c
// 启动配网服务
ESP_ERROR_CHECK( wifi_prov_mgr_start_provisioning(security, pop, service_name, service_key) );
// 等待服务完成
wifi_prov_mgr_wait();
// 最后反初始化管理器
wifi_prov_mgr_deinit();
另一种方法是使用默认的事件循环处理程序捕获 ``WIFI_PROV_EVENT`` 并在事件 ID 为 ``WIFI_PROV_END`` 时调用 :cpp:func:`wifi_prov_mgr_deinit()`:
.. code-block:: c
static void event_handler(void* arg, esp_event_base_t event_base,
int event_id, void* event_data)
{
if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_END) {
/* 配网完成后反初始化管理器 */
wifi_prov_mgr_deinit();
}
}
用户端实现
^^^^^^^^^^^^^^^^^^^^^^^^
启动服务时,通过广播服务名称识别即将配网的设备。根据选择的传输方式,该服务名称为低功耗蓝牙设备的名称或 SoftAP SSID。
使用 SoftAP 传输方式时,为便于服务发现,必须在启动配网之前初始化 mDNS。在这种情况下,应使用主应用程序设置的主机名,并且在内部将服务类型设置为 ``_esp_wifi_prov``。
使用低功耗蓝牙传输方式时,应使用 :cpp:func:`wifi_prov_scheme_ble_set_service_uuid()` 设置一个自定义的 128 位 UUID。该 UUID 将包含在低功耗蓝牙广播中,并对应于提供配网端点作为 GATT 特征的主要服务。每个 GATT 特征都基于主要服务 UUID 形成,其中从第 0 个字节开始计数,第 12 和第 13 个字节为自动分配的不同字节。由于端点特征 UUID 自动分配,因此不应将其用于识别端点。客户端应用程序应通过读取每个特征的用户特征描述符 (``0x2901``) 来识别端点,该描述符包含特征的端点名称。例如,如果将服务 UUID 设置为 ``55cc035e-fb27-4f80-be02-3c60828b7451``,每个端点特征将分配到一个类似于 ``55cc____-fb27-4f80-be02-3c60828b7451`` 的 UUID,其中第 12 和第 13 个字节具有唯一值。
连接设备后,可以通过以下方式识别与配网相关的 protocomm 端点:
.. |br| raw:: html
.. list-table:: 配网服务提供的端点
:widths: 35 35 30
:header-rows: 1
* - 端点名称 |br| 即低功耗蓝牙 + GATT 服务器
- URI 即 SoftAP |br| + HTTP 服务器 + mDNS
- 描述
* - prov-session
- http://.local/prov-session
- 用于建立会话的安全端点
* - prov-scan
- http://wifi-prov.local/prov-scan
- 用于启动 Wi-Fi 扫描和接收扫描结果的端点
* - prov-ctrl
- http://wifi-prov.local/prov-ctrl
- 用于控制 Wi-Fi 配网状态的端点
* - prov-config
- http://.local/prov-config
- 用于在设备上配置 Wi-Fi 凭据的端点
* - proto-ver
- http://.local/proto-ver
- 用于获取版本信息的端点
连接后,客户端应用程序可以立即从 ``proto-ver`` 端点获取版本或属性信息。所有与此端点的通信均未加密,因此在建立安全会话前,可以检索相关必要信息,确保会话兼容。响应结果以 JSON 格式返回,格式类似于 ``prov: { ver: v1.1, cap: [no_pop] }, my_app: { ver: 1.345, cap: [cloud, local_ctrl] },....``。其中 ``prov`` 标签提供了配网服务的版本 ``ver`` 和属性 ``cap``。目前仅支持 ``no_pop`` 属性,表示该服务不需要验证所有权证明。任何与应用程序相关的版本或属性将由其他标签给出,如本示例中的 ``my_app``。使用 :cpp:func:`wifi_prov_mgr_set_app_info()` 可以设置这些附加字段。
用户端应用程序需要根据所配置的安全方案实现签名握手,以建立和认证 protocomm 安全会话。当管理器配置为使用 protocomm security 0 时,则不需要实现签名握手。
关于安全握手和加密的详情,请参阅 :doc:`provisioning`。应用程序必须使用 :component:`protocomm/proto` 中的 ``.proto`` 文件。``.proto`` 文件定义了 ``prov-session`` 端点支持的 protobuf 消息结构。
建立会话后,以下 ``wifi_config`` 命令集可用于配置 Wi-Fi 凭据,这些命令会被序列化为 protobuf 消息,对应的 ``.proto`` 文件存放在 :component:`wifi_provisioning/proto` 中。
* ``get_status`` - 用于查询 Wi-Fi 连接状态。设备响应状态为连接中、已连接或已断开。如果状态为已断开,则还会包含断开原因。
* ``set_config`` - 用于设置 Wi-Fi 连接凭据。
* ``apply_config`` - 用于应用先前保存的凭据,即由 ``set_config`` 设置的凭据,并启动 Wi-Fi 站点。
建立会话后,客户端还可以从设备请求 Wi-Fi 扫描结果。返回结果为 AP SSID 的列表,按信号强度降序排序。由此,客户端应用程序可以在设备配网时显示附近的 AP,并且用户可以选择其中一个 SSID 并提供密码,然后使用上述 ``wifi_config`` 命令发送密码。``wifi_scan`` 端点支持以下 protobuf 命令:
* ``scan_start`` - 启动 Wi-Fi 扫描有多个选项,具体如下:
* ``blocking`` (输入)- 如果参数为 true,则命令只会在扫描完成后返回。
* ``passive`` (输入)- 如果参数为 true,则以被动模式启动扫描,扫描速度可能更慢。
* ``group_channels`` (输入)- 该参数用于指定是否分组扫描。如果参数为 0,表示一次性扫描所有信道;如果参数为非零值,则表示分组扫描信道且参数值为每组中的信道数,每个连续组之间有 120 毫秒的延迟。分组扫描非常适用于使用 SoftAP 的传输模式,因为一次性扫描所有信道可能会导致 Wi-Fi 驱动没有足够时间发送信标,进而导致与部分站点断连。分组扫描时,管理器每扫描完一组信道,至少会等待 120 毫秒,确保驱动程序有足够时间发送信标。例如,假设共有 14 个 Wi-Fi 信道,将 ``group_channels`` 设置为 3 则将创建 5 个分组,每个分组包含 3 个信道,最后一个分组则为 14 除以 3 余下的 2 个信道。因此,扫描开始时,首先会扫描前 3 个信道,然后等待 120 毫秒,再继续扫描后 3 个信道,以此类推,直到扫描完 14 个信道。可以根据实际情况调整此参数,因为分组中信道数量过少可能会增加整体扫描时间,而信道数量过多则可能会导致连接再次断开。大多数情况下,将参数值设置为 4 即可。请注意,对于低功耗蓝牙等其他传输模式,可以放心将该参数设置为 0,从而在最短时间内完成扫描。
* ``period_ms`` (输入)- 该扫描参数用于设置在每个信道上的等待时间。
* ``scan_status`` - 可以返回扫描过程的状态:
* ``scan_finished`` (输出)- 扫描完成时,该参数返回为 true。
* ``result_count`` (输出)- 该参数返回到目前为止获取的结果总数。如果扫描仍在进行,该数字会不断更新。
* ``scan_result`` - 用于获取扫描结果。即使扫描仍在进行,也可以调用此函数。
* ``start_index`` (输入)- 从结果列表中获取条目的起始索引位置。
* ``count`` (输入)- 从起始索引位置获取的条目数目。
* ``entries`` (输出)- 返回条目的列表。每个条目包含 ``ssid``、``channel`` 和 ``rssi`` 信息。
客户端还可以使用 ``wifi_ctrl`` 端点来控制设备的配网状态。``wifi_ctrl`` 端点支持的 protobuf 命令如下:
* ``ctrl_reset`` - 仅在配网失败时,重置设备的内部状态机并清除已配置的凭据。
* ``ctrl_reprov`` - 仅在设备已成功配网的前提下,设备需要重新配网获取新的凭据时,重置设备的内部状态机并清除已配置的凭据。
附加端点
^^^^^^^^^^^^^^^^^^^^
如果用户想要根据自己的需求定制一些附加 protocomm 端点,可以通过两步完成。第一步是创建一个具有特定名称的端点,第二步是为该端点注册一个处理程序。关于端点处理程序的函数签名,请参阅 :doc:`protocomm`。自定义端点必须在初始化后、配网服务启动之前创建,但只能在配网服务启动后为该端点注册 protocomm 处理程序。注意在自定义端点处理程序的函数中,应使用堆来分配响应这些 protocomm 端点的内存,因为一旦传输层发送完毕,protocomm 层就会释放这部分内存。
.. code-block:: c
wifi_prov_mgr_init(config);
wifi_prov_mgr_endpoint_create("custom-endpoint");
wifi_prov_mgr_start_provisioning(security, pop, service_name, service_key);
wifi_prov_mgr_endpoint_register("custom-endpoint", custom_ep_handler, custom_ep_data);
配网服务停止时,端点会自动取消注册。
在运行时,可以调用 :cpp:func:`wifi_prov_mgr_endpoint_unregister()` 来手动停用某个端点。该函数也可以用于停用配网服务使用的内部端点。
何时以及如何停止配网服务?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
当设备使用 ``apply_config`` 命令设置的 Wi-Fi 凭据成功连接,配网服务将默认停止,并在响应下一个 ``get_status`` 命令后自动关闭低功耗蓝牙或 softAP。如果设备没有收到 ``get_status`` 命令,配网服务将在超时 30 秒后停止。
如果设备因 SSID 或密码不正确等原因无法使用 Wi-Fi 凭据成功连接,配网服务将继续运行,并通过 ``get_status`` 命令持续响应为断连状态,并提供断连原因。此时设备不会再接受任何新的 Wi-Fi 凭据。除非强制启动配网服务或擦除 NVS 存储,这些凭据将保留。
可以调用 :cpp:func:`wifi_prov_mgr_disable_auto_stop()` 来禁用默认设置。禁用后,只有在显式调用 :cpp:func:`wifi_prov_mgr_stop_provisioning()` 之后,配网服务才会停止,且该函数会安排一个任务来停止配网服务,之后立即返回。配网服务将在一定延迟后停止,并触发 ``WIFI_PROV_END`` 事件。该延迟时间可以由 :cpp:func:`wifi_prov_mgr_disable_auto_stop()` 的参数指定。
如果需要在成功建立 Wi-Fi 连接后的某个时间再停止配网服务,应用程序可以采取定制行为。例如,如果应用程序需要设备连接到某个云服务并获取另一组凭证,继而通过自定义 protocomm 端点交换凭证,那么成功完成此操作后,可以在 protocomm 处理程序中调用 :cpp:func:`wifi_prov_mgr_stop_provisioning()` 来停止配网服务。设定适当的延迟时间可以确保 protocomm 处理程序的响应到达客户端应用程序后,才释放传输资源。
应用程序示例
---------------------
关于完整实现示例,请参阅 :example:`provisioning/wifi_prov_mgr`。
配网工具
--------------------
以下为各平台相应的配网应用程序,并附带源代码:
* Android:
* `Play Store 上的低功耗蓝牙配网应用程序 `_。
* `Play Store 上的 SoftAP 配网应用程序 `_。
* GitHub 上的源代码: `esp-idf-provisioning-android `_。
* iOS:
* `App Store 上的低功耗蓝牙配网应用程序 `_。
* `App Store 上的 SoftAP 配网应用程序 `_。
* GitHub 上的源代码: `esp-idf-provisioning-ios `_。
* Linux/MacOS/Windows: 基于 Python 的命令行工具 :idf:`tools/esp_prov`,可用于设备配网。
手机应用程序界面简洁,便于用户使用,而开发者可以使用命令行应用程序,便于调试。
API 参考
-------------
.. include-build-file:: inc/manager.inc
.. include-build-file:: inc/scheme_ble.inc
.. include-build-file:: inc/scheme_softap.inc
.. include-build-file:: inc/scheme_console.inc
.. include-build-file:: inc/wifi_config.inc