README.md

    @TOC


    前言

    本项目主要承接上回利用ESP32S3-EYE进行的一系列开发,此次利用ESP-BOX实现整个心率预警系统的完善,上一篇文章的内容可以在以下链接中找到: ESP32s3-EYE Ubuntu18.04 Pycharm 物联网 ESP-BOX部分代码采用ESP-BOX文件夹下的factory-demo进行修改,分为用户BOX以及AED-BOX


    一、项目架构

    在这里插入图片描述

    二、ESP-BOX(AED模块)

    自动体外除颤器又称自动体外电击器,是可被非专业人员使用的用于抢救心脏骤停患者的医疗设备。随着AED在城市中的普及,却少有人能够使用。因此我们将AED和ESP-BOX结合,通过远程报警,搜寻呼救者最近的AED,并利用啸叫吸引周围的人从而发出求救信号,若附近正好有人或专业医生,则可以在黄金四分钟内找到患者并救治。

    1.警报啸叫

    ESP-BOX在背部提供许多I/O端口,可以实现多种外设接入与控制,此处利用第41号端口,以及3V3电源和GND,连接上蜂鸣器即可

    代码触发部分:

    //gpio 
    #include "driver/gpio.h"
    gpio_set_direction(41, GPIO_MODE_OUTPUT);
    gpio_set_level(GPIO_NUM_41, 1);//1为高电平,0为低电平

    2.GUI设计部分

    在这里插入图片描述

    ESP-BOX的GUI设计是利用LVGL库进行设计的,因此我们可以直接调用已经集成的多种组件对我们的界面进行设计,LVGL的官方有详细的使用说明,此处推荐使用百问网的中文帮助文档 百问网LVGL中文教程手册文档

    1. 如何创建自己的图标 利用LVGL的官方转换器,将自己的图片上传后即可转换为C array的 .c 文件,具体转换方法可以在以下教程中找到 LVGL图像显示
    2. 按钮跳转&界面创建 当点击主页面的“Heart Help”按钮后,将会出发页面跳转函数,来到第二个界面,在这个界面上,我们创建了两个按钮“Information”以及“Picture”,并通过for循环进行显示: 在这里插入图片描述

    以下为两个按钮的显示代码:

    for (size_t i = 0; i < 2; i++) {
            //every square 
            g_func_btn[i] = lv_btn_create(page);
            lv_obj_set_size(g_func_btn[i], 110, 110);
            lv_obj_add_style(g_func_btn[i], &ui_button_styles()->style_focus, LV_STATE_FOCUS_KEY);
            lv_obj_add_style(g_func_btn[i], &ui_button_styles()->style_focus, LV_STATE_FOCUSED);
            lv_obj_set_style_bg_color(g_func_btn[i], lv_color_white(), LV_STATE_DEFAULT);
            lv_obj_set_style_bg_color(g_func_btn[i], lv_color_white(), LV_STATE_CHECKED);
            lv_obj_set_style_shadow_color(g_func_btn[i], lv_color_make(0, 0, 0), LV_PART_MAIN);
            lv_obj_set_style_shadow_width(g_func_btn[i], 10, LV_PART_MAIN);
            lv_obj_set_style_shadow_opa(g_func_btn[i], LV_OPA_40, LV_PART_MAIN);
            lv_obj_set_style_border_width(g_func_btn[i], 1, LV_PART_MAIN);
            lv_obj_set_style_border_color(g_func_btn[i], lv_palette_main(LV_PALETTE_GREY), LV_PART_MAIN);
            lv_obj_set_style_radius(g_func_btn[i], 10, LV_STATE_DEFAULT);
            lv_obj_align(g_func_btn[i], LV_ALIGN_CENTER, i % 2 ? 48+17 : -48-17 , i < 2 ? -48 +35 : 48 - 3);        
            //button image
            lv_obj_t *img = lv_img_create(g_func_btn[i]);
            lv_img_set_src(img, img_src_list[i].img_off);
            lv_obj_align(img, LV_ALIGN_CENTER, 0, -10);
            lv_obj_set_user_data(img, (void *) &img_src_list[i]);
            //button text
            lv_obj_t *label = lv_label_create(g_func_btn[i]);
            lv_label_set_text_static(label, img_src_list[i].name);
            lv_obj_set_style_text_color(label, lv_color_make(40, 40, 40), LV_STATE_DEFAULT);
            lv_obj_set_style_text_font(label, &font_en_16, LV_STATE_DEFAULT);
            lv_obj_align(label, LV_ALIGN_CENTER, 0, 30);
            lv_obj_set_user_data(g_func_btn[i], (void *) img);
            if (UI_DEV_INFO == i) {
                ui_dev_ctrl_set_state(i, 1);
            } 
            else if (UI_DEV_PIC == i) {
                ui_dev_ctrl_set_state(i, 1);
            } 
            lv_obj_add_event_cb(g_func_btn[i], ui_dev_ctrl_page_func_click_cb, LV_EVENT_CLICKED, (void *)i);
            if (ui_get_btn_op_group()) {
                lv_group_add_obj(ui_get_btn_op_group(), g_func_btn[i]);
            }
        }
        if (ui_get_btn_op_group()) {
            lv_group_add_obj(ui_get_btn_op_group(), btn_return);
        }
        if (ui_get_button_indev()) {
            lv_obj_update_layout(btn_return);
            lv_area_t a;
            lv_obj_get_click_area(btn_return, &a);
            static lv_point_t points_array[1];
            points_array[0].x = (a.x1 + a.x2) / 2;
            points_array[0].y = (a.y1 + a.y2) / 2;
            lv_indev_set_button_points(ui_get_button_indev(), points_array);
        }
    }
    

    点击Information按钮后会触发页面跳转,此处利用函数的形式进行集成,包含了灰色背景的创建,返回键以及文本框的创建,其中,文本框在没有警报触发时,会通过函数lv_textarea_set_text()显示“No information here”,在报警触发时,通过lv_textarea_add_char()函数显示患者的姓名、病历、地址等信息

    void information_ctrl_start(void)//heart help -> information
    {
        //background
        ESP_LOGI(TAG, "showing information");
        lv_obj_t *page = lv_obj_create(lv_scr_act());
        lv_obj_set_size(page, lv_obj_get_width(lv_obj_get_parent(page)), lv_obj_get_height(lv_obj_get_parent(page)) - lv_obj_get_height(ui_main_get_status_bar()));
        lv_obj_set_style_border_width(page, 0, LV_PART_MAIN);
        lv_obj_set_style_bg_color(page, lv_obj_get_style_bg_color(lv_scr_act(), LV_STATE_DEFAULT), LV_PART_MAIN);
        lv_obj_clear_flag(page, LV_OBJ_FLAG_SCROLLABLE);
        lv_obj_align_to(page, ui_main_get_status_bar(), LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
        //return button
        lv_obj_t *btn_return = lv_btn_create(page);
        lv_obj_set_size(btn_return, 24, 24);
        lv_obj_add_style(btn_return, &ui_button_styles()->style, 0);
        lv_obj_add_style(btn_return, &ui_button_styles()->style_pr, LV_STATE_PRESSED);
        lv_obj_add_style(btn_return, &ui_button_styles()->style_focus, LV_STATE_FOCUS_KEY);
        lv_obj_add_style(btn_return, &ui_button_styles()->style_focus, LV_STATE_FOCUSED);
        lv_obj_align(btn_return, LV_ALIGN_TOP_LEFT, 0, -8);
        lv_obj_t *lab_btn_text = lv_label_create(btn_return);
        lv_label_set_text_static(lab_btn_text, LV_SYMBOL_LEFT);
        lv_obj_set_style_text_color(lab_btn_text, lv_color_make(158, 158, 158), LV_STATE_DEFAULT);
        lv_obj_center(lab_btn_text);
        lv_obj_add_event_cb(btn_return, ui_text_page_return_click_cb, LV_EVENT_CLICKED, page);//return event
        //text bar
        lv_obj_t * ta1 = lv_textarea_create(page);
        lv_obj_set_size(ta1, 296, 160);
        lv_obj_align(ta1, LV_ALIGN_TOP_MID, 0, 20);
        if(recv_warning_flag==0)//if no information received:show"No information here"
        {
            lv_textarea_set_text(ta1, "No information here.");    /*Set an initial text*/
        }
        else if(recv_warning_flag==1)//if informaiton received :show the information
        {
            printf("%s",infor);
            for(int i=0;infor[i]!='\0';i++){
                 lv_textarea_add_char(ta1, infor[i]);
            }
            //lv_textarea_add_text(ta1, buf);
            ESP_LOGI(TAG, "showing  end");
        }
    }
    1. 图片显示 “Heart Help”按钮点击后,第二个按钮即为“Picture”,该按钮点击后,会显示从服务器传输过来的患者现场的图片,此图片来自于ESP-EYE,创建页面的具体方法与Information的按钮无异。首先,ESP-BOX会从服务器中获得图片并保存在文件夹中,此后利用LVGL库进行调用,此处需要强调的是ESP-BOX所利用的系统文件,我们都知道不管是Windows系统还是Linux系统都具有文件系统这个概念,同样的,单片机中也有类似的文件系统,ESP-BOX则利用了stdio系统格式,并将系统名称设置为“S”,这样我们就可以利用系统的概念对单片机进行操作了 直接调用存储在spiffs文件夹中的图片:
    	//image display
        lv_obj_t *img = lv_img_create(page);
        const char *file_name_with_path = "S:/spiffs/picture.jpg";
        if (NULL != file_name_with_path)
         {
    		lv_img_set_src(img, file_name_with_path);
    		lv_obj_align(img, LV_ALIGN_TOP_MID, 0,20);
    		ESP_LOGI(TAG, "Display image file : %s", file_name_with_path);
         }
    

    小组在图片显示上花了大量的时间,主要问题在于,如果将图片先行放到文件夹中,然后进行烧录,则可以完美展示png的图片,但是通过socket传输并保存在文件夹中图片,则只能显示很小的图片,稍微大一点的png则无法显示了,同时jpg的显示虽然可以显示较大图片,但会出现严重失真,根据官方文档所说,目前只支持16位深度的图片显示,然而要获得16位的jpg图片比较困难,因此小组放弃了jpg的显示,希望LVGL后续能够完善jpg图片的解码

    3.AED与服务器连接部分

    1. 图传触发 不同于RPPG,AED的图传为了不与其冲突,我们为其开辟了另一个线程进行服务。 通过将EYE设置为局域网的服务端,其在5555端口监听来自BOX的图传触发信号即可完成图传的触发。触发报文不用设置为RPPG中的HTTP GET报文,而是通过在线程内部的serverSocket进行局域网的信号收取。当接收到触发信号 “regImg”时,进入到下一步的图传服务
    2. 图传方式 由于在stream_handler当中已经确定了其发送的服务函数。通过对其的分析,我们得到其获取相机帧的方式是使用xQueueRecieve的函数,其通过一个在注册相机时调取的QueueHandle_t类型的xQueueFrame作为参数,将其数据获取到frame当中。由于相机注册时可能会存在图片格式不为JPG而是其他格式,所以才会需要调用fream2jpg的函数进行转换。 基于此思路的分析,我们仿写了该段代码,直接在线程当中调取作为全局变量的xQueueFrame进行相机帧的获取,随后将获取到的尺寸数据转为字符串进行发送。收到确认接受了的报文之后,进行相机帧的发送。 由于在服务器端接收时可能是由于其他的冗余信息,会使得服务端接收的时候存在8个字节的空白表头使得接收错误无法存储为JPG。考虑到该大小不一定一致而JPG文件的文件头应当为一致的JPG描述,不会有可能出现常规字符。所以在一开始加上字符‘z’用于识别其字节流的开始。过程中顺带将其转为char类型进行发送。当收到BOX的传输完成信号之后完成一整个图传

    图传代码如下:

    //AED socket 
    void * aedSocketMonitorFunc(void* args){
        while(1){
            int emergencyReponseSocket=socket(AF_INET,SOCK_STREAM,0);/*建立socket*/
            if(emergencyReponseSocket==-1){
                printf("socket创建失败");
                sleep(60);
                continue;
            }
            struct sockaddr_in dest_addr;
            dest_addr.sin_family=AF_INET;
            dest_addr.sin_port=htons(AED_PORT);
            if((dest_addr.sin_addr.s_addr=inet_addr(SERVER_IP))==INADDR_NONE) {
                printf("地址无效\n");
                sleep(60);
                continue;
            }
            bzero(&(dest_addr.sin_zero),8);
            if(connect(emergencyReponseSocket,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr))==-1){//连接方法,传入句柄,目标地址和大小
                printf( "服务器连接失败\n");
                recv_warning_flag=0;
                sleep(60);
                continue;
            } else{
                printf("正在注册为AED\n");
                if(send(emergencyReponseSocket,aedID,16,0)==-1) {
                    printf("sending failed\n");
                    sleep(60);
                    continue;
                }
                recv(emergencyReponseSocket,buf,dataMaxLength,0);
                printf("注册成功,等待唤醒\n");
                for(int i=0; i<dataMaxLength;i++)
                    buf[i]='\0';
                recv(emergencyReponseSocket,buf,dataMaxLength,0);
                if(strcmp(buf,"abort")==0){
                	printf("服务器关闭,正在重置连接");
                	close(emergencyReponseSocket);
                	continue;
                }
                for(int i=0; i<dataMaxLength;i++)
                    infor[i]=buf[i];
                printf("收到报警信息: %s\n",infor);
                send(emergencyReponseSocket,"1",1,0);
                //==========收图===========//
                printf("正在等待现场照片\n");
                char sizeBuff[8]={0};
                char flag[1]={0};
                for(int i=0;i<8;i++) sizeBuff[i]='\0';
                if(recv(emergencyReponseSocket, sizeBuff, 8, 0)<=0){
                	printf("接收失败或者对端关闭连接!\n");
                    return 0;
                }
                int size=0;
                for(int i=0;sizeBuff[i]!='\0';i++){
                    size*=10;
                    size+=sizeBuff[i]-'0';
                }
                printf("接收到尺寸: %d\n",size);
                send(emergencyReponseSocket, flag, 1, 0);
                printf("接收图片字节流 \n");
                //文件接收的
                imageByte=(uint8_t*)malloc(200);
                FILE * image;
                image = fopen("/spiffs/picture.jpg","w");
                fclose(image);
                int left=size;
                image = fopen("/spiffs/picture.jpg","wb+");
                while(left>0) 	{
                	printf("剩余%d\n",left);
                	int recvLen=200<left?200:left;
                	left-=recvLen;
                    if(0 >= recv(emergencyReponseSocket, imageByte, recvLen, 0)){
                        printf("接收失败或者对端关闭连接!\n");
                        return 0;
                    }
                    fwrite(imageByte,sizeof(uint8_t),recvLen,image);
                    fflush(image);
                }
                fclose(image);
                send(emergencyReponseSocket, flag, 1, 0);
                recv(emergencyReponseSocket, flag, 1, 0);
                printf("图片存储完成\n");
                gpio_set_direction(41, GPIO_MODE_OUTPUT);
                gpio_set_level(GPIO_NUM_41, 1);//1为高电平,0为低电平
                recv_warning_flag=1; //to stimulate the text shown in the text box(information)
            }
            close(emergencyReponseSocket);
        }
    }
    

    三、ESP-BOX(用户模块)

    用户BOX主要实现与ESP-EYE进行通信,当ESP-EYE发现用户心率异常或者久坐时,进行语音提醒,同时用户能够通过BOX听舒缓的音乐,查找缓解心率异常的动作指导以及有关于心脏疾病的小贴士以便缓解心率异常的状况,紧急情况下用户还可以语音呼救,此时BOX会将用户的所有信息(姓名、病历、所处地址、用户现场状况)上传至120调度中心以及附近的AED-BOX(体外除颤仪)

    1.GUI设计部分

    为了帮助用户快速上手本产品,并帮助用户进行心脏保健,我们设置了十分简洁的操作界面。点击“”Relaxing"按钮进入之后,界面分为三个按钮"MUSIC"、"EXERCISE"和"TIP"。通过MUSIC按钮,可以跳转至音乐播放界面。将按钮的触发方式改为clicked,并将事件函数设置为创建音乐播放界面,并标记当前页面,便于返回。部分代码如下:

    	lv_obj_add_event_cb(btn_music,  device_media_player_enter_cb, LV_EVENT_CLICKED, page); 
    //为按钮添加“进入音乐播放界面”的事件函数
    static void device_media_player_enter_cb(lv_event_t *e)  
    }
        ui_media_player(device_media_player_end_cb);     
    } //进入音乐播放界面
        g_player_end_cb = fn;  
        lv_obj_t *page = lv_obj_create(lv_scr_act());   
        //创建音乐播放界面中的播放、暂停、快进、音量控制等按钮
         lv_obj_t *btn_return = lv_btn_create(page);  
         lv_obj_t *lab_btn_text = lv_label_create(btn_return);  
         lv_label_set_text_static(lab_btn_text, LV_SYMBOL_LEFT);  
         lv_obj_t *img = lv_img_create(page);  
         lv_img_set_src(img, &img_music);  
         g_lab_file = lv_label_create(page);  
         lv_label_set_text_static(g_lab_file, app_player_get_name_from_index(app_player_get_index())); 

    在此页面中,用户可以通过聆听曲库内的歌曲,来达到放松身心,减缓心脏压力,保护心脏的效果。借助屏幕上的各个按钮,可以实现对歌曲的音量、进度的调节,从而使用户更方便找到自己爱听的音乐。通过EXERCISE按钮,我们会向用户推荐和介绍一些有助于减缓心脏压力、缓解心脏异常的运动。部分代码如下:

    	lv_obj_add_event_cb(btn_exercise, exercise_ctrl_start, LV_EVENT_CLICKED, (void *) img);  
    	
    	lv_obj_t *page = lv_obj_create(lv_scr_act());  
    	lv_obj_t *btn_return = lv_btn_create(page);  
    	lv_obj_add_event_cb(btn_return, ui_dev_ctrl_page_return_click_cb, LV_EVENT_CLICKED, page)  //创建返回按钮以及设置属性
    	lv_obj_t *lab_btn_text = lv_label_create(btn_return);  
    	lv_obj_t *img = lv_img_create(page);  //获取图片位置和名称,并输出图片
    	const char *file_name = "picture.png";  
    	char *file_name_with_path = (char *) heap_caps_malloc(256, MALLOC_CAP_INTERN	AL | MALLOC_CAP_8BIT);  
    	lv_img_set_src(img, file_name_with_path);  
    	free(file_name_with_path);  //释放图片路径

    最终呈现效果如图: 在这里插入图片描述

    2.语音报警部分

    我们创建了两个命令,分别是"Call for help"以及"End help",当用户呼叫第一个指令时,则会触发报警信息传输,通过socket连接到服务器,此时通过人脸识别得到呼救者姓名,并从数据库中找到匹配的病历以及住址,同时将ESP-EYE获得的图像转存至服务器,最后将所有数据一并传给120调度中心,同时服务器会对周围最近的AED进行呼叫,AED产生啸叫吸引周围人群

    {SR_CMD_HELP,SR_LANG_EN, 0, "call for help","KeL FeR hfLP;",{NULL}},
    {SR_CMD_ENDHELP,SR_LANG_EN, 0, "end help","fND hfLP;",{NULL}},

    利用ESP-BOX自带的工具components中可以自行创建语音识别命令,具体方法如下:

    • 打开components中的esp-sr文件夹,并打开tool,可以看到有一个官方转换教程以及一个py文件,根据教程指示
    pip install g2p_en
    python multinet_g2p.py -t "hello world,hi ESP;turn on the light;turn off the light"
    • 之后分别在factory_demo文件夹main的app中,找到app_sr.h, app_sr.c以及app_sr_handler.c将需要添加的指令在指定地方进行添加即可

    当用户执行指令”CALL FOR HELP"后,则会触发socket传输,将报警信息传给服务器,代码位于app_sr_handler.c中:

    case SR_CMD_HELP:
            ////sending information of the client////
            sr_echo_play(AUDIO_END);
        	helpSocket=socket(AF_INET,SOCK_STREAM,0);/*建立socket*/
        	if(helpSocket==-1){
        	    ESP_LOGE(TAG,"socket创建失败");
        	    break;
        	}
        	struct sockaddr_in dest_addr;
        	char buf[dataMaxLength];
        	for(int i=0;i<dataMaxLength;i++){
        		buf[i]='\0';
        	}
        	dest_addr.sin_family=AF_INET;
        	dest_addr.sin_port=htons(HELP_PORT);
        	ESP_LOGE(TAG,"服务器地址:%s",SERVER_IP);
        	if((dest_addr.sin_addr.s_addr=inet_addr(SERVER_IP))==INADDR_NONE) {
        		ESP_LOGE(TAG,"地址解析失败");
        		break;
        	}
        	bzero(&(dest_addr.sin_zero),8);
        	if(connect(helpSocket,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr))==-1){//连接方法,传入句柄,目标地址和大小
        		ESP_LOGE(TAG,"服务器连接失败");
        		break;
        	} else{
        		ESP_LOGI(TAG,"连接成功,发送求助设备编号 %s\n",deviceID);
        	    if(send(helpSocket,deviceID,17,0)==-1) {
        	    	ESP_LOGE(TAG,"sending failed\n");
        	        break;
        	    }
        	    ESP_LOGI(TAG, "发送成功,等待应答\n");
        	    recv(helpSocket,buf,dataMaxLength,0);
        	    ESP_LOGI(TAG,"服务器应答 :  %s",buf);
        	}
        	close(helpSocket);
            ////sending command to eye ////
            imgSocket=socket(AF_INET,SOCK_STREAM,0);
        	if(imgSocket==-1){
        	    ESP_LOGE(TAG,"socket创建失败");
        	    break;
        	}
        	struct sockaddr_in eye_addr;
        	char buffer[dataMaxLength];
        	for(int i=0;i<dataMaxLength;i++){
        		buffer[i]='\0';
        	}
        	eye_addr.sin_family=AF_INET;
        	eye_addr.sin_port=htons(img_PORT);
        	if((eye_addr.sin_addr.s_addr=inet_addr(EYE_IP))==INADDR_NONE) {
        		ESP_LOGE(TAG,"地址解析失败");
        		break;
        	}
        	bzero(&(eye_addr.sin_zero),8);
        	if(connect(imgSocket,(struct sockaddr*)&eye_addr,sizeof(struct sockaddr))==-1){
        		ESP_LOGE(TAG,"EYE连接失败");
        		break;
        	} else{
        	    if(send(imgSocket,'e',sizeof('e'),0)==-1) {
        	    	ESP_LOGE(TAG,"sending failed\n");
        	        break;
        	    }
        	    if(recv(imgSocket,1,sizeof(1),0)){
                ESP_LOGI(TAG, "发送成功\n");
                }
        	}
        	close(imgSocket);
        	break;
        default:
            ESP_LOGE(TAG, "Unknow cmd");
            break;
        }
    

    3.心率异常及久坐报警模块

    若服务器检测到心率过高或过低,并且持续一段时间,则会触发ESP-BOX发出提示语音“您的心率异常,请及时关注”或“您很久都没有离开椅子了”来提醒用户及时关注自身状态,通过调用app_player.c中的play_sound()函数,进行mp3音频播放,声音预先存储在文件夹spiffs中:

    语音播放部分代码如下:

    //play_sound
    void * play_sound(const char *path)
    {
        ESP_LOGI(TAG, "start to decode %s", path);
         /* Audio control event queue */
        audio_event_queue = xQueueCreate(4, sizeof(player_event_t));
        if (NULL == audio_event_queue) {
            vTaskDelete(NULL);
        }
        FILE *fp = NULL;
        int sample_rate = 0;
        uint32_t bits_cfg = 0;
        uint32_t nChans = 0;
        esp_err_t ret = ESP_OK;
        uint8_t *output = NULL;
        uint8_t *read_buf = NULL;
        MP3FrameInfo frame_info;
        HMP3Decoder mp3_decoder = MP3InitDecoder();
        player_event_t audio_event = AUDIO_EVENT_NONE;
    
        ESP_RETURN_ON_FALSE(NULL != mp3_decoder, ESP_ERR_NO_MEM, TAG, "Failed create MP3 decoder");
    
        read_buf = malloc(MAINBUF_SIZE);
        ESP_GOTO_ON_FALSE(NULL != read_buf, ESP_ERR_NO_MEM, clean_up, TAG, "Failed allocate read buffer");
    
        output = malloc(1152 * sizeof(int16_t) * 2);
        ESP_GOTO_ON_FALSE(NULL != output, ESP_ERR_NO_MEM, clean_up, TAG, "Failed allocate output buffer");
    
        /* Read audio file from given path */
        fp = fopen(path, "rb");
        ESP_GOTO_ON_FALSE(NULL != fp, ESP_ERR_NOT_FOUND, clean_up, TAG, "File \"%s\" does not exist", path);
    
        typedef struct {
            char header[3];     /*!< Always "ID3" */
            char ver;           /*!< Version, equals to3 if ID3V2.3 */
            char revision;      /*!< Revision, should be 0 */
            char flag;          /*!< Flag byte, use Bit[7..5] only */
            char size[4];       /*!< TAG size */
        } __attribute__((packed)) mp3_id3_header_v2_t;
        /* Get ID3 head */
        mp3_id3_header_v2_t tag;
        if (sizeof(mp3_id3_header_v2_t) == fread(&tag, 1, sizeof(mp3_id3_header_v2_t), fp)) {
            if (memcmp("ID3", (const void *) &tag, sizeof(tag.header)) == 0) {
                int tag_len =
                    ((tag.size[0] & 0x7F) << 21) +
                    ((tag.size[1] & 0x7F) << 14) +
                    ((tag.size[2] & 0x7F) << 7) +
                    ((tag.size[3] & 0x7F) << 0);
                fseek(fp, tag_len - sizeof(mp3_id3_header_v2_t), SEEK_SET);
            } else {
                /* Not ID3 header */
                fseek(fp, 0, SEEK_SET);
            }
        }
    
        /* Start MP3 decoding */
        int bytes_left = 0;
        unsigned char *read_ptr = read_buf;
        do {
            /* Process audio event sent from other task */
            if (pdPASS == xQueueReceive(audio_event_queue, &audio_event, 0)) {
                if (AUDIO_EVENT_PAUSE == audio_event) {
                    g_player_state = PLAYER_STATE_PAUSE;
                    i2s_zero_dma_buffer(I2S_NUM_0);
                    xQueuePeek(audio_event_queue, &audio_event, portMAX_DELAY);
                    continue;
                }
    
                if (AUDIO_EVENT_CHANGE == audio_event ||
                        AUDIO_EVENT_NEXT == audio_event ||
                        AUDIO_EVENT_PREV == audio_event) {
                    i2s_zero_dma_buffer(I2S_NUM_0);
                    ret = ESP_FAIL;
                    goto clean_up;
                }
            }
            g_player_state = PLAYER_STATE_PLAYING;
    
            /* Read `mainDataBegin` size to RAM */
            if (bytes_left < MAINBUF_SIZE) {
                memmove(read_buf, read_ptr, bytes_left);
                size_t bytes_read = fread(read_buf + bytes_left, 1, MAINBUF_SIZE - bytes_left, fp);
                ESP_GOTO_ON_FALSE(bytes_read > 0, ESP_OK, clean_up, TAG, "No data read from strorage device");
                bytes_left = bytes_left + bytes_read;
                read_ptr = read_buf;
            }
    
            /* Find MP3 sync word from read buffer */
            int offset = MP3FindSyncWord(read_buf, MAINBUF_SIZE);
    
            if (offset >= 0) {
                read_ptr += offset;         /*!< Data start point */
                bytes_left -= offset;       /*!< In buffer */
                int mp3_dec_err = MP3Decode(mp3_decoder, &read_ptr, &bytes_left, (int16_t *) output, 0);
                ESP_GOTO_ON_FALSE(ERR_MP3_NONE == mp3_dec_err, ESP_FAIL, clean_up, TAG, "Can't decode MP3 frame");
    
                /* Get MP3 frame info and configure I2S clock */
                MP3GetLastFrameInfo(mp3_decoder, &frame_info);
    
                /* Configure I2S clock if sample rate changed. Always reconfigure at first frame */
                if (sample_rate != frame_info.samprate ||
                        nChans != frame_info.nChans ||
                        bits_cfg != frame_info.bitsPerSample) {
                    sample_rate = frame_info.samprate;
                    bits_cfg = frame_info.bitsPerSample;
                    nChans = frame_info.nChans;
                    ESP_LOGI(TAG, "audio info: sr=%d, bit=%d, ch=%d", sample_rate, bits_cfg, nChans);
                    i2s_channel_t channel = (nChans == 1) ? I2S_CHANNEL_MONO : I2S_CHANNEL_STEREO;
                    //i2s_channel_t channel = I2S_CHANNEL_MONO ;
                    i2s_set_clk(I2S_NUM_0, sample_rate, bits_cfg, channel);
                }
    
                /* Write decoded data to audio decoder */
                size_t i2s_bytes_written = 0;
                size_t output_size = frame_info.outputSamps * nChans;
                i2s_write(I2S_NUM_0, output, output_size, &i2s_bytes_written, portMAX_DELAY);
                ESP_LOGI(TAG,"runing to here");
            } else {
                /* Sync word not found in frame. Try to read next frame */
                ESP_LOGE(TAG, "MP3 sync word not found");
                bytes_left = 0;
                continue;
            }
        } while (true);
    

    但是音频播放仍然存在问题,很多时候点击播放键都会卡在:

    i2s_channel_t channel = (nChans == 1) ? I2S_CHANNEL_MONO : I2S_CHANNEL_STEREO

    推断此处音频播放的代码仍然有一些问题,留在之后进行修复


    四、ESP-BOX应用场景演示

    在这里插入图片描述

    五、未来展望

    1. 增加雷达测量模块 团队后续将继续非接触式心率测量设备与整套系统的适配和研发,将采用77GHZ的毫米波雷达检测心率、呼吸等。能进一步实现全天候对人体健康状况的检测。此类雷达可以安装在卧室检测老人或者婴儿的呼吸和心跳,尤其是患有心脏病的老人,如果发现心脏骤停等还可以发出警报,进行及时抢救。同时毫米波雷达测量模块还可以和机器视觉模块进行结合,得到更加精准的心率值,完善心率测量不准确的问题,本系统搭载雷达模块后还可以对睡眠质量进行监测。并且针对于长时间驾驶汽车的司机如出租车司机、公交车司机以及货车司机等对象,该模块还可以进行长期心率检测和疲劳驾驶提醒等功能。

    在这里插入图片描述

    1. 封装rPPG模型到ESP芯片 由于时间问题,小组没有将ESP-EYE的边缘AI能力完全应用,所实现的心率检测算法由电脑端完成,未来我们将利用ESP-DL库把rPPG算法及其模型移植到ESP上,实现只用ESP-EYE即可进行用户身份识别以及心率检测,并将数据保存至本地,在固定时间点进行数据库的更新即可。实现的效果如下图所示: 在这里插入图片描述

    六、应用场景

    • 医院 在医院中,我们主要需要解决的问题是接触式心率测量仪器会对病人造成影响以及面对传染病人时,会给医护人员带来一定的危险等问题。在医院中,我们可以使用ESP-EYE来对患者的心率进行测量,并将设备连接至医院的专业心率显示仪器上,从而可以达到不接触患者,即可得到心率的目的
    • 部分公司、企业以及政府部门中 在很多企业中,都存在长期久坐的上班族。如长时间处理文件的法院工作人员、程序员、观看录像寻找线索的警察等等。长期久坐会对他们的健康造成损害,而部分由于部分工作场所的管制,导致他们不可能经常携带智能手表进行心率测量。此时我们可以使用ESP-EYE和ESP-BOX进行久坐提醒。有ESP32S3-EYE计算出上班族保持坐姿的时间,一旦超过一定的时间,ESP-BOX会对他们发出久坐提醒,提示用户适当站起来走动一下,或者听音乐放松,运动保持身体舒缓,防止颈椎病等健康问题的发生。 另外,ESP-EYE会时刻关注用户的面部情况,并对其心率进行计算。当心率出现异常时,会及时通过ESP32S3-BOX对用户进行提醒,防止心脏问题导致悲剧发生。当用户因为心脏问题感到难受时,可以语音呼叫ESP-BOX进行报警。ESP-BOX会将此时BOX会将用户的所有信息(姓名、病历、所处地址、用户现场状况)上传至120调度中心以及附近的AED-BOX(体外除颤仪),这时AED-BOX进行啸叫,提醒周围人群附近有人呼救,周围的人可以通过AED-BOX查看呼救者的所有信息并及时对其进行救治。
    • 有老人或小孩居住的居民家中 家中有老人居住时,必不可免的就会出现老人独自在家,其他家庭成员全部外出的情况。此时如果老人在家突发疾病,很容易由于无法及时拨打求救电话或者无法及时获取准确位置而导致悲剧发生。当老人因为心脏问题或者其他原因而感到不适时,可以及时通过ESP-BOX进行语音报警,ESP-BOX会将此时BOX会将用户的所有信息(姓名、病历、所处地址、用户现场状况)上传至120调度中心,方便医护人员及时救助。 另外,当装设在家中的ESP-EYE发现老人的心率出现异常时,也可以通过ESP-BOX及时对老人及其家庭成员进行提醒。可以让老人及时进行检查。降低突发疾病的几率。 另一方面,如果以后出现可以安装在家里的家用雷达装置,也可以与此产品配合,进行睡眠监测,帮助用户快速了解到自己的睡眠情况,并根据相应结果判断是否需要及时就医。
    • 商场、广场等公共场所 在商场等场景中,会在固定的地方装配ESP-BOX的用户端。当有人因为心脏问题或是其他疾病感到不适时,若来不及拨打求助电话,同时身边又没有其他人能够及时供自己求助时,可以附近的ESP-BOX进行语音报警。ESP-BOX会将此时BOX会将用户的所有信息(姓名、病历、所处地址、用户现场状况)上传至120调度中心以及附近的AED-BOX(体外除颤仪),这时AED-BOX进行啸叫,提醒周围人群附近有人呼救,周围的人可以通过AED-BOX查看呼救者的所有信息并及时对其进行救治。

    遇到问题

    1. 由于ESP-BOX需要不断进行socket连接,因此我们采用了双线程的形式进行,但是在实际操作中遇到了重启的问题:

    assert failed: tcpip_send_msg_wait_sem IDF/components/lwip/lwip/src/api/tcpip.c:455 (Invalid mbox)

    Backtrace:0x40379e36:0x3fcc5e600x4038d1b5:0x3fcc5e80 0x403932dd:0x3fcc5ea0 0x420b1b43:0x3fcc5fc0 0x420bc995:0x3fcc5ff0 0x420bca1e:0x3fcc6010 0x420b11a4:0x3fcc6060 0x42008101:0x3fcc6080 0x420027d8:0x3fcc60d0 0x4038d671:0x3fcc60f0

    问题在于Wifi连接初始化未完成前则开始进行socket请求,导致的重启,由于app_wifi.c中有一个s_connected标志用于确认wifi是否已连接,因此调用此标志以决定是否进入线程二,由于需要不断判断,因此小组最初采用了while循环,如下:

    while(true){
    	   if (s_connected==true){ //detect whether wifi is connected
    	         break; }}

    但是发现,程序会触发看门狗崩溃:

    E (36942) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time: E (36951) task_wdt: - IDLE0 (CPU 0) E (36951) task_wdt: Tasks currently running: E (36951) task_wdt: CPU 0: WiFiTask E (36951) task_wdt: CPU 1: IDLE1 E (36951) task_wdt: Aborting. abort() was called at PC 0x4016113c on core 0

    查阅资料后发现:

    ESP-IDF 为空闲任务创建了一个看门狗计时器,除了执行您工作的任何任务。IE,它创建了两个额外的任务 IDLE0 和 IDLE 1(每个内核一个),其唯一目的是什么都不做,即空闲。每当其各自的内核处于空闲状态时,这些空闲任务只是为看门狗提供服务。每当 ESP-IDF 中的任何任务以及中断例程以比 IDLE 任务更高的优先级运行而没有延迟或阻塞足够长/经常足够长的时间(例如在紧密的 while 循环中执行时),它都会触发看门狗,因为该核心现在完全忙碌,这意味着它不再经常空闲,这意味着空闲的“任务”会被饿死,所以他们无法重置看门狗。ESP-IDF 中的 app_main()比 IDLE 任务更高的优先级运行。这就是问题的根源。

    也就是说由于while循环写在main函数里面,其优先级高于IDLE,因此核心处于完全忙碌状态,无法重置看门狗,导致死循环,但是如果在每一个循环里面睡眠5s则可以让核心有时间重置看门狗,这样就可以解决掉此问题:

        while(!s_connected){sleep(5);};
    1. ESP-BOX连接wifi的方式是利用手机扫描二维码通过蓝牙与ESP-BOX进行连接(手机应连接BOX要连接的wifi作为中转),后在手机上输入wifi的ssid以及password辅助BOX进行连接。但是此种方法,在更换环境时便需要执行idf.py erase_flash 擦出掉nvs中存储的wifi站名及密码,然后再次烧录才能连接新的wifi,太过繁琐且烧录时间过长。 因此小组设置了一个Reset Wifi 的按钮,点击按钮即可执行nvs_flash_erase( )函数,然后按下BOX旁边的reset键即可重新利用手机辅助连接位wifi了,这样仅清除nvs中保存的内容,无需再次烧录,节约时间,代码如下:
    static void nvs_erase(void){
        nvs_flash_erase();
        printf("Wifi is reset, please scan the QR to reconnect the wifi again\n");
        }
        //Create a button to erase the nvs space where the wifi ssid and its password are located 
        lv_obj_t *wifi_reset=lv_btn_create(g_page);
        lv_obj_set_size(wifi_reset, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
        lv_obj_add_style(wifi_reset, &ui_button_styles()->style, 0);
        lv_obj_add_style(wifi_reset, &ui_button_styles()->style_pr, LV_STATE_PRESSED);
        lv_obj_add_style(wifi_reset, &ui_button_styles()->style_focus, LV_STATE_FOCUS_KEY);
        lv_obj_add_style(wifi_reset, &ui_button_styles()->style_focus, LV_STATE_FOCUSED);
        lv_obj_align_to(wifi_reset, g_img,LV_ALIGN_OUT_BOTTOM_MID, -30, 10);
        lv_obj_t *reset_label=lv_label_create(wifi_reset);
        lv_label_set_text_static(reset_label,"Reset WiFi");
        lv_obj_set_style_text_color(reset_label, lv_color_make(18, 18, 18), LV_STATE_DEFAULT);
        lv_obj_align(reset_label, LV_ALIGN_CENTER, 0, 0);
        lv_obj_add_event_cb(wifi_reset, nvs_erase, LV_EVENT_CLICKED, NULL);

    代码开源

    本项目所有代码均以上传至GitCode:

    • 获取AED-BOX代码:
    git clone https://gitcode.net/bale11guo/heart-rate-esp32s3.git
    • 获取用户-BOX代码:
    git clone https://gitcode.net/bale11guo/heart-rate-esp32s3-box_client.git

    再次感谢乐鑫公司在此次物联网竞赛中大力支持,所有芯片及设备均由乐鑫公司提供

    项目简介

    A system to detecting people's heart rate using rPPG algorithm and an application for it. Device: ESP-BOX、ESP-EYE

    发行版本

    当前项目没有发行版本

    贡献者 2

    CharlesGuo11 @bale11guo
    honorifica @honorifica

    开发语言

    • C 100.0 %
    • CMake 0.0 %
    • C++ 0.0 %