You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ESP32蓝牙SPP发送CSV文件:缓冲区溢出与回调使用问题

ESP32蓝牙SPP传输CSV文件的缓冲区溢出问题解决

问题背景

开发ESP32应用时,需通过蓝牙串行端口协议(SPP)传输CSV文件,传输由收到"SEND_CSV"命令触发。当前通过循环调用esp_spp_write()发送文件时遇到缓冲区溢出问题,希望利用ESP_SPP_WRITE_EVT回调确保数据包有序发送,避免溢出并保证文件完整传输。

现有代码

SPP数据接收回调处理

case ESP_SPP_DATA_IND_EVT:
            ESP_LOGI(SPP_TAG, "Data received");
            if (param->data_ind.len == 8 && strncmp((char*)param->data_ind.data, "SEND_CSV", 8) == 0) {
                send_csv_via_bluetooth(param->data_ind.handle);
            }
            break;

CSV发送函数

#define MAX_CHUNK_SIZE 127
static void send_csv_via_bluetooth(uint32_t handle) {
    FILE *file = fopen(FILE_PATH, "r");
    if (file == NULL) {
        ESP_LOGE(SPP_TAG, "Failed to open the CSV file");
        return;
    }
    ESP_LOGI(SPP_TAG, "Checking size...");
    // Get the file size
    fseek(file, 0, SEEK_END);
    size_t file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    char size_string[20]; 
    snprintf(size_string, sizeof(size_string), "%zu\n\r", file_size); 

    // Send the file size using Bluetooth
    esp_spp_write(handle, strlen(size_string), (uint8_t *)size_string);

    char buffer[MAX_CHUNK_SIZE];
    size_t bytes_read;

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
           esp_err_t err = esp_spp_write(handle, bytes_read, (uint8_t *)buffer);
           if (err != ESP_OK) {
               ESP_LOGE(SPP_TAG, "Send data error %s", esp_err_to_name(err));
               break;
           }
           vTaskDelay(10 / portTICK_PERIOD_MS);

    ESP_LOGI(SPP_TAG, "CSV file sent successfully.");
    fclose(file);
}

核心问题分析

直接循环调用esp_spp_write()会忽略SPP发送缓冲区的容量限制,当缓冲区满时继续写入就会导致溢出错误。ESP_SPP_WRITE_EVT是ESP32 SPP栈在当前数据包发送完成后触发的回调,必须利用这个事件来驱动下一包数据的发送,实现"发送-等待确认-发送下一包"的有序传输。

解决方案实现

1. 定义传输状态结构体

用于保存传输过程中的关键状态,同时通过互斥锁保证线程安全:

#define MAX_CHUNK_SIZE 127

typedef struct {
    FILE *csv_file;          // 打开的CSV文件句柄
    uint32_t spp_handle;     // SPP连接句柄
    bool is_transmitting;    // 是否正在传输中
    char buffer[MAX_CHUNK_SIZE]; // 数据缓冲区
} SppCsvTransferState_t;

static SppCsvTransferState_t s_transfer_state = {0};
static SemaphoreHandle_t s_transfer_mutex = NULL;

2. 初始化互斥锁

在应用初始化阶段创建互斥锁,保护传输状态的访问:

void app_main() {
    // ... 其他初始化代码 ...
    s_transfer_mutex = xSemaphoreCreateMutex();
}

3. 修改CSV发送触发函数

不再循环发送,而是初始化传输状态并发送第一包数据:

static void send_csv_via_bluetooth(uint32_t handle) {
    // 检查是否正在传输,避免重复触发
    if (xSemaphoreTake(s_transfer_mutex, portMAX_DELAY) != pdTRUE) {
        return;
    }
    if (s_transfer_state.is_transmitting) {
        xSemaphoreGive(s_transfer_mutex);
        ESP_LOGW(SPP_TAG, "CSV transfer already in progress");
        return;
    }

    // 打开CSV文件
    FILE *file = fopen(FILE_PATH, "r");
    if (file == NULL) {
        ESP_LOGE(SPP_TAG, "Failed to open the CSV file");
        xSemaphoreGive(s_transfer_mutex);
        return;
    }

    // 发送文件大小
    fseek(file, 0, SEEK_END);
    size_t file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    char size_string[20]; 
    snprintf(size_string, sizeof(size_string), "%zu\n\r", file_size); 
    esp_spp_write(handle, strlen(size_string), (uint8_t *)size_string);

    // 初始化传输状态
    s_transfer_state.csv_file = file;
    s_transfer_state.spp_handle = handle;
    s_transfer_state.is_transmitting = true;

    // 读取第一包数据并发送
    size_t bytes_read = fread(s_transfer_state.buffer, 1, MAX_CHUNK_SIZE, file);
    if (bytes_read > 0) {
        esp_spp_write(handle, bytes_read, (uint8_t *)s_transfer_state.buffer);
    } else {
        // 文件为空,直接结束
        fclose(file);
        s_transfer_state.is_transmitting = false;
        ESP_LOGI(SPP_TAG, "CSV file is empty");
    }

    xSemaphoreGive(s_transfer_mutex);
}

4. 在SPP回调中处理发送完成事件

当ESP_SPP_WRITE_EVT触发时,继续发送剩余数据:

// 在SPP回调函数中添加以下case
case ESP_SPP_WRITE_EVT:
    if (xSemaphoreTake(s_transfer_mutex, portMAX_DELAY) != pdTRUE) {
        break;
    }

    if (!s_transfer_state.is_transmitting || s_transfer_state.csv_file == NULL) {
        xSemaphoreGive(s_transfer_mutex);
        break;
    }

    // 读取下一包数据
    size_t bytes_read = fread(s_transfer_state.buffer, 1, MAX_CHUNK_SIZE, s_transfer_state.csv_file);
    if (bytes_read > 0) {
        // 发送下一包
        esp_spp_write(s_transfer_state.spp_handle, bytes_read, (uint8_t *)s_transfer_state.buffer);
    } else {
        // 传输完成,清理资源
        fclose(s_transfer_state.csv_file);
        s_transfer_state.csv_file = NULL;
        s_transfer_state.is_transmitting = false;
        ESP_LOGI(SPP_TAG, "CSV file sent successfully");
    }

    xSemaphoreGive(s_transfer_mutex);
    break;

关键注意事项

  • 缓冲区大小调整:MAX_CHUNK_SIZE建议设置为SPP栈的MTU值(默认一般为127或255),可通过esp_spp_get_mtu()获取当前连接的MTU动态调整。
  • 错误处理:如果esp_spp_write()返回错误,需在回调中终止传输并及时清理文件句柄。
  • 线程安全:所有访问传输状态的操作必须通过互斥锁保护,避免多线程同时修改状态导致崩溃。
  • 重复触发防护:触发传输前检查是否正在传输,避免同一文件被重复发送。

内容的提问来源于stack exchange,提问作者Sérgio Venturi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 19:47:06