Funpack2-5 基于ESP32-S3-BOX-LITE的天气情况播报
Funpack2-5ESP32-S3-BOX-LITE tts esp-idf wifi 心知天气
标签
嵌入式系统
Funpack活动
测试
happy
更新2023-08-02
903

硬件介绍:funpack第二季第5期活动带来的这款ESP32-S3-BOX-Lite AI语音开发套件,是乐鑫打造的一个智能语音设备开发平台。搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。为用户提供了一个基于语音助手、传感器、红外控制器和智能 Wi-Fi 网关等功能开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。
板子上集成了两个麦克风、一个扬声器,还有一个2.4寸的屏幕。看官方例程,是一个很厉害的语音方面的开发板。

任务选择:这次的任务选择比较曲折。这个开发板支持micropython、arduino、esp-idf的开发。这里边micropython觉着最简单,于是就选择使用micropython来作为开发工具,去尝试完成任务三:使用板卡的屏幕和联网功能,实现一个在线电子书浏览器,从网络上获取文本并显示在屏幕上,通过按键翻页。
FoE9SseqgPXd91CIulQn9ZmUllrq
不得不说micropython的效率是真高,很快就搞定了显示、wifi、按键。并且成功搞定了tft下的汉字显示。但是问题也是超级明显。问题1:汉字显示,这里的汉字显示使用的是查表方式,从中文字库中查找汉字。中文字库文件包含了常用的汉字,提供了三个尺寸的规格,字库文件倒也不大,ESP32-S3-BOX-Lite能轻松保存下来。可是查找效率就真的不高,速度很慢,汉字显示时一个一个汉字蹦出来。问题2:网页抓取问题。python有很成熟的html解析库,都很好用。可是在mpy下我没能找到一个能用的html解析库来用。而且小说网页的返回页面,都比较大,mpy处理这样超长的的文本字符串时,感觉内存有问题,经常报错。基于以上两个困难,最终放弃了任务三。
最后选择了任务1:使用ESP32的WiFi和TTS功能,实现一个语音播报系统,如联网获取粉丝数并播报或者获取天气并播报。micropython没有找到驱动乐鑫的TTS的方法,扬声器的I2S驱动也没能搞定,最终开发工具也只能放弃简单的mpy,改用ESP-IDF来进行开发。

任务实现:FpJr5qQ2Ck5Clcgo1kGjOPDAi_S0

先做了个流程图,可以看出需要的外设有按键、wifi、扬声器。ESP-IDF环境真的是麻烦,对网络要求很高,单单esp-idf的安装,就搞了几天。不过这块网络上的详细步骤挺多的,按照网络上的步骤,一步步地操作,就能完成。这里我使用的开发工具为vscode。FnheBf-cxxL-aaxXm3ahuAIq6T69esp-idf做开发还是蛮复杂的,好在乐鑫提供了大量的例程可以做参考。这里在群里老师的指导下去了解了https://github.com/espressif/esp-skainet.git这个例程。这个例程是乐鑫提供的文字转语音的例子。以前接触的文字转语音基本流程都是在互联网上,通过一下AI平台,提供需要转换的文字,然后获取到转换后的语音。而这里是离线转换,也就是说文字转换为对应的语音整个过程都是通过ESP32-S3-BOX-Lite来实现的(👍)。并且转换后的语音听着还挺不错的,没有早期电脑上的那种TTS,一个字一个字地往外蹦的生硬感,语句还算连贯。通过源代码查看,TTS部分是调用了esp-adf下的esp-sr/esp-tts的库文件,这部分乐鑫只提供了函数的定义和实现文件的类库,没有提供源代码。猜测使用了机器学习的技术。
FpWDFISLGcnP1a0NHfYaG2809UWq

首先驱动按键,这里提供了3个按键供用户使用。从电路图可以看出三个按键都接在GPIO1管脚上,通过电阻做分压,不同按键按下时,1管脚上的电压不同。通过电压来判断按键。这里参考esp-idf例程peripherals\adc\single_read\adc,来读取按键的值,将读取到的值除以1000,正好映射到 0、2、3、4四个整数值,用来标记 右键、中间键、左键 、无按键四个动作。并且启动一个线程,用来监控按键的动作。

// ADC初始化
u8_t key = 4; //按键值 0 右键  2 中间键   3 左键   4 无按键
static esp_adc_cal_characteristics_t adc1_chars;
#define ADC1_EXAMPLE_CHAN0 ADC1_CHANNEL_0 //使用GPOI1作为AD按键的输入
static bool adc_calibration_init(void)
{
    esp_err_t ret;
    bool cali_enable = false;
    ret = esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP_FIT);
    if (ret == ESP_ERR_NOT_SUPPORTED)
    {
        ESP_LOGW(TAG, "Calibration scheme not supported, skip software calibration");
    }
    else if (ret == ESP_ERR_INVALID_VERSION)
    {
        ESP_LOGW(TAG, "eFuse not burnt, skip software calibration");
    }
    else if (ret == ESP_OK)
    {
        cali_enable = true;
        esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_DEFAULT, 0, &adc1_chars);
    }
    else
    {
        ESP_LOGE(TAG, "Invalid arg");
    }
    return cali_enable;
}

void listen_key_task(void *pvPar)
{
    adc_calibration_init(); //初始化ADC
    // ADC1 config
    ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_DEFAULT));
    ESP_ERROR_CHECK(adc1_config_channel_atten(ADC1_EXAMPLE_CHAN0, ADC_ATTEN_DB_11));
    while (1)
    {
        u16_t adcval = adc1_get_raw(ADC1_EXAMPLE_CHAN0) / 1000;

        if (adcval != 4 && adcval != key)
        {
            ESP_LOGI(TAG, "Adc value key:%d", adcval);
            key = adcval;
        }
        // printf("key %d\r\n", );
        //使用此延时API可以将任务转入阻塞态,期间CPU继续运行其它任务
        vTaskDelay(80 / portTICK_PERIOD_MS);
    }
}

学着esp-idf例程中使用wifi的方法,这个非常简单,只需要调用example_connect()这个函数的方法(调用前包含好对应的头文件),就能轻易地连接上在sdkconfig中配置的wifi了。联网后,项目需要当前时间的天气情况,这个就需要从互联网上获得了。这里获取天气情况我使用的是心知天气http://api.seniverse.com/v3/weather/now.json?key=xxxxxxx&location=dongguan&language=zh-Hans&unit=c。从心知天气通过get方法,访问上边的地址,就能拿到一串当前天气的json字符串了。然后使用cJSON这个类库,来解析文件内容。这里需要获取当前城市、天气、温度信息,构建成需要播报的字符串。

 {"results":[{"location":{"id":"WS0GHKN5ZP7T","name":"东莞","country":"CN","path":"东莞,东莞,广东,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"大雨","code":"15","temperature":"27"},"last_update":"2023-07-02T16:40:16+08:00"}]}
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    switch (evt->event_id)
    {
    case HTTP_EVENT_ERROR:
        ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
        break;
    case HTTP_EVENT_ON_CONNECTED:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
        break;
    case HTTP_EVENT_HEADER_SENT:
        ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
        break;
    case HTTP_EVENT_ON_HEADER:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
        break;
    case HTTP_EVENT_ON_DATA:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
        break;
    case HTTP_EVENT_ON_FINISH:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
        break;
    case HTTP_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
        break;
    }
    return ESP_OK;
}
//获取心知网页,获得天气预报的json
static char *http_web_request()
{
    int8_t return_res = 1;
    char *weather_buffer = NULL;
    int content_length = 0;
    esp_http_client_config_t config = {
        .event_handler = _http_event_handler,
        .url = "http://api.seniverse.com/v3/weather/now.json?key=lxtscavxia1gwsbd&location=dongguan&language=zh-Hans&unit=c",
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);
    if (client == NULL) //如果URL有误,则返回
    {
        return NULL;
    }

    // GET Request
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    esp_err_t err = esp_http_client_open(client, 0);
    if (err != ESP_OK)
    {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
        return_res = 0;
    }
    else
    {
        content_length = esp_http_client_fetch_headers(client); //返回内容长度
        if (content_length < 0)
        {
            ESP_LOGE(TAG, "HTTP client fetch headers failed");
            return_res = 0;
        }
        else
        {
            weather_buffer = malloc(content_length + 1);
            memset(weather_buffer, 0, content_length + 1);
            if (weather_buffer == NULL)
            {
                return_res = 0;
            }
            else
            {
                int data_read = esp_http_client_read_response(client, weather_buffer, content_length);
                if (data_read >= 0)
                {
                    ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d, data_read = %d",
                             esp_http_client_get_status_code(client),
                             esp_http_client_get_content_length(client),
                             data_read);
                    // ESP_LOG_BUFFER_HEX(TAG, weather_buffer, data_read);
                    ESP_LOGI(TAG, "Data %s \r\n", weather_buffer);
                }
                else
                {
                    ESP_LOGE(TAG, "Failed to read response");
                    return_res = 0;
                }
            }
        }
    }
    esp_http_client_close(client);
    if (!return_res)
    {
        free(weather_buffer);
        weather_buffer = NULL;
    }
    return weather_buffer;
}

//将json字符串,拼接成需要预报的字符串
static bool parse_weather_json(char *analysis_buf, char *tts_str)
{
    if (analysis_buf == NULL)
    {
        return false;
    }

    cJSON *json_root = cJSON_Parse(analysis_buf);
    if (json_root != NULL)
    {
        cJSON *cjson_arr = cJSON_GetObjectItem(json_root, "results");
        cJSON *cjson_location = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0), "location");
        // ESP_LOGI(TAG, "城市 -> %s", cJSON_GetObjectItem(cjson_location, "name")->valuestring);
        sprintf(tts_str, "%s", cJSON_GetObjectItem(cjson_location, "name")->valuestring); //城市
        cJSON *cjson_now = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0), "now");
        // ESP_LOGI(TAG, "天气 -> %s", cJSON_GetObjectItem(cjson_now, "text")->valuestring);
        sprintf(tts_str, "%s:天气%s,", tts_str, cJSON_GetObjectItem(cjson_now, "text")->valuestring); //当前天气
        // ESP_LOGI(TAG, "code -> %s", cJSON_GetObjectItem(cjson_now, "code")->valuestring);
        // ESP_LOGI(TAG, "气温 -> %s", cJSON_GetObjectItem(cjson_now, "temperature")->valuestring);
        sprintf(tts_str, "%s:气温%s摄氏度.", tts_str, cJSON_GetObjectItem(cjson_now, "temperature")->valuestring);
        cJSON_Delete(json_root);
    }
    free(analysis_buf);

    return true;
}

最后在主函数中,监听这按键信息。当收到中间按键按下的消息后,就访问一次心知网页,然后调用esp_tts_parse_chinese方法将构建好的字符串形成语音数据,然后送到扬声器播放。若是左右按键按下,就调用es8156_codec_set_voice_volume方法来设置声音大小。这里声音大小范围从0~100,步长值设置为10,但是音量调到最大,声音依然不够嘹亮。直到最后也没能驱动起屏幕,算是留下了一个遗憾!

int app_main()
{
    int vol=50;           //音量
    char *tts_str[50]; // 需要转换的文字
    memset(tts_str, 0, 50);
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES)
    {
        // NVS partition was truncated and needs to be erased
        // Retry nvs_flash_init
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    xTaskCreatePinnedToCore(listen_key_task, "listen_key_task", 1024, "listen_key_task", 2, NULL, tskNO_AFFINITY);

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    ESP_ERROR_CHECK(example_connect()); //连接wifi

    ESP_ERROR_CHECK(esp_board_init(AUDIO_HAL_16K_SAMPLES, 1, 16));     // 初始化codec芯片,配置好采样率、声道数、采样大小
    esp_tts_voice_t *voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件,来自libvoice_set_xiaole
    esp_tts_handle_t *tts_handle = esp_tts_create(voice);              // 创建tts对象
    es8156_codec_set_voice_volume(vol);

    while (1)
    {
        if (key == 2)
        {
            char *weather_json = http_web_request(); //获取当前东莞的天气预报
            parse_weather_json(weather_json, tts_str);

            ESP_LOGI(TAG, "%s", tts_str);
            // free(weather_json);
            if (esp_tts_parse_chinese(tts_handle, tts_str)) // 文字解析成拼音
            {
                int len[1] = {0};
                do
                {
                    short *pcm_data = esp_tts_stream_play(tts_handle, len, 2); // 拼音转换成pcm音频
                    esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY);       //播放音频
                    // es8156_codec_set_voice_volume((num++)*10);
                } while (len[0] > 0);
            }
            esp_tts_stream_reset(tts_handle); // 重置 tts 流并清除 TTS 实例的所有缓存
            key = 4; //重置按键
        }
        else if (key == 0)          //增大音量
        {
            vol+=10;
            if(vol>100) vol=100;
            ESP_LOGI(TAG, "vol value :%d", vol);
            es8156_codec_set_voice_volume(vol);
            key = 4; //重置按键
        }
        else if (key == 3)          //缩小音量
        {
            vol-=10;
            if(vol<0) vol=0;
            ESP_LOGI(TAG, "vol value :%d", vol);
            es8156_codec_set_voice_volume(vol);
            key = 4; //重置按键
        }
            
        vTaskDelay(150 / portTICK_PERIOD_MS);
    }
    return 0;
}

FqqLI6pG27lqHSoOi5vrLiiT4kZ8

心得体会: 很开心参加funpack举办的这次活动,在这里感受到了ESP32-S3-BOX-Lite板子功能的强大,也感受到了esp-idf的复杂!任务的完成得到交流群里很多老师的帮助,在此表示非常感谢!期待学习各位老师的作品!

代码过大,无法作为附件上传,顾使用网盘共享。

链接:https://pan.baidu.com/s/1zGIxmaQTcxGTc_46paDz6w 
提取码:9271 

团队介绍
单片机爱好者
团队成员
happy
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号