# 007ePaperPhotoFrame
**Repository Path**: dwl301/007e-paper-photo-frame
## Basic Information
- **Project Name**: 007ePaperPhotoFrame
- **Description**: 基于ESP32C3的墨水屏显示项目
- **Primary Language**: Unknown
- **License**: GPL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 2
- **Created**: 2024-04-09
- **Last Updated**: 2024-04-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# ePaperPhotoFrame
* 实现了图片接收,并显示在EPD上,也可缓存多个Photo到SD Card中,并可选择显示,或删除已有缓存
* 使用esp-idf 5.0.1以上版本
## 1. 硬件设计
* 使用ESP32C3模块+SD Card模块+自建EPD驱动+充放电模块+拨动开关
### 1.1 ESP32C3核心板
> [ESP32C3-CORE开发板 - LuatOS 文档](https://wiki.luatos.com/chips/esp32c3/board.html)

#### 1.2 引脚分配
| ESP32 | Func | TypeC | Func | ESP32 |
| :--- | :---- | ---: | ---: | ---: |
| GND | | | | VBUS |
| VBUS | | | | PWB |
| GPIO9 | SD_CS | | | GND |
| GPIO8 | SPI_MOSI | | | 3V3 |
| GPIO4 | SPI_CLK | | | CHIP_EN |
| GPIO5 | SPI_MISO | | | NC |
| 3V3 | | | | GPIO13 |
| GND | | | | GPIO21 |
| GPIO11| | | | GPIO20 |
| GPIO7 | EPD_CS | | | GND |
| GPIO6 | EPD_D/C | | | GPIO19 |
| GPIO10| EPD_RES | | | GPIO18 |
| GPIO3 | EPD_BUSY | | BTN_UP | GPIO12 |
| GPIO2 | | | BTN_M | GPIO1 |
| 3V3 | | | BTN_DWN | GPIO0 |
| GND | | | | GND |
### 1.3 SD Card模块
* 淘宝购买模块
* 使用引脚:3V3/GND/MISO/MOSI/SCLK/CS
### 1.4 EPD屏幕 - GDEQ0426T82
[4.26寸 快刷1秒纸墨水屏 分辨率800x480 支持局刷 GDEQ0426T82-大连佳显电子有限公司](https://www.good-display.cn/product/452.html)

* 对应驱动板: DESPI-C02
* [24Pin串口电子墨水屏专用转接板 DESPI-C02](https://www.good-display.cn/product/505.html)
* 使用引脚:BUSY/RES/D\_C/CS/SCK/SDI/GND/3V3
* 驱动型号:SSD1677 **(具体寄存器配置可以查它的手测)**

### 1.5充放电模块
* 特性
* 5V输入
* 接3.7V锂电池
* 自升压到5V输出
#### 1.5.1 自建板子(未成功)
* 基于IP5306
### 1.6 拨轮开关
* 特性
* 具有上/中/下,三个独立开关
## 2 功能模块
### 2.1 主函数
#### 2.1.1 多SPI设备
* ESP32C3有三个SPI控制器,SPI0/1部分资源共用,SPI2基本独立
* SPI0/1可配存储器模式(SPI Memory)
* SPI2可配通用模式(GP-SPI)

* 程序中,在app_main内首先初始化SPI2_HOST总线,并指定所使用的**SPI控制器**和**部分pin(MOSI/MISO/CLK)**
``` C
#define SPI_HOST_USING SPI2_HOST //HSPI_HOST
//初始化SPI总线
void __ESP32_SPI_Bus_Init(spi_host_device_t host_id)
{
esp_err_t ret;
spi_bus_config_t spiBusCfg = {
.mosi_io_num = SPI_PIN_MOSI,
.miso_io_num = SPI_PIN_MISO,
.sclk_io_num = SPI_PIN_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 160*800*2+8 //TODO: 这里太大了???
};
ret = spi_bus_initialize(host_id, &spiBusCfg, SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret);
}
```
* 再在对应模块内,为已初始化的SPI总线添加设备,此时要指定该设备使用的CS
* 添加完一个设备后,可以再添加其他设备,app_main中为SPI2_HOST挂载了EPD和SDCard两个设备
``` C
void __ESP32_GPIO_SPI_Init(spi_host_device_t host_id)
{
esp_err_t ret;
//------------ GPIO init ------------
......
//------------ SPI device init ------------
spi_device_interface_config_t spiDevInterCfg = {
.clock_speed_hz = 10*1000*1000,
.mode= 0, //TODO: 也许是其他模式
.spics_io_num = PIN_CS, //指定该设备使用的CS
.queue_size = 7,
.pre_cb = __SPI_Each_Trans_Callback,
};
//attach device to spi bus
ret = spi_bus_add_device(host_id, &spiDevInterCfg, &SPI_Device_Handle); //将返回的句柄保存到全局变量SPI_Device_Handle中
ESP_ERROR_CHECK(ret);
//------------ EPD init ------------
}
```
### 2.2 WiFi模块 - WebServer
> [Waveshare提供的WiFi例程](https://www.waveshare.net/wiki/E-Paper_ESP32_Driver_Board)
* 作为AccessPoint,可提供图片传输接口
#### 2.2.1 作为WebServer
* Server需要处理Get请求,也就是浏览器向Server请求数据
* 从req->uri拿到请求的文件名,返回对应文件
``` C
//绑定get处理
httpd_uri_t httpd_uri_get = {
.uri = "/*",
.method = HTTP_GET, //要被处理的请求类型为get
.handler = http_uri_get_handler, //自定义的处理函数
// .user_ctx = , //传递数据
};
httpd_register_uri_handler(httpd_hdl, &httpd_uri_get);
```
``` C
static esp_err_t http_uri_get_handler(httpd_req_t *req)
{
uint8_t file_type;
//判断req->uri内容
if(strcmp(req->uri, "/index.html") == 0)
file_type = 0;
else if(strcmp(req->uri, "/styles.css") == 0)
file_type = 1;
else if(strcmp(req->uri, "/scriptA.js") == 0)
file_type = 2;
else if(strcmp(req->uri, "/scriptB.js") == 0)
file_type = 3;
else if(strcmp(req->uri, "/scriptC.js") == 0)
file_type = 4;
else if(strcmp(req->uri, "/scriptD.js") == 0)
file_type = 5;
else
{
ESP_LOGI(TAG_WEB, "!!! err, request a unknow file");
return ESP_FAIL;
}
//返回所需文件
//用于EMBED_FILES, 先以SDCard+FS替代
__sendWebFile(req, file_type);
//__sendWebFileFromFS(req, file_type);
return ESP_OK;
}
```
* Server还需要处理Post请求,也就是浏览器向Server发送数据
* 这里规定了POST的URI中携带了要传输的数据,因此要对req->uri进行解析
* 注意,处理完后要对Server要回复"200 OK"
``` C
//绑定post处理
httpd_uri_t httpd_uri_post = {
.uri = "/*",
.method = HTTP_POST, //要被处理的请求类型为post
.handler = http_uri_post_handler, //自定义的处理函数
.user_ctx = NULL
};
httpd_register_uri_handler(httpd_hdl, &httpd_uri_post);
```
``` C
static esp_err_t http_uri_post_handler(httpd_req_t *req)
{
//------------ step1 receive all data
char *buf = req->uri;
int buf_len = strlen(buf);
char data_cmd[5];
//--------- 从buf中获取cmd
__getSomeChar(data_cmd, 5,
buf, buf_len,
buf_len-5, 5);
//--------- 判断&执行cmd
//------ data_cmd='EPDx_'
if(strstr(data_cmd, "EPD"))
{
//......
}
//------ data_cmd='LOAD_'
else if(strstr(data_cmd, "LOAD"))
{
//......
}
//------ data_cmd='NEXT_'
else if(strstr(data_cmd, "NEXT"))
{
//......
}
//------ data_cmd='SHOW_'
else if(strstr(data_cmd, "SHOW"))
{
//......
}
//------ 其他情况不处理
else
{
//......
}
//------------ step2 response Client
const char resp[] = "POST Response!";
httpd_resp_set_status(req, "200 OK");
httpd_resp_sendstr(req, resp);
return ESP_OK;
}
```
#### 2.2.2 如何将.html/.css/.js前端文件放入工程
* 参考[ESP32作为WebServer](https://blog.csdn.net/qq_27114397/article/details/89643232):
* 方法一,MTML转为.h,编译在工程内
* 方法二(前期采用的方法),HTML嵌入FLASH中
##### HTML嵌入FLASH
* 该方式将文件写入到了.rodata中
* 实现步骤:
1. 将前端页面文件单独保存,例如,保存到./main/web_files文件中
2. 在./main/CMakeList.txt中罗列要嵌入的文件
``` shell
### ./main/CMakeList.txt
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
# 用于EMBED_FILES, 先以SDCard+FS替代
EMBED_FILES "./HTTPServer/web_files/styles.css"
"./HTTPServer/web_files/index.html"
"./HTTPServer/web_files/scriptA.js"
"./HTTPServer/web_files/scriptB.js"
"./HTTPServer/web_files/scriptC.js"
"./HTTPServer/web_files/scriptD.js"
)
```
3. 在程序中申明这些EMBED_FILES,这里的\_binary_index_html_start,绿色部分是固定的,红色部分来自文件名
``` C
//indext.html
extern const unsigned char html_index_start[] asm("_binary_index_html_start");
extern const unsigned char html_index_end[] asm("_binary_index_html_end");
//styles.css
extern const unsigned char html_styles_start[] asm("_binary_styles_css_start");
extern const unsigned char html_styles_end[] asm("_binary_styles_css_end");
//scriptA.js
extern const unsigned char html_scriptA_start[] asm("_binary_scriptA_js_start");
extern const unsigned char html_scriptA_end[] asm("_binary_scriptA_js_end");
//每个文件都是start和end标记, 并可计算size
const size_t html_tmp_size = (html_index_end - html_index_start);
```
4. 向浏览器返回前端文件,发送时要指定文件类型,
.html对应text/html,
.css对应text/css
.js对应applicaiont/javascript
``` C
//向浏览器响应页面, 发送Web页面数据
if(strcmp(req->uri, "/index.html") == 0)
{
//这些EMBED_FILE已集中声明过
// extern const unsigned char html_tmp_start[] asm("_binary_index_html_start");
// extern const unsigned char html_tmp_end[] asm("_binary_index_html_end");
// const size_t html_tmp_size = (html_index_end - html_index_start);
httpd_resp_set_status(req, "200 OK");
httpd_resp_set_type(req, "text/html"); //返回文件类型
httpd_resp_send_chunk(req, (const char *)html_index_start, html_tmp_size); //文件开始位置 & 文件长度
httpd_resp_sendstr_chunk(req, NULL); //表示返回结束
ESP_LOGI(TAG_MAIN, ">>> has sent index page");
}
```
### 2.3 EPD模块 - 驱动EPD显示
> [GoodDispaly提供的驱动例程](https://www.good-display.cn/companyfile/894.html)
* 具有全局刷新/局部刷新功能
* 提供一些GUI绘制(点/线/圆/字符)
#### 2.3.1 基础功能
##### 存储图像数据
* 使用一维数组缓存像素信息
* 将8bit合成1char,将char存入数组中,因此数组长度=480*800/8(即在X方向按8bit压缩)
* b0000_0000到b1111_1111对应字符'a'到'p'
``` C
#define IMG_INDEX_MAX EPD_ARRAY //48000=480*800/8
//
uint8_t PhotoImg[IMG_INDEX_MAX]; //缓存照片
uint8_t GUIImg[IMG_INDEX_MAX]; //缓存界面
```

##### EPD配置流程
###### 全局刷新
* 初始化
* 写入PhotoImg缓存
* 进入休眠
``` C
void ePaper_FullDispaly()
{
//PhotoImg中已有图像数据, DispImg指向PhotoImg
DispImg = PhotoImg;
EPD_W21_Init_Modes(Init_Mode_X_3); //Full screen refresh initialization.
EPD_WhiteScreen_ALL(DispImg); //To Display one image using full screen refresh.
EPD_DeepSleep(); //Enter the sleep mode and please do not delete it, otherwise it will reduce the lifespan of the screen.
}
```
###### 局部刷新
* 初始化,设置背景图像(ePaper_GuiImg_Init)
* 刷新GUIImg缓存
* GUIImg缓存写入EPD(ePaper_GuiImg_Display)
``` C
void ePaper_GuiImg_Init(void)
{
//------------ 使用GUI, 并支持局部刷新
GUI_Init_Img(GUIImg, EPD_X_LEN, EPD_Y_LEN, Rotate0, ColorWhite); //需要交换W和H
GUI_Clear_All(ColorWhite);
//
EPD_W21_Init_GUI();
EPD_SetRAMValue_BaseMap(GUIImg); //设置背景
}
void ePaper_GuiImg_Display()
{
DispImg = GUIImg;
EPD_Dis_PartAll(DispImg);
EPD_DeepSleep();
}
```
##### SSD1677中的关键寄存器
* addr=0x11 - Data Entry mode setting(EPD_W21_Init_Modes中使用)
[2:0] = [A2:A1:A0] = [AM:ID1:ID0]

* addr=0x44/0x45 - Set RAM X/Y(EPD_W21_Init_Modes中使用)
需要4组数据,每组10bit组成
X方向最大=0x3BF=959 -> 960
Y方向最大=0x2A7=679 -> 680

* addr=0x24 - Write RAM(Black White)(EPD_WhiteScreen_ALL中子函数使用)
绘制像素

* addr=0x22 - Display Update Control2(EPD_WhiteScreen_ALL中子函数使用)
未找到详细解释,按照例程用法有以下设置,
0x91 -> DISPLAY Mode1, fast refresh init
0xC7 -> DISPLAY Mode1, fast refresh update
0xF7 -> DISPLAY Mode1, full screen refresh update
0xFF -> DISPLAY Mode2, partial refersh update
###### 可能的问题
* 等待过长,触发WDT,
例如:等待BUSY相应时,如果时间过长会触发ESP32的WDT,因此要加入vTaskDelay,让系统响应其他任务
``` C
void __Epaper_ReadBusy(void)
{
while(1)
{ //=1 BUSY
if(EPD_W21_Get_BUSY == 0)
break;
else
vTaskDelay(3); //防止一直等待触发WDT
}
}
```
#### 2.3.2 GUI支持
##### 绘制线 - 基于Bresenham直线算法(我看不懂,但是十分震撼)
[A Rasterizing Algorithm for Drawing Curves](./doc/Bresenham.pdf)

##### 绘制圆 - 四分圆算法
* 设置圆心、半径
``` C
int8_t GUI_Paint_DrawCircle(uint16_t x_center, uint16_t y_center, uint16_t radius,
CircleStyle_t circle_style,
Color_t color, DotPixel_t dot_pixel)
{
int16_t r = radius;
int16_t x = -r, y = 0, err = 2 - 2 * r; //bottom left to top right
int16_t i;
//空心圆
do{
//绘制四分点
__Paint_SetPixel(x_center - x, y_center + y, color); // I. Quadrant +x +y
__Paint_SetPixel(x_center - y, y_center - x, color); // II. Quadrant -x +y
__Paint_SetPixel(x_center + x, y_center - y, color); // III. Quadrant -x -y
__Paint_SetPixel(x_center + y, y_center + x, color); // IV. Quadrant +x -y
//迭代
r = err;
if(r <= y) //e_xy + e_y < 0
err += ++y*2 + 1;
if(r > x || err >y) //e_xy + e_x > 0 OR no 2nd y-step
err += ++x*2 + 1; //-> x-step now
}while(x < 0);
return 0;
}
```
##### 绘制字符
* 预先规定好字符的Width、Height,并准备对应图像的一维数组

###### 字符方向问题
* 分别沿着X和Y方向,各自存在4个不同的朝向

* 如下,将从x_axis开始,沿着x方向移动(x_axis+x_add),font到一行处理完后,y方向再移动一(y_axis+y_add)
``` C
//举例: 沿EPD的X方向,该方向在循环中先变化
x_add = &x_font; //EPD的x方向将被施加Width
y_add = &y_font; //EPD的y方向将被施加Height
x_dir = -1; //从左到右, 因此x逐渐减小, 用减法
y_dir = 1; //从上到下, 因此y逐渐增大, 用加法
//
//沿着EPD的Y方向,该方向在循环中先变化
// x_add = &y_font; //EPD的x方向将被施加Height
// y_add = &x_font; //EPD的x方向将被施加Width
// x_dir = 1; y_dir = 1;
//绘制char
// 起始点是(x_axis, y_axis)
// 字符的长、高为font->Width、font->Height
if(color_background == FONT_BACKGROUND_DEFAULT) //背景色为 白
{
for(y_font = 0; y_font < font->Height; y_font++) //绘制height行
{
for(x_font = 0; x_font < font->Width; x_font++) //绘制一行中的width列, 一行中有n个8bits
{
if(*char_data & (0x80 >> (x_font % 8))) //展开成8bit取其中1bit, 设置对应颜色
{
__Paint_SetPixel(x_axis + (*x_add) * x_dir, y_axis + (*y_add) * y_dir, color_frontground); //(x_axis, y_axis)是起始点
}
//
if(x_font % 8 == 7) //处理完8bits后, 移动到下一个table数据
char_data++;
} //绘制完一行
if(font->Width % 8 != 0) //如果一行数据量font->Width不够n个char, 就跳过当前table数据
char_data++;
}
}
```
### 2.4 SDCard模块
* 实现向SDCard进行读/写/遍历操作
#### 2.4.1 SDCard初始化
* 使用esp-idf提供的便捷函数 esp_vfs_fat_sdspi_mount
``` C
#define MOUNT_POINT "/sdcard"
const char MountPoint[] = MOUNT_POINT;
static sdmmc_card_t *SDCard; //a SD Card, it keeps SD/MMC card information
//需要配置三组信息, 分别是 挂载配置/SD控制器配置/SD设备配置
ret = esp_vfs_fat_sdspi_mount(MountPoint, //挂载点
&sd_host, //SD host(控制器)
&sd_dev_cfg, //SD device(设备)
&sd_mount_cfg, //挂载配置
&SDCard //返回SDCard对象
);
sdmmc_card_print_info(stdout, SDCard); //输出SDCard信息
```
#### 2.4.2 打开文件
* 打开文件是读/写的前提
* SDCard.c中维护了一个全局变量**File**,打开文件时对其进行设置,操作完关闭时重新赋NULL
``` C
static FILE *File = NULL; //打开的文件描述符
```
* SDCard_File_Open,获取文件信息 & 打开文件
* 入参
* file_path,传入要操作的文件路径,会自动在前拼接上MountPoint
* mode,传入操作文件的模式,支持FILE_MODE_READ/FILE_MODE_WRITE/FILE_MODE_READ_WRITE
* 出参
* ESP_OK=0,打开文件成功
* ESP_FAIL=-1,打开文件失败
``` C
esp_err_t SDCard_File_Open(char *file_path, const char *mode) //
{
struct stat sta;
char full_file_path[50]; //限制完整文件路径要小于50byte
//拼接文件路径
sprintf(full_file_path, MOUNT_POINT"%s", file_path);
// ESP_LOGI(TAG_SD, "mode=%s, full_path=%s", mode, full_file_path);
//模式判断 - 读, 打开文件前需要判断文件是否存在
if((strstr(mode, FILE_MODE_READ) != NULL) || (strstr(mode, FILE_MODE_READ_WRITE) != NULL)) //比较字符串
{
if((stat(full_file_path, &sta) != 0)) // stat()!=0 表示没有这个文件
{
ESP_LOGI(TAG_SD, "FileOpen - !!! read mode AND no file");
return ESP_FAIL;
}
}
//模式判断 - 写, 可以直接创建文件
else if(strstr(mode, FILE_MODE_WRITE) != NULL)
ESP_LOGI(TAG_SD, "FileOpen - write mode");
//模式判断 - 其他
else
{
ESP_LOGI(TAG_SD, "FileOpen - !!! other mode");
return ESP_FAIL;
}
//打开文件并判断是否成功
File = fopen(full_file_path, mode); // File是.c全局变量
if(File == NULL)
{
ESP_LOGI(TAG_SD, "FileOpen - open file failed");
return ESP_FAIL;
}
else
{
ESP_LOGI(TAG_SD, "FileOpen - open file ok");
return ESP_OK;
}
}
```
#### 2.4.3 文件操作
##### 读取文件
* 实现SDCard读取后,WebServer部分的EMBED_FILE最改用SDCard进行存储
* 文件读取用**fgets**实现,每次返回一行数据到buf中,外部调用通过返回值判断是否可以继续读取
``` C
esp_err_t SDCard_File_Read(char *buf, uint16_t size)
{
if(feof(File) == 0) //遇到结束返回非0; 未遇到结束返回0
{
// memset(file_buf, NULL, sizeof(file_buf));
fgets(buf, sizeof(buf), File); //可以直接用sizeof而不使用size
return ESP_OK;
}
return ESP_FAIL;
}
```
##### 写入img
* 文件写入用**fprintf**实现,将准备好的字符串数据写入文件中
``` C
#define FILE_WRITE_BLOCK_SIZE 1024 //写文件允许操作的块长度
esp_err_t SDCard_File_Write_Img(char *imgVal, uint16_t len)
{
if(File == NULL) //检查是否已打开文件File
{
return ESP_FAIL;
}
char tmp[FILE_WRITE_BLOCK_SIZE]; //len最大应该在1000
tmp[len] = '\0'; //设置结束符
memcpy(tmp, imgVal, len); //将img数据组织成字符串
fprintf(File, "%s\n", tmp); //按十六进制写入
return ESP_OK;
}
```
##### 复制img
* 复制img文件时,使用**fgets**从源文件读取一行,再用**fputs**写入目标文件中,直到触发eof
``` C
int8_t SDCard_File_Copy_Img()
{
FILE *dest_file;
char full_file_path[FILE_PATH_LEN_MAX];
uint32_t random = esp_random();
char *buffer;
uint16_t cnt;
//设置被复制文件的路径, 文件名随机生成
sprintf(full_file_path, "%s%s%5ld.txt", MOUNT_POINT, DEFAULT_IMG_DIR, random % 0x0000FFFF);
//创建被复制的文件
dest_file = fopen(full_file_path, FILE_MODE_WRITE);
if(dest_file == NULL) //检查文件创建是否成功
{
ESP_LOGI(TAG_SD, "SDCard_File_Copy_Img: open file failed");
return -1;
}
else
{
//设置缓存
buffer = (char *)malloc(sizeof(char) * FILE_WRITE_BLOCK_SIZE);
cnt = 0;
while(fgets(buffer, FILE_WRITE_BLOCK_SIZE, File))
{
fputs(buffer, dest_file);
cnt++;
}
free(buffer); //释放缓存
fclose(dest_file);
ESP_LOGI(TAG_SD, "SDCard_File_Copy_Img: %d lines copied", cnt);
return 0;
}
}
```
##### 删除文件
* 删除文件使用**remove**,只需要指定文件路径
``` C
int8_t SDCard_File_Delete_Img()
{
struct stat sta;
char file_name[FILE_PATH_LEN_MAX];
//获取文件名
SDCard_File_GetOnePath(file_name);
ESP_LOGI(TAG_SD, "try to delete %s", file_name);
//判断文件存在
if(stat(file_name, &sta) == 0)
{
if(remove(file_name) == 0) //删除Photo
{
return 0;
}
else
{
ESP_LOGI(TAG_SD, "delete Photo failed..");
return -1;
}
}
else
{
ESP_LOGI(TAG_SD, "file not exist");
return -1;
}
}
```
##### 遍历操作
* 遍历文件时,先使用**opendir**打开路径,再使用**readdir**逐一读取文件,最后使用**closedir**关闭路径
``` C
int8_t SDCard_File_ListAllNames(char *dir)
{
char full_dir_path[50];
//拼接路径
sprintf(full_dir_path, MOUNT_POINT"%s", dir);
ESP_LOGI(TAG_SD, "list dir: %s", dir);
//打开目录
DIR *root_dir = opendir(full_dir_path);
if(root_dir == NULL)
return -1;
//遍历目录内文件
DirFileNum = 0;
memset(DirFileNames, '\0', sizeof(DirFileNames));
while(1)
{
struct dirent* de = readdir(root_dir); //用于遍历
if(!de)
break;
//保存文件名, 文件数量计数+1
sprintf(DirFileNames[DirFileNum++], "%s", de->d_name);
if(DirFileNum >= FILE_NUM_MAX)
break;
}
//关闭目录
closedir(root_dir);
//......
return 0;
}
```
### 2.5 BTN模块
* 响应拨轮开关操作,执行对应绑定的函数
* 使用esp-iot-component
* 工程中添加button组件,在CMake步骤中会自动下载
``` shell
$ idf.py add-dependency "espressif/button^3.1.3"
```
* 支持的按键事件 - Single Click/Double Click/Long Press

* 初始化iot_button
``` C
typedef enum
{
BTN_IDX_UP = 0,
BTN_IDX_M,
BTN_IDX_DWN,
BTN_IDX_ALL //按键数量
}Btn_Idx_t; //自定义类型
static button_handle_t g_btns[BTN_IDX_ALL] = {0}; //其中每个元素对应一个btn
void Btn_init()
{
uint8_t i;
//每个btn的公共设置
button_config_t cfg = {
.type = BUTTON_TYPE_GPIO, //btn类型, 这里是GPIO
.long_press_time = CONFIG_BUTTON_LONG_PRESS_TIME_MS, //CONFIG_BUTTON_LONG_PRESS_TIME_MS=1500
.short_press_time = CONFIG_BUTTON_SHORT_PRESS_TIME_MS, //CONFIG_BUTTON_SHORT_PRESS_TIME_MS=180
.gpio_button_config = {
// .gpio_num = BTN_UP,
.active_level = 1,
},
};
//设置&创建每个gpio按键
for(i = 0; i < BTN_IDX_ALL; i++)
{
cfg.gpio_button_config.gpio_num = BtnInfo[i].gpioNum;
g_btns[BtnInfo[i].btnIdx] = iot_button_create(&cfg); //使用cfg创建btn
}
//注册按键回调
// 单击事件
iot_button_register_cb(g_btns[BTN_IDX_UP], BUTTON_SINGLE_CLICK, __btnCb_PressDown, NULL);
iot_button_register_cb(g_btns[BTN_IDX_M], BUTTON_SINGLE_CLICK, __btnCb_PressDown, NULL);
iot_button_register_cb(g_btns[BTN_IDX_DWN], BUTTON_SINGLE_CLICK, __btnCb_PressDown, NULL);
// 长按事件
iot_button_register_cb(g_btns[BTN_IDX_M], BUTTON_LONG_PRESS_START, __btnCb_LongPressStart, NULL);
// 双击事件
iot_button_register_cb(g_btns[BTN_IDX_M], BUTTON_DOUBLE_CLICK, __btnCb_DblPressDown, NULL);
}
```
* btn处理函数
``` C
//获取当前btn的序号
static int __getBtnIndex(button_handle_t btn)
{
//通过遍历, 判断是哪个btn
for (size_t i = 0; i < BTN_IDX_ALL; i++) {
if (btn == g_btns[i]) {
return i; //返回该btn对应的序号
}
}
return -1;
}
//单击事件的处理函数
static void __btnCb_PressDown(void *arg, void *data)
{
TEST_ASSERT_EQUAL_HEX(BUTTON_PRESS_DOWN, iot_button_get_event(arg));
//获取btn序号
Btn_Idx_t btn_idx = __getBtnIndex((button_handle_t)arg);
if(MenuState == MENU_EMPTY) //空闲态
{
}
else if(MenuState == MENU_SELECT) //选择态
{
//根据当前触发的btn执行所需的操作
switch (btn_idx)
{
case BTN_IDX_UP:
ESP_LOGI(TAG_BTN, "BTN_IDX_UP");
if(Update_GuiImg_to_Select(-1) == 1)
Update_GuiImg_from_List();
break;
case BTN_IDX_DWN:
ESP_LOGI(TAG_BTN, "BTN_IDX_DWN");
if(Update_GuiImg_to_Select(1) == 1)
Update_GuiImg_from_List();
break;
default:
break;
}
}
else
return;
}
```
## 3 项目细节
### 3.1 工程配置
#### Flash Size
* ESP32C3-Core板载了4MB SPI flash,需要对应修改

#### stack大小
* 程序触发了stack overflow

* 调整main stack的大小 - **最终Main task stack size设置为5632**

#### WebServer的URI
* 增加Max HTTP URI Length,以便用POST方式回传img数据

#### file name长度
* 调整Max long filename length,程序中实际允许长度为50

## TODO:4 最终效果
## 5 遗留问题
* 响应速度待提高