# ESP-Moonlight

[中文]

## ESP-Moonlight Support Policy

From December 2023, we will offer limited support on this project, but Pull Request is still welcomed!

## Supported versions and chips

ESP-Moonlight | Dependent ESP-IDF | Target | Support State
--- | --- | --- | ---
master | >=release/v4.4 | ESP32 ESP32S3 | Limited Support

## Get Started

Building Products with ESP32 fast: MoonLight

Jumpstart Now

### 简介

#### ESP-Moonlight：使用 ESP32 快速构建一个月球灯

ESP-Moonlight 是乐鑫推出的基于 ESP32 开发的月球灯示例项目，内含产品开发的完整步骤、最佳做法，可实现驱动 LED 灯、局域网内使用小程序端控制灯的亮度和色彩等功能。本项目以快速开发为特点，硬件结构简单，代码架构清晰完善，方便功能扩展，可用于教育领域。

ESP-Moonlight

本项目中的月球灯有以下功能：

- 板载自动下载电路

- 电池充电功能

- 支持手机进行配网

- 支持手机控制

- 支持触碰控制

- 支持 OTA 升级

- 支持语音唤醒控制

#### 开发板介绍

本项目使用的是 ESP32-Moonlight V2.0 开发板。

ESP32-Moonlight 开发板正面

ESP32-Moonlight 开发板背面

### 入门指南

在本章中，我们将介绍 ESP32 的开发环境，并帮助您了解 ESP32 可用的开发工具和代码仓库。

#### 开发过程概述

使用 ESP32 开发产品时，常用的开发环境如下图所示：

ESP32 产品开发过程

上图电脑（即开发主机）可以是 Linux、Windows 或 MacOS 操作系统。ESP32 开发板通过 USB 连接到开发主机，开发主机上有 ESP-IDF (乐鑫 SDK)、编译器工具链和项目代码。首先，主机编译代码生成可执行文件，然后电脑上的工具把生成的文件烧录到开发板上，开发版开始执行文件，最后你可以从主机查看日志。

#### 准备工作

- ESP32-Moonlight 开发板（也可以通过外接器件使用其他 ESP32 开发板）

- USB 数据线

- 用于开发的 PC（Windows、Linux 或 Mac OS）

#### ESP-IDF 介绍

ESP-IDF 是乐鑫为 ESP32 提供的物联网开发框架。

- ESP-IDF (Espressif IoT Development Framework) 是由乐鑫官方推出的针对 ESP32 系列芯片的开发框架。除此之外，乐鑫还有用于音频开发的 ESP-ADF 和用于 MESH 网络开发的 ESP-MDF 等各种开发框架，全部开放在 github 上。

- ESP-IDF 包含一系列库及头文件，提供了基于 ESP32 构建软件项目所需的核心组件，还提供了开发和量产过程中最常用的工具及功能，例如：构建、烧录、调试和测量等。

- ESP-IDF 支持在 Windows、Mac、Linux 多种操作系统下编译，在 V4.0 版本上除了安装编译环境时的差异之外几乎无差别，编译、烧录的操作都是一致的。

##### 设置 ESP-IDF

请参照 ESP-IDF 入门指南，按照步骤设置 ESP-IDF。注：请完成链接页面的所有步骤。

在进行下面步骤之前，请确认您已经正确设置了开发主机，并按照上面链接中的步骤构建了第一个应用程序。如果上面步骤已经完成，那让我们继续探索 ESP-IDF。

##### ESP-IDF 详解

ESP-IDF 采用了一种基于组件的架构：

ESP-IDF 组件设计

ESP-IDF 中的所有软件均以“组件”的形式提供，比如操作系统、网络协议栈、Wi-Fi 驱动程序、以及 HTTP 服务器等中间件等等。在这种基于“组件”的架构下，你可以轻松使用更多自己研发或第三方提供的组件。

##### 工程目录结构

开发人员通常借助 ESP-IDF 构建 应用程序，包含业务逻辑、外设驱动程序和 SDK 配置。

应用程序架构

- CMakeLists.txt 和 Makefile 文件，用于控制工程的编译过程。

- components 文件夹，包含该项目的组件文件夹。

- main 文件夹，一个特殊的组件，默认编译这里面的代码，应用程序必须包含一个 main 组件。

- 一个可选的 sdkconfig.defaults 文件，存放应用程序默认的 SDK 配置。

在编译完成后会生成以下文件:

- build 文件夹，存放编译输出的文件。

- sdkconfig 文件，定义项目的所有配置。这个文件无需手动修改，编译时会自动从你在 menuconfig 中的设置来更新该文件。

Note

更多关于工程结构和编译过程的细节，请参阅 编程指南/构建系统。

#### 获取 ESP-Moonlight

ESP-Moonlight 库包含了一系列由 ESP-IDF 构建的应用程序，我们将在本次练习中使用这些应用程序。首先克隆 ESP-Moonlight 库：

    $ git clone --recursive https://github.com/espressif/esp-moonlight

我们将构建一个可用的固件，因此选择使用 ESP-IDF 稳定版本进行开发。目前 ESP-Moonlight 使用的是 ESP-IDF V4.0.1 稳定版本，使用如下命令切换 esp-idf 到 v4.0.1 的 tag：

    $ cd esp-idf
    $ git checkout -b v4.0.1 v4.0.1
    $ git submodule update --recursive

Note

不同的版本之间会有一些差异，可能导致编译不通过等问题，关于如何选择 IDF 的版本参见 ESP-IDF 版本简介。

现在，我们构建 ESP-Jumpstart 中的第一个应用程序 Hello World，并将其烧录到开发板上，具体步骤如下，相信您已经熟悉这些步骤：

    $ cd esp-moonlight/1_hello_world
    $ idf.py flash monitor

上面的步骤将编译生成一个应用程序。编译成功后，将会把生成的固件烧录到开发板。

烧录成功后，设备将重启。同时，你还可以在控制台看到该固件的输出。

#### 代码

现在，让我们研究一下 Hello World 应用程序的代码，位于 `examples/1_hello_world`，它非常简单，包含了一些基本的程序功能:

    void app_main()
    {
        int i = 0;
        while (1) {
            printf("[%d] Hello world!\n", i);
            i++;
            vTaskDelay(5000 / portTICK_PERIOD_MS);
        }
    }

下面是这组代码的一些要点：

- `app_main()` 函数是应用程序入口点，FreeRTOS 一旦完成初始化，即将在 ESP32 的其中一个核上新建一个应用程序线程，称为主线程，并在这一线程中调用 `app_main()` 函数。这个就相当于众所周知的程序入口 main 函数。这个函数在 idf 中可以写成死循环操作，也可以在创建一些任务后返回。

- printf()、strlen()、time() 等 C 库函数可以直接调用。IDF 使用 newlib C 标准库，newlib 是一个占用空间较低的 C 标准库，支持 stdio、stdlib、字符串操作、数学、时间/时区、文件/目录操作等 C 库中的大多数函数，不支持 signal、locale、wchr 等。在上面示例中，我们使用 printf() 函数将数据输出打印到控制台。

- `vTaskDelay()` 函数是 FreeRTOS 操作系统提供的一个延时函数。FreeRTOS 是驱动 ESP32 双核的操作系统。FreeRTOS 是一个很小的内核，提供了任务创建、任务间通信（信号量、信息队列、互斥量）、中断和定时器等机制。在上面示例中，我们使用 vTaskDelay 函数让线程休眠 5 秒。有关 FreeRTOS API 的详细信息，请查看 FreeRTOS 文档。

#### 未完待续

到现在为止，我们已经具备了基本的开发能力，可以进行编译代码、烧录固件、查看固件日志和消息等基本开发操作。从这里开始，我们已经成功运行了第一个程序，接下来就是一步一步完成更多的功能

### 驱动程序

这个示例将展示我们如何使用 ESP32 驱动开发板上的外设，本章节只关注实现基本的驱动的功能，后续章节会具体介绍连网功能。如需查看相关代码，请前往 `examples/2_drivers` 目录。

该示例包含以下功能：

- 驱动 LED 灯，可改变灯的颜色、亮度等。

- 一个实体按钮，按下按钮即可控制 LED 灯。

- 监测电池电压，并且把电压值输出到串口监视器。

- 接收震动传感器的触发，实现触碰可变色。

驱动程序的代码已按照功能分开单独放入工程目录下的 `components` 文件夹，因此如果后续需要修改此应用程序用于产品，您只需更改此文件夹下对应的内容。

#### LED 灯

首先，我们需要使 LED 灯亮起来。开发板上有 6 颗三种颜色（红、绿、蓝）分别并联在一起的 RGB LED 灯，ESP32 输出的信号经过三个 MOS 管驱动这些 LED 灯。

硬件的接口定义如下：

接口 | ESP32-S2-SOLO | ESP32-WROOM-32D
--- | --- | ---
红灯 (RED) | IO36 | IO16
绿灯 (GREEN) | IO35 | IO4
蓝灯 (BLUE) | IO37 | IO17

##### 代码

初始化配置 LED 驱动的代码如下：

    /**< configure led driver */
    led_rgb_config_t rgb_config = {0};
    rgb_config.red_gpio_num   = BOARD_GPIO_LED_R;
    rgb_config.green_gpio_num = BOARD_GPIO_LED_G;
    rgb_config.blue_gpio_num  = BOARD_GPIO_LED_B;
    rgb_config.red_ledc_ch    = LEDC_CHANNEL_0;
    rgb_config.green_ledc_ch  = LEDC_CHANNEL_1;
    rgb_config.blue_ledc_ch   = LEDC_CHANNEL_2;
    rgb_config.speed_mode = LEDC_LOW_SPEED_MODE;
    rgb_config.timer_sel  = LEDC_TIMER_0;
    rgb_config.freq       = 20000;
    rgb_config.resolution = LEDC_TIMER_8_BIT;
    g_leds = led_rgb_create(&rgb_config);

    if (!g_leds) {
        ESP_LOGE(TAG, "install LED driver failed");
    }

LEDC 是 ESP32 中主要用于控制 LED 亮度和颜色的一个外设，也可以产生 PWM 信号用于其他用途。上面的程序中调用 `led_rgb_create()` 函数初始化了三个通道分别控制 RGB 三色灯，在参数中指定了 PWM 的频率是 20 KHz ，分辨率为 8 位。

控制 LED 灯的函数如下：

    /**< Write HSV values to LED */
    ESP_ERROR_CHECK(g_leds->set_hsv(g_leds, a, b, c));

    /**< Write RGB values to LED */
    ESP_ERROR_CHECK(g_leds->set_rgb(g_leds, a, b, c));

这里是分别以 HSV 参数和 RGB 参数两种方式控制 LED 灯。

#### 实体按钮

在 ESP32-Moonlight 开发板上有一个按钮，连接至 ESP32 的 GPIO0，我们将配置此按钮，用于切换灯光的模式。

##### 代码

实现此功能的代码如下：

    static void button_press_cb(void *arg)
    {
        if (g_led_mode) {
            g_led_mode = 0;
        } else {
            g_led_mode = 1;
        }

        ESP_LOGI(TAG, "Set the light mode to %d", g_led_mode);
    }

    static void configure_push_button(int gpio_num, void (*btn_cb)(void *))
    {
        button_handle_t btn_handle = iot_button_create(gpio_num, 0);

        if (btn_handle) {
            iot_button_set_evt_cb(btn_handle, BUTTON_CB_TAP, button_press_cb, NULL);
        }
    }

我们使用 `configure_push_button()` 函数来配置按钮功能。首先，创建 button 对象，指定 GPIO 输出端及其有效电平用于检测按钮动作。 然后我们为按钮注册事件回调函数，松开按钮时，就会在 esp-timer 线程中调用 `button_press_cb()` 函数。请确保为 esp-timer 线程配置的默认堆栈足以满足回调函数需求。

#### 震动传感器

在开发板上有一个圆柱形的震动传感器。震动开关在静止时为开路状态，当受到外力触碰而达到一定振动力时或移动速度达到一定离心力时，两个引脚将会瞬间导通，外力消失后恢复到开路状态，传感器将震动转换为可以被 ESP32 检测到的高低电平信号。

和按键的特性类似，在一次震动中会存在很多的抖动信号，电路上传感器的两端并联了一个小电容来消除一些电平的抖动。ESP32 上使用 IO 中断来检测震动传感器的电平变化，软件上通过在检测到第一次电平变化后延时一段时间来避开连续的抖动。

##### 代码

实现此功能的代码如下：

    sensor_vibration_init(BOARD_GPIO_SENSOR_INT);
    sensor_vibration_triggered_register(vibration_handle, NULL);

首先调用 `sensor_vibration_init()` 进行初始化，指定震动传感器的 IO 口，其后注册一个传感器触发的回调函数，当震动传感器输出的电平发生变化即会进行回调。

回调函数代码如下：

    static void vibration_handle(void *arg)
    {
        uint16_t h;
        uint8_t s;

        if (!g_led_mode) {
            return;
        }

        /**< Set a random color */
        h = esp_random() / 11930465;
        s = esp_random() / 42949673;
        s = s < 40 ? 40 : s;

        ESP_ERROR_CHECK(g_leds->set_hsv(g_leds, h, s, 100));
    }

#### 电池电压监测

ESP32 集成有两个 12 位的逐次逼近型 ADC，一共支持 18 个模拟通道输入。电池的电压经过电阻 1/2 分压后输入到 ESP32 ADC 的一个通道。 ADC 内部的参考电压为 1100 mv ，内部还有一个可调的衰减系数，增大了 ADC 的输入范围。

##### 代码

实现此功能的代码如下：

    esp_err_t sensor_adc_init(int32_t adc_channel)
    {
        g_adc_ch_bat = adc_channel;
        /**< Check if Two Point or Vref are burned into eFuse */
        adc_check_efuse();

        /**< Configure ADC */
        adc1_config_width(ADC_WIDTH_BIT_12);
        adc1_config_channel_atten(g_adc_ch_bat, ADC_ATTEN_DB_12);

        /**< Characterize ADC */
        g_adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
        esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_12, ADC_WIDTH_BIT_12, DEFAULT_VREF, g_adc_chars);
        print_char_val_type(val_type);

        xTaskCreatePinnedToCore(sensor_battery_task, "battery", 1024 * 2, NULL, 3, NULL, 1);

        return ESP_OK;
    }

调用 `adc1_config_width()` 配置 ADC 的分辨率为 12 位，然后使用 `adc1_config_channel_atten()` 设置内部衰减为 11 DB，也就是大约原来的 1/3.6 ，再加上外部电阻的衰减才能保证电压动态范围不超过 ADC 的量程， 最后通过 `xTaskCreatePinnedToCore()` 在 CPU1 上创建了一个用于电池监测的任务。

#### 演示

将此固件编译并烧录至设备后，LED 灯的颜色从红色渐变到绿色再到蓝色，以此不断循环。每次按下按钮，ESP32 就会在灯光受震动传感器控制和自动渐变之间来回切换。 同时，在串口 monitor 中还会不断打印出当前 ADC 测量得到的电池电压值。

#### 未完待续

现在，我们已经实现了一个小夜灯本身的驱动功能，当然，目前该设备还无法连网。 下一步，我们将增加 Wi-Fi 连接功能。

### Wi-Fi 连接

ESP32 芯片具有 Wi-Fi 和蓝牙的功能，本节我们将使用 ESP32 连接上一个指定的 Wi-Fi 网络，此 Wi-Fi 网络信息已嵌入到设备固件中。如需查看相关代码，请前往 `examples/3_wifi_connection` 目录。

#### Wi-Fi 模式

ESP32 的 Wi-Fi 具有以下模式：

- 基站模式（即 STA 模式或 Wi-Fi 客户端模式），此时 ESP32 连接到接入点 (AP)。

- AP 模式（即 Soft-AP 模式或接入点模式），此时基站连接到 ESP32。

- AP-STA 共存模式（ESP32 既是接入点，同时又作为基站连接到另外一个接入点）。

- 使用混杂模式监控 IEEE802.11 Wi-Fi 数据包。

在本示例中将设置为 STA 模式去连接一个接入点，并且在程序中嵌入 SSID 和 PASSWORD，让 ESP32 一上电就直接去进行连接，并且设置了一个最大重连次数，超过这个次数认为本次连接失败。

Note

ESP32 芯片只支持 2.4G 的 Wi-Fi，不能连接 5G Wi-Fi。

#### 代码

在 `app_main.c` 添加了几个函数，下面是把 Wi-Fi 初始化为 sta 模式的代码：

     1
     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
     esp_err_t wifi_init_sta(void)
     {
         esp_err_t ret = ESP_OK;
         s_wifi_event_group = xEventGroupCreate();

         tcpip_adapter_init();

         ESP_ERROR_CHECK(esp_event_loop_create_default());

         wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
         ESP_ERROR_CHECK(esp_wifi_init(&cfg));

         ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
         ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));

         wifi_config_t wifi_config = {
             .sta = {
                 .ssid = EXAMPLE_ESP_WIFI_SSID,
                 .password = EXAMPLE_ESP_WIFI_PASS
             },
         };
         ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
         ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
         ESP_ERROR_CHECK(esp_wifi_start());

         ESP_LOGI(TAG, "wifi_init_sta finished.");

         /* 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);

         /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
         * happened. */
         if (bits & WIFI_CONNECTED_BIT) {
             ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                     EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
         } else if (bits & WIFI_FAIL_BIT) {
             ret = ESP_FAIL;
             ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                     EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
         } else {
             ESP_LOGE(TAG, "UNEXPECTED EVENT");
         }

         ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler));
         ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler));
         vEventGroupDelete(s_wifi_event_group);
         return ret;
     }

- 调用 `xEventGroupCreate()` 来创建一个事件标志组，它是操作系统提供的功能， 使用参见 Event Group API。

- 调用 `esp_event_handler_register()` 向 Wi-Fi 组件注册一些事件通知。

- 调用 `tcpip_adapter_init()` 来初始化 TCP/IP 堆栈。

- 通过 `WIFI_INIT_CONFIG_DEFAULT` 读取一个 Wi-Fi 的默认配置。

- 调用 `esp_wifi_init()` 、`esp_wifi_set_config()` 和 `esp_wifi_set_mode()` 来初始化 Wi-Fi 子系统及其 station 接口。将连接的 Wi-Fi 名称和密码分别是 `EXAMPLE_ESP_WIFI_SSID` 和 `EXAMPLE_ESP_WIFI_PASS`。

- 30 ~ 52 行代码是无限等待事件标志组被置位。当 `WIFI_CONNECTED_BIT` 或 `WIFI_FAIL_BIT` 被置位后，打印出信息并删除一些变量且注销事件通知。

Event loop 是 idf 中一个组件之间事件通知的库，它允许低耦合组件在不涉及应用程序的情况下将所需的动作行为附加到其他组件的状态更改上。 下面就是我们前面注册到 Wi-Fi 组件的回调函数:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     static void event_handler(void *arg, esp_event_base_t event_base,
                             int32_t event_id, void *event_data)
     {
         static int32_t s_retry_num = 0;

         if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
             esp_wifi_connect();
         } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
             if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
                 esp_wifi_connect();
                 s_retry_num++;
                 ESP_LOGI(TAG, "retry to connect to the AP");
             } else {
                 xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
             }

             ESP_LOGI(TAG, "connect to the AP fail");
         } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
             ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
             ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
             s_retry_num = 0;
             xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
         }
     }

- 当调用 `esp_wifi_start()` 后会产生一个 `WIFI_EVENT_STA_START` 事件，随即调用 `esp_wifi_connect()` 函数开始连接过程。

- 当连接上后，会产生 `IP_EVENT_STA_GOT_IP` 事件，这时读取 event_data 来获得 ip 地址，并调用 `xEventGroupSetBits()` 来设置事件标志组的 `WIFI_CONNECTED_BIT` 位。

- 当 Wi-Fi 断开连接后将产生 `WIFI_EVENT_STA_DISCONNECTED` 事件，这时将执行 9 ~ 15 行的代码进行重连。

主程序代码：

     1
     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
     void app_main(void)
     {
         uint32_t hue = 0;
         /**< 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);
         /**< install ws2812 driver */
         led_strip_config_t strip_config = LED_STRIP_DEFAULT_CONFIG(BOARD_GPIO_WS2812_DIN, BOARD_STRIP_LED_NUMBER, (led_strip_dev_t)RMT_CHANNEL_0);
         g_strip = led_strip_new_rmt_ws2812(&strip_config);

         if (!g_strip) {
             ESP_LOGE(TAG, "install WS2812 driver failed");
         }

         xTaskCreate(breath_light_task, "breath_light_task", 1024 * 3, NULL, 5, &g_breath_light_task_handle);
         ESP_LOGI(TAG, "Wait for connect");

         /**< Start the station */
         ret = wifi_init_sta();
         vTaskDelete(g_breath_light_task_handle);

         if (ESP_OK != ret) {
             /**< Set leds to red to indicate failure */
             ESP_ERROR_CHECK(g_strip->set_all_rgb(g_strip, 60, 0, 0));
             ESP_ERROR_CHECK(g_strip->refresh(g_strip, 10));
             ESP_LOGW(TAG, "Connect failed");
             return;
         }

         ESP_LOGI(TAG, "Color fade start");

         while (true) {

             /**< Write HSV values to strip driver */
             ESP_ERROR_CHECK(g_strip->set_all_hsv(g_strip, hue, 100, 100));
             /**< Flush to LEDs */
             ESP_ERROR_CHECK(g_strip->refresh(g_strip, 10));
             vTaskDelay(pdMS_TO_TICKS(30));
             hue++;

             if (hue > 360) {/**< The maximum value of hue in HSV color space is 360 */
                 hue = 0;
             }
         }
     }

- 5 ~ 10 行初始化 NVS (Non-volatile storage)，Wi-Fi 需要保存一些参数到 NVS 中。

- 第 21 行调用 `xTaskCreate()` 创建了一个任务来控制 LED 灯实现呼吸效果，表示正在进行连接。

- `wifi_init_sta()` 连接 Wi-Fi ，连接结束后函数才会返回。

Note

在编译例程之前，需要配置连接的 Wi-Fi 的信息：输入 `idf.py menuconfig` 进入 menuconfig ，按照提示在 Example Configuration 设置项中输入信息。

#### 使用演示

- 上电进行 Wi-Fi 连接，LED 亮黄色呼吸灯，这表示 ESP32 还在连接中。

- 一段时间后，如果连接成功 LED 将会高亮并颜色渐变；如果失败了则保持低亮度并变成红色。

- 程序结束。

#### 未完待续

这种连接的方法对业余开发项目而言没有问题，但是对于实际的使用场景，则希望自定义配置设备。这就是我们下一章要讨论的问题。

### SoftAP 配网和 Bluetooth Low Energy 配网

在上个示例中有个很不方便的地方，我们把 Wi-Fi 信息（SSID 和 PASSWORD）直接固定写在了程序中，不能随时改变想要连接的 Wi-Fi，显然这样是不实用的。 所以出现了包括 SoftAP 配网、Bluetooth Low Energy 配网、smartconfig 等多种配网方式，允许用户在设备运行时，将其 Wi-Fi 信息配置到设备中。不同的方式各有优缺点，主要取决于应用场景。

由于用户的网络信息将永久储存在设备中，所以另外提供了 恢复出厂设置 功能，可擦除设备中储存的用户配置信息。如需查看相关代码，请前往 `examples/4_network_config` 目录。

#### 概述

如下图所示，在配网阶段，用户通常使用智能手机将 Wi-Fi 信息安全地配置到设备中。设备获取 Wi-Fi 信息后，就会连接到用户指定的 Wi-Fi 网络中。

配网过程

#### 配网方式

- SoftAP 配网：ESP32 会建立一个 Wi-Fi 热点，用户将手机连接到这个热点后将要连接的 Wi-Fi 信息发送给 ESP32。这种配网模式需要用户手动连接到 ESP32 的热点网络，这会让用户感到奇怪和不友好，不过这种方式很可靠，设备端的代码也简单。

- Bluetooth Low Energy 配网：ESP32 会进行 Bluetooth Low Energy 广播，附近的手机收到该广播后会询问用户是否进行 Bluetooth Low Energy 连接，如选择连接，则手机即可将信息发送给 ESP32。在这个的过程中用户无需切换 Wi-Fi 网络，但是需要打开蓝牙，用户体验相对 SoftAP 配网好一些。但是，需要在设备端加入蓝牙相关代码，这会增加固件的大小，并在配网完成前占用一定内存。

- Smartconfig 配网：这种方式不需要建立任何通信链路，手机端通过发送不同长度的 UDP 广播包来表示 Wi-Fi 信息，ESP32 在混杂模式监听信号覆盖范围内的所有数据帧，通过一定算法得到 Wi-Fi 信息。缺点是配网成功率受环境的影响较大。

- WEB 配网：在 ESP32 上建立热点，使用手机连接上后在浏览器打开配置网页，在网页中完成配网，这种方式很可靠，而且允许在电脑端完成配网，缺点是需要在设备端占用空间来嵌入网页。

#### BluFi 配网

在本节的例程中使用的是 Bluetooth Low Energy 的配网方式，ESP32 上提供了一个完整的解决方案——BluFi。它是一个基于蓝牙通道的 Wi-Fi 网络配置功能，通过安全协议将 Wi-Fi 配置和证书传输到 ESP32，然后 ESP32 可基于这些信息连接到 AP 或建立 SoftAP。

Blufi 是完全开源的，以下为相关下载链接：

- BluFi 协议

- iOS 源码

- Android 源码

你可以从 Android version 和 iOS version 下载手机端 APP 来进行配置。

Note

目前的 IOS 和 Android 的 APP 使用逻辑因为系统的权限等原因而不尽相同，在未来的版本上可能会统一起来。

##### 代码

BluFi 的使用非常简单，下面是主程序部分的代码：

    /**< Initialize the BluFi */
    blufi_init();

    ESP_LOGI(TAG, "Wait for connect");
    blufi_wait_connect();

`blufi_init()` 进行 BluFi 的初始化，`blufi_wait_connect()` 函数则一直等待配网完成。 在 components/blufi 文件夹中存放了 BluFi 的相关代码。

这里给出一些工程中新增文件的作用：

- `components/blufi/blufi.c`：关于 BluFi 的应用代码。

- `components/blufi/blufi_security.c`：关于 BluFi 安全加密相关。

- `sdkconfig.default`：保存了项目的默认配置，用于指定某些配置项。

- `partitions.csv`：ESP32 flash 的分区表，默认的分区表中留给 factory 应用程序的空间对于本节程序是不足的，自定义的分区表将 factory 分区增大到了 2 MB。

#### Wi-Fi 信息的存储

我们希望在进行配网后，设备接收到的网络信息保存起来，以便下次直接读取出来进行连接，这里使用 NVS 进行保存。NVS(Non-volatile storage) 是一种软件组件，用于永久储存键值对，即便设备重启或断电，这些信息也不会丢失。NVS 在 flash 中有一个专门的分区来储存这些信息。

NVS 经过专门设计，不但可以防止设备断电带来的数据损坏影响，而且还可以通过将写入的内容分布到整个 NVS 分中以处理 flash 磨损的问题。请参考 NVS 相关文档，查看详细信息。

默认情况下，Wi-Fi 组件会自动帮我们在 NVS 中保存上一次连接的 Wi-Fi 信息，但是如果调用函数 esp_wifi_set_storage(WIFI_STORAGE_RAM) 将 Wi-Fi 信息保存到了 RAM 中，则掉电丢失该信息。当然我们也可以自己使用 NVS 的相关函数来实现存储包括 Wi-Fi 信息在内的自己的信息。

##### 恢复出厂设置

当我们想要重新进行配网时，恢复出厂设置 便是一个常见需要。通常而言，长按设备上的某个按钮即可恢复出厂设置。

在应用程序中，我们通过长按按钮动作来恢复出厂设置，下面是按键配置的程序：

    static void configure_push_button(int gpio_num)
    {
        button_handle_t btn_handle = iot_button_create(gpio_num, 0);

        if (btn_handle) {
            iot_button_add_on_press_cb(btn_handle, 3, button_press_3sec_cb, NULL);
        }
    }

在成功创建了一个按键驱动后添加了一个长按的动作回调函数，一旦按钮被按下超过 3 秒，就会回调 `button_press_3sec_cb()` 函数。

回调函数内容如下：

    static void button_press_3sec_cb(void *arg)
    {
        ESP_LOGW(TAG, "Restore factory settings");
        nvs_flash_erase();
        esp_restart();
    }

这段代码的作用是擦除 NVS 的所有内容，然后触发设备重启。由于 NVS 内容已被清除，设备下次启动时将回到未配置状态。

#### 演示

在 ESP-MoonLight 提供的微信小程序中集成了配网功能，扫描下面二维码进入：

微信小程序二维码

微信小程序的源码可在 https://github.com/EspressifApps/Moonlight 查看。

- 上电等待 10 秒的时间用之前保存的 Wi-Fi 信息进行自动连接，此时 LED 是黄色呼吸灯状态。

- 如果自动连接成功则 LED 直接高亮并开始颜色渐变，配网结束；如果未连接则启动 BluFi 并保持呼吸灯状态。

- 使用手机进行配网，这里有两种可选方式：

  - 打开手机 APP 扫描设备后配置网络。有关如何配网的详细介绍，可参阅 ESP32 蓝牙配⽹用户指南。

  - 使用微信小程序，按照提示步骤进行配网。

- ESP32 按照接收到的 Wi-Fi 信息进行连接，成功后 LED 高亮并开始颜色渐变，代表配网完成。

- 如果此时重启设备，设备将不再进入网络配置模式，而是直接连接已配置的 Wi-Fi 网络。这就是我们想要的效果。

- 这里，如果想重新配置你的设备，可尝试长按按钮（3 秒以上），你可以看到恢复出厂设置的整个过程。

#### 未完待续

目前，我们已经有了这样一个通过手机 app 连入家庭 Wi-Fi 网络的月球灯，并且网络配置的过程已经很方便了。下一步，我们会将连网和自身的功能结合起来，远程控制灯的状态。

### 远程控制（局域网）

要想成为一个智能硬件，单纯的连网是不够的。真正的智能在于通过联网对设备进行远程控制或监测，然后依托于强大的云服务，赋予设备一定的智慧。 在本章中，我们先将月球灯在局域网内连接到手机，实现对设备的控制。如需查看相关代码，请前往 `examples/5_app_control` 目录。

#### 通信连接方式

首先需要确定使用何种连接方式，下面提供了几种方式：

1. 将设备接入云，手机通过云平台的 API 来进行控制，如下图

云连接

这种方式需要一个设备云服务器，这样不仅允许手机的远程控制，还允许通过其他的云来透过设备云控制设备，具有极大的灵活性。

2. 设备在局域网内连接，不经过外网，如下图

局域网连接

相比之下，这种在局域网内的连接方式简单了许多，但同时也限制了控制只能在该局域网下进行。如果手机离开了该网络，则无法控制我们的设备，因为 Wi-Fi 覆盖范围有限，这样的断连情况较为常见。

#### 通信过程

简单起见，这里我们选用第二种方式并且使用 UDP 进行通信，使 ESP32 设备端作为 UDP Server，手机作为 Client 端，手机上使用微信小程序作为控制端。 当在微信小程序中点击颜色块时，手机会以 UDP 广播的方式发送 JSON 数据包，ESP32 接收到数据后进行解析，将得到的颜色值发送给 LED 灯。

通信过程

##### 数据格式

数据使用 JSON 格式，如下所示:

    {
        "led":{
            "red":255,
            "green":255,
            "blue":255
        }
    }

其中 red、green、blue 分别控制着红、绿、蓝三色的亮度，其范围值都是 0 ~ 255。

#### 代码

以下所示为 UDP 通信的部分代码：

     1
     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
    if (addr_family == AF_INET) {
        struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
        dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
        dest_addr_ip4->sin_family = AF_INET;
        dest_addr_ip4->sin_port = htons(PORT);
        ip_protocol = IPPROTO_IP;
    }

    int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);

    if (sock < 0) {
        ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
        break;
    }

    ESP_LOGI(TAG, "Socket created");

    int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));

    if (err < 0) {
        ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
    }

    ESP_LOGI(TAG, "Socket bound, port %d", PORT);

    while (1) {

        ESP_LOGI(TAG, "Waiting for data");
        struct sockaddr_in6 source_addr; /**< Large enough for both IPv4 or IPv6 */
        socklen_t socklen = sizeof(source_addr);
        int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);

        /**< Error occurred during receiving */
        if (len < 0) {
            ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
            break;
        }
        /**< Data received */
        else {
            /**< Get the sender's ip address as string */
            if (source_addr.sin6_family == PF_INET) {
                inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
            } else if (source_addr.sin6_family == PF_INET6) {
                inet6_ntoa_r(source_addr.sin6_addr, addr_str, sizeof(addr_str) - 1);
            }

            rx_buffer[len] = 0; /**< Null-terminate whatever we received and treat like a string... */
            ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);

            cJSON *root = cJSON_Parse(rx_buffer);

            if (!root) {
                printf("JSON format error:%s \r\n", cJSON_GetErrorPtr());
            } else {
                cJSON *item = cJSON_GetObjectItem(root, "led");
                int32_t red = cJSON_GetObjectItem(item, "red")->valueint;
                int32_t green = cJSON_GetObjectItem(item, "green")->valueint;
                int32_t blue = cJSON_GetObjectItem(item, "blue")->valueint;
                cJSON_Delete(root);

                if (red != g_red || green != g_green || blue != g_blue) {
                    g_red = red;
                    g_green = green;
                    g_blue = blue;
                    ESP_LOGI(TAG, "Light control: red = %d, green = %d, blue = %d", g_red, g_green, g_blue);
                    ESP_ERROR_CHECK(g_leds->set_rgb(g_leds, g_red, g_green, g_blue));
                }
            }
        }
    }

- 1 ～ 26 行为 UDP 的通信配置过程

- 在循环中不断调用 `recvfrom()` 来接收数据

- 将接收到的数据使用 `cJSON_Parse()` 进行解析得到 LED 灯的颜色值

- 最后用解析出的颜色值去控制 LED 灯

Note

为了确保通信的可靠性，微信小程序在发送 UDP 广播时，会重复发送多次。

#### 未完待续

通过这个应用程序，我们将月球灯本身的功能与网络连接功能结合到了一起，实现了一个简单的远程控制。云端的控制我们将在以后介绍。下一章，我们会探讨连网设备的一个常见功能：空中固件升级。

### 固件升级

有时我们开发了新的功能，希望给已经在运行的设备进行固件升级。但很可能因为电路板已经被安装了外壳等原因而不能方便地使用下载器将新的固件烧录到设备，此时将需要使用 OTA (over-the-air) 升级功能。

本节将实现 ESP32 从指定的 URL 来更新固件的功能。如需查看相关代码，请前往 `examples/6_ota` 目录。

#### Flash 分区

在讨论固件升级之前，让我们先了解一下 ESP32 中的 flash 分区。

在 ESP32 的应用中，通常包含多种不同类型的数据，因此通过分区表将 flash 划分为多个逻辑分区。具体结构如下：

Flash 分区结构

从上图可以看出，flash 地址在 0x9000 之前的结构是固定的，第一部分包括二级 Bootloader，后面紧接着就是分区表，分区表用来管理储存在 flash 剩余区域的数据分布。 关于分区表内容的详细信息可参见 分区表介绍。

##### 创建分区表

使用 OTA 功能需要包含 OTA Data 分区和两个应用程序分区，可以通过创建一个分区表文件来实现，即 CSV 文件（Comma Separated Values，逗号分隔值）。

本示例所用的分区表文件内容如下：

    # Name,   Type, SubType, Offset,   Size, Flags
    nvs,      data, nvs,     ,        0x4000,
    otadata,  data, ota,     ,        0x2000,
    phy_init, data, phy,     ,        0x1000,
    ota_0,    app,  ota_0,   ,        1600K,
    ota_1,    app,  ota_1,   ,        1600K,

在这个分区表中，指定了两个 1600 KB 大小的应用程序分区，足够存放我们待升级的固件。

OTA Flash 分区

创建此分区文件后，我们还需在 menuconfig 中配置使用该自定义分区，而非默认分区。你可以在 `Partition Table ---> Partition Table` 中选择 `Custom partition table CSV`，同时在下面指定分区表文件名。 本示例中，已经在 `sdkconfig.defaults` 文件中重写了默认配置，因此无需再进行其他配置操作。

#### 空中升级过程

空中升级使用两个应用程序分区交替工作的方式，确保不会因升级失败而无法启动设备，OTA Data 分区将记录哪个是活动分区。

OTA 固件升级过程中，状态变更如图所示：

固件升级步骤

- 步骤 0：OTA 0 为活动固件，该信息储存在 OTA Data 分区。

- 步骤 1：固件升级开始，识别并擦除非活动分区，新的固件将写入 OTA 1 分区。

- 步骤 2：固件写入完毕，开始进行验证。

- 步骤 3：固件升级成功，OTA Data 分区已更新，并指示 OTA 1 现在是活动分区。下次启动时，固件将从此分区启动。

#### 代码

现在我们来看一下实际执行固件升级的代码：

    esp_http_client_config_t config = {
         .url = url,
         .cert_pem = (char *)server_cert_pem_start,
         .event_handler = _http_event_handler,
      };

      esp_err_t ret = esp_https_ota(&config);
      if (ret == ESP_OK) {
         esp_restart();
      } else {
         ESP_LOGE(TAG, "Firmware upgrade failed");
      }
      return ret;

- 使用 `esp_http_client_config_t` 配置 OTA 升级源，包括升级地址的 URL，用于验证服务器的 CA 证书（升级从此服务器处获取）。

- 然后执行 `esp_https_ota()` API 启动固件升级，固件升级成功后将设备重启。

##### 固件升级 URL

使用本示例之前需要配置一个 URL 链接，在 menuconfig 中的 `Example Configuration ---> firmware upgrade url endpoint` 进行配置。

示例中使用的是本地的 http server，所以这里的 IP 地址需改成本机的。

#### 演示

本示例中的升级过程如下图所示：

OTA 升级过程

##### 运行 HTTPS Server

- 输入 `cd https_server`，进入该文件夹。

- 执行命令：`openssl req -x509 -newkey rsa:2048 -keyout ca_key.pem -out ca_cert.pem -days 365 -nodes`，创建一个自签名的证书和 KEY，后续设置可参照 生成证书演示。该步骤完成后会在当前目录下生成两个后缀为 .pem 的文件。

- 启动 HTTPS server，执行命令：`openssl s_server -WWW -key ca_key.pem -cert ca_cert.pem -port 8070`。

- 在这个文件夹下我们已经放了一个示例 2_drivers 的程序固件 moonlight.bin。你也可以替换成自己的固件，当然你需要去配置对应的 firmware upgrade url endpoint。

Note

如果有防火墙软件阻止对端口 8070 的访问，请将其配置为在运行本示例时允许访问。

Note

Windows 系统的用户来需要在 openssl 命令前加上 winpty。命令行如下所示：

- `winpty openssl req -x509 -newkey rsa:2048 -keyout ca_key.pem -out ca_cert.pem -days 365 -nodes`

- `winpty openssl s_server -WWW -key ca_key.pem -cert ca_cert.pem -port 8070`

##### 编译烧录固件

和以前一样的执行 `idf.py flash monitor` 即可编译并烧录固件到开发板，同时打开串口监视器。 在编译时，会将我们前面生成的 `ca_cert.pem` 证书文件嵌入到最终的固件中。

##### 执行固件升级

烧录固件后的开发板将处于等待配网的状态，表现为黄色的呼吸灯。只有在经过配网后才能进行 OTA 的操作，在配网后就可以通过短按按键来触发固件升级操作。 升级成功将会自动重启运行升级后的固件。

在升级开始时，运行 HTTPS Server 的终端下将会出现如下信息：

    ACCEPT
    FILE:moonlight.bin

#### 未完待续

有了这个空中升级的功能，我们就可以方便的对设备进行升级。虽然这样有一个缺点是必须多空出一个固件大小的 flash 空间，不过它所带来的益处要更大。

### 语音识别控制

这个示例将展示如何使用一个乐鑫提供的语音识别相关方向算法模型 ESP-SR 来控制 LED 灯。如需查看相关代码，请前往 `examples/7_recognition` 目录。

该示例包含以下功能：

- 使用语音唤醒开发板

- 支持连续对话

- 语音控制 LED 灯的开关、颜色、亮度

#### ESP-SR 介绍

包含三个主要模块：

- 唤醒词识别模型：WakeNet

- 语音命令识别模型：MultiNet

- 声学算法：麦克风阵列语音增强（Mic-Array Speech Enhancement，简称 MASE）、回声消除（Acoustic Echo Cancellation，简称 AEC）、语音端点检测（Voice Activity Detection，简称 VAD）、自动增益控制（Automatic Gain Control，简称 AGC）、噪声抑制（Noise Suppression，简称 NS）

唤醒词模型 WakeNet 目前提供了免费的 “Hi，乐鑫” 唤醒词，这也是我们这里使用的唤醒词。命令词识别模型 MultiNet 则允许用户自定义语音命令而无需重新训练模型。

详细信息可参见 ESP-SR。

#### 麦克风输入

要使用语音识别控制，首先应该有一个音频输入。开发板使用了一个 I2S 数字输出的 MEMS 麦克风直接与 ESP32 的 I2S 接口连接。

硬件接口如下：

Pin Name | ESP32-S3-WROOM-1 | ESP32-S2-SOLO | ESP32-WROOM-32D
--- | --- | --- | ---
DMIC_I2S_SCK | IO39 | IO15 | IO32
DMIC_I2S_WS | IO38 | IO16 | IO32
DMIC_I2S_SDO | IO40 | IO17 | IO25

这种设计无需使用外置 Audio Codec ，极大地简化了电路。

#### 扬声器输出

同时该实例还支持一路扬声器输出，通过 I2S 接口连接。硬件接口如下：

Pin Name | ESP32-S3-WROOM-1 | ESP32-S2-SOLO | ESP32-WROOM-32D
--- | --- | --- | ---
DSPEAKER_I2S_BCK | IO48 | × | ×
DSPEAKER_I2S_WS | IO45 | × | ×
DSPEAKER_I2S_SDO | IO47 | × | ×

Note

ESP32S3 开启扬声器功能，需使用 IDF 版本 v5.0 及以上。

#### 代码

以下所示为调用语音识别模型的部分代码：

    i2s_read(1, buffer, size * 2 * sizeof(int), &read_len, portMAX_DELAY);

    for (int x = 0; x < size * 2 / 4; x++) {
       int s1 = ((buffer[x * 4] + buffer[x * 4 + 1]) >> 13) & 0x0000FFFF;
       int s2 = ((buffer[x * 4 + 2] + buffer[x * 4 + 3]) << 3) & 0xFFFF0000;
       buffer[x] = s1 | s2;
    }

    if (enable_wn) {
       wakenet_state_t r = wakenet->detect(model_wn_data, (int16_t *)buffer);

       if (r == WAKENET_DETECTED) {
             ESP_LOGI(TAG, "%s DETECTED", wakenet->get_word_name(model_wn_data, r));

             if (NULL != g_sr_callback_func[SR_CB_TYPE_WAKE].fn) {
                g_sr_callback_func[SR_CB_TYPE_WAKE].fn(g_sr_callback_func[SR_CB_TYPE_WAKE].args);
             }

             enable_wn = false;
       }
    } else {
       esp_mn_state_t mn_state = multinet->detect(model_mn_data, (int16_t *)buffer);
       if (mn_state == ESP_MN_STATE_DETECTED) {
             esp_mn_results_t *mn_result = multinet->get_results(model_mn_data);
             ESP_LOGI(TAG, "MN test successfully, Commands ID: %d", mn_result->phrase_id[0]);
             int command_id = mn_result->phrase_id[0];

             if (NULL != g_sr_callback_func[SR_CB_TYPE_CMD].fn) {
                if (NULL != g_sr_callback_func[SR_CB_TYPE_CMD].args) {
                   g_sr_callback_func[SR_CB_TYPE_CMD].fn(g_sr_callback_func[SR_CB_TYPE_CMD].args);
                } else {
                   g_sr_callback_func[SR_CB_TYPE_CMD].fn((void *)command_id);
                }
             }

       } else if (mn_state == ESP_MN_STATE_TIMEOUT) {
             esp_mn_results_t *mn_result = multinet->get_results(model_mn_data);
             ESP_LOGI(TAG, "timeout, string:%s\n", mn_result->string);

             if (NULL != g_sr_callback_func[SR_CB_TYPE_CMD_EXIT].fn) {
                g_sr_callback_func[SR_CB_TYPE_CMD_EXIT].fn(g_sr_callback_func[SR_CB_TYPE_CMD_EXIT].args);
             }

             enable_wn = true;
       } else {
             continue;
       }
    }

- 首先调用 `i2s_read()` 从麦克风读取一段音频数据，然后进行数据格式的调整。

- 根据 `enable_wn` 变量来控制使用唤醒识别还是命令词识别。

- 调用 `detect()` 函数将音频数据送入对应的识别网络进行识别。

- 在识别命令词时，当识别超时时回到唤醒词识别状态。

#### 命令词定义

`app_speech_rec.c` 文件中定义了 11 条控制命令，如下所示：

    char *commands[] = {
       "da kai dian deng",
       "kai deng",
       "da kai xiao ye deng",
       "guan bi dian deng",
       "guan deng",
       "guan bi xiao ye deng",
       "huan yi ge yan se",
       "liang yi dian",
       "zeng da liang du",
       "an yi dian",
       "jian xiao liang du",
    };

    esp_mn_commands_clear();
    for (int i = 0; i<COMMANDS_NUM ; i++) {
       esp_mn_commands_add(i, commands[i]);
    }
    esp_mn_commands_update();

通过调用 esp_mn_commands_add 接口添加命令词，你也可以其他方式添加自己的语音命令，具体方法可参见 MultiNet 介绍。 请注意，添加语音命令后需更改语音命令的数量，使之显示实际数量。

#### 演示

- 先说出唤醒词“Hi，乐鑫”，唤醒开发板，让 ESP32 运行命令词识别模型，此时 LED 呈现绿色呼吸灯状态。

- 唤醒后可说出“打开电灯”、“关闭电灯”、“增大亮度”等命令来控制灯的状态，前文已列出可支持的全部语音指令。

- 支持连续对话，唤醒一次后，可连续识别多条命令。

- 唤醒后亮绿色呼吸灯为命令词识别状态，若一段时间后未识别到有效指令，开发板将自动回到等待唤醒的状态。

Read the Docs v: latest

Versions
  latest

Downloads
  pdf
  html

On Read the Docs
  Project Home
  Builds