在物联网设备开发中,ESP32因其出色的网络能力和丰富的外设接口成为热门选择。今天我要分享的是如何在ESP32上搭建一个支持文件上传下载的Web服务器,这个方案特别适合需要远程管理设备文件的场景,比如固件更新、配置文件修改等。
这个项目核心使用了ESP-IDF的HTTP服务器组件和LittleFS文件系统。通过浏览器界面,用户可以:
我们首先设计一个简洁的文件管理界面,使用纯HTML和JavaScript实现,不依赖任何前端框架:
html复制<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>LittleFS Manager</title>
</head>
<body>
<h2>LittleFS File Manager</h2>
<input type='file' id='file'>
<button onclick='ul()'>Upload</button>
<ul id='list'></ul>
<script>
function refresh(){
fetch('/api/list')
.then(r=>r.json())
.then(j=>{
const h=document.getElementById('list');
h.innerHTML='';
j.forEach(f=>{
h.innerHTML+=`<li>${f.name} (${f.size} bytes) `+
`<a href='/api/${f.name}' download>download</a> | `+
`<a href='#' onclick="del('${f.name}')">delete</a></li>`;
});
});
}
async function ul(){
const file = document.getElementById('file').files[0];
await fetch('/api/upload?fname=' + encodeURIComponent(file.name), {
method : 'POST',
body : file
});
refresh();
}
function del(n){
fetch('/api/'+n,{method:'DELETE'})
.then(()=>refresh());
}
refresh();
</script>
</body>
</html>
最初实现文件上传时,我采用了FormData方式:
javascript复制function ul(){
const f=document.getElementById('file').files[0];
fetch('/api/upload',{
method:'POST',
body:new FormData().append('file',f)
}).then(()=>refresh());
}
这个实现会导致上传失败,问题出在:
经过多次调试,最终解决方案是改用纯二进制流上传:
javascript复制async function ul(){
const file = document.getElementById('file').files[0];
await fetch('/api/upload?fname=' + encodeURIComponent(file.name), {
method : 'POST',
body : file
});
refresh();
}
关键点:二进制流上传相比multipart格式更简单可靠,减少了边界条件处理,特别适合资源受限的嵌入式设备。
最初尝试手动解析multipart格式,代码复杂且容易出错:
c复制static esp_err_t upload_post_handler(httpd_req_t *req)
{
char buf[UPLOAD_BUF_SIZE];
char boundary[BOUNDARY_MAX] = {0};
char filename[FILENAME_MAX] = {0};
FILE *fp = NULL;
// 复杂的multipart解析逻辑...
}
经过实践发现手动解析存在诸多问题:
最终采用二进制流方案,代码简洁可靠:
c复制static esp_err_t upload_post_handler(httpd_req_t *req)
{
char filename[128] = {0};
if (httpd_req_get_url_query_str(req, filename, sizeof(filename)) == ESP_OK) {
char *dec = strchr(filename, '=');
if (dec) dec = url_decode(dec + 1);
else dec = "noname.bin";
char path[256];
snprintf(path, sizeof(path), "/littlefs/%s", dec);
FILE *fp = fopen(path, "wb");
if (!fp) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "create failed");
return ESP_FAIL;
}
char buf[1024];
int recv;
while ((recv = httpd_req_recv(req, buf, sizeof(buf))) > 0) {
fwrite(buf, 1, recv, fp);
}
fclose(fp);
httpd_resp_sendstr(req, "upload OK");
return ESP_OK;
}
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "need fname");
return ESP_FAIL;
}
文件下载需要特别注意中文文件名处理,实现URL解码是关键:
c复制static const char *url_decode(const char *src)
{
static char dec[256];
size_t len = strlen(src);
if (len >= sizeof(dec)) len = sizeof(dec) - 1;
size_t di = 0;
for (size_t si = 0; si < len; si++) {
unsigned char c = src[si];
if (c == '%' && si + 2 < len) {
unsigned char hi = src[si + 1];
unsigned char lo = src[si + 2];
if (isxdigit(hi) && isxdigit(lo)) {
unsigned char hex = 0;
sscanf((const char[]){ hi, lo, '\0' }, "%2hhx", &hex);
dec[di++] = hex;
si += 2;
continue;
}
}
if (c == '+') {
dec[di++] = ' ';
} else {
dec[di++] = c;
}
}
dec[di] = '\0';
return dec;
}
static esp_err_t api_download_get(httpd_req_t *req)
{
const char *encoded = req->uri + 5; // 去掉 "/api/"
const char *decoded = url_decode(encoded);
char path[256];
snprintf(path, sizeof(path), "/littlefs/%s", decoded);
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
httpd_resp_send_404(req);
return ESP_OK;
}
httpd_resp_set_hdr(req, "Content-Disposition", "attachment");
httpd_resp_set_type(req, "application/octet-stream");
FILE *f = fopen(path, "rb");
if (!f) { httpd_resp_send_404(req); return ESP_OK; }
char blk[1024];
size_t n;
while ((n = fread(blk, 1, sizeof(blk), f)) > 0)
httpd_resp_send_chunk(req, blk, n);
httpd_resp_send_chunk(req, NULL, 0);
fclose(f);
return ESP_OK;
}
在文件上传下载过程中,遇到了中文文件名处理的问题:
解决方案是实现URL解码函数,处理百分号编码的字符串。
这个基础框架可以进一步扩展:
在实际项目中,根据需求选择合适的扩展功能,避免过度设计。这个方案已经在多个物联网设备管理系统中稳定运行,特别适合需要远程管理设备文件的场景。